/* * 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 "settings.h" #include "util.h" #include "utf8.h" #define NICK_LIST_WIDTH 75 #define CHATTER_SCRAP_ELEMENTS 20 #define MAX_TAB_WIDTH 120 #define TAB_BAR_HEIGHT 15 static Handle scrp_rec_h = NULL; static Pattern tab_bar_pattern; static Rect zerorect = { 0, 0, 0, 0 }; static BitMap shadow_cur_bits; void chatter_layout(struct chatter *chatter, bool init, Rect *init_bounds); void chatter_draw_tab_bar(struct chatter *chatter); void chatter_layout_tab(struct chatter *chatter, struct chatter_tab *tab, Rect *win_bounds, bool init); 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, bool force); void chatter_set_input(struct chatter *chatter, char *input, size_t len); 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_update_menu(struct focusable *focusable); 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); void chatter_use_shadow(struct chatter *chatter); void chatter_reveal_shadow(struct chatter *chatter); bool chatter_tab_complete(struct chatter *chatter); struct chatter * chatter_init(const char *server, const unsigned short port, const char *password, const char *nick, const char *ident, const char *realname, bool hide_motd, const char *channel) { struct focusable *focusable; struct chatter *chatter; struct chatter_tab *tab; char title[64]; Rect bounds = { 0 }; short padding = 20, width, height; GetIndPattern(&tab_bar_pattern, sysPatListID, 23); chatter = xmalloczero(sizeof(struct chatter)); if (chatter == NULL) return NULL; SLIST_INIT(&chatter->tabs_list); focusable = xmalloczero(sizeof(struct focusable)); if (focusable == NULL) { xfree(&chatter); return NULL; } chatter->focusable = focusable; width = screenBits.bounds.right - screenBits.bounds.left - padding; width = MIN(width, 640); height = screenBits.bounds.bottom - screenBits.bounds.top - padding - (GetMBarHeight() * 2); height = MIN(height, 350); center_in_screen(width, height, true, &bounds); width = screenBits.bounds.right - screenBits.bounds.left; height = screenBits.bounds.bottom - screenBits.bounds.top; chatter->shadow.rowBytes = (((width - 1) / 16) + 1) * 2; chatter->shadow.baseAddr = xmalloczero((long)chatter->shadow.rowBytes * height); if (chatter->shadow.baseAddr == NULL) { xfree(&chatter); xfree(&focusable); warn("malloc(%ld) failed", (long)(chatter->shadow.rowBytes * height)); return NULL; } 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->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->update_menu = chatter_update_menu; focusable->resume = chatter_resume; focusable_add(focusable); chatter_update_menu(focusable); chatter_draw_tab_bar(chatter); snprintf(title, sizeof(title), "Disconnected"); CtoPstr(title); InsMenuItem(window_menu, title, WINDOW_MENU_N_ID + focusable->id); chatter_printf(chatter, NULL, NULL, "$B***$0 Welcome to %s %s", PROGRAM_NAME, get_version(false)); tab = SLIST_FIRST(&chatter->tabs_list); tab->conn = irc_connect(chatter, server, port, password, nick, ident, realname, hide_motd, channel); DrawControls(chatter->win); chatter_update_titlebar(chatter); chatter_draw_tab_bar(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 + 3; bounds.right = win_bounds->right - SCROLLBAR_WIDTH + 1 - 3; bounds.top = win_bounds->bottom - SCROLLBAR_WIDTH + 1; bounds.bottom = win_bounds->bottom; inset_bounds = bounds; inset_bounds.top += 1; inset_bounds.bottom -= 1; inset_bounds.right = win_bounds->right * 2; if (init) { chatter->input_te = TENew(&inset_bounds, &bounds); HLock(chatter->input_te); (*(chatter->input_te))->crOnly = -1; HUnlock(chatter->input_te); TEAutoView(true, chatter->input_te); TEActivate(chatter->input_te); } else { HLock(chatter->input_te); (*(chatter->input_te))->viewRect = bounds; (*(chatter->input_te))->destRect = inset_bounds; TECalText(chatter->input_te); HUnlock(chatter->input_te); } if (init) { chatter_add_tab(chatter, win_bounds, NULL, NULL, NULL); } else { SLIST_FOREACH(tab, &chatter->tabs_list, list) { chatter_layout_tab(chatter, tab, win_bounds, false); } } } struct chatter_tab * chatter_add_tab(struct chatter *chatter, Rect *win_bounds, struct irc_connection *conn, struct irc_channel *channel, char *query_nick) { 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 (channel && query_nick) panic("chatter_add_tab called for both channel and query"); if (conn) chatter_use_shadow(chatter); if (conn && (tab = chatter_find_tab(chatter, conn, channel ? channel->name : query_nick))) { chatter_focus_tab(chatter, tab); chatter_reveal_shadow(chatter); return tab; } if (win_bounds == NULL) win_bounds = &chatter->win->portRect; tab = xmalloczero(sizeof(struct chatter_tab)); if (tab == NULL) panic("Out of memory for new tab"); SLIST_APPEND(&chatter->tabs_list, tab, chatter_tab, list); chatter->ntabs++; tab->conn = conn; tab->channel = channel; if (query_nick) strlcpy(tab->query_nick, query_nick, sizeof(tab->query_nick)); chatter_layout_tab(chatter, tab, win_bounds, true); chatter_focus_tab(chatter, tab); if (conn) chatter_reveal_shadow(chatter); return tab; } void chatter_layout_tab(struct chatter *chatter, struct chatter_tab *tab, Rect *win_bounds, bool init) { 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; 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; if (init) { 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); HLock(tab->nick_list); (*(tab->nick_list))->selFlags = lOnlyOne | lNoNilHilite; } else { HLock(tab->nick_list); (*(tab->nick_list))->rView = bounds; LSize(bounds.right - bounds.left, bounds.bottom - bounds.top, tab->nick_list); } } /* 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; if (init) tab->messages_scroller = NewControl(chatter->win, &bounds, "\p", true, 1, 1, 1, scrollBarProc, 0L); else { HLock(tab->messages_scroller); MoveControl(tab->messages_scroller, bounds.left, bounds.top); SizeControl(tab->messages_scroller, bounds.right - bounds.left, bounds.bottom - bounds.top); } /* 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); EraseRect(&bounds); if (init) { 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, false); } else { HLock(tab->messages_te); (*(tab->messages_te))->viewRect = bounds; (*(tab->messages_te))->destRect = inset_bounds; TECalText(tab->messages_te); } HUnlock(tab->messages_te); HUnlock(tab->messages_scroller); if (tab->channel) HUnlock(tab->nick_list); } void chatter_set_input(struct chatter *chatter, char *input, size_t len) { TESetText(input, len, chatter->input_te); TEPinScroll(-SHRT_MAX, SHRT_MAX, chatter->input_te); TEIdle(chatter->input_te); /* this is needed to get TEIdle working again */ TESelView(chatter->input_te); } void chatter_focus_tab(struct chatter *chatter, struct chatter_tab *tab) { RgnHandle clip; Rect r; if (chatter->current_tab && chatter->current_tab == tab) return; chatter_use_shadow(chatter); if (chatter->current_tab) { /* * Doing the HideControl takes out the top line of our tab bar, * so clip to just above it */ HLock(chatter->current_tab->messages_scroller); HLock(chatter->current_tab->nick_list); GetClip(clip = NewRgn()); r.left = 0; r.top = 0; r.bottom = (*(chatter->current_tab->messages_scroller))->contrlRect.bottom - 1; if (chatter->current_tab->nick_list) r.right = (*(chatter->current_tab->nick_list))->rView.right; else r.right = (*(chatter->current_tab->messages_scroller))->contrlRect.right; ClipRect(&r); HUnlock(chatter->current_tab->nick_list); HUnlock(chatter->current_tab->messages_scroller); EraseRect(&r); TEDeactivate(chatter->current_tab->messages_te); if (chatter->current_tab->nick_list) { LActivate(false, chatter->current_tab->nick_list); LDoDraw(false, chatter->current_tab->nick_list); } /* HideControl will flash, clip it out */ r.right = 0; ClipRect(&r); HideControl(chatter->current_tab->messages_scroller); SetClip(clip); DisposeRgn(clip); } chatter->current_tab = tab; HLock(tab->messages_te); EraseRect(&(*(tab->messages_te))->viewRect); TEActivate(tab->messages_te); TEUpdate(&(*(tab->messages_te))->viewRect, tab->messages_te); HUnlock(tab->messages_te); ShowControl(tab->messages_scroller); if (tab->nick_list) { HLock(tab->nick_list); EraseRect(&(*(tab->nick_list))->rView); LDoDraw(true, tab->nick_list); LActivate(true, tab->nick_list); LUpdate(chatter->win->visRgn, tab->nick_list); HUnlock(tab->nick_list); } HLock(chatter->input_te); EraseRect(&(*(chatter->input_te))->viewRect); TEUpdate(&(*(chatter->input_te))->viewRect, chatter->input_te); HUnlock(chatter->input_te); DrawControls(chatter->win); chatter_draw_tab_bar(chatter); chatter_reveal_shadow(chatter); if (chatter->focusable) chatter_update_menu(chatter->focusable); /* invalidate any current tab completion */ chatter->tab_comp_match[0] = chatter->tab_comp_input[0] = '\0'; } void chatter_resume(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); focusable_show(focusable); if (chatter->current_tab->nick_list) LActivate(true, chatter->current_tab->nick_list); TEActivate(chatter->current_tab->messages_te); TEActivate(chatter->input_te); InvalRect(&chatter->win->portRect); } 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; } DelMenuItem(window_menu, WINDOW_MENU_N_ID + focusable->id); 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->shadow.baseAddr) xfree(&chatter->shadow.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], menu_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); strlcpy(menu_title, "Disconnected", sizeof(menu_title)); } else { snprintf(title, sizeof(title), "%s: %s@%s", PROGRAM_NAME, tab->conn->nick, tab->conn->hostname); snprintf(menu_title, sizeof(menu_title), "%s@%s", tab->conn->nick, tab->conn->hostname); } GetWTitle(chatter->win, &curtitle); PtoCstr(curtitle); if (strcmp((char *)&curtitle, title) != 0) SetWTitle(chatter->win, CtoPstr(title)); CtoPstr(menu_title); SetItem(window_menu, WINDOW_MENU_N_ID + chatter->focusable->id, menu_title); } void chatter_idle(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab; short n, was_state; bool redraw = false; TEIdle(chatter->input_te); SLIST_FOREACH(tab, &chatter->tabs_list, list) { was_state = tab->conn->state; irc_process(tab->conn); if (tab->conn->state != was_state) redraw = true; } if (chatter->need_tab_bar_redraw) { redraw = true; chatter->need_tab_bar_redraw = false; } if (redraw) { chatter_draw_tab_bar(chatter); chatter_update_titlebar(chatter); } if (chatter->shadow_refcnt != 0) { warn("shadow refcnt %d", chatter->shadow_refcnt); chatter_reveal_shadow(chatter); chatter->shadow_refcnt = 0; } } void chatter_update(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab; Rect r; short what = -1; if (event != NULL) what = event->what; switch (what) { case -1: case updateEvt: chatter_use_shadow(chatter); TextFont(applFont); TextSize(10); EraseRect(&chatter->win->portRect); if (tab->nick_list) { HLock(tab->nick_list); r = (*(tab->nick_list))->rView; HUnlock(tab->nick_list); LUpdate(chatter->win->visRgn, tab->nick_list); InsetRect(&r, -1, -1); FrameRect(&r); } HLock(tab->messages_te); r = (*(tab->messages_te))->viewRect; InsetRect(&r, -1, -1); FrameRect(&r); TEUpdate(&(*(tab->messages_te))->viewRect, tab->messages_te); HUnlock(tab->messages_te); HLock(chatter->input_te); r = (*(chatter->input_te))->viewRect; InsetRect(&r, -4, -1); FrameRect(&r); TEUpdate(&(*(chatter->input_te))->viewRect, chatter->input_te); HUnlock(chatter->input_te); DrawControls(chatter->win); chatter_draw_tab_bar(chatter); chatter_reveal_shadow(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) { static char label[64]; char *tlabel; 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; 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; HLock(chatter->input_te); r.left = 0; r.right = chatter->win->portRect.right - chatter->win->portRect.left; r.bottom = (*(chatter->input_te))->viewRect.top; r.top = r.bottom - TAB_BAR_HEIGHT; HUnlock(chatter->input_te); 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; EraseRect(&r); FrameRect(&r); if (tab == chatter->current_tab) { FrameRect(&r2); EraseRect(&r2); tab->have_activity = false; } ClipRect(&r); tlabel = label; if (tab->channel && tab->channel->mode[0]) len = snprintf(label, sizeof(label), "%s (%s)", tab->channel->name, tab->channel->mode); else if (tab->channel) len = strlcpy(label, tab->channel->name, sizeof(label)); else if (tab->query_nick[0]) len = strlcpy(label, tab->query_nick, sizeof(label)); else if (tab->conn->state >= IRC_STATE_UNREGISTERED) len = strlcpy(label, tab->conn->hostname, sizeof(label)); else { tlabel = (char *)&no_connection; len = sizeof(no_connection) - 1; } if (tab->have_activity) TextFace(bold | condense); else TextFace(0); width = TextWidth(tlabel, 0, len); if (width > tab_width - 4) width = tab_width - 4; MoveTo(r.left + ((tab_width - width) / 2), r.bottom - 3); DrawText(tlabel, 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); 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; EraseRect(&r); 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; bool dclick; char nick[member_size(struct irc_user, nick) + 1]; p = event->where; GlobalToLocal(&p); if (tab->nick_list) { HLock(tab->nick_list); r = (*(tab->nick_list))->rView; HUnlock(tab->nick_list); r.right += SCROLLBAR_WIDTH; if (PtInRect(p, &r)) { /* store what is selected now */ LGetSelect(true, &selected, tab->nick_list); /* possibly highlight a new cell or scroll */ dclick = LClick(p, event->modifiers, tab->nick_list); LGetSelect(true, &now, tab->nick_list); if (selected.v != now.v) LSetSelect(false, selected, tab->nick_list); if (dclick) { /* double-click, query this user (or focus existing) */ n = sizeof(nick) - 2; LGetCell(&nick, &n, now, tab->nick_list); nick[n] = '\0'; n = 0; if (nick[0] == '@' || nick[0] == '+') n = 1; chatter_add_tab(chatter, NULL, tab->conn, NULL, nick + n); } return; } } HLock(tab->messages_te); 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; } HUnlock(tab->messages_te); HLock(chatter->input_te); 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; } HUnlock(chatter->input_te); 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); struct chatter_tab *ttab; RgnHandle savergn; 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); InvalRect(&chatter->win->portRect); SizeWindow(focusable->win, width, height, true); EraseRect(&chatter->win->portRect); /* update each tab that isn't the current one */ savergn = NewRgn(); GetClip(savergn); /* create an empty clip region so all TE updates are hidden */ ClipRect(&zerorect); SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (ttab == chatter->current_tab) continue; chatter_layout_tab(chatter, ttab, NULL, false); HLock(ttab->messages_te); TEUpdate(&(*(ttab->messages_te))->viewRect, ttab->messages_te); HUnlock(ttab->messages_te); chatter_autoscroll(chatter, ttab->messages_te, ttab->messages_scroller, true); } /* resume normal drawing */ SetClip(savergn); DisposeRgn(savergn); /* update the current tab as-is */ chatter_use_shadow(chatter); chatter_layout(chatter, false, NULL); HLock(chatter->current_tab->messages_te); TEUpdate(&(*(chatter->current_tab->messages_te))->viewRect, chatter->current_tab->messages_te); HUnlock(chatter->current_tab->messages_te); TEPinScroll(0, SHRT_MAX, chatter->current_tab->messages_te); TEPinScroll(0, -SHRT_MAX, chatter->current_tab->messages_te); chatter_autoscroll(chatter, chatter->current_tab->messages_te, chatter->current_tab->messages_scroller, true); chatter_update(focusable, NULL); chatter_reveal_shadow(chatter); ValidRect(&chatter->win->portRect); } bool chatter_menu(struct focusable *focusable, short menu, short item) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab, *ttab; 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; } break; case VIEW_MENU_ID: switch (item) { case VIEW_MENU_PREV_TAB_ID: { struct chatter_tab *last_tab = NULL; SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (ttab == tab) { if (last_tab != NULL) { chatter_use_shadow(chatter); chatter_focus_tab(chatter, last_tab); chatter_reveal_shadow(chatter); } /* TODO: otherwise wrap around to last tab? */ break; } last_tab = ttab; } return true; } case VIEW_MENU_NEXT_TAB_ID: if ((ttab = SLIST_NEXT(tab, list))) { chatter_use_shadow(chatter); chatter_focus_tab(chatter, ttab); chatter_reveal_shadow(chatter); } /* TODO: otherwise wrap around to first tab? */ return true; case VIEW_MENU_CLOSE_ID: if (chatter->current_tab->query_nick[0] || chatter->current_tab->channel) chatter_close_tab(chatter, chatter->current_tab); return true; } } return false; } void chatter_update_menu(struct focusable *focusable) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab; if (!chatter) return; HLock(chatter->input_te); HLock(tab->messages_te); if (chatter->current_tab == SLIST_FIRST(&chatter->tabs_list)) DisableItem(view_menu, VIEW_MENU_PREV_TAB_ID); else EnableItem(view_menu, VIEW_MENU_PREV_TAB_ID); if (SLIST_NEXT(chatter->current_tab, list)) EnableItem(view_menu, VIEW_MENU_NEXT_TAB_ID); else DisableItem(view_menu, VIEW_MENU_NEXT_TAB_ID); if (chatter->current_tab->query_nick[0] || chatter->current_tab->channel) EnableItem(view_menu, VIEW_MENU_CLOSE_ID); else DisableItem(view_menu, VIEW_MENU_CLOSE_ID); EnableItem(window_menu, WINDOW_MENU_HIDE_ID); EnableItem(edit_menu, EDIT_MENU_PASTE_ID); if ((*(chatter->input_te))->selStart != (*(chatter->input_te))->selEnd) { EnableItem(edit_menu, EDIT_MENU_CUT_ID); EnableItem(edit_menu, EDIT_MENU_COPY_ID); goto done; } if ((*(tab->messages_te))->selStart != (*(tab->messages_te))->selEnd) { DisableItem(edit_menu, EDIT_MENU_CUT_ID); EnableItem(edit_menu, EDIT_MENU_COPY_ID); goto done; } DisableItem(edit_menu, EDIT_MENU_CUT_ID); DisableItem(edit_menu, EDIT_MENU_COPY_ID); done: HUnlock(tab->messages_te); HUnlock(chatter->input_te); } void chatter_key_down(struct focusable *focusable, EventRecord *event) { struct chatter *chatter = (struct chatter *)(focusable->cookie); struct chatter_tab *tab = chatter->current_tab, *ttab; TERec *te; short n; char k; k = (event->message & charCodeMask); if ((event->modifiers & cmdKey) != 0) { /* cmd+number focuses that tab */ if (k >= '1' && k <= '9') { n = 0; SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (n == (k - '1')) { chatter_use_shadow(chatter); chatter_focus_tab(chatter, ttab); chatter_reveal_shadow(chatter); break; } n++; } } return; } if (k == '\t') { chatter_tab_complete(chatter); return; } if (chatter->tab_comp_input[0] != '\0') { chatter->tab_comp_input[0] = '\0'; chatter->tab_comp_match[0] = '\0'; } HLock(chatter->input_te); te = *(chatter->input_te); if (k == '\r') { if (te->teLength == 0) { HUnlock(chatter->input_te); return; } HLock(te->hText); memcpy(chatter->input, *(te->hText), MIN(te->teLength, sizeof(chatter->input) - 1)); HUnlock(te->hText); chatter->input[MIN(te->teLength, sizeof(chatter->input) - 1)] = '\0'; EraseRect(&te->viewRect); chatter_set_input(chatter, "", 0); irc_process_input(tab->conn, tab->channel, (tab->query_nick[0] ? tab->query_nick : NULL), chatter->input); } else if (te->teLength < IRC_MAX_MSG_SIZE - 1) { TEKey(k, chatter->input_te); TESelView(chatter->input_te); } else SysBeep(10); HUnlock(chatter->input_te); } struct chatter_tab * chatter_find_tab(struct chatter *chatter, struct irc_connection *conn, char *dest_tab) { short n; struct chatter_tab *tab; SLIST_FOREACH(tab, &chatter->tabs_list, list) { if (conn != tab->conn) continue; if (dest_tab == NULL || dest_tab[0] == '\0') { /* connection tab */ if (tab->channel == NULL && tab->query_nick[0] == '\0') return tab; } else if (tab->channel) { if (strcasecmp(tab->channel->name, dest_tab) == 0) return tab; } else if (tab->query_nick[0]) { if (strcasecmp(tab->query_nick, dest_tab) == 0) return tab; } } return NULL; } size_t chatter_printf(struct chatter *chatter, struct irc_connection *conn, char *dest_tab, const char *format, ...) { static char buf[600], buf_out[600], conv_buf[601]; struct chatter_tab *tab = chatter->current_tab, *ttab; StScrpRec *scrp_rec; ScrpSTElement *scrp_ele, *prev_scrp_ele; RgnHandle savergn; GrafPtr old_port; 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; tab = chatter_find_tab(chatter, conn, dest_tab); if (tab == NULL) { tab = SLIST_FIRST(&chatter->tabs_list); if (tab == NULL) panic("chatter_printf: no tab for %s", dest_tab); } had_activity = tab->have_activity; HLock(tab->messages_te); 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); len = utf8_to_macroman_string(buf, len, conv_buf); 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 && conv_buf[n] == '$') { if (in_this_style > 0) { scrp_rec->scrpNStyles++; if (scrp_rec->scrpNStyles >= CHATTER_SCRAP_ELEMENTS) { warn("chatter_printf: too many elements"); return; } 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 (conv_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++] = conv_buf[n]; in_this_style++; } if (!buf_out_len) { HUnlock(scrp_rec_h); HUnlock(tab->messages_te); return 0; } /* 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, SHRT_MAX, tab->messages_te); /* then scroll back down to what it looked like before we did anything */ TEPinScroll(0, -SHRT_MAX, tab->messages_te); /* resume normal drawing */ SetClip(savergn); DisposeRgn(savergn); no_overflow: GetPort(&old_port); SetPort(chatter->win); 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, false); if (chatter->current_tab == tab) { tab->have_activity = false; } else { /* resume normal drawing */ SetClip(savergn); DisposeRgn(savergn); if (!had_activity && !tab->ignore_activity) { tab->have_activity = true; chatter_draw_tab_bar(chatter); } } SetPort(old_port); return buf_out_len; } void chatter_autoscroll(struct chatter *chatter, TEHandle te, ControlHandle scroller, bool force) { /* only scroll down if we're already at the last line */ if (force || GetCtlValue(scroller) == GetCtlMax(scroller)) { TEPinScroll(0, -SHRT_MAX, te); SetCtlValue(scroller, GetCtlMax(scroller)); } UpdateScrollbarForTE(chatter->win, scroller, te, false); } void chatter_clear_messages(struct chatter *chatter, struct chatter_tab *tab) { RgnHandle savergn; savergn = NewRgn(); GetClip(savergn); ClipRect(&zerorect); TESetText(NULL, 0, tab->messages_te); /* scroll up, causing a repaint */ TEPinScroll(0, SHRT_MAX, tab->messages_te); /* then scroll back down to what it looked like before we did anything */ TEPinScroll(0, -SHRT_MAX, tab->messages_te); chatter_autoscroll(chatter, tab->messages_te, tab->messages_scroller, true); SetClip(savergn); DisposeRgn(savergn); if (tab == chatter->current_tab) { chatter_update(chatter->focusable, NULL); ValidRect(&chatter->win->portRect); } } void chatter_close_tab(struct chatter *chatter, struct chatter_tab *tab) { struct chatter_tab *ttab = NULL, *next_tab = NULL; struct irc_connection *conn = NULL; struct irc_channel *channel = NULL; short n; if (!tab) return; conn = tab->conn; if (tab->channel) channel = tab->channel; SLIST_FOREACH(ttab, &chatter->tabs_list, list) { if (ttab->conn != tab->conn) continue; if (tab != ttab) next_tab = ttab; } chatter_use_shadow(chatter); if (tab->nick_list) LDispose(tab->nick_list); DisposeControl(tab->messages_scroller); TEDispose(tab->messages_te); SLIST_REMOVE(&chatter->tabs_list, tab, chatter_tab, list); chatter->ntabs--; if (chatter->current_tab == tab) chatter->current_tab = NULL; xfree(&tab); if (channel) irc_part_channel(conn, channel); if (next_tab == NULL) { chatter_draw_tab_bar(chatter); chatter_update_menu(chatter->focusable); } else chatter_focus_tab(chatter, next_tab); chatter_reveal_shadow(chatter); } void chatter_sync_nick_list(struct chatter *chatter, struct irc_channel *channel) { 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(chatter, channel->connection, channel->name); if (!tab) return; 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++; chatter_insert_to_nick_list(chatter, channel, nick, cellv); cellv++; if (nick->next_nick == -1) break; nick = &channel->nicks[nick->next_nick]; } tab->ignore_activity = true; chatter_printf(chatter, channel->connection, channel->name, "$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"); tab->ignore_activity = false; } LDoDraw(true, tab->nick_list); if (tab == chatter->current_tab) { HLock(tab->nick_list); InvalRect(&(*(tab->nick_list))->rView); HUnlock(tab->nick_list); } } 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(chatter, channel->connection, channel->name); 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); } void chatter_remove_from_nick_list(struct chatter *chatter, struct irc_channel *channel, struct irc_channel_nick *nick, short pos) { struct chatter_tab *tab; tab = chatter_find_tab(chatter, channel->connection, channel->name); if (!tab) return; if (pos == -1) { /* TODO: find nick in list */ } LDelRow(1, pos, tab->nick_list); } void chatter_use_shadow(struct chatter *chatter) { if (++chatter->shadow_refcnt != 1) return; shadow_cur_bits = thePort->portBits; SetRect(&chatter->shadow.bounds, 0, 0, chatter->win->portRect.right - chatter->win->portRect.left, chatter->win->portRect.bottom - chatter->win->portRect.top); SetPortBits(&chatter->shadow); CopyBits(&chatter->win->portBits, &chatter->shadow, &chatter->shadow.bounds, &chatter->shadow.bounds, srcCopy, nil); } void chatter_reveal_shadow(struct chatter *chatter) { if (--chatter->shadow_refcnt != 0) return; SetPortBits(&shadow_cur_bits); CopyBits(&chatter->shadow, &chatter->win->portBits, &chatter->shadow.bounds, &chatter->shadow.bounds, srcCopy, nil); ValidRect(&chatter->win->portRect); } bool chatter_tab_complete(struct chatter *chatter) { TERec *te; size_t size; long n; struct irc_channel *channel; struct irc_channel_nick *nick; char *skip_until = NULL; if (!chatter->current_tab || !chatter->current_tab->channel) return false; if (chatter->tab_comp_input[0] == '\0') { /* new search, store original input */ HLock(chatter->input_te); te = *(chatter->input_te); HLock(te->hText); size = MIN(te->teLength, sizeof(chatter->tab_comp_input) - 1); memcpy(chatter->tab_comp_input, *(te->hText), size); chatter->tab_comp_input[size] = '\0'; } else size = strlen(chatter->tab_comp_input); /* if previous match, skip nicks in chain until we see it again */ if (chatter->tab_comp_match[0] != '\0') skip_until = chatter->tab_comp_match; channel = chatter->current_tab->channel; nick = &channel->nicks[channel->first_nick]; while (nick) { if (strncasecmp(nick->nick, chatter->tab_comp_input, size) != 0) goto next_nick; /* but don't let us win */ if (strcasecmp(nick->nick, channel->connection->nick) == 0) goto next_nick; /* if we had a last match, skip until we see it again */ if (skip_until) { if (strcmp(nick->nick, skip_until) == 0) skip_until = NULL; goto next_nick; } strlcpy(chatter->tab_comp_match, nick->nick, sizeof(chatter->tab_comp_match)); size = snprintf(chatter->input, sizeof(chatter->input), "%s: ", nick->nick); chatter_set_input(chatter, chatter->input, size); return true; next_nick: if (nick->next_nick == -1) break; nick = &channel->nicks[nick->next_nick]; } /* no match (or cycled through all matches), return to original input */ if (chatter->tab_comp_match[0] != '\0') { chatter_set_input(chatter, chatter->tab_comp_input, strlen(chatter->tab_comp_input)); chatter->tab_comp_match[0] = chatter->tab_comp_input[0] = '\0'; } return false; }