/* * Copyright (c) 2021-2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include "chatter.h" #include "irc.h" #include "util.h" #define NICK_LIST_WIDTH 75 #define CHATTER_SCRAP_ELEMENTS 20 #define MAX_TAB_WIDTH 100 #define TAB_BAR_HEIGHT 15 static Handle scrp_rec_h = NULL; static Pattern tab_bar_pattern; void chatter_layout(struct chatter *chatter, bool init, Rect *init_bounds); void chatter_draw_tab_bar(struct chatter *chatter); void chatter_focus_tab(struct chatter *chatter, struct chatter_tab *tab); void chatter_draw_grow_icon(struct chatter *chatter); void chatter_autoscroll(struct chatter *chatter, TEHandle te, ControlHandle scroller); short chatter_wait_type(struct focusable *focusable); void chatter_key_down(struct focusable *focusable, EventRecord *event); void chatter_mouse_down(struct focusable *focusable, EventRecord *event); void chatter_resize(struct focusable *focusable, EventRecord *event); bool chatter_menu(struct focusable *focusable, short menu, short item); void chatter_idle(struct focusable *focusable, EventRecord *event); void chatter_update(struct focusable *focusable, EventRecord *event); void chatter_resume(struct focusable *focusable, EventRecord *event); bool chatter_close(struct focusable *focusable); bool chatter_quit(struct focusable *focusable); struct chatter_tab * chatter_find_tab_for_conn_and_channel( struct chatter *chatter, struct irc_connection *conn, struct irc_channel *channel); struct chatter * chatter_init(const char *server, const unsigned short port, const char *password, const char *nick, const char *ident, const char *realname, const char *channel) { struct focusable *focusable; struct chatter *chatter; struct chatter_tab *tab; char title[64]; Rect bounds = { 0 }; short padding = 20; GetIndPattern(&tab_bar_pattern, sysPatListID, 23); chatter = xmalloczero(sizeof(struct chatter), "chatter"); SLIST_INIT(&chatter->tabs_list); bounds.left = padding; bounds.top = screenBits.bounds.top + padding + (GetMBarHeight() * 2) - 1; bounds.right = screenBits.bounds.right - padding - 1; bounds.bottom = screenBits.bounds.bottom - padding - 1; snprintf(title, sizeof(title), "%s: Disconnected", PROGRAM_NAME); chatter->win = NewWindow(0L, &bounds, CtoPstr(title), false, documentProc, (WindowPtr)-1L, true, 0); if (!chatter->win) panic("Can't create chatter window"); SetPort(chatter->win); TextFont(applFont); TextSize(CHATTER_FONT_SIZE); bounds.right -= bounds.left; bounds.bottom -= bounds.top; bounds.top = bounds.left = 0; chatter_layout(chatter, true, &bounds); focusable = xmalloczero(sizeof(struct focusable), "focusable"); focusable->win = chatter->win; focusable->cookie = chatter; focusable->wait_type = chatter_wait_type; focusable->idle = chatter_idle; focusable->key_down = chatter_key_down; focusable->mouse_down = chatter_mouse_down; focusable->update = chatter_update; focusable->close = chatter_close; focusable->quit = chatter_quit; focusable->resize = chatter_resize; focusable->menu = chatter_menu; focusable->resume = chatter_resume; focusable_add(focusable); chatter->focusable = focusable; chatter_draw_tab_bar(chatter); chatter_printf(chatter, NULL, NULL, "$B***$0 Welcome to %s", PROGRAM_NAME); tab = SLIST_FIRST(&chatter->tabs_list); tab->conn = irc_connect(chatter, server, port, password, nick, ident, realname, channel); DrawControls(chatter->win); chatter_update_titlebar(chatter); ValidRect(&bounds); return chatter; } short chatter_wait_type(struct focusable *focusable) { struct irc_connection *conn; struct chatter *chatter = (struct chatter *)(focusable->cookie); short n; SLIST_FOREACH(conn, &irc_connections_list, list) { if (conn->ibuflen) return WAIT_TYPE_URGENT; } if (!focusable->visible) return WAIT_TYPE_BACKGROUND; return WAIT_TYPE_FOREGROUND; } void chatter_layout(struct chatter *chatter, bool init, Rect *win_bounds) { Rect bounds, inset_bounds; Rect control_bounds = { 0 }; struct chatter_tab *tab; if (win_bounds == NULL) win_bounds = &chatter->win->portRect; /* input */ bounds.left = win_bounds->left; bounds.right = win_bounds->right - SCROLLBAR_WIDTH + 1; bounds.top = win_bounds->bottom - SCROLLBAR_WIDTH + 1; bounds.bottom = win_bounds->bottom; if (init) { inset_bounds = bounds; InsetRect(&inset_bounds, 3, 1); inset_bounds.right = win_bounds->right * 2; chatter->input_te = TENew(&inset_bounds, &bounds); (*(chatter->input_te))->crOnly = -1; TEAutoView(true, chatter->input_te); TEActivate(chatter->input_te); } else { (*(chatter->input_te))->viewRect = bounds; InsetRect(&bounds, 3, 1); (*(chatter->input_te))->destRect = bounds; TECalText(chatter->input_te); } if (init) { chatter_add_tab(chatter, win_bounds, NULL, NULL); } else { /* TODO: move tabs around */ } } struct chatter_tab * chatter_add_tab(struct chatter *chatter, Rect *win_bounds, struct irc_connection *conn, struct irc_channel *channel) { struct chatter_tab *tab = NULL; Rect bounds, inset_bounds; Rect data_bounds = { 0, 0, 0, 1 }; /* tlbr */ Point cell_size = { 0 }; Cell cell = { 0, 0 }; if (win_bounds == NULL) win_bounds = &chatter->win->portRect; tab = xmalloczero(sizeof(struct chatter_tab), "tab"); SLIST_APPEND(&chatter->tabs_list, tab, chatter_tab, list); tab->index = chatter->ntabs++; tab->conn = conn; tab->channel = channel; bounds.bottom = (*(chatter->input_te))->viewRect.top - 15; if (tab->channel) { /* nick list */ bounds.top = 0; bounds.right = win_bounds->right - SCROLLBAR_WIDTH + 1; bounds.left = bounds.right - NICK_LIST_WIDTH; tab->nick_list = LNew(&bounds, &data_bounds, cell_size, 0, chatter->win, true, true, false, true); if (!tab->nick_list) panic("Can't create nick list"); LAddColumn(1, 0, tab->nick_list); (*(tab->nick_list))->selFlags = lOnlyOne | lNoNilHilite; } /* messages scrollbar */ bounds.top = -1; if (tab->channel) bounds.right = (*(tab->nick_list))->rView.left; else bounds.right = win_bounds->right + 1; bounds.left = bounds.right - SCROLLBAR_WIDTH; bounds.bottom += 1; tab->messages_scroller = NewControl(chatter->win, &bounds, "\p", true, 1, 1, 1, scrollBarProc, 0L); /* messages */ bounds.right = (*(tab->messages_scroller))->contrlRect.left; bounds.left = 0; bounds.top = 0; bounds.bottom -= 1; inset_bounds = bounds; InsetRect(&inset_bounds, 4, 4); tab->messages_te = TEStylNew(&inset_bounds, &bounds); (*(tab->messages_te))->caretHook = NullCaretHook; TEActivate(tab->messages_te); chatter_autoscroll(chatter, tab->messages_te, tab->messages_scroller); chatter_focus_tab(chatter, tab); return tab; } void chatter_focus_tab(struct chatter *chatter, struct chatter_tab *tab) { RgnHandle clip; Rect zerorect = { 0, 0, 0, 0 }, r; if (chatter->current_tab) { if (chatter->current_tab == tab) return; /* * Doing the HideControl takes out the top line of our tab bar, * so clip to just above it */ GetClip(clip = NewRgn()); r.left = 0; r.top = 0; r.right = (*(chatter->current_tab->messages_scroller))->contrlRect.right; r.bottom = (*(chatter->current_tab->messages_scroller))->contrlRect.bottom - 1; ClipRect(&r); TEDeactivate(chatter->current_tab->messages_te); if (chatter->current_tab->nick_list) LDoDraw(false, chatter->current_tab->nick_list); HideControl(chatter->current_tab->messages_scroller); SetClip(clip); } chatter->current_tab = tab; FillRect(&(*(tab->messages_te))->viewRect, white); TEActivate(tab->messages_te); TEUpdate(&(*(tab->messages_te))->viewRect, tab->messages_te); ShowControl(tab->messages_scroller); if (tab->nick_list) { FillRect(&(*(tab->nick_list))->rView, white); LDoDraw(true, tab->nick_list); LUpdate(chatter->win->visRgn, tab->nick_list); } DrawControls(chatter->win); chatter_draw_tab_bar(chatter); } void chatter_resume(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); focusable_show(focusable); InvalRect(chatter->win->visRgn); } bool chatter_close(struct focusable *focusable) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab; struct irc_connection *conn, *tconn; bool connected = false; SLIST_FOREACH(tab, &chatter->tabs_list, list) { if (tab->conn->state == IRC_STATE_CONNECTED) { connected = true; break; } } if (connected && !chatter->quitting) { focusable_hide(focusable); return false; } SLIST_FOREACH_SAFE(conn, &irc_connections_list, list, tconn) { if (conn->chatter == chatter) { irc_close_connection(conn); /* this will kill any channels as well */ irc_dealloc_connection(conn); } } if (chatter->tab_bar.baseAddr) xfree(&chatter->tab_bar.baseAddr); DisposeWindow(focusable->win); return true; } bool chatter_quit(struct focusable *focusable) { struct chatter *chatter = (struct chatter *)(focusable->cookie); chatter->quitting = true; focusable_close(focusable); return true; } void chatter_update_titlebar(struct chatter *chatter) { Str255 curtitle; char title[64]; struct chatter_tab *tab = chatter->current_tab; if (!tab->conn || tab->conn->state <= IRC_STATE_DISCONNECTED) snprintf(title, sizeof(title), "%s: Disconnected", PROGRAM_NAME); else if (tab->conn->state == IRC_STATE_CONNECTING) snprintf(title, sizeof(title), "%s: Connecting to %s", PROGRAM_NAME, tab->conn->hostname); else if (tab->channel) snprintf(title, sizeof(title), "%s: %s@%s: %s", PROGRAM_NAME, tab->conn->nick, tab->conn->hostname, tab->channel->name); else snprintf(title, sizeof(title), "%s: %s@%s", PROGRAM_NAME, tab->conn->nick, tab->conn->hostname); GetWTitle(chatter->win, &curtitle); PtoCstr(curtitle); if (strcmp((char *)&curtitle, title) != 0) SetWTitle(chatter->win, CtoPstr(title)); } void chatter_idle(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab; short n; TEIdle(chatter->input_te); SLIST_FOREACH(tab, &chatter->tabs_list, list) { irc_process(tab->conn); } } void chatter_update(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab; GrafPtr old_port; Rect r; short what = -1; GetPort(&old_port); if (event != NULL) what = event->what; switch (what) { case -1: case updateEvt: TextFont(applFont); TextSize(10); EraseRect(&chatter->win->portRect); if (tab->nick_list) { r = (*(tab->nick_list))->rView; LUpdate(chatter->win->visRgn, tab->nick_list); InsetRect(&r, -1, -1); FrameRect(&r); } r = (*(tab->messages_te))->viewRect; InsetRect(&r, -1, -1); FrameRect(&r); TEUpdate(&(*(tab->messages_te))->viewRect, tab->messages_te); r = (*(chatter->input_te))->viewRect; InsetRect(&r, -1, -1); FrameRect(&r); TEUpdate(&(*(chatter->input_te))->viewRect, chatter->input_te); DrawControls(chatter->win); chatter_draw_tab_bar(chatter); break; case activateEvt: if (event->modifiers & activeFlag) { if (tab->nick_list) LActivate(true, tab->nick_list); TEActivate(tab->messages_te); TEActivate(chatter->input_te); } else { if (tab->nick_list) LActivate(false, tab->nick_list); TEDeactivate(tab->messages_te); TEDeactivate(chatter->input_te); } break; } } void chatter_draw_tab_bar(struct chatter *chatter) { Rect r, r2; RgnHandle clip; BitMap cur_bits; short tab_width, tab_offset, n, width; size_t len; static const char no_connection[] = "Disconnected"; struct chatter_tab *tab; char *label; cur_bits = chatter->win->portBits; if (chatter->tab_bar.baseAddr == 0) { width = chatter->win->portRect.right - chatter->win->portRect.left; chatter->tab_bar.rowBytes = (((width - 1) / 16) + 1) * 2; chatter->tab_bar.baseAddr = xmalloczero( chatter->tab_bar.rowBytes * TAB_BAR_HEIGHT, "tab_bar"); SetRect(&chatter->tab_bar.bounds, 0, 0, width, TAB_BAR_HEIGHT); } SetPortBits(&chatter->tab_bar); TextFont(geneva); TextSize(9); tab_offset = 5; tab_width = (chatter->win->portRect.right - chatter->win->portRect.left - tab_offset - tab_offset) / chatter->ntabs; if (tab_width > MAX_TAB_WIDTH) tab_width = MAX_TAB_WIDTH; r.top = 0; r.bottom = TAB_BAR_HEIGHT; r.left = 0; r.right = chatter->win->portRect.right - chatter->win->portRect.left; FillRect(&r, tab_bar_pattern); r.left--; r.right++; FrameRect(&r); r.left++; r.right--; r.left = tab_offset; r.bottom -= 2; r.right = r.left + tab_width; r2.left = r.left + 1; r2.right = r.right - 1; r2.top = r.top; r2.bottom = r2.top + 1; GetClip(clip = NewRgn()); SLIST_FOREACH(tab, &chatter->tabs_list, list) { tab->label_rect = r; FillRect(&r, white); FrameRect(&r); if (tab == chatter->current_tab) { FrameRect(&r2); FillRect(&r2, white); tab->have_activity = false; } ClipRect(&r); if (tab->conn) { if (tab->channel) label = tab->channel->name; else label = tab->conn->hostname; } else label = (char *)&no_connection; len = strlen(label); if (tab->have_activity) TextFace(bold | condense); else TextFace(0); width = TextWidth(label, 0, len); if (width > tab_width - 4) width = tab_width - 4; MoveTo(r.left + ((tab_width - width) / 2), r.bottom - 3); DrawText(label, 0, len); SetClip(clip); r.left += tab_width + 2; r.right += tab_width + 2; r2.left += tab_width + 2; r2.right += tab_width + 2; } DisposeRgn(clip); TextFont(applFont); TextSize(10); TextFace(0); SetPortBits(&cur_bits); r = chatter->tab_bar.bounds; r.bottom = (*(chatter->input_te))->viewRect.top; r.top += r.bottom - TAB_BAR_HEIGHT; SLIST_FOREACH(tab, &chatter->tabs_list, list) { tab->label_rect.top += r.top; tab->label_rect.bottom += r.top; } CopyBits(&chatter->tab_bar, &chatter->win->portBits, &chatter->tab_bar.bounds, &r, srcCopy, nil); chatter_draw_grow_icon(chatter); } void chatter_draw_grow_icon(struct chatter *chatter) { Rect r, *te; RgnHandle tmp; WindowPtr win = chatter->win; /* * Our input bar is taller than a scrollbar, so we can't use the * normal DrawGrowIcon or our DrawGrowIconOnly */ HLock(*(chatter->input_te)); te = &(*(chatter->input_te))->viewRect; r = win->portRect; r.top = r.bottom - (te->bottom - te->top + 1); r.left = r.right - SCROLLBAR_WIDTH + 1; r.right += 1; r.bottom += 1; FrameRect(&r); r.bottom -= 2; r.right -= 2; r.top = r.bottom - 9; r.left = r.right - 9; FrameRect(&r); r.top -= 3; r.left -= 2; r.bottom -= 4; r.right -= 4; FillRect(&r, white); FrameRect(&r); HUnlock(*(chatter->input_te)); } void chatter_mouse_down(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab, *ttab; Point p; Cell selected = { 0 }, now = { 0 }, t = { 0 }; ControlHandle control; Rect r; short val, adj, ret, part, n; p = event->where; GlobalToLocal(&p); if (tab->nick_list) { r = (*(tab->nick_list))->rView; r.right += SCROLLBAR_WIDTH; if (PtInRect(p, &r)) { /* store what is selected now */ ret = LGetSelect(true, &selected, tab->nick_list); /* possibly highlight a new cell */ LClick(p, event->modifiers, tab->nick_list); if (selected.v != now.v) { LSetSelect(false, selected, tab->nick_list); /* TODO: detect double-click, open query window? */ } return; } } r = (*(tab->messages_te))->viewRect; if (PtInRect(p, &r)) { TEClick(p, ((event->modifiers & shiftKey) != 0), tab->messages_te); HLock(tab->messages_te); if ((*(tab->messages_te))->selStart != (*(tab->messages_te))->selEnd) TESetSelect(0, 0, chatter->input_te); HUnlock(tab->messages_te); return; } r = (*(chatter->input_te))->viewRect; if (PtInRect(p, &r)) { TEClick(p, ((event->modifiers & shiftKey) != 0), chatter->input_te); HLock(chatter->input_te); if ((*(chatter->input_te))->selStart != (*(chatter->input_te))->selEnd) TESetSelect(0, 0, tab->messages_te); HUnlock(chatter->input_te); return; } SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (PtInRect(p, &ttab->label_rect)) { chatter_focus_tab(chatter, ttab); return; } } switch (part = FindControl(p, chatter->win, &control)) { case inUpButton: case inDownButton: case inPageUp: case inPageDown: if (control != tab->messages_scroller) break; SetTrackControlTE(tab->messages_te); TrackControl(control, p, TrackMouseDownInControl); break; case inThumb: val = GetCtlValue(control); if (TrackControl(control, p, 0L) == 0) break; adj = val - GetCtlValue(control); if (adj != 0) { val -= adj; if (control == tab->messages_scroller) TEScroll(0, adj * TEGetHeight(0, 0, tab->messages_te), tab->messages_te); SetCtlValue(control, val); } break; } } void chatter_resize(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); Rect bounds; long newsize, width, height; bounds.left = 100; bounds.top = 100; bounds.right = screenBits.bounds.right; bounds.bottom = screenBits.bounds.bottom; newsize = GrowWindow(focusable->win, event->where, &bounds); height = HiWord(newsize); width = LoWord(newsize); SizeWindow(focusable->win, width, height, true); EraseRect(&chatter->win->portRect); /* chatter_draw_tab_bar will recreate this to the new size */ if (chatter->tab_bar.baseAddr) xfree(&chatter->tab_bar.baseAddr); chatter_layout(chatter, false, NULL); } bool chatter_menu(struct focusable *focusable, short menu, short item) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab; switch (menu) { case EDIT_MENU_ID: switch (item) { case EDIT_MENU_CUT_ID: TECut(chatter->input_te); return true; case EDIT_MENU_COPY_ID: HLock(chatter->input_te); HLock(tab->messages_te); if ((*(chatter->input_te))->selStart != (*(chatter->input_te))->selEnd) TECopy(chatter->input_te); else if ((*(tab->messages_te))->selStart != (*(tab->messages_te))->selEnd) TECopy(tab->messages_te); HUnlock(tab->messages_te); HUnlock(chatter->input_te); return true; case EDIT_MENU_PASTE_ID: TEPaste(chatter->input_te); return true; } } return false; } void chatter_key_down(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab; TERec *te; char *input, k; k = (event->message & charCodeMask); if (k == '\r') { HLock(chatter->input_te); te = *(chatter->input_te); HLock(te->hText); (*(te->hText))[te->teLength] = '\0'; input = xstrdup(*(te->hText), "input"); TESetText(&k, 0, chatter->input_te); EraseRect(&te->viewRect); ValidRect(&te->viewRect); TEIdle(chatter->input_te); HUnlock(te->hText); HUnlock(chatter->input_te); irc_process_input(tab->conn, tab->channel, input); xfree(&input); } else { TEKey(k, chatter->input_te); TESelView(chatter->input_te); } } struct chatter_tab * chatter_find_tab_for_conn_and_channel(struct chatter *chatter, struct irc_connection *conn, struct irc_channel *channel) { short n; struct chatter_tab *tab; if (conn == NULL) return NULL; SLIST_FOREACH(tab, &chatter->tabs_list, list) { if (tab->conn == conn && channel == tab->channel) return tab; } return NULL; } size_t chatter_printf(struct chatter *chatter, struct irc_connection *conn, struct irc_channel *channel, const char *format, ...) { static char buf[600], buf_out[600]; struct chatter_tab *tab = chatter->current_tab, *ttab; StScrpRec *scrp_rec; ScrpSTElement *scrp_ele, *prev_scrp_ele; RgnHandle savergn; Rect zerorect = { 0, 0, 0, 0 }; va_list argptr; size_t len, n, buf_out_len, in_this_style; time_t now = Time; short line_height = 0; bool stop_formatting = false, had_activity; len = 0; if (conn != NULL) { SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (ttab->conn == conn && channel == ttab->channel) { tab = ttab; break; } } } if (tab == NULL) panic("no tab"); had_activity = tab->have_activity; if ((*(tab->messages_te))->teLength > 0) { buf[0] = '\r'; len++; } len += strftime(buf + len, sizeof(buf) - len, "$B[%H:%M]$0 ", localtime(&now)); va_start(argptr, format); len += vsnprintf(buf + len, sizeof(buf) - len, format, argptr); va_end(argptr); if (scrp_rec_h == NULL) { scrp_rec_h = xNewHandle(4 + (20 * CHATTER_SCRAP_ELEMENTS)); HLock(scrp_rec_h); memset(*scrp_rec_h, 0, (4 + (20 * CHATTER_SCRAP_ELEMENTS))); } else HLock(scrp_rec_h); line_height = CHATTER_FONT_SIZE + 3; scrp_rec = (StScrpRec *)(*scrp_rec_h); scrp_rec->scrpNStyles = 1; scrp_ele = &scrp_rec->scrpStyleTab[scrp_rec->scrpNStyles - 1]; scrp_ele->scrpStartChar = 0; scrp_ele->scrpHeight = line_height; scrp_ele->scrpAscent = CHATTER_FONT_SIZE; scrp_ele->scrpFont = CHATTER_FONT; scrp_ele->scrpSize = CHATTER_FONT_SIZE; scrp_ele->scrpFace = 0; for (n = 0, buf_out_len = 0, in_this_style = 0; n < len; n++) { if (!stop_formatting && buf[n] == '$') { if (in_this_style > 0) { scrp_rec->scrpNStyles++; if (scrp_rec->scrpNStyles >= CHATTER_SCRAP_ELEMENTS) panic("chatter_printf: too many elements"); prev_scrp_ele = scrp_ele; scrp_ele = &scrp_rec->scrpStyleTab[ scrp_rec->scrpNStyles - 1]; /* carry style forward */ memcpy(scrp_ele, prev_scrp_ele, sizeof(ScrpSTElement)); } scrp_ele->scrpStartChar = buf_out_len; switch (buf[n + 1]) { case 'B': scrp_ele->scrpFace |= bold | condense; break; case 'U': scrp_ele->scrpFace |= underline; break; case 's': scrp_ele->scrpSize--; break; case 'S': scrp_ele->scrpSize++; break; case '/': stop_formatting = true; /* FALLTHROUGH */ case '0': scrp_ele->scrpHeight = line_height; scrp_ele->scrpAscent = CHATTER_FONT_SIZE; scrp_ele->scrpFont = CHATTER_FONT; scrp_ele->scrpSize = CHATTER_FONT_SIZE; scrp_ele->scrpFace = 0; break; } n++; continue; } buf_out[buf_out_len++] = buf[n]; in_this_style++; } if (!buf_out_len) { HUnlock(scrp_rec_h); return 0; } HLock(tab->messages_te); /* check for TE overflow */ /* too many lines */ if ((*(tab->messages_te))->nLines >= (nitems((*(tab->messages_te))->lineStarts) - 10)) goto te_overflow; /* too many characters */ if ((*(tab->messages_te))->teLength >= (SHRT_MAX - 500)) goto te_overflow; /* rect of all lines is too tall */ if ((*(tab->messages_te))->nLines * line_height >= (SHRT_MAX - 100)) goto te_overflow; goto no_overflow; te_overflow: savergn = NewRgn(); GetClip(savergn); /* create an empty clip region so all TE updates are hidden */ ClipRect(&zerorect); /* select some lines at the start, delete them */ TESetSelect(0, (*(tab->messages_te))->lineStarts[5], tab->messages_te); TEDelete(tab->messages_te); /* scroll up, causing a repaint */ TEPinScroll(0, INT_MAX, tab->messages_te); /* then scroll back down to what it looked like before we did anything */ TEPinScroll(0, -INT_MAX, tab->messages_te); /* resume normal drawing */ SetClip(savergn); DisposeRgn(savergn); no_overflow: if (chatter->current_tab != tab) { savergn = NewRgn(); GetClip(savergn); /* create an empty clip region so all TE updates are hidden */ ClipRect(&zerorect); } TESetSelect(SHRT_MAX, SHRT_MAX, tab->messages_te); TEStylInsert(buf_out, buf_out_len, scrp_rec_h, tab->messages_te); HUnlock(scrp_rec_h); HUnlock(tab->messages_te); chatter_autoscroll(chatter, tab->messages_te, tab->messages_scroller); if (chatter->current_tab == tab) tab->have_activity = false; else { /* resume normal drawing */ SetClip(savergn); DisposeRgn(savergn); if (!had_activity) { tab->have_activity = true; chatter_draw_tab_bar(chatter); } } return buf_out_len; } void chatter_autoscroll(struct chatter *chatter, TEHandle te, ControlHandle scroller) { TEPinScroll(0, -INT_MAX, te); SetCtlValue(scroller, GetCtlMax(scroller)); UpdateScrollbarForTE(chatter->win, scroller, te, false); } void chatter_remove_channel(struct chatter *chatter, struct irc_channel *channel) { struct chatter_tab *tab = NULL, *ttab = NULL, *next_tab = NULL; short n; SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (ttab->conn == channel->connection) { if (ttab->channel == channel) tab = ttab; else next_tab = ttab; } } if (tab != NULL) { LDispose(tab->nick_list); DisposeControl(tab->messages_scroller); TEDispose(tab->messages_te); SLIST_REMOVE(&chatter->tabs_list, tab, chatter_tab, list); if (chatter->current_tab == tab) chatter->current_tab = NULL; } if (next_tab == NULL) chatter_draw_tab_bar(chatter); else chatter_focus_tab(chatter, next_tab); /* * Don't bother updating titlebar because we should be inside * irc_dealloc_channel which will then call chatter_update_titlebar */ } void chatter_sync_nick_list(struct chatter *chatter, struct irc_channel *channel, bool just_summary) { size_t n, i, j, tj, ops, voices; short ret, cellv; struct irc_channel_nick *nick = NULL; struct chatter_tab *tab; tab = chatter_find_tab_for_conn_and_channel(chatter, channel->connection, channel); if (!tab) return; if (!just_summary && tab == chatter->current_tab) { LDoDraw(false, tab->nick_list); LDelRow(0, 0, tab->nick_list); } if (channel) { cellv = 0; ops = voices = 0; nick = &channel->nicks[channel->first_nick]; while (nick) { if (nick->flags & IRC_NICK_FLAG_OP) ops++; else if (nick->flags & IRC_NICK_FLAG_VOICE) voices++; if (!just_summary) chatter_insert_to_nick_list(chatter, channel, nick, cellv); cellv++; if (nick->next_nick == -1) break; nick = &channel->nicks[nick->next_nick]; } if (just_summary) chatter_printf(chatter, channel->connection, channel, "$B%s$0: Total of $B%ld$0 nick%s $B(%ld$0 op%s, $B%ld$0 " "voice%s$B)$0", channel->name, channel->nnicks, channel->nnicks == 1 ? "" : "s", ops, ops == 1 ? "" : "s", voices, voices == 1 ? "" : "s"); } if (!just_summary) { LDoDraw(true, tab->nick_list); InvalRect(&(*(tab->nick_list))->rView); } } void chatter_insert_to_nick_list(struct chatter *chatter, struct irc_channel *channel, struct irc_channel_nick *nick, short pos) { Cell cell = { 0, 0 }; struct irc_channel_nick tnick; struct chatter_tab *tab; short j = 0; tab = chatter_find_tab_for_conn_and_channel(chatter, channel->connection, channel); if (!tab) return; if (nick->flags & IRC_NICK_FLAG_OP) { tnick.nick[0] = '@'; j++; } else if (nick->flags & IRC_NICK_FLAG_VOICE) { tnick.nick[0] = '+'; j++; } cell.v = pos; j += strlcpy(tnick.nick + j, nick->nick, sizeof(tnick.nick) - j); LAddRow(1, cell.v, tab->nick_list); LSetCell(&tnick.nick, j, cell, tab->nick_list); }