/* * Copyright (c) 2002-2023 Julien Nadeau Carriere * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * The base Agar text editor (the low-level widget behind AG_Textbox(3)). * It enables the user to edit single- or multi-line, NUL-terminated strings. * It supports ASCII, UTF-8, UCS-4 (and other encodings through iconv), and * also interprets ANSI SGR attributes. */ #include #ifdef AG_WIDGETS #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include AG_EditableClipboard agEditableClipbrd; /* For Copy/Cut/Paste */ AG_EditableClipboard agEditableKillring; /* For Emacs-style Kill/Yank */ static Uint32 AutocompleteTimeout(AG_Timer *_Nonnull, AG_Event *_Nonnull); /* #define DEBUG_CLIPBOARD */ /* #define DEBUG_UNDO */ /* * Return a new working buffer. The variable is returned locked; the caller * should invoke ReleaseBuffer() after use. */ static __inline__ AG_EditableBuffer *_Nullable GetBuffer(AG_Editable *_Nonnull ed) { AG_EditableBuffer *buf; if (ed->flags & AG_EDITABLE_EXCL) { buf = &ed->sBuf; } else { if ((buf = TryMalloc(sizeof(AG_EditableBuffer))) == NULL) { return (NULL); } buf->s = NULL; buf->len = 0; buf->maxLen = 0; } #ifdef AG_UNICODE if (AG_Defined(ed, "text")) { /* AG_TextElement(3) */ AG_TextElement *txt; buf->var = AG_GetVariable(ed, "text", (void *)&txt); buf->reallocable = 1; AG_MutexLock(&txt->lock); if ((ed->flags & AG_EDITABLE_EXCL) == 0 || buf->s == NULL) { const AG_TextEnt *te = &txt->ent[ed->lang]; if (te->buf != NULL) { buf->s = AG_ImportUnicode("UTF-8", te->buf, &buf->len, &buf->maxLen); } else { if ((buf->s = TryMalloc(sizeof(AG_Char))) != NULL) { buf->s[0] = (AG_Char)'\0'; } buf->len = 0; } if (buf->s == NULL) { AG_MutexUnlock(&txt->lock); AG_UnlockVariable(buf->var); buf->var = NULL; goto fail; } } } else #endif /* AG_UNICODE */ { /* "C" string */ char *s; buf->var = AG_GetVariable(ed, "string", (void *)&s); buf->reallocable = 0; if ((ed->flags & AG_EDITABLE_EXCL) == 0 || buf->s == NULL) { #ifdef AG_UNICODE buf->s = AG_ImportUnicode(ed->encoding, s, &buf->len, &buf->maxLen); #else buf->s = (Uint8 *)TryStrdup(s); buf->maxLen = buf->len = strlen(s); #endif if (buf->s == NULL) { AG_UnlockVariable(buf->var); goto fail; } } } return (buf); fail: if ((ed->flags & AG_EDITABLE_EXCL) == 0) { free(buf); } return (NULL); } /* Clear a working buffer. */ static __inline__ void ClearBuffer(AG_EditableBuffer *_Nonnull buf) { AG_Free(buf->s); buf->s = NULL; buf->len = 0; buf->maxLen = 0; } /* Commit changes to the working buffer. */ static void CommitBuffer(AG_Editable *_Nonnull ed, AG_EditableBuffer *_Nonnull buf) { #ifdef AG_UNICODE if (AG_Defined(ed, "text")) { /* AG_TextElement(3) */ AG_TextElement *txt = buf->var->data.p; AG_TextEnt *te = &txt->ent[ed->lang]; AG_Size len; if (AG_LengthUTF8FromUCS4(buf->s, &len) == -1) goto fail; if ((++len > te->maxLen) && AG_TextRealloc(te, len) == -1) goto fail; if (AG_ExportUnicode(ed->encoding, te->buf, buf->s, te->maxLen + 1) == -1) goto fail; } else { /* "C" string */ if (AG_ExportUnicode(ed->encoding, buf->var->data.s, buf->s, buf->var->info.size) == -1) goto fail; } #else /* !AG_UNICODE */ Strlcpy(buf->var->data.s, (const char *)buf->s, buf->var->info.size); #endif /* !AG_UNICODE */ ed->flags |= AG_EDITABLE_MARKPREF; AG_PostEvent(ed, "editable-postchg", NULL); return; #ifdef AG_UNICODE fail: Verbose("CommitBuffer: %s; ignoring\n", AG_GetError()); #endif } /* Release the working buffer. */ static __inline__ void ReleaseBuffer(AG_Editable *_Nonnull ed, AG_EditableBuffer *_Nonnull buf) { #ifdef AG_UNICODE if (AG_Defined(ed, "text")) AG_MutexUnlock(&AGTEXTELEMENT(buf->var->data.p)->lock); #endif if (buf->var != NULL) { AG_UnlockVariable(buf->var); buf->var = NULL; } if (!(ed->flags & AG_EDITABLE_EXCL)) { ClearBuffer(buf); Free(buf); } } /* Allocate and return a new buffer handle in a locked condition */ AG_EditableBuffer * AG_EditableGetBuffer(AG_Editable *ed) { AG_EditableBuffer *buf; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if ((buf = GetBuffer(ed)) == NULL) { AG_ObjectUnlock(ed); } return (buf); } /* Clear a working buffer. */ void AG_EditableClearBuffer(AG_Editable *ed, AG_EditableBuffer *buf) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); ClearBuffer(buf); } /* Increase the working buffer size to accomodate new characters. */ int AG_EditableGrowBuffer(AG_Editable *ed, AG_EditableBuffer *buf, AG_Char *ins, AG_Size nIns) { AG_Size newLen; /* UCS-4 buffer size in bytes */ AG_Size convLen; /* Converted string length in bytes */ AG_Char *sNew; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); newLen = (buf->len + nIns + 1)*sizeof(AG_Char); #ifdef AG_UNICODE if (strcmp(ed->encoding, "UTF-8") == 0) { AG_Size sLen, insLen; if (AG_LengthUTF8FromUCS4(buf->s, &sLen) == -1 || AG_LengthUTF8FromUCS4(ins, &insLen) == -1) { return (-1); } convLen = sLen + insLen + 1; } else if (strcmp(ed->encoding, "US-ASCII") == 0) { convLen = AG_LengthUCS4(buf->s) + nIns + 1; } else { /* TODO Proper estimates for other charsets */ convLen = newLen; } #else /* !AG_UNICODE */ if (strcmp(ed->encoding, "US-ASCII") == 0) { convLen = strlen((const char *)buf->s) + nIns + 1; } else { convLen = newLen; } #endif /* AG_UNICODE */ if (!buf->reallocable) { if (convLen > buf->var->info.size) { AG_SetError("%u > %u bytes", (Uint)convLen, (Uint)buf->var->info.size); return (-1); } } if (newLen > buf->maxLen) { if ((sNew = TryRealloc(buf->s, newLen)) == NULL) { return (-1); } buf->s = sNew; buf->maxLen = newLen; } return (0); } /* Release a working buffer. */ void AG_EditableReleaseBuffer(AG_Editable *ed, AG_EditableBuffer *buf) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); ReleaseBuffer(ed, buf); AG_ObjectUnlock(ed); } AG_Editable * AG_EditableNew(void *parent, Uint flags) { AG_Editable *ed; ed = Malloc(sizeof(AG_Editable)); AG_ObjectInit(ed, &agEditableClass); if (flags & AG_EDITABLE_HFILL) { WIDGET(ed)->flags |= AG_WIDGET_HFILL; } if (flags & AG_EDITABLE_VFILL) { WIDGET(ed)->flags |= AG_WIDGET_VFILL; } if (flags & AG_EDITABLE_CATCH_TAB) { WIDGET(ed)->flags |= AG_WIDGET_CATCH_TAB; } ed->flags |= flags; if ((flags & AG_EDITABLE_EXCL) == 0) AG_RedrawOnTick(ed, 1000); /* Default update rate */ AG_ObjectAttach(parent, ed); return (ed); } /* Bind to a C string containing ASCII text. */ void AG_EditableBindASCII(AG_Editable *ed, char *buf, AG_Size bufSize) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_Unset(ed, "text"); AG_BindString(ed, "string", buf, bufSize); ed->encoding = "US-ASCII"; AG_ObjectUnlock(ed); } #ifdef AG_UNICODE /* Bind to a C string containing UTF-8 encoded text. */ void AG_EditableBindUTF8(AG_Editable *ed, char *buf, AG_Size bufSize) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_Unset(ed, "text"); AG_BindString(ed, "string", buf, bufSize); ed->encoding = "UTF-8"; AG_ObjectUnlock(ed); } /* Bind to a C string containing text in specified encoding. */ void AG_EditableBindEncoded(AG_Editable *ed, const char *encoding, char *buf, AG_Size bufSize) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_Unset(ed, "text"); AG_BindString(ed, "string", buf, bufSize); ed->encoding = encoding; AG_ObjectUnlock(ed); } /* Bind to an AG_TextElement(3). */ void AG_EditableBindText(AG_Editable *ed, AG_TextElement *txt) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_Unset(ed, "string"); AG_BindPointer(ed, "text", (void *)txt); ed->encoding = "UTF-8"; AG_ObjectUnlock(ed); } /* * Set the active language (when bound to an AG_TextElement(3)). * If the language ID is invalid, set the language to AG_LANG_NONE. */ void AG_EditableSetLang(AG_Editable *ed, enum ag_language lang) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); ed->lang = (lang < AG_LANG_LAST) ? lang : AG_LANG_NONE; ed->pos = 0; ed->selStart = 0; ed->selEnd = 0; AG_Redraw(ed); AG_ObjectUnlock(ed); } #endif /* AG_UNICODE */ /* Enable or disable password entry mode. */ void AG_EditableSetPassword(AG_Editable *ed, int enable) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_SETFLAGS(ed->flags, AG_EDITABLE_PASSWORD, enable); AG_ObjectUnlock(ed); } /* Enable or disable word wrapping. */ void AG_EditableSetWordWrap(AG_Editable *ed, int enable) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); ed->x = 0; ed->y = 0; ed->pos = 0; ed->selStart = 0; ed->selEnd = 0; AG_SETFLAGS(ed->flags, AG_EDITABLE_WORDWRAP, enable); AG_ObjectUnlock(ed); } /* * Set Exclusive Access (EXCL) or Shared Access (!EXCL) mode. * * Under Exclusive Access mode, the caller guarantees that the bound text * buffer will never be modified externally (by anything other than this * particular Editable instance). Other Editable instances in Read-Only or * Disabled mode may still access its contents for reading. * * In Shared Access mode (the default), every interaction and operation must * acquire an internal working buffer (which involves allocations and UTF-8 * conversions). Checksums must be calculated in order for Undo/Redo to work * safely against potentially-changing buffer, and the Editable must be * redrawn periodically. * * Exclusive Access mode allows Editable to operate more efficiently through * the use of a persistent working buffer, which avoids the need for regular * redrawing, allocations, UTF-8 conversions and checksumming. */ void AG_EditableSetExcl(AG_Editable *ed, int enable) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (enable) { if (!(ed->flags & AG_EDITABLE_EXCL)) { ed->flags |= AG_EDITABLE_EXCL; AG_RedrawOnTick(ed, -1); AG_EditableClearHistory(ed); ClearBuffer(&ed->sBuf); } } else { if (ed->flags & AG_EDITABLE_EXCL) { ed->flags &= ~(AG_EDITABLE_EXCL); AG_RedrawOnTick(ed, 1000); AG_EditableClearHistory(ed); ClearBuffer(&ed->sBuf); } } AG_ObjectUnlock(ed); } /* Toggle floating-point only input */ void AG_EditableSetFltOnly(AG_Editable *ed, int enable) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (enable) { ed->flags |= AG_EDITABLE_FLT_ONLY; ed->flags &= ~(AG_EDITABLE_INT_ONLY); } else { ed->flags &= ~(AG_EDITABLE_FLT_ONLY); } AG_ObjectUnlock(ed); } /* Toggle integer only input */ void AG_EditableSetIntOnly(AG_Editable *ed, int enable) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (enable) { ed->flags |= AG_EDITABLE_INT_ONLY; ed->flags &= ~(AG_EDITABLE_FLT_ONLY); } else { ed->flags &= ~(AG_EDITABLE_INT_ONLY); } AG_ObjectUnlock(ed); } /* Evaluate if a character is acceptable in integer-only mode. */ static __inline__ _Const_Attribute int CharIsIntOnly(AG_Char c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '-': case '+': return (1); default: return (0); } } /* Evaluate if a character is acceptable in float-only mode. */ static __inline__ _Const_Attribute int CharIsFltOnly(AG_Char c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': case '-': case '+': case 'e': case 'i': case 'n': case 'f': case 'a': case 0x221e: /* inf */ return (1); default: return (0); } } /* * Process a keystroke. May be invoked from the repeat timeout routine or * the keydown handler. If we return 1, the current delay/repeat cycle will * be maintained, otherwise it will be cancelled. */ static int ProcessKey(AG_Editable *_Nonnull ed, AG_KeySym ks, AG_KeyMod kmod, AG_Char ch) { AG_EditableBuffer *buf; int i, rv; if (ks == AG_KEY_ESCAPE) { return (0); } if ((ks == AG_KEY_RETURN || ks == AG_KEY_KP_ENTER) && (ed->flags & AG_EDITABLE_MULTILINE) == 0) return (0); if (kmod == 0 && (ks & ~0x7f) == 0 && /* is ascii */ isprint((int)ks)) { if ((ed->flags & AG_EDITABLE_INT_ONLY) && !CharIsIntOnly((AG_Char)ks)) { return (0); } else if ((ed->flags & AG_EDITABLE_FLT_ONLY) && !CharIsFltOnly((AG_Char)ks)) { return (0); } } if ((buf = GetBuffer(ed)) == NULL) return (0); if (ed->pos < 0) { ed->pos = 0; } if (ed->pos > buf->len) { ed->pos = buf->len; } for (i=0, rv=0; ; i++) { const struct ag_keycode *kc = &agKeymap[i]; const char *flag; if ((kc->key != AG_KEY_LAST) && (kc->key != ks)) { continue; } if (kc->modKeys != 0 && ((kmod & kc->modKeys) == 0)) { continue; } for (flag = &kc->flags[0]; *flag != '\0'; flag++) { switch (*flag) { case 'w': if (AGEDITABLE_IS_READONLY(ed)) { goto next_key; } break; case 'k': if (ed->flags & AG_EDITABLE_NO_KILL_YANK) { goto next_key; } break; } } AG_PostEvent(ed, "editable-prechg", NULL); rv = kc->func(ed, buf, ks, kmod, ch); break; next_key: if (kc->key == AG_KEY_LAST) { break; } else { continue; } } if (rv == 1) { CommitBuffer(ed, buf); } ReleaseBuffer(ed, buf); return (1); } /* Timer callback for handling key repeat */ static Uint32 KeyRepeatTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); const int keysym = AG_INT(1); const int keymod = AG_INT(2); const AG_Char ch = AG_CHAR(3); if (ProcessKey(ed, keysym, keymod, ch) == 0) { return (0); } ed->flags |= AG_EDITABLE_BLINK_ON; AG_Redraw(ed); return (agKbdRepeat); } /* Timer callback for blinking cursor */ static Uint32 BlinkTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); if ((ed->flags & AG_EDITABLE_CURSOR_MOVING) == 0) { AG_INVFLAGS(ed->flags, AG_EDITABLE_BLINK_ON); AG_Redraw(ed); } return (to->ival); } static void OnFocusGain(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); int i; AG_LockTimers(ed); AG_DelTimer(ed, &ed->toRepeat); for (i = 0; i < 4; i++) { AG_DelTimer(ed, &ed->toRepeatDirs[i]); } AG_AddTimer(ed, &ed->toCursorBlink, agTextBlinkRate, BlinkTimeout, NULL); ed->flags |= AG_EDITABLE_BLINK_ON; AG_UnlockTimers(ed); AG_Redraw(ed); } static void OnFocusLoss(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); int i; AG_LockTimers(ed); AG_DelTimer(ed, &ed->toRepeat); for (i = 0; i < 4; i++) { AG_DelTimer(ed, &ed->toRepeatDirs[i]); } AG_DelTimer(ed, &ed->toCursorBlink); AG_DelTimer(ed, &ed->toDblClick); ed->flags &= ~(AG_EDITABLE_BLINK_ON | AG_EDITABLE_CURSOR_MOVING | AG_EDITABLE_RETURN_HELD); AG_UnlockTimers(ed); AG_Redraw(ed); } static void OnHide(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); if (ed->complete) { AG_DelTimer(ed, &ed->complete->to); free(ed->complete); ed->complete = NULL; } if (ed->pm != NULL) { AG_PopupHide(ed->pm); } OnFocusLoss(event); } static void OnFontChange(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); const int height = WFONT(ed)->height; const int lineskip = WFONT(ed)->lineskip; ed->fontMaxHeight = height; ed->lineSkip = lineskip; ed->yVis = HEIGHT(ed) / lineskip; if (ed->suPlaceholder != -1) { AG_WidgetUnmapSurface(ed, ed->suPlaceholder); ed->suPlaceholder = -1; } } /* * Evaluate word wrapping at given character (possibly rendering it). */ static __inline__ int WrapAtChar(AG_Editable *_Nonnull ed, int x, AG_Char *_Nonnull s, AG_Font *_Nonnull font, const AG_Color *_Nonnull cBg, const AG_Color *_Nonnull cFg) { AG_Driver *drv = WIDGET(ed)->drv; AG_Glyph *G; AG_Char *t; int x2; if (!(ed->flags & AG_EDITABLE_WORDWRAP) || x == 0 || !AG_CharIsSpace(*s)) { return (0); } for (t = &s[1], x2 = x; *t != '\0'; t++) { G = AG_TextRenderGlyph(drv, font, cBg, cFg, *t); x2 += G->advance; if (AG_CharIsSpace(*t) || *t == '\n') { if (x2 > WIDTH(ed)) { return (1); } else { break; } } } return (0); } /* * Map mouse coordinates to a character position within the buffer. */ #define ON_LINE(my,y) ((my) >= (y) && (my) <= (y)+ed->lineSkip) #define ON_CHAR(mx,x,adv) ((mx) >= (x) && (mx) <= (x)+(adv)) int AG_EditableMapPosition(AG_Editable *ed, AG_EditableBuffer *buf, int mx, int my, int *pos) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Driver *drv = WIDGET(ed)->drv; AG_Font *font = WFONT(ed); const enum ag_font_type fontType = font->spec.type; AG_TextANSI ansi; int i, x, y, yMouse; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if ((yMouse = my + ed->y*ed->lineSkip) < 0) { *pos = 0; goto out; } for (i=0, x=0, y=0; i < buf->len; i++) { const AG_Char c = buf->s[i]; if (mx <= 0 && ON_LINE(yMouse,y)) { *pos = i; goto out; } if (WrapAtChar(ed, x, &buf->s[i], font, &ts->colorBG, &ts->color)) { if (ON_LINE(yMouse,y) && mx > x) { *pos = i; goto out; } y += ed->lineSkip; x = 0; } if (c == '\n') { if (ON_LINE(yMouse,y) && mx > x) { *pos = i; goto out; } y += ed->lineSkip; x = 0; continue; } else if (c == '\t') { if (ON_LINE(yMouse,y) && mx >= x && mx <= x+agTextTabWidth) { *pos = (mx < x + (agTextTabWidth >> 1)) ? i : i+1; goto out; } x += agTextTabWidth; continue; } else if ( c == 0x1b && buf->s[i+1] >= 0x40 && buf->s[i+1] <= 0x5f && buf->s[i+2] != '\0') { if (AG_TextParseANSI(ts, &ansi, &buf->s[i+1]) == 0) { i += ansi.len; continue; } } switch (fontType) { #ifdef HAVE_FREETYPE case AG_FONT_FREETYPE: { AG_GlyphFt *Gft; Gft = AGFONT_OPS(font)->get_glyph(font, c, AG_GLYPH_FT_METRICS); if (Gft == NULL) { /* TODO blank box advance */ continue; } if (ON_LINE(yMouse, y) && ON_CHAR(mx, x, Gft->advance)) { *pos = (mx < x + (Gft->advance >> 1)) ? i : i+1; if (buf->s[*pos] == 0x1b && buf->s[*pos+1] >= 0x40 && buf->s[*pos+1] <= 0x5f && buf->s[*pos+2] != '\0') { if (AG_TextParseANSI(ts, &ansi, &buf->s[*pos+1]) == 0) *pos += ansi.len+1; } goto out; } x += Gft->advance; } break; #endif /* HAVE_FREETYPE */ case AG_FONT_BITMAP: { AG_Glyph *G; G = AG_TextRenderGlyph(drv, font, &ts->colorBG, &ts->color, c); if (ON_LINE(yMouse,y) && mx >= x && mx <= x + G->su->w) { *pos = i; if (buf->s[*pos] == 0x1b && buf->s[*pos+1] >= 0x40 && buf->s[*pos+1] <= 0x5f && buf->s[*pos+2] != '\0') { if (AG_TextParseANSI(ts, &ansi, &buf->s[*pos+1]) == 0) *pos += ansi.len+1; } goto out; } x += G->su->w; } break; case AG_FONT_DUMMY: default: break; } } *pos = buf->len; out: AG_ObjectUnlock(ed); return (0); } #undef ON_LINE #undef ON_CHAR /* Move cursor to the given position in pixels. */ void AG_EditableMoveCursor(AG_Editable *ed, AG_EditableBuffer *buf, int mx, int my) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); mx -= WIDGET(ed)->paddingLeft; my -= WIDGET(ed)->paddingTop; if (AG_EditableMapPosition(ed, buf, mx, my, &ed->pos) == 0) { ed->selStart = 0; ed->selEnd = 0; AG_Redraw(ed); } AG_ObjectUnlock(ed); } /* Set the cursor position (-1 = end of the string) with bounds checking. */ int AG_EditableSetCursorPos(AG_Editable *ed, AG_EditableBuffer *buf, int pos) { int rv; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); ed->pos = pos; ed->selStart = 0; ed->selEnd = 0; if (ed->pos < 0) { if (pos == -1 || ed->pos > buf->len) ed->pos = buf->len; } rv = ed->pos; ed->xScrollTo = &ed->xCurs; ed->yScrollTo = &ed->yCurs; AG_Redraw(ed); AG_ObjectUnlock(ed); return (rv); } /* Try to maintain visibility of the cursor (KEEPVISCURSOR option). */ static void KeepVisibleCursor(AG_Editable *_Nonnull ed, AG_EditableBuffer *_Nonnull buf) { if (ed->yCurs < ed->y) { if (ed->flags & AG_EDITABLE_MULTILINE) { AG_EditableMoveCursor(ed, buf, ed->xCursPref - ed->x, 1); } } else if (ed->yCurs > ed->y + ed->yVis - 1) { if (ed->flags & AG_EDITABLE_MULTILINE) { AG_EditableMoveCursor(ed, buf, ed->xCursPref - ed->x, ed->yVis*ed->lineSkip - 1); } } else if (ed->xCurs < ed->x+10) { if (!(ed->flags & AG_EDITABLE_WORDWRAP)) { AG_EditableMoveCursor(ed, buf, ed->x+10, (ed->yCurs - ed->y)*ed->lineSkip + 1); } } else if (ed->xCurs > ed->x+WIDTH(ed)-10) { if (!(ed->flags & AG_EDITABLE_WORDWRAP)) { AG_EditableMoveCursor(ed, buf, ed->x+WIDTH(ed)-10, (ed->yCurs - ed->y)*ed->lineSkip + 1); } } } static void Draw(void *_Nonnull obj) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Editable *ed = obj; AG_Driver *drv = WIDGET(ed)->drv; const AG_DriverClass *drvOps = WIDGET(ed)->drvOps; AG_EditableBuffer *buf; const AG_Color *cEditableBg = &WCOLOR(ed, BG_COLOR); const AG_Color *cSel = &WCOLOR(ed, SELECTION_COLOR); AG_Color cFg = ts->color; AG_Color cBg = ts->colorBG; AG_Font *font = ts->font; const int flags = ed->flags; const int isTransformed = (flags & (AG_EDITABLE_PASSWORD | AG_EDITABLE_UPPERCASE | AG_EDITABLE_LOWERCASE)); const int pos = ed->pos; const int lineSkip = ed->lineSkip; const int clipX1 = WIDGET(ed)->rView.x1 - (ed->fontMaxHeight << 1); const int clipX2 = WIDGET(ed)->rView.x2 + (ed->fontMaxHeight << 1); const int clipY1 = WIDGET(ed)->rView.y1 - lineSkip; const int clipY2 = WIDGET(ed)->rView.y2 + lineSkip; const int selStart = ed->selStart; const int selEnd = ed->selEnd; const int paddingLeft = WIDGET(ed)->paddingLeft; const int paddingTop = WIDGET(ed)->paddingTop; int i, dx,dy, x,y, yCurs=0, selected=0; if (cEditableBg->a > 0) AG_DrawRectFilled(ed, &WIDGET(ed)->r, cEditableBg); if ((buf = GetBuffer(ed)) == NULL) { return; } AG_EditableValidateSelection(ed, buf); /* TODO Opaque glyph optimizations */ AG_PushBlendingMode(ed, AG_ALPHA_SRC, AG_ALPHA_ONE_MINUS_SRC); if ((flags & AG_EDITABLE_NO_CLIPPING) == 0) AG_PushClipRect(ed, &ed->r); if (buf->len == 0 && AG_Defined(ed, "placeholder")) { AG_Variable *Vph; if (ed->suPlaceholder == -1 && (Vph = AG_AccessVariable(ed, "placeholder")) != NULL) { AG_Color c = WCOLOR(ed, TEXT_COLOR); AG_PushTextState(); AG_ColorDarken(&c,8); /* XXX color scheme dependent */ AG_TextColor(&c); ed->suPlaceholder = AG_WidgetMapSurface(ed, AG_TextRender((char *)Vph->data.p)); AG_UnlockVariable(Vph); AG_PopTextState(); } AG_WidgetBlitFrom(ed, ed->suPlaceholder, NULL, paddingLeft, paddingTop); } x = paddingLeft; y = paddingTop - ed->y*lineSkip; ed->xMax = 10; ed->yMax = 1; for (i = 0; i <= buf->len; i++) { AG_Glyph *G; AG_Char c = buf->s[i]; AG_Rect r; if (i == pos) { /* At cursor */ ed->xCurs = x; if (flags & AG_EDITABLE_MARKPREF) { ed->flags &= ~(AG_EDITABLE_MARKPREF); ed->xCursPref = x; } ed->yCurs = y/lineSkip + ed->y; yCurs = y; } if (selEnd > selStart) { if (i >= selStart && i < selEnd) { if (!selected) { selected = 1; ed->xSelStart = x; ed->ySelStart = y/lineSkip + ed->y; } } else { if (selected) { selected = 0; ed->xSelEnd = x; ed->ySelEnd = y/lineSkip + ed->y; } } } if (i == buf->len || c == '\0') break; if (WrapAtChar(ed, x, &buf->s[i], font, &cBg, &cFg)) { y += lineSkip; ed->xMax = MAX(ed->xMax, x); ed->yMax++; x = paddingLeft; } if (c == '\n') { y += lineSkip; if (selected) { AG_DrawLineV(ed, 1, y, y+lineSkip, cSel); } ed->xMax = MAX(ed->xMax, x+10); ed->yMax++; x = paddingLeft; continue; } else if (c == '\t') { if (selected) { r.x = x - ed->x; r.y = y; r.w = agTextTabWidth + 1; r.h = lineSkip + 1; AG_DrawRectFilled(ed, &r, cSel); } /* TODO */ x += agTextTabWidth; continue; } else if (c == 0x1b && buf->s[i+1] >= 0x40 && buf->s[i+1] <= 0x5f && buf->s[i+2] != '\0') { AG_TextANSI ansi; if (AG_TextParseANSI(ts, &ansi, &buf->s[i+1]) == 0) { if (ansi.ctrl == AG_ANSI_CSI_SGR) { switch (ansi.sgr) { case AG_SGR_RESET: case AG_SGR_NO_FG_NO_BG: cFg = ts->color; cBg = ts->colorBG; break; case AG_SGR_FG: cFg = ansi.color; break; case AG_SGR_BG: cBg = ansi.color; break; case AG_SGR_BOLD: break; case AG_SGR_UNDERLINE: break; default: break; } } i += ansi.len; continue; } } /* TODO separate loops for transformed cases */ if (isTransformed) { if (flags & AG_EDITABLE_PASSWORD) { c = '*'; } else if (flags & AG_EDITABLE_UPPERCASE) { c = toupper(c); } else if (flags & AG_EDITABLE_LOWERCASE) { c = tolower(c); } } G = AG_TextRenderGlyph(drv, font, &cBg, &cFg, c); dx = WIDGET(ed)->rView.x1 + x - ed->x; dy = WIDGET(ed)->rView.y1 + y; if (dx < clipX1 || dx >= clipX2 || /* Outside */ dy < clipY1 || dy >= clipY2) { x += G->advance; continue; } if (selected) { r.x = x - ed->x; r.y = y + 1; r.w = G->su->w + 1; r.h = lineSkip + 1; /* TODO queue and combine rectangles */ AG_DrawRectFilled(ed, &r, cSel); } /* * Render the character to the display using the drawGlyph() * driver call. This allows driver-specific filters or * optimizations to be applied. */ drvOps->drawGlyph(drv, G, dx,dy); x += G->advance; } if (ed->yMax == 1) ed->xMax = x; /* * Draw the cursor. * TODO different styles. */ if ((flags & AG_EDITABLE_BLINK_ON) && AG_WidgetIsFocused(ed) && (ed->y >= 0 && ed->y <= ed->yMax-1)) { AG_DrawLineV(ed, ed->xCurs - ed->x + 1, (yCurs + 1), (yCurs + lineSkip - 1), &WCOLOR(ed, TEXT_COLOR)); } if (flags & AG_EDITABLE_KEEPVISCURSOR) { /* Maintain visible */ KeepVisibleCursor(ed, buf); } if (ed->xScrollTo != NULL) { /* X scroll request (chars) */ const int xScrollTo = *ed->xScrollTo; if ((xScrollTo - ed->x) < 0) { ed->x += (xScrollTo - ed->x); if (ed->x < 0) { ed->x = 0; } } if ((xScrollTo - ed->x) > WIDTH(ed) - 10) { ed->x = xScrollTo - WIDTH(ed) + 10; } ed->xScrollTo = NULL; WIDGET(ed)->window->dirty = 1; } if (ed->yScrollTo != NULL && ed->flags & AG_EDITABLE_MULTILINE) { /* Y scroll request (chars) */ const int yScrollTo = *ed->yScrollTo; if ((yScrollTo - ed->y) < 0) { ed->y += (yScrollTo - ed->y); if (ed->y < 0) { ed->y = 0; } } else if ((yScrollTo - ed->y) > ed->yVis - 1) { ed->y = yScrollTo - ed->yVis + 1; } else if (yScrollTo <= ed->y) { if (ed->y > 0) ed->y--; } ed->yScrollTo = NULL; WIDGET(ed)->window->dirty = 1; } if (ed->xScrollPx != 0) { /* X scroll request (px) */ if (ed->xCurs < ed->x - ed->xScrollPx || ed->xCurs > ed->x + WIDTH(ed) - ed->xScrollPx) { ed->x += ed->xScrollPx; } ed->xScrollPx = 0; WIDGET(ed)->window->dirty = 1; } if ((flags & AG_EDITABLE_NO_CLIPPING) == 0) AG_PopClipRect(ed); AG_PopBlendingMode(ed); ReleaseBuffer(ed, buf); } void AG_EditableSizeHint(AG_Editable *ed, const char *text) { int hPre; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_TextSize(text, &ed->wPre, &hPre); ed->hPre = MIN(1, hPre/ed->lineSkip); AG_ObjectUnlock(ed); } void AG_EditableSizeHintPixels(AG_Editable *ed, Uint w, Uint h) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); ed->wPre = w; ed->hPre = MIN(1, h/ed->lineSkip); AG_ObjectUnlock(ed); } void AG_EditableSizeHintLines(AG_Editable *ed, Uint nLines) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); ed->hPre = nLines; } static void SizeRequest(void *_Nonnull obj, AG_SizeReq *_Nonnull r) { AG_Editable *ed = obj; r->w = WIDGET(ed)->paddingLeft + ed->wPre + WIDGET(ed)->paddingRight; r->h = WIDGET(ed)->paddingTop + ed->hPre*ed->lineSkip + WIDGET(ed)->paddingBottom; } static int SizeAllocate(void *_Nonnull obj, const AG_SizeAlloc *_Nonnull a) { AG_Editable *ed = obj; AG_Rect r; const int paddingLeft = WIDGET(ed)->paddingLeft; const int paddingRight = WIDGET(ed)->paddingRight; const int paddingTop = WIDGET(ed)->paddingTop; const int paddingBottom = WIDGET(ed)->paddingBottom; if (a->w < paddingLeft + paddingRight || (a->h < paddingTop + paddingBottom)) return (-1); ed->yVis = (a->h - paddingTop - paddingBottom) / ed->lineSkip; ed->r.x = paddingLeft; ed->r.y = paddingTop; ed->r.w = a->w - paddingLeft - paddingRight; ed->r.h = a->h - paddingTop - paddingBottom; r.x = 0; r.y = 0; r.w = a->w; r.h = a->h; AG_SetStockCursor(ed, &ed->ca, &r, AG_TEXT_CURSOR); return (0); } static void KeyDown(void *obj, AG_KeySym ks, AG_KeyMod kmod, AG_Char ch) { AG_Editable *ed = obj; int rv; switch (ks) { case AG_KEY_RETURN: case AG_KEY_KP_ENTER: if ((ed->flags & AG_EDITABLE_MULTILINE) == 0) { ed->flags |= AG_EDITABLE_RETURN_HELD; } break; case AG_KEY_LSHIFT: case AG_KEY_RSHIFT: ed->flags |= AG_EDITABLE_SHIFT_SELECT; ed->posKbdSel = ed->pos; return; case AG_KEY_LALT: case AG_KEY_RALT: case AG_KEY_LMETA: case AG_KEY_RMETA: case AG_KEY_LCTRL: case AG_KEY_RCTRL: return; case AG_KEY_TAB: if (ed->complete && /* Autocomplete */ (ed->flags & AG_EDITABLE_MULTILINE) == 0) { AG_Window *win; AG_Tlist *tl; if (ed->complete->winName[0] == '\0') { AG_AddTimer(ed, &ed->complete->to, agAutocompleteDelay, AutocompleteTimeout, NULL); AG_ExecTimer(&ed->complete->to); } if ((win = AG_WindowFind(ed->complete->winName)) && (tl = AG_ObjectFindChild(win, "suggestions")) != NULL) { AG_TlistSelectIdx(tl, 0); } return; } else if (!(WIDGET(ed)->flags & AG_WIDGET_CATCH_TAB)) { return; } break; default: break; } ed->flags |= AG_EDITABLE_BLINK_ON; if ((ed->flags & AG_EDITABLE_NUMERICAL) && (ed->flags & AG_EDITABLE_MULTILINE) == 0 && !AG_WidgetDisabled(ed)) { switch (ks) { case AG_KEY_UP: AG_PostEvent(ed, "editable-increment", NULL); goto out; case AG_KEY_DOWN: AG_PostEvent(ed, "editable-decrement", NULL); goto out; default: break; } } rv = ProcessKey(ed, ks, kmod, ch); switch (ks) { case AG_KEY_UP: case AG_KEY_DOWN: case AG_KEY_RIGHT: case AG_KEY_LEFT: if (rv == 1) { AG_AddTimer(ed, &ed->toRepeatDirs[ks-AG_KEY_UP], agKbdDelay, KeyRepeatTimeout, "%i,%i,%lu", ks, kmod, ch); } else { AG_DelTimer(ed, &ed->toRepeatDirs[ks-AG_KEY_UP]); } default: if (rv == 1) { AG_AddTimer(ed, &ed->toRepeat, agKbdDelay, KeyRepeatTimeout, "%i,%i,%lu", ks, kmod, ch); } else { AG_DelTimer(ed, &ed->toRepeat); } break; } out: AG_Redraw(ed); } static void AutocompletePoll(AG_Event *_Nonnull event) { AG_Tlist *tl = AG_TLIST_SELF(); AG_Editable *ed = AG_EDITABLE_PTR(1); if (ed->complete && ed->complete->fn) AG_PostEventByPtr(ed, ed->complete->fn, "%p", tl); } static void AutocompleteSelected(AG_Event *_Nonnull event) { AG_Tlist *tl = AG_TLIST_SELF(); AG_Editable *ed = AG_EDITABLE_PTR(1); AG_TlistItem *it = AG_TLISTITEM_PTR(2); AG_EditableSetString(ed, it->text); AG_PostEvent(WIDGET(tl)->window, "window-close", NULL); } static Uint32 AutocompleteTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); AG_Window *win, *winParent = WIDGET(ed)->window; AG_Tlist *tl; int xParent, yParent; if (AGDRIVER_MULTIPLE(WIDGET(ed)->drv)) { xParent = WIDGET(winParent)->x; yParent = WIDGET(winParent)->y; } else { xParent = 0; yParent = 0; } if (ed->complete->winName[0] != '\0') { /* Focus existing */ if ((win = AG_WindowFind(ed->complete->winName)) != NULL) { AG_WindowSetGeometry(win, xParent + WIDGET(ed)->rView.x1, yParent + WIDGET(ed)->rView.y1 + HEIGHT(ed), WIDTH(ed), 200); AG_WindowShow(win); AG_WindowFocus(win); } return (0); } win = AG_WindowNew(AG_WINDOW_MODAL | AG_WINDOW_DENYFOCUS | AG_WINDOW_NOTITLE | AG_WINDOW_KEEPABOVE); if (win == NULL) { return (0); } win->wmType = AG_WINDOW_WM_COMBO; AG_WindowAttach(winParent, win); AG_WindowSetCloseAction(win, AG_WINDOW_HIDE); AG_SetPadding(win, "0"); tl = AG_TlistNewPolledMs(win, AG_TLIST_EXPAND, agAutocompleteRate, AutocompletePoll,"%p",ed); AG_ObjectSetNameS(tl, "suggestions"); AG_SetEvent(tl, "tlist-selected", AutocompleteSelected,"%p",ed); AG_WindowSetGeometry(win, xParent + WIDGET(ed)->rView.x1, yParent + WIDGET(ed)->rView.y1 + HEIGHT(ed), WIDTH(ed), 200); AG_WindowShow(win); Strlcpy(ed->complete->winName, OBJECT(win)->name, sizeof(ed->complete->winName)); return (0); } static void KeyUp(void *obj, AG_KeySym ks, AG_KeyMod kmod, AG_Char ch) { AG_Editable *ed = obj; AG_DelTimer(ed, &ed->toRepeat); if ((ed->flags & AG_EDITABLE_NUMERICAL) && (ed->flags & AG_EDITABLE_MULTILINE) == 0 && !AG_WidgetDisabled(ed)) { switch (ks) { case AG_KEY_UP: case AG_KEY_DOWN: AG_PostEvent(ed, "editable-stop", NULL); break; default: break; } } switch (ks) { case AG_KEY_UP: case AG_KEY_DOWN: case AG_KEY_RIGHT: case AG_KEY_LEFT: AG_DelTimer(ed, &ed->toRepeatDirs[ks-AG_KEY_UP]); break; case AG_KEY_LSHIFT: case AG_KEY_RSHIFT: ed->flags &= ~(AG_EDITABLE_SHIFT_SELECT); ed->posKbdSel = 0; return; case AG_KEY_RETURN: case AG_KEY_KP_ENTER: if ((ed->flags & AG_EDITABLE_MULTILINE) == 0) { if (ed->flags & AG_EDITABLE_ABANDON_FOCUS) { AG_WidgetUnfocus(ed); } AG_PostEvent(ed, "editable-return", NULL); ed->flags &= ~(AG_EDITABLE_RETURN_HELD); AG_Redraw(ed); } break; default: break; } if (ed->complete) { switch (ks) { case AG_KEY_HOME: case AG_KEY_END: case AG_KEY_LEFT: case AG_KEY_RIGHT: case AG_KEY_UP: case AG_KEY_DOWN: case AG_KEY_PAGEUP: case AG_KEY_PAGEDOWN: /* Don't let directional keys trigger autocomplete */ break; default: AG_AddTimer(ed, &ed->complete->to, agAutocompleteDelay, AutocompleteTimeout, NULL); break; } } } static void Ctrl(void *obj, void *inputDevice, const AG_DriverEvent *dev) { AG_Editable *ed = obj; const int isDisabled = AG_WidgetDisabled(ed); ed->flags |= AG_EDITABLE_BLINK_ON; switch (dev->type) { case AG_DRIVER_CTRL_AXIS_MOTION: if (dev->ctrlAxis.axis == AG_CTRL_AXIS_LEFT_Y) { if ((ed->flags & AG_EDITABLE_NUMERICAL) && (ed->flags & AG_EDITABLE_MULTILINE) == 0 && !isDisabled) { if (dev->ctrlAxis.value == -0x8000) { AG_PostEvent(ed, "editable-increment", NULL); } else if (dev->ctrlAxis.value == 0x7fff) { AG_PostEvent(ed, "editable-decrement", NULL); } else { AG_PostEvent(ed, "editable-stop", NULL); } } else { if (dev->ctrlAxis.value == -0x8000 && ProcessKey(ed, AG_KEY_UP, 0, 0) != 0) { AG_AddTimer(ed, &ed->toRepeatDirs[0], /* UP */ agKbdDelay, KeyRepeatTimeout,"%i,%i,%lu", AG_KEY_UP, 0, 0U); } else if (dev->ctrlAxis.value == 0x7fff && ProcessKey(ed, AG_KEY_DOWN, 0, 0) != 0) { AG_AddTimer(ed, &ed->toRepeatDirs[1], /* DN */ agKbdDelay, KeyRepeatTimeout,"%i,%i,%lu", AG_KEY_DOWN, 0, 0U); } else { AG_DelTimer(ed, &ed->toRepeatDirs[0]); AG_DelTimer(ed, &ed->toRepeatDirs[1]); } } } else if (dev->ctrlAxis.axis == AG_CTRL_AXIS_LEFT_X) { if (dev->ctrlAxis.value == -0x8000 && ProcessKey(ed, AG_KEY_LEFT, 0, 0) != 0) { AG_AddTimer(ed, &ed->toRepeatDirs[3], /* L */ agKbdDelay, KeyRepeatTimeout,"%i,%i,%lu", AG_KEY_LEFT, 0, 0U); } else if (dev->ctrlAxis.value == 0x7fff && ProcessKey(ed, AG_KEY_RIGHT, 0, 0) != 0) { AG_AddTimer(ed, &ed->toRepeatDirs[2], /* R */ agKbdDelay, KeyRepeatTimeout,"%i,%i,%lu", AG_KEY_RIGHT, 0, 0U); } else { AG_DelTimer(ed, &ed->toRepeatDirs[2]); AG_DelTimer(ed, &ed->toRepeatDirs[3]); } } break; default: break; } } /* * Set up an autocomplete context. Function fn will be called to populate * the list of autocomplete candidates. * * If fn is NULL, disable autocomplete (closing any open windows). * If an autocomplete context already exists, update its function pointer * and arguments (cancelling any running timers). */ void AG_EditableAutocomplete(AG_Editable *ed, AG_EventFn fn, const char *fmt, ...) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (fn == NULL) { /* Disable autocomplete */ if (ed->complete) { AG_EditableCloseAutocomplete(ed); AG_UnsetEventByPtr(ed, ed->complete->fn); free(ed->complete); ed->complete = NULL; WIDGET(ed)->flags &= ~(AG_WIDGET_CATCH_TAB); } goto out; } if (ed->complete == NULL) { ed->complete = Malloc(sizeof(AG_Autocomplete)); AG_InitTimer(&ed->complete->to, "complete", 0); } else { /* Replace */ AG_UnsetEventByPtr(ed, ed->complete->fn); AG_DelTimer(ed, &ed->complete->to); } ed->complete->fn = AG_SetEvent(ed, NULL, fn, NULL); ed->complete->winName[0] = '\0'; if (fmt) { va_list ap; va_start(ap, fmt); AG_EventGetArgs(ed->complete->fn, fmt, ap); va_end(ap); } WIDGET(ed)->flags |= AG_WIDGET_CATCH_TAB; out: AG_ObjectUnlock(ed); } /* Close any active autocomplete windows and cancel any running timers. */ void AG_EditableCloseAutocomplete(AG_Editable *ed) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); if (ed->complete == NULL) return; AG_DelTimer(ed, &ed->complete->to); if (ed->complete->winName[0] != '\0') { AG_Window *win; if ((win = AG_WindowFind(ed->complete->winName)) != NULL) AG_PostEvent(win, "window-close", NULL); } } static void DoubleClick(AG_Editable *_Nonnull ed) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_EditableBuffer *buf; const AG_Char *c, *cEnd, *cAns; AG_TextANSI ansi; int pos, offsAns, ansiLen; AG_DelTimer(ed, &ed->toDblClick); ed->flags |= AG_EDITABLE_WORDSELECT; ed->selDblClick = -1; if ((buf = GetBuffer(ed)) == NULL) { return; } if (buf->len == 0) { goto out; } if ((pos = ed->pos) < 0) { /* Select first word */ /* TODO */ goto out; } else if (pos >= buf->len) { /* Select last word */ pos = buf->len-1; c = &buf->s[pos]; if (*c == '\n') { goto out; } while (AG_CharIsSpace(*c) && c >= &buf->s[0]) { c--; pos--; } for (ed->selStart = pos, ed->selEnd = pos+1; c >= &buf->s[0]; c--, ed->selStart--) { if (AG_CharIsSpaceOrLF(*c)) { ed->selStart++; break; } } goto out; } cEnd = &buf->s[buf->len]; c = &buf->s[pos]; if (*c == '\n') /* An empty line */ goto out; if (AG_CharIsSpace(*c)) { /* Select contiguous whitespace */ for (ed->selStart = pos-1, c--; c >= &buf->s[0]; c--) { if (!AG_CharIsSpace(*c)) { break; } ed->selStart--; } ed->selStart++; c = &buf->s[pos]; for (ed->selEnd = pos; c < cEnd; c++) { if (!AG_CharIsSpace(*c)) { break; } ed->selEnd++; } if (ed->selStart < ed->selEnd) { ed->pos = ed->selStart; } goto out; } else if (AG_CharIsPunct(*c)) { /* Select a block of punctuation */ for (ed->selStart = pos; c >= &buf->s[0]; c--) { if (!AG_CharIsPunct(*c)) { break; } ed->selStart--; } ed->selStart++; c = &buf->s[pos]; for (ed->selEnd = pos; c < cEnd; c++) { if (!AG_CharIsPunct(*c)) { break; } ed->selEnd++; } if (ed->selStart < ed->selEnd) { ed->pos = ed->selStart; } goto out; } /* Seek backwards for an ANSI sequence and remember its length. */ for (cAns=c, offsAns=0, ansiLen=0; cAns >= &buf->s[0] && offsAns < AG_TEXT_ANSI_SEQ_MAX; cAns--, offsAns++) { if (cAns[0] == 0x1b && cAns[1] >= 0x40 && cAns[1] <= 0x5f && cAns[2] != '\0' && AG_TextParseANSI(ts, &ansi, &cAns[1]) == 0) { ansiLen = ansi.len; break; } } /* * Select a word, including ANSI sequences but excluding adjacent * punctuation (further "mouse-motion" would include punctuation). */ for (ed->selStart = pos; c >= &buf->s[0]; c--) { if (*c == '\n' || AG_CharIsSpaceOrPunct(*c)) { ed->selStart++; break; } if (ansiLen > 0 && (ed->selStart == (pos - offsAns + ansiLen))) { ed->selStart -= ansiLen; break; } ed->selStart--; } if (ed->selStart < 0) ed->selStart = 0; c = &buf->s[pos]; for (ed->selEnd = pos; c < cEnd; c++) { if (*c == '\n' || AG_CharIsSpaceOrPunct(*c)) { break; } if (c[0] == 0x1b && c[1] >= 0x40 && c[1] <= 0x5f && c[2] != '\0' && AG_TextParseANSI(ts, &ansi, &c[1]) == 0) { ed->selEnd += ansi.len+1; break; } ed->selEnd++; } if (ed->selEnd < ed->selStart) { int temp = ed->selStart; ed->selStart = ed->selEnd; ed->selEnd = temp; } out: ReleaseBuffer(ed, buf); } /* Copy specified range to given internal clipboard. */ void AG_EditableCopyChunk(AG_Editable *ed, AG_EditableClipboard *cb, AG_Char *s, AG_Size len) { AG_Char *sNew; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_MutexLock(&cb->lock); sNew = TryRealloc(cb->s, (len+1)*sizeof(AG_Char)); if (sNew != NULL) { cb->s = sNew; memcpy(cb->s, s, len*sizeof(AG_Char)); cb->s[len] = '\0'; cb->len = len; } Strlcpy(cb->encoding, ed->encoding, sizeof(cb->encoding)); AG_MutexUnlock(&cb->lock); AG_ObjectUnlock(ed); } static __inline__ void InitRevision(AG_EditableRevision *_Nonnull rev) { rev->lenBuffer = 0; rev->crc32 = 0; rev->posStart = 0; rev->posEnd = 0; rev->nCharsAdded = 0; rev->nCharsRemoved = 0; rev->s = NULL; } static __inline__ void FreeRevision(AG_EditableRevision *_Nonnull rev) { if (rev->nCharsRemoved > 0) free(rev->s); } /* Destroy the History Buffer. */ void AG_EditableClearHistory(AG_Editable *ed) { Uint i; for (i = 0; i < ed->nUndo; i++) { FreeRevision(&ed->undo[i]); } ed->undo = Realloc(ed->undo, sizeof(AG_EditableRevision)); ed->nUndo = 0; for (i = 0; i < ed->nRedo; i++) { FreeRevision(&ed->redo[i]); } ed->redo = Realloc(ed->redo, sizeof(AG_EditableRevision)); ed->nRedo = 0; } /* * Create a new revision. * * This function must be called *before* any modifications are made to * the buffer. Under Shared Access, a checksum of the buffer contents is * generated and recorded in the Undo/Redo stack. If any external changes * are detected then the Undo/Redo stack is invalidated. */ AG_EditableRevision * AG_EditableBeginRevision(AG_Editable *ed, AG_EditableBuffer *buf) { AG_EditableRevision *rev; int i; #ifdef DEBUG_UNDO Debug(ed, "BEGIN Undo Rev#%d\n", ed->nUndo + 1); #endif if (ed->nRedo > 0) { for (i = 0; i < ed->nRedo; i++) { FreeRevision(&ed->redo[i]); } ed->nRedo = 0; } ed->undo = Realloc(ed->undo, (ed->nUndo + 1)*sizeof(AG_EditableRevision)); rev = &ed->undo[ed->nUndo++]; InitRevision(rev); return (rev); } /* Abort the current undo level. */ void AG_EditableAbortRevision(AG_Editable *ed) { AG_EditableRevision *rev = &ed->undo[ed->nUndo - 1]; #ifdef AG_DEBUG if (ed->nUndo == 0) AG_FatalError("AbortRevision"); #endif if (rev->nCharsRemoved > 0) { free(rev->s); } rev->s = NULL; rev->nCharsAdded = 0; rev->nCharsRemoved = 0; } /* * Commit the current revision and start a new one. */ void AG_EditableCommitRevision(AG_Editable *ed, AG_EditableBuffer *buf) { AG_EditableRevision *rev; if (ed->nUndo == 0) { AG_EditableBeginRevision(ed, buf); } rev = &ed->undo[ed->nUndo - 1]; if (rev->nCharsAdded == 0 && rev->nCharsRemoved == 0) { ed->nUndo--; /* No changes */ } else { if (!(ed->flags & AG_EDITABLE_EXCL)) { rev->crc32 = AG_GetCRC32(buf->s, (buf->len << 2)); rev->lenBuffer = buf->len; #ifdef DEBUG_UNDO Debug(ed, "COMMIT Undo Rev#%d (crc=%x, lenBuffer=%u)\n", ed->nUndo - 1, rev->crc32, rev->lenBuffer); #endif } #ifdef DEBUG_UNDO else { Debug(ed, "COMMIT Undo Rev#%d\n", ed->nUndo - 1); } #endif } } /* * Revert the changes effected by the given revision. * The Editable must be locked. */ void AG_EditableRevert(AG_Editable *ed, AG_EditableBuffer *buf, AG_EditableRevision *rev, AG_EditableRevision *undoRedo, Uint nUndoRedo) { const int nCharsAdded = rev->nCharsAdded; const int nCharsRemoved = rev->nCharsRemoved; const int posStart = rev->posStart, posEnd = rev->posEnd; const int len = buf->len; AG_EditableRevision *revUR; #ifdef DEBUG_UNDO Debug(ed, "REVERT (%d,%d, +%d,-%d)\n", posStart, posEnd, nCharsAdded, rev->nCharsRemoved); #endif if (nCharsAdded > 0) { revUR = &undoRedo[nUndoRedo - 1]; revUR->posStart = posStart; revUR->posEnd = posEnd; revUR->nCharsRemoved = nCharsAdded; revUR->s = Malloc((nCharsAdded + 1)*sizeof(AG_Char)); memcpy(revUR->s, &buf->s[posStart], nCharsAdded*sizeof(AG_Char)); revUR->s[nCharsAdded] = '\0'; if (!(ed->flags & AG_EDITABLE_EXCL)) { revUR->lenBuffer = rev->lenBuffer; revUR->crc32 = rev->crc32; } if (posStart == (len - 1)) { buf->s[len - nCharsAdded] = '\0'; } else if (posStart < posEnd) { memmove(&buf->s[posStart], &buf->s[posEnd], (len - nCharsAdded - posStart + 1)*sizeof(AG_Char)); } buf->len -= nCharsAdded; ed->pos = posStart; } else if (nCharsRemoved > 0) { if (AG_EditableGrowBuffer(ed, buf, rev->s, nCharsRemoved) == -1) { Verbose("AG_EditableGrowBuffer: %s\n", AG_GetError()); return; } revUR = &undoRedo[nUndoRedo - 1]; revUR->posStart = posStart; revUR->posEnd = posEnd; revUR->nCharsAdded = nCharsRemoved; if (!(ed->flags & AG_EDITABLE_EXCL)) { revUR->lenBuffer = rev->lenBuffer; revUR->crc32 = rev->crc32; } if (posStart < len) { memmove(&buf->s[posStart + nCharsRemoved], &buf->s[posStart], (buf->len - posStart)*sizeof(AG_Char)); } memcpy(&buf->s[posStart], rev->s, nCharsRemoved*sizeof(AG_Char)); buf->len += nCharsRemoved; buf->s[buf->len] = '\0'; ed->pos = posStart + nCharsRemoved; } ed->xScrollTo = &ed->xCurs; ed->yScrollTo = &ed->yCurs; } /* Undo last action. */ void AG_EditableUndo(AG_Editable *ed, AG_EditableBuffer *buf) { AG_EditableRevision *revUndo, *revRedo; if (ed->nUndo < 1) { return; } #ifdef DEBUG_UNDO Debug(ed, "UNDO (Rev#%d)\n", ed->nUndo - 1); #endif revUndo = &ed->undo[ed->nUndo - 1]; if (!(ed->flags & AG_EDITABLE_EXCL)) { Uint32 crc; crc = AG_GetCRC32(buf->s, (buf->len << 2)); if (revUndo->lenBuffer != buf->len || revUndo->crc32 != crc) { Debug(ed, "Buffer changed externally (len %u->%lu, crc %x->%x). " "Clearing history.\n", revUndo->lenBuffer, (Ulong)buf->len, revUndo->crc32, crc); AG_EditableClearHistory(ed); return; } } /* Record changes made by AG_EditableRevert() onto the Redo stack. */ ed->redo = Realloc(ed->redo, (ed->nRedo + 1)*sizeof(AG_EditableRevision)); revRedo = &ed->redo[ed->nRedo++]; InitRevision(revRedo); AG_EditableRevert(ed, buf, revUndo, ed->redo, ed->nRedo); if (revRedo->nCharsAdded == 0 && revRedo->nCharsRemoved == 0) { ed->nRedo--; /* No changes */ } else { ed->selStart = 0; /* Clear selection */ ed->selEnd = 0; } FreeRevision(revUndo); ed->nUndo--; } /* Redo last undone action. */ void AG_EditableRedo(AG_Editable *ed, AG_EditableBuffer *buf) { AG_EditableRevision *revRedo, *revUndo; if (ed->nRedo < 1) return; #ifdef DEBUG_UNDO Debug(ed, "REDO (Undo Rev#%d, Redo Rev#%d)\n", ed->nUndo - 1, ed->nRedo - 1); #endif revRedo = &ed->redo[ed->nRedo - 1]; /* Record changes made by AG_EditableRevert() back onto the Undo stack. */ ed->undo = Realloc(ed->undo, (ed->nUndo + 1)*sizeof(AG_EditableRevision)); revUndo = &ed->undo[ed->nUndo++]; InitRevision(revUndo); AG_EditableRevert(ed, buf, revRedo, ed->undo, ed->nUndo); if (revUndo->nCharsAdded == 0 && revUndo->nCharsRemoved == 0) { ed->nUndo--; /* No changes */ } else { ed->selStart = 0; /* Clear selection */ ed->selEnd = 0; } FreeRevision(revRedo); ed->nRedo--; } /* * Perform Cut action on buffer. * Return 1 = buffer changed or 0 = copy failed or no change. */ int AG_EditableCut(AG_Editable *ed, AG_EditableBuffer *buf, AG_EditableClipboard *cb, int internalOnly) { AG_Size selLen; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); selLen = (ed->selEnd - ed->selStart); if (AGEDITABLE_IS_READONLY(ed) || selLen <= 0) { goto no_change; } AG_EditableValidateSelection(ed, buf); if (AG_EditableCopy(ed, buf, cb, internalOnly)) { AG_EditableDelete(ed, buf); } AG_ObjectUnlock(ed); return (1); no_change: AG_ObjectUnlock(ed); return (1); } /* * Perform Copy action against clipboard (internal or external). * Return 1 = copy successful or 0 = copy failed or selection empty. */ int AG_EditableCopy(AG_Editable *ed, AG_EditableBuffer *buf, AG_EditableClipboard *cb, int internalOnly) { AG_Driver *drv = WIDGET(ed)->drv; const AG_DriverClass *drvOps = AGDRIVER_CLASS(drv); AG_Size selLen; const int selStart = ed->selStart; const int selEnd = ed->selEnd; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); AG_EditableValidateSelection(ed, buf); if ((selLen = (selEnd - selStart)) <= 0) goto no_change; if (internalOnly || !agClipboardIntegration || /* Internal clipboard */ drvOps->setClipboardText == NULL) { AG_EditableCopyChunk(ed, cb, &buf->s[selStart], selLen); } else { /* Integrated clipboard */ char *s; #ifdef AG_UNICODE AG_Char cEndSave; AG_Size utf8len; cEndSave = buf->s[selEnd]; buf->s[selEnd] = '\0'; if (AG_LengthUTF8FromUCS4(&buf->s[selStart], &utf8len) == -1) { buf->s[selEnd] = cEndSave; goto no_change; } utf8len++; if ((s = TryMalloc(utf8len)) == NULL) { Verbose(_("Out of memory for Copy (%lu bytes)\n"), (Ulong)utf8len); buf->s[selEnd] = cEndSave; goto no_change; } if (AG_TextExportUnicode_StripANSI("UTF-8", s, &buf->s[selStart], utf8len) == -1) { buf->s[selEnd] = cEndSave; Verbose(_("Copy failed (%s)\n"), AG_GetError()); goto no_change; } buf->s[selEnd] = cEndSave; #else if ((s = TryMalloc(selLen+1)) == NULL) { Verbose(_("Out of memory for Copy (%lu bytes)\n"), selLen+1); goto no_change; } memcpy(s, &buf->s[selStart], selLen); s[selLen] = '\0'; #endif /* !AG_UNICODE */ if (drvOps->setClipboardText(drv, s) == -1) { free(s); goto no_change; } free(s); } AG_ObjectUnlock(ed); return (1); no_change: AG_ObjectUnlock(ed); return (0); } /* * Perform Paste action from clipboard (internal or external). * Return 1 = buffer changed (paste ok) or 0 = paste failed or empty. */ int AG_EditablePaste(AG_Editable *ed, AG_EditableBuffer *buf, AG_EditableClipboard *cb, int internalOnly) { AG_Driver *drv = WIDGET(ed)->drv; const AG_DriverClass *drvOps = AGDRIVER_CLASS(drv); AG_EditableRevision *rev; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (AGEDITABLE_IS_READONLY(ed)) goto no_change; if (ed->selEnd > ed->selStart) AG_EditableDelete(ed, buf); if (!internalOnly && agClipboardIntegration && drvOps->getClipboardText != NULL) { /* Integrated clipboard */ char *s, *c; AG_Char *ucs; AG_Size ucsLen; const int pos = ed->pos; if ((s = drvOps->getClipboardText(drv)) == NULL) { goto out; } if (!(ed->flags & AG_EDITABLE_MULTILINE) && (c = strchr(s, '\n')) != NULL) { *c = '\0'; } #ifdef DEBUG_CLIPBOARD Debug(drv, "GetClipboardText() -> \"%s\"\n", s); #endif if (ed->flags & AG_EDITABLE_INT_ONLY) { for (c = &s[0]; *c != '\0'; c++) { if (!CharIsIntOnly(*c)) { AG_SetError(_("Non-integer input near `%c'"), (char)*c); goto fail; } } } else if (ed->flags & AG_EDITABLE_FLT_ONLY) { if (strcmp(s, AGSI_INFINITY) != 0 && strcmp(s, "-" AGSI_INFINITY) != 0) { for (c = &s[0]; *c != '\0'; c++) { if (!CharIsFltOnly(*c)) { AG_SetError(_("Non-float input near `%c'"), (char)*c); goto fail; } } } } if ((ucs = AG_ImportUnicode("UTF-8", s, &ucsLen, NULL)) == NULL) { goto fail; } if (AG_EditableGrowBuffer(ed, buf, ucs, ucsLen) == -1) { free(ucs); goto fail; } rev = AG_EditableBeginRevision(ed, buf); rev->posStart = pos; rev->posEnd = pos + ucsLen; rev->nCharsAdded = ucsLen; if (pos < buf->len) { memmove(&buf->s[pos + ucsLen], &buf->s[pos], (buf->len - pos)*sizeof(AG_Char)); } memcpy(&buf->s[pos], ucs, ucsLen*sizeof(AG_Char)); buf->len += ucsLen; buf->s[buf->len] = '\0'; ed->pos += ucsLen; AG_EditableCommitRevision(ed, buf); free(ucs); free(s); } else { /* Internal clipboard */ AG_Char *c; const int pos = ed->pos; AG_MutexLock(&cb->lock); if (cb->s == NULL) { AG_MutexUnlock(&cb->lock); goto out; } if (!(ed->flags & AG_EDITABLE_MULTILINE)) { for (c = &cb->s[0]; *c != '\0'; c++) { if (*c == '\n') { *c = '\0'; break; } } } if (ed->flags & AG_EDITABLE_INT_ONLY) { for (c = &cb->s[0]; *c != '\0'; c++) { if (!CharIsIntOnly(*c)) { AG_SetError(_("Non-integer input near `%c'"), (char)*c); AG_MutexUnlock(&cb->lock); goto fail; } } } else if (ed->flags & AG_EDITABLE_FLT_ONLY) { for (c = &cb->s[0]; *c != '\0'; c++) { if (!CharIsFltOnly(*c)) { AG_SetError(_("Non-float input near `%c'"), (char)*c); AG_MutexUnlock(&cb->lock); goto fail; } } } if (AG_EditableGrowBuffer(ed, buf, cb->s, cb->len) == -1) { AG_MutexUnlock(&cb->lock); goto fail; } rev = AG_EditableBeginRevision(ed, buf); rev->posStart = pos; rev->posEnd = pos + cb->len; rev->nCharsAdded = cb->len; if (pos < buf->len) { memmove(&buf->s[rev->posEnd], &buf->s[pos], (buf->len - pos)*sizeof(AG_Char)); } memcpy(&buf->s[pos], cb->s, cb->len * sizeof(AG_Char)); buf->len += cb->len; buf->s[buf->len] = '\0'; ed->pos += cb->len; AG_EditableCommitRevision(ed, buf); AG_MutexUnlock(&cb->lock); } out: ed->xScrollTo = &ed->xCurs; ed->yScrollTo = &ed->yCurs; AG_ObjectUnlock(ed); return (1); fail: Verbose(_("Paste Failed (%s)\n"), AG_GetError()); no_change: AG_ObjectUnlock(ed); return (0); } /* * Delete the current selection. * Return 1 = buffer changed (delete OK) or 0 = delete failed or empty. */ int AG_EditableDelete(AG_Editable *ed, AG_EditableBuffer *buf) { AG_EditableRevision *rev; AG_Size selLen; int selStart, selEnd; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); selStart = ed->selStart; selEnd = ed->selEnd; selLen = (selEnd - selStart); if (AGEDITABLE_IS_READONLY(ed) || selLen <= 0) goto no_change; AG_EditableValidateSelection(ed, buf); if (selStart == selEnd) goto no_change; rev = AG_EditableBeginRevision(ed, buf); rev->posStart = selStart; rev->posEnd = selEnd; rev->nCharsRemoved = selLen; rev->s = Malloc((selLen + 1)*sizeof(AG_Char)); memcpy(rev->s, &buf->s[selStart], selLen*sizeof(AG_Char)); rev->s[selLen] = '\0'; if (ed->selEnd == buf->len) { buf->s[selStart] = '\0'; } else { memmove(&buf->s[selStart], &buf->s[ed->selEnd], (buf->len - selLen + 1 - selStart)*sizeof(AG_Char)); } buf->len -= selLen; ed->pos = selStart; ed->selStart = 0; ed->selEnd = 0; ed->xScrollTo = &ed->xCurs; ed->yScrollTo = &ed->yCurs; AG_EditableCommitRevision(ed, buf); AG_ObjectUnlock(ed); return (1); no_change: AG_ObjectUnlock(ed); return (0); } /* Perform "Select All" on buffer. */ void AG_EditableSelectAll(AG_Editable *ed, AG_EditableBuffer *buf) { AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); /* ed->pos = 0; */ ed->selStart = 0; ed->selEnd = buf->len; AG_Redraw(ed); AG_ObjectUnlock(ed); } /* Undo via contextual menu. */ static void MenuUndo(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { AG_EditableUndo(ed, buf); CommitBuffer(ed, buf); ReleaseBuffer(ed, buf); } } static void MenuUndoActive(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); int *enable = AG_PTR(2); *enable = (!AGEDITABLE_IS_READONLY(ed) && (ed->nUndo > 0)); } /* Redo via contextual menu. */ static void MenuRedo(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { AG_EditableRedo(ed, buf); CommitBuffer(ed, buf); ReleaseBuffer(ed, buf); } } static void MenuRedoActive(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); int *enable = AG_PTR(2); *enable = (!AGEDITABLE_IS_READONLY(ed) && (ed->nRedo > 0)); } /* Undo via contextual menu */ static void MenuCut(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { if (AG_EditableCut(ed, buf, &agEditableClipbrd, 0)) { CommitBuffer(ed, buf); } ReleaseBuffer(ed, buf); } } static void MenuCutActive(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); int *enable = AG_PTR(2); *enable = (!AGEDITABLE_IS_READONLY(ed) && (ed->selEnd > ed->selStart)); } static void MenuCopy(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { AG_EditableCopy(ed, buf, &agEditableClipbrd, 0); ReleaseBuffer(ed, buf); } } static void MenuCopyActive(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); int *enable = AG_PTR(2); *enable = (ed->selEnd > ed->selStart); } static void MenuPaste(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { if (AG_EditablePaste(ed, buf, &agEditableClipbrd, 0)) { CommitBuffer(ed, buf); } ReleaseBuffer(ed, buf); } } static void MenuPasteActive(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); int *enable = AG_PTR(2); #if 1 *enable = (!AGEDITABLE_IS_READONLY(ed) && (agClipboardIntegration || (agEditableClipbrd.len > 0))); #else AG_Driver *drv = WIDGET(ed)->drv; const AG_DriverClass *drvOps = AGDRIVER_CLASS(drv); char *s; if (AGEDITABLE_IS_READONLY(ed)) { *enable = 0; return; } if (!agClipboardIntegration) { *enable = (agEditableClipbrd.len > 0); } else { if (drvOps->getClipboardText == NULL) { *enable = 1; return; } if ((s = drvOps->getClipboardText(drv)) != NULL) { *enable = !( (s[0] == '\0' || (s[0] == ' ' && s[1] == '\0')) ); free(s); } else { *enable = 1; } } #endif } static void MenuDelete(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { if (AG_EditableDelete(ed, buf)) { CommitBuffer(ed, buf); } ReleaseBuffer(ed, buf); } } static void MenuDeleteActive(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); int *enable = AG_PTR(2); *enable = (!AGEDITABLE_IS_READONLY(ed) && (ed->selEnd > ed->selStart)); } static void MenuSelectAll(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_EditableBuffer *buf; if ((buf = GetBuffer(ed)) != NULL) { AG_EditableSelectAll(ed, buf); ReleaseBuffer(ed, buf); } } #ifdef AG_DEBUG static void PollHistoryBuffer(AG_Event *_Nonnull event) { AG_Tlist *tl = AG_TLIST_SELF(); AG_Editable *ed = AG_EDITABLE_PTR(1); AG_TlistItem *it; AG_EditableRevision *rev; Uint i; AG_TlistClear(tl); AG_ObjectLock(ed); for (i = 0; i < ed->nUndo; i++) { rev = &ed->undo[i]; it = AG_TlistAdd(tl, agIconDown.s, _("%s" "Undo Level %d ([%d,%d] +%d -%d) crc(%x,%d)"), (i == ed->nUndo - 1) ? ">" : "", i, rev->posStart, rev->posEnd, rev->nCharsAdded, rev->nCharsRemoved, rev->crc32, rev->lenBuffer); it->depth = 0; it->p1 = rev; } if (ed->nUndo == 0) { it = AG_TlistAddS(tl, NULL, _("(Empty undo buffer)")); it->flags |= AG_TLIST_NO_SELECT; } for (i = 0; i < ed->nRedo; i++) { rev = &ed->redo[i]; it = AG_TlistAdd(tl, agIconUp.s, _("%s" "Redo Level %d ([%d,%d] +%d -%d) crc(%x,%d)"), (i == ed->nRedo - 1) ? ">" : "", i, rev->posStart, rev->posEnd, rev->nCharsAdded, rev->nCharsRemoved, rev->crc32, rev->lenBuffer); it->depth = 0; it->p1 = rev; } if (ed->nRedo == 0) { it = AG_TlistAddS(tl, NULL, _("(Empty redo buffer)")); it->flags |= AG_TLIST_NO_SELECT; } AG_TlistRestore(tl); AG_ObjectUnlock(ed); } static void MenuHistoryBuffer(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); AG_Window *win, *winParent; AG_Tlist *tl; if ((win = AG_WindowNew(0)) == NULL) return; tl = AG_TlistNew(win, AG_TLIST_POLL | AG_TLIST_EXPAND); AG_TlistSetRefresh(tl, 125); AG_TlistSizeHint(tl, "", 25); AG_SetEvent(tl, "tlist-poll", PollHistoryBuffer,"%p",ed); AG_WindowSetCaption(win, _("%s - History Buffer"), OBJECT(ed)->name); AG_WindowSetPosition(win, AG_WINDOW_MIDDLE_RIGHT, 0); if ((winParent = WIDGET(ed)->window) != NULL) { AG_WindowAttach(winParent, win); } AG_WindowShow(win); } #endif /* AG_DEBUG */ #ifdef AG_UNICODE static void MenuSetLang(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_PTR(1); const enum ag_language lang = (enum ag_language)AG_INT(2); AG_EditableSetLang(ed, lang); } #endif static AG_PopupMenu *_Nullable PopupMenu(AG_Editable *_Nonnull ed) { AG_PopupMenu *pm; AG_MenuItem *mi; #ifdef AG_UNICODE AG_Variable *vText; AG_TextElement *txt; #endif if ((pm = AG_PopupNew(ed)) == NULL) return (NULL); AG_MenuSeparator(pm->root); mi = AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_UNDO AGSI_RST " Undo"), NULL, AG_KEY_Z, AGSI_WINCMD, MenuUndo,"%p",ed); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuUndoActive,"%p",ed); mi = AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_REDO AGSI_RST " Redo"), NULL, AG_KEY_Y, AGSI_WINCMD, MenuRedo,"%p",ed); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuRedoActive,"%p",ed); #ifdef AG_DEBUG AG_MenuAction(pm->root, _(AGSI_IDEOGRAM AGSI_CLIPBOARD AGSI_RST " History Buffer..."), NULL, MenuHistoryBuffer,"%p",ed); #endif AG_MenuSeparator(pm->root); mi = AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_CUT AGSI_RST " Cut"), NULL, AG_KEY_X, AGSI_WINCMD, MenuCut,"%p",ed); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuCutActive,"%p",ed); mi = AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_COPY AGSI_RST " Copy"), NULL, AG_KEY_C, AGSI_WINCMD, MenuCopy,"%p",ed); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuCopyActive,"%p",ed); mi = AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_PASTE AGSI_RST " Paste"), NULL, AG_KEY_V, AGSI_WINCMD, MenuPaste,"%p",ed); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuPasteActive,"%p",ed); mi = AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_CLOSE_X AGSI_RST " Delete"), NULL, AG_KEY_K, AGSI_WINCMD, MenuDelete,"%p",ed); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuDeleteActive,"%p",ed); AG_MenuSeparator(pm->root); AG_MenuActionKb(pm->root, _(AGSI_IDEOGRAM AGSI_SELECT_ALL AGSI_RST " Select All"), NULL, AG_KEY_A, AGSI_WINCMD, MenuSelectAll,"%p",ed); #ifdef AG_UNICODE if ((ed->flags & AG_EDITABLE_MULTILINGUAL) && AG_Defined(ed, "text") && (vText = AG_GetVariable(ed, "text", (void *)&txt)) != NULL) { AG_MenuItem *mLang; int i; AG_MenuSeparator(pm->root); mLang = AG_MenuNode(pm->root, _(AGSI_IDEOGRAM AGSI_USER_W_3_SOUND_WAVES AGSI_RST " Select Language"), NULL); for (i = 0; i < AG_LANG_LAST; i++) { AG_TextEnt *te = &txt->ent[i]; if (te->len == 0) continue; AG_MenuAction(mLang, _(agLanguageNames[i]), NULL, MenuSetLang, "%p,%i", ed, i); } AG_UnlockVariable(vText); } #endif /* AG_UNICODE */ return (pm); } /* Timer for detecting double clicks. */ static Uint32 DoubleClickTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); ed->selDblClick = -1; return (0); } static void MouseButtonDown(void *obj, AG_MouseButton button, int x, int y) { AG_Editable *ed = obj; AG_EditableBuffer *buf; if (!AG_WidgetIsFocused(ed)) AG_WidgetFocus(ed); x -= WIDGET(ed)->paddingLeft; y -= WIDGET(ed)->paddingTop; switch (button) { case AG_MOUSE_LEFT: if (ed->pm != NULL) { AG_PopupHide(ed->pm); } ed->flags |= AG_EDITABLE_CURSOR_MOVING | AG_EDITABLE_BLINK_ON; if ((buf = GetBuffer(ed)) == NULL) { goto out; } if (AG_EditableMapPosition(ed, buf, (ed->x + x), y, &ed->pos) == 0) { ed->selStart = 0; ed->selEnd = 0; AG_Redraw(ed); } ReleaseBuffer(ed, buf); ed->flags |= AG_EDITABLE_MARKPREF; if (ed->selDblClick != -1 && abs(ed->selDblClick - ed->pos) <= 1) { DoubleClick(ed); } else { ed->selDblClick = ed->pos; AG_AddTimer(ed, &ed->toDblClick, agMouseDblclickDelay, DoubleClickTimeout, NULL); } break; case AG_MOUSE_RIGHT: if ((ed->flags & AG_EDITABLE_NOPOPUP) == 0) { if (ed->pm != NULL) { AG_PopupShowAt(ed->pm, x,y); } else { if ((ed->pm = PopupMenu(ed)) != NULL) AG_PopupShowAt(ed->pm, x,y); } } break; case AG_MOUSE_WHEELUP: if (ed->flags & AG_EDITABLE_MULTILINE) { if ((AG_GetModState(ed) & AG_KEYMOD_SHIFT) && (ed->flags & AG_EDITABLE_WORDWRAP) == 0) { ed->x -= ed->fontMaxHeight; if (ed->x < 0) ed->x = 0; } else { ed->y -= ed->lineScrollAmount; if (ed->y < 0) { ed->y = 0; } } } break; case AG_MOUSE_WHEELDOWN: if (ed->flags & AG_EDITABLE_MULTILINE) { if ((AG_GetModState(ed) & AG_KEYMOD_SHIFT) && (ed->flags & AG_EDITABLE_WORDWRAP) == 0) { ed->x += ed->fontMaxHeight; if (ed->x > ed->xMax - WIDTH(ed)) ed->x = ed->xMax - WIDTH(ed); } else { ed->y += ed->lineScrollAmount; ed->y = MIN(ed->y, ed->yMax - ed->yVis); } } break; default: break; } out: AG_Redraw(ed); } static void MouseButtonUp(void *obj, AG_MouseButton button, int x, int y) { AG_Editable *ed = obj; x -= WIDGET(ed)->paddingLeft; y -= WIDGET(ed)->paddingTop; if (button != AG_MOUSE_LEFT) return; ed->flags &= ~(AG_EDITABLE_WORDSELECT); if ((ed->flags & AG_EDITABLE_CURSOR_MOVING)) { AG_EditableBuffer *buf; ed->flags &= ~(AG_EDITABLE_CURSOR_MOVING); if ((buf = GetBuffer(ed)) == NULL) { return; } AG_EditableMapPosition(ed, buf, ed->x + x, y, &ed->pos); ReleaseBuffer(ed, buf); ed->flags |= AG_EDITABLE_MARKPREF; } AG_Redraw(ed); } static void MouseMotion(void *obj, int x, int y, int dx, int dy) { AG_Editable *ed = obj; AG_EditableBuffer *buf; int newPos; x -= WIDGET(ed)->paddingLeft; y -= WIDGET(ed)->paddingTop; if (!AG_WidgetIsFocused(ed) || (ed->flags & AG_EDITABLE_CURSOR_MOVING) == 0) return; if ((buf = GetBuffer(ed)) == NULL) return; if (AG_EditableMapPosition(ed, buf, ed->x + x, y, &newPos) == -1 || newPos == ed->pos) goto out; if (ed->flags & AG_EDITABLE_WORDSELECT) { /* Word selection */ const AG_Char *c, *cEnd = &buf->s[buf->len]; c = &buf->s[newPos]; if (newPos > ed->pos) { ed->selStart = ed->pos; for (c = &buf->s[ed->pos]; c >= &buf->s[0]; c--) { if (*c == '\n' || AG_CharIsSpace(*c)) { break; } ed->selStart--; } ed->selStart++; ed->selEnd = newPos; for (c = &buf->s[newPos]; c <= cEnd; c++) { if (*c == '\n' || AG_CharIsSpace(*c)) { break; } ed->selEnd++; } ed->xScrollTo = &ed->xSelEnd; ed->yScrollTo = &ed->ySelEnd; } else if (newPos < ed->pos) { ed->selStart = newPos; for (c = &buf->s[newPos]; c >= &buf->s[0]; c--) { if (*c == '\n' || AG_CharIsSpace(*c)) { break; } ed->selStart--; } ed->selStart++; ed->selEnd = ed->pos; for (c = &buf->s[ed->pos]; c < cEnd; c++) { if (*c == '\n' || AG_CharIsSpace(*c)) { break; } ed->selEnd++; } ed->xScrollTo = &ed->xSelStart; ed->yScrollTo = &ed->ySelStart; } } else { /* Character selection */ if (newPos > ed->pos) { ed->selStart = ed->pos; ed->selEnd = newPos; ed->xScrollTo = &ed->xSelEnd; ed->yScrollTo = &ed->ySelEnd; } else if (newPos <= ed->pos) { ed->selStart = newPos; ed->selEnd = ed->pos; ed->xScrollTo = &ed->xSelStart; ed->yScrollTo = &ed->ySelStart; } } AG_Redraw(ed); out: ReleaseBuffer(ed, buf); } /* Clear the buffer contents. */ void AG_EditableClearString(AG_Editable *ed) { AG_EditableSetString(ed, NULL); } /* * Replace the buffer contents with a formatted string. * Cancel any selection and move the cursor to the end. * If fmt is NULL, replace with empty string (""). */ void AG_EditablePrintf(AG_Editable *ed, const char *fmt, ...) { va_list ap; char *s; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); AG_EditableSetString(ed, s); free(s); } /* * Replace the buffer contents with a given string. * Cancel any selection and move the cursor to the end. * If text is NULL, replace with empty string (""). */ void AG_EditableSetString(AG_Editable *ed, const char *text) { AG_EditableBuffer *buf; AG_Char *sNew; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if ((buf = GetBuffer(ed)) == NULL) { goto out; } if (text != NULL) { #ifdef AG_UNICODE if ((sNew = AG_ImportUnicode("UTF-8", text, &buf->len, &buf->maxLen)) == NULL) goto out; #else if ((sNew = (Uint8 *)TryStrdup(text)) == NULL) { goto out; } buf->maxLen = buf->len = strlen(text); #endif Free(buf->s); buf->s = sNew; ed->pos = buf->len; } else { /* Empty string */ if ((sNew = TryMalloc(sizeof(AG_Char))) == NULL) { goto out; } sNew[0] = '\0'; Free(buf->s); buf->s = sNew; ed->pos = 0; buf->len = 0; } ed->selStart = 0; ed->selEnd = 0; CommitBuffer(ed, buf); ReleaseBuffer(ed, buf); out: AG_Redraw(ed); AG_ObjectUnlock(ed); } /* Return a duplicate of the buffer contents. */ char * AG_EditableDupString(AG_Editable *ed) { AG_Variable *V; char *sDup, *s; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (AG_Defined(ed, "text")) { AG_TextElement *txt; V = AG_GetVariable(ed, "text", (void *)&txt); s = txt->ent[ed->lang].buf; sDup = TryStrdup((s != NULL) ? s : ""); } else { V = AG_GetVariable(ed, "string", (void *)&s); sDup = TryStrdup(s); } AG_UnlockVariable(V); AG_ObjectUnlock(ed); return (sDup); } /* Copy buffer contents to a fixed-size buffer (and NUL-terminate always). */ AG_Size AG_EditableCopyString(AG_Editable *ed, char *dst, AG_Size dst_size) { AG_Variable *V; AG_Size rv; char *s; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (AG_Defined(ed, "text")) { AG_TextElement *txt; V = AG_GetVariable(ed, "text", (void *)&txt); s = txt->ent[ed->lang].buf; rv = Strlcpy(dst, (s != NULL) ? s : "", dst_size); } else { V = AG_GetVariable(ed, "string", (void *)&s); rv = Strlcpy(dst, s, dst_size); } AG_UnlockVariable(V); AG_ObjectUnlock(ed); return (rv); } /* Append a string to the contents of the text buffer. */ int AG_EditableCatString(AG_Editable *ed, const char *fmt, ...) { char *s; va_list ap; int rv; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); rv = AG_EditableCatStringS(ed, s); free(s); return (rv); } /* Append a string to the end of the text buffer. */ int AG_EditableCatStringS(AG_Editable *ed, const char *s) { AG_Variable *V; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if (AG_Defined(ed, "text")) { AG_TextElement *txt; AG_Language langSave; V = AG_GetVariable(ed, "text", (void *)&txt); langSave = txt->lang; txt->lang = ed->lang; if (AG_TextCatS(txt, s) == -1) { /* XXX pos */ txt->lang = langSave; goto fail; } txt->lang = langSave; } else { AG_EditableBuffer *buf; AG_Char *ucs; char *dst; AG_Size ucsLen; V = AG_GetVariable(ed, "string", (void *)&dst); if ((ucs = AG_ImportUnicode(ed->encoding, s, &ucsLen, NULL)) == NULL) { goto fail; } if ((buf = GetBuffer(ed)) == NULL) { free(ucs); goto fail; } if (AG_EditableGrowBuffer(ed, buf, ucs, ucsLen) == -1) { ReleaseBuffer(ed, buf); free(ucs); goto fail; } memcpy(&buf->s[buf->len], ucs, ucsLen*sizeof(AG_Char)); buf->len += ucsLen; ed->pos += ucsLen; CommitBuffer(ed, buf); ReleaseBuffer(ed, buf); free(ucs); } AG_UnlockVariable(V); AG_Redraw(ed); AG_ObjectUnlock(ed); return (0); fail: AG_UnlockVariable(V); AG_ObjectUnlock(ed); return (-1); } /* Convert buffer contents to an integer value and return it. */ int AG_EditableInt(AG_Editable *ed) { char abuf[32]; AG_EditableBuffer *buf; int i; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if ((buf = GetBuffer(ed)) == NULL) AG_FatalError(NULL); #ifdef AG_UNICODE AG_ExportUnicode("UTF-8", abuf, buf->s, sizeof(abuf)); #else Strlcpy(abuf, (const char *)buf->s, sizeof(abuf)); #endif i = atoi(abuf); ReleaseBuffer(ed, buf); AG_ObjectUnlock(ed); return (i); } /* Convert buffer contents to a single-precision float and return it. */ float AG_EditableFlt(AG_Editable *ed) { char abuf[32]; AG_EditableBuffer *buf; float flt; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if ((buf = GetBuffer(ed)) == NULL) AG_FatalError(NULL); #ifdef AG_UNICODE AG_ExportUnicode("UTF-8", abuf, buf->s, sizeof(abuf)); #else Strlcpy(abuf, (const char *)buf->s, sizeof(abuf)); #endif flt = (float)strtod(abuf, NULL); ReleaseBuffer(ed, buf); AG_ObjectUnlock(ed); return (flt); } /* Convert buffer contents to a double and return it. */ double AG_EditableDbl(AG_Editable *ed) { char abuf[32]; AG_EditableBuffer *buf; double flt; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); if ((buf = GetBuffer(ed)) == NULL) AG_FatalError(NULL); #ifdef AG_UNICODE AG_ExportUnicode("UTF-8", abuf, buf->s, sizeof(abuf)); #else Strlcpy(abuf, (const char *)buf->s, sizeof(abuf)); #endif flt = strtod(abuf, NULL); ReleaseBuffer(ed, buf); AG_ObjectUnlock(ed); return (flt); } static void OnBindingChange(AG_Event *_Nonnull event) { AG_Editable *ed = AG_EDITABLE_SELF(); const AG_Variable *binding = AG_PTR(1); AG_EditableClearHistory(ed); /* * "string" and "text" bindings are mutually exclusive. */ if (strcmp(binding->name, "string") == 0) { AG_Unset(ed, "text"); } else if (strcmp(binding->name, "text") == 0) { AG_Unset(ed, "string"); } } static void Init(void *_Nonnull obj) { AG_Editable *ed = obj; WIDGET(ed)->flags |= AG_WIDGET_FOCUSABLE | AG_WIDGET_UNFOCUSED_MOTION | AG_WIDGET_USE_TEXT | AG_WIDGET_USE_MOUSEOVER; ed->flags = AG_EDITABLE_BLINK_ON | AG_EDITABLE_MARKPREF; ed->lineScrollAmount = 5; #ifdef AG_UNICODE ed->encoding = "UTF-8"; #else ed->encoding = "US-ASCII"; #endif ed->text[0] = '\0'; ed->hPre = 1; memset(&ed->wPre, 0, sizeof(int) + /* wPre */ sizeof(int) + /* pos */ sizeof(int) + /* selStart */ sizeof(int) + /* selEnd */ sizeof(AG_Char) + /* compose */ sizeof(int) + /* xCurs */ sizeof(int) + /* yCurs */ sizeof(int) + /* xSelStart */ sizeof(int) + /* ySelStart */ sizeof(int) + /* xSelEnd */ sizeof(int) + /* ySelEnd */ sizeof(int) + /* xCursPref */ sizeof(int) + /* x */ sizeof(int) + /* xMax */ sizeof(int) + /* y */ sizeof(int) + /* yMax */ sizeof(int) + /* yVis */ sizeof(int) + /* posKbdSel */ sizeof(Uint32) + /* _pad */ sizeof(AG_EditableBuffer) + /* sBuf */ sizeof(AG_Rect) + /* r */ sizeof(AG_CursorArea *) + /* ca */ sizeof(enum ag_language) + /* lang */ sizeof(int) + /* xScrollPx */ sizeof(AG_PopupMenu *) + /* pm */ sizeof(int *) + /* xScrollTo */ sizeof(int *) + /* yScrollTo */ sizeof(AG_Autocomplete *)); /* complete */ ed->fontMaxHeight = agTextFontHeight; ed->lineSkip = agTextFontLineSkip; ed->suPlaceholder = -1; ed->selDblClick = -1; AG_InitTimer(&ed->toRepeat, "repeat", 0); AG_InitTimer(&ed->toRepeatDirs[0], "repeatU", 0); AG_InitTimer(&ed->toRepeatDirs[1], "repeatD", 0); AG_InitTimer(&ed->toRepeatDirs[2], "repeatL", 0); AG_InitTimer(&ed->toRepeatDirs[3], "repeatR", 0); AG_InitTimer(&ed->toCursorBlink, "cursorBlink", 0); AG_InitTimer(&ed->toDblClick, "dblClick", 0); ed->nUndo = 0; ed->nRedo = 0; ed->undo = Malloc(sizeof(AG_EditableRevision)); ed->redo = Malloc(sizeof(AG_EditableRevision)); AG_AddEvent(ed, "font-changed", OnFontChange, NULL); AG_AddEvent(ed, "widget-hidden", OnHide, NULL); AG_SetEvent(ed, "widget-gainfocus", OnFocusGain, NULL); AG_SetEvent(ed, "widget-lostfocus", OnFocusLoss, NULL); /* Default to a small, built-in text buffer. */ AG_BindString(ed, "string", ed->text, sizeof(ed->text)); AG_SetEvent(ed, "bound", OnBindingChange, NULL); OBJECT(ed)->flags |= AG_OBJECT_BOUND_EVENTS; } static void Destroy(void *_Nonnull obj) { AG_Editable *ed = obj; Uint i; if (ed->complete != NULL) { free(ed->complete); } if (ed->pm != NULL) { AG_PopupDestroy(ed->pm); } if (ed->flags & AG_EDITABLE_EXCL) Free(ed->sBuf.s); for (i = 0; i < ed->nUndo; i++) { FreeRevision(&ed->undo[i]); } for (i = 0; i < ed->nRedo; i++) { FreeRevision(&ed->redo[i]); } free(ed->undo); free(ed->redo); } /* Initialize/release the global clipboards. */ static void InitClipboard(AG_EditableClipboard *_Nonnull cb) { AG_MutexInit(&cb->lock); #ifdef AG_UNICODE Strlcpy(cb->encoding, "UTF-8", sizeof(cb->encoding)); #else Strlcpy(cb->encoding, "US-ASCII", sizeof(cb->encoding)); #endif cb->s = NULL; cb->len = 0; } static void FreeClipboard(AG_EditableClipboard *_Nonnull cb) { Free(cb->s); AG_MutexDestroy(&cb->lock); } void AG_EditableInitClipboards(void) { InitClipboard(&agEditableClipbrd); InitClipboard(&agEditableKillring); } void AG_EditableDestroyClipboards(void) { FreeClipboard(&agEditableClipbrd); FreeClipboard(&agEditableKillring); } /* Return current cursor position in text. */ int AG_EditableGetCursorPos(const AG_Editable *ed) { return (ed->pos); } /* Return 1 if the Editable is effectively read-only. */ int AG_EditableReadOnly(AG_Editable *_Nonnull ed) { int flag; AG_OBJECT_ISA(ed, "AG_Widget:AG_Editable:*"); AG_ObjectLock(ed); flag = AGEDITABLE_IS_READONLY(ed); AG_ObjectUnlock(ed); return (flag); } /* * Ensure that the cursor position and selection range lie within the size * of the given buffer; clamp or normalize them if necessary. * * The Editable and buffer must both be locked. */ void AG_EditableValidateSelection(AG_Editable *ed, const AG_EditableBuffer *buf) { if (ed->pos > (int)buf->len) { /* Allow one character past the end */ ed->pos = (int)buf->len; ed->selStart = 0; ed->selEnd = 0; } if (ed->selEnd < ed->selStart) { int temp = ed->selEnd; ed->selEnd = ed->selStart; ed->selStart = temp; } if (ed->selEnd > buf->len) { ed->selEnd = buf->len; if (ed->selEnd < ed->selStart) { ed->selStart = 0; ed->selEnd = 0; } } } static void *_Nullable Edit(void *_Nonnull obj) { static const AG_FlagDescr flagDescr[] = { { AG_EDITABLE_CURSOR_MOVING, N_("Cursor is moving"), 1 }, { AG_EDITABLE_WORDSELECT, N_("Word selection mode"), 1 }, { AG_EDITABLE_WORDWRAP, N_("Word wrapping"), 1 }, { AG_EDITABLE_PASSWORD, N_("Password entry"), 1 }, { AG_EDITABLE_INT_ONLY, N_("Integers only"), 1 }, { AG_EDITABLE_FLT_ONLY, N_("Real numbers only"), 1 }, { AG_EDITABLE_UPPERCASE, N_("Display in uppercase"), 1 }, { AG_EDITABLE_LOWERCASE, N_("Display in lowercase"), 1 }, { AG_EDITABLE_NO_KILL_YANK, N_("No emacs-style functions"), 1 }, { AG_EDITABLE_NO_ALT_LATIN1, N_("No latin1 key composition"), 1 }, { AG_EDITABLE_EXCL, N_("Exclusive buffer access"), 0 }, { AG_EDITABLE_MULTILINE, N_("Multiline mode"), 0 }, { AG_EDITABLE_MULTILINGUAL, N_("Multilingual field"), 0 }, { 0, NULL, 0 } }; AG_Editable *tgt = obj; AG_Box *box; box = AG_BoxNewVert(NULL, AG_BOX_EXPAND); AG_SetFontSize(box, "80%"); AG_LabelNewPolled(box, AG_LABEL_HFILL, _("Cursor: " AGSI_BOLD "%i" AGSI_RST " (@ %d,%d)"), &tgt->pos, &tgt->xCurs, &tgt->yCurs); AG_LabelNewPolled(box, AG_LABEL_HFILL, _("Selection: " AGSI_BOLD "%i-%i" AGSI_RST " (@ %d,%d - %d,%d)"), &tgt->selStart, &tgt->selEnd, &tgt->xSelStart, &tgt->ySelEnd, &tgt->ySelStart, &tgt->ySelEnd); AG_LabelNewPolled(box, AG_LABEL_HFILL, _("Offset: X=" AGSI_FONT5 "%i" AGSI_RST "/%i, Y=" AGSI_FONT5 "%i" AGSI_RST "/%i"), &tgt->x, &tgt->xMax, &tgt->y, &tgt->yMax); AG_SeparatorNewHoriz(box); AG_CheckboxSetFromFlags(box, 0, &tgt->flags, flagDescr); return (box); } AG_WidgetClass agEditableClass = { { "Agar(Widget:Editable)", sizeof(AG_Editable), { 1,0, AGC_EDITABLE, 0xE031 }, Init, NULL, /* reset */ Destroy, NULL, /* load */ NULL, /* save */ Edit }, Draw, SizeRequest, SizeAllocate, MouseButtonDown, MouseButtonUp, MouseMotion, KeyDown, KeyUp, NULL, /* touch */ Ctrl, NULL, /* joy */ }; #endif /* AG_WIDGETS */