Sign in Join
Wiki copytemporary

KControls/KMemo notes: what changed

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

OldNewLine
11# Introduction
22
33This 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.
44
55The 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.
66
77# Underlying structure
88
99Its 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.
1010
1111This *'line of*text is an*' example.*
1212
1313It will contain 5 blocks.
1414
1515- Block 0 "This " - normal font.
1616- Block 1 "line of " - bold
1717- Block 2 "text is an " - bold and italic
1818- Block 3 "example." - italic
1919- Block 4 - a paragraph marker.
2020
2121The 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.
2222
2323# Inserting Text
2424
2525Easy. 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.
2626
2727``` pascal
2828procedure Form1.AddText();
2929var
3030 TB: TKMemoTextBlock;
3131begin
3232 TB := KMemo1.Blocks.AddTextBlock(InStr);
3333 TB.TextStyle.Font.Size := 16;
3434 TB.TextStyle.Font.Style := TB.TextStyle.Font.Style + [fsBold];
3535end;
3636```
3737
3838Now, presumably, AddTextBlock() has allocated some memory for that TextBlock, we assume it will Free it when appropriate.
3939
4040If 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) -
4141
4242``` pascal
4343KMemo1.Blocks.InsertPlainText(SelIndex, 'some bit of text to insert');
4444```
4545
4646# Selections
4747
4848... On our selection ... (sorry, Aussie joke)
4949
5050Kmemo 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.
5151
5252The function KMemo1..Blocks.SelText returns the selected text (if any) irrespective of intervening block structure. Note that it might include newline characters.
5353
5454# Miscellaneous
5555
5656NOTE: 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:
5757
5858``` pascal
5959procedure Form1.AddText();
6060 KMemo1.Blocks.Clear;end;
6161```
6262
6363**WARNING:** TKmemo does **not** support RightToLeft languages.
6464
6565# Playing With Blocks
6666
6767You will get used to working with the two indexes, one (utf8) character based and the other Block based. Both start at zero.
6868
6969The 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.
7070
7171There 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.
7272
7373**How to convert between the two ? Like this**
7474
7575``` pascal
7676var
7777 BlockNo, CharNo, LocalIndex : longint;
7878begin
7979 CharNo := SomeValue;
8080 BlockNo := Kmemo1.Blocks.IndexToBlockIndex(CharNo, LocalIndex);
8181```
8282
8383BlockNo 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
8484
8585- KMemo.Blocks.Items\[0\].Text\[1\]
8686- TKMemoSelectionIndex = 0
8787- and Kmemo1.Blocks.IndexToBlockIndex(0, LocalIndex); // will return (block) 0 and set LocalIndex to 0
8888
8989**The reverse** -
9090
9191``` pascal
9292CharNo := KMemo1.Blocks.BlockToIndex(KMemo1.Blocks.Items[BlockNo]);
9393```
9494
9595will convert a blocknumber to SelectionIndex type of number. A SelectionIndex is a 0 based count of UTF8 characters, not bytes.
9696
9797When iterating over blocks, important to know what each block is -
9898
9999``` pascal
100100if KMemo1.Blocks.Items[BlockNo].ClassNameIs('TKMemoHyperlink') then
101101 URL := TKMemoHyperlink(KMemo1.Blocks.Items[BlockNo].URL);
102102```
103103
104104Here 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.
105105
106106``` pascal
107107Kmemo1.Blocks.Delete(BlockNo);
108108```
109109
110110To 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 !
111111
112112``` pascal
113113KMemo1.Blocks.DeleteChar(Index);
114114```
115115
116116Can 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:
117117
118118``` pascal
119119KMemo1.Select(Index, 0);
120120while Cnt < Len do begin
121121 KMemo1.Blocks.DeleteChar(Index);
122122 inc(Cnt);
123123end;
124124```
125125
126126And, you may need to restore the selection points afterwards.
127127
128128As 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').
129129
130130# Hyperlinks
131131
132132TKMemo supports text-only hyperlinks. As of October 2017 Hyperlinks containing images are not supported.
133133
134134OK, 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.
135135
136136This 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.
137137
138138``` pascal
139139procedure TForm1.MakeDemoLink(Index, Len : longint);
140140var
141141 Hyperlink: TKMemoHyperlink;
142142 BlockNo, Offset : longint;
143143 DontSplit : Boolean = false;
144144 Link : ANSIString;
145145begin
146146 // Is it already a Hyperlink ?
147147 BlockNo := KMemo1.Blocks.IndexToBlockIndex(Index, Offset);
148148 if KMemo1.Blocks.Items[BlockNo].ClassNameIs('TKHyperlink') then exit();
149149 Link := copy(KMemo1.Blocks.Items[BlockNo].Text, Offset+1, Len);
150150 if length(Kmemo1.Blocks.Items[BlockNo].Text) = Len then DontSplit := True;
151151 TheKMemo.SelStart := Index;
152152 TheKMemo.SelLength := Len;
153153 TheKMemo.ClearSelection();
154154 if not DontSplit then BlockNo := KMemo1.SplitAt(Index);
155155 Hyperlink := TKMemoHyperlink.Create;
156156 Hyperlink.Text := Link;
157157 Hyperlink.OnClick := @OnUserClickLink;
158158 KMemo1.Blocks.AddHyperlink(Hyperlink, BlockNo);
159159end;
160160```
161161
162162Now, 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.
163163
164164And you may need to first record the existing selection and restore it afterwards too.
165165
166166Oh, and whats this "@OnUserClickLink" ? Its the address of a procedure "OnUserClickLink()" that might be defined like this -
167167
168168``` pascal
169169procedure TEditBoxForm.OnUserClickLink(sender : TObject);
170170begin
171171 showmessage('Wow, someone clicked ' + TKMemoHyperlink(Sender).Text);
172172end;
173173```
174174
175175# Search
176176
177177Despite 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.
178178
179179This 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.
180180
181181``` pascal
182182function TForm1.Search(Term : ANSIString; StartAt : longint = 1; MoveCursor : Boolean = False) : longint;
183183var
184184 Ptr, EndP : pchar;
185185 Offset : longint;
186186 NumbCR : longint;
187187begin
188188 Result := UTF8Pos(Term, KMemo1.Blocks.text, StartAt);
189189 {$IFDEF WINDOWS} // Sadley we need to subtract the extra CR windows adds to a newline
190190 if Result = 0 then exit();
191191 NumbCR := 0;
192192 Ptr := PChar(KMemo1.Blocks.text);
193193 EndP := Ptr + Result-1;
194194 while Ptr < EndP do begin
195195 if Ptr^ = #13 then inc(NumbCR);
196196 inc(Ptr);
197197 end;
198198 Result := Result - NumbCR;
199199 {$ENDIF} // does no harm in Unix but adds 66mS with my test note.
200200 if MoveCursor then begin
201201 KMemo1.SelStart := Result;
202202 KMemo1.SelEnd := Result;
203203 KMemo1.SetFocus;
204204 end;
205205end;
206206```
207207
208208# Bullets
209209
210210Bullets 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 -
211211
212212- 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).
213213- 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 !
214214
215215There 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.
216216
217217Firstly, 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 -
218218
219219``` pascal
220220procedure TEditBoxForm.KMemo1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
221221begin
222222 // do all sorts of stuff and exit early if not a Backspace
223223 if BackSpaceBullet() then
224224 Key := 0; // eat that key because it was used in BackSpaceBullet()
225225 // if BackspaceBullet() ret false, let the Backspace go through to the KMemo
226226end;
227227```
228228
229229OK, now BackspaceBullet() will check to see if the cursor is in a position near a bullet that needs our attention.
230230
231231``` pascal
232232function TEditBoxForm.BackSpaceBullet() : boolean;
233233var
234234 BlockNo, PosInBlock : longint;
235235 ParaBlockNo : integer = -1;
236236 IsBulletPara : boolean = false; // The para the cursor is in.
237237begin
238238 Result := false;
239239 ParaBlockNo := Kmemo1.NearestParagraphIndex; // Thats para blockno. that controls where cursor is.
240240 if ParaBlockNo < 2 then exit; // Don't mess with title.
241241 BlockNo := kmemo1.Blocks.IndexToBlockIndex(KMemo1.RealSelStart, PosInBlock); // Block with cursor
242242 if PosInBlock > 0 then exit; // not the sort of BS we are looking for.
243243 // ======== BS when on first char of bullet line.========
244244 if TKMemoParagraph(KMemo1.Blocks[ParaBlockNo]).Numbering <> pnuNone then begin // ie, this is a bullet para.
245245 IsBulletPara := True;
246246 if kmemo1.Blocks.Items[BlockNo-1].ClassNameIs('TkMemoParagraph') then begin // Cursor is on first char of a bullet para.
247247 SetBullet(TKMemoParagraph(KMemo1.Blocks[ParaBlockNo]), False);
248248 exit(True); // My work here is done.
249249 end;
250250 end;
251251 // If previous para is already a bullet and current one is not, we set current one to same
252252 // bullet level as previous and allow BS to do the rest. BlockNo would have been set above
253253 // and we know we are on the first char of the paragraph.
254254 if (BlockNo > 0) and (not IsBulletPara) then begin // Cursor is on first char of a non-bullet para.
255255 if kmemo1.Blocks.Items[BlockNo-1].ClassNameIs('TkMemoParagraph') // previous block is a para ......
256256 and (TKMemoParagraph(KMemo1.Blocks[BlockNo-1]).Numbering <> pnuNone) then begin // and previous block is bullet !
257257 SetBullet(TKMemoParagraph(KMemo1.Blocks[ParaBlockNo]), True, TKMemoParagraph(KMemo1.Blocks[BlockNo-1]).Numbering);
258258 exit(False); // lets BS go through to KMemo.
259259 end;
260260 end;
261261end;
262262```
263263
264264If 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>
265265
266266# Font names, styles, and sizes
267267
268268KMemo 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:
269269
270270``` pascal
271271Block.TextStyle.Font.Name := 'monospace'; // make font fixed width (untested on macOS and Windows)
272272Block.TextStyle.Font.Name := 'Arial'; // set font to Arial
273273Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsBold]; // embolden it!
274274Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsItalic]; // italicize
275275Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsStrikeout]; // strikeout
276276Block.TextStyle.Font.Style := Block.TextStyle.Font.Style + [fsUnderline]; // underline
277277```
278278
279279You can use similar syntax for font size, color, subscript, and superscript.
280280
281281``` pascal
282282Block.TextStyle.Font.Size := 16; // set font size to 16
283283Block.TextStyle.Font.Color := clRed // use the TColor value of any color
284284Block.TextStyle.ScriptPosition := tpoSubscript; // subscript
285285Block.TextStyle.ScriptPosition := tpoSuperscript; // superscript
286286```
287287
288288(The snippets above don't exhaustively cover all font style options provided with KMemo)
289289
290290# Copy on Selection and Middle Button Paste
291291
292292Unix (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.
293293
294294When 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 -
295295
296296``` pascal
297297uses clipbrd;
298298....
299299
300300procedure TForm1.KMemo1MouseUp(Sender: TObject; Button: TMouseButton;
301301 Shift: TShiftState; X, Y: Integer);
302302var
303303 Point : TPoint;
304304 LinePos : TKmemoLinePosition;
305305begin
306306 if Button = mbMiddle then begin
307307 Point := TPoint.Create(X, Y);
308308 PrimaryPaste(KMemo1.PointToIndex(Point, true, true, LinePos));
309309 exit();
310310 end;
311311 if KMemo1.SelAvail and
312312 (Kmemo1.Blocks.SelLength <> 0) then
313313 SetPrimarySelection()
314314 else
315315 UnsetPrimarySelection();
316316end;
317317
318318procedure TForm1.SetPrimarySelection;
319319var
320320 FormatList: Array [0..1] of TClipboardFormat;
321321begin
322322 if (PrimarySelection.OnRequest=@PrimaryCopy) then exit;
323323 FormatList[0] := CF_TEXT;
324324 try
325325 PrimarySelection.SetSupportedFormats(1, @FormatList[0]);
326326 PrimarySelection.OnRequest:=@PrimaryCopy;
327327 except
328328 end;
329329end;
330330
331331procedure TForm1.UnsetPrimarySelection;
332332begin
333333 if PrimarySelection.OnRequest=@PrimaryCopy then
334334 PrimarySelection.OnRequest:=nil;
335335end;
336336
337337procedure TForm1.PrimaryCopy(
338338 const RequestedFormatID: TClipboardFormat; Data: TStream);
339339var
340340 s : string;
341341begin
342342 S := KMemo1.Blocks.SelText;
343343 if RequestedFormatID = CF_TEXT then
344344 if length(S) > 0 then
345345 Data.Write(s[1],length(s));
346346end;
347347
348348procedure TForm1.PrimaryPaste(SelIndex : integer);
349349var
350350 Buff : string;
351351begin
352352 if PrimarySelection.HasFormat(CF_TEXT) then begin // I don't know if this is useful at all ?
353353 Buff := PrimarySelection().AsText;
354354 if Buff <> '' then
355355 KMemo1.Blocks.InsertPlainText(SelIndex, Buff);
356356 end;
357357 // PrimarySelection is a function that returns a TClipboard object.
358358 // so, here, we call TClipboard.astext, but not the 'normal' clipboard.
359359end;
360360
361361procedure TForm1.FormDestroy(Sender: TObject);
362362begin
363363 UnsetPrimarySelection;
364364end;
365365```
366366
367367Note 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.
368368
369369The Lazarus IDE does more than just plain text, see synedit.pp and search for PrimarySelection - thanks to Martin_fr for the pointer.
370370
371371Further research is indicated -
372372
373373- 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.
374374- Need to do more through tests with UTF8.
375375- Is the CF_TEXT test in PrimaryPaste useful ? Is there a better way to test if content is available ?
376376
377377# Lines
378378
379379You 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.
380380
381381You can get the (0 based) line number the cursor is currently on or the text that line contains -
382382
383383``` pascal
384384 LineNo := Kmemo1.Blocks.IndexToLineIndex(KMemo1.Blocks.RealSelStart);
385385 debugln('Current Line number ' + inttostr(LineNo));
386386 debugln('[' + Kmemo1.Blocks.LineText[LineNo] + ']');
387387```
388388
389389# Locking
390390
391391As 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 -
392392
393393``` pascal
394394try
395395 KMemo1.Blocks.LockUpdate;
396396 while BlockNo < Kmemo1.Blocks.Count do begin
397397 SomeCrazyProcedure(KMemo.Blocks.Items[BlockNo]);
398398 inc(BlockNo);
399399 end
400400finally
401401 KMemo1.Blocks.UnlockUpdate;
402402end;
403403```
404404
405405Locking 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.
406406
407407- WARNING: Do not use *KMemo1.LockUpdate* (without *.Blocks.*)!
408408- 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.
409409- WARNING: Do not attempt to move the cursor while locked. Bad things happen ....
410410
411411- **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.
412412
413413Example:
414414
415415``` pascal
416416 KMemo1.Blocks.LockUpdate;
417417 KMemo1.Blocks.LockUpdate;
418418 KMemo1.Blocks.LockUpdate;
419419
420420 KMemo1.Blocks.UnLockUpdate; // Kmemo1 is still locked
421421 KMemo1.Blocks.UnlockUpdate; // Kmemo1 is still locked
422422 KMemo1.Blocks.UnLockUpdate; // Kmemo1 is unlocked
423423```
424424
425425In order to make sure that something is unlocked you could:
426426
427427``` pascal
428428 for i:=0 to 2147483647 do // or some counter with lower value to prevent running the loop forever
429429 if KMemo1.Blocks.UpdateUnlocked=False
430430 then KMemo1.Blocks.UnLockUpdate
431431 else break;
432432 if KMemo1.Blocks.UnLockUpdate=False then {error handling or whatever}
433433```
434434
435435# Bugs
436436
437437Yep, it has some bugs and some not yet implemented features. But not many ! Please report bugs at the link below.
438438
439439**Canceling Bullets**
440440
441441Late 2017 its been noted that there is a problem canceling Bullets on linux. Its fixed in the 2017-09-24 github version.
442442
443443**Direction of Selection**
444444
445445You 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.
446446
447447## Copy and Paste Bug
448448
449449Kmemo, 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.
450450
451451In fact this bug actually consists of two unrelated bugs, one affecting the copying (or writeRTF) behaviour, the other affecting pasting (or loadRTF).
452452
453453## The Copy (or WriteRTF) Problem
454454
455455On 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.
456456
457457However in kmemortf#4067 there is a procedure TKMemoRTFWriter.WriteUnicodeString(const AText: TKString) that gets the string 'converted' above. At line#4099 there is -
458458
459459``` pascal
460460 if (Ansi <> '') and (Ansi <> #0) then
461461 begin
462462 S := AnsiString(Format('%s\''%.2x', [S, Ord(Ansi[1])]));
463463 WasAnsi := True;
464464 end;
465465```
466466
467467This 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.
468468
469469Interestingly, 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.
470470
471471So, replace the test line \#4099, the block becomes -
472472
473473``` pascal
474474 if (length(Ansi) = 1) and (Ansi <> #0) then
475475 begin
476476 S := AnsiString(Format('%s\''%.2x', [S, Ord(Ansi[1])]));
477477 WasAnsi := True;
478478 end;
479479```
480480
481481This will ensure that a multibyte char is passed on to next part of the procedure as was probably intended.
482482
483483## Paste (or LoadRTF) Issue
484484
485485KMemortf 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.
486486
487487For example KMemo with a Bullet Point writes an RTF file that looks like this -
488488
489-`   {\listtext{\f0\fs20\cf1 \u8226\'3F\tab }}  `
489+```text
490+ {\listtext{\f0\fs20\cf1 \u8226\'3F\tab }}
491+```
490492
491493The "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.
492494
493495After 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.
494496
495497The Solution - Line 2712 at the end of procedure TKMemoRTFReader.ReadSpecialCharacter(...) -
496498
497-` if SetIgnoreChars  then `\
498-`   FIgnoreChars := FIgnoreCharsAfterUnicode; `
499+``` pascal
500+ if SetIgnoreChars then
501+ FIgnoreChars := FIgnoreCharsAfterUnicode;
502+```
499503
500504becomes -
501505
502-` if SetIgnoreChars and (FActiveState.Group <> RGUNKNOWN) then  // DRB - April 2018`\
503-`   FIgnoreChars := FIgnoreCharsAfterUnicode;`
506+``` pascal
507+ if SetIgnoreChars and (FActiveState.Group <> RGUNKNOWN) then // DRB - April 2018
508+ FIgnoreChars := FIgnoreCharsAfterUnicode;
509+```
504510
505511# Installation
506512
507513KMemo 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.
508514
509515Click Compile.
510516
511517Lazarus 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.
512518
513519If 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 !
514520
515521# See Also
516522
517523- [[KControls]] - the package KMemo is part of.
518524- <https://github.com/kryslt/KControls> - Github repository.
519525- [[RichMemo]] - an alternative package.
520526- [[UTF8 strings and characters]]
521527- How to hide / show the caret? - TBD
522528
523529---
524530*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).*
525531Category: Components

Back to history