Een wysiwyg wiskunde editor(2)


naar deel (1)



De formule hierboven (Newton binomium) werd ingetikt in enkele seconden.
Alle tekst is te editen, ook -undo- werkt.

Deze wiskunde editor staat op de volgende pijlers:



DavArrayButton
Eigen component met menuknoppen in rijen en kolommen.
Gebruikt voor menu keuzes.
Beschrijving [ hier ]

XBitmap
Eigen unit/Class.
Uitbreiding van TBitmap met clipping rectangle, verbeterde floodfill,
verbeterde stretchrect, stippellijnen met grotere pendikten, lijnen met pijlpunten.
Beschrijving [ hier ]

Xfont
Eigen Class met schaalbare fonts (3 stuks), opgebouwd uit ASCII en Griekse letters,
aangevuld met meetkundige tekens.
Beschrijving [ hier ]

Xtree
Eigen unit/Class met boomstructuur voor text editing.
Voorziet ook in het UNDO mechanisme.
Beschrijving [ hier ]

Data Flows




Er zijn 3 XBitmaps van elk 760 x 1080 pixels in 32bit kleuren formaat.
    map1 - raster achtergrond, tekeningen
    map2 - map1 + tekst
    map3 - map2 + tijdelijke tekeningen tijdens constructie
Map1 bevat tekeningen en dient als achtergrond voor de tekst.
Tekst wordt gewist door een gedeelte van map1 naar map2 te schrijven.
Tekst wordt toegevoegd in map2.
Map3 bevat tekeningen tijdens de constructie.
Grafische elementen worden dan gewist door een gedeelte van map2 naar map3 te kopiëren.
Het resultaat wordt pas zichtbaar na kopiëren van map3 naar de paintbox.

De hoogte van de paintbox is minder dan van de bitmaps, een scrollbar bepaalt welk deel van map3
in de paintbox wordt afgebeeld.

Bij elke XBitmap hoort een Trect (rect1,rect2,rect3).
Zowel de XFont als de XBitmap class hebben de property modrect.
Dat is het rechthoekige deel van de bitmap waarin de laatste wijziging plaatsvond.
Deze rechthoeken moeten in rect1,rect2,rect3 worden ingevoerd voor de overdracht tussen de XBitmaps.
Als een tekening uit meerdere elementen (lijnen, cirkels) bestaat dan moeten de modrects gecombineerd worden.
Alleen gewijzigde delen worden gekopieerd.

Bewerkingen met rectangles
Unirect(r1,r2) : rechthoek r2 wordt toegevoegd aan r1.
Interrect(r1,r2) : r1 wordt beperkt tot overlapping van r1 en r2.
Packrect(x1,y1,x2,y2 : smallInt) : Trect : maak Trect uit x,y waarden.



Bij rect1,rect2,rect3 horen ook de variabelen map1full,map2full,map3full : Boolean.
Deze vlaggen zijn false als de rectangles geen informatie bevatten.
In dat geval is een unirect(r1,r2) gelijk aan r1 := r2.

Alle variabelen voor de coördinaten zijn van het type smallInt.
Dat maakt ook coördinaten mogelijk die buiten de bitmap vallen.
Het maximale aantal pagina's is 30.
De y-coördinaat (gedeeld door 1080) bepaalt de pagina.

Het menu systeem




Hierboven staat het hoofdmenu.
Een enumerated variabele (dat is een volgnummer met een naam) houdt bij welke menuknop actief is.
type TMainbutton   = (mbDoc,mbPage,mbEdit,mbFrame,mbText,mbGraph,mbSymbol,mbGeo,
                      mbInfo,mbhelp,mbOff);
....
var mainmenubutten : TMainbutton;					  
 
Activeren van een mainbutton opent weer submenu's en toont property buttons.



Menu voor gemeenschappelijke properties.
Activeren van een property knop opent dialogs voor de keuze van
kleuren, lijndikten, streep-stip patronen,
aantallen rijen en kolommen.
Geselecteerde eigenschappen van grafische elementen en tekst worden opgeslagen in records zoals
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 selectie xxx komt terecht in variable xxxProp.
De tekstelementen bevatten een index naar de tabel waarin de properties zijn opgeborgen.
Voor het font is dat array fontDEF, in het algemeen xxxDEF.
Tekst Element[element nr].p1 is in geval van lijnen, macro's en characters een index naar de font tabel.

Zo zijn er vele property variabelen, al of niet met DEF tabellen.
Deze properties blijven in de tabellen staan, ook als de elementen worden verwijderd.
Bijschrijven van een nieuwe propertie in een tabel doen procedures met de naam registerXXX(....)

Control flow

Keyboard en muis genereren events.
Menuvariabelen zoals mainmenubutton sturen met case-statements die events door
naar procedures voor het tekenen of wijzigen van grafische symbolen of tekstverwerking.
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;
Hierboven is te zien dat de code voor het editen van grafische elementen nog moet komen.
Dit is een eenvoudig voorbeeld van de structuur.
Andere, soortgelijke, procedures bestaan uit veel meer code.

Tekstverwerking

Alle tekstelementen (letters, macro's, lijnen) zijn van:
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					    
De samenhang tussen deze elementen staat in:
type TLinks = record
               parent   : dword;
               child    : dword;
               next     : dword;
               previous : dword;
              end;
.....
var Links : array[1..maxelement] of TLinks;
De Element en Link arrays liggen dus precies naast elkaar.
Het Link array is niet rechtstreeks te lezen of te wijzigen:
dat kan alleen via de aanroep van procedures en functies.
Ook het UNDO systeem is hieraan gekoppeld.

In tegenstelling tot de properties worden entries in Elements[ ] gewist zodat
ze weer voor hergebruik beschikbaar komen.
Alleen de XTree unit kan dat en die stelt hiervan de textcontrol_unit op de hoogte.
XTree wijzigt het Element[ ] array niet omdat Xtree zich niet bemoeit met het soort element.
Deleten van een element koppelt het alleen los van de Tree.
UNDO voegt het weer in.

Tekst controle

De textcontrol_unit is hier verantwoordelijk voor.
Een eerste vereiste is cursor controle.

Cursorbeweging binnen lijnen: Dat is de afhandeling van events van de linker- en rechter- cursortoetsen.
Binnen een lijn kunnen tekens van verschillend font (kleur, steil,grootte) broederlijk naast elkaar staan.
Daarom bevat de cursor property een veld posR:Boolean dat aangeeft
of de cursor links dan wel rechts van het element staat.
De cursor past zijn grootte aan op het element en nieuw ingevoerde tekst heeft ook die properties.
Bij posR = true zal
- een left event de cursor links van het element plaatsen
- een right event de cursor rechts van het volgende element plaatsen
- bij het laatste element van de lijn de parent aanroepen

Bij posR = false zal
- een left event de cursor links van het vorige element plaatsen
- een right event de cursor rechts van het element plaatsen
- bij het eerste element van de lijn de parent aanroepen

De aangeroepen parent kan van het type elBock of elMacro zijn.
In het geval van een macro moet per soort worden bepaald wat de cursor doet.
Meestal zal naar een line element (child) van de macro worden verwezen.

En dan er er nog cursor Up- en Down events.
Bij lijnen binnen macro's wordt de (parent) macro aangeroepen.
In elk geval verlaat de cursor de line.

Voor cursorcontrole heeft elke macro zijn eigen procedure waar de events binnenkomen.
Daarbij zijn er nog algemene procedures per type macro: met 1, 2 of 3 (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;
Variable cuNAV bepaalt de route van de cursor.
Hier een stukje van de procedure die het text event ontvangt
....
  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;
....						   
leidt de cursor naar het type element....
procedure CuNavigate;
//switch to element type
//use cuNAV
var mac : TTextbutton;
    framecode : TFrameButton;
    el : dword;
    m23 : boolean;
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;
....			
In geval van een macro met één line:
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 plaatst de cursor zo mogelijk op het child element met dezelfde X positie,
gaat dus vertikaal omhoog en omlaag.
De procedure GotoChild ziet er zo uit:
procedure GotoChild;
begin
 with cuNAV do
  begin
   srcEL := destEL;
   cproc := cpM2C;
   getChild(destEL,srcEL);
  end;
 cuNavigate;
end;
cpM2C betekent cursor procedure Message to Child.
Er is ook een cpM2P, message to parent.
Een element weet zo of het door parent of child werd aangeroepen.

Tekstverwerking

Drie units houden zich met de tekst bezig:
    - textcontrol : cursor bewegingen
    - text paint : tekst en macro's
    - text calculation
Elementen zijn tamelijk abstract.
Voor elk soort element (elBlock,elMacro,elLine,elChar) zijn er drie specifieke procedures
    create : beschrijven het element en gebruiken de geselecteere properties
    paint : tekenen het element
    recalculate : berekent de hoogte,breedte en positie van de children
Een element berekent nooit zijn eigen positie, dat doet zijn parent.

Het voordeel van deze abstracte aanpak is dat met elementen kan worden gesleept zonder ons om de
inhoud te bekommeren. De (x,y) positie van children is altijd relatief t.o.v. de parent.
Als een element is gewijzigd door een verwijdering of toevoeging van een element dan wordt de parent
in kennis gesteld. Die herberekend zijn afmetingen, zet de children op hun plek.
Bij veranderde afmetingen wordt weer de parent aangeroepen.
Het laatste herberekende element wordt opnieuw getekend.
De kern is deze procedure :
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 : oude breedte van element
oldh : oude hoogte
eraseRect : te wissen rectangle van oude element
getElementRect : berekent de rectangle van het element relatief t.o.v. de pagina
updateRect : voegt de rectangles samen
recalculateElement( ): naar groot case statement om juiste element te kiezen.

Element Insert of Delete procedures zijn dus onafhankelijk van het type element.

Hiermee besluit ik deel twee van de math editor beschrijving.
Ik hoop de structuur enigszins te hebben weergegeven.

Als afsluiter een afbeelding van een vector inproduct, wederom ingetikt in enkele seconden: