unit web;

{This unit encapsulates SHDocVw.dll and MSHTML.dll functionality by subclassing
TWebBrowser object as TWeb object

TWeb was designed for easy use of HTML display and editing capacity in
SuperMemo 2002 for Windows developed by SuperMemo R&D in Fall 2001.

SuperMemo 2002 implements HTML-based incremental reading in which extensive HTML
support is vital

Pieces of this units can be used by anyone in other Delphi applications that make
use of HTML WYSIWYG interfaces made open by Microsoft

Please send comments, questions, suggestions for improvements or bugs to:
bugs(AT)supermemo(.)com}

{IMPORTANT! you need to assign TWinControl(Web).Parent to make Web:TWeb visible}
{For example, putting TWeb on a panel:
   TheWeb:=TWeb.Create(Panel);
   TheWeb.TheParent:=Self; //Form parenting the panel
   TWinControl(TheWeb).Parent:=Panel;
   TWinControl(TheWeb).Align:=alClient;
   TheWeb.Tag:=1; //Set different Tag if more TWeb's placed in a form}

interface

uses SysUtils,WinTypes,
     ActiveX,MSHTMLEvents,SHDocVw,MSHTML,
     IeConst,Classes,Forms,Graphics;

const
  CGID_MSHTML:TGUID='{DE4BA900-59CA-11CF-9592-444553540000}';
  IID_IOleCommandTarget:TGUID='{B722BCCB-4E68-101B-A2BC-00AA00404770}';
  CGID_WebBrowser:TGUID='{ED016940-BD5B-11cf-BA4E-00C04FD70816}';
  FontScale=3;

var IEStr,LastHyperlink:string;
    ApplicationOnMessage:TMessageEvent; {set to Forms.Application.OnMessage when creating the main form}
    OnMessageCompNo:byte;

type TWeb=class(TWebBrowser)
          public
             Modified,ReadOnly,Extrinsic,SuperMemoMenu,_Filter:boolean;
             TheDoc:IHTMLDocument2; //MSHTML HTML Document 2 interface
             TheWind:IHTMLWindow2;
             DocCmd,WebCmd:IOleCommandTarget; //MSHTML IOLECommandTarget interface
             Editable:boolean;
             TheSlot,TheTextPosit:integer;
             LastClickX,LastClickY:integer;
             TheParent:TForm;
             ElWind:byte; {pointer reference error protection; to occlude ElementWindow.ElWind in SuperMemo}
             LoadedHTMLFile:string;
             procedure Clear;
             procedure Save;
             procedure SaveAs(DefaultName:string);
             function SaveFile(Filename:string):boolean;
             procedure Print;
             procedure SetState(Edit:boolean);
             procedure LoadFile(FileName:string);
             procedure StatusTextChange(Sender: TObject; const Text: WideString);
             procedure CommandStateChange(Sender: TObject; Command: Integer; Enable: WordBool);
             procedure BeforeNavigate2(Sender: TObject;const pDisp: IDispatch;
                 var URL: OleVariant;var Flags: OleVariant;var TargetFrameName:OleVariant;
                 var PostData: OleVariant;var Headers: OleVariant;var Cancel: WordBool);
             procedure NavigateComplete2(Sender: TObject;const pDisp: IDispatch; var URL: OleVariant);
             constructor Create(Owner:TComponent); override;
             destructor Destroy; override;
             procedure SetFocus; override;
             function SourceText:string;
             procedure SetSelection(Start,Length:integer);
             function GetTextRange:IHtmlTxtRange;
             function SelStart:integer;
             function SelEnd:integer;
             function SelLength:integer;
             procedure SetBackgroundColor(Color:TColor);
             function GetBackgroundColor:TColor;
             procedure SetHTMLText(HTML:WideString);
             procedure WaitLoad(peek:boolean);
             function GetScrollTop:integer;
             procedure SetScrollTop(ScrollTop:integer);
             function GetHTMLSelection:WideString;
             procedure ClearSelection;
             procedure ReplaceSelection(HTML:string);
             procedure SetFont(AFont:TFont;SetFontMode:byte);
             function Text:string;
             procedure SetBorder(Border:boolean);
             procedure SetScrollBar(ScrollBar:boolean);
             procedure Undo;
             procedure GetFont(var AFont:TFont);
             procedure FontDialog;
             function  SpecialCommand(Cmd : Cardinal; PromptUser : boolean;
                       editModeOnly : boolean; bTriEditCommandGroup : boolean;
                       InputArgs : OleVariant) : HRESULT;
             procedure ToggleBullet;
             procedure ToggleNumbering;
             procedure Indent(Indent:boolean);
             procedure Align(IDM:integer);
             function SelText:string;
             procedure Paste;
             procedure Copy;
             procedure Cut;
             procedure Delete;
             procedure SelectAll;
             procedure SetText(HTML:string);
             procedure CompleteLoading;
             function CompNo:byte;
             function Visible:boolean;
             procedure Find;
             procedure Subscript;
             procedure Superscript;
             {end public}
          private
             OleInPlaceActiveObject: IOleInPlaceActiveObject;
             Events:TMSHTMLHTMLDocumentEvents;
             CtrlToBeProcessed,ShiftToBeProcessed:boolean;
             procedure SuperMemoMessageHandler(var Msg: TMsg; var Handled: Boolean);
             procedure OnMouseDown(Sender:TObject);
             procedure OnMouseUp(Sender:TObject);
             procedure OnMouseMove(Sender:TObject);
             procedure OnMouseOver(Sender:TObject);
             procedure OnMouseOut(Sender:TObject);
             function OnClick(Sender:TObject):WordBool;
             function OnSelectStart(Sender:TObject):WordBool;
             procedure OnFocusOut(Sender:TObject);
             procedure OnFocusIn(Sender:TObject);
             function OnContextMenu(Sender:TObject):WordBool;
             function OnKeyPress(Sender:TObject):WordBool;
             procedure OnKeyDown(Sender:TObject);
             procedure OnKeyUp(Sender:TObject);
             procedure ClickPoint(X,Y:integer);
             procedure DefineEvents;
             function HyperlinkClicked:boolean;
             function HrExecCommand(ucmdID: cardinal;
                       const pVarIn: OleVariant; var pVarOut: OleVariant; bPromptUser,
                       bTriEditCmdGroup: boolean): HResult;
             procedure ProcessLoadMessages;
             procedure SetBorderWidth;
             procedure ReassignKeyboardHandler(CompNo:byte;TurnOn:boolean);
            {end private}
          end;

implementation

uses WinProcs,Messages,Controls,Variants,Clipbrd,
     {Pieces of code specific to SuperMemo are marked as: SMSpecific:}
     {SMSpecific:}{These units are specific to SuperMemo}
     const8,Basic,Dial,Files,Component,
     DBDat,Col,Option,LayoutMan,ElementWindow,Main;

var PtrWGUID,PtrMGUID,PtrDGUID:PGUID;
    NULL:OleVariant;

constructor TWeb.Create(Owner:TComponent);
begin
  inherited Create(Owner);
  LoadedHTMLFile:='';
  OnBeforeNavigate2:=BeforeNavigate2;
  OnStatusTextChange:=StatusTextChange;
  OnCommandStateChange:=CommandStateChange;
  OnNavigateComplete2:=NavigateComplete2;
  TheDoc:=nil;
  DocCmd:=nil;
  WebCmd:=nil;
  Modified:=false;
  SuperMemoMenu:=true; {SMSpecific}
  LastClickX:=0;
  LastClickY:=0;
  ReadOnly:=false;
  CtrlToBeProcessed:=false;
  ShiftToBeProcessed:=false;
  Events:=nil;
end;

destructor TWeb.Destroy;
begin
  ReassignKeyboardHandler(Tag,false);
  inherited Destroy;
end;

procedure TWeb.SaveAs(DefaultName:string);
begin
  if TheDoc=nil then
     exit;
  TheDoc.ExecCommand('SaveAs',false,DefaultName);
  Modified:=false;
end;

procedure TWeb.Print;
begin
  ExecWB(OLECMDID_PRINT,OLECMDEXECOPT_PROMPTUSER,NULL,NULL);
end;

procedure TWeb.Save;
var
  sHTML:string;
  fsOut:TFileStream;
begin
  sHTML:=SourceText;
  fsOut:=TFileStream.Create(TheDoc.URL,fmCreate or fmShareExclusive);
  try
    fsOut.Write(pchar(sHTML)^, length(sHTML));
  finally
    fsOut.Free;
    Modified:=false;
  end;
end;

procedure TWeb.OnKeyUp(Sender:TObject);
begin {those empty handlers are needed for the keyboard and mouse to behave correctly with EventSink!}
  {nop}
end;

procedure TWeb.OnMouseMove(Sender:TObject);
begin
 {nop}
end;

procedure TWeb.OnMouseOver(Sender:TObject);
begin
  {nop}
end;

procedure TWeb.OnMouseOut(Sender:TObject);
begin
 {nop}
end;

function TWeb.OnClick(Sender:TObject):WordBool;
begin
  Result:=true;
end;

function TWeb.OnSelectStart(Sender:TObject):WordBool;
begin
  Result:=true;
end;

procedure TWeb.OnMouseUp(Sender:TObject);
begin
  {nop}
end;

procedure TWeb.Clear;
begin
  TheTextPosit:=0;
  SetHTMLText('');
end;

function TWeb.SelStart:integer;
var TextRange:IHtmlTxtRange;
begin
  Result:=0;
  TextRange:=GetTextRange;
  if TextRange=nil then
     exit;
  Result:=Abs(Integer(TextRange.move('character',-MaxTextLength)));
end;

function TWeb.SelEnd:integer;
var TextRange:IHtmlTxtRange;
begin
  Result:=0;
  TextRange:=GetTextRange;
  if TextRange=nil then
     exit;
  Result:=Abs(Integer(TextRange.MoveEnd('character',-MaxTextLength)));
end;

function TWeb.SelLength:integer;
begin
  Result:=SelEnd-SelStart;
end;

function TWeb.GetScrollTop:integer;
var FocusElement:IHTMLElement2;
begin
  Result:=0;
  try
     if TheDoc=nil then
        exit;
     FocusElement:=TheDoc.ActiveElement as IHTMLElement2;
     if FocusElement=nil then
        exit;
     Result:=FocusElement.ScrollTop;
  except
    on E:Exception do EError('Cannot get scroll top position',E);
    end;
end;

function TWeb.GetHTMLSelection:WideString;
var TextRange:IHtmlTxtRange;
begin
  Result:='';
  TextRange:=GetTextRange;
  if TextRange=nil then
     exit;
  Result:=TextRange.HTMLText;
end;

procedure TWeb.ClearSelection;
begin
  if TheDoc=nil then
     exit;
  TheDoc.Selection.Clear;
  Modified:=true;
end;

procedure TWeb.ReplaceSelection(HTML:string);
var TextRange:IHtmlTxtRange;
begin
  try
     TextRange:=GetTextRange;
     if TextRange=nil then
        exit;
     TextRange.PasteHTML(HTML); {Warning! pasting relative paths will result in conversion to absolute paths!}
     Modified:=true;
  except
   on E:Exception do begin
      ShortenString(HTML,80);
      EError('Error pasting HTML'+nl+
      'Microsoft HTML refuses to paste this string:'+nl+
      HTML+nl,E);
      end;
   end;
end;

procedure TWeb.SetFont(AFont:TFont;SetFontMode:byte);
begin
  try
     if TheDoc=nil then
        exit;
     Modified:=true;
     case TSetFontMode(SetFontMode) of
        sfAll:begin
          SetFont(AFont,byte(sfName));
          SetFont(AFont,byte(sfSize));
          SetFont(AFont,byte(sfStyle));
          SetFont(AFont,byte(sfColor));
          SetFont(AFont,byte(sfCharset));
          end;
        sfSize:TheDoc.ExecCommand('FontSize',false,AFont.Size div FontScale);
        sfColor:TheDoc.ExecCommand('ForeColor',false,AFont.Color);
        sfName:TheDoc.ExecCommand('FontName',false,AFont.Name);
        sfStyle:begin
           {bold}
           if TheDoc.queryCommandValue('Bold') then
              TheDoc.ExecCommand('Bold',false,true);
           if fsBold in AFont.Style then
              TheDoc.ExecCommand('Bold',false,true);
           {italic}
           if TheDoc.queryCommandValue('Italic') then
              TheDoc.ExecCommand('Italic',false,true);
           if fsItalic in AFont.Style then
              TheDoc.ExecCommand('Italic',false,true);
           {underline}
           if TheDoc.queryCommandValue('Underline') then
              TheDoc.ExecCommand('Underline',false,true);
           if fsUnderline in AFont.Style then
              TheDoc.ExecCommand('Underline',false,true);
          end;
        end;
  except
     on E:Exception do EError('Error setting HTML font',E);
    end;
end;

procedure TWeb.SetBorder(Border:boolean);
begin
  if TheDoc=nil then
     exit;
  if TheDoc.body=nil then
     exit;
  if not Border then begin
     if not Editable then begin
        TheDoc.body.style.borderStyle:='none';
        TheDoc.body.style.borderWidth:='thin';
        TheDoc.body.style.borderColor:='white';
        end;
     if Editable then begin
        TheDoc.body.style.borderStyle:='none';
        TheDoc.body.style.borderWidth:='thin';
        TheDoc.body.style.borderColor:='blue';
        end;
     end;
  if Border then begin
     if not Editable then begin
        TheDoc.body.style.borderStyle:='solid';
        TheDoc.body.style.borderWidth:='thin';
        TheDoc.body.style.borderColor:='silver';
        end;
     if Editable then begin
        TheDoc.body.style.borderStyle:='solid';
        TheDoc.body.style.borderWidth:='thin';
        TheDoc.body.style.borderColor:='blue';
        end;
     end;
end;

procedure TWeb.SetScrollBar(ScrollBar:boolean);
begin
  if TheDoc=nil then
     exit;
  if TheDoc.body=nil then
     exit;
  if ScrollBar then {values of "hidden" and "visible" lock PgUp and PgDn!}
     TheDoc.body.style.overflow:='scroll'
  else
     TheDoc.body.style.overflow:='auto';
end;

procedure TWeb.Undo;
begin
  if TheDoc=nil then
     exit;
  TheDoc.ExecCommand('Undo',false,0);
  Modified:=true;
end;

procedure TWeb.FontDialog;
begin
  SpecialCommand(IDM_FONT,True,True,False,Null);
  Modified:=true;
end;

function TWeb.SpecialCommand(Cmd:Cardinal;PromptUser:boolean;
                              editModeOnly:boolean;bTriEditCommandGroup:boolean;
                              InputArgs:OleVariant):HRESULT;
begin
  Result:=HrExecCommand(Cmd,null,InputArgs,promptUser,bTriEditCommandGroup);
end;

function TWeb.HrExecCommand(ucmdID: cardinal;
  const pVarIn: OleVariant; var pVarOut: OleVariant; bPromptUser,
  bTriEditCmdGroup: boolean): HResult;
var dwCmdOpt:DWORD;
begin
   result := S_OK;
   if DocCmd = nil then
      Exit;
   if (bPromptUser) then
      dwCmdOpt := MSOCMDEXECOPT_PROMPTUSER
   else
      dwCmdOpt := MSOCMDEXECOPT_DONTPROMPTUSER;
   if (bTriEditCmdGroup) then
      result := DocCmd.Exec(@GUID_TriEditCommandGroup,ucmdID,dwCmdOpt,pVarIn,pVarOut)
   else
      result := DocCmd.Exec(@CMDSETID_Forms3,ucmdID,dwCmdOpt,pVarIn,pVarOut);
end;

procedure TWeb.ToggleBullet;
begin
  if TheDoc=nil then
     exit;
  SpecialCommand(IDM_UnORDERLIST,false,true,false,Null);
  Modified:=true;
end;

procedure TWeb.ToggleNumbering;
begin
  if TheDoc=nil then
     exit;
  SpecialCommand(IDM_ORDERLIST,false,true,false,Null);
  Modified:=true;
end;

procedure TWeb.Align(IDM:integer);
begin
  if TheDoc=nil then
     exit;
  SpecialCommand(IDM,false,true,false,Null);
  Modified:=true;
end;

procedure TWeb.SetBackgroundColor(Color:TColor);
begin
  if TheDoc=nil then
     exit;
  TheDoc.ExecCommand('BackColor',false,Color);
  Modified:=true;
end;

function TWeb.SelText:string;
var TextRange:IHtmlTxtRange;
begin
  Result:='';
  TextRange:=GetTextRange;
  if TextRange=nil then
     exit;
  Result:=TextRange.text;
end;

procedure TWeb.Indent(Indent:boolean);
begin
  if TheDoc=nil then
     exit;
  if Indent then
     TheDoc.ExecCommand('Indent',false,0)
  else
     TheDoc.ExecCommand('Outdent',false,0);
  Modified:=true;
end;

procedure TWeb.BeforeNavigate2(Sender: TObject;
   const pDisp: IDispatch;var URL: OleVariant;var Flags: OleVariant;var TargetFrameName:OleVariant;var PostData: OleVariant;
   var Headers: OleVariant;var Cancel: WordBool);
begin
  if SuperMemoURL(URL) then begin {cancel the link if it uses an internal SuperMemo protocol}{SMSpecific}
     Cancel:=true;
     LastHyperlink:='';
     exit;
     end;
end;

procedure TWeb.SelectAll;
begin
  SetSelection(0,MaxTextLength);
end;

procedure TWeb.NavigateComplete2(Sender: TObject;const pDisp: IDispatch; var URL: OleVariant);
begin
  CompleteLoading;
end;

procedure TWeb.CompleteLoading;
begin
  Waitload(false); {used only to set up interface variables}
  SetBorderWidth;
  if ReadOnly then
     exit;
  if TheDoc=nil then
     exit;
  DefineEvents;
  {do not use Modified:=false here as navigation might go from an edited page to a hyperlinked page}
  if TheParent<>ElementWindow.ElWind then
     exit;
  if TElWind(TheParent).GetState(CompNo)=stEdit then
     SetState(true)
  else
     SetState(false);
end;

procedure TWeb.SetHTMLText(Html:WideString); {after this command, Copy and Paste will not work}
var V:OleVariant;
begin
   try
      Stop;
      V:=Document;
      V.Open;
      V.Clear;
      V.Write(Html);
      V.Close;
      Modified:=true;
   except
      on E:Exception do EError('Error setting HTML text',E);
     end;
end;

function TWeb.CompNo:byte;
begin
  Result:=byte(Tag);
end;

function TWeb.Visible;
begin
  Result:=true;
  if TheParent<>ElementWindow.ElWind then
     exit;
  Result:=TWinControl(ElementWindow.ElWind.Objects[CompNo]).Visible;
end;

function TWeb.GetBackgroundColor:TColor;
var Background:OleVariant;
    vt:TVarType;
begin
  Result:=clWindow;
  try
     if TheDoc=nil then
        exit;
     if not Visible then {SMSpecific:}{to avoid errors on Ctrl+T before Show Answer}{Nov 8, 2001}
        exit;
     Background:=TheDoc.queryCommandValue('BackColor');
     vt:=varType(Background);
     if vt<>varNull then
        Result:=Background;
  except
    on E:Exception do EError('Error retrieving background color',E);
    end;
end;

procedure TWeb.GetFont(var AFont:TFont);
var FontName,FontSize,FontColor:OleVariant;
    vt:TVarType;
begin
  try
     if TheDoc=nil then
        exit;
     {name}
     FontName:=TheDoc.queryCommandValue('FontName');
     vt:=varType(FontName);
     if vt<>varNull then
        AFont.Name:=FontName
     else
        AFont.Name:=Database.DefaultFont.Name;
     {size}
     FontSize:=TheDoc.queryCommandValue('FontSize');
     vt:=varType(FontSize);
     if vt<>varNull then
        AFont.Size:=FontSize*FontScale
     else
        AFont.Size:=Database.DefaultFont.Size;
     {color}
     FontColor:=TheDoc.queryCommandValue('ForeColor');
     vt:=varType(FontColor);
     if vt<>varNull then
        AFont.Color:=FontColor
     else
        AFont.Color:=Database.DefaultFont.Color;
     {style}
     AFont.Style:=[];
     {bold}
     if TheDoc.queryCommandValue('Bold') then
        AFont.Style:=AFont.Style+[fsBold];
     {italic}
     if TheDoc.queryCommandValue('Italic') then
        AFont.Style:=AFont.Style+[fsItalic];
     {underline}
     if TheDoc.queryCommandValue('Underline') then
        AFont.Style:=AFont.Style+[fsUnderline];
  except
    on E:Exception do EError('Error detecing HTML font',E);
    end;
end;

procedure TWeb.Find;
const HTMLID_FIND=1;
var vaIn,vaOut:OleVariant;
begin
  if WebCmd=nil then
     exit;
  WebCmd.Exec(PtrWGUID,HTMLID_FIND,0,vaIn,vaOut); {this command is not guaranteed to work in future versions of IE!}
end;

function TWeb.OnKeyPress(Sender:TObject):WordBool;
begin {must be defined empty for the keyboard to work correctly}
  Modified:=true;
  Result:=true;
end;

function TWeb.OnContextMenu(Sender:TObject):WordBool;
var APoint:TPoint;
begin
  Result:=true;
  if AccessMode<amFull then
     exit;
  if SM8Main.IsSimplified then
     exit;
  if not SuperMemoMenu then
     exit;
  Result:=false;
  if TheParent<>ElementWindow.ElWind then
     exit;
  if TheDoc=nil then
     exit;
  if TheDoc.body=nil then
     exit;
  if Extrinsic then
     exit;
  if ReadOnly then
     exit;
  if TheWind=nil then
     exit;
  if TheWind.Event=nil then
     exit;
  GetCursorPos(APoint);
  ElementWindow.ElWind.CurrentComponent:=CompNo;
  ElementWindow.ElWind.ComponentMenu.PopUp(APoint.X,APoint.Y);
end;

procedure TWeb.SetText(HTML:string);
begin
  if (TheDoc=nil)or(TheDoc.body=nil) then
     SetHTMLText(HTML)
  else
     TheDoc.body.innerHTML:=HTML;
end;

procedure TWeb.DefineEvents;
begin
  if IEVer<5.5 then {if Internet Explorer older than 5.5 then ignore events}
     exit;
  if Events<>nil then
     Events.Free;
  Events:=TMSHTMLHTMLDocumentEvents.Create(Self);
  Events.Connect(IUnknown(Document));
  Events.OnMouseDown:=OnMouseDown;
  Events.OnMouseUp:=OnMouseUp;
  Events.OnMouseMove:=OnMouseMove;
  Events.OnMouseOver:=OnMouseOver;
  Events.OnMouseOut:=OnMouseOut;
  Events.OnClick:=OnClick;
  Events.OnSelectStart:=OnSelectStart;
  Events.OnKeyPress:=OnKeyPress;
  Events.OnKeyDown:=OnKeyDown;
  Events.OnKeyUp:=OnKeyUp;
  Events.OnContextMenu:=OnContextMenu;
  Events.OnFocusOut:=OnFocusOut;
  Events.OnFocusIn:=OnFocusIn;
end;

procedure TWeb.Paste;
begin
  if TheDoc=nil then
     exit;
  TheDoc.ExecCommand('Paste',false,0);
  Modified:=true;
  _Filter:=true;
end;

procedure TWeb.Delete;
begin
  if TheDoc=nil then
     exit;
  TheDoc.ExecCommand('Delete',false,0);
  Modified:=true;
  _Filter:=true;
end;

procedure TWeb.Cut;
begin
  Copy;
  Delete;
end;

procedure TWeb.SetFocus;
begin
  try
    if TheDoc=nil then
       exit;
    SendMessage(Handle,wm_Activate,1,0);
    if TheWind<>nil then
       TheWind.Focus; {TheWind.Focus must come before TWeb.SetFocus}
    if TheParent.Visible then {Parenting window hosting web browser}
       if Visible then
          if CanFocus then
             inherited SetFocus; {must come AFTER TheWind.Focus}
  except
    on E:Exception do EError('Error setting focus on HTML component',E);
    end;
end;

function TWeb.HyperlinkClicked:boolean;
var Element:IHTMLElement;
begin
  Result:=false;
  LastHyperlink:='';
  if TheWind.Event=nil then
     exit;
  Element:=TheWind.Event.srcElement;
  repeat
    if Element.tagName='A' then begin
       LastHyperlink:=Element.getAttribute('href',0);
       if LastHyperlink<>'' then begin
          Result:=TheWind.Event.Button=1; {Button=1 is the left mouse button}
          exit;
          end;
       end;
    if Element<>nil then
       Element:=Element.ParentElement;
    until Element=nil;
end;

procedure TWeb.SetScrollTop(ScrollTop:integer);
begin
  if TheDoc=nil then
     exit;
  if TheWind=nil then
     exit;
  TheWind.scrollTo(0,ScrollTop);
end;

procedure TWeb.Subscript;
begin
  if TheDoc=nil then
     exit;
  TheDoc.execCommand('Subscript',False,0);
  Modified:=true;
end;

procedure TWeb.Superscript;
begin
  if TheDoc=nil then
     exit;
  TheDoc.execCommand('Superscript',False,0);
  Modified:=true;
end;

procedure TWeb.ProcessLoadMessages;
var msg:TMsg;
    OldElementNo:integer;
    MessageQueue:array of TMsg;
    m:integer;
begin
  OldElementNo:=ElementWindow.ElWind.TheElement;
  while PeekMessage(msg,0,wm_KeyFirst,wm_KeyLast,pm_Remove) do; {remove keyboard input first}
  while PeekMessage(msg,0,wm_MouseFirst,wm_MouseLast,pm_Remove) do; {remove mouse input}
  while PeekMessage(msg,0,wm_Close,wm_Close,pm_Remove) do; {disallow closing the application}
  while PeekMessage(msg,0,wm_ActivateApp,wm_ActivateApp,pm_Remove) do; {disallow activating the application}
  while PeekMessage(msg,0,wm_User,cm_LastUserMessage,pm_Remove) do begin
        SetLength(MessageQueue,length(MessageQueue)+1);
        MessageQueue[length(MessageQueue)-1]:=msg;
        end;
  forms.Application.ProcessMessages; {process messages needed to complete navigation}
  for m:=1 to length(MessageQueue) do begin
      msg:=MessageQueue[m-1];
      PostMessage(msg.hwnd,msg.message,msg.wParam,msg.lParam);
      end;
  if ElementWindow.ElWind.TheElement<>OldElementNo then
     Error('Element changed while loading HTML'+nl+
           'Loading: '+ElementStr(OldElementNo)+nl+
           'Changed to: '+ElementStr(ElementWindow.ElWind.TheElement));
end;

function TWeb.SaveFile(Filename:string):boolean;
var Source:string;
begin
  Result:=false;
  try
     Modified:=false;
     Source:=SourceText;
     if pos('&#',Source)=0 then
        if pos('<',Source)=0 then
           if pos('>',Source)=0 then begin
              Result:=false; {do not save plain text into the file}
              exit;
              end;
     WriteStringToTXTFile(FileName,Source);
     Result:=true;
  except
    on Exception do Error('Error writing to "'+Filename+'"');
    end;
end;

function TWeb.SourceText:string;
var WS:WideString;
    ch:WideChar;
    n:integer;
    w:word;
    s:string;
begin
  Result:='';
  if TheDoc=nil then
     exit;
  WS:=TheDoc.body.innerHTML;
  for n:=1 to length(WS) do begin
      ch:=WS[n];
      w:=word(ch);
      if w>255 then begin
         s:=IntToStr(w);
         s:='&#'+s+';';
         end
      else
         s:=ch;
      Result:=Result+s;
      end;
end;

function TWeb.Text:string;
var WS:WideString;
    ch:WideChar;
    n:integer;
    w:word;
    s:string;
begin
  Result:='';
  if TheDoc=nil then
     exit;
  WS:=TheDoc.body.innerText;
  for n:=1 to length(WS) do begin
      ch:=WS[n];
      w:=word(ch);
      if w>255 then begin
         w:=(w mod 256)+48;
         s:=IntToStr(w);
         s:=char(w);
         end
      else
         s:=ch;
      Result:=Result+s;
      end;
end;

procedure TWeb.ClickPoint(X,Y:integer);
var TextRange:IHtmlTxtRange;
begin
  try
     TextRange:=GetTextRange;
     if TextRange=nil then
        exit;
     TextRange.MoveToPoint(X,Y);
     TextRange.Select;
  except
    on E:Exception do EError('Error processing mouse click',E);
    end;
end;

procedure TWeb.WaitLoad(peek:boolean);
begin
  try
     TheDoc:=Document as IHTMLDocument2;
     while TheDoc=nil do begin
        if peek then
           ProcessLoadMessages
        else
           exit;
        TheDoc:=Document as IHTMLDocument2;
        end;

     repeat
        ControlInterface.QueryInterface(IID_IOleCommandTarget,WebCmd);
        until WebCmd<>nil;

     repeat
        TheDoc.QueryInterface(IOleCommandTarget,DocCmd);
        until DocCmd<>nil;

     repeat
        TheWind:=TheDoc.parentWindow;
        until TheWind<>nil;

     while (TheDoc=nil)or((theDoc.ReadyState<>'complete')and(theDoc.ReadyState<>'interactive')) do begin
        {remove messages that should not be processed while the element is loading}
        {TheDoc can become nil when switching applications!}
        if TheDoc=nil then
           MessageBeep(0); {this beep is sounded while page is loading while SuperMemo is no longer in forefront}
        if peek then
           ProcessLoadMessages
        else
           exit;
        end;

  except
       on E:Exception do EError('Error loading the document',E);
    end;
end;

procedure TWeb.SetSelection(Start,Length:integer);
var TextRange:IHtmlTxtRange;
begin
  try
     if TheDoc=nil then
        exit;
     TheDoc.Selection.Empty;
     TextRange:=GetTextRange;
     if TextRange=nil then
        exit;
     TextRange.collapse(true);
     l:=TextRange.moveEnd('character',Start+Length);
     l:=TextRange.moveStart('character',Start);
     TextRange.select;
  except
    on E:Exception do EError('Error setting HTML selection'+nl+
                             'Start='+IntToStr(Start)+nl+
                             'Length='+IntToStr(Length),E);
    end;
end;

procedure TWeb.SuperMemoMessageHandler(var Msg: TMsg; var Handled: Boolean);
{this message handler is vital to enable accelerators, Del, Backspace and other keys}
var iOIPAO: IOleInPlaceActiveObject;
    Dispatch: IDispatch;
begin
  Handled:=false;
  if msg.message=wm_SysChar then begin
     SendMessage(ElementWindow.ElWind.Handle,Msg.message,Msg.wparam,Msg.lParam); {Nov 17, 2001}{to enable the main menu}
     Handled:=true;
     exit;
     end;
  if not ((msg.message=wm_keydown)or(msg.message=wm_keyup)or(msg.message=wm_char)) then
     exit;
  if msg.wparam=ord('P') then begin
     if ShiftToBeProcessed then
        if CtrlToBeProcessed then begin {Block printing}
           msg.wparam:=0;
           CtrlToBeProcessed:=false;
           ShiftToBeProcessed:=false;
           Handled:=true;
           SM8Main.MIElementParametersClick(nil);
           exit;
           end;
     if CtrlToBeProcessed then begin {Block printing}
        msg.wparam:=0;
        CtrlToBeProcessed:=false;
        Handled:=true;
        Sm8Main.MIPlanClick(nil); {SMSpecific}{Open Plan with Ctrl+P}
        exit;
        end;
     end;
  if msg.wparam=ord('F') then begin
     if CtrlToBeProcessed then begin {Block searching}
        msg.wparam:=0;
        CtrlToBeProcessed:=false;
        Handled:=true;
        ElementWindow.ElWind.LockReadPointOnSearch:=true;
        Sm8Main.ActFindStringExecute(nil); {SMSpecific}{Search whole collection}
        exit;
        end;
     end;
  if msg.wparam=17 then  {Ctrl key pressed}
     if msg.message=wm_keydown then begin
        CtrlToBeProcessed:=true;
        Handled:=true;
        exit;
        end;
  if msg.wparam=vk_Shift then
     if msg.message=wm_keydown then begin
        ShiftToBeProcessed:=true;
        Handled:=true;
        exit;
        end;
  CtrlToBeProcessed:=false;
  ShiftToBeProcessed:=false;
  if msg.wparam in [vk_Return,vk_Escape, {to ensure that Enter and shortcuts work}
     {vk_F3 cannot be used due to conflict with 'R'}
     vk_F4, {tasklist}
     {vk_F6 is handled in OnKeyDown}
     vk_F7, {read points}
     vk_F11, {random jump}
     vk_F12, {quick backup, recovery}
     vk_Up, {to handle Ctrl+Alt+Up}
     ord('1'), {Ctrl+Shift+1 for deHTMLize}
     {'A' cannot be listed due to conflict with Show Answer}
     {'D' cannot be listed due to conflict with Dismiss}
     ord('E'), {Ctrl+alt+E}
     ord('F'), {Ctrl+Shift+F}
     {'G' cannot be listed due to conflict with Cancel Grade}
     ord('H'), {Ctrl+Alt+H}
     ord('J'), {Ctrl+J}
     ord('K'), {Ctrl+Alt+K}
     {'M' cannot be listed due to conflict with Remember}
     ord('T'), {Ctrl+T}
     ord('U'), {Ctrl+Alt+U}
     ord('W') {Ctrl+W}
     ] then exit;
  if Msg.wParam in [vk_Back,vk_Delete] then
     Modified:=true;
  Handled:=(IsDialogMessage(Handle, Msg) = True);
  if (Handled) and (not Busy) then begin
    if OleInPlaceActiveObject = nil then begin
      Dispatch := Application;
      if Dispatch <> nil then begin
         Dispatch.QueryInterface(IOleInPlaceActiveObject, iOIPAO);
         if iOIPAO <> nil then
            OleInPlaceActiveObject := iOIPAO;
         end;
       end;
    if OleInPlaceActiveObject <> nil then
      if ((Msg.message = WM_KEYDOWN) or (Msg.message = WM_KEYUP)) and
         ((Msg.wParam=VK_BACK)or {do not include vk_Delete here!!!}{Nov 11, 2001}
         (Msg.wParam=VK_LEFT)or(Msg.wParam=VK_RIGHT)or
         (Msg.wParam=VK_Up)or
         (Msg.wParam=VK_Down)or
         (Msg.wParam=vk_Next)or(Msg.wParam=vk_Prior)) then
         {nop}
      else
        OleInPlaceActiveObject.TranslateAccelerator(Msg);
  end;
end;

procedure TWeb.SetBorderWidth;
begin
  try
     if TheDoc=nil then
        exit;
     if TheParent<>ElementWindow.ElWind then
        exit;
     SetBorder(TElWind(TheParent).GetBorder(CompNo));
     SetScrollBar(TElWind(TheParent).GetScrollBar(CompNo));
  except
    on Exception do Error('Cannot set HTML border');
    end;
end;

procedure TWeb.OnFocusIn(Sender:TObject);
begin
  if Editable then
     ReassignKeyboardHandler(CompNo,true); {each TWeb has a unique handler which must be reassigned at SetFocus}
end;

procedure TWeb.ReassignKeyboardHandler(CompNo:byte;TurnOn:boolean);
{assign HTML keyboard handler to HTML component; assign standard if TurnOn=false}
var IsDefault:boolean;
begin
  if TurnOn then
     if CompNo<>0 then begin
        Forms.Application.OnMessage:=SuperMemoMessageHandler; {each TWeb has a unique handler which must be reassigned at SetFocus}
        OnMessageCompNo:=CompNo;
        end;
  if not TurnOn then
     if CompNo=OnMessageCompNo then begin
        Forms.Application.OnMessage:=ApplicationOnMessage;
        OnMessageCompNo:=0;
        end;
  IsDefault:=OnMessageCompNo=0;
  if TheParent=ElementWindow.ElWind then begin
     ElementWindow.ElWind.Learn.Default:=IsDefault;
     ElementWindow.ElWind.ShowAnswer.Default:=IsDefault;
     ElementWindow.ElWind.Pass.Default:=IsDefault;
     ElementWindow.ElWind.NextRepetition.Default:=IsDefault;
     end;
end;

procedure TWeb.Copy;
var TheRange:OleVariant;
begin
  if TheDoc=nil then
     exit;
  TheDoc.ExecCommand('Copy',false,0);
  if Clipboard.AsText='' then begin
     if (TheDoc.Selection.type_='Text')or(TheDoc.Selection.type_='None') then
        TheRange:=TheDoc.Selection.CreateRange as IHtmlTxtRange
     else
        TheRange:=TheDoc.Selection.CreateRange as IHtmlControlRange;
     TheRange.execCommand('Copy');
     end;
end;

procedure TWeb.StatusTextChange(Sender: TObject; const Text: WideString);
begin
   SM8Main.ShowStatusText(Text);
end;

procedure TWeb.CommandStateChange(Sender: TObject; Command: Integer; Enable: WordBool);
begin
  case Command of
    CSC_NAVIGATEBACK:if not Enable then
       Extrinsic:=false; {to make sure Back< does not execute TWeb.GoBack}
    CSC_NAVIGATEFORWARD:{nop};
  end;
end;

procedure TWeb.SetState(Edit:boolean);
begin
  try
    SetBorderWidth;
    if ReadOnly then
       exit;
    if IEVer<5.5 then
       exit;
    if TheDoc<>nil then begin
       if Edit then begin
          if TheDoc.body<>nil then begin
             TheDoc.body.setAttribute('contentEditable','true',0);
             SetFocus;
             SuperMemoMenu:=true;
             Editable:=true;
             end;
          end;
       if not Edit then begin
          if TheDoc.body<>nil then begin
             TheDoc.body.setAttribute('contentEditable','false',0);
             ReassignKeyboardHandler(CompNo,false);
             Editable:=false;
             end;
          end;
       end;
  except
    on E:Exception do EError('Error switching HTML editing state',E);
    end;
end;

procedure TWeb.OnMouseDown(Sender:TObject);
var ScrollTop:integer;
begin
  if HyperlinkClicked then
     if not Editable then begin
        SuperMemoMenu:=false; {SMSpecific}
        Extrinsic:=true;
        exit;
        end;
  TComponent(Sender).Tag:=CompNo;
  if TheDoc=nil then
     exit;
  if TheDoc.body=nil then
     exit;
  if Extrinsic then
     exit;
  if ReadOnly then
     exit;
  if TheWind=nil then
     exit;
  if TheWind.Event=nil then
     exit;
  if Editable then begin
     if TheParent=ElementWindow.ElWind then begin
        if not TheWind.Event.altKey then
           exit;
        ElementWindow.ElWind.SetState(CompNo,stDrag);
        end;
     SetState(true); {Parent not ElementWindow}
     SetFocus;
     exit;
     end;
  if not Editable then begin
     if TheWind.Event.button<>1 {mbLeft} then
        exit;
     if Width-TheWind.event.offsetX<VerticalScrollBarWidth+2 then {scrollbar click}
        exit;
     if Height-(TheWind.event.offsetY-GetScrollTop)<HorizontalScrollBarHeight+2 then {scrollbar click}
        exit;
     LastClickX:=TheWind.Event.X;
     LastClickY:=TheWind.Event.Y;
     ScrollTop:=GetScrollTop;
     if TheParent=ElementWindow.ElWind then
        ElementWindow.ElWind.FormMouseDown(Sender,mbLeft,[],LastClickX,LastClickY) {SMSpecific}
     else
        SetState(true);
     SetFocus;
     SetScrollTop(ScrollTop);
     ClickPoint(LastClickX,LastClickY);
     end;
end;

procedure TWeb.OnFocusOut(Sender:TObject);
begin
  ReassignKeyboardHandler(CompNo,false); {each TWeb has a unique handler which must be cleared at Focus out}
end;

procedure TWeb.LoadFile(FileName:string);
var OldWidth,OldHeight:integer;
begin
  try
     Hourglass;
     OldHeight:=Height;
     OldWidth:=Width;
     Navigate(FileName);
     LoadedHTMLFile:=FileName;
     Width:=OldWidth; {due to a bug that sizes down HTML components on start}{Oct 15, 2001}
     Height:=OldHeight;
     TheDoc:=nil;
     if DocCmd<>nil then begin
        DocCmd._Release;
        DocCmd:=nil;
        end;
     Modified:=false;
     Extrinsic:=false;
     _Filter:=false;
  except
    on E:Exception do begin
       EError('Cannot load '+Filename,E);
       ElementWindow.ElWind.MustReloadComponents:=false; {otherwise wm_ActicateApp can loop}
       end;
    end;
end;

procedure TWeb.OnKeyDown(Sender:TObject);
{SMSpecific: most of the codes used here are specific to SuperMemo}
begin
  if TheWind=nil then
     exit;
  if TheWind.Event=nil then
     exit;
  if TheParent<>ElementWindow.ElWind then begin
     Modified:=true;
     exit;
     end;
  if not Editable then begin
     SendMessage(ElementWindow.ElWind.Handle,wm_KeyDown,TheWind.Event.KeyCode,0);
     exit;
     end;
  with TheWind.Event do begin
     if KeyCode=vk_Return then begin
        if (not CtrlKey) and (not ShiftKey) and (not AltKey) then
           if SelLength>0 then begin {SMSpecific}
              SetState(false);
              SendMessage(ElementWindow.ElWind.Handle,wm_KeyDown,TheWind.Event.KeyCode,0);
              exit;
              end;
        end;
     if KeyCode=vk_Delete then
        if CtrlKey and (not ShiftKey) and (not AltKey) then begin
           SetState(false);
           ElementWindow.ElWind.MIDeleteComponentClick(Sender);
           exit;
           end;
     if KeyCode=vk_Up then
        if CtrlKey and (not AltKey) and (not ShiftKey) then begin
           ElementWindow.ElWind.MIParentElementClick(Sender);
           exit;
           end;
     if KeyCode=vk_Left then
        if (not CtrlKey) and AltKey and (not ShiftKey) then begin
           PostMessage(ElementWindow.ElWind.Handle,cm_Back,0,0);
           exit;
           end;
     if KeyCode=vk_Right then
        if (not CtrlKey) and AltKey and (not ShiftKey) then begin
           PostMessage(ElementWindow.ElWind.Handle,cm_Forward,0,0);
           exit;
           end;
     if KeyCode in [vk_Control,
        vk_Left,vk_Up,vk_Down,vk_Right,vk_Escape] then {do not set modified to True on navigation keys}
           exit;
     if KeyCode=vk_F3 then begin
        if (not CtrlKey) and (not ShiftKey) and (not AltKey) then begin
           ElementWindow.ElWind.LockReadPointOnSearch:=true;
           ElementWindow.ElWind.MISearchTextClick(Sender);
           exit;
           end;
        if (CtrlKey) and (ShiftKey) and (not AltKey) then begin
           ElementWindow.ElWind.MIShowCitationClick(Sender);
           exit;
           end;
        end;
     if KeyCode=vk_F6 then begin
        if (not CtrlKey) and (not ShiftKey) and (not AltKey) then begin
           PostMessage(ElementWindow.ElWind.Handle,cm_FilterSource,0,0);
           exit;
           end;
        if (CtrlKey) and (ShiftKey) and (not AltKey) then begin
           ElementWindow.ElWind.MIViewSourceClick(Sender);
           exit;
           end;
        end;
     if KeyCode=ord('A') then {SMSpecific}
        if not TrimShortcuts then
           if CtrlKey and (not ShiftKey) and AltKey then begin
              Sm8Main.ActAddTaskExecute(Sender);
              exit;
              end;
     if KeyCode=ord('C') then {SMSpecific}{Ctrl+Shift+C does not work}
        if CtrlKey and ShiftKey and (not AltKey) then begin
           SM8Main.MICopyClick(Sender);
           exit;
           end;
     if KeyCode=ord('D') then begin {SMSpecific}{Ctrl+Shift+C does not work}
        if CtrlKey and (not ShiftKey) and (not AltKey) then begin
           ElementWindow.ElWind.DismissCurrentElement;
           exit;
           end;
        if CtrlKey and (not ShiftKey) and AltKey then begin
           ElementWindow.ElWind.MIDuplicateClick(Sender);
           exit;
           end;
        end;
     if KeyCode=ord('F') then
        if CtrlKey and (not ShiftKey) and (not AltKey) then begin
           ElementWindow.ElWind.LockReadPointOnSearch:=true;
           exit;
           end;
     if KeyCode=ord('G') then
        if CtrlKey and (not ShiftKey) and (not AltKey) then begin
           SM8Main.MIGoToClick(Sender);
           exit;
           end;
     if KeyCode=ord('L') then {SMSpecific}{Ctrl+L does not work}
        if CtrlKey and (not ShiftKey) and (not AltKey) then begin
           ElementWindow.ElWind.Learning(lsOutstanding);
           exit;
           end;
     if KeyCode=ord('M') then
        if CtrlKey and ShiftKey and (not AltKey) then begin
           ElementWindow.ElWind.ApplyTemplate(false{=do not replace});
           exit;
           end;
     if KeyCode=ord('N') then
        if not TrimShortcuts then begin
           if CtrlKey and (not ShiftKey) and (AltKey) then begin
              SM8Main.MIAddArticleClick(Sender);
              exit;
              end;
           if (not CtrlKey) and (not ShiftKey) and (AltKey) then begin
              ElementWindow.ElWind.NextRepetitionClick(Sender);
              exit;
              end;
           end;
     if KeyCode=ord('P') then begin
        if CtrlKey and ShiftKey and (not AltKey) then begin
           ElementWindow.ElWind.MIEditParamtersClick(Sender);
           exit;
           end;
        end;
     if KeyCode=ord('R') then {SMSpecific}{Ctrl+L does not work}
        if CtrlKey and ShiftKey and (not AltKey) then begin
           PostMessage(ElementWindow.ElWind.Handle,cm_ForceRepetition,0,ElementWindow.ElWind.TheElement);
           exit;
           end;
     if KeyCode=ord('Y') then {SMSpecific}{Ctrl+L does not work}
        if CtrlKey and (not ShiftKey) and (not AltKey) then begin
           SM8Main.MIMercyClick(Sender);
           exit;
           end;
     if KeyCode=221 {Ctrl+]} then
        ElementWindow.ElWind.ChangeFontSize(+1);
     if KeyCode=219 {Ctrl+[} then
        ElementWindow.ElWind.ChangeFontSize(-1);
  end;
  Modified:=true;
end;

function TWeb.GetTextRange:IHtmlTxtRange;
begin
  Result:=nil;
  try
     if TheDoc=nil then
        exit;
     while TheDoc.body=nil do begin
        WaitLoad(true);
        if TheDoc.body=nil then
           if QueryC('Wait for document loading?')<>id_Yes then
              exit;
        end;
     if (TheDoc.Selection.type_='Text')or(TheDoc.Selection.type_='None') then
        Result:=TheDoc.Selection.CreateRange as IHtmlTxtRange;
  except
    on E:Exception do EError('This type of selection cannot be processed',E);
    end;
end;

initialization
  FillChar(BrowserData,SizeOf(BrowserData),#0);
  OleInitialize(nil);
  New(PtrWGUID);
  New(PtrMGUID);
  New(PtrDGUID);
  PtrWGUID^:=CGID_WebBrowser;
  PtrMGUID^:=CGID_MSHTML;
  PtrDGuid:=PGUID(nil);
  GetBrowserData(BrowserData);
  IEStr:=IEVerStr;
  OnMessageCompNo:=0;

finalization
  Dispose(PtrWGUID);
  Dispose(PtrMGUID);
  Dispose(PtrDGUID);
  OleUninitialize;

end.
1.4.35-dev.2