A wysiwyg math editor (2)


to part (1)



Above formula (the Newton binomium) was typed in just a few seconds.
All text is editable, -undo- works.
Some more editing functions like cut and paste have to be implemented however.

My math editor rests on these pillars:



DavArrayButton
Own component with rows or columns of menu buttons.
Used for menu choices.
Description is found [ here ]

XBitmap
Own unit/Class.
Is an extension of TBitmap with clipping rectangle, improved floodfill,
improved stretchrect, dash-dot lines of multipixel penwidths, lines with arrow points.
Description is found [ here ]

Xfont
Own class with scalable fonts (3 types), build with ASCII and Greek characters,
plus some geometrical symbols.
Description is found [ here ]

Xtree
Own unit/Class with operations on tree graphs.
Also provides UNDO mechanism.
Description is found [ here ]

Data Flows




There are 3 XBitmaps each of 760 x 1080 pixels in 32bit color format.
    map1 - raster background, drawings
    map2 - map1 + text
    map3 - map2 + trial drawings during construction
Map1 contains all the drawings and is background of the text.
Text is erased by copying part of map1 to map2.
Text is added to map2.
Map3 holds drawings under construction.
Graphic elements (lines, circles, color fillings) are erased by copying part of map2 to map3.
The final drawing (after mouse-up) is then made in map1.
Results are visualized by copying map3 to a paintbox on the main form.
This paintbox has a lesser height that the bitmaps, a scrollbar selects a part of map3.

Associated with each map1,2,3 are a Trect rectangle with a mapfull flag of type boolean.
XFont and XBitmap classes have a property modrect.
This is the rectangular part of the bitmap that was modified during the last operation.
This rectangle is merged with one or more rect1,2,3 to enable a copy between the bitmaps.
Only modified parts of a bitmap are copied.

Operations on rectangles
Unirect(r1,r2) : rectangle r2 is combined with r1 so r1 surrounds r2.
Interrect(r1,r2) : r1 is limited to the part overlapping r2.
Packrect(x1,y1,x2,y2 : smallInt) : Trect : build Trect from x,y values after sorting.



mapfull flags are false if the rectangle holds no information. (no modified parts)
In this case a unirect(r1,r2) equals r1 := r2 after which the flag is set true.

All variables for coordinates are of type smallInt.
This allows for coordinates outside the maps.
The maximal number of pages is 30.
The y-coordinate of an element (divided by 1080) denotes the page number.

The menu system




Above pictured is the main menu.
An enumerated variabele (which simply is a name attached to a sequential number) holds the active button.
type TMainbutton   = (mbDoc,mbPage,mbEdit,mbFrame,mbText,mbGraph,mbSymbol,mbGeo,
                      mbInfo,mbhelp,mbOff);
....
var mainmenubutten : TMainbutton;					  
 
Activating a button opens submenu's with new choices and buttons for property selections.



Menu for common properties.
Pressing a property button opens dialog form for the choice of
colors, penwidths, dash-dot patterns, row- and column count for tables and matices.
Selected properties of text and graphic elements are stored in records such as:
type TFontProp = record
                  nr     : byte;   //font number 1..3
                  height : byte;
                  base   : byte;
                  style  : byte;
                  color1 : DWORD;  //foreground
                  color2 : DWORD;  //background
                 end;
......
var fontprops : TFontProp;
    fontDef  : array[1..maxFontDef] of TFontProp;	
    fontdefcount : byte;   //top of array				  
Property selection xxx is stored in a variable named xxxProp.
Text elements hold an index into a table which holds the font properties.
For fonts this is array fontDEF, in general: array xxxDEF[ ].
Text element[element nr].p1 is index into the font table.

There are many more property variables and xxxDEF tables.
These properties are once entered in the table and are never removed.
Adding new properties to the xxxDEF tables is done by procedures called registerXXX(....)

Control flow

Keyboard and mouse generate events.
Menuvariables controlling a case statement route these events to the proper procedures
to handle text editing or drawing of graphical symbols.
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
 case key of
  VK_Next     : begin
                 nextpage;
                 key := 0;
                end;
  VK_Prior    : begin
                 previouspage;
                 key := 0;
                end;
 end;
 if key <> 0 then
  begin
   keyEvent := kbDown;
   keyword := key;
   keyShift := shift;
   case mainmenubutton of
    mbText : textKeyEvent;//--> text control unit
    mbEdit : case editbutton of
              ebGraph : ; //--> graph edit unit
              ebText  : texteditKeyboardEvent; //--> textedit unit;
             end;
   end;//case
   key := 0;
  end;//if
end;
Code above shows that procedures for the editing of graphical symbols is under construction.
This is a very simple example: other similar procedures are of far greater length.

Handling text

All text elements (characters, macro's, lines) are of type:
type Telementtype   = (elFree,elblock,elLine,elChar,elMacro,elYTab,
                       elReserved);

           TElSet   = set of TElementType;
           TElement = record
                       elType : TelementType;
                       elCode : byte;
                       p1     : byte;
                       p2     : byte;
                       p3     : smallInt;
                       x      : smallInt;  //position relative to parent
                       y      : smallInt;  //..
                       width  : smallInt;
                       height : smallInt;
                      end;
.....
var element : array[1..maxelement] of TElement; //frame,line,macro,char					    
The relationship between these elements is recorded here:
type TLinks = record
               parent   : dword;
               child    : dword;
               next     : dword;
               previous : dword;
              end;
.....
var Links : array[1..maxelement] of TLinks;
The Element and Link arrays have a 1:1 relation.
The Link array cannot be read or written directly:
only procedures and functions may be called to perform tree operations.
The tree system also provides for UNDO.

Contrary to properties, elements may be removed from the element array to be reused.
This happens if an element was deleted long ago and the maximum depth of the UNDO stack was reached.
In this case the latest UNDO stack entry was purged, no UNDO is possible for this deleted element
and the Tree system notifies element destruction, so the element can be set free.
XTree cannot change the Element[ ] array because Xtree is not concerned about the type of element.
Deleting an element only separates the links from the tree.
UNDO reconnects the links.

Text control

Text control is handled by the textcontrol_unit.
The first concern here is cursor movement.

Cursor movement within lines:
This is the processing of left- right cursor key events.
Within a line, characters may vary in height, color and style.
The cursor (record) has the property posR:Boolean which indicates the place of the cursor:
left or right of the element.
The cursor adjusts it's size to the element of it's position.
If posR = true then
- a left key event will place the cursor left of the same element
- a right key event will place the cursor right of the next element
- at the last line element, the event is passed to the parent.

If posR = false then
- a left key event will place the cursor left of the previous element.
- een right event will place the cursor right of the same element.
- at the first element the event will be passed to the parent.

The parent of a character always is a line.
The parent of a line may be a macro or a block.
In the case of a macro cursor movement is handled by macro type specific procedures.
Macro's receiving cursor key events will redirect these events to one of their (line) children.

Also there exist cursor Up- and Down events.
These events are redirected to the parent because the cursor leaves the line or macro.

So, a lot of code is needed for cursor movement.
The cuNAV record holds control data for the cursor route in lines or macro's having 1, 2, 3 or more (line) children.
type TCursordirection = (cdLeft,cdRight,cdUp,cdDown);
     TCursorProc = (cpKB,cpM2P,cpM2C);           //message to Parent / Child
     TCursorNavigation = record
                          cdir   : TCursordirection;
                          cproc  : TCursorProc;
                          destEL : dword;        //destination element
                          srcEL  : dword;        //source element
                         end;
...
var cuNAV : Tcursornavigation;
Here a simple piece of code where cuNAV starts it's journey:
(keyEvent was stored before by the form1.Keydown event)
....
  case keyEvent of
   kbDown  : begin
              case keyword of
               VK_LEFT   : begin
                            cuNAV.cdir := cdLeft;
                            cuNavigate;
                           end;
               VK_RIGHT  : begin
                            cuNav.cdir := cdRight;
                            cuNavigate;
                           end;
               VK_UP     : begin
                            cuNav.cdir := cdUP;
                            cuNavigate;
                           end;
               VK_DOWN   : begin
                            cuNav.cdir := cdDown;
                            cuNavigate;
                           end;
....						   
steering the cursor further....
procedure CuNavigate;
//switch to element type
//use cuNAV
var mac : TTextbutton;
    framecode : TFrameButton;
    el : dword;
    m23 : boolean;//root,symbol types:>1 child
begin
 el := cuNAV.destEL;
 case element[el].elType of
  elChar : NavChar;
  elLine : Navline;
  elMacro : begin
             mac := TTextbutton(element[el].elCode);
             m23 := (textprops[mac].code and 1) = 1;
             case mac of
              txtPower,
              txtIndex,
              txtParenth    : NavMacro1;
              txtLimit,
              txtPowInd,
              txtFraction,
              txtOver       : NAVmacro2;
              txtRoot       : if m23 then NAVroot else NAVmacro1;
              txtPowerLine  : NAVpowerLine;
              txtIndexLine  : NAVIndexLine;
              txtPowIndLine : NAVpowIndLine;
              txtSymbol     : if m23 then NAVmacro3 else NAVmacro1;
              txtVector     : NAVmacroVector;
             end;//case
            end;
....			
For the case of a single line macro:
procedure NavMacro1;
//single line macros
begin
 case cuNAV.cproc of
  cpKB  : begin
           case cuNAV.cdir of
            cdLeft  : if textcursor.posR then gotoChild
                       else gotoParent;
            cdRight : if textcursor.posR = false then gotochild
                       else gotoParent;
            cdUp,
            cdDown  : gotoParent;
           end;
          end;
  cpM2P : begin
           case cuNAV.cdir of
            cdLeft  : begin
                       textcursor.posR := false;
                       setcursorOnElement(cuNAV.destEL);
                      end;
            cdRight : begin
                       textcursor.posR := true;
                       setcursorOnElement(cuNAV.destEL);
                      end;
            cdUp,
            cdDown  : gotoParent;
           end;
          end;
  cpM2C : begin
           case cuNAV.cdir of
            cdLeft  : begin
                       textcursor.posR := false;
                       gotochild;
                      end;
            cdRight : begin
                       textcursor.posR := false;
                       gotoChild;
                      end;
            cdUp,
            cdDown  : gotoXmatchChildN(1);
           end;
          end;
 end;
end;
Procedure gotoXmatchChild tries to place the cursor on a line element having the same X position,
so moves the cursor vertically up or down.
The GotoChild procedure:
procedure GotoChild;
begin
 with cuNAV do
  begin
   srcEL := destEL;//destiation becomes source
   cproc := cpM2C;//message to child
   getChild(destEL,srcEL);//destination is child
  end;
 cuNavigate;
end;
cpKB means the keyboard was the originator.
cpM2C means cursor was send to Child.
Also there is cpM2P, cursor was send to parent.
An element has to know who was calling, where the cursor comes from.

Text handling units

Three units take care of text procedures:
    - textcontrol : cursor movement
    - text paint : text and macro's painting
    - text calculation
Elements are rather abstract.
For each type (elBlock,elMacro,elLine,elChar) there are three specific procedures:
    create : reserve and define element, including children, using the selected properties.
    paint : painting of element.
    recalculate : calculate width, height and positions of the children
Note: an element never calculates it's own position.
This is the task of the parent.

The advantage of this method is that elements may be moved without caring about their contents.
The (x,y) position of the children is relative to the parent.
If the size of an element changes due to inserts or deletes, the parent is informed.
Then the parent recalculates it's width and height together with the position of the children.
In the case of changes the parent calls it's parent.. and so on.
Finally, the last recalculated element, together with it's children, are repainted.
Note: children are painted automatically.
So, the specific paint procedures per macro only take care of graphical symbols like
fraction lines, root or sigma symbols.
The macro paint procedures also calculate these graphic symbol dimensions.
The core is this small procedure processing element changes:
procedure processELchange(el : dword);
//element reports change to it's parent el
//el can be macro,line,column (not char or block)
//purpose is
//1. to call for recalculation of parents
//2. set area to be erased
//3. set element that needs repainting
var oldw,oldh : smallInt;
    Done : boolean;
    r1 : Trect;
begin
 eraseflag := false;
 spaceflag := false;
 repeat
  repaintELnr := el;
  oldw := element[el].width;
  oldh := element[el].height;
  r1 := getElementRect(pageNr,el);
  updateRect(eraseflag,eraseRect,r1);
  recalculateElement(el);
  if (oldw <> element[el].width) or (oldh <> element[el].height) then
   begin
    getParent(el,el);
    Done := element[el].elType = elBlock;
   end
    else Done := true;
 until Done;
end;
oldw : old width of element
oldh : old height
eraseRect : rectangle to be erased (old element size)
getElementRect : calculates rectangle of element relative to page
updateRect : merges rectangles
recalculateElement( ): contains big case statement to select the proper element procedure.

Element Insert or Delete procedures are element type independent.

Here I conclude this part 2 description of my math editor.
More work has to be done such as copy-paste, storing and opening data, graphic symbol editing
and solving many, many errors showing up on the way.
So far, my intention was to describe the general structure.

Finally I add a formula which was typed in a few seconds: the vector dot product