Additional Widget Option '-steadymarks'

Command-Line Name: -steadymarks
Database Name: steadyMarks
Database Class: SteadyMarks

This option, if activated, will change the behavior of marks. Any mark now behaves simultaneously as an invisible character. One of the side effects is that the gravity will also apply to the insertion of marks. Another side effect is that any reference specified by a mark now will reference the pseudo-character position of that mark. (But it is important to mention that the real character position count will not be affected.)

The change of this behavior will apply to all peers of this widget, it's not possible to have a different behavior in different peers.

This option cannot be set if the widget is not overall clean (see command isclean -overall), an error will be thrown if this is not satisified. (It is not useful to switch this option, or setting this option, if the widget is not overall clean, this may cause errors in the order of the marks, therefore this is forbidden.)

This option is facilitating programmed editor control, because the shuffle effect is eliminated, any mark now has a defined order relative to other marks, this order is not changing as long as the mark will not be moved with mark set. Note that the handling of marks, especially the change of the gravity, will be performed more performant when this option is activated.

This option needs a motivation:

The text widget has an odd behavior, marks will be shuffled when re-setting the gravity, and also when deleting a range of characters.

At first a simple example to demonstrate the shuffle effect. Try this script:

This is the output:

m0 - m3 - m3
m0 - m2 - m1

I've tried to build a minimal example (derived from a practice case) to demonstrate, why this shuffle effect is causing headaches:

When executing this script the text widget will show the content "2". This result is not intuitive (in sense of programmed control), the content "12" is expected. Is something wrong with the widget? We will do a proof. The basis for the proof is the definition from the Tk Documentation:

Each mark also has a "gravity", which is either left or right. The gravity for a mark specifies what happens to the mark when text is inserted at the point of the mark. If a mark has left gravity, then the mark is treated as if it were attached to the character on its left, so the mark will remain to the left of any text inserted at the mark position. If the mark has right gravity, new text inserted at the mark position will appear to the left of the mark (so that the mark remains rightmost). The gravity for a mark defaults to right.

Now we will examine the algorithm step by step, applying the rules of the Tk documentation:

(Note that the insert cursor and the ending newline are not of interest here.)
(We append the gravity to each mark for a better understanding.)
("cur" is denoting the current mark.)

<Empty> {cur:R}
Insert m1 "2"
mark set m1 current {m1:R cur:R}
mark gravity m1 left {m1:L cur:R}
insert m1 2 {m1:L} "2" {cur:R}
Insert m2
mark set m2 current {m1:L} "2" {m2:R cur:R}
mark gravity m2 left {m1:L} "2" {m2:L cur:R}
insert m2 "" {m1:L} "2" {m2:L cur:R}
Insert m3 "1"
mark set m3 current {m1:L} "2" {m2:L m3:R cur:R}
mark gravity m3 left {m1:L} "2" {m2:L m3:L cur:R}
insert m3 "1" {m1:L} "2" {m2:L m3:L} "1" {cur:R}
Insert m4
mark set m4 current {m1:L} "2" {m2:L m3:L} "1" {m4:R cur:R}
mark gravity m4 left {m1:L} "2" {m2:L m3:L} "1" {m4:L cur:R}
insert m4 "" {m1:L} "2" {m2:L m3:L} "1" {m4:L cur:R}
Delete m1 m2
delete m1 m2 {m1:L m2:L m3:L} "1" {m4:L cur:R}
mark gravity m2 right {m1:L m3:L m2:R} "1" {m4:L cur:R}
mark set current m2 {m1:L m3:L cur:R m2:R} "1" {m4:L}
Insert m1 "1"
mark set m1 current {m1:L m3:L cur:R m2:R} "1" {m4:L}
mark gravity m1 left {m1:L m3:L cur:R m2:R} "1" {m4:L}
insert m1 "1" {m1:L m3:L} "1" {cur:R m2:R} "1" {m4:L}
Delete m3 m4
delete m3 m4 {m1:L m3:L m4:L cur:R m2:R}
mark gravity m4 right {m1:L m3:L m4:R cur:R m2:R}
mark set current m4 {m1:L m3:L m4:R cur:R m2:R}
Insert m3 "2"
mark set m3 current {m1:L m3:L m4:R cur:R m2:R}
mark gravity m3 left {m1:L m3:L m4:R cur:R m2:R}
insert m3 "2" {m1:L m3:L} "2" {m4:R cur:R m2:R}

In fact the content "2" is ok, everything is right. The rules of gravity are forcing a shuffle of the marks, they will in general not keep the defined order, this is causing the (bit) surprising result.

Now we will apply a trick, we insert an invisible character after each mark, in this way the shuffle effect will be prevented. Uncomment the line with the insertion of "\u200b", and look at the result, the content now is "12".

We will examine step by step what happens:

(We use "~" for the invisible char.)

<Empty> {cur:R}
Insert m1 "2"
insert current "~" "~" {cur:R}
mark set m1 current "~" {m1:R cur:R}
mark gravity m1 left "~" {m1:L cur:R}
insert m1 "2" "~" {m1:L} "2" {cur:R}
Insert m2
insert current "~" "~" {m1:L} "2~" {cur:R}
mark set m2 current "~" {m1:L} "2~" {m2:R cur:R}
mark gravity m2 left "~" {m1:L} "2~" {m2:L cur:R}
insert m2 "" "~" {m1:L} "2~" {m2:L cur:R}
Insert m3 "1"
insert current "~" "~" {m1:L} "2~" {m2:L} "~" {cur:R} "~"
mark set m3 current "~" {m1:L} "2~" {m2:L} "~" {m3:R cur:R} "~"
mark gravity m3 left "~" {m1:L} "2~" {m2:L} "~" {m3:L cur:R} "~"
insert m3 "1" "~" {m1:L} "2~" {m2:L} "~" {m3:L} "1" {cur:R}
Insert m4
insert current "~" "~" {m1:L} "2~" {m2:L} "~" {m3:L} "1~" {cur:R}
mark set m4 current "~" {m1:L} "2~" {m2:L} "~" {m3:L} "1~" {m4:R cur:R}
mark gravity m4 left "~" {m1:L} "2~" {m2:L} "~" {m3:L} "1~" {m4:L cur:R}
insert m4 "" "~" {m1:L} "2~" {m2:L} "~" {m3:L} "1~" {m4:L cur:R}
Delete m1 m2
delete m1 m2 "~" {m1:L m2:L} "~" {m3:L} "1~" {m4:L cur:R}
mark gravity m2 right "~" {m1:L m2:R} "~" {m3:L} "1~" {m4:L cur:R}
mark set current m2 "~" {m1:L cur:R m2:R} "~" {m3:L} "1~" {m4:L}
Insert m1 "1"
insert current "~" "~~" {m1:L cur:R m2:R} "~" {m3:L} "1~" {m4:L}
mark set m1 current "~~" {m1:L cur:R m2:R} "~" {m3:L} "1~" {m4:L}
mark gravity m1 left "~~" {m1:L cur:R m2:R} "~" {m3:L} "1~" {m4:L}
insert m1 "1" "~~" {m1:L} "1" {cur:R m2:R} "~" {m3:L} "1~" {m4:L}
Delete m3 m4
delete m3 m4 "~~" {m1:L} "1" {cur:R m2:R} "~" {m3:L m4:L}
mark gravity m4 right "~~" {m1:L} "1" {cur:R m2:R} "~" {m3:L m4:R}
mark set current m4 "~~" {m1:L} "1" {m2:R} "~" {m3:L cur:R m4:R}
Insert m3 "2"
insert current "~" "~~" {m1:L} "1" {m2:R} "~" {m3:L} "~" {cur:R m4:R}
mark set m3 current "~~" {m1:L} "1" {m2:R} "~~" {m3:R cur:R m4:R}
mark gravity m3 left "~~" {m1:L} "1" {m2:R} "~~" {m3:L cur:R m4:R}
insert m3 "2" "~~" {m1:L} "1" {m2:R} "~~" {m3:L} "2" {cur:R m4:R}

We can see that this trick helps, the shuffle effect does not occur. This has an significant advantage, we have an exact editor control, because marks now can be used like parentheses, and this is not possible without the new feature (or without this trick with the invisible char).

Now we examine the behavior of the new option, what happens if we consider marks simultaneously as invisible characters:

<Empty> {cur:R}
Insert m1 "2"
mark set m1 current m1:R cur:R
mark gravity m1 left m1:L cur:R
insert m1 "2" m1:L "2" cur:R
Insert m2
mark set m2 current m1:L "2" m2:R cur:R
mark gravity m2 left m1:L "2" m2:L cur:R
insert m2 "" m1:L "2" m2:L cur:R
Insert m3 "1"
mark set m3 current m1:L "2" m2:L m3:R cur:R
mark gravity m3 left m1:L "2" m2:L m3:L cur:R
insert m3 "1" m1:L "2" m2:L m3:L "1" cur:R
Insert m4
mark set m4 current m1:L "2" m2:L m3:L "1" m4:R cur:R
mark gravity m4 left m1:L "2" m2:L m3:L "1" m4:L cur:R
insert m4 "" m1:L "2" m2:L m3:L "1" m4:L cur:R
Delete m1 m2
delete m1 m2 m1:L m2:L m3:L "1" m4:L cur:R
mark gravity m2 right m1:L m2:R m3:L "1" m4:L cur:R
mark set current m2 m1:L cur:R m2:R m3:L "1" m4:L
Insert m1 "1"
mark set m1 current m1:L cur:R m2:R m3:L "1" m4:L
mark gravity m1 left m1:L cur:R m2:R m3:L "1" m4:L
insert m1 "1" m1:L "1" cur:R m2:R m3:L "1" m4:L
Delete m3 m4
delete m3 m4 m1:L "1" cur:R m2:R m3:L m4:L
mark gravity m4 right m1:L "1" cur:R m2:R m3:L m4:R
mark set current m4 m1:L "1" m2:R m3:L cur:R m4:R
Insert m3 "2"
mark set m3 current m1:L "1" m2:R m3:L cur:R m4:R
mark gravity m3 left m1:L "1" m2:R m3:L cur:R m4:R
insert m3 "2" m1:L "1" m2:R m3:L "2" cur:R m4:L

As we can see it works as expected, it works in the same way as with the invsible char trick (which prevents the shuffling).

The shuffle effect isn't noticeable with a normal editor flow, filling the content from left to right, line by line. But programmed editor control is a crux without the new feature. And the example with the invisible char trick demonstrates that the steadymarks feature is the natural way how the marks should behave.

This feature is used in chess application Scidb. This application has a sophisticated editor control for the display of chess games, with multi-level folding, and some more specials (the Tk widget is very powerful). It began with this invisible char trick.

Also the chess application Scid is using (nearly) all the features of the editor, but here the shuffle effect is breaking the layout (this occurs when appending moves, and then doing some operations like deletions, changing a variation to mainline, appending more moves, and so on).

The old behavior with marks is a bit archaic, so I would replace it with the new concept (as default), but it may happen - although this is in general not expected - that an existing application breaks, therefore I did not replace the old concept.

The effort for the implementation of this feature was minimal, but the benefit is huge.

With this new option we need an extension in description of mark set.

pathName mark set markName index ?direction?

Sets the mark named markName to a position just before the character at index. If markName already exists, it is moved from its old position; if it does not exist, a new mark is created. If the third parameter direction is specified – it must be left or right, – the gravity of markName is set to the given value (the gravity for a mark defaults to right). This command returns an empty string.

If option -steadymarks is enabled, and index is the name of an existing mark, then markName will be set left or right from index (but same numerical position), according to the gravity of index.

This command has also been extended with an optional third parameter, the direction of the gravity. It is a very common case that the gravity will be specified after setting the mark, now this can be done with one command (this also results in a slight performance improvement when option -steadymarks is not enabled).