hedgewars/uChat.pas
branchqmlfrontend
changeset 10886 99273b7afbff
parent 10868 acb03a9712c3
child 10919 8aed2bfc43c5
--- a/hedgewars/uChat.pas	Mon Feb 16 22:33:15 2015 +0300
+++ b/hedgewars/uChat.pas	Thu Apr 02 21:09:56 2015 +0300
@@ -28,13 +28,15 @@
 procedure CleanupInput;
 procedure AddChatString(s: shortstring);
 procedure DrawChat;
-procedure KeyPressChat(Key, Sym: Longword);
+procedure KeyPressChat(Key, Sym: Longword; Modifier: Word);
 procedure SendHogSpeech(s: shortstring);
+procedure CopyToClipboard(var newContent: shortstring);
 
 implementation
 uses SDLh, uInputHandler, uTypes, uVariables, uCommands, uUtils, uTextures, uRender, uIO, uScript, uRenderUtils;
 
 const MaxStrIndex = 27;
+      MaxInputStrLen = 240;
 
 type TChatLine = record
     Tex: PTexture;
@@ -45,22 +47,33 @@
     end;
     TChatCmd = (ccQuit, ccPause, ccFinish, ccShowHistory, ccFullScreen);
 
+type TInputStrL = array[0..260] of byte;
+
 var Strs: array[0 .. MaxStrIndex] of TChatLine;
     MStrs: array[0 .. MaxStrIndex] of shortstring;
     LocalStrs: array[0 .. MaxStrIndex] of shortstring;
+    LocalStrsL: array[0 .. MaxStrIndex] of TInputStrL;
     missedCount: LongWord;
     lastStr: LongWord;
     localLastStr: LongInt;
     history: LongInt;
     visibleCount: LongWord;
     InputStr: TChatLine;
-    InputStrL: array[0..260] of char; // for full str + 4-byte utf-8 char
+    InputStrL: TInputStrL; // for full str + 4-byte utf-8 char
     ChatReady: boolean;
     showAll: boolean;
     liveLua: boolean;
     ChatHidden: boolean;
+    firstDraw: boolean;
+    InputLinePrefix: TChatLine;
+    // cursor
+    cursorPos, cursorX, selectedPos, selectionDx: LongInt;
+    LastKeyPressTick: LongWord;
+
 
 const
+    InputStrLNoPred: byte = 255;
+
     colors: array[#0..#6] of TSDL_Color = (
             (r:$FF; g:$FF; b:$FF; a:$FF), // unused, feel free to take it for anything
             (r:$FF; g:$FF; b:$FF; a:$FF), // chat message [White]
@@ -85,6 +98,59 @@
 const Padding  = 2;
       ClHeight = 2 * Padding + 16; // font height
 
+function charIsForHogSpeech(c: char): boolean;
+begin
+exit((c = '"') or (c = '''') or (c = '-'));
+end;
+
+procedure ResetSelection();
+begin
+    selectedPos:= -1;
+end;
+
+procedure UpdateCursorCoords();
+var font: THWFont;
+    str : shortstring;
+    coff, soff: LongInt;
+begin
+    if cursorPos = selectedPos then
+        ResetSelection();
+
+    // calculate cursor offset
+
+    str:= InputStr.s;
+    font:= CheckCJKFont(ansistring(str), fnt16);
+
+    // get only substring before cursor to determine length
+    // SetLength(str, cursorPos); // makes pas2c unhappy
+    str[0]:= char(cursorPos);
+    // get render size of text
+    TTF_SizeUTF8(Fontz[font].Handle, Str2PChar(str), @coff, nil);
+
+    cursorX:= 2 + coff;
+
+    // calculate selection width on screen
+    if selectedPos >= 0 then
+        begin
+        if selectedPos > cursorPos then
+            str:= InputStr.s;
+        // SetLength(str, selectedPos); // makes pas2c unhappy
+        str[0]:= char(selectedPos);
+        TTF_SizeUTF8(Fontz[font].Handle, Str2PChar(str), @soff, nil);
+        selectionDx:= soff - coff;
+        end
+    else
+        selectionDx:= 0;
+end;
+
+
+procedure ResetCursor();
+begin
+    ResetSelection();
+    cursorPos:= 0;
+    UpdateCursorCoords();
+end;
+
 procedure RenderChatLineTex(var cl: TChatLine; var str: shortstring);
 var strSurface,
     resSurface: PSDL_Surface;
@@ -139,12 +205,19 @@
     begin
     cl.s:= str;
     color:= colors[#6];
-    str:= UserNick + '> ' + str + '_'
+    str:= str + ' ';
     end
 else
     begin
-    color:= colors[str[1]];
-    delete(str, 1, 1);
+    if str[1] <= High(colors) then
+        begin
+        color:= colors[str[1]];
+        delete(str, 1, 1);
+        end
+    // fallback if invalid color
+    else
+        color:= colors[Low(colors)];
+
     cl.s:= str;
     end;
 
@@ -190,8 +263,28 @@
 inc(visibleCount)
 end;
 
+procedure CheckPasteBuffer(); forward;
+
+procedure UpdateInputLinePrefix();
+begin
+if liveLua then
+    begin
+    InputLinePrefix.color:= colors[#1];
+    InputLinePrefix.s:= '[Lua] >';
+    end
+else
+    begin
+    InputLinePrefix.color:= colors[#6];
+    InputLinePrefix.s:= UserNick + '>';
+    end;
+
+FreeAndNilTexture(InputLinePrefix.Tex);
+end;
+
 procedure DrawChat;
 var i, t, left, top, cnt: LongInt;
+    selRect: TSDL_Rect;
+    c: char;
 begin
 ChatReady:= true; // maybe move to somewhere else?
 
@@ -204,8 +297,78 @@
 
 // draw chat input line first and under all other lines
 if (GameState = gsChat) and (InputStr.Tex <> nil) then
+    begin
+    CheckPasteBuffer();
+
+    if InputLinePrefix.Tex = nil then
+        RenderChatLineTex(InputLinePrefix, InputLinePrefix.s);
+
+    DrawTexture(left, top, InputLinePrefix.Tex);
+    inc(left, InputLinePrefix.Width);
     DrawTexture(left, top, InputStr.Tex);
 
+    if firstDraw then
+        begin
+        UpdateCursorCoords();
+        firstDraw:= false;
+        end;
+
+    if selectedPos < 0 then
+        begin
+        // draw cursor
+        if ((RealTicks - LastKeyPressTick) and 512) < 256 then
+            DrawLineOnScreen(left + cursorX, top + 2, left + cursorX, top + ClHeight - 2, 2.0, $00, $FF, $FF, $FF);
+        end
+    else // draw selection
+        begin
+        selRect.y:= top + 2;
+        selRect.h:= clHeight - 4;
+        if selectionDx < 0 then
+            begin
+            selRect.x:= left + cursorX + selectionDx;
+            selRect.w:= -selectionDx;
+            end
+        else
+            begin
+            selRect.x:= left + cursorX;
+            selRect.w:= selectionDx;
+            end;
+
+        DrawRect(selRect, $FF, $FF, $FF, $40, true);
+        end;
+
+    dec(left, InputLinePrefix.Width);
+
+
+    if (Length(InputStr.s) > 0) and ((CursorPos = 1) or (CursorPos = 2)) then
+        begin
+        c:= InputStr.s[1];
+        if charIsForHogSpeech(c) then
+            begin
+            SpeechHogNumber:= 0;
+            if Length(InputStr.s) > 1 then
+                begin
+                c:= InputStr.s[2];
+                if (c > '0') and (c < '9') then
+                    SpeechHogNumber:= byte(c) - 48;
+                end;
+            // default to current hedgehog (if own) or first hedgehog
+            if SpeechHogNumber = 0 then
+                begin
+                if not CurrentTeam^.ExtDriven then
+                    SpeechHogNumber:= CurrentTeam^.CurrHedgehog + 1
+                else
+                    SpeechHogNumber:= 1;
+                end;
+            end;
+        end
+    else
+        SpeechHogNumber:= -1;
+    end
+else
+    SpeechHogNumber:= -1;
+
+// draw chat lines
 if ((not ChatHidden) or showAll) and (UIDisplay <> uiNone) then
     begin
     if MissedCount <> 0 then // there are chat strings we missed, so print them now
@@ -264,6 +427,7 @@
     // put in input history
     localLastStr:= (localLastStr + 1) mod MaxStrIndex;
     LocalStrs[localLastStr]:= s;
+    LocalStrsL[localLastStr]:= InputStrL;
     end;
 
 t:= LocalTeam;
@@ -365,6 +529,7 @@
                 AddFileLog('[Lua] chat input string parsing disabled');
                 AddChatString(#3 + 'Lua parsing: OFF');
                 end;
+            UpdateInputLinePrefix();
             end;
         exit
         end;
@@ -410,26 +575,308 @@
     ResetKbd;
 end;
 
-procedure KeyPressChat(Key, Sym: Longword);
+procedure DelBytesFromInputStrBack(endIdx: integer; count: byte);
+var i, startIdx: integer;
+begin
+    // nothing to do if count is 0
+    if count = 0 then
+        exit;
+
+    // first byte to delete
+    startIdx:= endIdx - (count - 1);
+
+    // delete bytes from string
+    Delete(InputStr.s, startIdx, count);
+
+    // wipe utf8 info for deleted char
+    InputStrL[endIdx]:= InputStrLNoPred;
+
+    // shift utf8 char info to reflect new string
+    for i:= endIdx + 1 to Length(InputStr.s) + count do
+        begin
+        if InputStrL[i] <> InputStrLNoPred then
+            begin
+            InputStrL[i-count]:= InputStrL[i] - count;
+            InputStrL[i]:= InputStrLNoPred;
+            end;
+        end;
+
+    SetLine(InputStr, InputStr.s, true);
+end;
+
+// returns count of removed bytes
+function DelCharFromInputStr(idx: integer): integer;
+var btw: byte;
+begin
+    // note: idx is always at last byte of utf8 chars. cuz relevant for InputStrL
+
+    if (Length(InputStr.s) < 1) or (idx < 1) or (idx > Length(InputStr.s)) then
+        exit(0);
+
+    btw:= byte(idx) - InputStrL[idx];
+
+    DelCharFromInputStr:= btw;
+
+    DelBytesFromInputStrBack(idx, btw);
+end;
+
+// unchecked
+procedure DoCursorStepForward();
+begin
+    // go to end of next utf8-char
+    repeat
+        inc(cursorPos);
+    until InputStrL[cursorPos] <> InputStrLNoPred;
+end;
+
+procedure DeleteSelected();
+begin
+    if (selectedPos >= 0) and (cursorPos <> selectedPos) then
+        begin
+        DelBytesFromInputStrBack(max(cursorPos, selectedPos), abs(selectedPos-cursorPos));
+        cursorPos:= min(cursorPos, selectedPos);
+        ResetSelection();
+        end;
+    UpdateCursorCoords();
+end;
+
+procedure HandleSelection(enabled: boolean);
+begin
+if enabled then
+    begin
+    if selectedPos < 0 then
+        selectedPos:= cursorPos;
+    end
+else
+    ResetSelection();
+end;
+
+type TCharSkip = ( none, wspace, numalpha, special );
+
+function GetInputCharSkipClass(index: LongInt): TCharSkip;
+var  c: char;
+begin
+    // multi-byte chars counts as letter
+    if (index > 1) and (InputStrL[index] <> index - 1) then
+        exit(numalpha);
+
+    c:= InputStr.s[index];
+
+    // non-ascii counts as letter
+    if c > #127 then
+        exit(numalpha);
+
+    // low-ascii whitespaces and DEL
+    if (c < #33) or (c = #127) then
+        exit(wspace);
+
+    // low-ascii special chars
+    if c < #48 then
+        exit(special);
+
+    // digits
+    if c < #58 then
+        exit(numalpha);
+
+    // make c upper-case
+    if c > #96 then
+        c:= char(byte(c) - 32);
+
+    // letters
+    if (c > #64) and (c < #90) then
+        exit(numalpha);
+
+    // remaining ascii are special chars
+    exit(special);
+end;
+
+// skip from word to word, similar to Qt
+procedure SkipInputChars(skip: TCharSkip; backwards: boolean);
+begin
+if backwards then
+    begin
+    // skip trailing whitespace, similar to Qt
+    while (skip = wspace) and (cursorPos > 0) do
+        begin
+        skip:= GetInputCharSkipClass(cursorPos);
+        if skip = wspace then
+            cursorPos:= InputStrL[cursorPos];
+        end;
+    // skip same-type chars
+    while (cursorPos > 0) and (GetInputCharSkipClass(cursorPos) = skip) do
+        cursorPos:= InputStrL[cursorPos];
+    end
+else
+    begin
+    // skip same-type chars
+    while cursorPos < Length(InputStr.s) do
+        begin
+        DoCursorStepForward();
+        if (GetInputCharSkipClass(cursorPos) <> skip) then
+            begin
+            // go back 1 char
+            cursorPos:= InputStrL[cursorPos];
+            break;
+            end;
+        end;
+    // skip trailing whitespace, similar to Qt
+    while cursorPos < Length(InputStr.s) do
+        begin
+        DoCursorStepForward();
+        if (GetInputCharSkipClass(cursorPos) <> wspace) then
+            begin
+            // go back 1 char
+            cursorPos:= InputStrL[cursorPos];
+            break;
+            end;
+        end;
+    end;
+end;
+
+procedure CopyToClipboard(var newContent: shortstring);
+begin
+    SendIPC(_S'y' + copy(newContent, 1, 253) + #0);
+end;
+
+procedure CopySelectionToClipboard();
+var selection: shortstring;
+begin
+    if selectedPos >= 0 then
+        begin
+        selection:= copy(InputStr.s, min(CursorPos, selectedPos) + 1, abs(CursorPos - selectedPos));
+        CopyToClipboard(selection);
+        end;
+end;
+
+// TODO: honor utf8, don't break utf8 chars when shifting chars beyond limit
+procedure InsertIntoInputStr(s: shortstring);
+var i, l, il, lastc: integer;
+begin
+    // safe length for string
+    l:= min(MaxInputStrLen-cursorPos, Length(s));
+    s:= copy(s,1,l);
+
+    // if we insert rather than append, shift info in InputStrL accordingly
+    if cursorPos < Length(InputStr.s) then
+        begin
+        for i:= Length(InputStr.s) downto cursorPos + 1 do
+            begin
+            if InputStrL[i] <> InputStrLNoPred then
+                begin
+                il:= i + l;
+                // only shift if not overflowing
+                if il <= MaxInputStrLen then
+                    InputStrL[il]:= InputStrL[i] + l;
+                InputStrL[i]:= InputStrLNoPred;
+                end;
+            end;
+        end;
+
+    InputStrL[cursorPos + l]:= cursorPos;
+    // insert string truncated to safe length
+    Insert(s, InputStr.s, cursorPos + 1);
+    if Length(InputStr.s) > MaxInputStrLen then
+        InputStr.s[0]:= char(MaxInputStrLen);
+
+    SetLine(InputStr, InputStr.s, true);
+
+    // move cursor to end of inserted string
+    lastc:= MaxInputStrLen;
+    cursorPos:= min(lastc, cursorPos + l);
+    UpdateCursorCoords();
+end;
+
+procedure PasteFromClipboard();
+begin
+    SendIPC(_S'Y');
+end;
+
+procedure CheckPasteBuffer();
+begin
+    if Length(ChatPasteBuffer) > 0 then
+        begin
+        InsertIntoInputStr(ChatPasteBuffer);
+        ChatPasteBuffer:= '';
+        end;
+end;
+
+procedure KeyPressChat(Key, Sym: Longword; Modifier: Word);
 const firstByteMark: array[0..3] of byte = (0, $C0, $E0, $F0);
 var i, btw, index: integer;
     utf8: shortstring;
-    action: boolean;
+    action, selMode, ctrl: boolean;
+    skip: TCharSkip;
 begin
+    LastKeyPressTick:= RealTicks;
     action:= true;
+
+    CheckPasteBuffer();
+
+    selMode:= (modifier and (KMOD_LSHIFT or KMOD_RSHIFT)) <> 0;
+    ctrl:= (modifier and (KMOD_LCTRL or KMOD_RCTRL)) <> 0;
+    skip:= none;
+
     case Sym of
         SDLK_BACKSPACE:
             begin
-            if Length(InputStr.s) > 0 then
+            if selectedPos < 0 then
                 begin
-                InputStr.s[0]:= InputStrL[byte(InputStr.s[0])];
-                SetLine(InputStr, InputStr.s, true)
+                if ctrl then
+                    skip:= GetInputCharSkipClass(cursorPos);
+
+                // remove char before cursor
+                dec(cursorPos, DelCharFromInputStr(cursorPos));
+
+                // delete more if ctrl is held
+                if ctrl and (selectedPos < 0) then
+                    begin
+                    HandleSelection(true);
+                    SkipInputChars(skip, true);
+                    DeleteSelected();
+                    end
+                else
+                    UpdateCursorCoords();
+
                 end
+            else
+                DeleteSelected();
+            end;
+        SDLK_DELETE:
+            begin
+            if selectedPos < 0 then
+                begin
+                // remove char after cursor
+                if cursorPos < Length(InputStr.s) then
+                    begin
+                    DoCursorStepForward();
+                    if ctrl then
+                        skip:= GetInputCharSkipClass(cursorPos);
+
+                    // delete char
+                    dec(cursorPos, DelCharFromInputStr(cursorPos));
+
+                    // delete more if ctrl is held
+                    if ctrl and (cursorPos < Length(InputStr.s)) then
+                        begin
+                        HandleSelection(true);
+                        SkipInputChars(skip, false);
+                        DeleteSelected();
+                        end;
+                    end
+                else
+                    UpdateCursorCoords();
+                end
+            else
+                DeleteSelected();
             end;
         SDLK_ESCAPE:
             begin
             if Length(InputStr.s) > 0 then
-                SetLine(InputStr, '', true)
+                begin
+                SetLine(InputStr, '', true);
+                FillChar(InputStrL, sizeof(InputStrL), InputStrLNoPred);
+                ResetCursor();
+                end
             else CleanupInput
             end;
         SDLK_RETURN, SDLK_KP_ENTER:
@@ -437,7 +884,9 @@
             if Length(InputStr.s) > 0 then
                 begin
                 AcceptChatString(InputStr.s);
-                SetLine(InputStr, '', false)
+                SetLine(InputStr, '', false);
+                FillChar(InputStrL, sizeof(InputStrL), InputStrLNoPred);
+                ResetCursor();
                 end;
             CleanupInput
             end;
@@ -447,20 +896,150 @@
             if (Sym = SDLK_DOWN) and (history > 0) then dec(history);
             index:= localLastStr - history + 1;
             if (index > localLastStr) then
-                 SetLine(InputStr, '', true)
-            else SetLine(InputStr, LocalStrs[index], true)
+                begin
+                SetLine(InputStr, '', true);
+                FillChar(InputStrL, sizeof(InputStrL), InputStrLNoPred);
+                end
+            else
+                begin
+                SetLine(InputStr, LocalStrs[index], true);
+                InputStrL:= LocalStrsL[index];
+                end;
+            cursorPos:= Length(InputStr.s);
+            ResetSelection();
+            UpdateCursorCoords();
+            end;
+        SDLK_HOME:
+            begin
+            if cursorPos > 0 then
+                begin
+                HandleSelection(selMode);
+                cursorPos:= 0;
+                end
+            else if (not selMode) then
+                ResetSelection();
+
+            UpdateCursorCoords();
+            end;
+        SDLK_END:
+            begin
+            i:= Length(InputStr.s);
+            if cursorPos < i then
+                begin
+                HandleSelection(selMode);
+                cursorPos:= i;
+                end
+            else if (not selMode) then
+                ResetSelection();
+
+            UpdateCursorCoords();
             end;
-        SDLK_RIGHT, SDLK_LEFT, SDLK_DELETE,
-        SDLK_HOME, SDLK_END,
+        SDLK_LEFT:
+            begin
+            if cursorPos > 0 then
+                begin
+
+                if ctrl then
+                    skip:= GetInputCharSkipClass(cursorPos);
+
+                if selMode or (selectedPos < 0) then
+                    begin
+                    HandleSelection(selMode);
+                    // go to end of previous utf8-char
+                    cursorPos:= InputStrL[cursorPos];
+                    end
+                else // if we're leaving selection mode, jump to its left end
+                    begin
+                    cursorPos:= min(cursorPos, selectedPos);
+                    ResetSelection();
+                    end;
+
+                if ctrl then
+                    SkipInputChars(skip, true);
+
+                end
+            else if (not selMode) then
+                ResetSelection();
+
+            UpdateCursorCoords();
+            end;
+        SDLK_RIGHT:
+            begin
+            if cursorPos < Length(InputStr.s) then
+                begin
+
+                if selMode or (selectedPos < 0) then
+                    begin
+                    HandleSelection(selMode);
+                    DoCursorStepForward();
+                    end
+                else // if we're leaving selection mode, jump to its right end
+                    begin
+                    cursorPos:= max(cursorPos, selectedPos);
+                    ResetSelection();
+                    end;
+
+                if ctrl then
+                    SkipInputChars(GetInputCharSkipClass(cursorPos), false);
+
+                end
+            else if (not selMode) then
+                ResetSelection();
+
+            UpdateCursorCoords();
+            end;
         SDLK_PAGEUP, SDLK_PAGEDOWN:
             begin
             // ignore me!!!
             end;
+        SDLK_a:
+            begin
+            // select all
+            if ctrl then
+                begin
+                ResetSelection();
+                cursorPos:= 0;
+                HandleSelection(true);
+                cursorPos:= Length(InputStr.s);
+                UpdateCursorCoords();
+                end
+            else
+                action:= false;
+            end;
+        SDLK_c:
+            begin
+            // copy
+            if ctrl then
+                CopySelectionToClipboard()
+            else
+                action:= false;
+            end;
+        SDLK_v:
+            begin
+            // paste
+            if ctrl then
+                PasteFromClipboard()
+            else
+                action:= false;
+            end;
+        SDLK_x:
+            begin
+            // cut
+            if ctrl then
+                begin
+                CopySelectionToClipboard();
+                DeleteSelected();
+                end
+            else
+                action:= false;
+            end;
         else
             action:= false;
         end;
     if not action and (Key <> 0) then
         begin
+        DeleteSelected();
+
         if (Key < $80) then
             btw:= 1
         else if (Key < $800) then
@@ -480,11 +1059,18 @@
 
         utf8:= char(Key or firstByteMark[Pred(btw)]) + utf8;
 
-        if byte(InputStr.s[0]) + btw > 240 then
+        if Length(InputStr.s) + btw > MaxInputStrLen then
             exit;
 
-        InputStrL[byte(InputStr.s[0]) + btw]:= InputStr.s[0];
-        SetLine(InputStr, InputStr.s + utf8, true)
+        if (Length(InputStr.s) = 0) and (Length(utf8) = 1) and (charIsForHogSpeech(utf8[1])) then
+            begin
+            InsertIntoInputStr(utf8);
+            InsertIntoInputStr(utf8);
+            cursorPos:= 1;
+            UpdateCursorCoords();
+            end
+        else
+            InsertIntoInputStr(utf8);
         end
 end;
 
@@ -539,7 +1125,14 @@
     if length(s) = 0 then
         SetLine(InputStr, '', true)
     else
-        SetLine(InputStr, '/team ', true)
+        begin
+        SetLine(InputStr, '/team ', true);
+        // update InputStrL and cursor accordingly
+        // this allows cursor-jumping over '/team ' as if it was a single char
+        InputStrL[6]:= 0;
+        cursorPos:= 6;
+        UpdateCursorCoords();
+        end;
 end;
 
 procedure initModule;
@@ -560,15 +1153,25 @@
     missedCount:= 0;
     liveLua:= false;
     ChatHidden:= false;
+    firstDraw:= true;
 
+    InputLinePrefix.Tex:= nil;
+    UpdateInputLinePrefix();
+    inputStr.s:= '';
     inputStr.Tex := nil;
     for i:= 0 to MaxStrIndex do
         Strs[i].Tex := nil;
+
+    FillChar(InputStrL, sizeof(InputStrL), InputStrLNoPred);
+
+    LastKeyPressTick:= 0;
+    ResetCursor();
 end;
 
 procedure freeModule;
 var i: ShortInt;
 begin
+    FreeAndNilTexture(InputLinePrefix.Tex);
     FreeAndNilTexture(InputStr.Tex);
     for i:= 0 to MaxStrIndex do
         FreeAndNilTexture(Strs[i].Tex);