diff --git a/Quake/common.c b/Quake/common.c index cf4798716..58c2c79dd 100644 --- a/Quake/common.c +++ b/Quake/common.c @@ -4031,6 +4031,31 @@ static const uint32_t qchar_to_unicode[256] = ---------------------------------------------------------------------------------------------------------------------------------- */}; +/* +================== +UTF8_CodePointLength + +Returns the number of bytes needed to encode the codepoint +using UTF-8 (max 4), or 0 for an invalid code point +================== +*/ +size_t UTF8_CodePointLength (uint32_t codepoint) +{ + if (codepoint < 0x80) + return 1; + + if (codepoint < 0x800) + return 2; + + if (codepoint < 0x10000) + return 3; + + if (codepoint < 0x110000) + return 4; + + return 0; +} + /* ================== UTF8_WriteCodePoint @@ -4154,14 +4179,30 @@ uint32_t UTF8_ReadCodePoint (const char **src) UTF8_FromQuake Converts a string from Quake encoding to UTF-8 + +Returns the number of written characters (including the NUL terminator) +if a valid output buffer is provided (dst is non-NULL, maxbytes > 0), +or the total amount of space necessary to encode the entire src string +if dst is NULL and maxbytes is 0. ================== */ -void UTF8_FromQuake (char *dst, size_t maxbytes, const char *src) +size_t UTF8_FromQuake (char *dst, size_t maxbytes, const char *src) { size_t i, j, written; if (!maxbytes) - return; + { + if (dst) + return 0; // error + for (i = 0, j = 0; src[i]; i++) + { + uint32_t codepoint = qchar_to_unicode[(unsigned char) src[i]]; + if (codepoint) + j += UTF8_CodePointLength (codepoint); + } + return j + 1; // include terminator + } + --maxbytes; for (i = 0, j = 0; j < maxbytes && src[i]; i++) @@ -4175,7 +4216,9 @@ void UTF8_FromQuake (char *dst, size_t maxbytes, const char *src) j += written; } - dst[j] = '\0'; + dst[j++] = '\0'; + + return j; } /* @@ -4186,11 +4229,16 @@ Transliterates a string from UTF-8 to Quake encoding Note: only single-character transliterations are used for now, mainly to remove diacritics + +Returns the number of written characters (including the NUL terminator) +if a valid output buffer is provided (dst is non-NULL, maxbytes > 0), +or the total amount of space necessary to encode the entire src string +if dst is NULL and maxbytes is 0. ================== */ -void UTF8_ToQuake (char *dst, size_t maxbytes, const char *src) +size_t UTF8_ToQuake (char *dst, size_t maxbytes, const char *src) { - size_t i; + size_t i, j; if (!unicode_translit_init) { @@ -4214,7 +4262,32 @@ void UTF8_ToQuake (char *dst, size_t maxbytes, const char *src) } if (!maxbytes) - return; + { + if (dst) + return 0; // error + + // Determine necessary output buffer size + for (i = 0, j = 0; *src; i++) + { + // ASCII fast path + while (*src && (byte)*src < 0x80) + { + src++; + j++; + } + + if (!*src) + break; + + // Every codepoint maps to a single Quake character + UTF8_ReadCodePoint (&src); + + j++; + } + + return j + 1; // include terminator + } + --maxbytes; for (i = 0; i < maxbytes && *src; i++) @@ -4242,4 +4315,6 @@ void UTF8_ToQuake (char *dst, size_t maxbytes, const char *src) } dst[i++] = '\0'; + + return i; } diff --git a/Quake/common.h b/Quake/common.h index 8d84d2ce7..2dea37d4a 100644 --- a/Quake/common.h +++ b/Quake/common.h @@ -332,8 +332,8 @@ size_t LOC_Format (const char *format, const char* (*getarg_fn)(int idx, void* u // Unicode size_t UTF8_WriteCodePoint (char *dst, size_t maxbytes, uint32_t codepoint); uint32_t UTF8_ReadCodePoint (const char **src); -void UTF8_FromQuake (char *dst, size_t maxbytes, const char *src); -void UTF8_ToQuake (char *dst, size_t maxbytes, const char *src); +size_t UTF8_FromQuake (char *dst, size_t maxbytes, const char *src); +size_t UTF8_ToQuake (char *dst, size_t maxbytes, const char *src); #define UNICODE_UNKNOWN 0xFFFD #define UNICODE_MAX 0x10FFFF diff --git a/Quake/console.c b/Quake/console.c index 36a771b14..ff0601492 100644 --- a/Quake/console.c +++ b/Quake/console.c @@ -34,6 +34,8 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #endif +extern qboolean keydown[256]; + int con_linewidth; float con_cursorspeed = 4; @@ -65,9 +67,38 @@ typedef struct conofs_t end; } conlink_t; +typedef struct +{ + conofs_t begin; + conofs_t end; +} conselection_t; + +typedef enum +{ + // Used for link hover/clicking: + // - picks the character that contains the cursor + // - rejects areas outside the visible console region + CT_INSIDE, + + // Used for text selection: + // - picks the closest edge horizontally, on whichever line contains the cursor vertically + // - clamps to the margins of the visible console region + CT_NEAREST, +} contest_t; // Console hit testing mode + +typedef enum +{ + CMS_NOTPRESSED, + CMS_PRESSED, + CMS_DRAGGING, +} conmouse_t; + static conlink_t **con_links = NULL; static conlink_t *con_hotlink = NULL; +static conmouse_t con_mousestate = CMS_NOTPRESSED; +static conselection_t con_selection; + cvar_t con_notifytime = {"con_notifytime","3",CVAR_NONE}; //seconds cvar_t con_logcenterprint = {"con_logcenterprint", "1", CVAR_NONE}; //johnfitz cvar_t con_notifycenter = {"con_notifycenter", "0", CVAR_ARCHIVE}; @@ -87,24 +118,39 @@ qboolean con_initialized; /* ================ -Con_ScreenToOffset +Con_GetLine +================ +*/ +static const char *Con_GetLine (int line) +{ + return con_text + (line%con_totallines)*con_linewidth; +} -Converts screen (pixel) coordinates to a console offset -Returns true if the offset is inside the visible portion of the console +/* +================ +Con_StrLen ================ */ -static qboolean Con_ScreenToOffset (int x, int y, conofs_t *ofs) +static size_t Con_StrLen (int line) +{ + const char *text = Con_GetLine (line); + size_t len = con_linewidth; + while (len > 0 && (char)(text[len - 1] & 0x7f) == ' ') + len--; + return len; +} + +static void Con_ScreenToCanvas (int x, int y, int *outx, int *outy) { drawtransform_t transform; float px, py; - qboolean ret = true; -// screen space to [-1..1] + // screen space to [-1..1] px = (x - glx) * 2.f / (float) glwidth - 1.f; py = (y - gly) * 2.f / (float) glheight - 1.f; py = -py; - -// [-1..1] to console canvas + + // [-1..1] to console canvas Draw_GetCanvasTransform (CANVAS_CONSOLE, &transform); px = (px - transform.offset[0]) / transform.scale[0]; py = (py - transform.offset[1]) / transform.scale[1]; @@ -113,6 +159,29 @@ static qboolean Con_ScreenToOffset (int x, int y, conofs_t *ofs) y = vid.conheight - y; + *outx = x; + *outy = y; +} + + +/* +================ +Con_ScreenToOffset + +Converts screen (pixel) coordinates to a console offset +Returns true if the offset is inside the visible portion of the console +================ +*/ +static qboolean Con_ScreenToOffset (int x, int y, conofs_t *ofs, contest_t testmode) +{ + qboolean ret = true; + + Con_ScreenToCanvas (x, y, &x, &y); + +// Apply rounding + if (testmode == CT_NEAREST) + x += 4; + // pixels to characters x >>= 3; y >>= 3; @@ -120,12 +189,36 @@ static qboolean Con_ScreenToOffset (int x, int y, conofs_t *ofs) // apply margins and scrolling x -= CON_MARGIN; y -= 2; - if (x < 0 || x >= con_linewidth) - ret = false; - if (y < 0 || y >= con_vislines) - ret = false; - if (con_backscroll && y < 2) - ret = false; + + if (testmode == CT_INSIDE) + { + if (x < 0 || x >= con_linewidth) + ret = false; + if (y < 0 || y >= con_vislines) + ret = false; + if (con_backscroll && y < 2) + ret = false; + } + else + { + // Allow the cursor to move one character past the end of the line + // by clamping to con_linewidth instead of con_linewidth - 1 + x = CLAMP (0, x, con_linewidth); + + // Enable selecting the entire bottom line by allowing the cursor + // to move to the beginning of the line below it (line -1) + y = CLAMP (-1, y, con_vislines); + if (y < 0) + x = 0; + + // Enable selecting the entire line above the backscroll cutoff by + // allowing the cursor to move to the beginning of the line below it + if (con_backscroll && y < 2) + { + x = 0; + y = 1; + } + } y += con_backscroll; y = con_current - y; @@ -162,6 +255,39 @@ static qboolean Con_OfsInRange (const conofs_t *ofs, const conofs_t *begin, cons return Con_OfsCompare (ofs, begin) >= 0 && Con_OfsCompare (ofs, end) < 0; } +/* +================ +Con_GetCurrentRange +================ +*/ +static void Con_GetCurrentRange (conofs_t *begin, conofs_t *end) +{ + begin->line = con_current - con_totallines + 1; + begin->col = 0; + end->line = con_current + 1; + end->col = 0; +} + +/* +================ +Con_IntersectRanges +================ +*/ +static qboolean Con_IntersectRanges (conofs_t *begin, conofs_t *end, const conofs_t *selbegin, const conofs_t *selend) +{ + if (Con_OfsCompare (selend, begin) <= 0) + return false; + if (Con_OfsCompare (end, selbegin) <= 0) + return false; + + if (Con_OfsCompare (begin, selbegin) < 0) + *begin = *selbegin; + if (Con_OfsCompare (selend, end) < 0) + *end = *selend; + + return true; +} + /* ================ Con_GetLinkAtOfs @@ -204,7 +330,7 @@ Returns the link at the given pixel coordinates, if any, or NULL otherwise static conlink_t *Con_GetLinkAtPixel (int x, int y) { conofs_t ofs; - if (!Con_ScreenToOffset (x, y, &ofs)) + if (!Con_ScreenToOffset (x, y, &ofs, CT_INSIDE)) return NULL; return Con_GetLinkAtOfs (&ofs); } @@ -221,7 +347,6 @@ static void Con_SetHotLink (conlink_t *link) if (link == con_hotlink) return; con_hotlink = link; - VID_SetMouseCursor (con_hotlink ? MOUSECURSOR_HAND : MOUSECURSOR_DEFAULT); } /* @@ -232,11 +357,11 @@ Computes the console offset corresponding to the current mouse position Returns true if the offset is inside the visible portion of the console ================ */ -static qboolean Con_GetMousePos (conofs_t *ofs) +static qboolean Con_GetMousePos (conofs_t *ofs, contest_t testmode) { int x, y; SDL_GetMouseState (&x, &y); - return Con_ScreenToOffset (x, y, ofs); + return Con_ScreenToOffset (x, y, ofs, testmode); } /* @@ -249,11 +374,107 @@ Returns the link at the current mouse position, if any, or NULL otherwise static conlink_t *Con_GetMouseLink (void) { conofs_t ofs; - if (Con_GetMousePos (&ofs)) + if (Con_GetMousePos (&ofs, CT_INSIDE)) return Con_GetLinkAtOfs (&ofs); return NULL; } +/* +================ +Con_ClearSelection +================ +*/ +static void Con_ClearSelection (void) +{ + memset (&con_selection, 0, sizeof (con_selection)); +} + +/* +================ +Con_HasSelection +================ +*/ +static qboolean Con_HasSelection (void) +{ + return Con_OfsCompare (&con_selection.begin, &con_selection.end) != 0; +} + +/* +================ +Con_GetNormalizedSelection +================ +*/ +static qboolean Con_GetNormalizedSelection (conofs_t *begin, conofs_t *end) +{ + conofs_t *selbegin = &con_selection.begin; + conofs_t *selend = &con_selection.end; + conofs_t tbegin, tend; + + if (Con_OfsCompare (selbegin, selend) > 0) + { + conofs_t *tmp = selbegin; + selbegin = selend; + selend = tmp; + } + *begin = *selbegin; + *end = *selend; + + Con_GetCurrentRange (&tbegin, &tend); + + return Con_IntersectRanges (begin, end, &tbegin, &tend); +} + +/* +================ +Con_IsOfsSelected +================ +*/ +static qboolean Con_IsOfsSelected (const conofs_t *ofs) +{ + int range = Con_OfsCompare (&con_selection.begin, &con_selection.end); + if (!range) + return 0; + if (range < 0) + return Con_OfsInRange (ofs, &con_selection.begin, &con_selection.end); // forward selection + else + return Con_OfsInRange (ofs, &con_selection.end, &con_selection.begin); // backward selection +} + +/* +================ +Con_SetMouseState +================ +*/ +static void Con_SetMouseState (conmouse_t state) +{ + if (con_mousestate == state) + return; + + switch (state) + { + case CMS_PRESSED: + Con_GetMousePos (&con_selection.begin, CT_NEAREST); + con_selection.end = con_selection.begin; + break; + + case CMS_DRAGGING: + Con_SetHotLink (NULL); + VID_SetMouseCursor (MOUSECURSOR_IBEAM); + break; + + case CMS_NOTPRESSED: + if (con_mousestate != CMS_DRAGGING && con_hotlink && !Sys_Explore (con_hotlink->path)) + S_LocalSound ("misc/menu2.wav"); + break; + + default: + break; + } + + con_mousestate = state; + Con_ForceMouseMove (); +} + /* ================ Con_Mousemove @@ -262,21 +483,50 @@ Mouse movement callback */ void Con_Mousemove (int x, int y) { - Con_SetHotLink (Con_GetLinkAtPixel (x, y)); + if (con_mousestate == CMS_NOTPRESSED) + { + Con_SetHotLink (Con_GetLinkAtPixel (x, y)); + VID_SetMouseCursor (con_hotlink ? MOUSECURSOR_HAND : MOUSECURSOR_DEFAULT); + } + else + { + Con_ScreenToOffset (x, y, &con_selection.end, CT_NEAREST); + if (Con_OfsCompare (&con_selection.begin, &con_selection.end) != 0) + Con_SetMouseState (CMS_DRAGGING); + } } /* ================ -Con_Click +Con_ForceMouseMove +================ +*/ +void Con_ForceMouseMove (void) +{ + int x, y; + SDL_GetMouseState (&x, &y); + Con_Mousemove (x, y); +} -Mouse click callback +/* +================ +Con_UpdateMouseState ================ */ -void Con_Click (void) +static void Con_UpdateMouseState (void) { - conlink_t *link = Con_GetMouseLink (); - if (link && !Sys_Explore (link->path)) - S_LocalSound ("misc/menu2.wav"); + if (key_dest != key_console) + { + Con_SetHotLink (NULL); + Con_SetMouseState (CMS_NOTPRESSED); + Con_ClearSelection (); + return; + } + + if (!keydown[K_MOUSE1]) + Con_SetMouseState (CMS_NOTPRESSED); + else if (con_mousestate == CMS_NOTPRESSED) + Con_SetMouseState (CMS_PRESSED); } /* @@ -368,6 +618,56 @@ static void Con_Clear_f (void) VEC_CLEAR (con_links); } +/* +================ +Con_CopySelectionToClipboard +================ +*/ +qboolean Con_CopySelectionToClipboard (void) +{ + conofs_t selbegin, selend; + conofs_t cursor, eol; + char *qtext = NULL; + char *utf8 = NULL; + size_t maxsize; + + S_LocalSound ("misc/menu2.wav"); + + // Get forward selection range + if (!Con_GetNormalizedSelection (&selbegin, &selend)) + return false; + + // Iterate through all lines in the selection + for (cursor = selbegin; Con_OfsCompare (&cursor, &selend) <= 0; cursor.line++, cursor.col = 0) + { + const char *text = Con_GetLine (cursor.line); + eol.line = cursor.line; + eol.col = Con_StrLen (cursor.line); + if (cursor.line == selend.line) + eol.col = q_min (eol.col, selend.col); + Vec_Append ((void **)&qtext, 1, text + cursor.col, eol.col - cursor.col); + if (eol.line != selend.line) + VEC_PUSH (qtext, '\n'); + } + VEC_PUSH (qtext, '\0'); + + // Convert to UTF-8 + maxsize = UTF8_FromQuake (NULL, 0, qtext); + utf8 = (char *) malloc (maxsize); + UTF8_FromQuake (utf8, maxsize, qtext); + + // Copy the UTF-8 text to clipboard + SDL_SetClipboardText (utf8); + + // Clean up temporary buffers + free (utf8); + VEC_FREE (qtext); + + Con_ClearSelection (); + + return true; +} + /* ================ Con_Dump_f -- johnfitz -- adapted from quake2 source @@ -1024,7 +1324,6 @@ typedef struct tab_s tab_t *tablist; //defs from elsewhere -extern qboolean keydown[256]; extern cmd_function_t *cmd_functions; #define MAX_ALIAS_NAME 32 typedef struct cmdalias_s @@ -1668,6 +1967,39 @@ void Con_DrawInput (void) } } +/* +================ +Con_DrawSelectionHighlight +================ +*/ +void Con_DrawSelectionHighlight (int x, int y, int line) +{ + conofs_t selbegin, selend; + conofs_t begin, end; + size_t len; + + if (!Con_GetNormalizedSelection (&selbegin, &selend)) + return; + + len = Con_StrLen (line); + begin.line = line; + begin.col = 0; + end.line = line; + end.col = len; + + if (!Con_IntersectRanges (&begin, &end, &selbegin, &selend)) + return; + + // Highlight line ends (as in Notepad, Visual Studio etc.) + if (end.line != selend.line && end.col == len) + end.col++; + + // ...unless we would end up overlapping the console margin + end.col = q_min (end.col, con_linewidth); + + Draw_Fill (x + begin.col*8, y, (end.col-begin.col)*8, 8, 220, 1.f); +} + /* ================ Con_DrawConsole -- johnfitz -- heavy revision @@ -1681,6 +2013,8 @@ void Con_DrawConsole (int lines, qboolean drawinput) int i, x, y, j, sb, rows; const char *text; + Con_UpdateMouseState (); + if (lines <= 0) return; @@ -1696,6 +2030,16 @@ void Con_DrawConsole (int lines, qboolean drawinput) rows -= 2; //for input and version lines sb = (con_backscroll) ? 2 : 0; + for (i = con_current - rows + 1; i <= con_current - sb; i++, y += 8) + { + j = i - con_backscroll; + if (j < 0) + j = 0; + text = con_text + (j % con_totallines)*con_linewidth; + Con_DrawSelectionHighlight (8, y, j); + } + + y = vid.conheight - (rows+2)*8; // +2 for input and version lines for (i = con_current - rows + 1; i <= con_current - sb; i++, y += 8) { conofs_t ofs; diff --git a/Quake/console.h b/Quake/console.h index 12d6d9e0b..119916326 100644 --- a/Quake/console.h +++ b/Quake/console.h @@ -62,7 +62,8 @@ qboolean Con_Match (const char *str, const char *partial); void Con_LogCenterPrint (const char *str); void Con_Mousemove (int x, int y); -void Con_Click (void); +void Con_ForceMouseMove (void); +qboolean Con_CopySelectionToClipboard (void); // // debuglog diff --git a/Quake/gl_vidsdl.c b/Quake/gl_vidsdl.c index fb8289c7e..7483554ab 100644 --- a/Quake/gl_vidsdl.c +++ b/Quake/gl_vidsdl.c @@ -67,6 +67,7 @@ static SDL_Window *draw_context; static SDL_GLContext gl_context; static SDL_Cursor *cursor_arrow; static SDL_Cursor *cursor_hand; +static SDL_Cursor *cursor_ibeam; static qboolean vid_locked = false; //johnfitz static qboolean vid_changed = false; @@ -180,14 +181,17 @@ static void VID_InitMouseCursors (void) { cursor_arrow = SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_ARROW); cursor_hand = SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_HAND); + cursor_ibeam = SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_IBEAM); } static void VID_FreeMouseCursors (void) { SDL_FreeCursor (cursor_arrow); SDL_FreeCursor (cursor_hand); + SDL_FreeCursor (cursor_ibeam); cursor_arrow = NULL; cursor_hand = NULL; + cursor_ibeam = NULL; } void VID_SetMouseCursor (mousecursor_t cursor) @@ -202,6 +206,10 @@ void VID_SetMouseCursor (mousecursor_t cursor) SDL_SetCursor (cursor_hand); return; + case MOUSECURSOR_IBEAM: + SDL_SetCursor (cursor_ibeam); + return; + default: return; } diff --git a/Quake/keys.c b/Quake/keys.c index 6fe67e263..d0c86160e 100644 --- a/Quake/keys.c +++ b/Quake/keys.c @@ -369,6 +369,7 @@ void Key_Console (int key) } else key_linepos = 1; Con_TabComplete (TABCOMPLETE_AUTOHINT); + Con_ForceMouseMove (); return; case K_END: @@ -376,6 +377,7 @@ void Key_Console (int key) con_backscroll = 0; else key_linepos = strlen(workline); Con_TabComplete (TABCOMPLETE_AUTOHINT); + Con_ForceMouseMove (); return; case K_PGUP: @@ -383,6 +385,7 @@ void Key_Console (int key) con_backscroll += keydown[K_CTRL] ? ((con_vislines>>3) - 4) : 2; if (con_backscroll > con_totallines - (vid.height>>3) - 1) con_backscroll = con_totallines - (vid.height>>3) - 1; + Con_ForceMouseMove (); return; case K_PGDN: @@ -390,6 +393,7 @@ void Key_Console (int key) con_backscroll -= keydown[K_CTRL] ? ((con_vislines>>3) - 4) : 2; if (con_backscroll < 0) con_backscroll = 0; + Con_ForceMouseMove (); return; case K_LEFTARROW: @@ -478,6 +482,11 @@ void Key_Console (int key) case K_INS: if (keydown[K_SHIFT]) /* Shift-Ins paste */ PasteToConsole(); + else if (keydown[K_CTRL]) + { + Con_CopySelectionToClipboard (); + return; + } else key_insert ^= 1; Con_TabComplete (TABCOMPLETE_AUTOHINT); return; @@ -501,6 +510,8 @@ void Key_Console (int key) case 'c': case 'C': if (keydown[K_CTRL]) { /* Ctrl+C: abort the line -- S.A */ + if (Con_CopySelectionToClipboard ()) + return; Con_Printf ("%s\n", workline); workline[0] = ']'; workline[1] = 0; @@ -1141,8 +1152,6 @@ void Key_Event (int key, qboolean down) q_snprintf (cmd, sizeof (cmd), "-%s %i\n", kb+1, key); Cbuf_AddText (cmd); } - if (key_dest == key_console && key == K_MOUSE1) - Con_Click (); return; } diff --git a/Quake/vid.h b/Quake/vid.h index 5134d8208..efac76047 100644 --- a/Quake/vid.h +++ b/Quake/vid.h @@ -89,6 +89,7 @@ typedef enum { MOUSECURSOR_DEFAULT, MOUSECURSOR_HAND, + MOUSECURSOR_IBEAM, } mousecursor_t; void *VID_GetWindow (void);