[Example] Typing opening and closing quotes

Demos, code samples. Only questions related to the existing topics are allowed here.
Post Reply
Sergey Tkachenko
Site Admin
Posts: 17236
Joined: Sat Aug 27, 2005 10:28 am
Contact:

[Example] Typing opening and closing quotes

Post by Sergey Tkachenko »

The example automatically changes typed " and ' characters to the appropriate opening or closing quote.
The result depends on the keyboard language:
“ ” (example: English)
„ “ (example: German)
„ ” (example: Polish)
« » (example: Russian)
» « (Danish)
” ” (Finnish, Swedish)

I based this work on the following tables:
http://en.wikipedia.org/wiki/Internatio ... tion_marks
http://technet.microsoft.com/en-us/libr ... 25682.aspx
If this procedure chooses an incorrect (or not traditional) quotes for your language, please report to richviewgmailcom or to this topic.

How it works.
In OnKeyPress event, we check for ' and " characters. If one of these characters is typed, we find an appropriate replacement using GetQuote().
Of course, we can assign this replacement character to Key parameter of OnKeyPress, but this is not a good solution, because undo will simple delete this character. Instead, we assign this character to a global variable ReplaceChar, and perform a replacement in OnChange event. In this way, the first undo will revert a quote replacement, the second undo will delete a typed character.

The code below is for TRichView version 17.2 and older. For newer versions of TRichView, see http://www.trichview.com/forums/viewtop ... 706#p34706

Code: Select all

uses RVUni;

{$IFnDEF UNICODE}
const LDQuo = #147;
const RDQuo = #148;
const LAQuo = #171;
const RAQuo = #187;
const LSQuo = #145;
const RSQuo = #146;
const BDQuo = #132;
{$ELSE}
const LDQuo = #$201C;
const RDQuo = #$201D;
const LAQuo = #$00AB;
const RAQuo = #$00BB;
const LSQuo = #$2018;
const RSQuo = #$2019;
const BDQuo = #$201E;
{$ENDIF}

function GetQuote(rve: TCustomRichViewEdit; Ch: Char): Char;
var ItemNo1, ItemNo2, Offs1, Offs2: Integer;
    DelimBefore, DelimAfter: Boolean;
    {..........................................}
    function IsDelim(ItemNo, Index: Integer): Boolean;
    var
      s : String;
      {$IFnDEF UNICODE}
      CodePage: Cardinal;
      {$ENDIF}
    begin
      s := rve.GetItemText(ItemNo);
      if (Index>Length(s)) or (Index<0) then begin
        Result := True;
        exit;
      end;
      {$IFnDEF UNICODE}
      CodePage := rve.RVData.GetItemCodePage(ItemNo);
      {$ENDIF}
      Result :=
        {$IFDEF UNICODE}
        rve.RVData.IsDelimiterW(s[Index])
        {$ELSE}
        rve.RVData.IsDelimiterA(s[Index], CodePage)
      {$ENDIF}
    end;
    {..........................................}
    function DoGetQuote(Opening: Boolean): Char;
    var Lang: Cardinal;
    begin
      Lang := RVU_GetKeyboardLanguage;
      case Ch of
      '"':
        case Lang of
        // Albanian, Bulgarian, Czech, Estonian, Georgian,
        // German,
        // Icelandic, Lithuanian, Macedonian,
        // Slovak, Slovene, Sorbian
        $041c, $0402, $0405, $0425, $0437,
        $0c07, $0407, $1407, $1007,
        $040f, $0427, $042f,
        $041b, $0424, $082e:
          if Opening then
            Result := BDQuo
          else
            Result := LDQuo;
        // Croatian, Hungarian, Polish, Romanian,
        // Serbian,
        $041a, $101a, $040e, $0415, $0418,
        $1c1a, $181a, $301a, $2c1a, $281a, $241a, $0c1a, $081a:
          if Opening then
            Result := BDQuo
          else
            Result := RDQuo;
        // Armenian, Azerbaijani, Basque, Belarusian, Catalan,
        // French,
        // German-Swiss,
        // Greek, Italian, Latvian, Norwegian
        // Persian, Portuguese, Russian,
        // Spanish,
        // Ukrainian
        $042b, $082c, $042c, $042d, $0423, $0403,
        $080c, $0c0c, $040c, $140c, $180c, $100c,
        $0807,
        $0408, $0410, $0810, $0426, $0414, $0814,
        $0429, $0416, $0816, $0419,
        $2c0a, $200a, $400a, $340a, $240a, $140a, $1c0a, $300a, $440a, $100a,
        $480a, $080a, $4c0a, $180a, $3c0a, $280a, $500a, $0c0a, $040a, $540a, $380a,
        $0422:
          if Opening then
            Result := LAQuo
          else
            Result := RAQuo;
        // Danish
        $0406:
          if Opening then
            Result := RAQuo
          else
            Result := LAQuo;
        // Finnish, Swedish
        $040b, $081d, $041d:
          Result := RDQuo;
        else
          if Opening then
            Result := LDQuo
          else
            Result := RDQuo;
        end;
      '''':
        case Lang of
        // Finnish, Swedish
        $040b, $081d, $041d:
          Result := RSQuo;
        else
          if Opening then
            Result := LSQuo
          else
            Result := RSQuo
        end
      else
        Result := Ch;
      end;
    end;
    {..........................................}
begin
  // Returing the original character by default
  Result := Ch;
  rve := rve.TopLevelEditor;
  if rve.SelectionExists then
    exit;
  // Finding the position before (ItemNo1, Offs1) and after (ItemNo2, Offs2) the insertion point
  ItemNo1 := rve.CurItemNo;
  Offs1 := rve.OffsetInCurItem;
  if Offs1<=rve.GetOffsBeforeItem(ItemNo1) then begin
    ItemNo2 := ItemNo1;
    Offs2 := Offs1;
    dec(ItemNo1);
    if ItemNo1>=0 then
      if not rve.IsFromNewLine(ItemNo1) then
        Offs1 := rve.GetOffsAfterItem(ItemNo1)
      else begin
        ItemNo1 := -1;
        Offs1 := -1;
      end
    else
      Offs1 := -1;
    end
  else if Offs1>=rve.GetOffsAfterItem(ItemNo1) then begin
    ItemNo2 := ItemNo1+1;
    if ItemNo2<rve.ItemCount then
      if not rve.IsFromNewLine(ItemNo2) then
        Offs2 := rve.GetOffsBeforeItem(ItemNo2)
      else begin
        ItemNo2 := rve.ItemCount;
        Offs2 := -1;
      end
    else
      Offs2 := -1;
    end
  else begin
    ItemNo2 := ItemNo1;
    Offs2 := Offs1;
  end;
  // Is a delimiter (or nothing, or non-text) before the insertion point?
  if (ItemNo1<0) or (rve.GetItemStyle(ItemNo1)<0) or
    (rve.Style.TextStyles[rve.GetItemStyle(ItemNo1)].Charset=SYMBOL_CHARSET) then
    DelimBefore := True
  else
    DelimBefore := IsDelim(ItemNo1, Offs1-1);
  // Is a delimiter (or nothing, or non-text) after the insertion point?
  if (ItemNo2>=rve.ItemCount) or (rve.GetItemStyle(ItemNo2)<0) or
    (rve.Style.TextStyles[rve.GetItemStyle(ItemNo2)].Charset=SYMBOL_CHARSET) then
    DelimAfter := True
  else
    DelimAfter := IsDelim(ItemNo2, Offs2);
  // Choosing a quote
  if DelimBefore and not DelimAfter then
    Result := DoGetQuote(True)
  else if not DelimBefore and DelimAfter then
    Result := DoGetQuote(False);
end;

// this variable can be moved to a form
const ReplaceChar: Char = #0; 

procedure TForm3.RichViewEdit1KeyPress(Sender: TObject; var Key: Char);
begin
  ReplaceChar := #0;
  if (Key='"') or (Key='''') then begin
    ReplaceChar := GetQuote(RichViewEdit1, Key);
    if ReplaceChar=Key then
      ReplaceChar := #0;
  end;
end;

procedure TForm3.RichViewEdit1Change(Sender: TObject);
var rve: TCustomRichViewEdit;
    ItemNo, Offs: Integer;
    Ch: Char;
begin
  if ReplaceChar<>#0 then begin
    Ch := ReplaceChar;
    ReplaceChar := #0;
    rve := RichViewEdit1.TopLevelEditor;
    ItemNo := rve.CurItemNo;
    Offs := rve.OffsetInCurItem;
    rve.SetSelectionBounds(ItemNo, Offs-1, ItemNo, Offs);
    rve.InsertText(Ch);
  end;
end;
DavidRM
Posts: 91
Joined: Mon Aug 29, 2005 5:18 pm
Location: Tulsa, OK
Contact:

Post by DavidRM »

I was unable to get this code to work as expected. I essentially just copied-and-pasted it in. The results are very inconsistent.

My own implementation boils down to this logic:

Code: Select all

when user types a " or '
  if this is the beginning of a new paragraph, or if the previous character is whitespace
    replace with an open/left quote
  else
    replace with a close/right quote
From my experimenting with MS Word 2003, they use pretty much the same simple logic.

-David
Sergey Tkachenko
Site Admin
Posts: 17236
Joined: Sat Aug 27, 2005 10:28 am
Contact:

Post by Sergey Tkachenko »

In this case, you can change the code fragment

Code: Select all

 if DelimBefore and not DelimAfter then 
    Result := DoGetQuote(True) 
  else if not DelimBefore and DelimAfter then 
    Result := DoGetQuote(False); 
to

Code: Select all

Result := DoGetQuote(DelimBefore) 
Sergey Tkachenko
Site Admin
Posts: 17236
Joined: Sat Aug 27, 2005 10:28 am
Contact:

Re: [Example] Typing opening and closing quotes

Post by Sergey Tkachenko »

This code is for TRichView 17.3 and newer (till version 18, see below).

Code: Select all

uses RVTypes, CRVData, RVUni;

function GetQuote(rve: TCustomRichViewEdit; Ch: TRVUnicodeChar): TRVUnicodeChar;
var
  ItemNo1, Offs1: Integer;
  DelimBefore: Boolean;
  Lang: Cardinal;
  OpeningQuote: TRVUnicodeChar;
    {..........................................}
    // Returns True if the Index-th character in the ItemNo-th item is a delimiter,
    // but not an opening quote
    function IsDelim(ItemNo, Index: Integer): Boolean;
    var
      s : TRVUnicodeString;
    begin
      s := rve.GetItemText(ItemNo);
      if (Index>Length(s)) or (Index<0) then
      begin
        Result := True;
        exit;
      end;
      Result := (s[Index] <> OpeningQuote) and rve.RVData.IsDelimiterW(s[Index])
    end;
    {..........................................}
    function DoGetQuote(Opening: Boolean): TRVUnicodeChar;
    begin
      case Ch of
      '"':
        case Lang of
        // Albanian, Bulgarian, Czech, Estonian, Georgian,
        // German,
        // Icelandic, Lithuanian, Macedonian,
        // Slovak, Slovene, Sorbian
        $041c, $0402, $0405, $0425, $0437,
        $0c07, $0407, $1407, $1007,
        $040f, $0427, $042f,
        $041b, $0424, $082e:
          if Opening then
            Result := UNI_RIGHT_DOUBLE_LOW_QUOTE
          else
            Result := UNI_LEFT_DOUBLE_QUOTE;
        // Croatian, Hungarian, Polish, Romanian,
        // Serbian,
        $041a, $101a, $040e, $0415, $0418,
        $1c1a, $181a, $301a, $2c1a, $281a, $241a, $0c1a, $081a:
          if Opening then
            Result := UNI_RIGHT_DOUBLE_LOW_QUOTE
          else
            Result := UNI_RIGHT_DOUBLE_QUOTE;
        // Armenian, Azerbaijani, Basque, Belarusian, Catalan,
        // French,
        // German-Swiss,
        // Greek, Italian, Latvian, Norwegian
        // Persian, Portuguese, Russian,
        // Spanish,
        // Ukrainian
        $042b, $082c, $042c, $042d, $0423, $0403,
        $080c, $0c0c, $040c, $140c, $180c, $100c,
        $0807,
        $0408, $0410, $0810, $0426, $0414, $0814,
        $0429, $0416, $0816, $0419,
        $2c0a, $200a, $400a, $340a, $240a, $140a, $1c0a, $300a, $440a, $100a,
        $480a, $080a, $4c0a, $180a, $3c0a, $280a, $500a, $0c0a, $040a, $540a, $380a,
        $0422:
          if Opening then
            Result := UNI_LEFT_DOUBLE_ANGLE_QUOTE
          else
            Result := UNI_RIGHT_DOUBLE_ANGLE_QUOTE;
        // Danish
        $0406:
          if Opening then
            Result := UNI_RIGHT_DOUBLE_ANGLE_QUOTE
          else
            Result := UNI_LEFT_DOUBLE_ANGLE_QUOTE;
        // Finnish, Swedish
        $040b, $081d, $041d:
          Result := UNI_RIGHT_DOUBLE_QUOTE;
        else
          if Opening then
            Result := UNI_LEFT_DOUBLE_QUOTE
          else
            Result := UNI_RIGHT_DOUBLE_QUOTE;
        end;
      '''':
        case Lang of
        // Finnish, Swedish
        $040b, $081d, $041d:
          Result := UNI_RIGHT_SINGLE_QUOTE;
        else
          if Opening then
            Result := UNI_LEFT_SINGLE_QUOTE
          else
            Result := UNI_RIGHT_SINGLE_QUOTE
        end
      else
        Result := Ch;
      end;
    end;
    {..........................................}
begin
  // Returing the original character by default
  Result := Ch;
  rve := rve.TopLevelEditor;
  if rve.SelectionExists then
    exit;
  // Finding the position before (ItemNo1, Offs1) and after (ItemNo2, Offs2) the insertion point
  ItemNo1 := rve.CurItemNo;
  Offs1 := rve.OffsetInCurItem;
  if Offs1<=rve.GetOffsBeforeItem(ItemNo1) then
  begin
    dec(ItemNo1);
    if ItemNo1>=0 then
      if not rve.IsFromNewLine(ItemNo1) then
        Offs1 := rve.GetOffsAfterItem(ItemNo1)
      else
      begin
        ItemNo1 := -1;
        Offs1 := -1;
      end
    else
      Offs1 := -1;
  end;
  // Getting the keyboard language and opening quote
  Lang := RVU_GetKeyboardLanguage;
  OpeningQuote := DoGetQuote(True);
  // Is a delimiter (or nothing, or non-text) before the insertion point?
  if (ItemNo1<0) or (rve.GetItemStyle(ItemNo1)<0) or
    rve.Style.TextStyles[rve.GetItemStyle(ItemNo1)].IsSymbolCharset then
    DelimBefore := True
  else
    DelimBefore := IsDelim(ItemNo1, Offs1-1);
  // Choosing a quote
  Result := DoGetQuote(DelimBefore);
end;

// this variable can be moved to a form
const ReplaceChar: TRVUnicodeChar = #0;

procedure TForm3.RichViewEdit1KeyPress(Sender: TObject; var Key: Char);
begin
  ReplaceChar := #0;
  if (Key='"') or (Key='''') then
  begin
    ReplaceChar := GetQuote(RichViewEdit1, TRVUnicodeChar(Key));
    if ReplaceChar=TRVUnicodeChar(Key) then
      ReplaceChar := #0;
  end;
end;

procedure TForm3.RichViewEdit1Change(Sender: TObject);
var rve: TCustomRichViewEdit;
    ItemNo, Offs: Integer;
    Ch: TRVUnicodeChar;
begin
  if ReplaceChar<>#0 then
  begin
    Ch := ReplaceChar;
    ReplaceChar := #0;
    rve := RichViewEdit1.TopLevelEditor;
    ItemNo := rve.CurItemNo;
    Offs := rve.OffsetInCurItem;
    rve.SetSelectionBounds(ItemNo, Offs-1, ItemNo, Offs);
    rve.InsertTextW(Ch);
  end;
end;
In this update, all text is processed as Unicode.
Also, this update fixes the problem with a separate quote (noticed by DavidRM) - it must be an opening quote.
Also, this update fixes the problem with a quote after an opening quote - it must be a closing quote.
rhett.price
Posts: 44
Joined: Tue Jul 10, 2007 6:38 pm

Re: [Example] Typing opening and closing quotes

Post by rhett.price »

In version 18 - this code breaks on reference to:
RVU_GetKeyboardLanguage

Could we get updated code that works in v18?

Thanks!

Rhett Price
IndySoft
rhett.price
Posts: 44
Joined: Tue Jul 10, 2007 6:38 pm

Re: [Example] Typing opening and closing quotes

Post by rhett.price »

Ah, I see. Just change function to RVGetKeyboardLanguage and add RVKeyboardFuncs to the uses and it looks like we're good now.

Thanks,

Rhett
Sergey Tkachenko
Site Admin
Posts: 17236
Joined: Sat Aug 27, 2005 10:28 am
Contact:

Re: [Example] Typing opening and closing quotes

Post by Sergey Tkachenko »

Thank you!
Here is the code changed for compatibility with TRichView 18 and newer.

Code: Select all

uses RVTypes, CRVData, RVUni, RVKeyboardFuncs;

function GetQuote(rve: TCustomRichViewEdit; Ch: TRVUnicodeChar): TRVUnicodeChar;
var
  ItemNo1, Offs1: Integer;
  DelimBefore: Boolean;
  Lang: Cardinal;
  OpeningQuote: TRVUnicodeChar;
    {..........................................}
    // Returns True if the Index-th character in the ItemNo-th item is a delimiter,
    // but not an opening quote
    function IsDelim(ItemNo, Index: Integer): Boolean;
    var
      s : TRVUnicodeString;
    begin
      s := rve.GetItemText(ItemNo);
      if (Index>Length(s)) or (Index<0) then
      begin
        Result := True;
        exit;
      end;
      Result := (s[Index] <> OpeningQuote) and rve.RVData.IsDelimiterW(s[Index])
    end;
    {..........................................}
    function DoGetQuote(Opening: Boolean): TRVUnicodeChar;
    begin
      case Ch of
      '"':
        case Lang of
        // Albanian, Bulgarian, Czech, Estonian, Georgian,
        // German,
        // Icelandic, Lithuanian, Macedonian,
        // Slovak, Slovene, Sorbian
        $041c, $0402, $0405, $0425, $0437,
        $0c07, $0407, $1407, $1007,
        $040f, $0427, $042f,
        $041b, $0424, $082e:
          if Opening then
            Result := UNI_RIGHT_DOUBLE_LOW_QUOTE
          else
            Result := UNI_LEFT_DOUBLE_QUOTE;
        // Croatian, Hungarian, Polish, Romanian,
        // Serbian,
        $041a, $101a, $040e, $0415, $0418,
        $1c1a, $181a, $301a, $2c1a, $281a, $241a, $0c1a, $081a:
          if Opening then
            Result := UNI_RIGHT_DOUBLE_LOW_QUOTE
          else
            Result := UNI_RIGHT_DOUBLE_QUOTE;
        // Armenian, Azerbaijani, Basque, Belarusian, Catalan,
        // French,
        // German-Swiss,
        // Greek, Italian, Latvian, Norwegian
        // Persian, Portuguese, Russian,
        // Spanish,
        // Ukrainian
        $042b, $082c, $042c, $042d, $0423, $0403,
        $080c, $0c0c, $040c, $140c, $180c, $100c,
        $0807,
        $0408, $0410, $0810, $0426, $0414, $0814,
        $0429, $0416, $0816, $0419,
        $2c0a, $200a, $400a, $340a, $240a, $140a, $1c0a, $300a, $440a, $100a,
        $480a, $080a, $4c0a, $180a, $3c0a, $280a, $500a, $0c0a, $040a, $540a, $380a,
        $0422:
          if Opening then
            Result := UNI_LEFT_DOUBLE_ANGLE_QUOTE
          else
            Result := UNI_RIGHT_DOUBLE_ANGLE_QUOTE;
        // Danish
        $0406:
          if Opening then
            Result := UNI_RIGHT_DOUBLE_ANGLE_QUOTE
          else
            Result := UNI_LEFT_DOUBLE_ANGLE_QUOTE;
        // Finnish, Swedish
        $040b, $081d, $041d:
          Result := UNI_RIGHT_DOUBLE_QUOTE;
        else
          if Opening then
            Result := UNI_LEFT_DOUBLE_QUOTE
          else
            Result := UNI_RIGHT_DOUBLE_QUOTE;
        end;
      '''':
        case Lang of
        // Finnish, Swedish
        $040b, $081d, $041d:
          Result := UNI_RIGHT_SINGLE_QUOTE;
        else
          if Opening then
            Result := UNI_LEFT_SINGLE_QUOTE
          else
            Result := UNI_RIGHT_SINGLE_QUOTE
        end
      else
        Result := Ch;
      end;
    end;
    {..........................................}
begin
  // Returing the original character by default
  Result := Ch;
  rve := rve.TopLevelEditor;
  if rve.SelectionExists then
    exit;
  // Finding the position before (ItemNo1, Offs1) and after (ItemNo2, Offs2) the insertion point
  ItemNo1 := rve.CurItemNo;
  Offs1 := rve.OffsetInCurItem;
  if Offs1<=rve.GetOffsBeforeItem(ItemNo1) then
  begin
    dec(ItemNo1);
    if ItemNo1>=0 then
      if not rve.IsFromNewLine(ItemNo1) then
        Offs1 := rve.GetOffsAfterItem(ItemNo1)
      else
      begin
        ItemNo1 := -1;
        Offs1 := -1;
      end
    else
      Offs1 := -1;
  end;
  // Getting the keyboard language and opening quote
  Lang := RVGetKeyboardLanguage;
  OpeningQuote := DoGetQuote(True);
  // Is a delimiter (or nothing, or non-text) before the insertion point?
  if (ItemNo1<0) or (rve.GetItemStyle(ItemNo1)<0) or
    rve.Style.TextStyles[rve.GetItemStyle(ItemNo1)].IsSymbolCharset then
    DelimBefore := True
  else
    DelimBefore := IsDelim(ItemNo1, Offs1-1);
  // Choosing a quote
  Result := DoGetQuote(DelimBefore);
end;

// this variable can be moved to a form
const ReplaceChar: TRVUnicodeChar = #0;

procedure TForm3.RichViewEdit1KeyPress(Sender: TObject; var Key: Char);
begin
  ReplaceChar := #0;
  if (Key='"') or (Key='''') then
  begin
    ReplaceChar := GetQuote(RichViewEdit1, TRVUnicodeChar(Key));
    if ReplaceChar=TRVUnicodeChar(Key) then
      ReplaceChar := #0;
  end;
end;

procedure TForm3.RichViewEdit1Change(Sender: TObject);
var rve: TCustomRichViewEdit;
    ItemNo, Offs: Integer;
    Ch: TRVUnicodeChar;
begin
  if ReplaceChar<>#0 then
  begin
    Ch := ReplaceChar;
    ReplaceChar := #0;
    rve := RichViewEdit1.TopLevelEditor;
    ItemNo := rve.CurItemNo;
    Offs := rve.OffsetInCurItem;
    rve.SetSelectionBounds(ItemNo, Offs-1, ItemNo, Offs);
    rve.InsertTextW(Ch);
  end;
end;
In this update, all text is processed as Unicode.
Also, this update fixes the problem with a separate quote (noticed by DavidRM) - it must be an opening quote.
Also, this update fixes the problem with a quote after an opening quote - it must be a closing quote.
Post Reply