diff --git a/packages/delphi11/heidisql.dpr b/packages/delphi11/heidisql.dpr index c852cab9..c45d4bf8 100644 --- a/packages/delphi11/heidisql.dpr +++ b/packages/delphi11/heidisql.dpr @@ -39,6 +39,7 @@ uses view in '..\..\source\view.pas' {frmView}, selectdbobject in '..\..\source\selectdbobject.pas' {frmSelectDBObject}, texteditor in '..\..\source\texteditor.pas' {frmTextEditor}, + bineditor in '..\..\source\bineditor.pas' {frmBinEditor}, grideditlinks in '..\..\source\grideditlinks.pas', uVistaFuncs in '..\..\source\uVistaFuncs.pas'; diff --git a/packages/delphi11/heidisql.dproj b/packages/delphi11/heidisql.dproj index 1bd03e09..6719e95c 100644 --- a/packages/delphi11/heidisql.dproj +++ b/packages/delphi11/heidisql.dproj @@ -63,6 +63,9 @@
AboutBox
+ +
frmBinEditor
+
MDIChild
@@ -109,7 +112,6 @@
MainForm
-
frmMemoEditor
@@ -140,6 +142,9 @@
tbl_properties_form
+ +
frmTextEditor
+
frmUpdateCheck
@@ -152,4 +157,4 @@
frmView
- \ No newline at end of file + diff --git a/source/bineditor.dfm b/source/bineditor.dfm new file mode 100644 index 00000000..59304bac --- /dev/null +++ b/source/bineditor.dfm @@ -0,0 +1,97 @@ +object frmBinEditor: TfrmBinEditor + Left = 0 + Top = 0 + BorderStyle = bsSizeToolWin + Caption = 'Binary editor' + ClientHeight = 95 + ClientWidth = 215 + Color = clBtnFace + Constraints.MinHeight = 100 + Constraints.MinWidth = 130 + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + Position = poMainFormCenter + OnCloseQuery = FormCloseQuery + OnCreate = FormCreate + OnDestroy = FormDestroy + OnShow = FormShow + DesignSize = ( + 215 + 95) + PixelsPerInch = 96 + TextHeight = 13 + object lblTextLength: TLabel + Left = 103 + Top = 77 + Width = 65 + Height = 13 + Anchors = [akLeft, akBottom] + BiDiMode = bdLeftToRight + Caption = 'lblTextLength' + ParentBiDiMode = False + end + object memoText: TTntMemo + Left = 0 + Top = 0 + Width = 215 + Height = 72 + Align = alTop + Anchors = [akLeft, akTop, akRight, akBottom] + Lines.Strings = ( + 'memoText') + ScrollBars = ssBoth + TabOrder = 0 + WantTabs = True + OnChange = memoTextChange + OnKeyDown = memoTextKeyDown + end + object tlbStandard: TToolBar + Left = 0 + Top = 73 + Width = 97 + Height = 22 + Align = alNone + Anchors = [akLeft, akBottom] + Caption = 'tlbStandard' + Images = MainForm.PngImageListMain + ParentShowHint = False + ShowHint = True + TabOrder = 1 + object btnWrap: TToolButton + Left = 0 + Top = 0 + Hint = 'Wrap long lines' + Caption = 'Wrap long lines' + ImageIndex = 62 + OnClick = btnWrapClick + end + object btnLoadBinary: TToolButton + Left = 23 + Top = 0 + Hint = 'Load binary file' + Caption = 'Load binary file' + ImageIndex = 52 + OnClick = btnLoadBinaryClick + end + object btnCancel: TToolButton + Left = 46 + Top = 0 + Hint = 'Cancel' + Caption = 'Cancel' + ImageIndex = 26 + OnClick = btnCancelClick + end + object btnApply: TToolButton + Left = 69 + Top = 0 + Hint = 'Apply changes' + Caption = 'Apply changes' + ImageIndex = 55 + OnClick = btnApplyClick + end + end +end diff --git a/source/bineditor.pas b/source/bineditor.pas new file mode 100644 index 00000000..dab8b345 --- /dev/null +++ b/source/bineditor.pas @@ -0,0 +1,198 @@ +unit bineditor; + +interface + +uses + Windows, Classes, Graphics, Forms, Controls, helpers, StdCtrls, TntStdCtrls, Registry, VirtualTrees, + ComCtrls, ToolWin, Dialogs, SysUtils; + +{$I const.inc} + +type + TfrmBinEditor = class(TMemoEditor) + memoText: TTntMemo; + tlbStandard: TToolBar; + btnWrap: TToolButton; + btnLoadBinary: TToolButton; + btnApply: TToolButton; + btnCancel: TToolButton; + lblTextLength: TLabel; + procedure btnApplyClick(Sender: TObject); + procedure btnCancelClick(Sender: TObject); + procedure btnLoadBinaryClick(Sender: TObject); + procedure btnWrapClick(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure FormShow(Sender: TObject); + procedure memoTextChange(Sender: TObject); + procedure memoTextKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); + procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); + procedure FormCreate(Sender: TObject); + private + { Private declarations } + FModified: Boolean; + procedure SetModified(NewVal: Boolean); + property Modified: Boolean read FModified write SetModified; + public + function GetText: WideString; override; + procedure SetText(text: WideString); override; + procedure SetMaxLength(len: integer); override; + procedure SetFont(font: TFont); override; + end; + + +implementation + +uses main; + +{$R *.dfm} + + +function TfrmBinEditor.GetText: WideString; +begin + Result := '0x' + memoText.Text; +end; + +procedure TfrmBinEditor.SetText(text: WideString); +begin + // Skip '0x'. + memoText.Text := Copy(text, 3); +end; + +procedure TfrmBinEditor.SetMaxLength(len: integer); +begin + // Input: Length in bytes. + memoText.MaxLength := len * 2; +end; + +procedure TfrmBinEditor.SetFont(font: TFont); +begin + memoText.Font := font; +end; + +procedure TfrmBinEditor.FormCreate(Sender: TObject); +begin + InheritFont(Font); +end; + + +procedure TfrmBinEditor.FormDestroy(Sender: TObject); +var + reg: TRegistry; +begin + reg := TRegistry.Create; + if reg.OpenKey(REGPATH, False) then begin + reg.WriteInteger( REGNAME_EDITOR_WIDTH, Width ); + reg.WriteInteger( REGNAME_EDITOR_HEIGHT, Height ); + reg.CloseKey; + end; + reg.Free; +end; + + +procedure TfrmBinEditor.FormShow(Sender: TObject); +begin + // Restore form dimensions + Width := Mainform.GetRegValue(REGNAME_EDITOR_WIDTH, DEFAULT_EDITOR_WIDTH); + Height := Mainform.GetRegValue(REGNAME_EDITOR_HEIGHT, DEFAULT_EDITOR_HEIGHT); + // Fix label position: + lblTextLength.Top := tlbStandard.Top + (tlbStandard.Height-lblTextLength.Height) div 2; + SetWindowSizeGrip(Handle, True); + memoText.SelectAll; + memoText.SetFocus; + memoTextChange(Sender); + Modified := False; +end; + + +procedure TfrmBinEditor.memoTextKeyDown(Sender: TObject; var Key: Word; Shift: + TShiftState); +begin + case Key of + // Cancel by Escape + VK_ESCAPE: btnCancelClick(Sender); + // Apply changes and end editing by Ctrl + Enter + VK_RETURN: if ssCtrl in Shift then btnApplyClick(Sender); + end; +end; + +procedure TfrmBinEditor.btnWrapClick(Sender: TObject); +var + WasModified: Boolean; +begin + Screen.Cursor := crHourglass; + // Changing the scrollbars invoke the OnChange event. We avoid thinking the text was really modified. + WasModified := Modified; + if memoText.ScrollBars = ssBoth then + memoText.ScrollBars := ssVertical + else + memoText.ScrollBars := ssBoth; + TToolbutton(Sender).Down := memoText.ScrollBars = ssVertical; + Modified := WasModified; + Screen.Cursor := crDefault; +end; + + +procedure TfrmBinEditor.btnLoadBinaryClick(Sender: TObject); +var + d: TOpenDialog; +begin + d := TOpenDialog.Create(Self); + d.Filter := 'All binary files (*.*)|*.*'; + d.FilterIndex := 0; + if d.Execute then try + Screen.Cursor := crHourglass; + memoText.Text := BinToWideHex(ReadBinaryFile(d.FileName, memoText.MaxLength)); + finally + Screen.Cursor := crDefault; + end; + d.Free; +end; + + +procedure TfrmBinEditor.btnCancelClick(Sender: TObject); +var + DoPost: Boolean; +begin + if Modified then + DoPost := MessageDlg('Apply modifications?', mtConfirmation, [mbYes, mbNo], 0) = mrYes + else + DoPost := False; + if DoPost then + TCustomVirtualStringTree(Owner).EndEditNode + else + TCustomVirtualStringTree(Owner).CancelEditNode; +end; + + +procedure TfrmBinEditor.FormCloseQuery(Sender: TObject; var CanClose: Boolean); +begin + btnCancelClick(Sender); + CanClose := False; // Done by editor link +end; + + +procedure TfrmBinEditor.btnApplyClick(Sender: TObject); +begin + TCustomVirtualStringTree(Owner).EndEditNode; +end; + + +procedure TfrmBinEditor.memoTextChange(Sender: TObject); +begin + lblTextLength.Caption := FormatNumber(Length(memoText.Text) / 2) + ' bytes.'; + if memoText.MaxLength > 0 then + lblTextLength.Caption := lblTextLength.Caption + ' (Max: '+FormatNumber(memoText.MaxLength / 2)+')'; + Modified := True; +end; + + +procedure TfrmBinEditor.SetModified(NewVal: Boolean); +begin + if FModified <> NewVal then begin + FModified := NewVal; + btnApply.Enabled := FModified; + end; +end; + + +end. diff --git a/source/childwin.pas b/source/childwin.pas index 0581843a..358debf1 100644 --- a/source/childwin.pas +++ b/source/childwin.pas @@ -1204,7 +1204,11 @@ begin rx.Expression := '^((tiny|medium|long)?text|(var)?char)\b(\(\d+\))?'; if rx.Exec(ColType) then begin FDataGridResult.Columns[idx].IsText := True; - if ColType = 'tinytext' then + if rx.Match[4] <> '' then + FDataGridResult.Columns[idx].MaxLength := MakeInt(rx.Match[4]) + else if ColType = 'tinytext' then + // 255 is the width in bytes. If characters that use multiple bytes are + // contained, the width in characters is decreased below this number. FDataGridResult.Columns[idx].MaxLength := 255 else if ColType = 'text' then FDataGridResult.Columns[idx].MaxLength := 65535 @@ -1212,14 +1216,13 @@ begin FDataGridResult.Columns[idx].MaxLength := 16777215 else if ColType = 'longtext' then FDataGridResult.Columns[idx].MaxLength := 4294967295 - else if rx.Match[4] <> '' then - FDataGridResult.Columns[idx].MaxLength := MakeInt(rx.Match[4]) - else // Fallback for unknown column types + else + // Fallback for unknown column types FDataGridResult.Columns[idx].MaxLength := MaxInt; end; rx.Expression := '^((tiny|medium|long)?blob|(var)?binary|bit)\b'; if rx.Exec(ColType) then - FDataGridResult.Columns[idx].IsBlob := True; + FDataGridResult.Columns[idx].IsBinary := True; if Copy(ColType, 1, 5) = 'enum(' then begin FDataGridResult.Columns[idx].IsEnum := True; FDataGridResult.Columns[idx].EnumVals := WideStrings.TWideStringList.Create; @@ -2482,7 +2485,7 @@ begin else if ds.Fields[i].DataType in [ftWideString, ftMemo, ftWideMemo] then FQueryGridResult.Columns[i].IsText := True else if ds.Fields[i].DataType in [ftBlob] then - FQueryGridResult.Columns[i].IsBlob := True; + FQueryGridResult.Columns[i].IsBinary := True; end; SetLength(FQueryGridResult.Rows, 0); SetLength(FQueryGridResult.Rows, ds.RecordCount); @@ -2491,8 +2494,8 @@ begin FQueryGridResult.Rows[i].Loaded := True; SetLength(FQueryGridResult.Rows[i].Cells, ds.FieldCount); for j:=0 to ds.FieldCount-1 do begin - if FQueryGridResult.Columns[j].IsBlob then - FQueryGridResult.Rows[i].Cells[j].Text := Utf8Decode(ds.Fields[j].AsString) + if FQueryGridResult.Columns[j].IsBinary then + FQueryGridResult.Rows[i].Cells[j].Text := '0x' + BinToWideHex(ds.Fields[j].AsString) else FQueryGridResult.Rows[i].Cells[j].Text := ds.Fields[j].AsWideString; FQueryGridResult.Rows[i].Cells[j].IsNull := ds.Fields[j].IsNull; @@ -3575,6 +3578,12 @@ begin end; // Create instance of the progress form (but don't show it yet) + // Todo: This apparently causes an exception if invoked via an event handler? + // Classes.TStream.ReadComponent(???) + // Classes.InternalReadComponentRes(???,???,???) + // Classes.InitComponent(TfrmQueryProgress) + // Classes.InitInheritedComponent($17E0710,TForm) + // Forms.TCustomForm.Create(???) FProgressForm := TFrmQueryProgress.Create(Self); { Launch a thread of execution that passes the query to the server @@ -5478,8 +5487,8 @@ begin for i := start to start + limit - 1 do begin SetLength(FDataGridResult.Rows[i].Cells, ds.Fields.Count); for j := 0 to ds.Fields.Count - 1 do begin - if FDataGridResult.Columns[j].IsBlob then - FDataGridResult.Rows[i].Cells[j].Text := Utf8Decode(ds.Fields[j].AsString) + if FDataGridResult.Columns[j].IsBinary then + FDataGridResult.Rows[i].Cells[j].Text := '0x' + BinToWideHex(ds.Fields[j].AsString) else FDataGridResult.Rows[i].Cells[j].Text := ds.Fields[j].AsWideString; FDataGridResult.Rows[i].Cells[j].IsNull := ds.Fields[j].IsNull; @@ -5489,6 +5498,7 @@ begin end; MainForm.ShowStatus( STATUS_MSG_READY ); + // Todo: Seen an AV next line when this method was invoked via an event handler. FreeAndNil(ds); end; end; @@ -5573,7 +5583,7 @@ begin else if r.Columns[Column].isText then if isNull then cl := $60CC60 else cl := clGreen // Text field - else if r.Columns[Column].isBlob then + else if r.Columns[Column].isBinary then if isNull then cl := $CC60CC else cl := clPurple else if isNull then cl := COLOR_NULLVALUE else cl := clWindowText; @@ -5723,7 +5733,8 @@ begin if Row.Cells[i].Modified then begin Val := Row.Cells[i].NewText; if FDataGridResult.Columns[i].IsFloat then Val := FloatStr(Val); - Val := esc(Val); + if not FDataGridResult.Columns[i].IsBinary then Val := esc(Val); + if FDataGridResult.Columns[i].IsBinary then CheckHex(Copy(Val, 3), 'Invalid hexadecimal string given in field "' + FDataGridResult.Columns[i].Name + '".'); if Row.Cells[i].NewIsNull then Val := 'NULL'; sql := sql + ' ' + mask(FDataGridResult.Columns[i].Name) + '=' + Val + ', '; end; @@ -5759,8 +5770,8 @@ begin ds := ExecSelectQuery(sql); if ds.RecordCount = 1 then begin for i := 0 to ds.FieldCount - 1 do begin - if FDataGridResult.Columns[i].IsBlob then - Row.Cells[i].Text := Utf8Decode(ds.Fields[i].AsString) + if FDataGridResult.Columns[i].IsBinary then + Row.Cells[i].Text := '0x' + BinToWideHex(ds.Fields[i].AsString) else Row.Cells[i].Text := ds.Fields[i].AsWideString; Row.Cells[i].IsNull := ds.Fields[i].IsNull; @@ -5814,7 +5825,7 @@ begin KeyVal := Row.Cells[j].Text; // Quote if needed if FDataGridResult.Columns[j].IsFloat then KeyVal := FloatStr(KeyVal); - KeyVal := esc(KeyVal); + if not FDataGridResult.Columns[j].IsBinary then KeyVal := esc(KeyVal); if Row.Cells[j].IsNull then KeyVal := ' IS NULL' else KeyVal := '=' + KeyVal; Result := Result + mask(KeyCols[i]) + KeyVal + ' AND '; @@ -5898,7 +5909,8 @@ begin Cols := Cols + mask(FDataGridResult.Columns[i].Name) + ', '; Val := Row.Cells[i].NewText; if FDataGridResult.Columns[i].IsFloat then Val := FloatStr(Val); - Val := esc(Val); + if not FDataGridResult.Columns[i].IsBinary then Val := esc(Val); + if FDataGridResult.Columns[i].IsBinary then CheckHex(Copy(Val, 3), 'Invalid hexadecimal string given in field "' + FDataGridResult.Columns[i].Name + '".'); if Row.Cells[i].NewIsNull then Val := 'NULL'; Vals := Vals + Val + ', '; end; @@ -6080,7 +6092,10 @@ var DateTimeEditor: TDateTimeEditorLink; EnumEditor: TEnumEditorLink; begin - if FDataGridResult.Columns[Column].IsText then begin + if + FDataGridResult.Columns[Column].IsText or + FDataGridResult.Columns[Column].IsBinary + then begin MemoEditor := TMemoEditorLink.Create; MemoEditor.MaxLength := FDataGridResult.Columns[Column].MaxLength; EditLink := MemoEditor; diff --git a/source/grideditlinks.pas b/source/grideditlinks.pas index 5ceaaa4d..ab2c74d1 100644 --- a/source/grideditlinks.pas +++ b/source/grideditlinks.pas @@ -4,8 +4,8 @@ unit grideditlinks; interface -uses Windows, Forms, Graphics, messages, VirtualTrees, texteditor, ComCtrls, SysUtils, Classes, - mysql_structures, Main, helpers, TntStdCtrls, WideStrings, StdCtrls; +uses Windows, Forms, Graphics, messages, VirtualTrees, texteditor, bineditor, ComCtrls, SysUtils, Classes, + mysql_structures, Main, ChildWin, helpers, TntStdCtrls, WideStrings, StdCtrls; type TMemoEditorLink = class(TInterfacedObject, IVTEditLink) @@ -95,6 +95,7 @@ end; function TMemoEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall; // Retrieves the true text bounds from the owner tree. var + IsBinary: Boolean; Text: WideString; F: TFont; begin @@ -110,11 +111,15 @@ begin F := TFont.Create; FTree.GetTextInfo(Node, Column, F, FTextBounds, Text); + IsBinary := MainForm.ChildWin.FDataGridResult.Columns[Column].IsBinary; + // Get wide text of the node. Text := FTree.Text[FNode, FColumn]; - // Create the editor form - FForm := TfrmTextEditor.Create(Ftree); + // Create the text editor form + if IsBinary then FForm := TfrmBinEditor.Create(Ftree) + else FForm := TfrmTextEditor.Create(Ftree); + FForm.SetFont(F); FForm.SetText(Text); FForm.SetMaxLength(MaxLength); diff --git a/source/helpers.pas b/source/helpers.pas index 10f8add8..4d64c5f1 100644 --- a/source/helpers.pas +++ b/source/helpers.pas @@ -51,7 +51,7 @@ type DataType: Byte; // @see constants in mysql_structures.pas MaxLength: Cardinal; IsPriPart: Boolean; - IsBlob: Boolean; + IsBinary: Boolean; IsText: Boolean; IsEnum: Boolean; IsInt: Boolean; @@ -150,8 +150,12 @@ type function GetFileCharset(Stream: TFileStream): TFileCharset; function ReadTextfileChunk(Stream: TFileStream; FileCharset: TFileCharset; ChunkSize: Int64 = 0): WideString; function ReadTextfile(Filename: String): WideString; + function ReadBinaryFile(Filename: String; MaxBytes: Int64): string; procedure CopyToClipboard(Value: WideString); procedure StreamToClipboard(S: TMemoryStream); + function WideHexToBin(text: WideString): string; + function BinToWideHex(bin: string): WideString; + procedure CheckHex(text: WideString; errorMessage: string); var MYSQL_KEYWORDS : TStringList; @@ -194,6 +198,38 @@ var +function WideHexToBin(text: WideString): string; +var + buf: string; +begin + // Todo: test. + buf := text; + SetLength(Result, Length(text) div 2); + HexToBin(@buf[1], @Result[1], Length(Result)); +end; + +function BinToWideHex(bin: string): WideString; +var + buf: string; +begin + SetLength(buf, Length(bin) * 2); + BinToHex(@bin[1], @buf[1], Length(buf)); + Result := buf; +end; + + procedure CheckHex(text: WideString; errorMessage: string); +const + allowed: string = '0123456789abcdefABCDEF'; +var + i: Cardinal; +begin + for i := 1 to Length(text) do begin + if Pos(text[i], allowed) < 1 then begin + raise Exception.Create(errorMessage); + end; + end; +end; + {*** Convert a TStringList to a string using a separator-string @@ -2444,6 +2480,17 @@ begin Stream.Free; end; +function ReadBinaryFile(Filename: String; MaxBytes: Int64): string; +var + Stream: TFileStream; +begin + Stream := TFileStream.Create(Filename, fmOpenRead or fmShareDenyNone); + Stream.Position := 0; + if (MaxBytes < 1) or (MaxBytes > Stream.Size) then MaxBytes := Stream.Size; + SetLength(Result, MaxBytes); + Stream.Read(PChar(Result)^, Length(Result)); + Stream.Free; +end; { TUniClipboard } diff --git a/source/main.pas b/source/main.pas index 621edcbc..6ed4a8cd 100644 --- a/source/main.pas +++ b/source/main.pas @@ -1230,7 +1230,7 @@ var f : Textfile; tmppath : array[0..MAX_PATH] of char; Content : WideString; - IsBlob : Boolean; + IsBinary : Boolean; begin g := ChildWin.ActiveGrid; if g = nil then begin messagebeep(MB_ICONASTERISK); exit; end; @@ -1238,19 +1238,19 @@ begin showstatus('Saving contents to file...'); if g = Childwin.DataGrid then begin Content := Childwin.FDataGridResult.Rows[Childwin.DataGrid.FocusedNode.Index].Cells[Childwin.DataGrid.FocusedColumn].Text; - IsBlob := Childwin.FDataGridResult.Columns[Childwin.DataGrid.FocusedColumn].IsBlob; + IsBinary := Childwin.FDataGridResult.Columns[Childwin.DataGrid.FocusedColumn].IsBinary; end else begin Content := Childwin.FQueryGridResult.Rows[Childwin.QueryGrid.FocusedNode.Index].Cells[Childwin.QueryGrid.FocusedColumn].Text; - IsBlob := Childwin.FQueryGridResult.Columns[Childwin.QueryGrid.FocusedColumn].IsBlob; + IsBinary := Childwin.FQueryGridResult.Columns[Childwin.QueryGrid.FocusedColumn].IsBinary; end; childwin.logsql(g.Name); GetTempPath(MAX_PATH, tmppath); filename := tmppath; filename := filename+'\'+APPNAME+'-preview.'; - if IsBlob then begin + if IsBinary then begin if pos('JFIF', copy(Content, 0, 20)) <> 0 then - filename := filename + 'jpg' + filename := filename + 'jpeg' else if StrCmpBegin('GIF', Content) then filename := filename + 'gif' else if StrCmpBegin('BM', Content) then diff --git a/source/texteditor.pas b/source/texteditor.pas index 0829a9f5..20047ec4 100644 --- a/source/texteditor.pas +++ b/source/texteditor.pas @@ -4,7 +4,7 @@ interface uses Windows, Classes, Graphics, Forms, Controls, helpers, StdCtrls, TntStdCtrls, Registry, VirtualTrees, - ComCtrls, ToolWin, Dialogs; + ComCtrls, ToolWin, Dialogs, SysUtils; {$I const.inc} @@ -60,6 +60,7 @@ end; procedure TfrmTextEditor.SetMaxLength(len: integer); begin + // Input: Length in number of bytes. memoText.MaxLength := len; end;