Sign in Join
Wiki copytemporary

KControls/KMemo notes: what changed

Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.

Title: KControls/KMemo notes

OldNewLine
1+# Introduction
2+
3+This page is about the KMemo Component, a part of [[KControls]]. KMemo provides a Cross Platform (Linux, Windows, macOS) memo capable of a range of text font styles, colours and similar. Its also capable of showing images and a range of other things but that may need to be dealt with at a later stage. And, importantly, it really does work pretty much the same way across Linux, Windows and Mac. Possibly other platforms as well but I cannot speak authoritatively there.
4+
5+The content presented here is in addition to the manual distributed with the KControls package. Its written by a KMemo user, not the author and could contain errors, omissions and possibly outright lies ! Someone else will undoubtedly use a different set of methods so that person is urged to document what they find too. And correct any errors they find here.
6+
7+# Underlying structure
8+
9+Its important, when using KMemo, to understand how the memo content and its meta data is stored. Everything is in a Block, and KMemo itself has a Blocks property. A Block has a Text property that contains the text being displayed, a Font property and a number of others controlling how the text is displayed. Every change to font style, colour, size and so on requires a new block. Block classes discussed here include TKMemoTextBlock, TKMemoParagraph, TKMemoHyperlink. A TKMemoParagraph appears between two paragraphs.
10+
11+This *'line of*text is an*' example.*
12+
13+It will contain 5 blocks.
14+
15+- Block 0 "This " - normal font.
16+- Block 1 "line of " - bold
17+- Block 2 "text is an " - bold and italic
18+- Block 3 "example." - italic
19+- Block 4 - a paragraph marker.
20+
21+The KMemo, Block and Blocks classes have a large number of other Properties and Methods, far too many to possibly document here. And there are also many associated classes. Lazarus will prompt you with them and the names are reasonably intuitive.
22+
23+# Inserting Text
24+
25+Easy. KMemo1.Blocks.AddTextBlock(AString). That will append the block at the end of any existing blocks. You can add a number after the string parameter and it will be inserted at that BlockIndex. Its a function, returning the TKMemoBlock so, once created you can alter how that text is presented.
26+
27+``` pascal
28+procedure Form1.AddText();
29+var
30+ TB: TKMemoTextBlock;
31+begin
32+ TB := KMemo1.Blocks.AddTextBlock(InStr);
33+ TB.TextStyle.Font.Size := 16;
34+ TB.TextStyle.Font.Style := TB.TextStyle.Font.Style + [fsBold];
35+end;
36+```
37+
38+Now, presumably, AddTextBlock() has allocated some memory for that TextBlock, we assume it will Free it when appropriate.
39+
40+If you wish to insert some text at a particular spot that will take on the font characteristics of the insertion point, its even easier. Several functions will return a SelIndex (its just a count of characters starting at zero) -
41+
42+``` pascal
43+KMemo1.Blocks.InsertPlainText(SelIndex, 'some bit of text to insert');
44+```
45+
46+# Selections
47+
48+... On our selection ... (sorry, Aussie joke)
49+
50+Kmemo offers a couple of functions, KMemo1.Blocks.RealSelStart, KMemo1.Blocks.RealSelEnd that return (read only) SelectionIndexes. ~End is always greater (or equal to) than ~Start unlike KMemo1.SelStart, Kmemo1.SelEnd which are sensitive to the direction the user wiped the mouse. KMemo1.Blocks.RealSelStart tracks the cursor position while a user is typing.
51+
52+The function KMemo1..Blocks.SelText returns the selected text (if any) irrespective of intervening block structure. Note that it might include newline characters.
53+
54+# Miscellaneous
55+
56+NOTE: By default upon creation of a table, a text block containing a new line is created within it. Usually it has to be removed by:
57+
58+``` pascal
59+procedure Form1.AddText();
60+ KMemo1.Blocks.Clear;end;
61+```
62+
63+**WARNING:** TKmemo does **not** support RightToLeft languages.
64+
65+# Playing With Blocks
66+
67+You will get used to working with the two indexes, one (utf8) character based and the other Block based. Both start at zero.
68+
69+The Blocks class has a property, Items that can let you address an individual block, so KMemo1.Blocks.Items\[BlockNo\] is a particular block. And it has a whole bunch of properties and methods.
70+
71+There are KMemo1.Blocks.Count blocks in a KMemo. And there is length(KMemo1.Blocks.Text) characters but only in a Unix based system such as Linux or macOS. All systems allow one character for a Paragraph marker in the KMemo itself but Windows, being Windows puts a CR/LF, (#13#10) at the end of each paragraph in KMemo1.Blocks.Text. So you need allow for that, for an example, see below about Searching.
72+
73+**How to convert between the two ? Like this**
74+
75+``` pascal
76+var
77+ BlockNo, CharNo, LocalIndex : longint;
78+begin
79+ CharNo := SomeValue;
80+ BlockNo := Kmemo1.Blocks.IndexToBlockIndex(CharNo, LocalIndex);
81+```
82+
83+BlockNo now has what block number CharNo points to and LocalIndex tells us how far in the block we are. Useful information, if LocalIndex is 0, we are at the start of a block, if its length(KMemo1.Blocks.Items\[BlockNo\].Text)-1 we must be at the end of a block. Dont forget that the Text component of a Block is, effectively, a Pascal string and it starts at 1, not zero. So, the first character on the first line of a KMemo is
84+
85+- KMemo.Blocks.Items\[0\].Text\[1\]
86+- TKMemoSelectionIndex = 0
87+- and Kmemo1.Blocks.IndexToBlockIndex(0, LocalIndex); // will return (block) 0 and set LocalIndex to 0
88+
89+**The reverse** -
90+
91+``` pascal
92+CharNo := KMemo1.Blocks.BlockToIndex(KMemo1.Blocks.Items[BlockNo]);
93+```
94+
95+will convert a blocknumber to SelectionIndex type of number. A SelectionIndex is a 0 based count of UTF8 characters, not bytes.
96+
97+When iterating over blocks, important to know what each block is -
98+
99+``` pascal
100+if KMemo1.Blocks.Items[BlockNo].ClassNameIs('TKMemoHyperlink') then
101+ URL := TKMemoHyperlink(KMemo1.Blocks.Items[BlockNo].URL);
102+```
103+
104+Here we have found out that the Block at BlockNo is a Hyperlink block, we can therefore cast it to TKMemoHyperlink and get the URL stored there previously.
105+
106+``` pascal
107+Kmemo1.Blocks.Delete(BlockNo);
108+```
109+
110+To delete a block. The source says parameter is an integer, not a TKMemoBlockIndex so you may miss it when looking for something to delete a block. But that appears to be the one to use !
111+
112+``` pascal
113+KMemo1.Blocks.DeleteChar(Index);
114+```
115+
116+Can also be a bit confusing. You would expect it to delete the character pointed to by Index, and yes, it might. But if there is another area of Text selected when you call it, instead, it will delete that other area of text. And that can be quite a surprise ! So, something like:
117+
118+``` pascal
119+KMemo1.Select(Index, 0);
120+while Cnt < Len do begin
121+ KMemo1.Blocks.DeleteChar(Index);
122+ inc(Cnt);
123+end;
124+```
125+
126+And, you may need to restore the selection points afterwards.
127+
128+As a paragraph may consist of many blocks and one para block, its useful to know where that para block is. KMemo1.NearestParagraphIndex returns the index of the FOLLOWING para block (obviously, not necessarily the 'nearest').
129+
130+# Hyperlinks
131+
132+TKMemo supports text-only hyperlinks. As of October 2017 Hyperlinks containing images are not supported.
133+
134+OK, lets suppose you have a KMemo full of text and you want to make a word of it a hyperlink. It will be blue, have an underline and when clicked, do something. The hyperlink will be one block (remember, everything is a block), so if the text we want to convert is already a discrete block, easy, delete it and then insert a new hyperlink block with the same text. However, its more likely we need to split a block. The process is to delete the characters that we want to become the link, split the block at that spot, create a TKMemoHyperlink block, configure it and insert it between the split blocks.
135+
136+This procedure will make a hyperlink at the character position, Index and it will be Len long. It exits doing nothing if that spot is already a hyperlink.
137+
138+``` pascal
139+procedure TForm1.MakeDemoLink(Index, Len : longint);
140+var
141+ Hyperlink: TKMemoHyperlink;
142+ BlockNo, Offset : longint;
143+ DontSplit : Boolean = false;
144+ Link : ANSIString;
145+begin
146+ // Is it already a Hyperlink ?
147+ BlockNo := KMemo1.Blocks.IndexToBlockIndex(Index, Offset);
148+ if KMemo1.Blocks.Items[BlockNo].ClassNameIs('TKHyperlink') then exit();
149+ Link := copy(KMemo1.Blocks.Items[BlockNo].Text, Offset+1, Len);
150+ if length(Kmemo1.Blocks.Items[BlockNo].Text) = Len then DontSplit := True;
151+ TheKMemo.SelStart := Index;
152+ TheKMemo.SelLength := Len;
153+ TheKMemo.ClearSelection();
154+ if not DontSplit then BlockNo := KMemo1.SplitAt(Index);
155+ Hyperlink := TKMemoHyperlink.Create;
156+ Hyperlink.Text := Link;
157+ Hyperlink.OnClick := @OnUserClickLink;
158+ KMemo1.Blocks.AddHyperlink(Hyperlink, BlockNo);
159+end;
160+```
161+
162+Now, danger Will Robertson ! This function will fail if you pass it parameters of text that are not within one block. But only because of the "Link := copy(..." line. Easy to fix with a few extra lines of code.
163+
164+And you may need to first record the existing selection and restore it afterwards too.
165+
166+Oh, and whats this "@OnUserClickLink" ? Its the address of a procedure "OnUserClickLink()" that might be defined like this -
167+
168+``` pascal
169+procedure TEditBoxForm.OnUserClickLink(sender : TObject);
170+begin
171+ showmessage('Wow, someone clicked ' + TKMemoHyperlink(Sender).Text);
172+end;
173+```
174+
175+# Search
176+
177+Despite all those methods, I could not find a Search function. But if your application is just text based (ie no images or nested containers) its not that difficult to search KMemo1.Blocks.Text, treat it as an ANSIString. On Unix like systems, the position in KMemo1.Blocks.Text can be used directly as a TKMemoSelectionIndex. Sadly, in Windows, KMemo1.Blocks.Text has Window's two character line endings, but in KMemo itself, only one char is reserved for line endings. So, count the \#13 characters from the start to the position of interest and subtract that from what becomes our TKMemoSelection index.
178+
179+This UTF8 compliant function will return 0 or the TKMemoSelectionIndex of the searched for term. It depends on the LazUTF8 unit so add it to your uses clause. It can be called repeatably, passing the previously found index plus 1 each time to find multiple incidences of a term. Set MoveCursor to True and it will show the user where the searched for term is. You will note most of the code is about fixing up the Windows problem.
180+
181+``` pascal
182+function TForm1.Search(Term : ANSIString; StartAt : longint = 1; MoveCursor : Boolean = False) : longint;
183+var
184+ Ptr, EndP : pchar;
185+ Offset : longint;
186+ NumbCR : longint;
187+begin
188+ Result := UTF8Pos(Term, KMemo1.Blocks.text, StartAt);
189+ {$IFDEF WINDOWS} // Sadley we need to subtract the extra CR windows adds to a newline
190+ if Result = 0 then exit();
191+ NumbCR := 0;
192+ Ptr := PChar(KMemo1.Blocks.text);
193+ EndP := Ptr + Result-1;
194+ while Ptr < EndP do begin
195+ if Ptr^ = #13 then inc(NumbCR);
196+ inc(Ptr);
197+ end;
198+ Result := Result - NumbCR;
199+ {$ENDIF} // does no harm in Unix but adds 66mS with my test note.
200+ if MoveCursor then begin
201+ KMemo1.SelStart := Result;
202+ KMemo1.SelEnd := Result;
203+ KMemo1.SetFocus;
204+ end;
205+end;
206+```
207+
208+# Bullets
209+
210+Bullets are a popular way to display a series of (maybe) short items. KMemo does have an "out of the box" bullet capability but not every part of it works exactly as people generally expect. The underlying issue is that KMemo records the "bulletness" of a bit of text in the paragraph marker at the end of the text. This results in some unexpected (to the end user) behaviour, particularly when using backspace key at beginning or end of a line of bullet text -
211+
212+- If the user backspaces from immediately after a bullet, perhaps hoping to delete the last character there, instead, they will delete the paragraph marker (not surprising) and remove the bullet attribute from that line of text (thats is surprising to end user).
213+- If the user places the cursor at start of a bullet text and presses backspace, expecting to cancel the "bulletness", instead, it acts as a backspace should and removes the leading paragraph marked and merges the line with the line above. And if the line above was not already a bullet, it now becomes one !
214+
215+There are a number of subcases of the above. Each has to be dealt with. The solution at the moment is to intercept the backspace key and, if the cursor is in one of the critical positions with respect to a bullet, prevent it being passed on and doing what is expected there and then. This works but but is complicated.
216+
217+Firstly, in KMemo1KeyDown() we look at every keystroke, we have to anyway, will need to intercept any one of the ctrl or similar key strokes for other reasons. But we are very interested in a Backspace -
218+
219+``` pascal
220+procedure TEditBoxForm.KMemo1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
221+begin
222+ // do all sorts of stuff and exit early if not a Backspace
223+ if BackSpaceBullet() then
224+ Key := 0; // eat that key because it was used in BackSpaceBullet()
225+ // if BackspaceBullet() ret false, let the Backspace go through to the KMemo
226+end;
227+```
228+
229+OK, now BackspaceBullet() will check to see if the cursor is in a position near a bullet that needs our attention.
230+
231+``` pascal
232+function TEditBoxForm.BackSpaceBullet() : boolean;
233+var
234+ BlockNo, PosInBlock : longint;
235+ ParaBlockNo : integer = -1;
236+ IsBulletPara : boolean = false; // The para the cursor is in.
237+begin
238+ Result := false;
239+ ParaBlockNo := Kmemo1.NearestParagraphIndex; // Thats para blockno. that controls where cursor is.
240+ if ParaBlockNo < 2 then exit; // Don't mess with title.
241+ BlockNo := kmemo1.Blocks.IndexToBlockIndex(KMemo1.RealSelStart, PosInBlock); // Block with cursor
242+ if PosInBlock > 0 then exit; // not the sort of BS we are looking for.
243+ // ======== BS when on first char of bullet line.========
244+ if TKMemoParagraph(KMemo1.Blocks[ParaBlockNo]).Numbering <> pnuNone then begin // ie, this is a bullet para.
245+ IsBulletPara := True;
246+ if kmemo1.Blocks.Items[BlockNo-1].ClassNameIs('TkMemoParagraph') then begin // Cursor is on first char of a bullet para.
247+ SetBullet(TKMemoParagraph(KMemo1.Blocks[ParaBlockNo]), False);
248+ exit(True); // My work here is done.
249+ end;
250+ end;
251+ // If previous para is already a bullet and current one is not, we set current one to same
252+ // bullet level as previous and allow BS to do the rest. BlockNo would have been set above
253+ // and we know we are on the first char of the paragraph.
254+ if (BlockNo > 0) and (not IsBulletPara) then begin // Cursor is on first char of a non-bullet para.
255+ if kmemo1.Blocks.Items[BlockNo-1].ClassNameIs('TkMemoParagraph') // previous block is a para ......
256+ and (TKMemoParagraph(KMemo1.Blocks[BlockNo-1]).Numbering <> pnuNone) then begin // and previous block is bullet !
257+ SetBullet(TKMemoParagraph(KMemo1.Blocks[ParaBlockNo]), True, TKMemoParagraph(KMemo1.Blocks[BlockNo-1]).Numbering);
258+ exit(False); // lets BS go through to KMemo.
259+ end;
260+ end;
261+end;
262+```
263+
264+If it decides it needs to act, it will what is necessary and return True, indicating to the calling process that it should not allow the Backspace keystroke to be passed on to the KMemo. If the cursor is not in a position that should affect existing bullets, it returns false, the calling procedure will allow the Backspace to go to the KMemo which will use it as expected. The SetBullet() method will increase of decrease the Bullet Level. Further details can be seen in <https://github.com/tomboy-notes/tomboy-ng/blob/master/source/editbox.pas>
265+
266+# Font names, styles, and sizes
267+
268+KMemo supports many of the same font style options as [TFont](https://support.smartbear.com/testcomplete/docs/reference/user-forms/helper-objects/tfont/style.html), its Delphi cousin. To set styles:
269+
270+``` pascal
271+Block.TextStyle.Font.Name := 'monospace'; // make font fixed width (untested on macOS and Windows)
272+Block.TextStyle.Font.Name := 'Arial'; // set font to Arial
273+Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsBold]; // embolden it!
274+Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsItalic]; // italicize
275+Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsStrikeout]; // strikeout
276+Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsUnderline]; // underline
277+```
278+
279+You can use similar syntax for font size, color, subscript, and superscript.
280+
281+``` pascal
282+Block.TextStyle.Font.Size := 16; // set font size to 16
283+Block.TextStyle.Font.Color := clRed // use the TColor value of any color
284+Block.TextStyle.ScriptPosition := tpoSubscript; // subscript
285+Block.TextStyle.ScriptPosition := tpoSuperscript; // superscript
286+```
287+
288+(The snippets above don't exhaustively cover all font style options provided with KMemo)
289+
290+# Copy on Selection and Middle Button Paste
291+
292+Unix (ie Linux but not Mac) users have long been used to an alternative copy and paste model. Any text selected is automatically made available to current and other applications with a press of the middle mouse button (or, typically, three finger tap on the touch pad). Most LCL components already do this on Linux but KMemo does not. Its easily implemented by using a capability built into the clipbrd unit.
293+
294+When an app detects you have selected some text, it registers the address of a function that knows how to copy that text to where ever it happens to be required. Unlike the 'other' clipboard, the text is not copied until needed. You will use the PrimarySelection TClipboard object already declared in clipbrd unit and a couple of small procedures -
295+
296+``` pascal
297+uses clipbrd;
298+....
299+
300+procedure TForm1.KMemo1MouseUp(Sender: TObject; Button: TMouseButton;
301+ Shift: TShiftState; X, Y: Integer);
302+var
303+ Point : TPoint;
304+ LinePos : TKmemoLinePosition;
305+begin
306+ if Button = mbMiddle then begin
307+ Point := TPoint.Create(X, Y);
308+ PrimaryPaste(KMemo1.PointToIndex(Point, true, true, LinePos));
309+ exit();
310+ end;
311+ if KMemo1.SelAvail and
312+ (Kmemo1.Blocks.SelLength <> 0) then
313+ SetPrimarySelection()
314+ else
315+ UnsetPrimarySelection();
316+end;
317+
318+procedure TForm1.SetPrimarySelection;
319+var
320+ FormatList: Array [0..1] of TClipboardFormat;
321+begin
322+ if (PrimarySelection.OnRequest=@PrimaryCopy) then exit;
323+ FormatList[0] := CF_TEXT;
324+ try
325+ PrimarySelection.SetSupportedFormats(1, @FormatList[0]);
326+ PrimarySelection.OnRequest:=@PrimaryCopy;
327+ except
328+ end;
329+end;
330+
331+procedure TForm1.UnsetPrimarySelection;
332+begin
333+ if PrimarySelection.OnRequest=@PrimaryCopy then
334+ PrimarySelection.OnRequest:=nil;
335+end;
336+
337+procedure TForm1.PrimaryCopy(
338+ const RequestedFormatID: TClipboardFormat; Data: TStream);
339+var
340+ s : string;
341+begin
342+ S := KMemo1.Blocks.SelText;
343+ if RequestedFormatID = CF_TEXT then
344+ if length(S) > 0 then
345+ Data.Write(s[1],length(s));
346+end;
347+
348+procedure TForm1.PrimaryPaste(SelIndex : integer);
349+var
350+ Buff : string;
351+begin
352+ if PrimarySelection.HasFormat(CF_TEXT) then begin // I don't know if this is useful at all ?
353+ Buff := PrimarySelection().AsText;
354+ if Buff <> '' then
355+ KMemo1.Blocks.InsertPlainText(SelIndex, Buff);
356+ end;
357+ // PrimarySelection is a function that returns a TClipboard object.
358+ // so, here, we call TClipboard.astext, but not the 'normal' clipboard.
359+end;
360+
361+procedure TForm1.FormDestroy(Sender: TObject);
362+begin
363+ UnsetPrimarySelection;
364+end;
365+```
366+
367+Note we copy the address of PrimaryCopy() into the PrimarySelection system. As other applications may well be calling this function, you must ensure the function parameter list remains as presented. Primary paste on the other hand is up to you. It needs to know how to paste an arbitrary bit of text into your application.
368+
369+The Lazarus IDE does more than just plain text, see synedit.pp and search for PrimarySelection - thanks to Martin_fr for the pointer.
370+
371+Further research is indicated -
372+
373+- Should we {\$ifdef LINUX} it so it does no harm in Mac and Windows. Lazarus allows windows users to do this within the app. Mac users have no concept of a middle mouse button so can never use it. Test.
374+- Need to do more through tests with UTF8.
375+- Is the CF_TEXT test in PrimaryPaste useful ? Is there a better way to test if content is available ?
376+
377+# Lines
378+
379+You can refer to individual lines of text within the KMemo using KMemo1.Blocks.Lines.Items\[I\] which returns a TKMemoLine. It has useful data in it about the line including start and end blocks and indexes. Note in this case, 'Line' refers to a line as currently displayed in KMemo. Just to be clear, that means, for example, altering the width of the KMemo moves the text wrapping point and alters the line numbering.
380+
381+You can get the (0 based) line number the cursor is currently on or the text that line contains -
382+
383+``` pascal
384+ LineNo := Kmemo1.Blocks.IndexToLineIndex(KMemo1.Blocks.RealSelStart);
385+ debugln('Current Line number ' + inttostr(LineNo));
386+ debugln('[' + Kmemo1.Blocks.LineText[LineNo] + ']');
387+```
388+
389+# Locking
390+
391+As your app grows, you may find that some processes start to take a significant amount of time. Here, Locking is not refering to where we lock a process to avoid interruptions, it actually speeds up the process, and quite substantially. If you have a series of (write) operations to perform on the contents of a KMemo, apply the lock before you start, release it when finished. This sort of thing -
392+
393+``` pascal
394+try
395+ KMemo1.Blocks.LockUpdate;
396+ while BlockNo < Kmemo1.Blocks.Count do begin
397+ SomeCrazyProcedure(KMemo.Blocks.Items[BlockNo]);
398+ inc(BlockNo);
399+ end
400+finally
401+ KMemo1.Blocks.UnlockUpdate;
402+end;
403+```
404+
405+Locking does not seem to make much difference where you are not writing to the contents nor for one-off procedures. It makes a big difference when you are doing a lot of stuff all at once.
406+
407+- WARNING: Do not use *KMemo1.LockUpdate* (without *.Blocks.*)!
408+- WARNING: If your application starts behaving oddly (selecting text happens slowly or does not happen at all) you have most likely missed to do *KMemo1.Blocks.UnlockUpdate;* Makes sense to use a try..finally to ensure unlock happens.
409+- WARNING: Do not attempt to move the cursor while locked. Bad things happen ....
410+
411+- **WARNING**: LockUpdate uses an internal counter (type int32), to prevent undesired unlocking by nested routines. So in order to unlock something you need to call .UnlockUpdate as many times as you have called .LockUpdate.
412+
413+Example:
414+
415+``` pascal
416+ KMemo1.Blocks.LockUpdate;
417+ KMemo1.Blocks.LockUpdate;
418+ KMemo1.Blocks.LockUpdate;
419+
420+ KMemo1.Blocks.UnLockUpdate; // Kmemo1 is still locked
421+ KMemo1.Blocks.UnlockUpdate; // Kmemo1 is still locked
422+ KMemo1.Blocks.UnLockUpdate; // Kmemo1 is unlocked
423+```
424+
425+In order to make sure that something is unlocked you could:
426+
427+``` pascal
428+ for i:=0 to 2147483647 do // or some counter with lower value to prevent running the loop forever
429+ if KMemo1.Blocks.UpdateUnlocked=False
430+ then KMemo1.Blocks.UnLockUpdate
431+ else break;
432+ if KMemo1.Blocks.UnLockUpdate=False then {error handling or whatever}
433+```
434+
435+# Bugs
436+
437+Yep, it has some bugs and some not yet implemented features. But not many ! Please report bugs at the link below.
438+
439+**Canceling Bullets**
440+
441+Late 2017 its been noted that there is a problem canceling Bullets on linux. Its fixed in the 2017-09-24 github version.
442+
443+**Direction of Selection**
444+
445+You select text (with a mouse) either from left to right or from right to left. You may notice that in KMemo, this direction is important if you use KMemo1.SelStart etc. This is **NOT** a bug, in fact, if it matters, you should be using KMemo1.RealSelStart (etc) instead.
446+
447+## Copy and Paste Bug
448+
449+Kmemo, as of April, 2018 and previous has a Unicode bug that affects Linux and Mac. It shows up if you copy unicode characters between KMemos but not, for example if you copy and paste the same text via an intermediate (non KMemo) app. The reason is that when copying directly between KMemos the text is copied in RTF format (to retain any formatting). A similar and directly related issue may occur when reading or writing RTF that contains UNicode text or Bullet Points, noting the Bullet Point is, itself, a Unicode character.
450+
451+In fact this bug actually consists of two unrelated bugs, one affecting the copying (or writeRTF) behaviour, the other affecting pasting (or loadRTF).
452+
453+## The Copy (or WriteRTF) Problem
454+
455+On windows, the call to StringToAnsiString(C, FCodePage); at kmemortf#4098 converts a single Unicode multibyte character (in C) to single byte (code page aware ?). For example the english pound becomes \#163. On linux, it is not changed, that is, it remains a two byte unicode character. That is because in LConvEncoding#7392, the function ConvertEncoding(const s, FromEncoding, ToEncoding: string ): string; has decided the string does not need to be converted. Effectivly, as GetDefautTextEncoding() returns 'utf8'as we are asking it to convert UTF8 to UTF8. So, quite resonably, It decides to pass the string back as it is, still in multibyte form.
456+
457+However in kmemortf#4067 there is a procedure TKMemoRTFWriter.WriteUnicodeString(const AText: TKString) that gets the string 'converted' above. At line#4099 there is -
458+
459+``` pascal
460+ if (Ansi <> '') and (Ansi <> #0) then
461+ begin
462+ S := AnsiString(Format('%s\''%.2x', [S, Ord(Ansi[1])]));
463+ WasAnsi := True;
464+ end;
465+```
466+
467+This code assumes all char in S are single byte but as discussed, on a Linux/Mac, being UTF8 ANSI codepage system, that is not the case.
468+
469+Interestingly, in the next few lines of WriteUnicodeString() is code obviously intended to deal with multibyte characters. But it does not get used because line#4099 has already stepped in.
470+
471+So, replace the test line \#4099, the block becomes -
472+
473+``` pascal
474+ if (length(Ansi) = 1) and (Ansi <> #0) then
475+ begin
476+ S := AnsiString(Format('%s\''%.2x', [S, Ord(Ansi[1])]));
477+ WasAnsi := True;
478+ end;
479+```
480+
481+This will ensure that a multibyte char is passed on to next part of the procedure as was probably intended.
482+
483+## Paste (or LoadRTF) Issue
484+
485+KMemortf ignores "listtext" as an RTF tag (kmemortf#2381). It sets FActiveState.Group to RGUNKNOWN so other methods know not to do anything with it. However, it is still processed to some extent. In particular, if the listtext params include a unicode character, ReadSpecialCharacter() will set FIgnoreChars to one so we don't use the "code page escape" character In AddText(). But we are not using any part of the listtext section so we don't call AddText() on the "code page escape" character and therefore we don't clear FIgnoreChars until we move on to the next RTF section.
486+
487+For example KMemo with a Bullet Point writes an RTF file that looks like this -
488+
489+`   {\listtext{\f0\fs20\cf1 \u8226\'3F\tab }}  `
490+
491+The "u8226" is a bullet point character (it is ignored by KMemo). The "'3F' is the "?" character, there in case the app cannot handle Unicode escapes.
492+
493+After that listtext block is processed FIgnore insists we still need to drop the first displayable character. So we loose first character of text following a bullet point.
494+
495+The Solution - Line 2712 at the end of procedure TKMemoRTFReader.ReadSpecialCharacter(...) -
496+
497+` if SetIgnoreChars  then `\
498+`   FIgnoreChars := FIgnoreCharsAfterUnicode; `
499+
500+becomes -
501+
502+` if SetIgnoreChars and (FActiveState.Group <> RGUNKNOWN) then  // DRB - April 2018`\
503+`   FIgnoreChars := FIgnoreCharsAfterUnicode;`
504+
505+# Installation
506+
507+KMemo must be installed as part of the KControls Package. As is usual, its easy. You should be using a recent version of FPC and Lazarus. Download the Github vesion of KControls, unzip and put it somewhere suitable. Fire up Lazarus, click Packages, click "Open Pack File (.lpk)" and find the kcontrolslaz.lpk file in the kit you downloaded.
508+
509+Click Compile.
510+
511+Lazarus will do its install thing, shortly will ask you about rebuilding, answer yes, it will then shutdown. On startup, you will find an extra row of components at the far right, KMemo amongst them.
512+
513+If you find this as valuable a component as I did, I suggest you hit the link below and thank TK, it is one hell of an effort !
514+
515+# See Also
516+
517+- [[KControls]] - the package KMemo is part of.
518+- <https://github.com/kryslt/KControls> - Github repository.
519+- [[RichMemo]] - an alternative package.
520+- [[UTF8 strings and characters]]
521+- How to hide / show the caret? - TBD
522+
523+---
524+*From the Free Pascal wiki, page "KControls/KMemo notes", as it stood on 2024-01-29 after a change by Dbannon, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
525+Category: Components

Back to history