unit grideditlinks; // The editor links, instanciated by VirtualTree.CreateEditor {$mode delphi}{$H+} interface uses Forms, Graphics, Messages, laz.VirtualTrees, ComCtrls, SysUtils, Classes, StdCtrls, ExtCtrls, CheckLst, Controls, Types, Dialogs, Menus, MaskEdit, DateUtils, Math, dbconnection, dbstructures, apphelpers, texteditor, bineditor, StrUtils, System.UITypes, RegExpr, extra_controls, EditBtn, LCLType, LCLIntf; type // Radio buttons and checkboxes which do not pass key to their parent control // so a OnKeyDown event using has the chance to end editing. {TAllKeysRadioButton = class(TRadioButton) procedure WMGetDlgCode(var Msg: TMessage); message WM_GETDLGCODE; end;} TAllKeysRadioButton = TRadioButton; {TAllKeysCheckBox = class(TCheckBox) procedure WMGetDlgCode(var Msg: TMessage); message WM_GETDLGCODE; end;} TAllKeysCheckBox = TCheckBox; TBaseGridEditorLink = class(TInterfacedObject, IVTEditLink) private FInstanceId: Integer; FParentForm: TWinControl; // A back reference to the main form FTree: TVirtualStringTree; // A back reference to the tree calling. FNode: PVirtualNode; // The node to be edited. FColumn: TColumnIndex; // The column of the node. FCellText: String; // Original cell text value FCellFont: TFont; // Cosmetic FCellBackground: TColor; FMainControl: TWinControl; // The editor's most important component FStopping: Boolean; // Set to True when the edit link requests stopping the edit action. FLastKeyDown: Integer; // Set in OnKeyDown on the editor's main control FLastShiftState: TShiftState; FOldWindowProc: TWndMethod; // Temporary switched to TempWindowProc to be able to catch Tab key FTableColumn: TTableColumn; FModified: Boolean; FAllowEdit: Boolean; FBeginEditTime: Cardinal; procedure Log(Msg: String); //procedure TempWindowProc(var Message: TMessage); procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure DoEndEdit(Sender: TObject); procedure DoCancelEdit(Sender: TObject); function GetCellRect(InnerTextBounds: Boolean): TRect; public // The table column of the cell being edited. Mostly used in data grids. property TableColumn: TTableColumn read FTableColumn; // The original constructor, not used any more, throws an exception if you do constructor Create; overload; // The right constructor, we need the Tree reference constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); overload; virtual; destructor Destroy; override; property Tree: TVirtualStringTree read FTree; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; virtual; stdcall; function BeginEdit: Boolean; virtual; stdcall; function CancelEdit: Boolean; virtual; stdcall; function EndEdit: Boolean; virtual; stdcall; abstract; function EndEditHelper(NewText: String): Boolean; function GetBounds: TRect; virtual; stdcall; // Normally useless and unused procedure ProcessMessage(var Message: TMessage); stdcall; procedure SetBounds(R: TRect); virtual; stdcall; abstract; end; THexEditorLink = class(TBaseGridEditorLink) private FForm: TfrmBinEditor; public MaxLength: Integer; TitleText: String; constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function CancelEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; end; TDateTimeEditorLink = class(TBaseGridEditorLink) private FPanel: TPanel; FMaskEdit: TMaskEdit; FTimer: TTimer; FModifyOffset: Integer; FTimerCalls: Integer; FUpDown: TUpDown; procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure DoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); procedure UpDownChangingEx(Sender: TObject; var AllowChange: Boolean; NewValue: SmallInt; Direction: TUpDownDirection); procedure UpDownMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure DoOnTimer(Sender: TObject); procedure ModifyDate(Offset: Integer); procedure TextChange(Sender: TObject); function MicroSecondsPrecision: Integer; public constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; end; TEnumEditorLink = class(TBaseGridEditorLink) private FCombo: TExtComboBox; procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure DoSelect(Sender: TObject); public ValueList, DisplayList: TStringList; AllowCustomText: Boolean; ItemMustExist: Boolean; constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; end; TSetEditorLink = class(TBaseGridEditorLink) private FPanel: TPanel; FCheckList: TCheckListBox; FBtnOK, FBtnCancel: TButton; FEndTimer: TTimer; procedure BtnOkClick(Sender: TObject); procedure BtnCancelClick(Sender: TObject); public ValueList: TStringList; constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; end; // Inplace editor with button TInplaceEditorLink = class(TBaseGridEditorLink) private FPanel: TPanel; FEdit: TEdit; FButton: TButton; FMaxLength: Integer; procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure ButtonClick(Sender: TObject); public ButtonVisible: Boolean; TitleText: String; constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; property MaxLength: Integer read FMaxLength write FMaxLength; end; TColumnDefaultEditorLink = class(TBaseGridEditorLink) private FPanel: TPanel; FRadioNothing, FRadioText, FRadioNULL, FRadioExpression, FRadioAutoInc: TAllKeysRadioButton; FlblOnUpdate: TLabel; FTextEdit: TEditButton; FTextDropDown: TPopupMenu; FExpressionEdit: TComboBox; FOnUpdateEdit: TComboBox; FBtnOK, FBtnCancel: TButton; FEndTimer: TTimer; procedure RadioClick(Sender: TObject); procedure EditChange(Sender: TObject); procedure EditButtonClick(Sender: TObject); procedure EditDropDownClick(Sender: TObject); procedure BtnOkClick(Sender: TObject); procedure BtnCancelClick(Sender: TObject); public DefaultType, OnUpdateType: TColumnDefaultType; DefaultText, OnUpdateText: String; constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; end; TDataTypeEditorLink = class(TBaseGridEditorLink) private FTreeSelect: TVirtualStringTree; FMemoHelp: TMemo; procedure DoTreeSelectGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: String); procedure DoTreeSelectInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); procedure DoTreeSelectInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); procedure DoTreeSelectHotChange(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode); procedure DoTreeSelectPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); procedure DoTreeSelectFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex; var Allowed: Boolean); procedure DoTreeSelectFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); public constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override; destructor Destroy; override; function BeginEdit: Boolean; override; function EndEdit: Boolean; override; function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override; procedure SetBounds(R: TRect); override; end; var ActiveGridEditor: TBaseGridEditorLink=nil; implementation uses main; {procedure TAllKeysRadioButton.WMGetDlgCode(var Msg: TMessage); begin inherited; Msg.Result := Msg.Result or DLGC_WANTALLKEYS; end;} {procedure TAllKeysCheckBox.WMGetDlgCode(var Msg: TMessage); begin inherited; Msg.Result := Msg.Result or DLGC_WANTALLKEYS; end;} procedure TBaseGridEditorLink.Log(Msg: String); begin MainForm.LogSQL('#'+FInstanceId.ToString+': '+Msg, lcDebug); end; constructor TBaseGridEditorLink.Create; begin raise Exception.CreateFmt(_('Wrong constructor called: %s.%s. Instead, please call the overloaded version %s.%s.'), [Self.ClassName, 'Create', Self.ClassName, 'Create(VirtualStringTree)']); end; constructor TBaseGridEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited Create; FInstanceId := Random(100); FTree := Tree; // Enable mouse scrolling, plus ensure the editor component // is not partly hidden when it pops up in a bottom cell FParentForm := GetParentForm(FTree); // Avoid flicker FParentForm.Repaint; FMainControl := nil; FModified := False; FAllowEdit := AllowEdit; ActiveGridEditor := Self; FTableColumn := Col; end; destructor TBaseGridEditorLink.Destroy; var NewColumn, FirstCol, LastCol: TColumnIndex; NewNode: PVirtualNode; DoPrev: Boolean; begin ActiveGridEditor := nil; if Assigned(FMainControl) then begin FMainControl.WindowProc := FOldWindowProc; FMainControl := nil; end; if FLastKeyDown = VK_TAB then begin DoPrev := ssShift in FLastShiftState; // Advance to next/previous visible column/node. NewNode := FNode; NewColumn := FColumn; FirstCol := FTree.Header.Columns.GetFirstVisibleColumn; LastCol := FTree.Header.Columns.GetLastVisibleColumn; while true do begin // Find a column for the current node which can be focused. if DoPrev then begin if NewColumn = FirstCol then begin NewColumn := LastCol; NewNode := FTree.GetPreviousVisible(NewNode); end else NewColumn := FTree.Header.Columns.GetPreviousVisibleColumn(NewColumn); end else begin if NewColumn = LastCol then begin NewColumn := FirstCol; NewNode := FTree.GetNextVisible(NewNode); end else NewColumn := FTree.Header.Columns.GetNextVisibleColumn(NewColumn); end; if not Assigned(NewNode) then Break; if not FTree.CanEdit(NewNode, NewColumn) then Continue; FTree.ClearSelection; FTree.Selected[NewNode] := True; FTree.EditNode(NewNode, NewColumn); Break; end; end; inherited; end; function TBaseGridEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; var FCellTextBounds: TRect; HasNulls: Boolean; begin Result := not FStopping; if not Result then Exit; FNode := Node; FColumn := Column; FCellFont := TFont.Create; FCellTextBounds := Rect(0, 0, 0, 0); FTree.GetTextInfo(FNode, FColumn, FCellFont, FCellTextBounds, FCellText); HasNulls := False; apphelpers.RemoveNullChars(FCellText, HasNulls); if HasNulls and FAllowEdit then begin FAllowEdit := False; end; // Not all editors have a connection assigned, e.g. session manager tree if Assigned(FTableColumn) then begin FCellFont.Color := DatatypeCategories[FTableColumn.DataType.Category].Color; end; FCellBackground := FTree.Header.Columns[FColumn].Color; if Assigned(FMainControl) then begin FOldWindowProc := FMainControl.WindowProc; //FMainControl.WindowProc := TempWindowProc; TExtForm.FixControls(FMainControl); end; // Adjust editor position and allow repainting mainform SetBounds(FCellTextBounds); if not IsWine then FParentForm.Repaint; end; function TBaseGridEditorLink.BeginEdit: Boolean; begin Result := not FStopping; FBeginEditTime := GetTickCount; end; function TBaseGridEditorLink.CancelEdit: Boolean; begin Result := not FStopping; if Result then begin FStopping := True; FTree.CancelEditNode; if FTree.CanFocus then FTree.SetFocus; end; end; function TBaseGridEditorLink.EndEditHelper(NewText: String): Boolean; begin Result := not FStopping; if FStopping then Exit; FStopping := True; if FModified and FAllowEdit then FTree.Text[FNode, FColumn] := NewText; if FTree.CanFocus and (FLastKeyDown <> VK_TAB) then FTree.SetFocus; end; {procedure TBaseGridEditorLink.TempWindowProc(var Message: TMessage); begin case Message.Msg of WM_CHAR: // Catch hotkeys if not (TWMChar(Message).CharCode = VK_TAB) then FOldWindowProc(Message); WM_GETDLGCODE: // "WantTabs" mode for main control Message.Result := Message.Result or DLGC_WANTARROWS or DLGC_WANTALLKEYS or DLGC_WANTTAB; else begin try FOldWindowProc(Message); except // EAccessViolation occurring in some cases on E:Exception do begin Log(E.Message+' Message CharCode:'+TWMChar(Message).CharCode.ToString+' Msg:'+Message.Msg.ToString); end; end; end; end; end;} procedure TBaseGridEditorLink.ProcessMessage(var Message: TMessage); begin if (FMainControl <> nil) and FMainControl.HandleAllocated then FMainControl.WindowProc(Message); end; function TBaseGridEditorLink.GetBounds: TRect; stdcall; begin // Only important if the editor resizes itself. Result := Rect(0, 0, 0, 0); end; function TBaseGridEditorLink.GetCellRect(InnerTextBounds: Boolean): TRect; var Text: String; CellBounds, TextBounds: TRect; Ghosted: Boolean; ImageIndex: Integer; f: TFont; begin // Return the cell's rectangle, relative to the parent form. f := TFont.Create; TextBounds := Rect(0, 0, 0, 0); FTree.GetTextInfo(FNode, FColumn, f, TextBounds, Text); CellBounds := FTree.GetDisplayRect(FNode, FColumn, False); Inc(CellBounds.Left, Integer(FTree.GetNodeLevel(FNode)) * (Integer(FTree.Indent)+FTree.TextMargin)); if (toShowRoot in FTree.TreeOptions.PaintOptions) and (FColumn = FTree.Header.MainColumn) then begin // Reserve space for plus or minus button Inc(CellBounds.Left, FTree.Indent); end; if Assigned(FTree.Images) and Assigned(FTree.OnGetImageIndex) then begin // Reserve space for image ImageIndex := -1; Ghosted := False; FTree.OnGetImageIndex(FTree, FNode, ikNormal, FColumn, Ghosted, ImageIndex); if ImageIndex > -1 then Inc(CellBounds.Left, FTree.Images.Width+2); end; TextBounds.Left := CellBounds.Left + 2*FTree.TextMargin; if InnerTextBounds then begin // Inner bounds are considered to be relative to the outer cell bounds Result := Rect(TextBounds.Left-CellBounds.Left, TextBounds.Top-CellBounds.Top, CellBounds.Right-CellBounds.Left, // Far right edge of cell, not of text CellBounds.Bottom-CellBounds.Top ); end else begin // Recalculate top left corner of rectangle, so it is relative to the parent form (which is FParentForm) Result := CellBounds; Types.OffsetRect(Result, FTree.ClientOrigin.X - FParentForm.ClientOrigin.X, FTree.ClientOrigin.Y - FParentForm.ClientOrigin.Y ); Dec(Result.Bottom, 1); end; end; procedure TBaseGridEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin FLastKeyDown := Key; FLastShiftState := Shift; case Key of // Cancel by Escape VK_ESCAPE: FTree.CancelEditNode; // Apply changes and end editing by [Ctrl +] Enter or Tab VK_RETURN, VK_TAB: FTree.EndEditNode; end; end; procedure TBaseGridEditorLink.DoEndEdit(Sender: TObject); begin FTree.EndEditNode; end; procedure TBaseGridEditorLink.DoCancelEdit(Sender: TObject); begin FTree.CancelEditNode; end; constructor THexEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited; end; destructor THexEditorLink.Destroy; begin inherited; FForm.Close; FreeAndNil(FForm); end; function THexEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall; begin Result := inherited PrepareEdit(Tree, Node, Column); if not Result then Exit; // Create the text editor form FForm := TfrmBinEditor.Create(Ftree); FForm.SetFont(FCellFont); FForm.SetText(FCellText); FForm.SetTitleText(TitleText); FForm.SetMaxLength(MaxLength); FForm.memoText.ReadOnly := not FAllowEdit; end; function THexEditorLink.BeginEdit: Boolean; stdcall; begin Result := inherited BeginEdit; if Result then FForm.ShowModal; end; function THexEditorLink.CancelEdit: Boolean; begin Result := inherited CancelEdit; if Result then FForm.Close; end; function THexEditorLink.EndEdit: Boolean; stdcall; begin FForm.Close; FModified := FForm.Modified; Result := EndEditHelper(FForm.GetText); end; procedure THexEditorLink.SetBounds(R: TRect); stdcall; begin // Not in use, form's position is centered on mainform end; { DateTime editor } constructor TDateTimeEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited; FPanel := TPanel.Create(FParentForm); FPanel.Parent := FParentForm; FPanel.Hide; FPanel.ParentBackground := False; FPanel.BevelOuter := bvNone; FPanel.OnExit := DoEndEdit; FMaskEdit := TMaskEdit.Create(FPanel); FMaskEdit.Parent := FPanel; FMaskEdit.ParentColor := True; FMaskEdit.BorderStyle := bsNone; FMaskEdit.OnKeyDown := DoKeyDown; FMaskEdit.OnKeyUp := DoKeyUp; FMaskEdit.OnChange := TextChange; FMainControl := FMaskEdit; FUpDown := TUpDown.Create(FPanel); FUpDown.Parent := FPanel; FUpDown.OnChangingEx := UpDownChangingEx; FUpDown.OnMouseUp := UpDownMouseUp; FTimer := TTimer.Create(FMaskEdit); FTimer.Interval := 50; FTimer.OnTimer := DoOnTimer; FTimer.Enabled := False; end; destructor TDateTimeEditorLink.Destroy; begin AppSettings.WriteInt(asDateTimeEditorCursorPos, FMaskEdit.SelStart, IntToStr(Integer(FTableColumn.DataType.Category))); FreeAndNil(FTimer); FreeAndNil(FUpDown); FreeAndNil(FMaskEdit); FreeAndNil(FPanel); inherited; end; function TDateTimeEditorLink.BeginEdit: Boolean; stdcall; begin Result := inherited BeginEdit; if Result then begin FPanel.Show; FMaskEdit.SetFocus; // Focus very last segment of date FMaskEdit.SelStart := AppSettings.ReadInt(asDateTimeEditorCursorPos, IntToStr(Integer(FTableColumn.DataType.Category))); FMaskEdit.SelLength := 1; end; end; function TDateTimeEditorLink.EndEdit: Boolean; begin Result := EndEditHelper(Trim(FMaskEdit.Text)); end; function TDateTimeEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall; var MinColWidth, ForceTextLen: Integer; begin Result := inherited PrepareEdit(Tree, Node, Column); if not Result then Exit; FMaskEdit.ReadOnly := not FAllowEdit; case FTableColumn.DataType.Index of dbdtDate: FMaskEdit.EditMask := '0000-00-00;1; '; dbdtDatetime, dbdtDatetime2, dbdtTimestamp, dbdtInt, dbdtBigint: begin if MicroSecondsPrecision > 0 then FMaskEdit.EditMask := '0000-00-00 00\:00\:00.'+StringOfChar('0', MicroSecondsPrecision)+';1; ' else FMaskEdit.EditMask := '0000-00-00 00\:00\:00;1; '; end; dbdtTime: begin ForceTextLen := 10; if MicroSecondsPrecision > 0 then begin FMaskEdit.EditMask := '#900\:00\:00.'+StringOfChar('0', MicroSecondsPrecision)+';1; '; Inc(ForceTextLen, MicroSecondsPrecision + 1); end else FMaskEdit.EditMask := '#900\:00\:00;1; '; while Length(FCellText) < ForceTextLen do FCellText := ' ' + FCellText; end; dbdtYear: FMaskEdit.EditMask := '0000;1; '; end; FMaskEdit.Text := FCellText; FModified := False; FMaskEdit.Font.Assign(FCellFont); FPanel.Color := FCellBackground; // Auto-enlarge current tree column so the text in the edit is not cut MinColWidth := FTree.Canvas.TextWidth(FCellText) + FTree.TextMargin + FUpDown.Width + 5; if FTree.Header.Columns[FColumn].Width < MinColWidth then FTree.Header.Columns[FColumn].Width := MinColWidth; end; procedure TDateTimeEditorLink.SetBounds(R: TRect); stdcall; var EditRect: TRect; OldSelStart, OldSelLen: Integer; begin FPanel.BoundsRect := GetCellRect(False); FUpDown.Left := FPanel.Width - FUpDown.Width; FUpDown.Height := FPanel.Height; EditRect := GetCellRect(True); EditRect.Right := FUpDown.Left; FMaskEdit.BoundsRect := EditRect; OldSelStart := FMaskEdit.SelStart; OldSelLen := FMaskEdit.SelLength; FMaskEdit.SelStart := 0; FMaskEdit.SelStart := OldSelStart; FMaskEdit.SelLength := OldSelLen; end; procedure TDateTimeEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin inherited DoKeyDown(Sender, Key, Shift); if (Key in [VK_UP, VK_DOWN]) and (not FTimer.Enabled) then begin if Key = VK_UP then FModifyOffset := 1 else FModifyOffset := -1; FTimerCalls := 0; DoOnTimer(Sender); FTimer.Enabled := True; end; end; procedure TDateTimeEditorLink.DoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin FTimer.Enabled := False; end; procedure TDateTimeEditorLink.UpDownChangingEx(Sender: TObject; var AllowChange: Boolean; NewValue: SmallInt; Direction: TUpDownDirection); begin if FTimer.Enabled then Exit; if Direction = updUp then FModifyOffset := 1 else FModifyOffset := -1; FTimerCalls := 0; DoOnTimer(Sender); FTimer.Enabled := True; end; procedure TDateTimeEditorLink.UpDownMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin FTimer.Enabled := False; end; procedure TDateTimeEditorLink.DoOnTimer(Sender: TObject); var DelayCalls: Integer; begin Inc(FTimerCalls); // short delay before counting up/down DelayCalls := 350 Div FTimer.Interval; if (FTimerCalls > DelayCalls) or (not (Sender is TTimer)) then ModifyDate(FModifyOffset); // Speed up counting in steps if FTimerCalls in [DelayCalls*5, DelayCalls*10] then begin if FModifyOffset > 0 then Inc(FModifyOffset, 3) else Dec(FModifyOffset, 3); end; end; procedure TDateTimeEditorLink.ModifyDate(Offset: Integer); var dt: TDateTime; d: TDate; i, MaxSeconds, MinSeconds: Int64; text: String; OldSelStart, OldSelLength, ms, DotPos: Integer; function TimeToSeconds(Str: String): Int64; var Hours: String; Seconds: Int64; begin Hours := Trim(Copy(Str, 1, 4)); Result := MakeInt(Hours) * 60 * 60; Seconds := MakeInt(Copy(Str, 6, 2)) * 60 + MakeInt(Copy(Str, 9, 2)); if (Result < 0) or ((Result = 0) and (Hours[1]='-')) then Dec(Result, Seconds) else Inc(Result, Seconds); end; function SecondsToTime(Seconds: Int64): String; var HoursNum, Minutes: Int64; Hours: String; begin HoursNum := Abs(Seconds div (60*60)); Hours := IntToStr(HoursNum); if Length(Hours) = 1 then Hours := '0' + Hours; if Seconds < 0 then Hours := '-' + Hours; Seconds := Abs(Seconds) mod (60*60); Minutes := Seconds div 60; Seconds := Seconds mod 60; Result := Format('%4s:%.2u:%.2u', [Hours, Minutes, Seconds]); end; begin try // Detect microseconds part of value if any if MicroSecondsPrecision > 0 then begin DotPos := Length(FMaskEdit.Text) - Pos('.', ReverseString(FMaskEdit.Text)) + 2; ms := MakeInt(Copy(FMaskEdit.Text, DotPos, Length(FMaskEdit.Text))); end else ms := 0; case FTableColumn.DataType.Index of dbdtYear: begin i := MakeInt(FMaskEdit.Text); i := i + Offset; text := IntToStr(i); end; dbdtDate: begin d := StrToDate(FMaskEdit.Text); // De- or increase focused date segment case FMaskEdit.SelStart of 0..3: d := IncYear(d, Offset); 5,6: d := IncMonth(d, Offset); 8..10: d := IncDay(d, Offset); end; text := DateToStr(d); end; dbdtDateTime, dbdtDateTime2, dbdtTimestamp, dbdtInt, dbdtBigint: begin dt := StrToDateTime(FMaskEdit.Text); case FMaskEdit.SelStart of 0..3: dt := IncYear(dt, Offset); 5,6: dt := IncMonth(dt, Offset); 8,9: dt := IncDay(dt, Offset); 11,12: dt := IncHour(dt, Offset); 14,15: dt := IncMinute(dt, Offset); 17..19: dt := IncSecond(dt, Offset); 20..26: Inc(ms, Offset); end; text := DateTimeToStr(dt); if Length(text) = 10 then text := text + ' 00:00:00'; if MicroSecondsPrecision > 0 then text := text + '.' + Format('%.'+IntToStr(MicroSecondsPrecision)+'d', [ms]); end; dbdtTime: begin i := TimeToSeconds(FMaskEdit.Text); case FMaskEdit.SelStart of 0..3: Inc(i, Offset*60*60); 5,6: Inc(i, Offset*60); 8,9: Inc(i, Offset); 10..16: Inc(ms, Offset); end; // Stop at max and min values. See http://dev.mysql.com/doc/refman/5.0/en/time.html MaxSeconds := 839*60*60-1; MinSeconds := -(MaxSeconds); if i > MaxSeconds then i := MaxSeconds; if i < MinSeconds then i := MinSeconds; text := SecondsToTime(i); if MicroSecondsPrecision > 0 then text := text + '.' + Format('%.'+IntToStr(MicroSecondsPrecision)+'d', [ms]); end; else text := ''; end; if text <> '' then begin OldSelStart := FMaskEdit.SelStart; OldSelLength := FMaskEdit.SelLength; FMaskEdit.Text := text; FMaskEdit.SelStart := OldSelStart; FMaskEdit.SelLength := OldSelLength; end; except on E:EConvertError do begin // Ignore any DateToStr exception. Should only appear in cases where the users // enters invalid dates end else raise; end; end; procedure TDateTimeEditorLink.TextChange; begin FModified := True; end; function TDateTimeEditorLink.MicroSecondsPrecision: Integer; var rx: TRegExpr; begin if not FTableColumn.LengthSet.IsEmpty then Result := MakeInt(FTableColumn.LengthSet) else begin // Find default length of supported microseconds in datatype definition // See dbstructures rx := TRegExpr.Create; rx.Expression := '\.([^\.]+)$'; if rx.Exec(FTableColumn.DataType.Format) then Result := rx.MatchLen[1] else Result := 0; rx.Free; end; // No microseconds for UNIX timestamp columns if FTableColumn.DataType.Index in [dbdtInt, dbdtBigint] then Result := 0; end; { Enum editor } constructor TEnumEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited; AllowCustomText := False; ItemMustExist := False; FCombo := TExtComboBox.Create(FParentForm); FCombo.Hide; FCombo.Parent := FParentForm; FCombo.OnKeyDown := DoKeyDown; FCombo.OnExit := DoEndEdit; FCombo.OnSelect := DoSelect; // Show some more than the default 8 items FCombo.DropDownCount := 16; ValueList := TStringList.Create; DisplayList := TStringList.Create; FMainControl := FCombo; end; destructor TEnumEditorLink.Destroy; begin FCombo.Free; inherited; end; function TEnumEditorLink.BeginEdit: Boolean; stdcall; begin Result := inherited BeginEdit; if Result then begin FCombo.Show; FCombo.SetFocus; end; end; function TEnumEditorLink.EndEdit: Boolean; stdcall; var NewText: String; begin if AllowCustomText and FAllowEdit and (not ItemMustExist) then NewText := FCombo.Text else if (ValueList.Count > 0) and (FCombo.ItemIndex > -1) then NewText := ValueList[FCombo.ItemIndex] else NewText := ''; FModified := NewText <> FCellText; Result := EndEditHelper(NewText); end; function TEnumEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall; begin Result := inherited PrepareEdit(Tree, Node, Column); if Result then begin if DisplayList.Count = ValueList.Count then FCombo.Items.AddStrings(DisplayList) else FCombo.Items.AddStrings(ValueList); FCombo.ItemIndex := ValueList.IndexOf(FCellText); if AllowCustomText and FAllowEdit then begin FCombo.Style := csDropDown; FCombo.Text := FCellText; end else begin // Set style to OwnerDraw, otherwise we wouldn't be able to adjust the combo's height FCombo.Style := csOwnerDrawFixed; end; end; end; procedure TEnumEditorLink.SetBounds(R: TRect); stdcall; begin FCombo.BoundsRect := GetCellRect(False); end; procedure TEnumEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin // Work around a magic automatic TAB key arriving the editor if the user got // into this cell via TAB. Only seen for a TComboBox with style=csDropDown. // See issue #2809 if (not AllowCustomText) and (GetTickCount-FBeginEditTime > 200) then inherited; end; procedure TEnumEditorLink.DoSelect(Sender: TObject); begin // Read only mode? if not FAllowEdit then begin FCombo.ItemIndex := ValueList.IndexOf(FCellText); end; end; { SET editor } constructor TSetEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited; ValueList := TStringList.Create; FPanel := TPanel.Create(FParentForm); FPanel.Hide; FPanel.Parent := FParentForm; FPanel.ParentBackground := False; FPanel.Height := TExtForm.ScaleSize(150, FParentForm); FPanel.OnExit := DoEndEdit; FCheckList := TCheckListBox.Create(FPanel); FCheckList.Parent := FPanel; FCheckList.OnKeyDown := DoKeyDown; FMainControl := FCheckList; FBtnOk := TButton.Create(FPanel); FBtnOk.Parent := FPanel; FBtnOk.Caption := _('OK'); FBtnOk.OnClick := BtnOkClick; FBtnCancel := TButton.Create(FPanel); FBtnCancel.Parent := FPanel; FBtnCancel.Caption := _('Cancel'); FBtnCancel.OnClick := BtnCancelClick; FEndTimer := TTimer.Create(FPanel); FEndTimer.Interval := 50; FEndTimer.Enabled := False; end; destructor TSetEditorLink.Destroy; begin FreeAndNil(FPanel); inherited; end; function TSetEditorLink.BeginEdit: Boolean; stdcall; begin Result := inherited BeginEdit; if Result then begin FPanel.Show; FCheckList.SetFocus; end; end; function TSetEditorLink.EndEdit: Boolean; stdcall; var newtext: String; i: Integer; begin Result := not FStopping; if FStopping then Exit; newText := ''; for i := 0 to FCheckList.Items.Count - 1 do if FCheckList.Checked[i] then newText := newText + FCheckList.Items[i] + ','; Delete(newText, Length(newText), 1); FModified := newText <> FCellText; Result := EndEditHelper(newText); end; function TSetEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall; var SelValues: TStringList; i: Integer; begin Result := inherited PrepareEdit(Tree, Node, Column); if not Result then Exit; FCheckList.Font.Assign(FCellFont); FCheckList.Items.Assign(ValueList); SelValues := TStringList.Create; SelValues.Delimiter := ','; SelValues.StrictDelimiter := True; SelValues.DelimitedText := FCellText; for i:=0 to FCheckList.Items.Count-1 do begin FCheckList.Checked[i] := SelValues.IndexOf(FCheckList.Items[i]) > -1; FCheckList.ItemEnabled[i] := FAllowEdit; end; SelValues.Free; end; procedure TSetEditorLink.SetBounds(R: TRect); stdcall; const margin = 5; begin R := GetCellRect(False); FPanel.Top := R.Top; FPanel.Left := R.Left; FPanel.Width := R.Width; FBtnOk.Width := (FPanel.Width - 3*margin) div 2; FBtnOk.Left := margin; FBtnOk.Height := TExtForm.ScaleSize(24, FParentForm); FBtnOk.Top := FPanel.Height - 2*margin - FBtnOk.Height; FBtnOk.Enabled := FAllowEdit; FBtnCancel.Width := FBtnOk.Width; FBtnCancel.Left := 2*margin + FBtnOk.Width; FBtnCancel.Height := FBtnOk.Height; FBtnCancel.Top := FBtnOk.Top; FCheckList.Top := margin; FCheckList.Left := margin; FCheckList.Width := FPanel.Width - 2*margin; FCheckList.Height := FBtnOk.Top - margin - FCheckList.Top; // FCheckList.Enabled := FAllowEdit; // crashes with "cannot focus if disabled" end; procedure TSetEditorLink.BtnOkClick(Sender: TObject); begin // Timer based click on OK button, to prevent crash when theming is active FEndTimer.OnTimer := DoEndEdit; FEndTimer.Enabled := True; end; procedure TSetEditorLink.BtnCancelClick(Sender: TObject); begin // Timer based click on Cancel button, to prevent crash when theming is active FEndTimer.OnTimer := DoCancelEdit; FEndTimer.Enabled := True; end; { TInplaceEditorLink } constructor TInplaceEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited; ButtonVisible := false; FPanel := TPanel.Create(FParentForm); FPanel.Parent := FParentForm; FPanel.Hide; FPanel.ParentBackground := False; FPanel.BevelOuter := bvNone; FPanel.OnExit := DoEndEdit; FEdit := TEdit.Create(FPanel); FEdit.Parent := FPanel; FEdit.ParentColor := True; FEdit.BorderStyle := bsNone; FEdit.OnKeyDown := DoKeyDown; FMainControl := FEdit; FButton := TButton.Create(FPanel); FButton.Parent := FPanel; FButton.TabStop := False; FButton.Caption := '…'; FButton.Hint := _('Edit text in popup editor ...'); FButton.ShowHint := True; FButton.OnClick := ButtonClick; end; destructor TInplaceEditorLink.Destroy; begin if not ((csDestroying in FPanel.ComponentState) or (csCreating in FPanel.ControlState)) then begin FEdit.Free; FButton.Free; FPanel.Free; end; inherited; end; function TInplaceEditorLink.BeginEdit: Boolean; begin Result := inherited BeginEdit; if Result then begin FButton.Visible := ButtonVisible; SetBounds(Rect(0, 0, 0, 0)); if (Length(FEdit.Text) > SIZE_KB) or (ScanLineBreaks(FEdit.Text) <> lbsNone) then ButtonClick(FTree) else begin FPanel.Show; FEdit.SetFocus; end; end; end; function TInplaceEditorLink.EndEdit: Boolean; var NewText: String; begin Result := not FStopping; if FStopping then Exit; NewText := FEdit.Text; FModified := NewText <> FCellText; Result := EndEditHelper(NewText); end; procedure TInplaceEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin inherited DoKeyDown(Sender, Key, Shift); if Key = VK_F2 then ButtonClick(FButton); end; procedure TInplaceEditorLink.ButtonClick(Sender: TObject); var Editor: TfrmTextEditor; begin if not FButton.Visible then Exit; // Button was invisible, but hotkey was pressed Editor := TfrmTextEditor.Create(FTree); Editor.SetFont(MainForm.SynMemoQuery.Font); Editor.SetText(FEdit.Text); if FEdit.HandleAllocated then begin Editor.MemoText.SelStart := FEdit.SelStart; Editor.MemoText.SelEnd := FEdit.SelStart + FEdit.SelLength; end; Editor.SetTitleText(TitleText); Editor.Modified := FEdit.Modified; Editor.SetMaxLength(FMaxLength); Editor.TableColumn := FTableColumn; Editor.MemoText.ReadOnly := not FAllowEdit; if Editor.ShowModal = mrYes then begin FEdit.Text := Editor.GetText; DoEndEdit(Sender); end else begin DoCancelEdit(Sender); end; Editor.Free; end; function TInplaceEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; begin Result := inherited PrepareEdit(Tree, Node, Column); if not Result then Exit; FEdit.ReadOnly := not FAllowEdit; FEdit.Font.Assign(FCellFont); FEdit.Font.Color := GetThemeColor(clWindowText); FPanel.Color := FCellBackground; FEdit.Text := FCellText; FEdit.Modified := False; end; procedure TInplaceEditorLink.SetBounds(R: TRect); begin if not FStopping then begin // Position edit control according to cell text bounds FPanel.BoundsRect := GetCellRect(False); R := GetCellRect(True); if FButton.Visible then Dec(R.Right, TExtForm.ScaleSize(20, FPanel)); FEdit.BoundsRect := R; FButton.BoundsRect := Rect(FEdit.BoundsRect.Right, 0, FPanel.Width, FPanel.Height); end; end; { Column default editor } constructor TColumnDefaultEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); var SQLFunc: TSQLFunction; m: Integer; begin inherited; // Margin between controls and to edge of panel m := TExtForm.ScaleSize(5, FParentForm); FPanel := TPanel.Create(FParentForm); FPanel.Hide; FPanel.Parent := FParentForm; FPanel.OnExit := DoEndEdit; FPanel.ParentBackground := False; FPanel.Color := GetThemeColor(clWindow); //FPanel.BevelKind := bkFlat; FPanel.BevelOuter := bvRaised; FPanel.DoubleBuffered := True; // Avoid flicker? FMainControl := FPanel; FRadioNothing := TAllKeysRadioButton.Create(FPanel); FRadioNothing.Parent := FPanel; FRadioNothing.Top := m; FRadioNothing.Left := m; FRadioNothing.Width := FRadioNothing.Parent.Width - 2 * FRadioNothing.Left; FRadioNothing.OnClick := RadioClick; FRadioNothing.OnKeyDown := DoKeyDown; FRadioNothing.Caption := _('No default value'); FRadioText := TAllKeysRadioButton.Create(FPanel); FRadioText.Parent := FPanel; FRadioText.Top := FRadioNothing.Top + FRadioNothing.Height + m;; FRadioText.Left := m; FRadioText.Width := FRadioText.Parent.Width - 2 * FRadioText.Left; FRadioText.OnClick := RadioClick; FRadioText.OnKeyDown := DoKeyDown; FRadioText.Caption := _('Custom text')+':'; FTextDropDown := TPopupMenu.Create(FPanel); FTextEdit := TEditButton.Create(FPanel); FTextEdit.Parent := FPanel; FTextEdit.Top := FRadioText.Top + FRadioText.Height + m; FTextEdit.Left := 2*m; FTextEdit.Width := FTextEdit.Parent.Width - 2*FTextEdit.Left; FTextEdit.OnChange := EditChange; FTextEdit.Images := Tree.Images; FTextEdit.ImageIndex := 75; // Drop down arrow FTextEdit.OnButtonClick := EditButtonClick; //FTextEdit.RightButton.DropDownMenu := FTextDropDown; FRadioNull := TAllKeysRadioButton.Create(FPanel); FRadioNull.Parent := FPanel; FRadioNull.Top := FTextEdit.Top + FTextEdit.Height + 2*m; FRadioNull.Left := m; FRadioNull.Width := FRadioNull.Parent.Width - 2 * FRadioNull.Left; FRadioNull.OnClick := RadioClick; FRadioNull.OnKeyDown := DoKeyDown; FRadioNull.Caption := 'NULL'; FRadioExpression := TAllKeysRadioButton.Create(FPanel); FRadioExpression.Parent := FPanel; FRadioExpression.Top := FRadioNull.Top + FRadioNull.Height + m; FRadioExpression.Left := m; FRadioExpression.Width := FRadioExpression.Parent.Width - 2 * FRadioExpression.Left; FRadioExpression.OnClick := RadioClick; FRadioExpression.OnKeyDown := DoKeyDown; FRadioExpression.Caption := _('Expression')+':'; FExpressionEdit := TComboBox.Create(FPanel); FExpressionEdit.Parent := FPanel; FExpressionEdit.Top := FRadioExpression.Top + FRadioExpression.Height + m; FExpressionEdit.Left := 2*m; FExpressionEdit.Width := FExpressionEdit.Parent.Width - 2*FExpressionEdit.Left; FExpressionEdit.OnChange := EditChange; FExpressionEdit.DropDownCount := 20; for SQLFunc in FTableColumn.Connection.SQLFunctions do begin FExpressionEdit.Items.Add(SQLFunc.Name + SQLFunc.Declaration); end; FlblOnUpdate := TLabel.Create(FPanel); FlblOnUpdate.Parent := FPanel; FlblOnUpdate.Top := FExpressionEdit.Top + FExpressionEdit.Height + m; FlblOnUpdate.Left := 2*m; FlblOnUpdate.Width := FlblOnUpdate.Parent.Width - 2*FlblOnUpdate.Left; FlblOnUpdate.Caption := _('On update') + ':'; FOnUpdateEdit := TComboBox.Create(FPanel); FOnUpdateEdit.Parent := FPanel; FOnUpdateEdit.Top := FlblOnUpdate.Top + FlblOnUpdate.Height + m; FOnUpdateEdit.Left := 2*m; FOnUpdateEdit.Width := FOnUpdateEdit.Parent.Width - 2*FOnUpdateEdit.Left; FOnUpdateEdit.OnChange := EditChange; FOnUpdateEdit.DropDownCount := 20; for SQLFunc in FTableColumn.Connection.SQLFunctions do begin FOnUpdateEdit.Items.Add(SQLFunc.Name + SQLFunc.Declaration); end; FRadioAutoInc := TAllKeysRadioButton.Create(FPanel); FRadioAutoInc.Parent := FPanel; FRadioAutoInc.Top := FOnUpdateEdit.Top + FOnUpdateEdit.Height + m; FRadioAutoInc.Left := m; FRadioAutoInc.Width := FRadioAutoInc.Parent.Width - 2 * FRadioAutoInc.Left; FRadioAutoInc.OnClick := RadioClick; FRadioAutoInc.OnKeyDown := DoKeyDown; FRadioAutoInc.Caption := Col.AutoIncName; FBtnOk := TButton.Create(FPanel); FBtnOk.Parent := FPanel; FBtnOk.Width := TExtForm.ScaleSize(60, FParentForm); FBtnOk.Top := FRadioAutoInc.Top + FRadioAutoInc.Height + m; FBtnOk.Left := FPanel.Width - 3*m - 2*FBtnOk.Width - 2*FPanel.BorderWidth; FBtnOk.OnClick := BtnOkClick; FBtnOk.Default := True; FBtnOk.Caption := _('OK'); FBtnCancel := TButton.Create(FPanel); FBtnCancel.Parent := FPanel; FBtnCancel.Top := FBtnOk.Top; FBtnCancel.Width := FBtnOk.Width; FBtnCancel.Left := FBtnOk.Left + FBtnOk.Width + m; FBtnCancel.OnClick := BtnCancelClick; FBtnCancel.Cancel := True; FBtnCancel.Caption := _('Cancel'); FEndTimer := TTimer.Create(FPanel); FEndTimer.Interval := 50; FEndTimer.Enabled := False; // Set outer panel (minimum) dimensions. Width is set in .SetBounds() FPanel.Height := 2*FPanel.BorderWidth + FBtnOk.Top + FBtnOk.Height + 2*m; FPanel.Constraints.MinWidth := 2*m + FBtnOK.Width + m + FBtnCancel.Width + 2*m; // Set anchors for all controls, so they are sticky when resizing the underlying column width FRadioNothing.Anchors := [akLeft, akTop, akRight]; FRadioText.Anchors := [akLeft, akTop, akRight]; FTextEdit.Anchors := [akLeft, akTop, akRight, akBottom]; FRadioNull.Anchors := [akLeft, akBottom, akRight]; FRadioExpression.Anchors := [akLeft, akBottom, akRight]; FExpressionEdit.Anchors := [akLeft, akBottom, akRight]; FOnUpdateEdit.Anchors := [akLeft, akBottom, akRight]; FRadioAutoInc.Anchors := [akLeft, akBottom, akRight]; FBtnOk.Anchors := [akBottom, akRight]; FBtnCancel.Anchors := FBtnOk.Anchors; end; destructor TColumnDefaultEditorLink.Destroy; begin FPanel.Free; inherited; end; function TColumnDefaultEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall; var ValueList, SelectedValues: TStringList; i: Integer; Item: TMenuItem; begin inherited PrepareEdit(Tree, Node, Column); // Check relevant radio button FRadioNothing.Checked := DefaultType = cdtNothing; FRadioText.Checked := DefaultType = cdtText; FRadioNull.Checked := DefaultType = cdtNull; FRadioExpression.Checked := DefaultType = cdtExpression; FRadioAutoInc.Checked := DefaultType = cdtAutoInc; if FRadioText.Checked then begin FTextEdit.Text := DefaultText; end; if FRadioExpression.Checked then begin FExpressionEdit.Text := DefaultText; end; FOnUpdateEdit.Text := OnUpdateText; // Disable non working default options per data type FRadioAutoInc.Enabled := FRadioAutoInc.Checked or (FTableColumn.DataType.Category = dtcInteger); // Provide items with a check mark for ENUM and SET columns if FTableColumn.DataType.Index in [dbdtEnum, dbdtSet] then begin FTextEdit.Button.Enabled := True; ValueList := FTableColumn.ValueList; SelectedValues := Explode(',', FTextEdit.Text); for i:=0 to ValueList.Count-1 do begin Item := TMenuItem.Create(FTextDropDown); Item.Caption := ValueList[i]; Item.RadioItem := FTableColumn.DataType.Index = dbdtEnum; Item.Checked := SelectedValues.IndexOf(Item.Caption) > -1; Item.OnClick := EditDropDownClick; FTextDropDown.Items.Add(Item); end; ValueList.Free; SelectedValues.Free; end; Result := True; end; procedure TColumnDefaultEditorLink.SetBounds(R: TRect); stdcall; var CellRect: TRect; P: TPoint; Room: Integer; begin CellRect := GetCellRect(False); FPanel.Left := CellRect.Left; FPanel.Top := CellRect.Top; FPanel.Width := CellRect.Width; // Reposition editor so it's not outside the main form P := FParentForm.ClientToScreen(FPanel.BoundsRect.TopLeft); Room := FParentForm.BoundsRect.Bottom - 8 {Borderwidth} - (P.Y + FPanel.Height); if Room < 0 then FPanel.Top := CellRect.Top + Room; P := FParentForm.ClientToScreen(FPanel.BoundsRect.BottomRight); Room := FParentForm.BoundsRect.Right - 8 {Borderwidth} - P.X; if Room < 0 then FPanel.Left := CellRect.Left + Room; end; function TColumnDefaultEditorLink.BeginEdit: Boolean; stdcall; begin Result := not FStopping; if Result then begin FPanel.Show; if FRadioNothing.Checked then FRadioNothing.SetFocus else if FRadioText.Checked then FTextEdit.SetFocus else if FRadioNull.Checked then FRadioNull.SetFocus else if FRadioExpression.Checked then FExpressionEdit.SetFocus else if FRadioAutoInc.Checked then FRadioAutoInc.SetFocus; end; end; function TColumnDefaultEditorLink.EndEdit: Boolean; stdcall; var Col: PTableColumn; begin Result := not FStopping; if Result then begin FStopping := True; Col := FTree.GetNodeData(FNode); if FRadioNothing.Checked then Col.DefaultType := cdtNothing else if FRadioText.Checked then Col.DefaultType := cdtText else if FRadioNull.Checked then Col.DefaultType := cdtNull else if FRadioExpression.Checked then Col.DefaultType := cdtExpression else if FRadioAutoInc.Checked then Col.DefaultType := cdtAutoInc else Col.DefaultType := cdtText; case Col.DefaultType of cdtNothing: Col.DefaultText := ''; cdtText: Col.DefaultText := FTextEdit.Text; cdtNull: Col.DefaultText := 'NULL'; cdtExpression: Col.DefaultText := FExpressionEdit.Text; cdtAutoInc: Col.DefaultText := Col.AutoIncName; end; if FOnUpdateEdit.Text <> '' then Col.OnUpdateType := cdtExpression else Col.OnUpdateType := cdtNothing; Col.OnUpdateText := FOnUpdateEdit.Text; FTree.Text[FNode, FColumn] := Col.DefaultText; if FTree.CanFocus then FTree.SetFocus; end; end; procedure TColumnDefaultEditorLink.RadioClick(Sender: TObject); begin if not FRadioText.Checked then FTextEdit.Color := clBtnFace else begin FTextEdit.Color := clWindow; if FTextEdit.CanFocus then FTextEdit.SetFocus; end; if not FRadioExpression.Checked then FExpressionEdit.Color := clBtnFace else begin FExpressionEdit.Color := clWindow; if FExpressionEdit.CanFocus then FExpressionEdit.SetFocus; end; FModified := True; end; procedure TColumnDefaultEditorLink.EditChange(Sender: TObject); begin if Sender = FTextEdit then FRadioText.Checked := True else if Sender = FExpressionEdit then FRadioExpression.Checked := True; FModified := True; end; procedure TColumnDefaultEditorLink.EditButtonClick(Sender: TObject); begin TExtForm.ShowPopup(FTextEdit.Button, FTextDropDown); end; procedure TColumnDefaultEditorLink.EditDropDownClick(Sender: TObject); var Item: TMenuItem; NewValue: String; begin // ENUM or SET value clicked in drop down menu Item := Sender as TMenuItem; Item.Checked := not Item.Checked; NewValue := ''; for Item in Item.GetParentMenu.Items do begin if Item.Checked then NewValue := NewValue + StripHotkey(Item.Caption) + ','; end; if not NewValue.IsEmpty then Delete(NewValue, Length(NewValue), 1); FTextEdit.Text := NewValue; FModified := True; end; procedure TColumnDefaultEditorLink.BtnOkClick(Sender: TObject); begin // Timer based click on OK button, to prevent crash when theming is active FEndTimer.OnTimer := DoEndEdit; FEndTimer.Enabled := True; end; procedure TColumnDefaultEditorLink.BtnCancelClick(Sender: TObject); begin // Timer based click on Cancel button, to prevent crash when theming is active FEndTimer.OnTimer := DoCancelEdit; FEndTimer.Enabled := True; end; { Datatype selector } constructor TDataTypeEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); begin inherited; FTreeSelect := TVirtualStringTree.Create(FParentForm); FTreeSelect.Hide; FTreeSelect.TreeOptions.PaintOptions := FTreeSelect.TreeOptions.PaintOptions - [toShowTreeLines, toShowButtons, toShowRoot] + [toHotTrack, toUseExplorerTheme, toHideTreeLinesIfThemed]; FTreeSelect.TreeOptions.SelectionOptions := FTreeSelect.TreeOptions.SelectionOptions + [toFullRowSelect]; FTreeSelect.Header.Columns.Add; FTreeSelect.Parent := FParentForm; FTreeSelect.TextMargin := 0; //FTreeSelect.BorderStyle := bsNone; //FTreeSelect.BevelKind := bkFlat; //FTreeSelect.BevelInner := bvNone; FTreeSelect.IncrementalSearch := isAll; FTreeSelect.RootNodeCount := Length(DatatypeCategories); FTreeSelect.OnGetText := DoTreeSelectGetText; FTreeSelect.OnInitNode := DoTreeSelectInitNode; FTreeSelect.OnInitChildren := DoTreeSelectInitChildren; FTreeSelect.OnKeyDown := DoKeyDown; FTreeSelect.OnHotChange := DoTreeSelectHotChange; FTreeSelect.OnPaintText := DoTreeSelectPaintText; FTreeSelect.OnExit := DoEndEdit; // See further events in PrepareEdit FixVT(FTreeSelect); FMainControl := FTreeSelect; FMemoHelp := TMemo.Create(FParentForm); FMemoHelp.Hide; FMemoHelp.Parent := FParentForm; FMemoHelp.Color := clInfoBk; FMemoHelp.Font.Color := clInfoText; //FMemoHelp.BevelKind := bkFlat; {if TStyleManager.IsCustomStyleActive then begin FMemoHelp.BorderStyle := bsSingle; FMemoHelp.BevelInner := bvNone; end else begin FMemoHelp.BorderStyle := bsNone; FMemoHelp.BevelInner := bvSpace; end;} end; destructor TDataTypeEditorLink.Destroy; begin FreeAndNil(FTreeSelect); FreeAndNil(FMemoHelp); inherited; end; function TDataTypeEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; var dt: TDBDatatype; CatNode, TypeNode: PVirtualNode; begin Result := inherited PrepareEdit(Tree, Node, Column); if not Result then Exit; // Just use font name and size, avoid bold style and white color FTreeSelect.Font.Name := FCellFont.Name; FTreeSelect.Font.Size := FCellFont.Size; // Find and select current datatype in tree dt := FTableColumn.Connection.GetDataTypeByName(FCellText, False, FTableColumn.Name); CatNode := FTreeSelect.GetFirst; while Assigned(CatNode) do begin // Since recent update to VT 5.2.1 we need to initialize root nodes by hand for some reason: FTreeSelect.ReinitNode(CatNode, True); if CatNode.Index = Cardinal(dt.Category) then begin TypeNode := FTreeSelect.GetFirstChild(CatNode); while Assigned(TypeNode) do begin if FTreeSelect.Text[TypeNode, 0] = FCellText then begin FTreeSelect.FocusedNode := TypeNode; FTreeSelect.Selected[TypeNode] := True; break; end; TypeNode := FTreeSelect.GetNextSibling(TypeNode); end; end; CatNode := FTreeSelect.GetNextSibling(CatNode); end; FTreeSelect.Header.AutoFitColumns(False, smaUseColumnOption, 0, 0); if Assigned(FTreeSelect.FocusedNode) then FTreeSelect.ScrollIntoView(FTreeSelect.FocusedNode, True); FTreeSelect.OnFocusChanging := DoTreeSelectFocusChanging; FTreeSelect.OnFocusChanged := DoTreeSelectFocusChanged; FTreeSelect.OnClick := DoEndEdit; end; function TDataTypeEditorLink.BeginEdit: Boolean; begin Result := inherited BeginEdit; if Result then begin FTreeSelect.Show; FTreeSelect.SetFocus; end; end; function TDataTypeEditorLink.EndEdit: Boolean; begin if Assigned(FTreeSelect.FocusedNode) then Result := EndEditHelper(FTreeSelect.Text[FTreeSelect.FocusedNode, 0]) else Result := FTree.CancelEditNode; end; procedure TDataTypeEditorLink.SetBounds(R: TRect); var CellRect: TRect; TreeHeight: Integer; begin // Set position of tree. As the tree's parent is mainform, not listcolumns, add listcolumn's x + y positions CellRect := GetCellRect(False); // Do not exceed lower edge of mainform, as that portion would be hidden TreeHeight := Min(250, FParentForm.ClientHeight-CellRect.Top-10); FTreeSelect.SetBounds(CellRect.Left, CellRect.Top, FTreeSelect.Header.Columns[0].Width + GetSystemMetrics(SM_CXVSCROLL) + 5, TreeHeight); end; procedure TDataTypeEditorLink.DoTreeSelectInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); begin // First level nodes always expanded if Sender.GetNodeLevel(Node) = 0 then InitialStates := InitialStates + [ivsExpanded, ivsHasChildren]; end; procedure TDataTypeEditorLink.DoTreeSelectInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); var i: Integer; begin // Tell number of datatypes per category ChildCount := 0; if Sender.GetNodeLevel(Node) = 0 then for i:=0 to High(FTableColumn.Connection.Datatypes) do begin if FTableColumn.Connection.Datatypes[i].Category = TDBDatatypeCategoryIndex(Node.Index) then Inc(ChildCount); end; end; procedure TDataTypeEditorLink.DoTreeSelectGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: String); var i: Integer; Counter: Cardinal; begin // Get cell text case Sender.GetNodeLevel(Node) of 0: CellText := DatatypeCategories[TDBDatatypeCategoryIndex(Node.Index)].Name; 1: begin Counter := 0; for i:=0 to High(FTableColumn.Connection.Datatypes) do begin if FTableColumn.Connection.Datatypes[i].Category = TDBDatatypeCategoryIndex(Node.Parent.Index) then begin Inc(Counter); if Counter = Node.Index+1 then begin CellText := FTableColumn.Connection.Datatypes[i].Name; break; end; end; end; end; end; end; procedure TDataTypeEditorLink.DoTreeSelectHotChange(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode); var R: TRect; NodeText: String; bmp: TBitMap; begin // Display help box for hovered datatype FMemoHelp.Clear; if Assigned(NewNode) and (Sender.GetNodeLevel(NewNode) = 1) then begin R := FTreeSelect.GetDisplayRect(NewNode, 0, False); NodeText := FTreeSelect.Text[NewNode, 0]; FMemoHelp.Width := Min(250, FTreeSelect.Left); FMemoHelp.Left := FTreeSelect.Left - FMemoHelp.Width + (Integer(FTreeSelect.Indent) Div 2); FMemoHelp.Top := FTreeSelect.Top + R.Top + 3; FMemoHelp.Text := FTableColumn.Connection.GetDatatypeByName(NodeText, False, FTableColumn.Name).Description; // Calc height of memo bmp := TBitMap.Create; bmp.Canvas.Font.Assign(FMemoHelp.Font); R := Rect(0, 0, FMemoHelp.Width-10, 0); DrawText(bmp.Canvas.Handle, PChar(FMemoHelp.Text), Length(FMemoHelp.Text), R, DT_WORDBREAK or DT_CALCRECT); FreeAndNil(bmp); FMemoHelp.Height := R.Bottom + 8; FMemoHelp.Show; end; if FMemoHelp.GetTextLen = 0 then FMemoHelp.Hide; end; procedure TDataTypeEditorLink.DoTreeSelectPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); begin // Give datatype column specific color, as set in preferences case Sender.GetNodeLevel(Node) of 0: TargetCanvas.Font.Style := TargetCanvas.Font.Style + [fsBold]; 1: if not (vsSelected in Node.States) then TargetCanvas.Font.Color := DatatypeCategories[TDBDatatypeCategoryIndex(Node.Parent.Index)].Color; end; end; procedure TDataTypeEditorLink.DoTreeSelectFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex; var Allowed: Boolean); var JumpToNode: PVirtualNode; begin // Allow only 2nd level datatypes to be focused, not their category Allowed := Sender.GetNodeLevel(NewNode) = 1; if not Allowed then begin JumpToNode := nil; if FLastKeyDown = VK_UP then JumpToNode := Sender.GetPrevious(NewNode) else if FLastKeyDown = VK_DOWN then JumpToNode := Sender.GetNext(NewNode); if Assigned(JumpToNode) then Sender.FocusedNode := JumpToNode; end; end; procedure TDataTypeEditorLink.DoTreeSelectFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); begin FModified := True; end; end.