-
Notifications
You must be signed in to change notification settings - Fork 38
/
note_lister.pas
2371 lines (2120 loc) · 101 KB
/
note_lister.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
unit Note_Lister;
{$define TOMBOY_NG}
{ Copyright (C) 2017-2024 David Bannon
License:
This code is licensed under MIT License, see the file License.txt
or https://spdx.org/licenses/MIT.html SPDX short identifier: MIT
------------------
A class that knows how to read a directory full of notes. It keeps those list
internally, sorted by date. Note details (
Title, LastChange) can be updated (eg when a note is saved).
This unit is all about maintaining a list (FPList) of all the notes. We use a threaded class,
TIndexThread, to read files and initially populate the list. When saving, deleting or syncing
in a note, we update the list.
Templates are not added to the list.
-------- Multithreaded Indexing ----------
1. Only used for indexing all notes, when a single note is indexed, main thread.
2. The IndexNotes() method will call ThreadIndex.Start (and therfore TIndexThread.Execute)
four times, passing a set of the first chars [0..9, a..f, A..F] of file names to index.
TIndexThread.Execute will call GetNoteDetails for each note that Execute finds
GetNoteDetails reads note, builds a data structure and, subject to critical
section, adds it to the main data structure.
3. NoteBooks cleaned up etc in the IndexNotes method.
4. Four theads on a multicore cpu gives some 2 - 3 times spead up. Little slower on single core.
5. We use RTL CriticalSection code, LCL version is similar performance.
6. Using FindFirst/Next is substantually faster than FindAllFiles.
Search Modes
------------
We have two Search Modes, SWYT and PressEnter. Both expect ListView to be OwnerData mode.
SWYT - We have maintained at all time full content of all notes in the NoteLister unit. Two
indexes, TitleSearchIndex and DataSearchIndex, all they hold (in the pointer FPList provides)
is the index into NoteList. So we can sort them (and use them backwards) to get sorted
data. When searching, on the first scan, we build new, probably shorter indexes and then
subsquent scans remove invalid entries as user types a search term.
PressEnter - is kinder on memory and not as CPU demanding. It effectivly just uses the SWYT
first scan. But it has to add all note content to NoteList first and clear it when
user appears to be finished searching. Uses TGetContentThread class to search.
In addition, we maintain another index, DateAllIndex to be able to display notes in date
listed order. Its always as long as NoteList and, like the two above, must be maintained
when a note is saved, deleted, synced in etc.
History
2017/11/23 - added functions to save and retreive the Form when a note
is open. Also added a function to turn a fullfilename, a filename or an ID
into a filename of the form GID.note
2017/11/29 Added FileName to "Note has no Title" error message.
2017/11/29 check to see if NoteList is still valid before passing
on updates to a Note's status. If we are quiting, it may not be.
2017/11/29 Fixed a memory leak that occurred when Delete-ing a entry in the list
Turns out you must dispose() that allocation before calling Delete.
2017/12/28 Commented out unnecessary DebugLn
2017/12/29 Added a debug line to ThisNoteIsOpen() to try and see if there is
a problem there. Really don't think there is but ...
2018/01/25 Changes to support Notebooks
2018/02/14 Changed code that does Search All Notes stuff cos old code stopped on a tag
2018/02/15 Can now search case sensitive or not and any combination or exact match
2018/04/28 Set FixedRows to zero after Clean-ing strgrid, seems necessary in Trunk
2018/06/26 Used E.Message in exception generated by bad XML - dah ....
2018/07/04 Added a flag, XMLError, set if we found a note unable to index. Why ?
2018/08/24 Debugmode now set by calling process.
2018/11/04 Added support for updating NoteList after a sync.
2018/12/29 Small improvements in time to save a file.
2019/04/13 Tweaks to overload to read help nodes
2019/05/06 Support saving pos and open on startup in note.
2020/01/03 When searching without AnyCombo ticked, string can be sub-grouped by double inverted commas "
2020/01/29 Fix multiple notebook tags for same notebook in note file.
Sort main list, added functions to populate MMenu Recent list.
Tweek func that populates the main stringGrid avoiding initial sort
2020/01/31 LoadStringGrid*() now uses the Lazarus column mode.
2020/02/03 Make contents of strgrid look like it claims to be after new data
Removed LoadSearchGrid, no use LoadStGrid in both modes.
2020/02/19 XML Escape the notebook list sent back.
2020/03/27 Better reporting on short lastchangedate string. But need an autofix.
2020/04/01 Bug fix for code that auto fixes short last-change-date.
2020/04/19 Missing $H+ caused 255 char default string, messed with RewriteBadChangeDate()
2020/05/10 Multithreaded search
2020/05/25 Don't read sett.checkcasesensitive in thread.
2020/08/01 Disable code to rewrite short lcd.
2021/01/03 LoadListView now uses TB_datetime, more tolerant of differing DT formats.
2021/02/14 Notebook list now sorted, A->z
2021/07/05 Changed a lot of "for X to 0" to "for 0 downto X" so searches start at end of list where current data is
2021/08/30 Removed dependencies on Sett and SearchUnit. Added Dump methods.
Added function GetNotebooks(const ID: ANSIString): string; for GitHub
2021/08/31 Added TheNoteLister to hold a ref to the NoteLister for any unit that 'uses' this unit.
2021/09/06 GetNotebooks result now wrapped in square brackets, JSON style
2022/01/12 Trapped out some errors that occur if XML element (field) is present but blank
2022/04/14 GetNotebooks() now takes a StringArray instead of List
TNoteLister.Count() removed, use TNoteLister.GetNoteCount() instead
2022/09/05 ---- Substantial Changes ----
Got rid of local global vars.
Use either SWYT, Search While You Type, or PressEnter mode. All here in NoteLister
SearchUnit.ListView now in ownerdata mode, faster with lots of notes.
NoteList now a public var of NoteLister, other units can iterate over it.
2022/10/29 Use test, "if TheMainNoteLister = self" to ensure Index are only used by TheMainNoteLister in IndexNotes()
2022/11/09 Trap out a totally bad xml note, reasonably gracefully.
}
{$mode objfpc} {$H+}
INTERFACE
uses
Classes, SysUtils, Grids, ComCtrls, Forms, FileUtil;
type TLVSortMode = (smRecentUp, smRecentDown, smAATitleUp, smAATitleDown, smAllRecentUp);
type
PNotebook=^TNotebook;
TNotebook = record
Name : ANSIString; // Name of the notebook
Template : ANSIString; // The FName of the Template for this Notebook, inc .note
Notes : TStringList; // A list of the Fnames of notes that are members of this Notebook, inc .note.
end;
type
{ TNoteBookList }
TNoteBookList = class(TList)
private
function Get(Index : integer) : PNoteBook;
procedure RemoveNoteBook(const NBName: AnsiString);
public
destructor Destroy; Override;
{ ID of Note to be added; Name of NoteBook it should be added to. Notebook rec
is created if necessary. But if IsTemplate ID is ID of a newly created Template }
procedure Add(const ID, ANoteBook : ANSIString; IsTemplate : boolean);
{ Returns True if the passed note ID is in the passed Notebook }
function IDinNotebook(const ID, Notebook : ANSIstring) : boolean;
// Returns a PNoteBook that has a name matching passed NoteBook.
function FindNoteBook(const NoteBook : ANSIString) : PNoteBook;
{ Removes any list entries that do not have a Template }
procedure CleanList();
property Items[Index : integer] : PNoteBook read Get; default;
end;
type
PNote=^TNote;
TNote = record
{ will have 36 char GUI plus '.note' }
ID : ANSIString;
Title : ANSIString;
{ An all lower case version of Title for searching }
TitleLow : string;
{ a 33 char date time string }
CreateDate : ANSIString;
{ a 33 char date time string, updateable }
LastChange : ANSIString;
IsTemplate : boolean;
OpenOnStart : boolean;
OpenNote : TForm; // If note is open, its in this TForm.
Content : string; // May contain note content, '' else.
InSearch : boolean; // indicates note 'passed' last filter, use again
end;
type { ---------- TNoteList ---------}
//TNoteList = class(TList)
TNoteList = class(TFPList)
private
function Get(Index: integer): PNote;
public
destructor Destroy; override;
function Add(ANote : PNote) : integer;
function FindID(const ID: ANSIString): pNote;
function FindID(out Index:integer; const ID: ANSIString): boolean;
property Items[Index: integer]: PNote read Get; default;
end;
type
{ SortList - Type for DateSortList and TitleSortList, indexes into NoteList }
TSortList = class(TList) // Provides a "revised index" into NoteList, sorted on either date or title
private // ToDo : TFPList is supposed to be a bit faster.
function Get(Index: integer): integer;
public
destructor Destroy; override;
function Add(ANumber : integer) : integer;
//function FindName(const Name : ANSIString) : PNote;
property Items[Index: integer]: integer read Get; default;
end;
type
{ TNoteLister }
TNoteLister = class
private
//DebugMode : boolean;
CriticalSection: TRTLCriticalSection; // we use RTL CriticalSection code, the LCL version is about the same
TitleSearchIndex : TSortList; // A list of Indexes into NoteList, filtered by search, sorted by Title
DateSearchIndex : TSortList; // A list of Indexes into NoteList, filtered by search, sorted by Date
DateAllIndex : TSortList; // An sorted on date index of all notes in NoteList (except templates)
EnterDateSearchIndex : TSortList; // A list of Indexes into NoteList, filtered by Press Enter search, sorted by Date
EnterTitleSearchIndex : TSortList; // A list of Indexes into NoteList, filtered by Press Enter search, sorted by Title
// SearchCount : integer; // How many notes are active in search, or all notes if search not active
OpenNoteIndex : integer; // Used for Find*OpenNote(), points to last found one, -1 meaning none found
SearchNoteList : TNoteList;
{ NoteBookList is a list of pointers. Each one points to a record
containing Name, Template ID and a List (called Notes) of IDs of
notes that are members of this Notebook. }
NoteBookList : TNoteBookList;
// Passed a StringList containing 0 to n strings. Returns False if any (non empty)
// string is not present in NoteList[index]^.Content. True if all present or list empty.
function CheckSearchTerms(const STermList: TStringList; const Index: integer): boolean;
{ Returns a simple note file name, accepts simple filename or ID }
function CleanFileName(const FileOrID: AnsiString): ANSIString;
// Indexes one note. Always multithread mode but sometimes its only one thread.
// Does require CriticalSection to be setup before calling.
// If note turns out to be a template, don't add it to main note list
// but still call Notebook.add to ensure its mentioned in notebook list.
// We might store Content and it may. or may not be all lower case.
procedure GetNoteDetails(const Dir, FileName: ANSIString; DontTestName: boolean; TheLister : TNoteLister);
// Inserts a new item into the ViewList, always Title, DateSt, FileName
// function NewLVItem(const LView: TListView; const Title, DateSt, FileName: string): TListItem;
{ A Early ver of -ng wrote a bad date stamp, here we try to fix any we find. First
just try to add missing bits, if that does not work, we replace the LCD with
current, and known good date.}
procedure RewriteBadChangeDate(const Dir, FileName, LCD: ANSIString);
public
DebugMode : boolean;
ThreadLock : integer; // -1 if unlocked, has value of thread when locked
FinishedThreads : integer; // There are here to allow the search threads to find them.
{ NoteList is a list of pointers. Each one points to a record that contains data
about a particular note. Only Notebook info it has is whether or not its a
template. The ID is stored as a 36 char GUI plus '.note'. Dates must be 33 char. }
NoteList : TNoteList;
XMLError : Boolean; // Indicates a note was found with an XML (or other) error, checked by calling process.
ErrorNotes : TStringList;
{ The directory, with trailing seperator, that the notes are in }
WorkingDir : ANSIString;
SearchIndex : integer;
procedure DumpNoteNoteList(WhereFrom: string);
// Puts name of any note that contains (case insensitive) the passed string into
// the passed stringlist. Does nothing to do with sorting, order etc.
// Used to get backlinks.
// ToDo : At present, assumes all content loaded.
procedure SearchContent(const St: string; Stl: TstringList);
procedure DumpNoteBookList(WhereFrom: String);
{ Returns true if there is a notebook of the passed title }
function IsANotebookTitle(NBTitle : string) : boolean;
{ Returns the Notebook Name for a given filename or ID (of the template itself)}
function GetNotebookName(FileorID: AnsiString): string;
{ returns a indexed pointer to a Notebookrecord }
function GetNoteBook(Index: integer): PNoteBook;
{ returns the number items in the notebook list}
function NotebookCount(): integer;
{Returns the number of records in the Notelist, NOT the index lists. }
function GetNoteCount() : integer;
{ Returns a pointer to PNote record, zero based, non sorted index }
function GetNote(Index: integer): PNote;
{ Returns a pointer to PNote record, zero based index is adjusted for current search }
function GetNote(Index: integer; mode : TLVSortMode): PNote;
{ Loads a TListView with note title, LCD and ID}
//procedure LoadListView(const LView: TListView; const SearchMode: boolean);
// ----------------- N O T E B O O K M E T H O D S ---------------------
{ Changes the name associated with a Notebook in the internal data structure }
function AlterNoteBook(const OldName, NewName: string): boolean;
{ Returns a multiline string to use in writing a notes notebook membership,
knows how to do a template too. String has special XML chars 'escaped'.
Includes the <tags> tags but returns empty string if no NB membership.
This function expects to be passed an ID + '.note'. }
function NoteBookTags(const NoteID: string): ANSIString;
{ Returns true if it has returned with a pointer to a list with one or more Note Fnames
that are members of NBName, it returns a pointer to the internal StList, do not create
or free. FNames mean ID.note ! }
function GetNotesInNoteBook(out NBIDList: TStringList; const NBName: string ): boolean;
{ Retuns the title of note at (zero based) index. }
function GetTitle(Index: integer): string;
{ Returns the title for a given ID or Filename }
function GetTitle(const ID: String): string;
{ Removes the Notebook entry with ID=Template from Notebook datastructure }
procedure DeleteNoteBookwithID(FileorID : AnsiString);
{ Returns True if passed string is the ID or short Filename of a Template }
function IsATemplate(FileOrID : AnsiString) : boolean;
{ ID of Note to be added; Name of NoteBook it should be added to. Notebook rec
is created if necessary. But if IsTemplate ID is ID of a newly created Template }
procedure AddNoteBook(const ID, ANoteBook: ANSIString; IsTemplate: Boolean);
{ Sets the passed Notebooks as 'parents' of the passed note. Any pre existing membership
will be cancelled. The list can contain zero to many notebooks. }
procedure SetNotebookMembership(const ID: ansistring; const MemberList: TStringList);
procedure RemoveNoteFromNoteBooks(NoteID: string);
{ If ID is empty, always returns false, puts all Notebook names in NBArray. If ID is not
empty, list is filtered for only notebooks that have that ID and returns True iff the
passed ID is that of a Template. A Notebook Template will have only one Notebook name in
its Tags and that will be added to strlist. The StartHere template won't have a Notebook
Name and therefore wont get mixed up here ???? Expects ID.note }
function GetNotebooks(out NBArray: TStringArray; const ID: ANSIString): boolean;
{ Rets a (JSON array like, escaped) string of Notebook names that this note is a member of.
It returns an empty array if the note has no notebooks or cannot be found.
If ID is a template, will send a two element array ["template', "notebook-name"].
Expects an ID.note . Result is like this ["Notebook One", "Notebook2", "Notebook"] }
function NotebookJArray(const ID: ANSIString): string;
{ Loads the Notebook ListBox up with the Notebook names we know about. Add a bool to indicate
we should only show Notebooks that have one or more notes mentioned in SearchNoteList. Call after
GetNotes(Term) }
procedure LoadListNotebooks(const NotebookItems: TStrings; SearchListOnly: boolean);
// ---------------------------------
{ Returns the LastChangeDate string for ID in the Notes list, empty string
if not found (empty string is its a notebook) }
function GetLastChangeDate(const ID: String): string;
{ Adds details of odd note to NoteList, does NOT update Indexes. }
procedure IndexThisNote(const ID : String);
{ Returns T is ID in current list, takes 36 char GUID or simple file name }
function IsIDPresent(ID : string) : boolean;
{ Adds a note to main list, ie when user creates a new note }
procedure AddNote(const FileName, Title, LastChange : ANSIString);
{ Read the metadata from all the notes into internal data structure,
this is the main "go and do it" function. Note, it uses threads and FindFirst.
Does NOT generate the note Indexes because its not always needed.}
function IndexNotes(DontTestName: boolean=false): longint;
{ Copy the internal Note data to the passed TStringGrid, empting it first.
NoCols can be 2, 3 or 4 being Name, LastChange, CreateDate, ID.
Special case only main List SearchMode True will get from the search list.
Only used by Recover unit now. }
procedure LoadStGrid(const Grid: TStringGrid; NoCols: integer; SearchMode: boolean=false);
{ Copy the internal Note Data to passed TStrings }
procedure LoadStrings(const TheStrings : TStrings);
// Returns True if its updated the internal record as indicated,
// will accept either an ID or a filename. Do NOT pass a Notebook ID !}
function AlterNote(ID, Change : ANSIString; Title : ANSIString = '') : boolean;
{ True if the passed LOWERCASE string is a valid note Title }
function IsThisATitle(const Title : ANSIString) : boolean;
{ Returns the Form this note is open on, Nil if its not open. Take ID or FileName }
function IsThisNoteOpen(const ID : ANSIString; out TheForm : TForm) : boolean;
{ Tells the list that this note is open, pass NIL to indicate its now closed }
function ThisNoteIsOpen(const ID: ANSIString; const TheForm: TForm): boolean;
{ Returns true if it can find a FileName (ie ID.note) to Match this Title }
function FileNameForTitle(const Title: ANSIString; out FileName : ANSIstring): boolean;
procedure StartSearch();
function NextNoteTitle(out SearchTerm : ANSIString) : boolean;
{ removes note from int data, accepting either an ID or Filename. Because this
alters the NoteList indexes, generate new Indexes from SearcUnit, not this method.}
function DeleteNote(const ID : ANSIString) : boolean;
{ Copy the internal data about notes in passed Notebook to passed TListView
for display. So, shown would be all the notes in the nominated notebook.}
//procedure LoadNotebookViewList(const VL: TListView; const NotebookName: AnsiString);
{ Copy the internal data about notes in passed Notebook to passed TStringGrid
for display. So, shown would be all the notes in the nominated notebook.}
procedure LoadNotebookGrid(const Grid : TStringGrid; const NotebookName : AnsiString);
{ Returns the ID (inc .note) of the notebook Template, if an empty string we did
not find a) the Entry in NotebookList or b) the entry had a blank template. }
function NotebookTemplateID(const NotebookName : ANSIString) : AnsiString;
{ Returns the Form of first open note and sets internal pointer to it, Nil if none found }
function FindFirstOpenNote(): TForm;
{ Call after FindFirstOpenNote(), it will return the next one or Nil if no more found }
function FindNextOpenNote() : TForm;
{ Returns the ID of first note that should be opened on startup internal pointer
(which is same interger as FindFirstOpenNate) to it, '' if none found }
function FindFirstOOSNote(out NTitle, NID: ANSIstring): boolean;
{ Call after FindFirstOOSNote(), it will return the next one or '' if no more found }
function FindNextOOSNote(var NTitle, NID: ANSIstring): boolean;
{ Ret True if we need to either rerun search search or redisplay it.
Called, typically, when a note is saved. May be a new note or a note
that is being updated. Will always have a new LCD, might have a new Title.
The note may or may not be displayed in SearchUnit. Depending on all that,
we may update Indexes, return false if nothing needs to be done, if True
we will refresh displayed list or, if ReRunSearch is true, we'll re-run the
current search, thus updating Search Indexes. We always update DateAllIndex.}
function AlterOrAddNote(out ReRunSearch : boolean; const FFName, LCD,
Title : string) : boolean;
// New Search methods
// Continues a possible existing search with an extra char in STerm, rets number
// of found items. Rewrites note indexes with only reference to Notes that pass test.
// Does not do anything about Notebook, it may, or may not be already applied.
// Calling process should trigger a redraw of Display.
function RefineSearch(STermList: TstringList): integer;
// Clears any search, returns number of notes represented in list (not inc Templates)
// Rewrites NoteIndexes using all Notes
// Calling process should trigger a redraw of Display.
function ClearSearch() : integer;
// An overload, accepts a string rather than the StringList.
// Triggers a new search, may have STerm or Notebook or both, rets number of found items.
// Rebuilds and sorts DateSortIndex and TitleSortIndex.
// Calling process should trigger a redraw of Display.
function NewSearch(STerm: string; NoteBook: string): integer;
// Triggers a new search, may have STerm or Notebook or both, rets number of found items.
// Rebuilds and sorts DateSortIndex and TitleSortIndex.
// Calling process should trigger a redraw of Display.
function NewSearch(STermList: TstringList; NoteBook: string): integer;
// Returns the number of notes still active in SWYT, Search While You Type. Its
// the number in NoteList or less. 0 is possible.
function NoteIndexCount() : integer;
// Unloads the note content from NoteList, thus saving some memory. Only used in
// PressEnter search mode.
procedure UnLoadContent();
{ This is only called when using the "Press Enter to search" mode - triggers
threads who's Execute add all the Note's content to NoteList. }
function LoadContentForPressEnter(): longint;
{ Builds a new date sorted index refrencing all notes in NoteList for Menu builder }
function BuildDateAllIndex(): integer;
constructor Create;
destructor Destroy; override;
end;
Type { ======================= GET CONTENT THREAD ========================== }
TGetContentThread = class(TThread)
private
protected
procedure Execute; override;
public
CaseSensitive : boolean;
NoteLister : TNoteLister; // Thats the note lister that called us
TIndex : integer; // Zero based count of threads
ThreadBlockSize : integer; // how many files each thread processes
ResultsList1, ResultsList2 : TSortList; // List to contain details of what we found, 1=date, 2=title
WorkDir : String; // Dir where notes files are
Term_List : TStringList; // Incoming list of terms to search for
Constructor Create(CreateSuspended : boolean);
end;
{ ======================= INDEX THREAD ========================== }
type
CharSet = set of char;
type TGetNoteDetailsProc = procedure(const Dir, FileName: ANSIString; DontTestName: boolean; TheLister : TNoteLister) of Object;
Type
TIndexThread = class(TThread)
private
protected
procedure Execute; override;
public
GetNoteDetailsProc : TGetNoteDetailsProc;
TIndex : integer; // Zero based count of threads
StartsWith : CharSet;
WorkingDir : string;
OneThread : boolean; // indicates its not regular UUID based notes, do single thread index
TheLister : TNoteLister;
Constructor Create(CreateSuspended : boolean);
end;
// Not in Class so that Threads can find it.
function NoteContains(const TermList : TStringList; FullFileName: ANSIString; const CaseSensitive : boolean): boolean;
var
// This is a pointer to the MAIN notelister, its really, really global !
// Its set after lister is created in Search unit and must not be used by
// any of the other units thinking its the NoteListers made for their own use.
TheMainNoteLister : TNoteLister = nil;
{ ------------------------------------------------------------------- }
{ -------------------------- IMPLEMENTATION ------------------------- }
{ ------------------------------------------------------------------- }
implementation
uses laz2_DOM, laz2_XMLRead, LazFileUtils, LazUTF8, LazLogger, tb_utils, syncutils
{, SearchUnit} {$ifdef TOMBOY_NG}, settings {$endif}; // project options -> Custom Options
{ Laz* are LCL packages, Projectinspector, double click Required Packages and add LCL }
// var // Look Mum, no Globals !
// FinishedThreads : integer; // There are here to allow the search threads to find them.
// ThreadLock : integer; // -1 if unlocked, has value of thread when locked
// CriticalSection: TRTLCriticalSection; // we use RTL CriticalSection code, the LCL version is about the same
// NoteList : TNoteList; // NO, not global !
{ -------------------------------- SortList --------------------------------- }
{ Several TSortLists are created. They are based on FPList, the only data they
store is stored in the pointer itself, cast to an integer. }
function TSortList.Get(Index: integer): integer;
begin
{$push}
{$hints off}
result := PtrUInt(inherited get(Index));
{$pop}
end;
destructor TSortList.Destroy;
begin
// we have not allocated any memory for data, no need to dispose
inherited Destroy;
end;
function TSortList.Add(ANumber: integer): integer;
begin
// result := inherited Add(pointer(ANumber)); // warning
{$push}
{$hints off}
result := inherited Add(pointer(PtrUInt(ANumber))); // hint
{$pop}
end;
// A sort function for TitleSortList
function SortOnTitle(Item1: Pointer; Item2: Pointer):Integer; inline; // BE VERY CAREFULL, usable ONLY by main NoteLister
var
LItem1: SizeInt absolute Item1; // Superimpose an Int like thing over pointer to avoid warnings
LItem2: SizeInt absolute Item2;
begin
if TheMainNoteLister.NoteList[Litem1]^.TitleLow = TheMainNoteLister.NoteList[Litem2]^.TitleLow then
Result := 0
else if TheMainNoteLister.NoteList[LItem1]^.TitleLow > TheMainNoteLister.NoteList[LItem2]^.TitleLow then // This gives alphabetical, AA at the top
Result := 1
else Result := -1;
(* {$push}
{$hints off}
if TheMainNoteLister.NoteList[PtrUInt(item1)]^.TitleLow = TheMainNoteLister.NoteList[PtrUInt(item2)]^.TitleLow then
Result := 0
else if TheMainNoteLister.NoteList[PtrUInt(item1)]^.TitleLow > TheMainNoteLister.NoteList[PtrUInt(item2)]^.TitleLow then // This gives alphabetical, AA at the top
Result := 1
else Result := -1;
{$pop} *)
end;
function SortOnDate(Item1, Item2 : Pointer):Integer; inline; // BE VERY CAREFULL, usable ONLY by main NoteLister
var
LItem1: SizeInt absolute Item1;
LItem2: SizeInt absolute Item2;
begin
if TheMainNoteLister.NoteList[LItem1]^.LastChange
= TheMainNoteLister.NoteList[LItem2]^.LastChange then
Result := 0
else if TheMainNoteLister.NoteList[LItem1]^.LastChange
> TheMainNoteLister.NoteList[LItem2]^.LastChange then // ?? This gives most recent at the top
Result := 1
else Result := -1;
(* {$push}
{$hints off}
if TheMainNoteLister.NoteList[PtrUInt(item1)]^.LastChange
= TheMainNoteLister.NoteList[PtrUInt(item2)]^.LastChange then
Result := 0
else if TheMainNoteLister.NoteList[PtrUInt(item1)]^.LastChange
> TheMainNoteLister.NoteList[PtrUInt(item2)]^.LastChange then // ?? This gives most recent at the top
Result := 1
else Result := -1;
{$pop} *)
end;
{ ================ I N D E X T H R E A D ======================= }
// ToDo : much of the work here is done in GetNoteDetails, maybe it belongs in this Type ?
constructor TIndexThread.Create(CreateSuspended : boolean);
begin
inherited Create(CreateSuspended);
FreeOnTerminate := True;
end;
procedure TIndexThread.Execute; // Might be only thread running or one of four.
var // This tread will index all notes starting with Ch
Ch : char; // The thread will clean up itself when terminated.
procedure FindNoteFile(Mask : string);
var
Info : TSearchRec;
Cnt : integer = 0;
begin
if FindFirst(WorkingDir + Mask, faAnyFile, Info)=0 then
repeat
inc(cnt);
GetNoteDetailsProc(WorkingDir, Info.Name, OneThread, TheLister);
//SearchForm.NoteLister.GetNoteDetails(WorkingDir, Info.Name, OneThread, TheLister);
until FindNext(Info) <> 0;
FindClose(Info);
end;
begin
{$ifdef FORCE_SINGLE_INDEX_THREAD}
FindNoteFile('*.note');
{$else}
if OneThread then
FindNoteFile('*.note')
else
for ch in StartsWith do
FindNoteFile(Ch + '*.note');
{$endif}
InterLockedIncrement(TheLister.FinishedThreads);
end;
{ ========================== SEARCH THREAD =========================== }
constructor TGetContentThread.Create(CreateSuspended : boolean);
begin
inherited Create(CreateSuspended);
FreeOnTerminate := True;
end;
procedure TGetContentThread.Execute;
var
EndBlock, I : integer;
// NoteP : PNote;
Doc : TXMLDocument;
Node : TDOMNode;
begin
EndBlock := (TIndex+1)*ThreadBlockSize;
if EndBlock > NoteLister.NoteList.Count then
EndBlock := NoteLister.NoteList.Count;
if (NoteLister.NoteList.Count - EndBlock) < ThreadBlockSize then
EndBlock := NoteLister.NoteList.Count;
I := TIndex * ThreadBlockSize;
{if EndBlock := FileList.Count then
debugln('Last Thread Endblock=' + dbgs(EndBlock)); }
while (not Terminated) and (I < EndBlock) do begin
if not NoteLister.NoteList[i]^.IsTemplate then begin
if not FileExistsUTF8(WorkDir + NoteLister.NoteList[i]^.ID) then begin
debugln('TNoteLister.TSearchThread.Execute ======== ERROR cannot find ' + WorkDir + NoteLister.NoteList[i]^.ID);
exit;
end;
ReadXMLFile(Doc, WorkDir + NoteLister.NoteList[i]^.ID); // requires free
try
Node := Doc.DocumentElement.FindNode('text');
while InterlockedCompareExchange(NoteLister.ThreadLock, TIndex, -1) <> -1 do
if Terminated then break; // cycle until its our turn
if assigned(Node) then begin
{$ifdef TOMBOY_NG}
if Sett.SearchCaseSensitive then
NoteLister.NoteList[i]^.Content := Node.TextContent
else {$endif} NoteLister.NoteList[i]^.Content := lowercase(Node.TextContent);
end
else debugln('TNoteLister.TSearchThread.Execute ======== ERROR unable to find text in '
+ WorkDir + NoteLister.NoteList[i]^.ID);
finally
InterlockedExchange(NoteLister.ThreadLock, -1);
doc.free;
end;
end;
inc(I);
end;
InterLockedIncrement(NoteLister.FinishedThreads);
end;
{ ========================= N O T E B O O K L I S T ======================== }
function TNoteBookList.Get(Index: integer): PNoteBook;
begin
Result := PNoteBook(inherited get(Index));
end;
destructor TNoteBookList.Destroy;
var
I : Integer;
begin
for I := 0 to Count-1 do begin
Items[I]^.Notes.free;
dispose(Items[I]);
end;
inherited Destroy;
end;
procedure TNoteBookList.Add(const ID, ANoteBook: ANSIString; IsTemplate: boolean );
var
NB : PNoteBook;
NewRecord : boolean = False;
I : integer;
begin
NB := FindNoteBook(ANoteBook);
if NB = Nil then begin
NewRecord := True;
new(NB);
NB^.Name:= ANoteBook;
NB^.Template := '';
NB^.Notes := TStringList.Create;
end;
if IsTemplate then begin
NB^.Template:= ID // should only happen if its a new template.
end else begin
// Check its not there already ....
I := NB^.Notes.Count;
while I > 0 do begin
dec(I);
if ID = NB^.Notes[i]
then exit; // cannot be there if its a new entry so no leak here
end;
NB^.Notes.Add(ID);
end;
if NewRecord then inherited Add(NB);
end;
function TNoteBookList.IDinNotebook(const ID, Notebook: ANSIstring): boolean;
var
Index : longint;
TheNoteBook : PNoteBook;
begin
Result := False;
TheNoteBook := FindNoteBook(NoteBook);
if TheNoteBook = Nil then exit();
for Index := 0 to TheNoteBook^.Notes.Count-1 do
if ID = TheNoteBook^.Notes[Index] then begin
Result := True;
exit();
end;
end;
function TNoteBookList.FindNoteBook(const NoteBook: ANSIString): PNoteBook;
var
Index : longint;
begin
Result := Nil;
for Index := 0 to Count-1 do begin
if Items[Index]^.Name = NoteBook then begin
Result := Items[Index];
exit()
end;
end;
end;
function TNoteLister.IsANotebookTitle(NBTitle: string): boolean;
var
P : PNoteBook;
begin
P := NoteBookList.FindNoteBook(NBTitle);
result := P <> nil;
end;
procedure TNoteBookList.CleanList;
var
Index : integer = 0;
begin
while Index < Count do begin
if Items[Index]^.Template = '' then begin
Items[Index]^.Notes.free;
dispose(Items[Index]);
Delete(Index);
end else
inc(Index);
end;
end;
// Don't think we use this method ?
procedure TNoteBookList.RemoveNoteBook(const NBName: AnsiString);
var
Index : integer;
begin
for Index := 0 to Count-1 do
if Items[Index]^.Name = NBName then begin
Items[Index]^.Notes.free;
dispose(Items[Index]);
Delete(Index);
break;
end;
debugln('ERROR, asked to remove a note book that I cannot find.');
end;
// =================== DEBUG PROC ======================================
procedure TNoteLister.DumpNoteBookList(WhereFrom : String);
var
P : PNotebook;
I : integer;
begin
debugln('------------ ' + WhereFrom + ' -----------');
for P in NoteBookList do begin
debugln('Name=' + P^.Name);
for I := 0 to P^.Notes.Count -1 do
debugln(' ' + P^.Notes[I]);
end;
debugln('-----------------------');
end;
procedure TNoteLister.DumpNoteNoteList(WhereFrom : string);
var
P : PNote;
Pnb : PNotebook
// I : integer;
;begin
debugln('-----------' + WhereFrom + '------------');
for P in NoteList do begin
debugln('ID=' + P^.ID + ' ' + P^.Title);
debugln('CDate=' + P^.CreateDate + ' template=' + booltostr(P^.IsTemplate, true));
end;
debugln('-----------------------------------------------');
for Pnb in NoteBookList do
debugln('Template ID=' + Pnb^.Template + ' NB Name='+Pnb^.Name + ' and Notes are ' + Pnb^.Notes.Text);
debugln('-----------------------------------------------');
end;
function TNoteLister.GetNoteCount(): integer;
begin
result := NoteList.Count;
end;
{ ============================== NoteLister ================================ }
{ ------------- Things relating to NoteBooks ------------------ }
function TNoteLister.NotebookCount(): integer;
begin
Result := NoteBookList.Count;
end;
function TNoteLister.GetNoteBook(Index : integer) : PNoteBook;
begin
Result := NoteBookList[Index];
end;
function TNoteLister.NoteBookTags(const NoteID : string): ANSIString;
var
NBArray : TStringArray;
Index : Integer;
begin
Result := '';
if GetNotebooks(NBArray, NoteID) then begin // its a template
Result := ' <tags>'#10' <tag>system:template</tag>'#10;
if length(NBArray) > 0 then
Result := Result + ' <tag>system:notebook:' + RemoveBadXMLCharacters(NBArray[0], True) + '</tag>'#10' </tags>'#10;
end else
if length(NBArray) > 0 then begin // its a Notebook Member
Result := ' <tags>'#10;
for Index := 0 to High(NBArray) do // here, we auto support multiple notebooks.
Result := Result + ' <tag>system:notebook:' + RemoveBadXMLCharacters(NBArray[Index], True) + '</tag>'#10;
Result := Result + ' </tags>'#10;
end;
end;
function TNoteLister.NotebookJArray(const ID: ANSIString): string;
var
NBArray : TStringArray;
Index : Integer;
begin
Result := '';
if GetNotebooks(NBArray, ID) then // its a template
Result := '"template", "' + EscapeJSON(NBArray[0]) + '"'
else begin // maybe its a Notebook Member
for Index := 0 to high(NBArray) do // here, we auto support multiple notebooks.
Result := Result + '"' + EscapeJSON(NBArray[Index]) + '", ';
if Result <> '' then // will be empty if note is not member of a notebook
delete(Result, length(Result)-1, 2); // remove trailing comma and space
end;
Result := '[' + Result + ']'; // Always return the brackets, even if empty
//debugln('TNoteLister.NotebookJArray returning Notebooks jArray = ' + Result);
end;
function TNoteLister.GetNotesInNoteBook(out NBIDList : TStringList; const NBName : string) : boolean;
var
NB : PNoteBook;
begin
{ It appears this returns the ID of a note that was added to this notebook
but, maybe, not saved, not added to main TnoteLister yet ? #312
}
// debugln('TNoteLister.GetNotesInNoteBook - 1 - NBName=[' + NBName + '] and NBIDList nil=' + booltostr(NBIDList=nil, true));
Result := True;
NB := NoteBookList.FindNoteBook(NBName);
if NB <> Nil then
NBIDList := NB^.Notes
else begin
Result := False;
NBIDList := nil;
end;
// debugln('TNoteLister.GetNotesInNoteBook - 2 - NBName=[' + NBName + '] and NBIDList nil=' + booltostr(NBIDList=nil, true));
end;
function TNoteLister.AlterNoteBook(const OldName, NewName : string) : boolean;
var
NB : PNoteBook;
begin
Result := True;
NB := NoteBookList.FindNoteBook(OldName);
if NB <> nil then
NB^.Name:= NewName
else Result := False;
end;
procedure TNoteLister.AddNoteBook(const ID, ANoteBook: ANSIString; IsTemplate : Boolean);
begin
NoteBookList.Add(ID, ANoteBook, IsTemplate);
//DumpNoteBookList('After TNoteLister.AddNoteBook');
end;
(*
procedure TNoteLister.LoadNotebookViewList(const VL : TListView; const NotebookName: AnsiString);
var
Index : integer;
LCDst : string;
begin
VL.Clear;
Index := NoteList.Count;
while Index > 0 do begin
dec(Index);
if NotebookList.IDinNotebook(NoteList.Items[Index]^.ID, NoteBookName) then begin
LCDst := NoteList.Items[Index]^.LastChange;
if length(LCDst) > 11 then // looks prettier, dates are stored in ISO std
LCDst[11] := ' '; // with a 'T' between date and time
NewLVItem(VL, NoteList.Items[Index]^.Title, LCDst, NoteList.Items[Index]^.ID);
end;
end;
end; *)
procedure TNoteLister.LoadNotebookGrid(const Grid: TStringGrid; const NotebookName: AnsiString);
var
Index : integer;
begin
while Grid.RowCount > 1 do Grid.DeleteRow(Grid.RowCount-1);
Index := NoteList.Count;
while Index > 0 do begin
dec(Index);
if NotebookList.IDinNotebook(NoteList.Items[Index]^.ID, NoteBookName) then begin
Grid.InsertRowWithValues(Grid.RowCount, [NoteList.Items[Index]^.Title,
NoteList.Items[Index]^.LastChange]);
end;
end;
end;
function TNoteLister.NotebookTemplateID(const NotebookName: ANSIString): AnsiString;
var
Index : integer;
//St : string;
begin
for Index := 0 to NotebookList.Count - 1 do begin
//St := NotebookList.Items[Index]^.Name;
if NotebookName = NotebookList.Items[Index]^.Name then begin
Result := NotebookList.Items[Index]^.Template;
exit();
end;
end;
debugln('ERROR - asked for the template for a non existing Notebook');
debugln('NotebookName = ' + Notebookname);
for Index := 0 to NotebookList.Count - 1 do begin
if NotebookName = NotebookList.Items[Index]^.Name then
debugln('Match [' + NotebookList.Items[Index]^.Name + ']')
else debugln('NO - Match [' + NotebookList.Items[Index]^.Name + ']')
end;
Result := '';
end;
function TNoteLister.GetNotebookName(FileorID: AnsiString) : string;
var
Index : integer;
begin
for Index := 0 to NotebookList.Count - 1 do
if CleanFileName(FileorID) = NotebookList.Items[Index]^.Template then
exit(NotebookList.Items[Index]^.Name);
//debugln('TNoteLister.GetNotebookName ALERT - asked to find a notebook name but cannot find it : ' + FileorID);
// thats not an error, sometimes sync systems asks, just in case .....
result := '';
end;