ViewVC Help
View File | Revision Log | Show Annotations | Download File | View Changeset | Root Listing
root/public/ibx/branches/journaling/ibcontrols/IBLookupComboEditBox.pas
Revision: 21
Committed: Thu Feb 26 10:33:34 2015 UTC (9 years, 9 months ago) by tony
Content type: text/x-pascal
Original Path: ibx/trunk/ibcontrols/IBLookupComboEditBox.pas
File size: 14748 byte(s)
Log Message:
Committing updates for Release R1-2-0

File Contents

# User Rev Content
1 tony 21 (*
2     * IBX For Lazarus (Firebird Express)
3     *
4     * The contents of this file are subject to the Initial Developer's
5     * Public License Version 1.0 (the "License"); you may not use this
6     * file except in compliance with the License. You may obtain a copy
7     * of the License here:
8     *
9     * http://www.firebirdsql.org/index.php?op=doc&id=idpl
10     *
11     * Software distributed under the License is distributed on an "AS
12     * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
13     * implied. See the License for the specific language governing rights
14     * and limitations under the License.
15     *
16     * The Initial Developer of the Original Code is Tony Whyman.
17     *
18     * The Original Code is (C) 2011 Tony Whyman, MWA Software
19     * (http://www.mwasoftware.co.uk).
20     *
21     * All Rights Reserved.
22     *
23     * Contributor(s): ______________________________________.
24     *
25     *)
26     unit IBLookupComboEditBox;
27    
28     {$mode objfpc}{$H+}
29    
30     interface
31    
32     uses
33     Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, DbCtrls,
34     ExtCtrls, IBSQLParser, DB, StdCtrls;
35    
36     type
37    
38     {TIBLookupComboEditBox is a TDBLookupComboBox descendent that implements "autocomplete"
39     of typed in text and "autoinsert" of new entries. Autocomplete uses SQL manipulation
40     to revise the available list and restrict it to items that are prefixed by the
41     typed text (either case sensitive or case insenstive). Autoinsert allows a
42     newly typed entry to be added to the list dataset and included in the available
43     list items. }
44    
45     TAutoInsert = procedure(Sender: TObject; aText: string; var NewKeyValue: variant) of object;
46     TCanAutoInsert = procedure (Sender: TObject; aText: string; var Accept: boolean) of object;
47    
48     TIBLookupComboEditBox = class;
49    
50     { TIBLookupComboDataLink }
51    
52     TIBLookupComboDataLink = class(TDataLink)
53     private
54     FOwner: TIBLookupComboEditBox;
55     protected
56     procedure ActiveChanged; override;
57     procedure DataEvent(Event: TDataEvent; Info: Ptrint); override;
58     procedure RecordChanged(Field: TField); override;
59     procedure UpdateData; override;
60     public
61     constructor Create(AOwner: TIBLookupComboEditBox);
62     end;
63    
64    
65     { TIBLookupComboEditBox }
66    
67     TIBLookupComboEditBox = class(TDBLookupComboBox)
68     private
69     FCanAutoInsert: TCanAutoInsert;
70     { Private declarations }
71     FDataLink: TIBLookupComboDataLink;
72     FAutoComplete: boolean;
73     FAutoInsert: boolean;
74     FKeyPressInterval: integer;
75     FOnCanAutoInsert: TCanAutoInsert;
76     FRelationName: string;
77     FTimer: TTimer;
78     FFiltered: boolean;
79     FOnAutoInsert: TAutoInsert;
80     FOriginalTextValue: string;
81     FUpdating: boolean;
82     FInserting: boolean;
83     FExiting: boolean;
84     FLastKeyValue: variant;
85     procedure DoActiveChanged(Data: PtrInt);
86     function GetAutoCompleteText: TComboBoxAutoCompleteText;
87     function GetListSource: TDataSource;
88     function GetRelationNameQualifier: string;
89     procedure HandleTimer(Sender: TObject);
90     procedure ResetParser;
91     procedure RecordChanged(Sender: TObject; aField: TField);
92     procedure SetAutoCompleteText(AValue: TComboBoxAutoCompleteText);
93     procedure SetListSource(AValue: TDataSource);
94     procedure UpdateList;
95     procedure UpdateSQL(Sender: TObject; Parser: TSelectSQLParser);
96     procedure HandleEnter(Data: PtrInt);
97     procedure UpdateLinkData(Sender: TObject);
98     protected
99     { Protected declarations }
100     procedure ActiveChanged(Sender: TObject);
101     procedure CheckAndInsert;
102     procedure DoEnter; override;
103     procedure DoExit; override;
104     procedure KeyUp(var Key: Word; Shift: TShiftState); override;
105     procedure SetItemIndex(const Val: integer); override;
106     procedure UpdateShowing; override;
107     public
108     { Public declarations }
109     constructor Create(TheComponent: TComponent); override;
110     destructor Destroy; override;
111     procedure EditingDone; override;
112     published
113     { Published declarations }
114     property AutoInsert: boolean read FAutoInsert write FAutoInsert;
115     property AutoComplete: boolean read FAutoComplete write FAutoComplete default true;
116     property AutoCompleteText: TComboBoxAutoCompleteText read GetAutoCompleteText
117     write SetAutoCompleteText;
118     property ItemHeight;
119     property ItemWidth;
120     property ListSource: TDataSource read GetListSource write SetListSource;
121     property KeyPressInterval: integer read FKeyPressInterval write FKeyPressInterval default 500;
122     property RelationName: string read FRelationName write FRelationName;
123     property OnAutoInsert: TAutoInsert read FOnAutoInsert write FOnAutoInsert;
124     property OnCanAutoInsert: TCanAutoInsert read FOnCanAutoInsert write FOnCanAutoInsert;
125     end;
126    
127    
128     implementation
129    
130     uses IBQuery, IBCustomDataSet, LCLType, Variants, LCLProc;
131    
132     { TIBLookupComboDataLink }
133    
134     procedure TIBLookupComboDataLink.ActiveChanged;
135     begin
136     FOwner.ActiveChanged(self)
137     end;
138    
139     procedure TIBLookupComboDataLink.DataEvent(Event: TDataEvent; Info: Ptrint);
140     begin
141     {If we are not visible then avoid unnecessary work}
142     if not FOwner.Showing then Exit;
143    
144     if (Event = deCheckBrowseMode) and (Info = 1) and not DataSet.Active then
145     begin
146     if (DataSet is TIBDataSet) then
147     FOwner.UpdateSQL(self,TIBDataSet(DataSet).Parser)
148     else
149     if (DataSet is TIBQuery) then
150     FOwner.UpdateSQL(self,TIBQuery(DataSet).Parser)
151     end
152     else
153     inherited DataEvent(Event, Info);
154     end;
155    
156     procedure TIBLookupComboDataLink.RecordChanged(Field: TField);
157     begin
158     FOwner.RecordChanged(self,Field);
159     end;
160    
161     procedure TIBLookupComboDataLink.UpdateData;
162     begin
163     FOwner.UpdateLinkData(self)
164     end;
165    
166     constructor TIBLookupComboDataLink.Create(AOwner: TIBLookupComboEditBox);
167     begin
168     inherited Create;
169     FOwner := AOwner
170     end;
171    
172     { TIBLookupComboEditBox }
173    
174     procedure TIBLookupComboEditBox.HandleTimer(Sender: TObject);
175     var ActiveState: boolean;
176     begin
177     FTimer.Interval := 0;
178     FFiltered := Text <> '';
179     UpdateList
180     end;
181    
182     function TIBLookupComboEditBox.GetListSource: TDataSource;
183     begin
184     Result := inherited ListSource;
185     end;
186    
187     function TIBLookupComboEditBox.GetRelationNameQualifier: string;
188     begin
189     if FRelationName <> '' then
190     Result := FRelationName + '.'
191     else
192     Result := ''
193     end;
194    
195     procedure TIBLookupComboEditBox.ActiveChanged(Sender: TObject);
196     begin
197     if not FInserting and not FUpdating then
198     Application.QueueAsyncCall(@DoActiveChanged,0);
199     end;
200    
201     procedure TIBLookupComboEditBox.DoActiveChanged(Data: PtrInt);
202     begin
203     if AppDestroying in Application.Flags then Exit;
204    
205     if (DataSource = nil) and assigned(ListSource) and assigned(ListSource.DataSet)
206     and ListSource.DataSet.Active then
207     begin
208     begin
209     if varIsNull(FLastKeyValue) and (ItemIndex = -1) then
210     KeyValue := ListSource.DataSet.FieldByName(KeyField).AsVariant
211     else
212     begin
213     KeyValue := FLastKeyValue;
214     UpdateData(self); {Force auto scroll}
215     if varIsNull(KeyValue) then {Value not present}
216     Text := ListSource.DataSet.FieldByName(ListField).AsString
217     end;
218     end;
219     end
220     else
221     if (DataSource <> nil) and assigned(DataSource.DataSet) and
222     (DataSource.DataSet.Active) and (DataField <> '') then
223     begin
224     ResetParser;
225     KeyValue := Field.AsVariant;
226     end
227     else
228     Text := '';
229     FOriginalTextValue := Text;
230     end;
231    
232     function TIBLookupComboEditBox.GetAutoCompleteText: TComboBoxAutoCompleteText;
233     begin
234     Result := inherited AutoCompleteText;
235     if AutoComplete then
236     Result := Result + [cbactEnabled]
237     end;
238    
239     procedure TIBLookupComboEditBox.ResetParser;
240     begin
241     if FFiltered then
242     begin
243     FFiltered := false;
244     UpdateList;
245     UpdateData(self); {Force Scroll}
246     end;
247     end;
248    
249     procedure TIBLookupComboEditBox.RecordChanged(Sender: TObject; aField: TField);
250     begin
251     {Make sure that we are in sync with other data controls}
252     if DataSource = nil then
253     begin
254     KeyValue := ListSource.DataSet.FieldByName(KeyField).AsVariant;
255     if VarIsNull(KeyValue) then {Probable deletion}
256     begin
257     UpdateList;
258     KeyValue := ListSource.DataSet.FieldByName(KeyField).AsVariant;
259     end;
260     end;
261     end;
262    
263     procedure TIBLookupComboEditBox.SetAutoCompleteText(
264     AValue: TComboBoxAutoCompleteText);
265     begin
266     if AValue <> AutoCompleteText then
267     begin
268     FAutoComplete := cbactEnabled in AValue;
269     inherited AutoCompleteText := AValue - [cbactEnabled]
270     end;
271     end;
272    
273     procedure TIBLookupComboEditBox.SetListSource(AValue: TDataSource);
274     begin
275     if AValue <> inherited ListSource then
276     begin
277     FDataLink.DataSource := AValue;
278     inherited ListSource := AValue;
279     end;
280     end;
281    
282     procedure TIBLookupComboEditBox.UpdateList;
283     { Note: Algorithm taken from TCustomComboBox.KeyUp but modified to use the
284     ListSource DataSet as the source for the autocomplete text. It also runs
285     after a delay rather than immediately on keyup
286     }
287     var
288     iSelStart: Integer; // char position
289     sCompleteText, sPrefixText, sResultText: string;
290     curText: string;
291     begin
292     if assigned(ListSource) and assigned(ListSource.DataSet) and (ListSource.DataSet is TIBCustomDataSet)
293     and ListSource.DataSet.Active then
294     begin
295     FUpdating := true;
296     try
297     iSelStart := SelStart;//Capture original cursor position
298     if ((iSelStart < UTF8Length(Text)) and
299     (cbactEndOfLineComplete in AutoCompleteText)) then
300     Exit;
301     curText := Text;
302     sPrefixText := UTF8Copy(Text, 1, iSelStart);
303     ListSource.DataSet.Active := false;
304     ListSource.DataSet.Active := true;
305     Text := curText;
306     if not FExiting and Focused and (Text <> '')then
307     begin
308     if ListSource.DataSet.Active and (ListSource.DataSet.RecordCount > 0) then
309     begin
310     sCompleteText := ListSource.DataSet.FieldByName(ListField).AsString;
311     if (sCompleteText <> Text) then
312     begin
313     sResultText := sCompleteText;
314     if ((cbactEndOfLineComplete in AutoCompleteText) and
315     (cbactRetainPrefixCase in AutoCompleteText)) then
316     begin//Retain Prefix Character cases
317     UTF8Delete(sResultText, 1, iSelStart);
318     UTF8Insert(sPrefixText, sResultText, 1);
319     end;
320     Text := sResultText;
321     SelStart := iSelStart;
322     SelLength := UTF8Length(Text);
323     end;
324     end;
325     end;
326     finally
327     FUpdating := false
328     end;
329     end;
330     end;
331    
332     procedure TIBLookupComboEditBox.UpdateSQL(Sender: TObject;
333     Parser: TSelectSQLParser);
334     var FieldPosition: integer;
335     begin
336     if FFiltered then
337     begin
338     if cbactSearchCaseSensitive in AutoCompleteText then
339     Parser.Add2WhereClause(GetRelationNameQualifier + '"' + ListField + '" Like ''' + Text + '%''')
340     else
341     Parser.Add2WhereClause(GetRelationNameQualifier + 'Upper("' + ListField + '") Like Upper(''' + Text + '%'')');
342    
343     end;
344     if cbactSearchAscending in AutoCompleteText then
345     begin
346     FieldPosition := Parser.GetFieldPosition(ListField);
347     if FieldPosition = 0 then Exit;
348    
349     Parser.OrderByClause := IntToStr(FieldPosition) + ' ascending';
350     end;
351     end;
352    
353     procedure TIBLookupComboEditBox.HandleEnter(Data: PtrInt);
354     begin
355     SelectAll
356     end;
357    
358     procedure TIBLookupComboEditBox.UpdateLinkData(Sender: TObject);
359     begin
360     if FInserting then
361     ListSource.DataSet.FieldByName(ListField).AsString := Text
362     end;
363    
364     procedure TIBLookupComboEditBox.CheckAndInsert;
365     var Accept: boolean;
366     NewKeyValue: variant;
367     begin
368     if AutoInsert and (Text <> '') and assigned(ListSource) and assigned(ListSource.DataSet)
369     and ListSource.DataSet.Active and (ListSource.DataSet.RecordCount = 0) then
370     try
371     {Is it OK to insert a new list member?}
372     Accept := true;
373     if assigned(FOnCanAutoInsert) then
374     OnCanAutoInsert(self,Text,Accept);
375     if not Accept then
376     begin
377     ResetParser;
378     Text := FOriginalTextValue;
379     SelectAll;
380     Exit;
381     end;
382    
383     FInserting := true;
384     try
385     {New Value}
386     FFiltered := false;
387     if assigned(FOnAutoInsert) then
388     begin
389     {In an OnAutoInsert handler, the client is expected to insert the new
390     row into the List DataSet and to set the KeyValue property to the
391     value of the primary key of the new row.}
392     OnAutoInsert(self,Text,NewKeyValue);
393     end
394     else
395     begin
396     ListSource.DataSet.Append;
397     {The new KeyValue should be determined by an external generator or
398     in the "OnInsert" handler. If it is the same as the ListField, then
399     it will be set from the UpdateLinkData method}
400     try
401     ListSource.DataSet.Post;
402     except
403     ListSource.DataSet.Cancel;
404     raise;
405     end;
406     NewKeyValue := ListSource.DataSet.FieldByName(KeyField).AsVariant;
407     end;
408     UpdateList;
409     KeyValue := NewKeyValue;
410     UpdateData(nil); {Force sync with DataField}
411     finally
412     FInserting := false
413     end;
414     except
415     Text := FOriginalTextValue;
416     ResetParser;
417     raise;
418     end;
419     end;
420    
421     procedure TIBLookupComboEditBox.DoEnter;
422     begin
423     inherited DoEnter;
424     FOriginalTextValue:= Text;
425     ResetParser;
426     Application.QueueAsyncCall(@HandleEnter,0);
427     end;
428    
429     procedure TIBLookupComboEditBox.DoExit;
430     begin
431     FExiting := true;
432     try
433     CheckAndInsert;
434     ResetParser;
435     FTimer.Interval := 0;
436     finally
437     FExiting := false;
438     end;
439     inherited DoExit;
440     end;
441    
442     procedure TIBLookupComboEditBox.KeyUp(var Key: Word; Shift: TShiftState);
443     begin
444     inherited KeyUp(Key, Shift);
445     if Key = VK_RETURN then
446     EditingDone
447     else
448     if Key = VK_ESCAPE then
449     begin
450     SelStart := UTF8Length(Text); {Ensure end of line selection}
451     ResetParser;
452     Text := FOriginalTextValue;
453     SelectAll;
454     end
455     else
456     if (IsEditableTextKey(Key) or (Key = VK_BACK))
457     and AutoComplete and (Style <> csDropDownList) and
458     (not (cbactEndOfLineComplete in AutoCompleteText) or (SelStart = UTF8Length(Text))) then
459     FTimer.Interval := FKeyPressInterval
460     else
461     FTimer.Interval := 0
462     end;
463    
464     procedure TIBLookupComboEditBox.SetItemIndex(const Val: integer);
465     begin
466     inherited SetItemIndex(Val);
467     FLastKeyValue := KeyValue;
468     end;
469    
470     procedure TIBLookupComboEditBox.UpdateShowing;
471     begin
472     inherited UpdateShowing;
473     if Showing then {Ensure up-to-date as we were ignoring any changes}
474     ActiveChanged(nil);
475     end;
476    
477     constructor TIBLookupComboEditBox.Create(TheComponent: TComponent);
478     begin
479     inherited Create(TheComponent);
480     FDataLink := TIBLookupComboDataLink.Create(self);
481     FKeyPressInterval := 500;
482     FAutoComplete := true;
483     FTimer := TTimer.Create(nil);
484     FTimer.Interval := 0;
485     FTimer.OnTimer := @HandleTimer;
486     FLastKeyValue := NULL;
487     end;
488    
489     destructor TIBLookupComboEditBox.Destroy;
490     begin
491     if assigned(FDataLink) then FDataLink.Free;
492     if assigned(FTimer) then FTimer.Free;
493     inherited Destroy;
494     end;
495    
496     procedure TIBLookupComboEditBox.EditingDone;
497     begin
498     CheckAndInsert;
499     inherited EditingDone;
500     end;
501    
502     end.