/* * Copyright (c) 2005-2023 Julien Nadeau Carriere * Copyright (c) 2019 Charles A Daniels * 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. */ /* * Basic console widget. */ #include #ifdef AG_WIDGETS #include #include #include #include #include #include #include #include #include #include #include #include #include static AG_ConsoleLine *AppendMultiLine(AG_Console *_Nonnull, const char *_Nonnull); #define RETURN_IF_INVALID(cons) \ if (!AG_OBJECT_VALID(cons) || !AG_CONSOLE_ISA(cons)) \ return #define RETURN_NULL_IF_INVALID(cons) \ if (!AG_OBJECT_VALID(cons) || !AG_CONSOLE_ISA(cons)) \ return (NULL) AG_Console * AG_ConsoleNew(void *parent, Uint flags) { AG_Console *cons; cons = Malloc(sizeof(AG_Console)); AG_ObjectInit(cons, &agConsoleClass); cons->flags |= flags; if (flags & AG_CONSOLE_HFILL) { WIDGET(cons)->flags |= AG_WIDGET_HFILL; } if (flags & AG_CONSOLE_VFILL) { WIDGET(cons)->flags |= AG_WIDGET_VFILL; } AG_ObjectAttach(parent, cons); return (cons); } static __inline__ void AdjustXoffs(AG_Console *_Nonnull cons) { Uint i; const int wCons = WIDTH(cons); const int wBar = WIDTH(cons->vBar) + WIDGET(cons)->paddingLeft; for (i = 0, cons->wMax = 0; i < cons->nLines; i++) { const AG_ConsoleLine *ln = cons->lines[i]; int w; if (ln->surface[0] != -1) { w = WSURFACE(cons,ln->surface[0])->w; } else if (ln->surface[1] != -1) { w = WSURFACE(cons,ln->surface[1])->w; } else { AG_TextSize(ln->text, &w, NULL); } w += wBar + WIDGET(cons)->paddingRight; if (w > cons->wMax) cons->wMax = w; } if ((cons->wMax - wCons - cons->xOffs) < 0) cons->xOffs = MAX(0, cons->wMax - wCons); } static __inline__ void ClampVisible(AG_Console *_Nonnull cons) { const int v = (int)cons->nLines - (int)cons->rVisible; cons->rOffs = MAX(0,v); } static void ScrollLeft(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); const int ssa = AG_GetInt(cons->hBar, "inc"); int newOffs = (cons->xOffs - ssa); int wiggleRoom; if (newOffs < 0) { newOffs = 0; } wiggleRoom = cons->wMax - WIDTH(cons) + WIDTH(cons->vBar); if (newOffs > wiggleRoom) { newOffs = wiggleRoom - 1; } if (newOffs != cons->xOffs) { cons->xOffs = newOffs; AdjustXoffs(cons); AG_Redraw(cons); } } static void ScrollRight(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); const int ssa = AG_GetInt(cons->hBar, "inc"); int newOffs = (cons->xOffs + ssa); int wiggleRoom; if (newOffs < 0) { newOffs = 0; } wiggleRoom = cons->wMax - WIDTH(cons) + WIDTH(cons->vBar); if (newOffs > wiggleRoom) { newOffs = wiggleRoom - 1; } if (newOffs != cons->xOffs) { cons->xOffs = newOffs; AdjustXoffs(cons); AG_Redraw(cons); } } /* Scroll Up (or Left) */ static void ScrollUp(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); int newOffs; if (AG_GetModState(cons) & AG_KEYMOD_SHIFT) { /* Left */ ScrollLeft(event); return; } newOffs = cons->rOffs - cons->lineScrollAmount; cons->rOffs = MAX(0,newOffs); AG_Redraw(cons); } /* Scroll Down (or Right) */ static void ScrollDown(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); int newOffs, maxOffs; if (AG_GetModState(cons) & AG_KEYMOD_SHIFT) { /* Right */ ScrollRight(event); return; } maxOffs = (cons->nLines - cons->rVisible); if (maxOffs < 0) { return; } newOffs = cons->rOffs + cons->lineScrollAmount; cons->rOffs = MIN(newOffs,maxOffs); AG_Redraw(cons); } static void PageUp(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); int newOffs = cons->rOffs - cons->rVisible; if (newOffs < 0) { newOffs = 0; } if (cons->rOffs != newOffs) { cons->rOffs = newOffs; AG_Redraw(cons); } } static void PageDown(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); const int maxOffs = (cons->nLines - cons->rVisible); int newOffs; if (maxOffs < 0) { return; } newOffs = cons->rOffs + cons->rVisible; newOffs = MIN(newOffs,maxOffs); if (cons->rOffs != newOffs) { cons->rOffs = (Uint)newOffs; AG_Redraw(cons); } } static void GoToTop(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); cons->xOffs = 0; cons->rOffs = 0; AG_Redraw(cons); } static void GoToBottom(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); int newOffs; newOffs = cons->nLines - cons->rVisible; if (newOffs < 0) { newOffs = 0; } cons->rOffs = (Uint)newOffs; AG_Redraw(cons); } /* Map mouse position to a line index. */ static void MapLine(AG_Console *_Nonnull cons, int yMouse, int *_Nonnull nLine) { Uint sel; if (yMouse < WIDGET(cons)->paddingTop) { *nLine = cons->rOffs; } else if (yMouse > WIDGET(cons)->h) { *nLine = (int)cons->nLines - 1; } else { sel = (yMouse - WIDGET(cons)->paddingTop) / cons->lineskip; if ((cons->rOffs + sel) >= cons->nLines) { *nLine = (int)cons->nLines - 1; } else { *nLine = (int)(cons->rOffs + sel); } } } /* * Join currently selected lines into a single C string using a given * type of newline character or sequence. */ char * AG_ConsoleExportText(const AG_Console *cons, enum ag_newline_type nl) { const AG_NewlineFormat *newline; char *s, *ps; AG_Size sizeReq, newlineLen; const int pos = cons->pos; const int sel = cons->sel; int i; AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); if (pos == -1) return (NULL); #ifdef AG_DEBUG if (nl >= AG_NEWLINE_LAST) { AG_FatalError("newline arg"); } #endif newline = &agNewlineFormats[nl]; newlineLen = newline->len; for (i=0, sizeReq=1; i < cons->nLines; i++) { const AG_ConsoleLine *ln = cons->lines[i]; if (((i == pos) || (sel > 0 && i > pos && i <= pos+sel+1) || (sel < 0 && i < pos && i >= pos+sel-1))) { sizeReq += ln->len + newlineLen; } } if ((s = TryMalloc(sizeReq)) == NULL) { return (NULL); } ps = &s[0]; *ps = '\0'; for (i=0; i < cons->nLines; i++) { const AG_ConsoleLine *ln = cons->lines[i]; if ((i == pos) || (sel > 0 && i > pos && i <= pos+sel+1) || (sel < 0 && i < pos && i >= pos+sel-1)) { memcpy(ps, ln->text, ln->len); memcpy(&ps[ln->len], newline->s, newlineLen+1); ps += ln->len + newlineLen; } } return (s); } /* * Join entire buffer contents into a single C string using a given * type of newline character or sequence. */ char * AG_ConsoleExportBuffer(const AG_Console *cons, enum ag_newline_type nl) { const AG_NewlineFormat *newline; char *s, *ps; AG_Size sizeReq, newlineLen; int i; AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); #ifdef AG_DEBUG if (nl >= AG_NEWLINE_LAST) { AG_FatalError("newline arg"); } #endif newline = &agNewlineFormats[nl]; newlineLen = newline->len; for (i=0, sizeReq=1; i < cons->nLines; i++) { sizeReq += cons->lines[i]->len + newlineLen; } if ((s = TryMalloc(sizeReq)) == NULL) { return (NULL); } ps = &s[0]; *ps = '\0'; for (i = 0; i < cons->nLines; i++) { const AG_ConsoleLine *ln = cons->lines[i]; const AG_Size len = ln->len; // strlen(ln->text); memcpy(ps, ln->text, len); memcpy(&ps[len], newline->s, newlineLen+1); ps += len+newlineLen; } return (s); } static void MenuCopy(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_PTR(1); AG_EditableClipboard *cb = &agEditableClipbrd; char *s; if ((s = AG_ConsoleExportText(cons, AG_NEWLINE_NATIVE)) == NULL) { return; } #ifdef AG_UNICODE Strlcpy(cb->encoding, "UTF-8", sizeof(cb->encoding)); Free(cb->s); if ((cb->s = AG_ImportUnicode("UTF-8", s, &cb->len, NULL)) == NULL) cb->len = 0; #else Strlcpy(cb->encoding, "US-ASCII", sizeof(cb->encoding)); Free(cb->s); if ((cb->s = (Uint8 *)TryStrdup(s)) == NULL) cb->len = 0; #endif free(s); } static void MenuCopyActive(AG_Event *_Nonnull event) { const AG_Console *cons = AG_cCONSOLE_PTR(1); int *status = AG_PTR(2); *status = (cons->pos != -1) ? 1 : 0; } /* * Save the console buffer to a text file. */ static void MenuExportToFileTXT(AG_Event *_Nonnull event) { const AG_Console *cons = AG_cCONSOLE_PTR(1); const char *path = AG_STRING(2); AG_FileType *ft = AG_PTR(3); char *s; FILE *f; if (AG_FileOptionBool(ft, "selected-lines")) { if ((s = AG_ConsoleExportText(cons, AG_NEWLINE_NATIVE)) == NULL) goto fail; } else { if ((s = AG_ConsoleExportBuffer(cons, AG_NEWLINE_NATIVE)) == NULL) goto fail; } if ((f = fopen(path, "wb")) == NULL) { AG_SetError("%s: %s", path, AG_Strerror(errno)); free(s); goto fail; } if (AG_FileOptionBool(ft, "strip-ansi")) { AG_TextANSI ansi; AG_Char *ucs, *ch; ucs = AG_ImportUnicode("US-ASCII", s, NULL, NULL); for (ch = &ucs[0]; *ch != '\0'; ch++) { if (ch[0] == 0x1b && ch[1] >= 0x40 && ch[1] <= 0x5f && ch[2] != '\0') { if (AG_TextParseANSI(AG_TEXT_STATE_CUR(), &ansi, &ch[1]) == 0) { ch += ansi.len; continue; } } fputc((char)(*ch), f); } free(ucs); } else { fwrite(s, strlen(s), 1, f); } fclose(f); free(s); AG_TextTmsg(AG_MSG_INFO, 1250, _("Saved to %s OK"), AG_ShortFilename(path)); return; fail: AG_TextMsgFromError(); } /* * Save a screenshot of the console to an image file. */ static void MenuExportToFileImage(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_PTR(1); const char *path = AG_STRING(2); AG_Surface *S; if ((S = AG_WidgetSurface(cons)) == NULL) { AG_TextMsgFromError(); return; } if (AG_SurfaceExportFile(S, path) == -1) { AG_TextMsgFromError(); goto out; } AG_TextTmsg(AG_MSG_INFO, 1250, _("Saved to %s OK"), AG_ShortFilename(path)); out: AG_SurfaceFree(S); } static void MenuExportDlg(AG_Event *_Nonnull event) { const AG_Console *cons = AG_cCONSOLE_PTR(1); AG_Window *win; AG_FileDlg *fd; AG_FileType *ft; if ((win = AG_WindowNew(0)) == NULL) { return; } AG_WindowSetCaption(win, _("Export %s to..."), OBJECT(cons)->name); fd = AG_FileDlgNewMRU(win, "console-out", AG_FILEDLG_SAVE | AG_FILEDLG_CLOSEWIN | AG_FILEDLG_EXPAND); AG_FileDlgSetOptionContainer(fd, AG_BoxNewVert(win, AG_BOX_HFILL)); ft = AG_FileDlgAddType(fd, _("Text file"), "*.txt,*.log", MenuExportToFileTXT, "%Cp", cons); AG_FileOptionNewBool(ft, _("Selected Lines Only"), "selected-lines", 0); AG_FileOptionNewBool(ft, _("Strip ANSI"), "strip-ansi", 1); AG_FileDlgAddType(fd, _("Screenshot image"), "*.png,*.jpg", MenuExportToFileImage, "%p", cons); AG_WindowShow(win); } static void MenuSelectAll(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_PTR(1); cons->pos = 0; cons->sel = cons->nLines-1; AG_Redraw(cons); } static void MenuClear(AG_Event *_Nonnull event) { AG_ConsoleClear(AG_CONSOLE_PTR(1)); } /* Timer callback for double click. */ static Uint32 BeginSelectTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Console *cons = AGCONSOLE(to->obj); AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); cons->flags &= ~(AG_CONSOLE_BEGIN_SELECT); return (0); } static void BeginSelect(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); const int x = AG_INT(2); const int y = AG_INT(3); if (!AG_WidgetIsFocused(cons)) AG_WidgetFocus(cons); if (x < cons->r.x || x > cons->r.x+cons->r.w) return; if (cons->pm) AG_PopupHide(cons->pm); if (cons->nLines > 0) { MapLine(cons, y, &cons->pos); cons->sel = 0; AG_Redraw(cons); cons->flags |= AG_CONSOLE_SELECTING; } if (cons->flags & AG_CONSOLE_BEGIN_SELECT) { cons->pos = 0; cons->sel = cons->nLines-1; cons->flags &= ~(AG_CONSOLE_SELECTING); } else { cons->flags |= AG_CONSOLE_BEGIN_SELECT; AG_AddTimer(cons, &cons->beginSelectTo, agMouseDblclickDelay, BeginSelectTimeout, NULL); } } static void CloseSelect(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); cons->flags &= ~(AG_CONSOLE_SELECTING); } static void PopupMenu(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); const int x = AG_INT(2); const int y = AG_INT(3); AG_PopupMenu *pm; AG_MenuItem *mi; if (!AG_WidgetIsFocused(cons)) AG_WidgetFocus(cons); if (cons->flags & AG_CONSOLE_NOPOPUP) return; if (cons->pm) { AG_PopupShowAt(cons->pm, x,y); return; } if ((pm = cons->pm = AG_PopupNew(cons)) == NULL) return; AG_MenuSeparator(pm->root); AG_MenuAction(pm->root, _(AGSI_IDEOGRAM AGSI_SELECT_ALL AGSI_RST " Select All"), NULL, MenuSelectAll,"%p",cons); mi = AG_MenuAction(pm->root, _(AGSI_IDEOGRAM AGSI_COPY AGSI_RST " Copy"), NULL, MenuCopy,"%p",cons); mi->stateFn = AG_SetEvent(pm->menu, NULL, MenuCopyActive,"%Cp",cons); AG_MenuAction(pm->root, _(AGSI_IDEOGRAM AGSI_EXPORT_DOCUMENT AGSI_RST " Export to file..."), NULL, MenuExportDlg,"%Cp",cons); AG_MenuSeparator(pm->root); AG_MenuAction(pm->root, _(AGSI_IDEOGRAM AGSI_CLEAR_ALL AGSI_RST " Clear All"), NULL, MenuClear,"%p",cons); AG_PopupShowAt(pm, x,y); } static void MouseMotion(void *obj, int x, int y, int dx, int dy) { AG_Console *cons = obj; int newPos, newSel; if ((cons->flags & AG_CONSOLE_SELECTING) == 0) return; if (y < 0 && cons->rOffs > 0) { cons->rOffs--; AG_Redraw(cons); } else if (y > HEIGHT(cons) && (cons->rOffs + cons->rVisible) < cons->nLines) { cons->rOffs++; AG_Redraw(cons); } if (y > 0 && cons->nLines > 0) { MapLine(cons, y, &newPos); if (newPos < cons->nLines) { if ((newSel = newPos - cons->pos) != cons->sel) { if (cons->pos + newSel == 0) { cons->sel = newSel+1; } else { cons->sel = newSel; } AG_Redraw(cons); } } } if (x < cons->r.x) { const int ssa = AG_GetInt(cons->hBar,"inc"); if (cons->xOffs > 0) { if ((cons->xOffs -= ssa) < 0) { cons->xOffs = 0; } AG_Redraw(cons); } } else if (x > cons->r.x + cons->r.w) { const int ssa = AG_GetInt(cons->hBar,"inc"); const int wMax = cons->wMax - WIDTH(cons); if (cons->xOffs < wMax) { cons->xOffs += ssa; if (cons->xOffs > wMax) { cons->xOffs = wMax; } AG_Redraw(cons); } } } static void ComputeVisible(AG_Console *_Nonnull cons) { cons->rVisible = (int)AG_Floor((float)(cons->r.h - (WIDGET(cons)->paddingTop + WIDGET(cons)->paddingBottom)) / (float)cons->lineskip); if (cons->rVisible > 0) cons->rVisible--; } static void StyleChanged(AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_SELF(); Uint i; int j; cons->lineskip = WFONT(cons)->lineskip + WIDGET(cons)->spacingVert; /* cons->rOffs = 0; */ ComputeVisible(cons); for (i = 0; i < cons->nLines; i++) { AG_ConsoleLine *ln = cons->lines[i]; for (j = 0; j < 2; j++) { if (ln->surface[j] != -1) { AG_WidgetUnmapSurface(cons, ln->surface[j]); ln->surface[j] = -1; } } } } static void Init(void *_Nonnull obj) { AG_Console *cons = obj; AG_Scrollbar *sb; WIDGET(cons)->flags |= AG_WIDGET_FOCUSABLE | AG_WIDGET_USE_TEXT; cons->flags = 0; cons->xOffs = 0; cons->lines = NULL; cons->lineScrollAmount = 5; cons->lineskip = 0; cons->nLines = 0; cons->wMax = 0; cons->rOffs = 0; cons->rVisible = 0; cons->pm = NULL; cons->pos = -1; cons->sel = 0; cons->gcCounter = 0; cons->gcTrigger = 128; cons->r.x = 0; cons->r.y = 0; cons->r.w = 0; cons->r.h = 0; cons->scrollTo = NULL; TAILQ_INIT(&cons->files); AG_InitTimer(&cons->beginSelectTo, "beginSel", 0); sb = AG_ScrollbarNew(cons, AG_SCROLLBAR_VERT, AG_SCROLLBAR_EXCL); WIDGET(sb)->flags &= ~(AG_WIDGET_FOCUSABLE); AG_SetUint(sb, "min", 0); AG_BindUint(sb, "max", &cons->nLines); AG_BindUint(sb, "value", &cons->rOffs); AG_BindUint(sb, "visible", &cons->rVisible); cons->vBar = sb; sb = AG_ScrollbarNew(cons, AG_SCROLLBAR_HORIZ, AG_SCROLLBAR_EXCL | AG_SCROLLBAR_SMOOTH); WIDGET(sb)->flags &= ~(AG_WIDGET_FOCUSABLE); AG_SetInt(sb, "min", 0); AG_SetInt(sb, "inc", 30); AG_BindInt(sb, "visible", &WIDGET(cons)->w); AG_BindInt(sb, "value", &cons->xOffs); AG_BindInt(sb, "max", &cons->wMax); cons->hBar = sb; AG_ActionFn(cons, "BeginSelect", BeginSelect, NULL); AG_ActionFn(cons, "CloseSelect", CloseSelect, NULL); AG_ActionFn(cons, "PopupMenu", PopupMenu, NULL); AG_ActionFn(cons, "ScrollUp", ScrollUp, NULL); AG_ActionFn(cons, "ScrollDown", ScrollDown, NULL); AG_ActionFn(cons, "ScrollLeft", ScrollLeft, NULL); AG_ActionFn(cons, "ScrollRight", ScrollRight, NULL); AG_ActionFn(cons, "PageUp", PageUp, NULL); AG_ActionFn(cons, "PageDown", PageDown, NULL); AG_ActionFn(cons, "GoToTop", GoToTop, NULL); AG_ActionFn(cons, "GoToBottom", GoToBottom, NULL); AG_ActionOnButtonDown(cons, AG_MOUSE_LEFT, "BeginSelect"); AG_ActionOnButtonUp (cons, AG_MOUSE_LEFT, "CloseSelect"); AG_ActionOnButtonDown(cons, AG_MOUSE_RIGHT, "PopupMenu"); AG_ActionOnButtonDown(cons, AG_MOUSE_WHEELUP, "ScrollUp"); AG_ActionOnButtonDown(cons, AG_MOUSE_WHEELDOWN, "ScrollDown"); AG_ActionOnButtonDown(cons, AG_MOUSE_X1, "ScrollLeft"); AG_ActionOnButtonDown(cons, AG_MOUSE_X2, "ScrollRight"); AG_ActionOnKey(cons, AG_KEY_UP, AG_KEYMOD_ANY, "ScrollUp"); AG_ActionOnKey(cons, AG_KEY_DOWN, AG_KEYMOD_ANY, "ScrollDown"); AG_ActionOnKey(cons, AG_KEY_LEFT, AG_KEYMOD_ANY, "ScrollLeft"); AG_ActionOnKey(cons, AG_KEY_RIGHT, AG_KEYMOD_ANY, "ScrollRight"); AG_ActionOnKey(cons, AG_KEY_PAGEUP, AG_KEYMOD_ANY, "PageUp"); AG_ActionOnKey(cons, AG_KEY_PAGEDOWN, AG_KEYMOD_ANY, "PageDown"); AG_ActionOnKey(cons, AG_KEY_HOME, AG_KEYMOD_ANY, "GoToTop"); AG_ActionOnKey(cons, AG_KEY_END, AG_KEYMOD_ANY, "GoToBottom"); AG_AddEvent(cons, "font-changed", StyleChanged, NULL); AG_AddEvent(cons, "palette-changed", StyleChanged, NULL); AG_AddEvent(cons, "widget-shown", StyleChanged, NULL); AG_AddEvent(cons, "widget-gainfocus", StyleChanged, NULL); AG_AddEvent(cons, "widget-lostfocus", StyleChanged, NULL); } static void SizeRequest(void *_Nonnull p, AG_SizeReq *_Nonnull r) { AG_TextSize("XXXXXXXXXXXXXXXXXXXXXXXXX", &r->w, &r->h); r->h <<= 1; } static int SizeAllocate(void *_Nonnull p, const AG_SizeAlloc *_Nonnull a) { AG_Console *cons = p; AG_SizeReq rBar; AG_SizeAlloc aBar; if (a->w < 8 || a->h < 8) return (-1); AG_WidgetSizeReq(cons->vBar, &rBar); aBar.x = a->w - rBar.w; aBar.y = 0; aBar.w = rBar.w; aBar.h = a->h - rBar.w; AG_WidgetSizeAlloc(cons->vBar, &aBar); cons->r.x = 0; cons->r.y = 0; cons->r.w = (a->w - aBar.w) - 1; AG_WidgetSizeReq(cons->hBar, &rBar); aBar.x = 0; aBar.y = a->h - rBar.h; aBar.w = a->w - rBar.h; aBar.h = rBar.h; AG_WidgetSizeAlloc(cons->hBar, &aBar); cons->r.h = (a->h - aBar.h); ComputeVisible(cons); ClampVisible(cons); AdjustXoffs(cons); return (0); } static void Draw(void *_Nonnull p) { AG_Console *cons = p; const AG_Color *cBg = &WCOLOR(cons, BG_COLOR); const AG_Color *cSel = &WCOLOR(cons, SELECTION_COLOR); const AG_Color *cText = &WCOLOR(cons, TEXT_COLOR); AG_Rect r; Uint lnIdx; int pos, sel; if (cBg->a > 0) { r.x = 0; r.y = 0; r.w = WIDTH(cons) - WIDTH(cons->vBar); r.h = HEIGHT(cons) - HEIGHT(cons->hBar); AG_DrawRectFilled(cons, &r, cBg); } if (cons->nLines == 0) goto out; if (cons->scrollTo != NULL) { cons->rOffs = *cons->scrollTo; cons->scrollTo = NULL; ClampVisible(cons); } AG_PushClipRect(cons, &cons->r); r.x = WIDGET(cons)->paddingLeft - cons->xOffs; r.y = WIDGET(cons)->paddingTop; r.w = WIDTH(cons) - WIDTH(cons->vBar); r.h = cons->lineskip + 1; pos = cons->pos; sel = cons->sel; for (lnIdx = cons->rOffs; lnIdx < cons->nLines && r.y < WIDGET(cons)->h; lnIdx++) { AG_ConsoleLine *ln = cons->lines[lnIdx]; AG_Surface *S; int isSel; if ((pos != -1) && ((lnIdx == pos) || ((sel > 0 && lnIdx > pos && lnIdx <= pos+sel+1) || (sel < 0 && lnIdx < pos && lnIdx >= pos+sel-1)))) { isSel = 1; } else { isSel = 0; } if (ln->text[0] == '\0') { r.y += cons->lineskip; continue; } if (ln->surface[isSel] == -1) { if (ln->parent) { /* Take the parent's color. */ AG_TextColor((ln->parent->c.a != 0) ? &ln->parent->c : cText); } else { AG_TextColor((ln->c.a != 0) ? &ln->c : cText); } AG_TextBGColor(isSel ? cSel : cBg); if ((S = AG_TextRender(ln->text)) == NULL) { r.y += cons->lineskip; continue; } if (S->w == 0 || S->h == 0) { AG_SurfaceFree(S); r.y += cons->lineskip; continue; } ln->surface[isSel] = AG_WidgetMapSurface(cons, S); } else { S = WSURFACE(cons, ln->surface[isSel]); } /* * Blit rendered label -> display. * Make extra space for group indicator. */ AG_WidgetBlitSurface(cons, ln->surface[isSel], (ln->parent != NULL) ? r.x + 9 : r.x, /* XXX */ r.y); if (ln->parent) { /* Group indicator */ AG_DrawLine(cons, r.x + 5, r.y, r.x + 5, r.y + cons->lineskip, cText); } r.y += cons->lineskip; } AG_PopClipRect(cons); out: AG_WidgetDraw(cons->vBar); AG_WidgetDraw(cons->hBar); } static void FreeLines(AG_Console *_Nonnull cons) { Uint i; for (i = 0; i < cons->nLines; i++) { AG_ConsoleLine *ln = cons->lines[i]; int j; for (j = 0; j < 2; j++) { if (ln->surface[j] != -1) AG_WidgetUnmapSurface(cons, ln->surface[j]); } free(ln->text); free(ln); } Free(cons->lines); cons->lines = NULL; cons->nLines = 0; } static void Destroy(void *_Nonnull p) { AG_Console *cons = p; if (cons->pm) { AG_PopupDestroy(cons->pm); } FreeLines(cons); } #ifdef AG_LEGACY /* Configure padding in pixels */ void AG_ConsoleSetPadding(AG_Console *cons, int padding) { AG_SetStyleF(cons, "padding", "%d", padding); } #endif /* AG_LEGACY */ /* Append a line to the console; backend to AG_ConsoleMsg(). */ AG_ConsoleLine * AG_ConsoleAppendLine(AG_Console *cons, const char *s) { AG_ConsoleLine *ln; RETURN_NULL_IF_INVALID(cons); AG_ObjectLock(cons); if (s && (strchr(s, agNewlineFormats[AG_NEWLINE_NATIVE].s[0]))) { ln = AppendMultiLine(cons, s); } else { ln = Malloc(sizeof(AG_ConsoleLine)); /* top level / standalone line by default */ ln->parent = NULL; if (s && strlen(s) == 0) { /* zero length strings */ ln->text = Strdup(s); ln->len = 0; } else if (s != NULL) { ln->text = Strdup(s); ln->len = strlen(s); } else { ln->text = NULL; ln->len = 0; } } cons->lines = Realloc(cons->lines, (cons->nLines+1) * sizeof(AG_ConsoleLine *)); cons->lines[cons->nLines++] = ln; ln->cons = cons; ln->p = NULL; ln->surface[0] = -1; ln->surface[1] = -1; AG_ColorNone(&ln->c); /* Inherit default */ if ((cons->flags & AG_CONSOLE_NOAUTOSCROLL) == 0) cons->scrollTo = &cons->nLines; if (++cons->gcCounter > cons->gcTrigger) { Uint i; for (i = 0; i < cons->nLines; i++) { AG_ConsoleLine *ln = cons->lines[i]; int j; if (i >= cons->rOffs && i < cons->rOffs + cons->rVisible) continue; for (j = 0; j < 2; j++) { if (ln->surface[j] != -1) { AG_WidgetUnmapSurface(cons, ln->surface[j]); ln->surface[j] = -1; } } } cons->gcCounter = 0; } AG_Redraw(cons); AG_ObjectUnlock(cons); return (ln); } /* Append a line to the console; backend to AG_ConsoleMsg(). */ static AG_ConsoleLine * AppendMultiLine(AG_Console *cons, const char *s) { const AG_NewlineFormat *newline = &agNewlineFormats[AG_NEWLINE_NATIVE]; AG_ConsoleLine *ln; char *tok = NULL, *dup, *pDup; int first = 1; ln = Malloc(sizeof(AG_ConsoleLine)); /* top level / standalone line by default */ ln->parent = NULL; dup = pDup = Strdup(s); while ((tok = Strsep(&dup, newline->s)) != NULL) { if (newline->len == 2 && tok[0] != '\0') { /* XXX */ tok[strlen(tok)-1] = '\0'; } if (first == 1) { first = 0; /* populate this line with the first slice */ ln->text = Strdup(tok); ln->len = strlen(tok); } else { /* create a child line */ (AG_ConsoleAppendLine(cons, tok))->parent = ln; } } free(pDup); /* we only return the parent, but that's ok since the children take * it's style */ return (ln); } /* Append a message to the console (format string). */ AG_ConsoleLine * AG_ConsoleMsg(AG_Console *cons, const char *fmt, ...) { AG_ConsoleLine *ln; va_list args; AG_Size len; RETURN_NULL_IF_INVALID(cons); AG_ObjectLock(cons); if ((ln = AG_ConsoleAppendLine(cons, NULL)) == NULL) { goto fail; } va_start(args, fmt); AG_Vasprintf(&ln->text, fmt, args); va_end(args); len = ln->len = strlen(ln->text); if (len > 1 && ln->text[len-1] == '\n') { ln->text[len-1] = '\0'; ln->len--; } AG_ObjectUnlock(cons); return (ln); fail: AG_ObjectUnlock(cons); return (NULL); } /* Append a message to the console (C string). */ AG_ConsoleLine * AG_ConsoleMsgS(AG_Console *cons, const char *s) { AG_ConsoleLine *ln; AG_Size len; RETURN_NULL_IF_INVALID(cons); AG_ObjectLock(cons); if ((ln = AG_ConsoleAppendLine(cons, s)) == NULL) { goto fail; } len = ln->len; if (len > 1 && ln->text[len-1] == '\n') { ln->text[len-1] = '\0'; ln->len--; } AG_ObjectUnlock(cons); return (ln); fail: AG_ObjectUnlock(cons); return (NULL); } /* * Produce formatted binary data on the console. * The default format is "%02x " for base 16, hexadecimal output. */ void AG_ConsoleBinary(AG_Console *cons, const void *data, AG_Size size, const char *label, const char *fmt) { const int columnWd = 16; AG_Size pos; char *buf; AG_Size bufSize = size*3 + 1 + (2+columnWd+1) + 1; /* One line */ int lineWd = 0; RETURN_IF_INVALID(cons); if (label) { bufSize += 1+strlen(label)+2; } if ((buf = Malloc(bufSize)) == NULL) { Verbose("Console: Out of memory"); return; } if (label) { Strlcpy(buf, "[", bufSize); Strlcat(buf, label, bufSize); Strlcat(buf, "] ", bufSize); } else { buf[0] = '\0'; } for (pos=0; pos < size; pos++) { const Uint8 val = ((const Uint8 *)data)[pos]; char num[4]; if (fmt) { Snprintf(num, sizeof(num), fmt, val); } else { Snprintf(num, sizeof(num), "%02x ", val); } AG_Strlcat(buf, num, bufSize); if (++lineWd == columnWd) { int i; char cp[2]; Strlcat(buf, " |", bufSize); for (i = 1; i <= lineWd; i++) { Uint8 pval; pval = ((const Uint8 *)data)[pos-lineWd+i]; cp[0] = isprint(pval) ? pval : '.'; cp[1] = '\0'; Strlcat(buf, cp, bufSize); } Strlcat(buf, "|", bufSize); if (AG_ConsoleAppendLine(cons, buf) == NULL) { Verbose("Console: %s", AG_GetError()); return; } lineWd = 0; buf[0] = '\0'; if (label) { Strlcpy(buf, "[", bufSize); Strlcat(buf, label, bufSize); Strlcat(buf, "] ", bufSize); } else { buf[0] = '\0'; } } else if (lineWd == 8) { AG_Strlcat(buf, " ", bufSize); } } } static void InvalidateCachedLabel(AG_Console *cons, AG_ConsoleLine *ln) { int i; for (i = 0; i < 2; i++) { if (ln->surface[i] != -1) { AG_WidgetUnmapSurface(cons, ln->surface[i]); ln->surface[i] = -1; } } AG_Redraw(cons); } /* Replace the text of a log entry. */ void AG_ConsoleMsgEdit(AG_ConsoleLine *ln, const char *s) { AG_Console *cons = ln->cons; RETURN_IF_INVALID(cons); AG_ObjectLock(cons); free(ln->text); ln->text = Strdup(s); ln->len = strlen(s); InvalidateCachedLabel(cons, ln); AG_ObjectUnlock(cons); } /* Append a string to the text of a log entry. */ void AG_ConsoleMsgCatS(AG_ConsoleLine *ln, const char *s) { AG_Console *cons = ln->cons; AG_Size sLen, newLen; sLen = strlen(s); RETURN_IF_INVALID(cons); AG_ObjectLock(cons); newLen = ln->len + sLen + 1; ln->text = Realloc(ln->text, newLen); ln->len = newLen-1; Strlcat(ln->text, s, newLen); InvalidateCachedLabel(cons, ln); AG_ObjectUnlock(cons); } /* Assign a user pointer to a log entry. */ void AG_ConsoleMsgPtr(AG_ConsoleLine *ln, void *p) { AG_Console *cons = ln->cons; RETURN_IF_INVALID(cons); AG_ObjectLock(cons); ln->p = p; AG_ObjectUnlock(cons); } /* Assign an alternate color to a log entry. */ void AG_ConsoleMsgColor(AG_ConsoleLine *ln, const AG_Color *c) { AG_Console *cons = ln->cons; RETURN_IF_INVALID(cons); AG_ObjectLock(cons); memcpy(&ln->c, c, sizeof(AG_Color)); AG_ObjectUnlock(cons); } /* Delete all log entries. */ void AG_ConsoleClear(AG_Console *cons) { RETURN_IF_INVALID(cons); AG_ObjectLock(cons); FreeLines(cons); cons->rOffs = 0; cons->pos = -1; cons->sel = 0; AG_ObjectUnlock(cons); AG_Redraw(cons); } /* * Process available data on a file we are following. */ static int ConsoleReadFile(AG_EventSink *_Nonnull es, AG_Event *_Nonnull event) { AG_Console *cons = AG_CONSOLE_PTR(1); AG_ConsoleFile *cf = AG_PTR(2); FILE *f = cf->pFILE; char *buf, *pLine, *line; #if AG_MODEL == AG_LARGE const AG_Size bufferMax = AG_BUFFER_MAX*8; #else const AG_Size bufferMax = AG_BUFFER_MAX*4; #endif AG_Size bufSize = bufferMax, rv, nRead=0; if ((buf = TryMalloc(bufSize+1)) == NULL) { return (0); } for (;;) { clearerr(f); rv = fread(&buf[nRead], 1, bufSize-nRead, f); if (ferror(f)) { AG_ConsoleMsg(cons, _("(read error on %s)"), cf->label); goto out; } if (rv == 0) { break; } if (rv == bufSize-nRead) { char *bufNew; if ((bufNew = TryRealloc(buf, bufSize+bufferMax+1)) == NULL) { AG_ConsoleMsg(cons, _("(out of memory for %s)"), cf->label); goto out; } buf = bufNew; bufSize += bufferMax; } nRead += rv; } buf[nRead] = '\0'; cf->offs += nRead; if (nRead > 0) { if (cf->flags & AG_CONSOLE_FILE_BINARY) { AG_ConsoleBinary(cons, buf, nRead, cf->label, NULL); } else { const char *newline_s = cf->newline->s; char sep[2]; switch (cf->newline->len) { case 1: for (pLine = buf; (line = AG_Strsep(&pLine, newline_s)) != NULL; ) { if (line[0] == '\0') { continue; } AG_ConsoleMsgS(cons, line); } break; case 2: sep[0] = newline_s[0]; /* Cheat */ sep[1] = '\0'; for (pLine = buf; (line = AG_Strsep(&pLine, sep)) != NULL; ) { if (line[0] == '\0') { continue; } line[strlen(line)-1] = '\0'; AG_ConsoleMsgS(cons, line); } break; } } } out: free(buf); return (0); } /* * Read, dump and follow a file. * * Files can contain text with UTF-8. AG_CONSOLE_FILE_BINARY may be used * to display a binary hex dump as opposed to text. * * (TODO: for named files, set up an extra AG_SINK_FSEVENT to detect * truncation and deletion). */ AG_ConsoleFile * AG_ConsoleOpenFD(AG_Console *cons, const char *lbl, int fd, enum ag_newline_type newline, Uint flags) { AG_ConsoleFile *mon; FILE *f; AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); if ((f = fdopen(fd, "r")) == NULL) { AG_SetErrorS("fdopen"); return (NULL); } if ((mon = AG_ConsoleOpenStream(cons, lbl ? lbl : "fd", NULL, newline, flags)) == NULL) { return (NULL); } mon->pFILE = fdopen(fd, "r"); mon->fd = fd; return (mon); } AG_ConsoleFile * AG_ConsoleOpenFile(AG_Console *cons, const char *lbl, const char *file, enum ag_newline_type newline, Uint flags) { FILE *f; AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); if ((f = fopen(file, "r")) == NULL) { AG_SetError(_("Could not open %s"), file); return (NULL); } return AG_ConsoleOpenStream(cons, lbl ? lbl : AG_ShortFilename(file), (void *)f, newline, flags); } AG_ConsoleFile * AG_ConsoleOpenStream(AG_Console *cons, const char *lbl, void *pFILE, enum ag_newline_type newline, Uint flags) { FILE *f = (FILE *)pFILE; AG_ConsoleFile *cf; AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); if ((cf = TryMalloc(sizeof(AG_ConsoleFile))) == NULL) { return (NULL); } cf->flags = flags; if ((cf->label = TryStrdup(lbl ? lbl : "stream")) == NULL) { free(cf); return (NULL); } cf->pFILE = pFILE; cf->fd = fileno(f); cf->offs = 0; cf->color = NULL; #ifdef AG_DEBUG if (newline >= AG_NEWLINE_LAST) { AG_FatalError("newline arg"); } #endif cf->newline = &agNewlineFormats[newline]; AG_AddEventSink(AG_SINK_READ, cf->fd, 0, ConsoleReadFile, "%p,%p", cons, cf); TAILQ_INSERT_TAIL(&cons->files, cf, files); return (cf); } /* Close an open file or stream being followed. */ void AG_ConsoleClose(AG_Console *cons, AG_ConsoleFile *cf) { AG_OBJECT_ISA(cons, "AG_Widget:AG_Console:*"); if ((cf->flags & AG_CONSOLE_FILE_LEAVE_OPEN) == 0) { if (cf->pFILE != NULL) { fclose((FILE *)cf->pFILE); } else { #if !defined(_WIN32) if (cf->fd != -1) close(cf->fd); #endif } } TAILQ_REMOVE(&cons->files, cf, files); Free(cf->label); free(cf); } AG_WidgetClass agConsoleClass = { { "Agar(Widget:Console)", sizeof(AG_Console), { 1,0, AGC_CONSOLE, 0xE017 }, Init, NULL, /* reset */ Destroy, NULL, /* load */ NULL, /* save */ NULL /* edit */ }, Draw, SizeRequest, SizeAllocate, NULL, /* mouse_button_down */ NULL, /* mouse_button_up */ MouseMotion, NULL, /* key_down */ NULL, /* key_up */ NULL, /* touch */ NULL, /* ctrl */ NULL /* joy */ }; #endif /* AG_WIDGETS */