From 4804c115929e5b617feb91bd469ec602944999a9 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 25 Nov 2019 15:00:21 +1100 Subject: [PATCH 01/16] feat(developer): Model editor, first brush strokes --- windows/src/developer/TIKE/Tike.dpr | 8 +- windows/src/developer/TIKE/Tike.dproj | 14 +- ...yman.Developer.UI.UframeWordlistEditor.dfm | 85 ++ ...yman.Developer.UI.UframeWordlistEditor.pas | 340 ++++++++ .../Keyman.Developer.UI.UfrmModelEditor.dfm | 393 +++++++++ .../Keyman.Developer.UI.UfrmModelEditor.pas | 435 ++++++++++ ...Keyman.Developer.UI.UfrmWordlistEditor.dfm | 66 -- ...Keyman.Developer.UI.UfrmWordlistEditor.pas | 251 +----- .../TIKE/child/UfrmPackageEditor.dfm | 5 +- ...an.Developer.System.LexicalModelParser.pas | 227 ++++++ ...veloper.System.LexicalModelParserTypes.pas | 52 ++ .../developer/TIKE/main/UframeTextEditor.dfm | 4 +- .../developer/TIKE/main/UframeTextEditor.pas | 2 +- windows/src/developer/TIKE/main/UfrmMain.pas | 13 + ...man.System.Test.LexicalModelParserTest.pas | 130 +++ .../model-ts-parser/ModelTsParserTest.dpr | 61 ++ .../model-ts-parser/ModelTsParserTest.dproj | 765 ++++++++++++++++++ .../model-ts-parser/ModelTsParserTest.res | Bin 0 -> 57140 bytes .../model-ts-parser/assets/custom.model.ts | 8 + .../model-ts-parser/assets/nocomment.model.ts | 7 + .../assets/nrc.en.mtnt.model.ts | 10 + 21 files changed, 2565 insertions(+), 311 deletions(-) create mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm create mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas create mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm create mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas create mode 100644 windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas create mode 100644 windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas create mode 100644 windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas create mode 100644 windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr create mode 100644 windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj create mode 100644 windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.res create mode 100644 windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts create mode 100644 windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts create mode 100644 windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts diff --git a/windows/src/developer/TIKE/Tike.dpr b/windows/src/developer/TIKE/Tike.dpr index 0a1babc728..6a187684dc 100644 --- a/windows/src/developer/TIKE/Tike.dpr +++ b/windows/src/developer/TIKE/Tike.dpr @@ -283,10 +283,14 @@ uses Keyman.Developer.System.ProjectTemplate in '..\kmconvert\Keyman.Developer.System.ProjectTemplate.pas', Keyman.Developer.UI.Project.UfrmNewModelProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas' {frmNewModelProjectParameters}, Keyman.Developer.System.Project.wordlistTsvProjectFile in 'project\Keyman.Developer.System.Project.wordlistTsvProjectFile.pas', - Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}, + Keyman.Developer.UI.UframeWordlistEditor in 'child\Keyman.Developer.UI.UframeWordlistEditor.pas' {frameWordlistEditor}, Keyman.System.WordlistTsvFile in '..\..\global\delphi\lexicalmodels\Keyman.System.WordlistTsvFile.pas', Keyman.Developer.UI.Project.wordlistTsvProjectFileUI in 'project\Keyman.Developer.UI.Project.wordlistTsvProjectFileUI.pas', - Keyman.Developer.System.Project.WelcomeRenderer in 'project\Keyman.Developer.System.Project.WelcomeRenderer.pas'; + Keyman.Developer.System.Project.WelcomeRenderer in 'project\Keyman.Developer.System.Project.WelcomeRenderer.pas', + Keyman.Developer.UI.UfrmModelEditor in 'child\Keyman.Developer.UI.UfrmModelEditor.pas' {frmModelEditor}, + Keyman.Developer.System.LexicalModelParser in 'main\Keyman.Developer.System.LexicalModelParser.pas', + Keyman.Developer.System.LexicalModelParserTypes in 'main\Keyman.Developer.System.LexicalModelParserTypes.pas', + Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}; {$R *.RES} {$R ICONS.RES} diff --git a/windows/src/developer/TIKE/Tike.dproj b/windows/src/developer/TIKE/Tike.dproj index c355614097..899304bd7d 100644 --- a/windows/src/developer/TIKE/Tike.dproj +++ b/windows/src/developer/TIKE/Tike.dproj @@ -529,13 +529,23 @@ dfm - -
frmWordlistEditor
+ +
frameWordlistEditor
dfm
+ +
frmModelEditor
+ dfm +
+ + + +
frmWordlistEditor
+ dfm +
Cfg_2 diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm new file mode 100644 index 0000000000..c169f740de --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm @@ -0,0 +1,85 @@ +inherited frameWordlistEditor: TframeWordlistEditor + BorderIcons = [] + BorderStyle = bsNone + Caption = 'frameWordlistEditor' + ClientHeight = 338 + ClientWidth = 651 + OnResize = FormResize + ExplicitWidth = 651 + ExplicitHeight = 338 + PixelsPerInch = 96 + TextHeight = 13 + object pages: TPageControl + Left = 0 + Top = 0 + Width = 651 + Height = 338 + ActivePage = pageDesign + Align = alClient + TabOrder = 0 + TabPosition = tpBottom + OnChanging = pagesChanging + ExplicitWidth = 635 + ExplicitHeight = 299 + object pageDesign: TTabSheet + Caption = 'Design' + ImageIndex = -1 + ExplicitWidth = 627 + ExplicitHeight = 273 + object gridWordlist: TStringGridEditControlled + Left = 0 + Top = 0 + Width = 643 + Height = 271 + Align = alClient + ColCount = 3 + DefaultRowHeight = 16 + DefaultDrawing = False + FixedCols = 0 + Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goEditing, goTabs, goAlwaysShowEditor] + TabOrder = 0 + OnClick = gridWordlistClick + OnDrawCell = gridWordlistDrawCell + OnSetEditText = gridWordlistSetEditText + ExplicitWidth = 627 + ExplicitHeight = 232 + end + object panGridControls: TPanel + Left = 0 + Top = 271 + Width = 643 + Height = 41 + Align = alBottom + TabOrder = 1 + ExplicitTop = 232 + ExplicitWidth = 627 + object cmdDeleteRow: TButton + Left = 0 + Top = 6 + Width = 75 + Height = 25 + Caption = '&Delete row' + TabOrder = 0 + OnClick = cmdDeleteRowClick + end + object cmdSortByFrequency: TButton + Left = 81 + Top = 6 + Width = 104 + Height = 25 + Caption = '&Sort by frequency' + TabOrder = 1 + OnClick = cmdSortByFrequencyClick + end + end + end + object pageCode: TTabSheet + Caption = 'Code' + ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 + end + end +end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas new file mode 100644 index 0000000000..e30fa8a5c2 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas @@ -0,0 +1,340 @@ +unit Keyman.Developer.UI.UframeWordlistEditor; + +interface + +uses + System.Classes, + System.SysUtils, + System.Variants, + Winapi.Messages, + Winapi.Windows, + Vcl.ComCtrls, + Vcl.Controls, + Vcl.Dialogs, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + + StringGridEditControlled, + + Keyman.System.WordlistTsvFile, + UframeTextEditor, + UfrmMDIEditor, Vcl.StdCtrls, Vcl.ExtCtrls, UfrmTike; + +type + TframeWordlistEditor = class(TTIKEForm) + pages: TPageControl; + pageDesign: TTabSheet; + pageCode: TTabSheet; + gridWordlist: TStringGridEditControlled; + panGridControls: TPanel; + cmdDeleteRow: TButton; + cmdSortByFrequency: TButton; + procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure FormResize(Sender: TObject); + procedure gridWordlistSetEditText(Sender: TObject; ACol, ARow: Integer; + const Value: string); + procedure gridWordlistDrawCell(Sender: TObject; ACol, ARow: Integer; + ARect: TRect; State: TGridDrawState); + procedure cmdDeleteRowClick(Sender: TObject); + procedure gridWordlistClick(Sender: TObject); + procedure cmdSortByFrequencyClick(Sender: TObject); + private + FWordlist: TWordlistTsvFile; + frameSource: TframeTextEditor; + FSetup: Integer; + FModified: Boolean; + FOnModifiedChanged: TNotifyEvent; + FFilename: string; + procedure UpdateData; + function MoveCodeToWordlist: Boolean; + procedure FillCode; + procedure FillGrid; + function MoveDesignToWordlist: Boolean; + procedure SourceChanged(Sender: TObject); + procedure FillGridNewRow; + procedure EnableControls; + procedure SetModified(const Value: Boolean); + protected + function GetHelpTopic: string; override; + function DoOpenFile: Boolean; + function DoSaveFile: Boolean; + public + procedure FindError(const Filename: string; s: string; line: Integer); // I4081 + function LoadFromFile(const Filename: string): Boolean; + function SaveToFile(const Filename: string): Boolean; + property Filename: string read FFilename; + property Modified: Boolean read FModified write SetModified; + property OnModifiedChanged: TNotifyEvent read FOnModifiedChanged write FOnModifiedChanged; + end; + +implementation + +{$R *.dfm} + +uses + Keyman.Developer.System.HelpTopics, + TextFileFormat, + UfrmMain, + UfrmMessages; + +const + ColWidth_Word = 120; + ColWidth_Frequency = 64; + S_AddRowText = 'Add word...'; + +{ TfrmWordlistEditor } + +procedure TframeWordlistEditor.FormCreate(Sender: TObject); +begin + inherited; + Inc(FSetup); + try + FWordlist := TWordlistTsvFile.Create; + frameSource := TframeTextEditor.Create(Self); + frameSource.Parent := pageCode; + frameSource.Align := alClient; + frameSource.EditorFormat := efWordlistTsv; + frameSource.Visible := True; + frameSource.OnChanged := SourceChanged; + frameSource.TextFileFormat := tffUTF8; +//TODO: frameSource.UseTabs := True; + pages.ActivePage := pageDesign; + + gridWordlist.Cells[0, 0] := 'Word Form'; + gridWordlist.Cells[1, 0] := 'Count'; + gridWordlist.Cells[2, 0] := 'Comment'; + gridWordlist.ColWidths[0] := ColWidth_Word; + gridWordlist.ColWidths[1] := ColWidth_Frequency; + gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; + finally + Dec(FSetup); + end; +end; + +procedure TframeWordlistEditor.FormDestroy(Sender: TObject); +begin + inherited; + FWordlist.Free; +end; + +procedure TframeWordlistEditor.FormResize(Sender: TObject); +begin + inherited; + gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; +end; + +procedure TframeWordlistEditor.cmdDeleteRowClick(Sender: TObject); +begin + FWordlist.RemoveWord(gridWordlist.Row-1); + FillGrid; + EnableControls; + Modified := True; +end; + +procedure TframeWordlistEditor.cmdSortByFrequencyClick(Sender: TObject); +begin + FWordlist.SortByFrequency; + FillGrid; + Modified := True; +end; + +function TframeWordlistEditor.DoOpenFile: Boolean; +begin + if FileExists(FileName) then + begin + FWordlist.LoadFromFile(FileName); + UpdateData; + end; + Result := True; +end; + +function TframeWordlistEditor.DoSaveFile: Boolean; +begin + MoveCodeToWordlist; // Does nothing if not in code page + FWordlist.SaveToFile(FileName); + Result := True; +end; + +procedure TframeWordlistEditor.EnableControls; +var + e: Boolean; +begin + e := gridWordlist.Row < gridWordlist.RowCount - 1; + cmdDeleteRow.Enabled := e; + cmdSortByFrequency.Enabled := e; +end; + +procedure TframeWordlistEditor.FindError(const Filename: string; s: string; line: Integer); +begin + // +end; + +function TframeWordlistEditor.GetHelpTopic: string; +begin + Result := SHelpTopic_Context_WordlistEditor; +end; + +procedure TframeWordlistEditor.gridWordlistClick(Sender: TObject); +begin + EnableControls; +end; + +procedure TframeWordlistEditor.gridWordlistDrawCell(Sender: TObject; ACol, + ARow: Integer; ARect: TRect; State: TGridDrawState); +var + LText: string; +begin + inherited; + if ARow = gridWordlist.RowCount - 1 then + begin + // Drawing last row + gridWordlist.Canvas.Font.Style := [fsItalic]; + end + else if gridWordlist.Cells[0, ARow] = '#' then + begin + // Drawing a comment cell + gridWordlist.Canvas.Font.Style := [fsBold]; + end + else + gridWordlist.Canvas.Font.Style := []; + + LText := gridWordlist.Cells[ACol, ARow]; + gridWordlist.Canvas.TextRect(ARect, ARect.Left+2, + ARect.Top+((ARect.Height - gridWordlist.Canvas.TextHeight(LText)) div 2), LText); +end; + +procedure TframeWordlistEditor.gridWordlistSetEditText(Sender: TObject; ACol, + ARow: Integer; const Value: string); +var + w: TWordlistWord; +begin + if FSetup > 0 then Exit; + Inc(FSetup); + try + Modified := True; + + if ARow = gridWordlist.RowCount - 1 then + begin + if (Value = '') or (Value = S_AddRowText) then + Exit; + + w.Word := Value; + w.Frequency := 0; + w.Comment := ''; + FWordlist.AddWord(w); + gridWordlist.RowCount := gridWordlist.RowCount + 1; + FillGridNewRow; + end + else + begin + w := FWordlist.Word[ARow-1]; + case ACol of + 0: w.Word := Value; + 1: w.Frequency := StrToIntDef(Value, 0); + 2: w.Comment := Value; + end; + FWordlist.Word[ARow-1] := w; + end; + finally + Dec(FSetup); + end; +end; + +procedure TframeWordlistEditor.pagesChanging(Sender: TObject; + var AllowChange: Boolean); +begin + if FSetup > 0 then Exit; + + if pages.ActivePage = pageCode then + AllowChange := MoveCodeToWordlist + else + AllowChange := MoveDesignToWordlist +end; + +function TframeWordlistEditor.MoveCodeToWordlist: Boolean; +begin + if pages.ActivePage <> pageCode then + Exit(False); + + FWordlist.Text := frameSource.EditorText; + FillGrid; + Result := True; +end; + +function TframeWordlistEditor.MoveDesignToWordlist: Boolean; +begin + FillCode; + Result := True; +end; + +function TframeWordlistEditor.LoadFromFile(const Filename: string): Boolean; +begin + FFilename := Filename; + Result := DoOpenFile; +end; + +procedure TframeWordlistEditor.UpdateData; +begin + if pages.ActivePage = pageCode + then FillCode + else FillGrid; +end; + +function TframeWordlistEditor.SaveToFile(const Filename: string): Boolean; +begin + FFilename := Filename; + Result := DoSaveFile; +end; + +procedure TframeWordlistEditor.SetModified(const Value: Boolean); +begin + if FModified <> Value then + begin + FModified := Value; + if Assigned(FOnModifiedChanged) then + FOnModifiedChanged(Self); + end; +end; + +procedure TframeWordlistEditor.SourceChanged(Sender: TObject); +begin + if FSetup = 0 then + Modified := True; +end; + +procedure TframeWordlistEditor.FillCode; +begin + frameSource.EditorText := FWordlist.Text; +end; + +procedure TframeWordlistEditor.FillGrid; +var + i: Integer; + w: TWordlistWord; +begin + gridWordlist.RowCount := FWordlist.Count + 2; + for i := 0 to FWordlist.Count - 1 do + begin + w := FWordlist.Word[i]; + gridWordlist.Cells[0, i+1] := w.Word; + if w.Frequency = 0 + then gridWordlist.Cells[1, i+1] := '' + else gridWordlist.Cells[1, i+1] := IntToStr(w.Frequency); + gridWordlist.Cells[2, i+1] := w.Comment; + end; + FillGridNewRow; + gridWordlist.FixedRows := 1; +end; + +procedure TframeWordlistEditor.FillGridNewRow; +begin + gridWordlist.Cells[0, gridWordlist.RowCount - 1] := S_AddRowText; + gridWordlist.Cells[1, gridWordlist.RowCount - 1] := ''; + gridWordlist.Cells[2, gridWordlist.RowCount - 1] := ''; + EnableControls; +end; + +end. diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm new file mode 100644 index 0000000000..631026f7ce --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm @@ -0,0 +1,393 @@ +inherited frmModelEditor: TfrmModelEditor + Left = 0 + Top = 0 + Caption = 'frmModelEditor' + ClientHeight = 708 + ClientWidth = 712 + Font.Name = 'Tahoma' + ExplicitWidth = 712 + ExplicitHeight = 708 + PixelsPerInch = 96 + TextHeight = 13 + object pages: TLeftTabbedPageControl + Left = 0 + Top = 0 + Width = 712 + Height = 708 + ActivePage = pageDetails + Align = alClient + Font.Charset = ANSI_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Segoe UI' + Font.Style = [] + Images = modActionsMain.ilEditorPages + MultiLine = True + OwnerDraw = True + ParentFont = False + TabHeight = 90 + TabOrder = 0 + TabPosition = tpLeft + TabWidth = 60 + OnChanging = pagesChanging + object pageDetails: TTabSheet + Caption = 'Details' + ImageIndex = 2 + object sbDetails: TScrollBox + Left = 0 + Top = 0 + Width = 619 + Height = 708 + Align = alClient + BevelInner = bvNone + BevelOuter = bvNone + BorderStyle = bsNone + Color = 14211288 + ParentColor = False + TabOrder = 0 + object panWordlists: TPanel + AlignWithMargins = True + Left = 4 + Top = 181 + Width = 611 + Height = 232 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alTop + BevelOuter = bvNone + TabOrder = 1 + DesignSize = ( + 611 + 232) + object lblWordlists: TLabel + AlignWithMargins = True + Left = 12 + Top = 4 + Width = 64 + Height = 17 + Caption = 'Wordlists' + Font.Charset = ANSI_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object lblReadOnly: TLabel + Left = 15 + Top = 200 + Width = 415 + Height = 13 + Caption = + 'The editor was unable to parse the source file. Details shown he' + + 're are read-only.' + end + object gridWordlists: TStringGrid + Left = 12 + Top = 27 + Width = 485 + Height = 160 + Anchors = [akLeft, akTop, akRight] + ColCount = 1 + FixedCols = 0 + FixedRows = 0 + Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRowSelect] + TabOrder = 0 + OnDblClick = gridWordlistsDblClick + ColWidths = ( + 271) + RowHeights = ( + 24 + 24 + 24 + 24 + 24) + end + object cmdNewWordlist: TButton + Left = 503 + Top = 27 + Width = 101 + Height = 25 + Anchors = [akTop, akRight] + Caption = '&New...' + TabOrder = 1 + OnClick = cmdNewWordlistClick + end + object cmdRemoveWordlist: TButton + Left = 503 + Top = 89 + Width = 100 + Height = 25 + Anchors = [akTop, akRight] + Caption = 'Remove' + TabOrder = 3 + OnClick = cmdRemoveWordlistClick + end + object cmdAddExistingWordlist: TButton + Left = 503 + Top = 58 + Width = 100 + Height = 25 + Anchors = [akTop, akRight] + Caption = '&Add existing...' + TabOrder = 2 + OnClick = cmdAddExistingWordlistClick + end + end + object panBasicInformation: TPanel + AlignWithMargins = True + Left = 4 + Top = 6 + Width = 611 + Height = 167 + Margins.Left = 4 + Margins.Top = 6 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alTop + AutoSize = True + BevelOuter = bvNone + ParentColor = True + TabOrder = 0 + DesignSize = ( + 611 + 167) + object lblFormat: TLabel + Left = 12 + Top = 30 + Width = 36 + Height = 13 + Caption = '&Format' + end + object lblBasicInformation: TLabel + AlignWithMargins = True + Left = 12 + Top = 3 + Width = 123 + Height = 17 + Caption = 'Basic Information' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object lblWordBreaker: TLabel + Left = 12 + Top = 56 + Width = 71 + Height = 13 + Caption = '&Word breaker' + end + object lblComments: TLabel + Left = 12 + Top = 83 + Width = 50 + Height = 13 + Caption = 'Comme&nts' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + ParentFont = False + end + object cbFormat: TComboBox + Left = 98 + Top = 26 + Width = 145 + Height = 21 + Style = csDropDownList + TabOrder = 0 + OnClick = cbFormatClick + Items.Strings = ( + 'Wordlist (trie-1.0)' + 'Custom (custom-1.0)') + end + object cbWordBreaker: TComboBox + Left = 98 + Top = 53 + Width = 145 + Height = 21 + Style = csDropDownList + TabOrder = 1 + OnClick = cbWordBreakerClick + Items.Strings = ( + 'default' + 'ascii' + 'custom') + end + object memoComments: TMemo + Left = 98 + Top = 80 + Width = 506 + Height = 87 + Anchors = [akLeft, akTop, akRight] + BevelOuter = bvNone + TabOrder = 2 + OnChange = memoCommentsChange + end + end + end + end + object pageSource: TTabSheet + Caption = 'Source' + ImageIndex = 9 + end + object pageCompile: TTabSheet + Caption = 'Build' + ImageIndex = 1 + object Panel1: TPanel + Left = 0 + Top = 0 + Width = 619 + Height = 708 + Align = alClient + BevelOuter = bvNone + Color = 14211288 + ParentBackground = False + TabOrder = 0 + object lblCongrats: TLabel + Left = 10 + Top = 13 + Width = 333 + Height = 13 + Caption = + 'The keyboard must be compiled in order to distribute or install ' + + 'it' + end + object cmdCompile: TButton + Left = 10 + Top = 40 + Width = 137 + Height = 25 + Caption = '&Compile Lexical Model' + ImageIndex = 37 + TabOrder = 0 + end + object cmdAddToProject: TButton + Left = 297 + Top = 40 + Width = 137 + Height = 25 + Hint = + 'Add Current Editor File|Adds the current editor file to the proj' + + 'ect' + Caption = '&Current Editor File' + TabOrder = 1 + end + object cmdOpenContainingFolder2: TButton + Left = 153 + Top = 40 + Width = 138 + Height = 25 + Caption = '&Open Containing Folder' + TabOrder = 2 + end + object panBuildLexicalModel: TPanel + Left = 10 + Top = 96 + Width = 424 + Height = 353 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 3 + object lblDebugHostCaption: TLabel + Left = 12 + Top = 158 + Width = 252 + Height = 13 + Caption = 'Debug host is active at following web addresses:' + end + object lblCrossPlatform: TLabel + Left = 12 + Top = 14 + Width = 237 + Height = 17 + Caption = 'Test Lexical Model in Web Browser' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object Label1: TLabel + Left = 12 + Top = 48 + Width = 105 + Height = 13 + Caption = 'Keyboard for testing' + end + object Label2: TLabel + Left = 12 + Top = 72 + Width = 389 + Height = 26 + Caption = + 'Optionally, select a compiled keyboard with which to test this l' + + 'exical model. Any keyboards already loaded in the web debugger w' + + 'ill also be available.' + WordWrap = True + end + object cmdTestLexicalModel: TButton + Left = 12 + Top = 122 + Width = 150 + Height = 25 + Caption = 'Test Lexical &Model' + TabOrder = 2 + end + object cmdOpenDebugHost: TButton + Left = 12 + Top = 286 + Width = 189 + Height = 25 + Caption = 'Open &debugger in local browser' + TabOrder = 3 + end + object lbDebugHosts: TListBox + Left = 12 + Top = 183 + Width = 289 + Height = 97 + ItemHeight = 13 + TabOrder = 4 + end + object cmdSendURLsToEmail: TButton + Left = 12 + Top = 317 + Width = 189 + Height = 25 + Caption = 'Send addresses to &email...' + TabOrder = 5 + end + object editTestKeyboard: TEdit + Left = 123 + Top = 45 + Width = 158 + Height = 21 + TabOrder = 0 + end + object cmdBrowseTestKeyboard: TButton + Left = 287 + Top = 43 + Width = 75 + Height = 25 + Caption = '&Browse...' + TabOrder = 1 + end + end + end + end + end + object dlgAddWordlist: TOpenDialog + Options = [ofHideReadOnly, ofPathMustExist, ofEnableSizing] + Left = 344 + Top = 344 + end +end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas new file mode 100644 index 0000000000..8888f09a30 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas @@ -0,0 +1,435 @@ +unit Keyman.Developer.UI.UfrmModelEditor; + +interface + +uses + System.Classes, + System.Generics.Collections, + System.SysUtils, + System.Variants, + Vcl.ComCtrls, + Vcl.Controls, + Vcl.Dialogs, + Vcl.ExtCtrls, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + Vcl.StdCtrls, + Winapi.Messages, + Winapi.Windows, + + LeftTabbedPageControl, + UfrmMDIEditor, + UframeTextEditor, + Keyman.Developer.UI.UframeWordlistEditor, + Keyman.Developer.System.LexicalModelParser, + Keyman.Developer.System.LexicalModelParserTypes; + +type + TfrmModelEditor = class(TfrmTikeEditor) + pages: TLeftTabbedPageControl; + pageDetails: TTabSheet; + sbDetails: TScrollBox; + panWordlists: TPanel; + lblWordlists: TLabel; + gridWordlists: TStringGrid; + cmdNewWordlist: TButton; + cmdRemoveWordlist: TButton; + panBasicInformation: TPanel; + lblFormat: TLabel; + lblBasicInformation: TLabel; + pageSource: TTabSheet; + pageCompile: TTabSheet; + Panel1: TPanel; + lblCongrats: TLabel; + cmdCompile: TButton; + cmdAddToProject: TButton; + cmdOpenContainingFolder2: TButton; + panBuildLexicalModel: TPanel; + cbFormat: TComboBox; + cbWordBreaker: TComboBox; + lblWordBreaker: TLabel; + memoComments: TMemo; + lblComments: TLabel; + lblDebugHostCaption: TLabel; + lblCrossPlatform: TLabel; + cmdTestLexicalModel: TButton; + cmdOpenDebugHost: TButton; + lbDebugHosts: TListBox; + cmdSendURLsToEmail: TButton; + Label1: TLabel; + editTestKeyboard: TEdit; + cmdBrowseTestKeyboard: TButton; + Label2: TLabel; + lblReadOnly: TLabel; + dlgAddWordlist: TOpenDialog; + cmdAddExistingWordlist: TButton; + procedure FormDestroy(Sender: TObject); + procedure cmdNewWordlistClick(Sender: TObject); + procedure cmdAddExistingWordlistClick(Sender: TObject); + procedure cmdRemoveWordlistClick(Sender: TObject); + procedure gridWordlistsDblClick(Sender: TObject); + procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); + procedure FormCreate(Sender: TObject); + procedure cbFormatClick(Sender: TObject); + procedure cbWordBreakerClick(Sender: TObject); + procedure memoCommentsChange(Sender: TObject); + private + type + TWordlist = class + Frame: TframeWordlistEditor; + Tab: TTabSheet; + Filename: string; + destructor Destroy; override; + end; + TWordlists = class(TObjectList) + public + function IndexOfFilename(const Filename: string): Integer; + end; + private + model: TStrings; + parser: TLexicalModelParser; + wordlists: TWordlists; + frameSource: TframeTextEditor; + FSetup: Integer; + procedure FillDetails; + procedure EnableControls; + procedure UpdateWordlistTabs; + function AddWordlistTab( + const WordlistFilename: string): TfrmModelEditor.TWordlist; + function MoveDesignToSource: Boolean; + function MoveSourceToDesign: Boolean; + { Private declarations } + protected + function GetHelpTopic: string; override; + function DoOpenFile: Boolean; override; + function DoSaveFile: Boolean; override; + function GetFileNameFilter: string; override; + function GetDefaultExt: string; override; + + public + procedure FindError(const Filename: string; s: string; line: Integer); override; // I4081 + end; + +implementation + +uses + TextFileFormat; + +{$R *.dfm} + +function FormatToIndex(format: TLexicalModelFormat): Integer; +begin + Result := Ord(format) - 1; // unknown = -1 +end; + +function WordBreakerToIndex(wordBreaker: TLexicalModelWordBreaker): Integer; +begin + Result := Ord(wordBreaker) - 1; // unknown = -1 +end; + +function FormatFromIndex(format: Integer): TLexicalModelFormat; +begin + Result := TLexicalModelFormat(format+1); // unknown = -1 +end; + +function WordBreakerFromIndex(wordBreaker: Integer): TLexicalModelWordBreaker; +begin + Result := TLexicalModelWordBreaker(wordBreaker+1); // unknown = -1 +end; + +{ TfrmModelEditor } + +function TfrmModelEditor.DoOpenFile: Boolean; +begin + model := TStringList.Create; + model.LoadFromFile(FileName); + parser := TLexicalModelParser.Create(model.Text); + wordlists := TWordlists.Create; + + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + + Result := True; +end; + +function TfrmModelEditor.DoSaveFile: Boolean; +begin + // TODO + Result := True; +end; + +procedure TfrmModelEditor.EnableControls; +var + e: Boolean; +begin + e := parser.IsEditable; + lblFormat.Enabled := e; + cbFormat.Enabled := e; + lblWordBreaker.Enabled := e; + cbWordBreaker.Enabled := e; + lblComments.Enabled := e; + memoComments.Enabled := e; + cmdAddExistingWordlist.Enabled := e; + gridWordlists.Enabled := e and (parser.Wordlists.Count > 0); + cmdRemoveWordlist.Enabled := e and (parser.Wordlists.Count > 0); + lblReadOnly.Visible := not e; +end; + +procedure TfrmModelEditor.FindError(const Filename: string; s: string; + line: Integer); +begin + inherited; + // TODO +end; + +procedure TfrmModelEditor.FormCreate(Sender: TObject); +begin + inherited; + Inc(FSetup); + try + frameSource := TframeTextEditor.Create(Self); + frameSource.Parent := pageSource; + + frameSource.Align := alClient; + frameSource.EditorFormat := efJS; + frameSource.Visible := True; +// frameSource.OnChanged := SourceChanged; + frameSource.TextFileFormat := tffUTF8; + + pages.ActivePage := pageDetails; + finally + Dec(FSetup); + end; +end; + +procedure TfrmModelEditor.FormDestroy(Sender: TObject); +begin + inherited; + FreeAndNil(parser); + FreeAndNil(model); + FreeAndNil(wordlists); +end; + +function TfrmModelEditor.GetDefaultExt: string; +begin + Result := '.model.ts'; // TODO: test if this actually works! +end; + +function TfrmModelEditor.GetFileNameFilter: string; +begin + Result := 'Lexical model files (*.model.ts)|*.model.ts|All files (*.*)|*.*'; +end; + +function TfrmModelEditor.GetHelpTopic: string; +begin + Result := ''; //SHelpTopic_Context_WordlistEditor; // TODO: use a better topic +end; + +procedure TfrmModelEditor.gridWordlistsDblClick(Sender: TObject); +begin + pages.ActivePageIndex := gridWordlists.Row + 1; +end; + +procedure TfrmModelEditor.memoCommentsChange(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.Comment := memoComments.Text; + Modified := True; +end; + +function TfrmModelEditor.MoveSourceToDesign: Boolean; +begin + Inc(FSetup); + try + FreeAndNil(parser); + parser := TLexicalModelParser.Create(frameSource.EditorText); + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Result := True; +end; + +function TfrmModelEditor.MoveDesignToSource: Boolean; +begin + frameSource.EditorText := parser.Text; + Result := True; +end; + +procedure TfrmModelEditor.pagesChanging(Sender: TObject; + var AllowChange: Boolean); +begin + if FSetup > 0 then Exit; + + if pages.ActivePage = pageSource then + AllowChange := MoveSourceToDesign + else + AllowChange := MoveDesignToSource +end; + +{ --------- Details tab --------- } + +procedure TfrmModelEditor.FillDetails; +var + i: Integer; +begin + cbFormat.ItemIndex := FormatToIndex(parser.Format); + cbWordBreaker.ItemIndex := WordBreakerToIndex(parser.WordBreaker); + memoComments.Text := parser.Comment; + + if parser.Wordlists.Count = 0 then + begin + gridWordlists.RowCount := 1; + gridWordlists.Cells[0, 0] := ''; + end + else + begin + gridWordlists.RowCount := parser.Wordlists.Count; + for i := 0 to parser.Wordlists.Count - 1 do + begin + gridWordlists.Cells[0, i] := parser.Wordlists[i]; + end; + end; + + EnableControls; +end; + +procedure TfrmModelEditor.UpdateWordlistTabs; +var + i, n: Integer; + wordlist: TWordlist; +begin + // Remove tabs that are no longer listed + for i := wordlists.Count - 1 downto 0 do + begin + if parser.Wordlists.IndexOf(wordlists[i].Filename) < 0 then + wordlists.Delete(i); + end; + + // Add new tabs + for i := 0 to parser.Wordlists.Count - 1 do + begin + n := wordlists.IndexOfFilename(parser.Wordlists[i]); + if n < 0 then + begin + wordlist := AddWordlistTab(parser.Wordlists[i]); + wordlist.Tab.PageIndex := i + 1; + end + else + wordlists[n].Tab.PageIndex := i + 1; + end; +end; + +function TfrmModelEditor.AddWordlistTab(const WordlistFilename: string): TfrmModelEditor.TWordlist; +begin + Result := TWordlist.Create; + + wordlists.Add(Result); + Result.Filename := WordlistFilename; + + Result.Tab := TTabSheet.Create(Self); + Result.Tab.TabVisible := True; + Result.Tab.ImageIndex := 17; // 'spreadsheet' + Result.Tab.Caption := ExtractFileName(Result.Filename); + Result.Tab.PageControl := pages; + + Result.Frame := TframeWordlistEditor.Create(Self); + Result.Frame.LoadFromFile(ExtractFilePath(Filename) + WordlistFilename); + Result.Frame.Align := alClient; + + Result.Frame.Parent := Result.Tab; + Result.Frame.Visible := True; +end; + +{ TfrmModelEditor.TWordlists } + +function TfrmModelEditor.TWordlists.IndexOfFilename( + const Filename: string): Integer; +begin + for Result := 0 to Count - 1 do + if SameText(Items[Result].Filename, Filename) then + Exit; + Result := -1; +end; + +{ Wordlist management } + +procedure TfrmModelEditor.cbFormatClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.Format := FormatFromIndex(cbFormat.ItemIndex); + Modified := True; +end; + +procedure TfrmModelEditor.cbWordBreakerClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.WordBreaker := WordBreakerFromIndex(cbWordBreaker.ItemIndex); + Modified := True; +end; + +procedure TfrmModelEditor.cmdAddExistingWordlistClick(Sender: TObject); +begin + if dlgAddWordlist.Execute then + begin + parser.Wordlists.Add(ExtractRelativePath(Filename, dlgAddWordlist.FileName)); + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Modified := True; + end; +end; + +procedure TfrmModelEditor.cmdNewWordlistClick(Sender: TObject); +begin + if dlgAddWordlist.Execute then + begin + parser.Wordlists.Add(ExtractRelativePath(Filename, dlgAddWordlist.FileName)); + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Modified := True; + end; +end; + +procedure TfrmModelEditor.cmdRemoveWordlistClick(Sender: TObject); +begin + parser.Wordlists.Delete(gridWordlists.Row); + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Modified := True; +end; + +{ TfrmModelEditor.TWordlist } + +destructor TfrmModelEditor.TWordlist.Destroy; +begin + Frame.Free; + Tab.Free; + inherited Destroy; +end; + +end. diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm index b7bd1388c6..428f6fdfcb 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm @@ -5,74 +5,8 @@ inherited frmWordlistEditor: TfrmWordlistEditor ClientHeight = 299 ClientWidth = 635 Font.Name = 'Tahoma' - OnResize = FormResize ExplicitWidth = 635 ExplicitHeight = 299 PixelsPerInch = 96 TextHeight = 13 - object pages: TPageControl - Left = 0 - Top = 0 - Width = 635 - Height = 299 - ActivePage = pageDesign - Align = alClient - TabOrder = 0 - TabPosition = tpBottom - OnChanging = pagesChanging - object pageDesign: TTabSheet - Caption = 'Design' - ImageIndex = -1 - object gridWordlist: TStringGridEditControlled - Left = 0 - Top = 0 - Width = 627 - Height = 232 - Align = alClient - ColCount = 3 - DefaultRowHeight = 16 - DefaultDrawing = False - FixedCols = 0 - Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goEditing, goTabs, goAlwaysShowEditor] - TabOrder = 0 - OnClick = gridWordlistClick - OnDrawCell = gridWordlistDrawCell - OnSetEditText = gridWordlistSetEditText - ExplicitHeight = 273 - end - object panGridControls: TPanel - Left = 0 - Top = 232 - Width = 627 - Height = 41 - Align = alBottom - TabOrder = 1 - ExplicitLeft = 224 - ExplicitTop = 120 - ExplicitWidth = 185 - object cmdDeleteRow: TButton - Left = 0 - Top = 6 - Width = 75 - Height = 25 - Caption = '&Delete row' - TabOrder = 0 - OnClick = cmdDeleteRowClick - end - object cmdSortByFrequency: TButton - Left = 81 - Top = 6 - Width = 104 - Height = 25 - Caption = '&Sort by frequency' - TabOrder = 1 - OnClick = cmdSortByFrequencyClick - end - end - end - object pageCode: TTabSheet - Caption = 'Code' - ImageIndex = -1 - end - end end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas index 29d534a648..b1795f6069 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas @@ -13,46 +13,17 @@ uses Vcl.Dialogs, Vcl.Forms, Vcl.Graphics, - Vcl.Grids, - StringGridEditControlled, - - Keyman.System.WordlistTsvFile, - UframeTextEditor, - UfrmMDIEditor, Vcl.StdCtrls, Vcl.ExtCtrls; + Keyman.Developer.UI.UframeWordlistEditor, + UfrmMDIEditor; type TfrmWordlistEditor = class(TfrmTikeEditor) - pages: TPageControl; - pageDesign: TTabSheet; - pageCode: TTabSheet; - gridWordlist: TStringGridEditControlled; - panGridControls: TPanel; - cmdDeleteRow: TButton; - cmdSortByFrequency: TButton; - procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); - procedure FormResize(Sender: TObject); - procedure gridWordlistSetEditText(Sender: TObject; ACol, ARow: Integer; - const Value: string); - procedure gridWordlistDrawCell(Sender: TObject; ACol, ARow: Integer; - ARect: TRect; State: TGridDrawState); - procedure cmdDeleteRowClick(Sender: TObject); - procedure gridWordlistClick(Sender: TObject); - procedure cmdSortByFrequencyClick(Sender: TObject); private - FWordlist: TWordlistTsvFile; - frameSource: TframeTextEditor; - FSetup: Integer; - procedure UpdateData; - function MoveCodeToWordlist: Boolean; - procedure FillCode; - procedure FillGrid; - function MoveDesignToWordlist: Boolean; - procedure SourceChanged(Sender: TObject); - procedure FillGridNewRow; - procedure EnableControls; + frame: TframeWordlistEditor; + procedure FrameModified(Sender: TObject); protected function GetHelpTopic: string; override; function DoOpenFile: Boolean; override; @@ -74,97 +45,41 @@ uses UfrmMain, UfrmMessages; -const - ColWidth_Word = 120; - ColWidth_Frequency = 64; - S_AddRowText = 'Add word...'; - { TfrmWordlistEditor } procedure TfrmWordlistEditor.FormCreate(Sender: TObject); begin inherited; - Inc(FSetup); - try - FWordlist := TWordlistTsvFile.Create; - frameSource := TframeTextEditor.Create(Self); - frameSource.Parent := pageCode; - frameSource.Align := alClient; - frameSource.EditorFormat := efWordlistTsv; - frameSource.Visible := True; - frameSource.OnChanged := SourceChanged; - frameSource.TextFileFormat := tffUTF8; -//TODO: frameSource.UseTabs := True; - pages.ActivePage := pageDesign; - - gridWordlist.Cells[0, 0] := 'Word Form'; - gridWordlist.Cells[1, 0] := 'Count'; - gridWordlist.Cells[2, 0] := 'Comment'; - gridWordlist.ColWidths[0] := ColWidth_Word; - gridWordlist.ColWidths[1] := ColWidth_Frequency; - gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; - finally - Dec(FSetup); - end; + frame := TframeWordlistEditor.Create(Self); + frame.Align := alClient; + frame.Parent := Self; + frame.OnModifiedChanged := FrameModified; end; procedure TfrmWordlistEditor.FormDestroy(Sender: TObject); begin inherited; - FWordlist.Free; + frame.Free; end; -procedure TfrmWordlistEditor.FormResize(Sender: TObject); +procedure TfrmWordlistEditor.FrameModified(Sender: TObject); begin - inherited; - gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; -end; - -procedure TfrmWordlistEditor.cmdDeleteRowClick(Sender: TObject); -begin - FWordlist.RemoveWord(gridWordlist.Row-1); - FillGrid; - EnableControls; - Modified := True; -end; - -procedure TfrmWordlistEditor.cmdSortByFrequencyClick(Sender: TObject); -begin - FWordlist.SortByFrequency; - FillGrid; - Modified := True; + Modified := frame.Modified; end; function TfrmWordlistEditor.DoOpenFile: Boolean; begin - RefreshOptions; - if FileExists(FileName) then - begin - FWordlist.LoadFromFile(FileName); - UpdateData; - end; - Result := True; + Result := frame.LoadFromFile(Filename); end; function TfrmWordlistEditor.DoSaveFile: Boolean; begin - MoveCodeToWordlist; // Does nothing if not in code page - FWordlist.SaveToFile(FileName); - Result := True; -end; - -procedure TfrmWordlistEditor.EnableControls; -var - e: Boolean; -begin - e := gridWordlist.Row < gridWordlist.RowCount - 1; - cmdDeleteRow.Enabled := e; - cmdSortByFrequency.Enabled := e; + Result := frame.SaveToFile(Filename); end; procedure TfrmWordlistEditor.FindError(const Filename: string; s: string; line: Integer); begin - // + frame.FindError(Filename, s, line); end; function TfrmWordlistEditor.GetDefaultExt: string; @@ -182,142 +97,4 @@ begin Result := SHelpTopic_Context_WordlistEditor; end; -procedure TfrmWordlistEditor.gridWordlistClick(Sender: TObject); -begin - EnableControls; -end; - -procedure TfrmWordlistEditor.gridWordlistDrawCell(Sender: TObject; ACol, - ARow: Integer; ARect: TRect; State: TGridDrawState); -var - LText: string; -begin - inherited; - if ARow = gridWordlist.RowCount - 1 then - begin - // Drawing last row - gridWordlist.Canvas.Font.Style := [fsItalic]; - end - else if gridWordlist.Cells[0, ARow] = '#' then - begin - // Drawing a comment cell - gridWordlist.Canvas.Font.Style := [fsBold]; - end - else - gridWordlist.Canvas.Font.Style := []; - - LText := gridWordlist.Cells[ACol, ARow]; - gridWordlist.Canvas.TextRect(ARect, ARect.Left+2, - ARect.Top+((ARect.Height - gridWordlist.Canvas.TextHeight(LText)) div 2), LText); -end; - -procedure TfrmWordlistEditor.gridWordlistSetEditText(Sender: TObject; ACol, - ARow: Integer; const Value: string); -var - w: TWordlistWord; -begin - if FSetup > 0 then Exit; - Inc(FSetup); - try - Modified := True; - - if ARow = gridWordlist.RowCount - 1 then - begin - if (Value = '') or (Value = S_AddRowText) then - Exit; - - w.Word := Value; - w.Frequency := 0; - w.Comment := ''; - FWordlist.AddWord(w); - gridWordlist.RowCount := gridWordlist.RowCount + 1; - FillGridNewRow; - end - else - begin - w := FWordlist.Word[ARow-1]; - case ACol of - 0: w.Word := Value; - 1: w.Frequency := StrToIntDef(Value, 0); - 2: w.Comment := Value; - end; - FWordlist.Word[ARow-1] := w; - end; - finally - Dec(FSetup); - end; -end; - -procedure TfrmWordlistEditor.pagesChanging(Sender: TObject; - var AllowChange: Boolean); -begin - if FSetup > 0 then Exit; - - if pages.ActivePage = pageCode then - AllowChange := MoveCodeToWordlist - else - AllowChange := MoveDesignToWordlist -end; - -function TfrmWordlistEditor.MoveCodeToWordlist: Boolean; -begin - if pages.ActivePage <> pageCode then - Exit(False); - - FWordlist.Text := frameSource.EditorText; - FillGrid; - Result := True; -end; - -function TfrmWordlistEditor.MoveDesignToWordlist: Boolean; -begin - FillCode; - Result := True; -end; - -procedure TfrmWordlistEditor.UpdateData; -begin - if pages.ActivePage = pageCode - then FillCode - else FillGrid; -end; - -procedure TfrmWordlistEditor.SourceChanged(Sender: TObject); -begin - if FSetup = 0 then - Modified := True; -end; - -procedure TfrmWordlistEditor.FillCode; -begin - frameSource.EditorText := FWordlist.Text; -end; - -procedure TfrmWordlistEditor.FillGrid; -var - i: Integer; - w: TWordlistWord; -begin - gridWordlist.RowCount := FWordlist.Count + 2; - for i := 0 to FWordlist.Count - 1 do - begin - w := FWordlist.Word[i]; - gridWordlist.Cells[0, i+1] := w.Word; - if w.Frequency = 0 - then gridWordlist.Cells[1, i+1] := '' - else gridWordlist.Cells[1, i+1] := IntToStr(w.Frequency); - gridWordlist.Cells[2, i+1] := w.Comment; - end; - FillGridNewRow; - gridWordlist.FixedRows := 1; -end; - -procedure TfrmWordlistEditor.FillGridNewRow; -begin - gridWordlist.Cells[0, gridWordlist.RowCount - 1] := S_AddRowText; - gridWordlist.Cells[1, gridWordlist.RowCount - 1] := ''; - gridWordlist.Cells[2, gridWordlist.RowCount - 1] := ''; - EnableControls; -end; - end. diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm index e2561dce1f..e6b404b46c 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm @@ -183,7 +183,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 0 Width = 965 Height = 622 - ActivePage = pageLexicalModels + ActivePage = pageFiles Align = alClient Images = modActionsMain.ilEditorPages MultiLine = True @@ -1119,6 +1119,9 @@ inherited frmPackageEditor: TfrmPackageEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Compile' diff --git a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas new file mode 100644 index 0000000000..78edf36e5f --- /dev/null +++ b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas @@ -0,0 +1,227 @@ +unit Keyman.Developer.System.LexicalModelParser; + +interface + +uses + System.Classes, + System.RegularExpressions, + + Keyman.Developer.System.LexicalModelParserTypes; + +type + TLexicalModelParser = class + private + FText: TStrings; + FModified: Boolean; + FComment: string; + FFormat: TLexicalModelFormat; + FWordlists: TStrings; + FWordBreaker: TLexicalModelWordBreaker; + FIsEditable: Boolean; + function GetText: string; + procedure PrepareText; + procedure SetComment(const Value: string); + procedure SetFormat(const Value: TLexicalModelFormat); + procedure SetWordBreaker(const Value: TLexicalModelWordBreaker); + procedure Parse; + procedure Modify; + function ReplaceFormat(const m: TMatch): string; + function ReplaceWordBreaker(const m: TMatch): string; + function ReplaceSources(const m: TMatch): string; + function ReplaceComment(const m: TMatch): string; + public + constructor Create(Source: string); + destructor Destroy; override; + property Text: string read GetText; + property Format: TLexicalModelFormat read FFormat write SetFormat; + property WordBreaker: TLexicalModelWordBreaker read FWordBreaker write SetWordBreaker; + property Wordlists: TStrings read FWordlists; + property Comment: string read FComment write SetComment; + property IsEditable: Boolean read FIsEditable; + end; + +implementation + +uses + System.SysUtils; + +{ TLexicalModelParser } + +const + SComment = '^\s*\/\*(.+?)\*\/'; + SFormat = 'format\s*:\s*([''"])(.+?)(\1)'; + SWordBreaker = 'wordBreaker\s*:\s*([''"])(.+?)(\1)'; + SSources = 'sources\s*:\s*\[(.+?)\]'; + SSource = '\s*([''"])(.+?)(\1)\s*(,?)'; + +constructor TLexicalModelParser.Create(Source: string); +begin + inherited Create; + FWordlists := TStringList.Create; + FText := TStringList.Create; + FText.Text := Source; + FModified := False; + Parse; +end; + +destructor TLexicalModelParser.Destroy; +begin + FreeAndNil(FWordlists); + FreeAndNil(FText); + inherited Destroy; +end; + +function TLexicalModelParser.GetText: string; +begin + PrepareText; + Result := FText.Text; +end; + +procedure TLexicalModelParser.Modify; +begin + FModified := True; +end; + +procedure TLexicalModelParser.Parse; +var + re: TRegEx; + m: TMatch; + s: string; +begin + // Long term, we should probably consider using the TypeScript compiler + // to real-time generate an AST and work from that. But that introduces a + // layer of glue to the Delphi code that I don't want to get into just now. + FIsEditable := True; + FFormat := lmfUnknown; + FWordBreaker := lmwbUnknown; + FComment := ''; + + s := FText.Text; + + // /* Comment */ + m := TRegEx.Match(s, SComment, [roSingleLine]); + if m.Success then + FComment := m.Groups[1].Value; + + // format: + m := TRegEx.Match(s, SFormat, [roMultiLine]); + if m.Success then + FFormat := TLexicalModelParserTypes.FormatFromText(m.Groups[2].Value); + + // wordBreaker: + m := TRegEx.Match(s, SWordBreaker, [roMultiLine]); + if m.Success then + FWordBreaker := TLexicalModelParserTypes.WordBreakerFromText(m.Groups[2].Value); + + // sources: + FWordlists.Clear; + m := TRegEx.Match(s, SSources, [roMultiLine]); + if m.Success then + begin + re := TRegEx.Create(SSource, [roMultiLine]); + m := re.Match(m.Groups[1].Value); + while m.Success do + begin + FWordlists.Add(m.Groups[2].Value); + m := m.NextMatch; + end; + end + else + FIsEditable := False; + + FIsEditable := FIsEditable and (FFormat <> lmfUnknown) and (FWordBreaker <> lmwbUnknown); +end; + +function TLexicalModelParser.ReplaceComment(const m: TMatch): string; +begin + if FComment = '' + then Result := '' + else Result := '/*'+FComment+'*/'; +end; + +function TLexicalModelParser.ReplaceFormat(const m: TMatch): string; +begin + Result := m.Value.Substring(0, m.Groups[2].Index - m.Index) + + TLexicalModelParserTypes.FormatToText(FFormat) + + m.Value.Substring(m.Groups[2].Index - m.Index + m.Groups[2].Length); +end; + +function TLexicalModelParser.ReplaceWordBreaker(const m: TMatch): string; +begin + Result := m.Value.Substring(0, m.Groups[2].Index - m.Index) + + TLexicalModelParserTypes.WordBreakerToText(FWordBreaker) + + m.Value.Substring(m.Groups[2].Index - m.Index + m.Groups[2].Length); +end; + +function TLexicalModelParser.ReplaceSources(const m: TMatch): string; +var + sources: string; + source: string; +begin + sources := ''; + for source in FWordlists do + begin + if sources <> '' then + sources := sources + ', '; + sources := sources + '''' + source + ''''; // assuming no quote characters + end; + + Result := m.Value.Substring(0, m.Groups[1].Index - m.Index) + + sources + + m.Value.Substring(m.Groups[1].Index - m.Index + m.Groups[1].Length); +end; + +procedure TLexicalModelParser.PrepareText; +var + s: string; +begin + if not FModified then Exit; + + s := FText.Text; + + // Replace tokens + s := TRegEx.Replace(s, SFormat, ReplaceFormat, [roMultiLine]); + s := TRegEx.Replace(s, SWordBreaker, ReplaceWordBreaker, [roMultiLine]); + s := TRegEx.Replace(s, SSources, ReplaceSources, [roMultiLine]); + + // Replace, remove or add comment + if TRegEx.IsMatch(s, SComment, [roSingleLine]) then + s := TRegEx.Replace(s, SComment, ReplaceComment, [roSingleLine]) + else if FComment <> '' then + s := '/*'+FComment+'*/'#13#10+s; + + + FText.Text := s; +end; + +procedure TLexicalModelParser.SetComment(const Value: string); +begin + Assert(FIsEditable); + if FComment <> Value then + begin + FComment := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.SetFormat(const Value: TLexicalModelFormat); +begin + Assert(FIsEditable); + if FFormat <> Value then + begin + FFormat := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.SetWordBreaker(const Value: TLexicalModelWordBreaker); +begin + Assert(FIsEditable); + if FWordBreaker <> Value then + begin + FWordBreaker := Value; + Modify; + end; +end; + +end. diff --git a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas new file mode 100644 index 0000000000..07539f72f7 --- /dev/null +++ b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas @@ -0,0 +1,52 @@ +unit Keyman.Developer.System.LexicalModelParserTypes; + +interface + +type + TLexicalModelFormat = (lmfUnknown, lmfTrie10, lmfCustom10); + TLexicalModelWordBreaker = (lmwbUnknown, lmwbDefault, lmwbAscii, lmwbCustom); + + TLexicalModelParserTypes = class sealed + private + const + SLexicalModelFormats: array[TLexicalModelFormat] of string = ('?', 'trie-1.0', 'custom-1.0'); + SLexicalModelWordBreakers: array[TLexicalModelWordBreaker] of string = ('?', 'default', 'ascii', 'custom'); + LexicalModelFormats: set of TLexicalModelFormat = [lmfUnknown..lmfCustom10]; + LexicalModelWordBreakers: set of TLexicalModelWordBreaker = [lmwbUnknown..lmwbCustom]; + public + class function FormatFromText(const Value: string): TLexicalModelFormat; static; + class function FormatToText(const Value: TLexicalModelFormat): string; static; + class function WordBreakerFromText(const Value: string): TLexicalModelWordBreaker; static; + class function WordBreakerToText(const Value: TLexicalModelWordBreaker): string; static; + end; + +implementation + +uses + System.SysUtils; + +class function TLexicalModelParserTypes.FormatFromText(const Value: string): TLexicalModelFormat; +begin + for Result in LexicalModelFormats do + if SLexicalModelFormats[Result].Equals(Value) then Exit; + Result := lmfUnknown; +end; + +class function TLexicalModelParserTypes.FormatToText(const Value: TLexicalModelFormat): string; +begin + Result := SLexicalModelFormats[Value]; +end; + +class function TLexicalModelParserTypes.WordBreakerFromText(const Value: string): TLexicalModelWordBreaker; +begin + for Result in LexicalModelWordBreakers do + if SLexicalModelWordBreakers[Result].Equals(Value) then Exit; + Result := lmwbUnknown; +end; + +class function TLexicalModelParserTypes.WordBreakerToText(const Value: TLexicalModelWordBreaker): string; +begin + Result := SLexicalModelWordBreakers[Value]; +end; + +end. diff --git a/windows/src/developer/TIKE/main/UframeTextEditor.dfm b/windows/src/developer/TIKE/main/UframeTextEditor.dfm index ec21b23c6e..410ad98d1f 100644 --- a/windows/src/developer/TIKE/main/UframeTextEditor.dfm +++ b/windows/src/developer/TIKE/main/UframeTextEditor.dfm @@ -17,7 +17,7 @@ inherited frameTextEditor: TframeTextEditor Left = 394 Top = 58 Bitmap = { - 494C010106000900880010001000C0C0C000FE00FFFFFFFFFFFFFFFF424D3600 + 494C010106000900040010001000C0C0C000FE00FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000400000002000000001002000000000000020 000000000000000000000000000000000000C6C6C600C6C6C600C6C6C600C6C6 C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6 @@ -295,7 +295,7 @@ inherited frameTextEditor: TframeTextEditor Left = 618 Top = 26 Bitmap = { - 494C010106000900880010001000FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 + 494C010106000900040010001000FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000400000002000000001002000000000000020 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 diff --git a/windows/src/developer/TIKE/main/UframeTextEditor.pas b/windows/src/developer/TIKE/main/UframeTextEditor.pas index fca4d88760..7bf3cbc9d3 100644 --- a/windows/src/developer/TIKE/main/UframeTextEditor.pas +++ b/windows/src/developer/TIKE/main/UframeTextEditor.pas @@ -34,7 +34,7 @@ uses type TEditorBreakpointClickedEvent = procedure(Sender: TObject; ALine: Integer) of object; - TframeTextEditor = class(TTIKEForm, IKMDEditActions, IKMDSearchActions, IKMDTextEditorActions) + TframeTextEditor = class(TTikeForm, IKMDEditActions, IKMDSearchActions, IKMDTextEditorActions) lstImages: TMenuImgList; dlgFonts: TFontDialog; dlgPrintSetup: TPrinterSetupDialog; diff --git a/windows/src/developer/TIKE/main/UfrmMain.pas b/windows/src/developer/TIKE/main/UfrmMain.pas index 8cc494d7ec..0d2003e0de 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.pas +++ b/windows/src/developer/TIKE/main/UfrmMain.pas @@ -341,6 +341,7 @@ type procedure SaveDockLayout; procedure CEFShutdownComplete(Sender: TObject); procedure ActivateActiveChild; + function OpenModelEditor(FFileName: string): TfrmTikeEditor; protected procedure WndProc(var Message: TMessage); override; @@ -432,6 +433,7 @@ uses Keyman.Developer.UI.Project.ProjectFileUI, Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.UfrmWordlistEditor, + Keyman.Developer.UI.UfrmModelEditor, TextFileFormat, RedistFiles, ErrorControlledRegistry, @@ -1104,6 +1106,11 @@ begin end; function TfrmKeymanDeveloper.OpenFile(FFileName: string; FCloseNewFile: Boolean): TfrmTikeChild; + function FileHasModelTsExt(Filename: string): Boolean; + begin + // We cannot use ExtractFileExt because of the two-part extension + Result := Filename.ToLower.EndsWith('.model.ts'); + end; var ext: string; begin @@ -1137,6 +1144,7 @@ begin else if ext = '.kvks' then Result := OpenKVKEditor(FFileName) else if ext = '.bmp' then Result := OpenEditor(FFileName, TfrmBitmapEditor) else if ext = '.tsv' then Result := OpenTSVEditor(FFileName) + else if FileHasModelTsExt(FFileName) then Result := OpenModelEditor(FFileName) else Result := OpenEditor(FFileName, TfrmEditor); end; @@ -1166,6 +1174,11 @@ begin //else Result := OpenEditor(FFileName, TfrmEditor); end; +function TfrmKeymanDeveloper.OpenModelEditor(FFileName: string): TfrmTikeEditor; +begin + Result := OpenEditor(FFileName, TfrmModelEditor); +end; + function TfrmKeymanDeveloper.OpenKPSEditor(FFileName: string): TfrmTikeEditor; begin Result := OpenEditor(FFileName, TfrmPackageEditor); diff --git a/windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas b/windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas new file mode 100644 index 0000000000..cf11b8e600 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas @@ -0,0 +1,130 @@ +unit Keyman.System.Test.LexicalModelParserTest; + +interface + +uses + System.Classes, + System.SysUtils, + DUnitX.TestFramework; + +type + [TestFixture] + TLexicalModelParserTest = class(TObject) + private + m: TStringList; + AssetRootPath: string; + mcustom: TStringList; + mnocomment: TStrings; + public + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure TestRead; + + [Test] + procedure TestWrite; + + [Test] + procedure TestWriteDeleteComment; + + [Test] + procedure TestWriteNewComment; + end; + +implementation + +uses + Keyman.Developer.System.LexicalModelParser, + Keyman.Developer.System.LexicalModelParserTypes; + +procedure TLexicalModelParserTest.Setup; +begin + // Running from platform/configuration/ folder has + // assets two levels up. + if FileExists('..\..\assets\nrc.en.mtnt.model.ts') + then AssetRootPath := '..\..\assets\' + else AssetRootPath := ''; + + m := TStringList.Create; + m.LoadFromFile(AssetRootPath + 'nrc.en.mtnt.model.ts'); + + mcustom := TStringList.Create; + mcustom.LoadFromFile(AssetRootPath + 'custom.model.ts'); + + mnocomment := TStringList.Create; + mnocomment.LoadFromFile(AssetRootPath + 'nocomment.model.ts'); +end; + +procedure TLexicalModelParserTest.TearDown; +begin + m.Free; + mcustom.Free; + mnocomment.Free; +end; + +procedure TLexicalModelParserTest.TestRead; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(m.Text); + try + Assert.IsTrue(lm.Format = lmfTrie10, 'Expected lmfTrie10'); + Assert.IsTrue(lm.WordBreaker = lmwbDefault, 'Expected lmwbDefault'); + Assert.AreEqual(1, lm.Wordlists.Count, 'Expected 1 wordlist'); + Assert.AreEqual('mtnt.tsv', lm.Wordlists[0], 'Wordlist name'); + Assert.AreEqual(#13#10' Sample model for testing parser'#13#10, lm.Comment); + finally + lm.Free; + end; +end; + +procedure TLexicalModelParserTest.TestWrite; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(m.Text); + try + lm.Comment := 'Testing'; + lm.Format := lmfCustom10; + lm.WordBreaker := lmwbAscii; + lm.Wordlists.Clear; + lm.Wordlists.Add('foo.tsv'); + lm.Wordlists.Add('bar.tsv'); + Assert.AreEqual(mcustom.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after + finally + lm.Free; + end; +end; + +procedure TLexicalModelParserTest.TestWriteNewComment; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(mnocomment.Text); + try + lm.Comment := #13#10' Sample model for testing parser'#13#10; + Assert.AreEqual(m.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after + finally + lm.Free; + end; +end; + +procedure TLexicalModelParserTest.TestWriteDeleteComment; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(m.Text); + try + lm.Comment := ''; + Assert.AreEqual(mnocomment.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after + finally + lm.Free; + end; +end; + +initialization + TDUnitX.RegisterTestFixture(TLexicalModelParserTest); +end. diff --git a/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr new file mode 100644 index 0000000000..214fb595a2 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr @@ -0,0 +1,61 @@ +program ModelTsParserTest; + +{$IFNDEF TESTINSIGHT} +{$APPTYPE CONSOLE} +{$ENDIF}{$STRONGLINKTYPES ON} +uses + System.SysUtils, + {$IFDEF TESTINSIGHT} + TestInsight.DUnitX, + {$ENDIF } + DUnitX.Loggers.Console, + DUnitX.Loggers.Xml.NUnit, + DUnitX.TestFramework, + Keyman.System.Test.LexicalModelParserTest in 'Keyman.System.Test.LexicalModelParserTest.pas', + Keyman.Developer.System.LexicalModelParser in '..\..\..\developer\TIKE\main\Keyman.Developer.System.LexicalModelParser.pas', + Keyman.Developer.System.LexicalModelParserTypes in '..\..\..\developer\TIKE\main\Keyman.Developer.System.LexicalModelParserTypes.pas'; + +var + runner : ITestRunner; + results : IRunResults; + logger : ITestLogger; + nunitLogger : ITestLogger; +begin +{$IFDEF TESTINSIGHT} + TestInsight.DUnitX.RunRegisteredTests; + exit; +{$ENDIF} + try + //Check command line options, will exit if invalid + TDUnitX.CheckCommandLine; + //Create the test runner + runner := TDUnitX.CreateRunner; + //Tell the runner to use RTTI to find Fixtures + runner.UseRTTI := True; + //tell the runner how we will log things + //Log to the console window + logger := TDUnitXConsoleLogger.Create(true); + runner.AddLogger(logger); + //Generate an NUnit compatible XML File + nunitLogger := TDUnitXXMLNUnitFileLogger.Create(TDUnitX.Options.XMLOutputFile); + runner.AddLogger(nunitLogger); + runner.FailsOnNoAsserts := False; //When true, Assertions must be made during tests; + + //Run tests + results := runner.Execute; + if not results.AllPassed then + System.ExitCode := EXIT_ERRORS; + + {$IFNDEF CI} + //We don't want this happening when running under CI. + if TDUnitX.Options.ExitBehavior = TDUnitXExitBehavior.Pause then + begin + System.Write('Done.. press key to quit.'); + System.Readln; + end; + {$ENDIF} + except + on E: Exception do + System.Writeln(E.ClassName, ': ', E.Message); + end; +end. diff --git a/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj new file mode 100644 index 0000000000..bbfac6492b --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj @@ -0,0 +1,765 @@ + + + {0158CB55-B9BC-4F32-B860-FD6073AE2EB7} + 18.7 + None + ModelTsParserTest.dpr + True + Debug + Win32 + 1 + Console + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\delphi_PROJECTICNS.icns + $(DUnitX);$(DCC_UnitSearchPath) + ModelTsParserTest + + + DBXSqliteDriver;IndyIPCommon;RESTComponents;bindcompdbx;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;svnui;mbColorLibD10;dsnapcon;FireDACADSDriver;scFontCombo;DCPdelphi2009;FireDACMSAccDriver;fmxFireDAC;vclimg;Jcl;FireDAC;vcltouch;JvCore;vcldb;bindcompfmx;svn;FireDACSqliteDriver;FireDACPgDriver;inetdb;CEF4Delphi;soaprtl;DbxCommonDriver;fmx;FireDACIBDriver;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;EmbeddedWebBrowser_XE;VCLRESTComponents;soapserver;dbxcds;VclSmp;JvDocking;adortl;JclVcl;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;IndyProtocols;inetdbxpress;keyman_components;FireDACCommonODBC;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + DBXSqliteDriver;IndyIPCommon;RESTComponents;bindcompdbx;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;Jcl;FireDAC;vcltouch;vcldb;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;inetdb;soaprtl;DbxCommonDriver;fmx;FireDACIBDriver;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;VCLRESTComponents;soapserver;dbxcds;VclSmp;adortl;JclVcl;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;IndyProtocols;inetdbxpress;FireDACCommonODBC;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + + + DEBUG;$(DCC_Define) + true + false + true + true + true + + + false + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + + MainSource + + + + + + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + + + + Delphi.Personality.12 + Console + + + + ModelTsParserTest.dpr + + + + + + true + + + + + true + + + + + true + + + + + ModelTsParserTest.exe + true + + + + + 1 + + + 0 + + + + + classes + 1 + + + + + res\xml + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\armeabi + 1 + + + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + + + res\values + 1 + + + + + res\values-v21 + 1 + + + + + res\values + 1 + + + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + + + 1 + + + 1 + + + 0 + + + + + 1 + .framework + + + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + + + + 1 + + + 1 + + + 1 + + + + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + + + + + + + + + + True + False + + + 12 + + + + + diff --git a/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.res b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.res new file mode 100644 index 0000000000000000000000000000000000000000..6876088a6648ed90b09c480197508e4404c57dbc GIT binary patch literal 57140 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 I2>8kW0h0=QvH$=8 literal 0 HcmV?d00001 diff --git a/windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts b/windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts new file mode 100644 index 0000000000..64e8f8e122 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts @@ -0,0 +1,8 @@ +/*Testing*/ +const source: LexicalModelSource = { + format: 'custom-1.0', + wordBreaker: 'ascii', + sources: ['foo.tsv', 'bar.tsv'] +}; + +export default source; diff --git a/windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts b/windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts new file mode 100644 index 0000000000..3d709e48a3 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts @@ -0,0 +1,7 @@ +const source: LexicalModelSource = { + format: 'trie-1.0', + wordBreaker: 'default', + sources: ['mtnt.tsv'] +}; + +export default source; diff --git a/windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts b/windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts new file mode 100644 index 0000000000..2d1cf76cda --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts @@ -0,0 +1,10 @@ +/* + Sample model for testing parser +*/ +const source: LexicalModelSource = { + format: 'trie-1.0', + wordBreaker: 'default', + sources: ['mtnt.tsv'] +}; + +export default source; From fb95c0f74fc52b1f8d9d93f2203fbc2e64221d08 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 28 Nov 2019 14:13:29 +1100 Subject: [PATCH 02/16] feat: model editor lifecycle --- ...yman.Developer.UI.UframeWordlistEditor.dfm | 2 + ...yman.Developer.UI.UframeWordlistEditor.pas | 2 +- .../Keyman.Developer.UI.UfrmModelEditor.dfm | 27 ++--- .../Keyman.Developer.UI.UfrmModelEditor.pas | 113 ++++++++++++++---- ...an.Developer.System.LexicalModelParser.pas | 9 +- 5 files changed, 111 insertions(+), 42 deletions(-) diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm index c169f740de..7cea795c20 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm @@ -24,6 +24,8 @@ inherited frameWordlistEditor: TframeWordlistEditor object pageDesign: TTabSheet Caption = 'Design' ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 ExplicitWidth = 627 ExplicitHeight = 273 object gridWordlist: TStringGridEditControlled diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas index e30fa8a5c2..c8739c0e59 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas @@ -146,8 +146,8 @@ begin if FileExists(FileName) then begin FWordlist.LoadFromFile(FileName); - UpdateData; end; + UpdateData; Result := True; end; diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm index 631026f7ce..95db5e2054 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm @@ -105,35 +105,25 @@ inherited frmModelEditor: TfrmModelEditor 24 24) end - object cmdNewWordlist: TButton + object cmdAddWordlist: TButton Left = 503 Top = 27 Width = 101 Height = 25 Anchors = [akTop, akRight] - Caption = '&New...' + Caption = '&Add...' TabOrder = 1 - OnClick = cmdNewWordlistClick + OnClick = cmdAddWordlistClick end object cmdRemoveWordlist: TButton - Left = 503 - Top = 89 - Width = 100 - Height = 25 - Anchors = [akTop, akRight] - Caption = 'Remove' - TabOrder = 3 - OnClick = cmdRemoveWordlistClick - end - object cmdAddExistingWordlist: TButton Left = 503 Top = 58 Width = 100 Height = 25 Anchors = [akTop, akRight] - Caption = '&Add existing...' + Caption = 'Remove' TabOrder = 2 - OnClick = cmdAddExistingWordlistClick + OnClick = cmdRemoveWordlistClick end end object panBasicInformation: TPanel @@ -236,10 +226,16 @@ inherited frmModelEditor: TfrmModelEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Build' ImageIndex = 1 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 object Panel1: TPanel Left = 0 Top = 0 @@ -387,6 +383,7 @@ inherited frmModelEditor: TfrmModelEditor end object dlgAddWordlist: TOpenDialog Options = [ofHideReadOnly, ofPathMustExist, ofEnableSizing] + Title = 'Create or Add Wordlist' Left = 344 Top = 344 end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas index 8888f09a30..64e05bfbc5 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas @@ -1,3 +1,4 @@ +//TODO: CEF destruction is incorrect in this unit unit Keyman.Developer.UI.UfrmModelEditor; interface @@ -21,6 +22,10 @@ uses LeftTabbedPageControl, UfrmMDIEditor, UframeTextEditor, + Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.System.Project.modeltsProjectFile, + Keyman.Developer.UI.Project.modeltsProjectFileUI, + Keyman.Developer.UI.UframeWordlistEditor, Keyman.Developer.System.LexicalModelParser, Keyman.Developer.System.LexicalModelParserTypes; @@ -33,7 +38,7 @@ type panWordlists: TPanel; lblWordlists: TLabel; gridWordlists: TStringGrid; - cmdNewWordlist: TButton; + cmdAddWordlist: TButton; cmdRemoveWordlist: TButton; panBasicInformation: TPanel; lblFormat: TLabel; @@ -63,10 +68,8 @@ type Label2: TLabel; lblReadOnly: TLabel; dlgAddWordlist: TOpenDialog; - cmdAddExistingWordlist: TButton; procedure FormDestroy(Sender: TObject); - procedure cmdNewWordlistClick(Sender: TObject); - procedure cmdAddExistingWordlistClick(Sender: TObject); + procedure cmdAddWordlistClick(Sender: TObject); procedure cmdRemoveWordlistClick(Sender: TObject); procedure gridWordlistsDblClick(Sender: TObject); procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); @@ -99,6 +102,9 @@ type const WordlistFilename: string): TfrmModelEditor.TWordlist; function MoveDesignToSource: Boolean; function MoveSourceToDesign: Boolean; + procedure SourceChanged(Sender: TObject); + function CheckModifiedWordlistsForRemoval( + newParser: TLexicalModelParser): Boolean; { Private declarations } protected function GetHelpTopic: string; override; @@ -107,6 +113,8 @@ type function GetFileNameFilter: string; override; function GetDefaultExt: string; override; + function GetProjectFile: TProjectFile; override; + public procedure FindError(const Filename: string; s: string; line: Integer); override; // I4081 end; @@ -114,6 +122,8 @@ type implementation uses + System.UITypes, + Keyman.Developer.System.Project.modeltsProjectFileAction, TextFileFormat; {$R *.dfm} @@ -159,8 +169,17 @@ begin end; function TfrmModelEditor.DoSaveFile: Boolean; +var + wordlist: TWordlist; begin - // TODO + for wordlist in wordlists do + begin + wordlist.Frame.SaveToFile(wordlist.Frame.Filename); + end; + + model.Text := parser.Text; + model.SaveToFile(FileName); + Result := True; end; @@ -175,7 +194,7 @@ begin cbWordBreaker.Enabled := e; lblComments.Enabled := e; memoComments.Enabled := e; - cmdAddExistingWordlist.Enabled := e; + cmdAddWordlist.Enabled := e; gridWordlists.Enabled := e and (parser.Wordlists.Count > 0); cmdRemoveWordlist.Enabled := e and (parser.Wordlists.Count > 0); lblReadOnly.Visible := not e; @@ -199,7 +218,7 @@ begin frameSource.Align := alClient; frameSource.EditorFormat := efJS; frameSource.Visible := True; -// frameSource.OnChanged := SourceChanged; + frameSource.OnChanged := SourceChanged; frameSource.TextFileFormat := tffUTF8; pages.ActivePage := pageDetails; @@ -231,6 +250,16 @@ begin Result := ''; //SHelpTopic_Context_WordlistEditor; // TODO: use a better topic end; +function TfrmModelEditor.GetProjectFile: TProjectFile; +begin + Result := inherited GetProjectFile; + if not Assigned(Result) then + begin + FStandaloneProjectFile := TmodelTsProjectFileAction.Create(nil, FileName, nil); + Result := FStandaloneProjectFile; + end; +end; + procedure TfrmModelEditor.gridWordlistsDblClick(Sender: TObject); begin pages.ActivePageIndex := gridWordlists.Row + 1; @@ -244,12 +273,48 @@ begin Modified := True; end; +procedure TfrmModelEditor.SourceChanged(Sender: TObject); +begin + if FSetup > 0 then + Exit; + Modified := True; +end; + +function TfrmModelEditor.CheckModifiedWordlistsForRemoval(newParser: TLexicalModelParser): Boolean; +var + wordlist: TWordlist; +begin + for wordlist in wordlists do + begin + if (newParser.Wordlists.IndexOf(wordlist.Filename) < 0) and + wordlist.Frame.Modified then + begin + case MessageDlg(Format('The wordlist %s has been removed but has been modified. Save changes before removal?', + [ExtractFileName(wordlist.Filename)]), mtConfirmation, mbYesNoCancel, 0) of + mrYes: wordlist.Frame.SaveToFile(wordlist.Frame.Filename); + mrNo: ; + mrCancel: Exit(False); + end; + end; + end; + + Result := True; +end; + function TfrmModelEditor.MoveSourceToDesign: Boolean; +var + newParser: TLexicalModelParser; begin Inc(FSetup); try + newParser := TLexicalModelParser.Create(frameSource.EditorText); + if not CheckModifiedWordlistsForRemoval(newParser) then + begin + FreeAndNil(newParser); + Exit(False); + end; FreeAndNil(parser); - parser := TLexicalModelParser.Create(frameSource.EditorText); + parser := newParser; FillDetails; UpdateWordlistTabs; finally @@ -378,23 +443,7 @@ begin Modified := True; end; -procedure TfrmModelEditor.cmdAddExistingWordlistClick(Sender: TObject); -begin - if dlgAddWordlist.Execute then - begin - parser.Wordlists.Add(ExtractRelativePath(Filename, dlgAddWordlist.FileName)); - Inc(FSetup); - try - FillDetails; - UpdateWordlistTabs; - finally - Dec(FSetup); - end; - Modified := True; - end; -end; - -procedure TfrmModelEditor.cmdNewWordlistClick(Sender: TObject); +procedure TfrmModelEditor.cmdAddWordlistClick(Sender: TObject); begin if dlgAddWordlist.Execute then begin @@ -411,7 +460,21 @@ begin end; procedure TfrmModelEditor.cmdRemoveWordlistClick(Sender: TObject); +var + wordlist: TWordlist; begin + wordlist := wordlists[gridWordlists.Row]; + if wordlist.Frame.Modified then + begin + case MessageDlg(Format('Save changes to wordlist %s before removing it?', + [ExtractFileName(wordlist.Filename)]), + mtConfirmation, mbYesNoCancel, 0) of + mrYes: wordlist.Frame.SaveToFile(wordlist.Frame.Filename); + mrNo: ; + mrCancel: Exit; + end; + end; + parser.Wordlists.Delete(gridWordlists.Row); Inc(FSetup); try diff --git a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas index 78edf36e5f..c21d4b5e6f 100644 --- a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas +++ b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas @@ -29,6 +29,7 @@ type function ReplaceWordBreaker(const m: TMatch): string; function ReplaceSources(const m: TMatch): string; function ReplaceComment(const m: TMatch): string; + procedure WordlistsChange(Sender: TObject); public constructor Create(Source: string); destructor Destroy; override; @@ -58,10 +59,11 @@ constructor TLexicalModelParser.Create(Source: string); begin inherited Create; FWordlists := TStringList.Create; + (FWordlists as TStringList).OnChange := WordlistsChange; FText := TStringList.Create; FText.Text := Source; - FModified := False; Parse; + FModified := False; end; destructor TLexicalModelParser.Destroy; @@ -224,4 +226,9 @@ begin end; end; +procedure TLexicalModelParser.WordlistsChange(Sender: TObject); +begin + Modify; +end; + end. From 30a1f06bdfc96180e54875f9e8e47726bcba5fa4 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 29 Nov 2019 15:11:05 +0000 Subject: [PATCH 03/16] feat(developer): Add model debugger framework --- windows/src/developer/TIKE/Tike.dpr | 8 +- windows/src/developer/TIKE/Tike.dproj | 9 ++ ...yman.Developer.UI.dmActionsModelEditor.dfm | 27 ++++ ...yman.Developer.UI.dmActionsModelEditor.pas | 73 ++++++++++ .../Keyman.Developer.UI.UfrmModelEditor.dfm | 29 ++-- .../Keyman.Developer.UI.UfrmModelEditor.pas | 51 ++++++- .../child/Keyman.Developer.UI.UfrmWebTest.dfm | 12 ++ .../child/Keyman.Developer.UI.UfrmWebTest.pas | 129 ++++++++++++++++++ .../developer/TIKE/child/UfrmKeymanWizard.dfm | 52 +------ ...n.Developer.System.HttpServer.Debugger.pas | 101 +++++++++++++- windows/src/developer/TIKE/main/UfrmMain.dfm | 4 + windows/src/developer/TIKE/main/UfrmMain.pas | 11 ++ ...eloper.UI.Project.modelTsProjectFileUI.pas | 70 +++++++++- 13 files changed, 504 insertions(+), 72 deletions(-) create mode 100644 windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm create mode 100644 windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas create mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm create mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas diff --git a/windows/src/developer/TIKE/Tike.dpr b/windows/src/developer/TIKE/Tike.dpr index 6a187684dc..25daf9b995 100644 --- a/windows/src/developer/TIKE/Tike.dpr +++ b/windows/src/developer/TIKE/Tike.dpr @@ -290,7 +290,9 @@ uses Keyman.Developer.UI.UfrmModelEditor in 'child\Keyman.Developer.UI.UfrmModelEditor.pas' {frmModelEditor}, Keyman.Developer.System.LexicalModelParser in 'main\Keyman.Developer.System.LexicalModelParser.pas', Keyman.Developer.System.LexicalModelParserTypes in 'main\Keyman.Developer.System.LexicalModelParserTypes.pas', - Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}; + Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}, + Keyman.Developer.UI.UfrmWebTest in 'child\Keyman.Developer.UI.UfrmWebTest.pas' {frmWebTest}, + Keyman.Developer.UI.dmActionsModelEditor in 'actions\Keyman.Developer.UI.dmActionsModelEditor.pas' {modActionsModelEditor: TDataModule}; {$R *.RES} {$R ICONS.RES} @@ -318,8 +320,8 @@ begin if TikeActive then Exit; InitClasses; Application.CreateForm(TmodWebHttpServer, modWebHttpServer); - Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); - Application.Run; + Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); + Application.Run; end; finally FInitializeCEF.Free; diff --git a/windows/src/developer/TIKE/Tike.dproj b/windows/src/developer/TIKE/Tike.dproj index 899304bd7d..cfbeb6a3ca 100644 --- a/windows/src/developer/TIKE/Tike.dproj +++ b/windows/src/developer/TIKE/Tike.dproj @@ -546,6 +546,15 @@

frmWordlistEditor dfm
+ +
frmWebTest
+ dfm +
+ +
modActionsModelEditor
+ dfm + TDataModule +
Cfg_2 diff --git a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm new file mode 100644 index 0000000000..274527d29d --- /dev/null +++ b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm @@ -0,0 +1,27 @@ +object modActionsModelEditor: TmodActionsModelEditor + OldCreateOrder = False + Height = 150 + Width = 215 + object actionsModelEditor: TActionList + Images = frmKeymanDeveloper.lstImages + Left = 60 + Top = 24 + object actModelCompile: TAction + Category = 'Lexical Model' + Caption = '&Compile Model' + ImageIndex = 37 + ShortCut = 118 + OnExecute = actModelCompileExecute + end + object actModelIncludeDebugInformation: TAction + Category = 'Lexical Model' + Caption = 'Include &Debug Information' + OnExecute = actModelIncludeDebugInformationExecute + end + object actModelTest: TAction + Category = 'Lexical Model' + Caption = 'Test &Lexical Model' + OnExecute = actModelTestExecute + end + end +end diff --git a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas new file mode 100644 index 0000000000..b4341b30f4 --- /dev/null +++ b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas @@ -0,0 +1,73 @@ +unit Keyman.Developer.UI.dmActionsModelEditor; + +interface + +uses + System.SysUtils, System.Classes, System.Actions, Vcl.ActnList; + +type + TmodActionsModelEditor = class(TDataModule) + actionsModelEditor: TActionList; + actModelCompile: TAction; + actModelTest: TAction; + actModelIncludeDebugInformation: TAction; + procedure actModelCompileExecute(Sender: TObject); + procedure actModelIncludeDebugInformationExecute(Sender: TObject); + procedure actModelTestExecute(Sender: TObject); + private + { Private declarations } + public + { Public declarations } + end; + +var + modActionsModelEditor: TmodActionsModelEditor; + +implementation + +{%CLASSGROUP 'Vcl.Controls.TControl'} + +uses + UfrmMain, + UfrmMessages, + Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.UI.Project.ProjectFileUI, + Keyman.Developer.System.Project.modelTsProjectFile, + Keyman.Developer.UI.UfrmModelEditor; + +{$R *.dfm} + +function ActiveModelEditor: TfrmModelEditor; +begin + if Assigned(frmKeymanDeveloper.ActiveChild) and (frmKeymanDeveloper.ActiveChild is TfrmModelEditor) + then Result := frmKeymanDeveloper.ActiveChild as TfrmModelEditor + else Result := nil; +end; + +function ActiveModelProjectFile: TmodelTsProjectFile; +begin + Result := ActiveModelEditor.ProjectFile as TmodelTsProjectFile; +end; + +procedure TmodActionsModelEditor.actModelCompileExecute(Sender: TObject); +begin + frmMessages.Clear; // I4686 + + if ActiveModelEditor <> nil then + begin + (ActiveModelProjectFile.UI as TProjectFileUI).DoAction(pfaCompile, False); + end; +end; + +procedure TmodActionsModelEditor.actModelIncludeDebugInformationExecute( + Sender: TObject); +begin +// TODO: (ActiveModelProjectFile.UI as TmodeltsProjectFileUI).Debug := not (ActiveModelProjectFile.UI as TmodeltsProjectFileUI).Debug; // I4687 +end; + +procedure TmodActionsModelEditor.actModelTestExecute(Sender: TObject); +begin + (ActiveModelProjectFile.UI as TProjectFileUI).DoAction(pfaTestKeymanWeb, False); // I4687 +end; + +end. diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm index 95db5e2054..4dca586eee 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm @@ -14,7 +14,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 0 Width = 712 Height = 708 - ActivePage = pageDetails + ActivePage = pageCompile Align = alClient Font.Charset = ANSI_CHARSET Font.Color = clWindowText @@ -226,16 +226,10 @@ inherited frmModelEditor: TfrmModelEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Build' ImageIndex = 1 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object Panel1: TPanel Left = 0 Top = 0 @@ -260,8 +254,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 40 Width = 137 Height = 25 - Caption = '&Compile Lexical Model' - ImageIndex = 37 + Action = modActionsModelEditor.actModelCompile TabOrder = 0 end object cmdAddToProject: TButton @@ -269,10 +262,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 40 Width = 137 Height = 25 - Hint = - 'Add Current Editor File|Adds the current editor file to the proj' + - 'ect' - Caption = '&Current Editor File' + Action = modActionsMain.actProjectAddCurrentEditorFile TabOrder = 1 end object cmdOpenContainingFolder2: TButton @@ -282,6 +272,7 @@ inherited frmModelEditor: TfrmModelEditor Height = 25 Caption = '&Open Containing Folder' TabOrder = 2 + OnClick = cmdOpenContainingFolder2Click end object panBuildLexicalModel: TPanel Left = 10 @@ -335,7 +326,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 122 Width = 150 Height = 25 - Caption = 'Test Lexical &Model' + Action = modActionsModelEditor.actModelTest TabOrder = 2 end object cmdOpenDebugHost: TButton @@ -345,6 +336,7 @@ inherited frmModelEditor: TfrmModelEditor Height = 25 Caption = 'Open &debugger in local browser' TabOrder = 3 + OnClick = cmdOpenDebugHostClick end object lbDebugHosts: TListBox Left = 12 @@ -361,6 +353,7 @@ inherited frmModelEditor: TfrmModelEditor Height = 25 Caption = 'Send addresses to &email...' TabOrder = 5 + OnClick = cmdSendURLsToEmailClick end object editTestKeyboard: TEdit Left = 123 @@ -387,4 +380,12 @@ inherited frmModelEditor: TfrmModelEditor Left = 344 Top = 344 end + object dlgBrowseTestKeyboard: TOpenDialog + DefaultExt = 'js' + Filter = 'Compiled keyboard files (*.js)|*.js|All files (*.*)|*.*' + Options = [ofHideReadOnly, ofPathMustExist, ofFileMustExist, ofEnableSizing] + Title = 'Select compiled keyboard to test with' + Left = 488 + Top = 120 + end end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas index 64e05bfbc5..b600a8d9ca 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas @@ -19,6 +19,8 @@ uses Winapi.Messages, Winapi.Windows, + Keyman.Developer.UI.dmActionsModelEditor, + dmActionsMain, LeftTabbedPageControl, UfrmMDIEditor, UframeTextEditor, @@ -68,6 +70,7 @@ type Label2: TLabel; lblReadOnly: TLabel; dlgAddWordlist: TOpenDialog; + dlgBrowseTestKeyboard: TOpenDialog; procedure FormDestroy(Sender: TObject); procedure cmdAddWordlistClick(Sender: TObject); procedure cmdRemoveWordlistClick(Sender: TObject); @@ -77,6 +80,9 @@ type procedure cbFormatClick(Sender: TObject); procedure cbWordBreakerClick(Sender: TObject); procedure memoCommentsChange(Sender: TObject); + procedure cmdOpenContainingFolder2Click(Sender: TObject); + procedure cmdOpenDebugHostClick(Sender: TObject); + procedure cmdSendURLsToEmailClick(Sender: TObject); private type TWordlist = class @@ -117,6 +123,7 @@ type public procedure FindError(const Filename: string; s: string; line: Integer); override; // I4081 + procedure NotifyStartedWebDebug; end; implementation @@ -124,7 +131,11 @@ implementation uses System.UITypes, Keyman.Developer.System.Project.modeltsProjectFileAction, - TextFileFormat; + TextFileFormat, + UmodWebHttpServer, + UfrmSendURLsToEmail, + utilexecute, + utilsystem; {$R *.dfm} @@ -188,6 +199,9 @@ var e: Boolean; begin e := parser.IsEditable; + + { Details tab } + lblFormat.Enabled := e; cbFormat.Enabled := e; lblWordBreaker.Enabled := e; @@ -198,6 +212,19 @@ begin gridWordlists.Enabled := e and (parser.Wordlists.Count > 0); cmdRemoveWordlist.Enabled := e and (parser.Wordlists.Count > 0); lblReadOnly.Visible := not e; + + { Build tab } + cmdOpenDebugHost.Enabled := lbDebugHosts.ItemIndex >= 0; + cmdSendURLsToEmail.Enabled := lbDebugHosts.Items.Count > 0; // I4506 +end; + +procedure TfrmModelEditor.NotifyStartedWebDebug; +begin + lbDebugHosts.Clear; + modWebHttpServer.GetURLs(lbDebugHosts.Items); + if lbDebugHosts.Items.Count > 0 then + lbDebugHosts.ItemIndex := 0; + EnableControls; end; procedure TfrmModelEditor.FindError(const Filename: string; s: string; @@ -459,6 +486,16 @@ begin end; end; +procedure TfrmModelEditor.cmdOpenContainingFolder2Click(Sender: TObject); +begin + OpenContainingFolder(FileName); +end; + +procedure TfrmModelEditor.cmdOpenDebugHostClick(Sender: TObject); +begin + TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]); +end; + procedure TfrmModelEditor.cmdRemoveWordlistClick(Sender: TObject); var wordlist: TWordlist; @@ -486,6 +523,18 @@ begin Modified := True; end; +procedure TfrmModelEditor.cmdSendURLsToEmailClick(Sender: TObject); +begin + with TfrmSendURLsToEmail.Create(Application.MainForm) do + try + //TODO: KeyboardName := Self.FKeyboardParser.GetSystemStoreValue(ssName); + Hosts.Assign(lbDebugHosts.Items); + ShowModal; + finally + Free; + end; +end; + { TfrmModelEditor.TWordlist } destructor TfrmModelEditor.TWordlist.Destroy; diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm new file mode 100644 index 0000000000..2266feec51 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm @@ -0,0 +1,12 @@ +inherited frmWebTest: TfrmWebTest + Left = 0 + Top = 0 + Caption = 'Web Test' + ClientHeight = 294 + ClientWidth = 563 + Font.Name = 'Tahoma' + ExplicitWidth = 563 + ExplicitHeight = 294 + PixelsPerInch = 96 + TextHeight = 13 +end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas new file mode 100644 index 0000000000..2f898c5dc5 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas @@ -0,0 +1,129 @@ +unit Keyman.Developer.UI.UfrmWebTest; + +interface + +uses + Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, + Vcl.Controls, Vcl.Forms, Vcl.Dialogs, UfrmMDIChild, + Keyman.Developer.UI.UframeCEFHost; + +type + TfrmWebTest = class(TfrmTikeChild) + procedure FormCreate(Sender: TObject); + private + cef: TframeCEFHost; + + procedure cefLoadEnd(Sender: TObject); + procedure cefBeforeBrowse(Sender: TObject; const Url: string; params: TStringList; wasHandled: Boolean); + procedure cefBeforeBrowseSync(Sender: TObject; const Url: string; out Handled: Boolean); + procedure WebCommand(Command: WideString; Params: TStringList); + function DoNavigate(URL: string): Boolean; + procedure RefreshHTML; + procedure RefreshCaption; + function ShouldHandleNavigation(URL: string): Boolean; + { Private declarations } + public + procedure SetFocus; override; + procedure StartClose; override; + end; + +var + frmWebTest: TfrmWebTest; + +implementation + +uses + UfrmMain, + UmodWebHttpServer; + +{$R *.dfm} + +{ TfrmWebTest } + +procedure TfrmWebTest.cefBeforeBrowse(Sender: TObject; const Url: string; + params: TStringList; wasHandled: Boolean); +begin + DoNavigate(Url); +end; + +procedure TfrmWebTest.cefBeforeBrowseSync(Sender: TObject; const Url: string; + out Handled: Boolean); +begin + Handled := ShouldHandleNavigation(Url); +end; + +function TfrmWebTest.ShouldHandleNavigation(URL: string): Boolean; +begin + Result := False; + if Copy(URL, 1, 7) = 'keyman:' then + begin + Result := True; + end + else if Copy(URL, 1, 5) = 'help:' then + begin + Result := True; + end + else if not URL.StartsWith(modWebHttpServer.GetLocalhostURL) and (Copy(URL, 1, 4) = 'http') then + begin + Result := True; + end +end; + +procedure TfrmWebTest.cefLoadEnd(Sender: TObject); +begin + if csDestroying in ComponentState then + Exit; + + if (frmKeymanDeveloper.ActiveChild = Self) and (Screen.ActiveForm = frmKeymanDeveloper) then + begin + cef.SetFocus; + end; +end; + +function TfrmWebTest.DoNavigate(URL: string): Boolean; +begin + // TODO' + Result := FALSE; +end; + +procedure TfrmWebTest.FormCreate(Sender: TObject); +begin + inherited; + cef := TframeCEFHost.Create(Self); + cef.Parent := Self; + cef.Visible := True; + cef.OnBeforeBrowse := cefBeforeBrowse; + cef.OnBeforeBrowseSync := cefBeforeBrowseSync; + cef.OnLoadEnd := cefLoadEnd; + RefreshHTML; +end; + +procedure TfrmWebTest.RefreshCaption; +begin + //TODO +end; + +procedure TfrmWebTest.RefreshHTML; +begin + cef.Navigate(modWebHttpServer.GetLocalhostURL); + RefreshCaption; +end; + +procedure TfrmWebTest.SetFocus; +begin + inherited; + cef.SetFocus; +end; + +procedure TfrmWebTest.StartClose; +begin + Visible := False; + cef.StartClose; +end; + +procedure TfrmWebTest.WebCommand(Command: WideString; Params: TStringList); +begin + // TODO +end; + +end. diff --git a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm index d83cf928bc..1c841b462b 100644 --- a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm +++ b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm @@ -177,7 +177,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Top = 0 Width = 1043 Height = 645 - ActivePage = pageDetails + ActivePage = pageCompile Align = alClient Font.Charset = ANSI_CHARSET Font.Color = clWindowText @@ -198,9 +198,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageDetails: TTabSheet Caption = 'Details' ImageIndex = 2 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object sbDetails: TScrollBox Left = 0 Top = 0 @@ -253,7 +250,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard BevelOuter = bvNone ParentColor = True TabOrder = 2 - ExplicitTop = 189 object lblWinLangTitle: TLabel Left = 12 Top = 5 @@ -318,7 +314,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard AutoSize = True BevelOuter = bvNone TabOrder = 5 - ExplicitTop = 569 object lblComments: TLabel Left = 12 Top = 3 @@ -350,7 +345,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard Align = alTop BevelOuter = bvNone TabOrder = 6 - ExplicitTop = 656 object Label9: TLabel AlignWithMargins = True Left = 12 @@ -424,7 +418,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard AutoSize = True BevelOuter = bvNone TabOrder = 4 - ExplicitTop = 442 object panMessage: TPanel Left = 0 Top = 42 @@ -598,7 +591,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard Align = alTop BevelOuter = bvNone TabOrder = 3 - ExplicitTop = 378 object lblISOLangTitle: TLabel Left = 12 Top = 3 @@ -729,9 +721,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayout: TTabSheet Caption = 'Layout' ImageIndex = 5 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object pagesLayout: TPageControl Left = 0 Top = 0 @@ -746,10 +735,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object panLayoutSimple: TPanel Left = 0 Top = 0 @@ -973,10 +958,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end end end @@ -1023,16 +1004,10 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageOnScreenKeyboard: TTabSheet Caption = 'On-Screen' ImageIndex = 7 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageTouchLayout: TTabSheet Caption = 'Touch Layout' ImageIndex = 16 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object pagesTouchLayout: TPageControl Left = 0 Top = 0 @@ -1047,55 +1022,32 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageTouchLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageTouchLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end end end object pageIncludeCodes: TTabSheet Caption = 'Char Codes' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWEmbedJS: TTabSheet Caption = 'Embedded JS' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWEmbedCSS: TTabSheet Caption = 'Embedded CSS' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWHelp: TTabSheet Caption = 'Embedded Help' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Build' ImageIndex = 1 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object Panel1: TPanel Left = 0 Top = 0 @@ -1233,7 +1185,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard end object cmdTestKeymanWeb: TButton Left = 12 - Top = 34 + Top = 29 Width = 150 Height = 25 Action = modActionsKeyboardEditor.actKeyboardTestKeymanWeb diff --git a/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas b/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas index 27d36a816c..532629c74c 100644 --- a/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas +++ b/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas @@ -54,13 +54,23 @@ type property Name: string read FName; end; + TWebDebugModelInfo = class + strict private + FFilename: string; + public + constructor Create(const AFilename: string); + property Filename: string read FFilename; + end; + TDebuggerHttpResponder = class(TBaseHttpResponder) private - FKeyboardsCS, FPackagesCS: TCriticalSection; // I4036 + FModelsCS, FKeyboardsCS, FPackagesCS: TCriticalSection; // I4036 + FModels: TObjectDictionary; FKeyboards: TObjectDictionary; // I4063 FPackages: TObjectDictionary; function GetKeyboardStoredFileName(const WebFilename: string): string; function GetPackageStoredFileName(const WebFilename: string): string; + function GetModelStoredFileName(const WebFilename: string): string; public constructor Create; destructor Destroy; override; @@ -68,6 +78,8 @@ type procedure UnregisterKeyboard(const Filename: string); procedure RegisterPackage(const Filename, Name: string); procedure UnregisterPackage(const Filename: string); + procedure RegisterModel(const Filename: string); + procedure UnregisterModel(const Filename: string); procedure ProcessRequest(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo); @@ -114,6 +126,9 @@ begin FPackagesCS := TCriticalSection.Create; FPackages := TObjectDictionary.Create; // I4063 + + FModelsCS := TCriticalSection.Create; + FModels := TObjectDictionary.Create; // I4063 end; destructor TDebuggerHttpResponder.Destroy; @@ -124,6 +139,9 @@ begin FreeAndNil(FPackages); FreeAndNil(FPackagesCS); // I4036 + FreeAndNil(FModels); + FreeAndNil(FModelsCS); // I4036 + inherited Destroy; end; @@ -139,6 +157,18 @@ begin end; end; +function TDebuggerHttpResponder.GetModelStoredFileName( + const WebFilename: string): string; +begin + FModelsCS.Enter; // I4036 + try + if FModels.ContainsKey(WebFilename) then Result := FModels[WebFilename].Filename + else Result := ''; + finally + FModelsCS.Leave; + end; +end; + function TDebuggerHttpResponder.GetPackageStoredFileName( const WebFilename: string): string; begin @@ -217,6 +247,7 @@ procedure TDebuggerHttpResponder.ProcessRequest(AContext: TIdContext; src: string; n: Integer; srcVersion: string; + model: string; begin // Get dynamic keyboard registration @@ -263,6 +294,23 @@ procedure TDebuggerHttpResponder.ProcessRequest(AContext: TIdContext; FKeyboardsCS.Leave; end; + FModelsCS.Enter; + try + for model in FModels.Keys do + begin + src := ChangeFileExt(model, ''); + response := response + Format( + ' keyman.modelManager.register({'#13#10+ + ' "id": "%s", '#13#10+ + ' "languages": ["en"],'#13#10+ + ' "path": "http://"+location.host+"/model/%s"'#13#10+ + ' });'#13#10, + [src, model]); + end; + finally + FModelsCS.Leave; + end; + response := response + '})();'; AResponseInfo.CharSet := 'UTF-8'; @@ -605,6 +653,26 @@ begin Exit; end; end + else if Copy(doc, 1, 6) = 'model/' then + begin + Delete(doc, 1, 6); + + // Models always expire immediately + AResponseInfo.Expires := EncodeDate(1990, 1, 1); // I4037 + AResponseInfo.CacheControl := 'no-cache, no-store'; // I4037 + AResponseInfo.LastModified := Now; // I4037 + if not FFileRegExp.Exec(doc) then + begin + Respond404; + Exit; + end; + doc := GetModelStoredFileName(doc); + if doc = '' then + begin + Respond404; + Exit; + end; + end else begin if not FFileRegExp.Exec(doc) then @@ -650,6 +718,19 @@ begin end; end; +procedure TDebuggerHttpResponder.RegisterModel(const Filename: string); +var + m: TWebDebugModelInfo; +begin + m := TWebDebugModelInfo.Create(Filename); + FModelsCS.Enter; + try + FModels.AddOrSetValue(ExtractFileName(Filename), m); + finally + FModelsCS.Leave; + end; +end; + procedure TDebuggerHttpResponder.RegisterPackage(const Filename, Name: string); var p: TWebDebugPackageInfo; @@ -673,6 +754,16 @@ begin end; end; +procedure TDebuggerHttpResponder.UnregisterModel(const Filename: string); +begin + FModelsCS.Enter; // I4036 + try + FModels.Remove(ExtractFileName(Filename)); + finally + FModelsCS.Leave; + end; +end; + procedure TDebuggerHttpResponder.UnregisterPackage(const Filename: string); begin FPackagesCS.Enter; // I4036 @@ -798,4 +889,12 @@ begin FName := AName; end; +{ TWebDebugModelInfo } + +constructor TWebDebugModelInfo.Create(const AFilename: string); +begin + inherited Create; + FFilename := AFilename; +end; + end. diff --git a/windows/src/developer/TIKE/main/UfrmMain.dfm b/windows/src/developer/TIKE/main/UfrmMain.dfm index 80a7409c84..97bf472556 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.dfm +++ b/windows/src/developer/TIKE/main/UfrmMain.dfm @@ -3085,6 +3085,10 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Caption = 'Crash Test' OnClick = crash1Click end + object CreateWebTestWindow1: TMenuItem + Caption = 'Create Web Test Window' + OnClick = CreateWebTestWindow1Click + end end end object Help1: TMenuItem diff --git a/windows/src/developer/TIKE/main/UfrmMain.pas b/windows/src/developer/TIKE/main/UfrmMain.pas index 0d2003e0de..2235ff3ddd 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.pas +++ b/windows/src/developer/TIKE/main/UfrmMain.pas @@ -97,6 +97,7 @@ uses mrulist, UfrmUnicodeDataStatus, CharacterDragObject, + Keyman.Developer.UI.dmActionsModelEditor, dmActionsMain, UnicodeData, UserMessages, webhelp, dmActionsKeyboardEditor, Dialogs, UfrmTike, AppEvnts, DropTarget, @@ -269,6 +270,7 @@ type DebugTests1: TMenuItem; CrashTest1: TMenuItem; CloseProject1: TMenuItem; + CreateWebTestWindow1: TMenuItem; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure mnuFileClick(Sender: TObject); @@ -288,6 +290,7 @@ type procedure pagesChange(Sender: TObject); procedure pagesCloseTab(Sender: TObject; Index: Integer); procedure ools1Click(Sender: TObject); + procedure CreateWebTestWindow1Click(Sender: TObject); private AppStorage: TJvAppRegistryStorage; @@ -434,6 +437,7 @@ uses Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.UfrmWordlistEditor, Keyman.Developer.UI.UfrmModelEditor, + Keyman.Developer.UI.UfrmWebTest, TextFileFormat, RedistFiles, ErrorControlledRegistry, @@ -481,6 +485,7 @@ begin modActionsTextEditor := TmodActionsTextEditor.Create(Self); modActionsKeyboardEditor := TmodActionsKeyboardEditor.Create(Self); modActionsMain := TmodActionsMain.Create(Self); + modActionsModelEditor := TmodActionsModelEditor.Create(Self); FProjectMRU := TMRUList.Create('Project'); FProjectMRU.OnChange := ProjectMRUChange; @@ -551,6 +556,12 @@ begin UpdateCaption; end; +procedure TfrmKeymanDeveloper.CreateWebTestWindow1Click(Sender: TObject); +begin + inherited; + ShowChild(TfrmWebTest.Create(Self)); +end; + procedure TfrmKeymanDeveloper.CreateWnd; begin inherited; diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas index b288af843e..961eca8e35 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas @@ -37,6 +37,8 @@ type function GetDebug: Boolean; procedure SetDebug(const Value: Boolean); function CompileModel(FSilent: Boolean): Boolean; + function TestKeymanWeb(FSilent: Boolean): Boolean; + function TestModelState(FCompiledName: string; FSilent: Boolean): Boolean; public function DoAction(action: TProjectFileAction; FSilent: Boolean): Boolean; override; property Debug: Boolean read GetDebug write SetDebug; @@ -52,9 +54,10 @@ uses Vcl.Graphics, Vcl.Controls, dmActionsMain, + UmodWebHttpServer, UfrmMain, UfrmMDIEditor, - KeyboardFonts, + Keyman.Developer.UI.UfrmModelEditor, KeymanDeveloperUtils, KeymanDeveloperOptions, System.Classes, @@ -64,8 +67,9 @@ uses function TmodelTsProjectFileUI.DoAction(action: TProjectFileAction; FSilent: Boolean): Boolean; begin case action of - pfaCompile: Result := CompileModel(FSilent); - pfaClean: Result := ProjectFile.Clean; + pfaCompile: Result := CompileModel(FSilent); + pfaClean: Result := ProjectFile.Clean; + pfaTestKeymanWeb: Result := TestKeymanWeb(FSilent); else Result := False; end; @@ -108,6 +112,66 @@ begin ProjectFile.Debug := Value; end; +function TmodelTsProjectFileUI.TestKeymanWeb(FSilent: Boolean): Boolean; +var + FCompiledName: string; + editor: TfrmTikeEditor; + wizard: TfrmModelEditor; +begin + editor := frmKeymanDeveloper.FindEditorByFileName(ProjectFile.FileName); // I4021 + if not Assigned(editor) or not (editor is TfrmModelEditor) then + Exit(False); + wizard := editor as TfrmModelEditor; + + FCompiledName := ProjectFile.TargetFilename; + if not TestModelState(FCompiledName, FSilent) then + Exit(False); + + modWebHttpServer.Debugger.RegisterModel(FCompiledName); + + wizard.NotifyStartedWebDebug; // I4021 + + Result := True; +end; + +function TmodelTsProjectFileUI.TestModelState(FCompiledName: string; + FSilent: Boolean): Boolean; +var + ftts, ftjs: TDateTime; +begin + Result := False; + + if not FileExists(FCompiledName) then + if FSilent then + begin + if not CompileModel(FSilent) then Exit; + end + else + case MessageDlg('You need to compile the model before you can continue. Compile now?', + mtConfirmation, mbOkCancel, 0) of + mrOk: if not CompileModel(FSilent) then Exit; + mrCancel: Exit; + end; + + FileAge(ProjectFile.FileName, ftts); + FileAge(FCompiledName, ftjs); + + if ProjectFile.Modified or (ftts > ftjs) then + if FSilent then + begin + if not CompileModel(FSilent) then Exit; + end + else + case MessageDlg('The source file has changed. Recompile before continuing?', + mtConfirmation, mbYesNoCancel, 0) of + mrYes: if not CompileModel(FSilent) then Exit; + mrNo: ; + mrCancel: Exit; + end; + + Result := True; +end; + initialization RegisterProjectFileUIType(TmodelTsProjectFileAction, TmodelTsProjectFileUI); end. From 7f4c7aadc8edcda815f65f25de3e74bca1437314 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 30 Nov 2019 07:04:23 +0000 Subject: [PATCH 04/16] chore: remove BuildLog.htm --- windows/src/developer/kmcmpdll/BuildLog.htm | Bin 10106 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 windows/src/developer/kmcmpdll/BuildLog.htm diff --git a/windows/src/developer/kmcmpdll/BuildLog.htm b/windows/src/developer/kmcmpdll/BuildLog.htm deleted file mode 100644 index 6559186b402dfe948ff89e1c4114a42c1727a7ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10106 zcmeI2eNP)l5XSfKO8E{aSB(Q=l&+hysa>b@CTEIf!+7Rf z{)?tyo{{std1JmaN9L7DnXKuU3#ioX`x$kvOqW!OHq}s>YOCG{-kEF8>$cu0TzjTZ z=>V#iyc=+OZkB1|%ceGA=_=5#!OySJg+~24HGpP?zSZII-kec$0EJgjo#*!jxfRp2 zGPa=8qvUgXvu&T8c>$f5wCM5g*)M5DUyWg}Jif}buhUY}pYd){qtDlGHup=jIFx3S zKAlloqkNb4&7rB*RJvA98l??b6u2p}FS{3q}=67>!b-8F3 zY~LM!H*+$KgF|2Pal8$Q(zl*45}lr`|ePj;_l^5kuibHMu#Y*gCX z-6KjipwlH+^vk4WKdOAS_%703d`h%C1naVF?)RM8x@eZnDp*}HUt>wrmQ&YSX3ct8 z!>EmRNch>}A!GI}N;24bk9q$qbD%@3btnZ>9!QDSsM`LWBi+>yZzUv2BU_2y_K?4X zo@93`!=4C<)sNCmL!-o_w`QC4CoAZ*7?o-(BKI+J?xB^1F+S0nC+It8HW`nd1a-4$ zY7hPIGd8&d^^X|qRdjIz{Q?{g7-R91_5Q%K%rl>WP96!f@QwC+2c2tFa(L}NTGjUf zSjmDbedqbs{Lf%tx}nz6>&V_^BDOM3?JB5=^4n${gLzRZ!7|6SHc|a!#!T&8Pkyp! z%g~1cx#AVJ=xOR{#>l4qV&CKQQ;|u~61J>eSEP`lbqlJBQs$O3pt7b}* zM`>YQKCQ=?*6F9dVo>-I_A&}(_c`?X3%uvV~Wc2;R2#wsZSO>UJOf)xQ}>3@!y zQ{-LXd!L*fv*D0=l_!4>oXCG3Qsczd)eMz|RlO5RHS6clsE0*Zvbq&r&6IHpe$613 zg}rg~IphdIZLFK(kThTNxo7wQ*{neb{nO%9R&<0@7fGgK=R*>SD4(o!Q3<@ zXUMEIhT18%ZrONaRB{PoRHi6ol-snr4d*FFyRi7`Ac=gEPu+PhNz#na(R%M4W7MT> zm|EYio+W!YUzF>yd1ZXbAX0Kko>tN$S+oMLpoJ(^mn%;kEO}%-Q*cbfvBEeK6(PP2 zp&*_{GzOT&<^|=M)*8DA;vCI5&i4~J+-#FH3-*{7yF5EQO6Txx@{@YYXX;DVNc%ZC z`#hY;A!>b>?!fwXETY#Kqj8)i!HaaLXiU2U4OTWM(33@QHc_QfKku-58^7chc3Nb% zk@tR4dGlEL8ua7Yhl96Gd}%QdslJH8o7Sp>#OvqRSOTq<6t(AR8I1otJbDhH zekhS(G+YQ(nc~hJ%NK-KZa3iNWq{6%)5Em#>VswJ_@XzG-?V6-(-`? z`pGXx_X#8^1qcwf&TK?hM(Dai6rG zuigCciXbSB_KZC4T)A`tCiUC~Bo+q&%`epxF= zoQ&ewqlG)S^iRRlwDMSIX!l6{_Gw9arv*u+Ju~eLiSI6TF3_{yLCxpTrrQ(uZzYWD zcQj6M68hTuKWOpOuwV|imtbQ6GBx@$tCjqgOxS7#@3Oc(65Y_zzJTr(m4|c8-w{Y% z-ziwbPw0G9CzZFKx+mM+9?7wbEzk^hH^tn3P1t^V+|=^eIq|G*awn5dkoKv)TcdpS zKyjqA2CmG Date: Sat, 30 Nov 2019 12:39:39 +0000 Subject: [PATCH 05/16] feat(developer): complete lexical model editor --- windows/src/developer/TIKE/Tike.dpr | 1 - windows/src/developer/TIKE/Tike.dproj | 4 - .../Keyman.Developer.UI.UfrmModelEditor.dfm | 21 ++- .../Keyman.Developer.UI.UfrmModelEditor.pas | 22 +++ .../child/Keyman.Developer.UI.UfrmWebTest.dfm | 12 -- .../child/Keyman.Developer.UI.UfrmWebTest.pas | 129 ------------------ .../TIKE/child/UfrmPackageEditor.dfm | 5 +- windows/src/developer/TIKE/main/UfrmMain.dfm | 4 - windows/src/developer/TIKE/main/UfrmMain.pas | 9 -- ...oper.System.Project.modelTsProjectFile.pas | 10 +- ...eloper.UI.Project.modelTsProjectFileUI.pas | 9 ++ 11 files changed, 60 insertions(+), 166 deletions(-) delete mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm delete mode 100644 windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas diff --git a/windows/src/developer/TIKE/Tike.dpr b/windows/src/developer/TIKE/Tike.dpr index 25daf9b995..2bb6b6ec9b 100644 --- a/windows/src/developer/TIKE/Tike.dpr +++ b/windows/src/developer/TIKE/Tike.dpr @@ -291,7 +291,6 @@ uses Keyman.Developer.System.LexicalModelParser in 'main\Keyman.Developer.System.LexicalModelParser.pas', Keyman.Developer.System.LexicalModelParserTypes in 'main\Keyman.Developer.System.LexicalModelParserTypes.pas', Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}, - Keyman.Developer.UI.UfrmWebTest in 'child\Keyman.Developer.UI.UfrmWebTest.pas' {frmWebTest}, Keyman.Developer.UI.dmActionsModelEditor in 'actions\Keyman.Developer.UI.dmActionsModelEditor.pas' {modActionsModelEditor: TDataModule}; {$R *.RES} diff --git a/windows/src/developer/TIKE/Tike.dproj b/windows/src/developer/TIKE/Tike.dproj index cfbeb6a3ca..b243fc69fb 100644 --- a/windows/src/developer/TIKE/Tike.dproj +++ b/windows/src/developer/TIKE/Tike.dproj @@ -546,10 +546,6 @@
frmWordlistEditor
dfm
- -
frmWebTest
- dfm -
modActionsModelEditor
dfm diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm index 4dca586eee..a9fbaca059 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm @@ -249,6 +249,13 @@ inherited frmModelEditor: TfrmModelEditor 'The keyboard must be compiled in order to distribute or install ' + 'it' end + object Label5: TLabel + Left = 10 + Top = 79 + Width = 82 + Height = 13 + Caption = 'Target filename:' + end object cmdCompile: TButton Left = 10 Top = 40 @@ -276,7 +283,7 @@ inherited frmModelEditor: TfrmModelEditor end object panBuildLexicalModel: TPanel Left = 10 - Top = 96 + Top = 112 Width = 424 Height = 353 BevelOuter = bvNone @@ -361,6 +368,7 @@ inherited frmModelEditor: TfrmModelEditor Width = 158 Height = 21 TabOrder = 0 + OnChange = editTestKeyboardChange end object cmdBrowseTestKeyboard: TButton Left = 287 @@ -369,8 +377,19 @@ inherited frmModelEditor: TfrmModelEditor Height = 25 Caption = '&Browse...' TabOrder = 1 + OnClick = cmdBrowseTestKeyboardClick end end + object editOutPath: TEdit + Left = 98 + Top = 76 + Width = 383 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 4 + end end end end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas index b600a8d9ca..2e0084f54c 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas @@ -71,6 +71,8 @@ type lblReadOnly: TLabel; dlgAddWordlist: TOpenDialog; dlgBrowseTestKeyboard: TOpenDialog; + Label5: TLabel; + editOutPath: TEdit; procedure FormDestroy(Sender: TObject); procedure cmdAddWordlistClick(Sender: TObject); procedure cmdRemoveWordlistClick(Sender: TObject); @@ -83,6 +85,8 @@ type procedure cmdOpenContainingFolder2Click(Sender: TObject); procedure cmdOpenDebugHostClick(Sender: TObject); procedure cmdSendURLsToEmailClick(Sender: TObject); + procedure cmdBrowseTestKeyboardClick(Sender: TObject); + procedure editTestKeyboardChange(Sender: TObject); private type TWordlist = class @@ -191,9 +195,18 @@ begin model.Text := parser.Text; model.SaveToFile(FileName); + editOutPath.Text := (ProjectFile as TmodelTsProjectFile).TargetFilename; // I4688 + Result := True; end; +procedure TfrmModelEditor.editTestKeyboardChange(Sender: TObject); +begin + if FSetup > 0 then + Exit; + (ProjectFile as TmodelTsProjectFile).TestKeyboard := editTestKeyboard.Text; +end; + procedure TfrmModelEditor.EnableControls; var e: Boolean; @@ -391,6 +404,9 @@ begin end; end; + editOutPath.Text := (ProjectFile as TmodelTsProjectFile).TargetFilename; // I4688 + editTestKeyboard.Text := (ProjectFile as TmodelTsProjectFile).TestKeyboard; + EnableControls; end; @@ -486,6 +502,12 @@ begin end; end; +procedure TfrmModelEditor.cmdBrowseTestKeyboardClick(Sender: TObject); +begin + if dlgBrowseTestKeyboard.Execute then + editTestKeyboard.Text := dlgBrowseTestKeyboard.FileName; +end; + procedure TfrmModelEditor.cmdOpenContainingFolder2Click(Sender: TObject); begin OpenContainingFolder(FileName); diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm deleted file mode 100644 index 2266feec51..0000000000 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.dfm +++ /dev/null @@ -1,12 +0,0 @@ -inherited frmWebTest: TfrmWebTest - Left = 0 - Top = 0 - Caption = 'Web Test' - ClientHeight = 294 - ClientWidth = 563 - Font.Name = 'Tahoma' - ExplicitWidth = 563 - ExplicitHeight = 294 - PixelsPerInch = 96 - TextHeight = 13 -end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas deleted file mode 100644 index 2f898c5dc5..0000000000 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWebTest.pas +++ /dev/null @@ -1,129 +0,0 @@ -unit Keyman.Developer.UI.UfrmWebTest; - -interface - -uses - Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, - Vcl.Controls, Vcl.Forms, Vcl.Dialogs, UfrmMDIChild, - Keyman.Developer.UI.UframeCEFHost; - -type - TfrmWebTest = class(TfrmTikeChild) - procedure FormCreate(Sender: TObject); - private - cef: TframeCEFHost; - - procedure cefLoadEnd(Sender: TObject); - procedure cefBeforeBrowse(Sender: TObject; const Url: string; params: TStringList; wasHandled: Boolean); - procedure cefBeforeBrowseSync(Sender: TObject; const Url: string; out Handled: Boolean); - procedure WebCommand(Command: WideString; Params: TStringList); - function DoNavigate(URL: string): Boolean; - procedure RefreshHTML; - procedure RefreshCaption; - function ShouldHandleNavigation(URL: string): Boolean; - { Private declarations } - public - procedure SetFocus; override; - procedure StartClose; override; - end; - -var - frmWebTest: TfrmWebTest; - -implementation - -uses - UfrmMain, - UmodWebHttpServer; - -{$R *.dfm} - -{ TfrmWebTest } - -procedure TfrmWebTest.cefBeforeBrowse(Sender: TObject; const Url: string; - params: TStringList; wasHandled: Boolean); -begin - DoNavigate(Url); -end; - -procedure TfrmWebTest.cefBeforeBrowseSync(Sender: TObject; const Url: string; - out Handled: Boolean); -begin - Handled := ShouldHandleNavigation(Url); -end; - -function TfrmWebTest.ShouldHandleNavigation(URL: string): Boolean; -begin - Result := False; - if Copy(URL, 1, 7) = 'keyman:' then - begin - Result := True; - end - else if Copy(URL, 1, 5) = 'help:' then - begin - Result := True; - end - else if not URL.StartsWith(modWebHttpServer.GetLocalhostURL) and (Copy(URL, 1, 4) = 'http') then - begin - Result := True; - end -end; - -procedure TfrmWebTest.cefLoadEnd(Sender: TObject); -begin - if csDestroying in ComponentState then - Exit; - - if (frmKeymanDeveloper.ActiveChild = Self) and (Screen.ActiveForm = frmKeymanDeveloper) then - begin - cef.SetFocus; - end; -end; - -function TfrmWebTest.DoNavigate(URL: string): Boolean; -begin - // TODO' - Result := FALSE; -end; - -procedure TfrmWebTest.FormCreate(Sender: TObject); -begin - inherited; - cef := TframeCEFHost.Create(Self); - cef.Parent := Self; - cef.Visible := True; - cef.OnBeforeBrowse := cefBeforeBrowse; - cef.OnBeforeBrowseSync := cefBeforeBrowseSync; - cef.OnLoadEnd := cefLoadEnd; - RefreshHTML; -end; - -procedure TfrmWebTest.RefreshCaption; -begin - //TODO -end; - -procedure TfrmWebTest.RefreshHTML; -begin - cef.Navigate(modWebHttpServer.GetLocalhostURL); - RefreshCaption; -end; - -procedure TfrmWebTest.SetFocus; -begin - inherited; - cef.SetFocus; -end; - -procedure TfrmWebTest.StartClose; -begin - Visible := False; - cef.StartClose; -end; - -procedure TfrmWebTest.WebCommand(Command: WideString; Params: TStringList); -begin - // TODO -end; - -end. diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm index e6b404b46c..d5a9e357f5 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm @@ -183,7 +183,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 0 Width = 965 Height = 622 - ActivePage = pageFiles + ActivePage = pageCompile Align = alClient Images = modActionsMain.ilEditorPages MultiLine = True @@ -1119,9 +1119,6 @@ inherited frmPackageEditor: TfrmPackageEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Compile' diff --git a/windows/src/developer/TIKE/main/UfrmMain.dfm b/windows/src/developer/TIKE/main/UfrmMain.dfm index 97bf472556..80a7409c84 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.dfm +++ b/windows/src/developer/TIKE/main/UfrmMain.dfm @@ -3085,10 +3085,6 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Caption = 'Crash Test' OnClick = crash1Click end - object CreateWebTestWindow1: TMenuItem - Caption = 'Create Web Test Window' - OnClick = CreateWebTestWindow1Click - end end end object Help1: TMenuItem diff --git a/windows/src/developer/TIKE/main/UfrmMain.pas b/windows/src/developer/TIKE/main/UfrmMain.pas index 2235ff3ddd..a3c5be204a 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.pas +++ b/windows/src/developer/TIKE/main/UfrmMain.pas @@ -270,7 +270,6 @@ type DebugTests1: TMenuItem; CrashTest1: TMenuItem; CloseProject1: TMenuItem; - CreateWebTestWindow1: TMenuItem; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure mnuFileClick(Sender: TObject); @@ -290,7 +289,6 @@ type procedure pagesChange(Sender: TObject); procedure pagesCloseTab(Sender: TObject; Index: Integer); procedure ools1Click(Sender: TObject); - procedure CreateWebTestWindow1Click(Sender: TObject); private AppStorage: TJvAppRegistryStorage; @@ -437,7 +435,6 @@ uses Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.UfrmWordlistEditor, Keyman.Developer.UI.UfrmModelEditor, - Keyman.Developer.UI.UfrmWebTest, TextFileFormat, RedistFiles, ErrorControlledRegistry, @@ -556,12 +553,6 @@ begin UpdateCaption; end; -procedure TfrmKeymanDeveloper.CreateWebTestWindow1Click(Sender: TObject); -begin - inherited; - ShowChild(TfrmWebTest.Create(Self)); -end; - procedure TfrmKeymanDeveloper.CreateWnd; begin inherited; diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas b/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas index fc011d57a0..822f920ee6 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas @@ -34,7 +34,8 @@ type TmodelTsProjectFile = class(TOpenableProjectFile) private FDebug: Boolean; - FWarnAsError: Boolean; // I4706 + FWarnAsError: Boolean; + FTestKeyboard: string; // I4706 function GetTargetFilename: string; protected @@ -49,10 +50,10 @@ type procedure SaveState(node: IXMLNode); override; // I4698 property Debug: Boolean read FDebug write FDebug; + property TestKeyboard: string read FTestKeyboard write FTestKeyboard; property WarnAsError: Boolean read FWarnAsError write FWarnAsError; // I4706 -// property OutputFilename: string read GetOutputFilename; property TargetFilename: string read GetTargetFilename; end; @@ -80,6 +81,7 @@ procedure TmodelTsProjectFile.SaveState(node: IXMLNode); // I4698 begin inherited SaveState(node); node.AddChild('Debug').NodeValue := FDebug; + node.AddChild('TestKeyboard').NodeValue := FTestKeyboard; end; procedure TmodelTsProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 @@ -95,8 +97,12 @@ begin inherited LoadState(node); try if node.ChildNodes.IndexOf('Debug') >= 0 then FDebug := node.ChildValues['Debug']; + if node.ChildNodes.IndexOf('TestKeyboard') >= 0 + then FTestKeyboard := node.ChildValues['TestKeyboard'] + else FTestKeyboard := ''; except FDebug := False; + FTestKeyboard := ''; end; end; diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas index 961eca8e35..937ed59e42 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas @@ -53,7 +53,9 @@ uses Vcl.Dialogs, Vcl.Graphics, Vcl.Controls, + dmActionsMain, + KeyboardFonts, UmodWebHttpServer, UfrmMain, UfrmMDIEditor, @@ -117,6 +119,8 @@ var FCompiledName: string; editor: TfrmTikeEditor; wizard: TfrmModelEditor; + FontNames: TKeyboardFontArray; + i: TKeyboardFont; begin editor := frmKeymanDeveloper.FindEditorByFileName(ProjectFile.FileName); // I4021 if not Assigned(editor) or not (editor is TfrmModelEditor) then @@ -127,6 +131,11 @@ begin if not TestModelState(FCompiledName, FSilent) then Exit(False); + for i := Low(TKeyboardFont) to High(TKeyboardFont) do + FontNames[i] := ''; + + if FileExists(ProjectFile.TestKeyboard) then + modWebHttpServer.Debugger.RegisterKeyboard(ProjectFile.TestKeyboard, '1.0', FontNames); modWebHttpServer.Debugger.RegisterModel(FCompiledName); wizard.NotifyStartedWebDebug; // I4021 From e84e0e8ed55745eeb046c5f554ff99495e94bd89 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 30 Nov 2019 12:42:33 +0000 Subject: [PATCH 06/16] chore: restore unchanged forms --- .../developer/TIKE/child/UfrmKeymanWizard.dfm | 52 ++++++++++++++++++- .../TIKE/child/UfrmPackageEditor.dfm | 2 +- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm index 1c841b462b..d83cf928bc 100644 --- a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm +++ b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm @@ -177,7 +177,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Top = 0 Width = 1043 Height = 645 - ActivePage = pageCompile + ActivePage = pageDetails Align = alClient Font.Charset = ANSI_CHARSET Font.Color = clWindowText @@ -198,6 +198,9 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageDetails: TTabSheet Caption = 'Details' ImageIndex = 2 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 object sbDetails: TScrollBox Left = 0 Top = 0 @@ -250,6 +253,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard BevelOuter = bvNone ParentColor = True TabOrder = 2 + ExplicitTop = 189 object lblWinLangTitle: TLabel Left = 12 Top = 5 @@ -314,6 +318,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard AutoSize = True BevelOuter = bvNone TabOrder = 5 + ExplicitTop = 569 object lblComments: TLabel Left = 12 Top = 3 @@ -345,6 +350,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Align = alTop BevelOuter = bvNone TabOrder = 6 + ExplicitTop = 656 object Label9: TLabel AlignWithMargins = True Left = 12 @@ -418,6 +424,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard AutoSize = True BevelOuter = bvNone TabOrder = 4 + ExplicitTop = 442 object panMessage: TPanel Left = 0 Top = 42 @@ -591,6 +598,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Align = alTop BevelOuter = bvNone TabOrder = 3 + ExplicitTop = 378 object lblISOLangTitle: TLabel Left = 12 Top = 3 @@ -721,6 +729,9 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayout: TTabSheet Caption = 'Layout' ImageIndex = 5 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 object pagesLayout: TPageControl Left = 0 Top = 0 @@ -735,6 +746,10 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 object panLayoutSimple: TPanel Left = 0 Top = 0 @@ -958,6 +973,10 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end end end @@ -1004,10 +1023,16 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageOnScreenKeyboard: TTabSheet Caption = 'On-Screen' ImageIndex = 7 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageTouchLayout: TTabSheet Caption = 'Touch Layout' ImageIndex = 16 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 object pagesTouchLayout: TPageControl Left = 0 Top = 0 @@ -1022,32 +1047,55 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageTouchLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageTouchLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end end end object pageIncludeCodes: TTabSheet Caption = 'Char Codes' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageKMWEmbedJS: TTabSheet Caption = 'Embedded JS' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageKMWEmbedCSS: TTabSheet Caption = 'Embedded CSS' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageKMWHelp: TTabSheet Caption = 'Embedded Help' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Build' ImageIndex = 1 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 object Panel1: TPanel Left = 0 Top = 0 @@ -1185,7 +1233,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard end object cmdTestKeymanWeb: TButton Left = 12 - Top = 29 + Top = 34 Width = 150 Height = 25 Action = modActionsKeyboardEditor.actKeyboardTestKeymanWeb diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm index d5a9e357f5..e2561dce1f 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm @@ -183,7 +183,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 0 Width = 965 Height = 622 - ActivePage = pageCompile + ActivePage = pageLexicalModels Align = alClient Images = modActionsMain.ilEditorPages MultiLine = True From 469bf1d763ac1d4e71029f1bb06523003aba9fcf Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 30 Nov 2019 12:44:34 +0000 Subject: [PATCH 07/16] chore: restore unchanged forms --- windows/src/developer/TIKE/main/UframeTextEditor.dfm | 4 ++-- windows/src/developer/TIKE/main/UframeTextEditor.pas | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/windows/src/developer/TIKE/main/UframeTextEditor.dfm b/windows/src/developer/TIKE/main/UframeTextEditor.dfm index 410ad98d1f..ec21b23c6e 100644 --- a/windows/src/developer/TIKE/main/UframeTextEditor.dfm +++ b/windows/src/developer/TIKE/main/UframeTextEditor.dfm @@ -17,7 +17,7 @@ inherited frameTextEditor: TframeTextEditor Left = 394 Top = 58 Bitmap = { - 494C010106000900040010001000C0C0C000FE00FFFFFFFFFFFFFFFF424D3600 + 494C010106000900880010001000C0C0C000FE00FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000400000002000000001002000000000000020 000000000000000000000000000000000000C6C6C600C6C6C600C6C6C600C6C6 C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6C600C6C6 @@ -295,7 +295,7 @@ inherited frameTextEditor: TframeTextEditor Left = 618 Top = 26 Bitmap = { - 494C010106000900040010001000FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 + 494C010106000900880010001000FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000400000002000000001002000000000000020 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 diff --git a/windows/src/developer/TIKE/main/UframeTextEditor.pas b/windows/src/developer/TIKE/main/UframeTextEditor.pas index 7bf3cbc9d3..fca4d88760 100644 --- a/windows/src/developer/TIKE/main/UframeTextEditor.pas +++ b/windows/src/developer/TIKE/main/UframeTextEditor.pas @@ -34,7 +34,7 @@ uses type TEditorBreakpointClickedEvent = procedure(Sender: TObject; ALine: Integer) of object; - TframeTextEditor = class(TTikeForm, IKMDEditActions, IKMDSearchActions, IKMDTextEditorActions) + TframeTextEditor = class(TTIKEForm, IKMDEditActions, IKMDSearchActions, IKMDTextEditorActions) lstImages: TMenuImgList; dlgFonts: TFontDialog; dlgPrintSetup: TPrinterSetupDialog; From a5a9a8f35decea05aed2fe4fe5b3933abb277946 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 3 Dec 2019 07:48:53 +0000 Subject: [PATCH 08/16] feat(developer): debugger menu to switch models Fixes #2402. This adds a menu to switch models in the web debugger. It is likely that most users will have only a single model loaded but we need this to complete the circle. It also gives a little more visibility in the model debugging process to validate that you are testing the expected model. --- ...n.Developer.System.HttpServer.Debugger.pas | 12 +- windows/src/developer/TIKE/xml/kmw/index.html | 70 ++++++++++- windows/src/developer/TIKE/xml/kmw/test.css | 118 +++++++++++------- 3 files changed, 140 insertions(+), 60 deletions(-) diff --git a/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas b/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas index 532629c74c..9ad35f5bc5 100644 --- a/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas +++ b/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas @@ -244,7 +244,7 @@ procedure TDebuggerHttpResponder.ProcessRequest(AContext: TIdContext; key: string; response: string; value: TWebDebugKeyboardInfo; - src: string; + id, src: string; n: Integer; srcVersion: string; model: string; @@ -298,14 +298,8 @@ procedure TDebuggerHttpResponder.ProcessRequest(AContext: TIdContext; try for model in FModels.Keys do begin - src := ChangeFileExt(model, ''); - response := response + Format( - ' keyman.modelManager.register({'#13#10+ - ' "id": "%s", '#13#10+ - ' "languages": ["en"],'#13#10+ - ' "path": "http://"+location.host+"/model/%s"'#13#10+ - ' });'#13#10, - [src, model]); + id := ChangeFileExt(model, ''); + response := response + Format('registerModel("%s", "%s");', [id, model]); end; finally FModelsCS.Leave; diff --git a/windows/src/developer/TIKE/xml/kmw/index.html b/windows/src/developer/TIKE/xml/kmw/index.html index ec5007592b..7d48be0920 100644 --- a/windows/src/developer/TIKE/xml/kmw/index.html +++ b/windows/src/developer/TIKE/xml/kmw/index.html @@ -59,6 +59,7 @@ .touch-device #install-link, .touch-device #install-link-packages, + .touch-device #model-link, .touch-device-ios #install-link-ios, .touch-device-android #install-link-android { display: inline-block; @@ -132,10 +133,6 @@ - - - - @@ -156,7 +153,15 @@

Keyboard Test

- +
+ Models +
+
+ No models are currently loaded. +
+
+ Install packages +
@@ -322,7 +327,62 @@ //document.getElementById('ta1').focus(); keyman.moveToElement('ta1'); }, 10); + + document.getElementById('model-link').addEventListener('click', modelLinkClick); } + + /* Lexical models */ + + var firstModel = true; + + /** + * Register a model for debugging. Called by keyboards.js. The + * first model registered will be activated automatically. + */ + function registerModel(model, src) { + var list = document.getElementById('model-list-inner'); + var a = document.createElement('a'); + a.href = '#'; + a.innerText = model; + a.addEventListener('click', function(ev) { + ev.returnValue = false; + + var lastModel = keyman.modelManager.activeModel; + if(lastModel) + keyman.modelManager.deregister(lastModel.id); + keyman.modelManager.register({ + id: model, + languages: ['en'], + path: 'http://'+location.host+'/model/'+src + }); + }); + list.appendChild(a); + + if(firstModel) { + keyman.modelManager.register({ + id: model, + languages: ['en'], + path: 'http://'+location.host+'/model/'+src + }); + document.getElementById('model-list-empty').style.display = 'none'; + firstModel = false; + } + } + + /** + * Toggle visibility of the model menu + */ + function modelLinkClick(e) { + var elem = document.getElementById('model-list'); + elem.className = elem.className == '' ? 'model-list-visible' : ''; + e.returnValue = false; + return false; + } + + + + + diff --git a/windows/src/developer/TIKE/xml/kmw/test.css b/windows/src/developer/TIKE/xml/kmw/test.css index 7feae12984..47c98987cf 100644 --- a/windows/src/developer/TIKE/xml/kmw/test.css +++ b/windows/src/developer/TIKE/xml/kmw/test.css @@ -1,55 +1,81 @@ - html, body { - margin: 0; - padding: 0; - } +html, body { + margin: 0; + padding: 0; +} - h2 { - font-family: sans-serif - } +h2 { + font-family: sans-serif +} - .install-link { - background: none repeat scroll 0 0 #CCCCCC; - border: 1px solid #444444; - border-radius: 4px; - box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); - display: inline-block; - color: #444444; - font-family: Sans-serif; - font-size: 10pt; - padding: 6px; - margin: 12px 2px 0 2px; - text-decoration: none; - } +.install-link { + background: none repeat scroll 0 0 #CCCCCC; + border: 1px solid #444444; + border-radius: 4px; + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); + display: inline-block; + color: #444444; + font-family: Sans-serif; + font-size: 10pt; + padding: 6px; + margin: 12px 2px 0 2px; + text-decoration: none; +} - .header { - padding: 4px 8px; - background: #424248; - color: #f0f0ff; - } +.header { + padding: 4px 8px; + background: #424248; + color: #f0f0ff; +} - .header div { - display: inline-block; - } +.header div { + display: inline-block; +} - .header div h2 { - margin: 4px; - } +.header div h2 { + margin: 4px; +} - .header div.float-right { - display: block; - position: absolute; - right: 0; - top: 0; - } +.header div.float-right { + display: block; + position: absolute; + right: 0; + top: 0; +} - .float-right a.install-link { - margin: 8px 8px 0 0 ; - } +.float-right a.install-link { + margin: 8px 8px 0 0 ; +} - .clear { - clear: both; - } +.clear { + clear: both; +} - #content { - margin: 8px 16px; - } +#content { + margin: 8px 16px; +} + +/* Lexical model menu */ + +.model-list-visible#model-list { + display: block; +} + +#model-list { + display: none; + position: absolute; + z-index: 3; + margin-top: 4px; + padding: 5px 5px 5px 5px; + background: #424248; + border-left: solid 1px #442244; + border-right: solid 1px #442244; + border-bottom: solid 1px #442244; + border-radius: 0 0 4px 4px; + box-shadow: 4px 4px 4px rgba(64,64,64,0.5); +} + +#model-list a { + display: block; + color: white; + padding: 6px; +} \ No newline at end of file From c6d4d58f2d51effdb42902cdce23c2e741abb44d Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 4 Dec 2019 19:00:59 +0100 Subject: [PATCH 09/16] fix: package editor didn't debug lexical models --- windows/src/developer/TIKE/child/UfrmPackageEditor.dfm | 5 ++++- windows/src/developer/TIKE/child/UfrmPackageEditor.pas | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm index e2561dce1f..e6b404b46c 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm @@ -183,7 +183,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 0 Width = 965 Height = 622 - ActivePage = pageLexicalModels + ActivePage = pageFiles Align = alClient Images = modActionsMain.ilEditorPages MultiLine = True @@ -1119,6 +1119,9 @@ inherited frmPackageEditor: TfrmPackageEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Compile' diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.pas b/windows/src/developer/TIKE/child/UfrmPackageEditor.pas index b956540872..72252e10c3 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.pas +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.pas @@ -1662,7 +1662,7 @@ var FHasDesktopTarget, FHasMobileTarget: Boolean; begin FHasDesktopTarget := False; - FHasMobileTarget := False; + FHasMobileTarget := pack.LexicalModels.Count > 0; for k in pack.Keyboards do begin for i := 0 to pack.Files.Count - 1 do From 6d10ecc0d63c01b0e3e42a1775d3b1ee05f9cb10 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 5 Dec 2019 06:13:34 +0100 Subject: [PATCH 10/16] feat(developer): F7 should compile model Fixes #2097. When the model editor is open, F7 should compile. --- .../Keyman.Developer.UI.dmActionsModelEditor.dfm | 2 ++ .../Keyman.Developer.UI.dmActionsModelEditor.pas | 13 +++++++++++++ .../TIKE/actions/dmActionsKeyboardEditor.dfm | 2 +- .../TIKE/actions/dmActionsKeyboardEditor.pas | 3 +++ windows/src/developer/TIKE/main/UfrmMain.dfm | 16 ++++++++++++++-- windows/src/developer/TIKE/main/UfrmMain.pas | 11 +++++++++-- 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm index 274527d29d..086f56fe7c 100644 --- a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm +++ b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm @@ -12,6 +12,7 @@ object modActionsModelEditor: TmodActionsModelEditor ImageIndex = 37 ShortCut = 118 OnExecute = actModelCompileExecute + OnUpdate = actModelCompileUpdate end object actModelIncludeDebugInformation: TAction Category = 'Lexical Model' @@ -22,6 +23,7 @@ object modActionsModelEditor: TmodActionsModelEditor Category = 'Lexical Model' Caption = 'Test &Lexical Model' OnExecute = actModelTestExecute + OnUpdate = actModelTestUpdate end end end diff --git a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas index b4341b30f4..594e25cde3 100644 --- a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas +++ b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas @@ -14,6 +14,8 @@ type procedure actModelCompileExecute(Sender: TObject); procedure actModelIncludeDebugInformationExecute(Sender: TObject); procedure actModelTestExecute(Sender: TObject); + procedure actModelCompileUpdate(Sender: TObject); + procedure actModelTestUpdate(Sender: TObject); private { Private declarations } public @@ -59,6 +61,12 @@ begin end; end; +procedure TmodActionsModelEditor.actModelCompileUpdate(Sender: TObject); +begin + actModelCompile.Enabled := ActiveModelEditor <> nil; + frmKeymanDeveloper.mnuModel.Visible := actModelCompile.Enabled; +end; + procedure TmodActionsModelEditor.actModelIncludeDebugInformationExecute( Sender: TObject); begin @@ -70,4 +78,9 @@ begin (ActiveModelProjectFile.UI as TProjectFileUI).DoAction(pfaTestKeymanWeb, False); // I4687 end; +procedure TmodActionsModelEditor.actModelTestUpdate(Sender: TObject); +begin + actModelTest.Enabled := ActiveModelEditor <> nil; +end; + end. diff --git a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm index 15096c36e3..9cb7da5f24 100644 --- a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm +++ b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm @@ -23,7 +23,7 @@ object modActionsKeyboardEditor: TmodActionsKeyboardEditor end object actKeyboardCreatePackage: TAction Category = 'Keyboard' - Caption = '&Package for Distribution...' + Caption = 'Package for Distribution...' ImageIndex = 40 OnExecute = actKeyboardCreatePackageExecute end diff --git a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas index a5a8c32d68..c7f7fdbf1b 100644 --- a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas +++ b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas @@ -446,7 +446,10 @@ end; procedure TmodActionsKeyboardEditor.actKeyboardCompileUpdate(Sender: TObject); begin + // TODO: Split Keyboard menu and package editor functions actKeyboardCompile.Enabled := (ActiveEditor <> nil) or (ActivePackageEditor <> nil); + frmKeymanDeveloper.mnuKeyboard.Visible := actKeyboardCompile.Enabled; + frmKeymanDeveloper.mnuDebug.Visible := ActiveEditor <> nil; end; procedure TmodActionsKeyboardEditor.actKeyboardCreatePackageExecute(Sender: TObject); diff --git a/windows/src/developer/TIKE/main/UfrmMain.dfm b/windows/src/developer/TIKE/main/UfrmMain.dfm index 80a7409c84..fb3c8d4764 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.dfm +++ b/windows/src/developer/TIKE/main/UfrmMain.dfm @@ -2950,7 +2950,19 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Action = modActionsMain.actProjectSettings end end - object Keyboards1: TMenuItem + object mnuModel: TMenuItem + Caption = '&Model' + object CompileModel1: TMenuItem + Action = modActionsModelEditor.actModelCompile + end + object N2: TMenuItem + Caption = '-' + end + object estLexicalModel1: TMenuItem + Action = modActionsModelEditor.actModelTest + end + end + object mnuKeyboard: TMenuItem Caption = '&Keyboard' object CompileKeyboard1: TMenuItem Action = modActionsKeyboardEditor.actKeyboardCompile @@ -2989,7 +3001,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Action = modActionsKeyboardEditor.actKeyboardFontHelper end end - object Debug2: TMenuItem + object mnuDebug: TMenuItem Caption = '&Debug' object SetBreakpoint1: TMenuItem Action = modActionsKeyboardEditor.actDebugSetClearBreakpoint diff --git a/windows/src/developer/TIKE/main/UfrmMain.pas b/windows/src/developer/TIKE/main/UfrmMain.pas index a3c5be204a..03d3021ec0 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.pas +++ b/windows/src/developer/TIKE/main/UfrmMain.pas @@ -157,8 +157,8 @@ type Edit1: TMenuItem; View1: TMenuItem; Project1: TMenuItem; - Keyboards1: TMenuItem; - Debug2: TMenuItem; + mnuKeyboard: TMenuItem; + mnuDebug: TMenuItem; ools1: TMenuItem; Help1: TMenuItem; New1: TMenuItem; @@ -270,6 +270,10 @@ type DebugTests1: TMenuItem; CrashTest1: TMenuItem; CloseProject1: TMenuItem; + mnuModel: TMenuItem; + CompileModel1: TMenuItem; + N2: TMenuItem; + estLexicalModel1: TMenuItem; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure mnuFileClick(Sender: TObject); @@ -1429,6 +1433,9 @@ begin if PrevFocus.Visible then PrevFocus.SetFocus; end; + + modActionsKeyboardEditor.actKeyboardCompile.Update; + modActionsModelEditor.actModelCompile.Update; end; procedure TfrmKeymanDeveloper.UDUI_Error(Sender: TUnicodeData; From 457c461af899ab7a9009cfe9192cc3b456586481 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 7 Dec 2019 10:46:18 +0100 Subject: [PATCH 11/16] fix(developer): character map drops chars reliably For all editors other than kmn, it only makes sense to drop characters, not character codes. Fixes #2125. --- windows/src/developer/TIKE/main/UframeTextEditor.pas | 10 +++++++++- .../TIKE/oskbuilder/UframeTouchLayoutBuilder.pas | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/windows/src/developer/TIKE/main/UframeTextEditor.pas b/windows/src/developer/TIKE/main/UframeTextEditor.pas index fca4d88760..e4b5b1dc8d 100644 --- a/windows/src/developer/TIKE/main/UframeTextEditor.pas +++ b/windows/src/developer/TIKE/main/UframeTextEditor.pas @@ -1116,7 +1116,15 @@ begin try j.AddPair('x', TJSONNumber.Create(X)); j.AddPair('y', TJSONNumber.Create(Y)); - j.AddPair('text', cdo.Text[cdo.InsertType]); + + // The character map insert format actually only + // makes sense for the .kmn source editor. For all + // other contexts, we currently only support cmimCharacter. + // It would be possible to do \uxxxx for JS/JSON etc but + // for now that is low priority. + if FEditorFormat = efKMN + then j.AddPair('text', cdo.Text[cdo.InsertType]) + else j.AddPair('text', cdo.Text[cmimCharacter]); ExecuteCommand('charmapDragDrop', j); finally j.Free; diff --git a/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas b/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas index 8d93a2564b..8c56439806 100644 --- a/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas +++ b/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas @@ -677,7 +677,7 @@ begin try j.AddPair('x', TJSONNumber.Create(X)); j.AddPair('y', TJSONNumber.Create(Y)); - j.AddPair('text', cdo.Text[cdo.InsertType]); + j.AddPair('text', cdo.Text[cmimCharacter]); // It never makes sense to drop anything other than char BuilderCommand('charmapDragDrop', j); finally j.Free; From 0032b59303a349c0821e6bcdf3b1fb22d00749bd Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sun, 8 Dec 2019 11:17:05 +0000 Subject: [PATCH 12/16] fix(developer): keyboard id should be clean in import --- .../Keyman.Developer.System.ImportWindowsKeyboard.pas | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas b/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas index 98c0650429..81ecdeb749 100644 --- a/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas +++ b/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas @@ -73,6 +73,7 @@ uses Keyman.Developer.System.ImportKeyboardDLL, Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter, Keyman.System.Util.RenderLanguageIcon, + Keyman.System.KeyboardUtils, KeymanVersion, KeyboardParser, kmxfileconsts, @@ -107,7 +108,7 @@ begin r.ValueExists(SRegValue_KeyboardLayoutText) then Exit(False); - KeyboardID := ChangeFileExt(r.ReadString(SRegValue_KeyboardLayoutFile), ''); + KeyboardID := TKeyboardUtils.CleanKeyboardID(ChangeFileExt(r.ReadString(SRegValue_KeyboardLayoutFile), '')); Name := r.ReadString(SRegValue_KeyboardLayoutText); finally r.Free; From 7929472a70bb0bd5ae52eb362e87d4de0ebdb7d9 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sun, 8 Dec 2019 11:18:17 +0000 Subject: [PATCH 13/16] chore: history --- windows/src/developer/history.md | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/src/developer/history.md b/windows/src/developer/history.md index efd53c26ed..3da23249cc 100644 --- a/windows/src/developer/history.md +++ b/windows/src/developer/history.md @@ -3,6 +3,7 @@ ## 13.0 alpha * Start version 13.0 * Feature: Add unsupported kmdecomp decompiler utility (#2419) +* Bug Fix: Keyboard ID was not clean by default with Import Windows Keyboard (#2431) ## 2019-11-18 12.0.55 stable * Bug Fix: Some keyboards were incorrectly marked as mobile-capable (#2334) From 1b83de9b65487d5e89bcaccefc2c22c368ad7a80 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sun, 8 Dec 2019 17:24:40 +0000 Subject: [PATCH 14/16] fix(developer): Hotkeys in .kmn unquoted Fixes #138. Hotkeys in .kmn files no longer need to be quoted. This update does not change compatibility with earlier versions of Keyman Engine as the difference is entirely in the compiler. --- windows/src/developer/history.md | 1 + windows/src/developer/kmcmpdll/Compiler.cpp | 21 +++++++++++++++++++++ windows/src/developer/kmcmpdll/Compiler.rc | 1 + windows/src/developer/kmcmpdll/kcframe.cpp | 5 ++--- windows/src/global/inc/Comperr.h | 2 ++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/windows/src/developer/history.md b/windows/src/developer/history.md index efd53c26ed..bbb7e00c11 100644 --- a/windows/src/developer/history.md +++ b/windows/src/developer/history.md @@ -3,6 +3,7 @@ ## 13.0 alpha * Start version 13.0 * Feature: Add unsupported kmdecomp decompiler utility (#2419) +* Feature: Hotkeys defined in .kmn no longer need to be quoted (#138) ## 2019-11-18 12.0.55 stable * Bug Fix: Some keyboards were incorrectly marked as mobile-capable (#2334) diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index a4c8623d6b..2d325e3dbf 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -2792,6 +2792,8 @@ DWORD ProcessEthnologueStore(PWSTR p) // I2646 return res; } +#define K_HOTKEYSHIFTFLAGS (K_SHIFTFLAG | K_CTRLFLAG | K_ALTFLAG | ISVIRTUALKEY) + DWORD ProcessHotKey(PWSTR p, DWORD *hk) { PWSTR q, r; @@ -2800,6 +2802,25 @@ DWORD ProcessHotKey(PWSTR p, DWORD *hk) *hk = 0; + if(*p == UC_SENTINEL && *(p+1) == CODE_EXTENDED) { + WORD Key = *(p + 3); + WORD ShiftFlags = *(p + 2); + + // Convert virtual key to hotkey (different bitflags) + + if (ShiftFlags & ~K_HOTKEYSHIFTFLAGS) { + AddWarning(CWARN_HotkeyHasInvalidModifier); + } + + if (ShiftFlags & K_SHIFTFLAG) *hk |= HK_SHIFT; + if (ShiftFlags & K_CTRLFLAG) *hk |= HK_SHIFT; + if (ShiftFlags & K_ALTFLAG) *hk |= HK_SHIFT; + + *hk |= Key; + + return CERR_None; + } + q = wcschr(p, '['); if(q) { diff --git a/windows/src/developer/kmcmpdll/Compiler.rc b/windows/src/developer/kmcmpdll/Compiler.rc index 33325d1a84..888c924bac 100644 --- a/windows/src/developer/kmcmpdll/Compiler.rc +++ b/windows/src/developer/kmcmpdll/Compiler.rc @@ -222,5 +222,6 @@ END STRINGTABLE BEGIN CWARN_LanguageHeadersDeprecatedInKeyman10 "This language header has been deprecated in Keyman 10. Instead, add language metadata in the package file" + CWARN_HotkeyHasInvalidModifier "Hotkey has modifiers that are not supported. Use only SHIFT, CTRL and ALT" CINFO_NonUnicodeFile "Keyman Developer has detected that the file has ANSI encoding. Consider converting this file to UTF-8" END diff --git a/windows/src/developer/kmcmpdll/kcframe.cpp b/windows/src/developer/kmcmpdll/kcframe.cpp index 2ec24edc2a..2d41947135 100644 --- a/windows/src/developer/kmcmpdll/kcframe.cpp +++ b/windows/src/developer/kmcmpdll/kcframe.cpp @@ -37,9 +37,8 @@ int main(int argc, char *argv[]) if(argc < 3) { puts("Usage: kcframe infile.kmn outfile.kmx"); - return 0; + return 1; } - CompileKeyboardFile(argv[1], argv[2], TRUE, FALSE, TRUE, msgproc); // I4865 // I4866 - return 1; + return CompileKeyboardFile(argv[1], argv[2], TRUE, FALSE, TRUE, msgproc) ? 0 : 1; // I4865 // I4866 } diff --git a/windows/src/global/inc/Comperr.h b/windows/src/global/inc/Comperr.h index 52bc24c554..b59687e378 100644 --- a/windows/src/global/inc/Comperr.h +++ b/windows/src/global/inc/Comperr.h @@ -189,6 +189,8 @@ #define CWARN_TooManyErrorsOrWarnings 0x000020A7 +#define CWARN_HotkeyHasInvalidModifier 0x000020A8 + #define CERR_BufferOverflow 0x000080C0 #define CERR_Break 0x000080C1 From 50a382d181ea20edea2a69bcac6c25982819523c Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sun, 8 Dec 2019 17:27:26 +0000 Subject: [PATCH 15/16] chore: typos --- windows/src/developer/kmcmpdll/Compiler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index 2d325e3dbf..d024e160cd 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -2813,8 +2813,8 @@ DWORD ProcessHotKey(PWSTR p, DWORD *hk) } if (ShiftFlags & K_SHIFTFLAG) *hk |= HK_SHIFT; - if (ShiftFlags & K_CTRLFLAG) *hk |= HK_SHIFT; - if (ShiftFlags & K_ALTFLAG) *hk |= HK_SHIFT; + if (ShiftFlags & K_CTRLFLAG) *hk |= HK_CTRL; + if (ShiftFlags & K_ALTFLAG) *hk |= HK_ALT; *hk |= Key; From 727059c7bf44536181ac9228b4919f30aa1adcf6 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sun, 8 Dec 2019 17:37:32 +0000 Subject: [PATCH 16/16] chore: history --- windows/src/developer/history.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/src/developer/history.md b/windows/src/developer/history.md index bbb7e00c11..9a2f116676 100644 --- a/windows/src/developer/history.md +++ b/windows/src/developer/history.md @@ -3,7 +3,7 @@ ## 13.0 alpha * Start version 13.0 * Feature: Add unsupported kmdecomp decompiler utility (#2419) -* Feature: Hotkeys defined in .kmn no longer need to be quoted (#138) +* Feature: Hotkeys defined in .kmn no longer need to be quoted (#2432) ## 2019-11-18 12.0.55 stable * Bug Fix: Some keyboards were incorrectly marked as mobile-capable (#2334)