Download
jcs
/wallops
/chatter.c
(View History)
jcs chatter: Tab bar is no longer a GrafPort, rename it | Latest amendment: 48 on 2023-01-18 |
1 | /* |
2 | * Copyright (c) 2021-2022 joshua stein <jcs@jcs.org> |
3 | * |
4 | * Permission to use, copy, modify, and distribute this software for any |
5 | * purpose with or without fee is hereby granted, provided that the above |
6 | * copyright notice and this permission notice appear in all copies. |
7 | * |
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
15 | */ |
16 | |
17 | #include <stdio.h> |
18 | #include <stdarg.h> |
19 | #include <string.h> |
20 | #include "chatter.h" |
21 | #include "irc.h" |
22 | #include "util.h" |
23 | |
24 | #define NICK_LIST_WIDTH 75 |
25 | #define CHATTER_SCRAP_ELEMENTS 20 |
26 | #define MAX_TAB_WIDTH 100 |
27 | #define TAB_BAR_HEIGHT 15 |
28 | |
29 | static Handle scrp_rec_h = NULL; |
30 | static Pattern tab_bar_pattern; |
31 | |
32 | void chatter_layout(struct chatter *chatter, bool init, Rect *init_bounds); |
33 | void chatter_draw_tab_bar(struct chatter *chatter); |
34 | void chatter_focus_tab(struct chatter *chatter, struct chatter_tab *tab); |
35 | void chatter_draw_grow_icon(struct chatter *chatter); |
36 | void chatter_autoscroll(struct chatter *chatter, TEHandle te, |
37 | ControlHandle scroller); |
38 | short chatter_wait_type(struct focusable *focusable); |
39 | void chatter_key_down(struct focusable *focusable, EventRecord *event); |
40 | void chatter_mouse_down(struct focusable *focusable, EventRecord *event); |
41 | void chatter_resize(struct focusable *focusable, EventRecord *event); |
42 | bool chatter_menu(struct focusable *focusable, short menu, short item); |
43 | void chatter_idle(struct focusable *focusable, EventRecord *event); |
44 | void chatter_update(struct focusable *focusable, EventRecord *event); |
45 | void chatter_resume(struct focusable *focusable, EventRecord *event); |
46 | bool chatter_close(struct focusable *focusable); |
47 | bool chatter_quit(struct focusable *focusable); |
48 | struct chatter_tab * chatter_find_tab_for_conn_and_channel( |
49 | struct chatter *chatter, struct irc_connection *conn, |
50 | struct irc_channel *channel); |
51 | |
52 | struct chatter * |
53 | chatter_init(const char *server, const unsigned short port, |
54 | const char *password, const char *nick, const char *ident, |
55 | const char *realname, const char *channel) |
56 | { |
57 | struct focusable *focusable; |
58 | struct chatter *chatter; |
59 | struct chatter_tab *tab; |
60 | char title[64]; |
61 | Rect bounds = { 0 }; |
62 | short padding = 20; |
63 | |
64 | GetIndPattern(&tab_bar_pattern, sysPatListID, 23); |
65 | |
66 | chatter = xmalloczero(sizeof(struct chatter), "chatter"); |
67 | SLIST_INIT(&chatter->tabs_list); |
68 | |
69 | bounds.left = padding; |
70 | bounds.top = screenBits.bounds.top + padding + |
71 | (GetMBarHeight() * 2) - 1; |
72 | bounds.right = screenBits.bounds.right - padding - 1; |
73 | bounds.bottom = screenBits.bounds.bottom - padding - 1; |
74 | |
75 | snprintf(title, sizeof(title), "%s: Disconnected", PROGRAM_NAME); |
76 | chatter->win = NewWindow(0L, &bounds, CtoPstr(title), false, |
77 | documentProc, (WindowPtr)-1L, true, 0); |
78 | if (!chatter->win) |
79 | panic("Can't create chatter window"); |
80 | |
81 | SetPort(chatter->win); |
82 | TextFont(applFont); |
83 | TextSize(CHATTER_FONT_SIZE); |
84 | |
85 | bounds.right -= bounds.left; |
86 | bounds.bottom -= bounds.top; |
87 | bounds.top = bounds.left = 0; |
88 | chatter_layout(chatter, true, &bounds); |
89 | |
90 | focusable = xmalloczero(sizeof(struct focusable), "focusable"); |
91 | focusable->win = chatter->win; |
92 | focusable->cookie = chatter; |
93 | focusable->wait_type = chatter_wait_type; |
94 | focusable->idle = chatter_idle; |
95 | focusable->key_down = chatter_key_down; |
96 | focusable->mouse_down = chatter_mouse_down; |
97 | focusable->update = chatter_update; |
98 | focusable->close = chatter_close; |
99 | focusable->quit = chatter_quit; |
100 | focusable->resize = chatter_resize; |
101 | focusable->menu = chatter_menu; |
102 | focusable->resume = chatter_resume; |
103 | focusable_add(focusable); |
104 | chatter->focusable = focusable; |
105 | |
106 | chatter_draw_tab_bar(chatter); |
107 | |
108 | chatter_printf(chatter, NULL, NULL, |
109 | "$B***$0 Welcome to %s", |
110 | PROGRAM_NAME); |
111 | |
112 | tab = SLIST_FIRST(&chatter->tabs_list); |
113 | tab->conn = irc_connect(chatter, server, port, password, nick, ident, |
114 | realname, channel); |
115 | DrawControls(chatter->win); |
116 | chatter_update_titlebar(chatter); |
117 | |
118 | ValidRect(&bounds); |
119 | |
120 | return chatter; |
121 | } |
122 | |
123 | short |
124 | chatter_wait_type(struct focusable *focusable) |
125 | { |
126 | struct irc_connection *conn; |
127 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
128 | short n; |
129 | |
130 | SLIST_FOREACH(conn, &irc_connections_list, list) { |
131 | if (conn->ibuflen) |
132 | return WAIT_TYPE_URGENT; |
133 | } |
134 | |
135 | if (!focusable->visible) |
136 | return WAIT_TYPE_BACKGROUND; |
137 | |
138 | return WAIT_TYPE_FOREGROUND; |
139 | } |
140 | |
141 | void |
142 | chatter_layout(struct chatter *chatter, bool init, Rect *win_bounds) |
143 | { |
144 | Rect bounds, inset_bounds; |
145 | Rect control_bounds = { 0 }; |
146 | struct chatter_tab *tab; |
147 | |
148 | if (win_bounds == NULL) |
149 | win_bounds = &chatter->win->portRect; |
150 | |
151 | /* input */ |
152 | bounds.left = win_bounds->left; |
153 | bounds.right = win_bounds->right - SCROLLBAR_WIDTH + 1; |
154 | bounds.top = win_bounds->bottom - SCROLLBAR_WIDTH + 1; |
155 | bounds.bottom = win_bounds->bottom; |
156 | if (init) { |
157 | inset_bounds = bounds; |
158 | InsetRect(&inset_bounds, 3, 1); |
159 | inset_bounds.right = win_bounds->right * 2; |
160 | chatter->input_te = TENew(&inset_bounds, &bounds); |
161 | (*(chatter->input_te))->crOnly = -1; |
162 | TEAutoView(true, chatter->input_te); |
163 | TEActivate(chatter->input_te); |
164 | } else { |
165 | (*(chatter->input_te))->viewRect = bounds; |
166 | InsetRect(&bounds, 3, 1); |
167 | (*(chatter->input_te))->destRect = bounds; |
168 | TECalText(chatter->input_te); |
169 | } |
170 | |
171 | if (init) { |
172 | chatter_add_tab(chatter, win_bounds, NULL, NULL); |
173 | } else { |
174 | /* TODO: move tabs around */ |
175 | } |
176 | } |
177 | |
178 | struct chatter_tab * |
179 | chatter_add_tab(struct chatter *chatter, Rect *win_bounds, |
180 | struct irc_connection *conn, struct irc_channel *channel) |
181 | { |
182 | struct chatter_tab *tab = NULL; |
183 | Rect bounds, inset_bounds; |
184 | Rect data_bounds = { 0, 0, 0, 1 }; /* tlbr */ |
185 | Point cell_size = { 0 }; |
186 | Cell cell = { 0, 0 }; |
187 | |
188 | if (win_bounds == NULL) |
189 | win_bounds = &chatter->win->portRect; |
190 | |
191 | tab = xmalloczero(sizeof(struct chatter_tab), "tab"); |
192 | SLIST_APPEND(&chatter->tabs_list, tab, chatter_tab, list); |
193 | tab->index = chatter->ntabs++; |
194 | tab->conn = conn; |
195 | tab->channel = channel; |
196 | |
197 | bounds.bottom = (*(chatter->input_te))->viewRect.top - 15; |
198 | |
199 | if (tab->channel) { |
200 | /* nick list */ |
201 | bounds.top = 0; |
202 | bounds.right = win_bounds->right - SCROLLBAR_WIDTH + 1; |
203 | bounds.left = bounds.right - NICK_LIST_WIDTH; |
204 | tab->nick_list = LNew(&bounds, &data_bounds, cell_size, 0, |
205 | chatter->win, true, true, false, true); |
206 | if (!tab->nick_list) |
207 | panic("Can't create nick list"); |
208 | LAddColumn(1, 0, tab->nick_list); |
209 | (*(tab->nick_list))->selFlags = lOnlyOne | lNoNilHilite; |
210 | } |
211 | |
212 | /* messages scrollbar */ |
213 | bounds.top = -1; |
214 | if (tab->channel) |
215 | bounds.right = (*(tab->nick_list))->rView.left; |
216 | else |
217 | bounds.right = win_bounds->right + 1; |
218 | bounds.left = bounds.right - SCROLLBAR_WIDTH; |
219 | bounds.bottom += 1; |
220 | tab->messages_scroller = NewControl(chatter->win, &bounds, "\p", true, |
221 | 1, 1, 1, scrollBarProc, 0L); |
222 | |
223 | /* messages */ |
224 | bounds.right = (*(tab->messages_scroller))->contrlRect.left; |
225 | bounds.left = 0; |
226 | bounds.top = 0; |
227 | bounds.bottom -= 1; |
228 | inset_bounds = bounds; |
229 | InsetRect(&inset_bounds, 4, 4); |
230 | tab->messages_te = TEStylNew(&inset_bounds, &bounds); |
231 | (*(tab->messages_te))->caretHook = NullCaretHook; |
232 | TEActivate(tab->messages_te); |
233 | chatter_autoscroll(chatter, tab->messages_te, tab->messages_scroller); |
234 | |
235 | chatter_focus_tab(chatter, tab); |
236 | |
237 | return tab; |
238 | } |
239 | |
240 | void |
241 | chatter_focus_tab(struct chatter *chatter, struct chatter_tab *tab) |
242 | { |
243 | RgnHandle clip; |
244 | Rect zerorect = { 0, 0, 0, 0 }, r; |
245 | |
246 | if (chatter->current_tab) { |
247 | if (chatter->current_tab == tab) |
248 | return; |
249 | |
250 | /* |
251 | * Doing the HideControl takes out the top line of our tab bar, |
252 | * so clip to just above it |
253 | */ |
254 | GetClip(clip = NewRgn()); |
255 | r.left = 0; |
256 | r.top = 0; |
257 | r.right = |
258 | (*(chatter->current_tab->messages_scroller))->contrlRect.right; |
259 | r.bottom = |
260 | (*(chatter->current_tab->messages_scroller))->contrlRect.bottom |
261 | - 1; |
262 | ClipRect(&r); |
263 | |
264 | TEDeactivate(chatter->current_tab->messages_te); |
265 | if (chatter->current_tab->nick_list) |
266 | LDoDraw(false, chatter->current_tab->nick_list); |
267 | HideControl(chatter->current_tab->messages_scroller); |
268 | |
269 | SetClip(clip); |
270 | } |
271 | |
272 | chatter->current_tab = tab; |
273 | |
274 | FillRect(&(*(tab->messages_te))->viewRect, white); |
275 | TEActivate(tab->messages_te); |
276 | TEUpdate(&(*(tab->messages_te))->viewRect, tab->messages_te); |
277 | ShowControl(tab->messages_scroller); |
278 | if (tab->nick_list) { |
279 | FillRect(&(*(tab->nick_list))->rView, white); |
280 | LDoDraw(true, tab->nick_list); |
281 | LUpdate(chatter->win->visRgn, tab->nick_list); |
282 | } |
283 | |
284 | DrawControls(chatter->win); |
285 | chatter_draw_tab_bar(chatter); |
286 | } |
287 | |
288 | void |
289 | chatter_resume(struct focusable *focusable, EventRecord *event) |
290 | { |
291 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
292 | |
293 | focusable_show(focusable); |
294 | InvalRect(chatter->win->visRgn); |
295 | } |
296 | |
297 | bool |
298 | chatter_close(struct focusable *focusable) |
299 | { |
300 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
301 | struct chatter_tab *tab; |
302 | struct irc_connection *conn, *tconn; |
303 | bool connected = false; |
304 | |
305 | SLIST_FOREACH(tab, &chatter->tabs_list, list) { |
306 | if (tab->conn->state == IRC_STATE_CONNECTED) { |
307 | connected = true; |
308 | break; |
309 | } |
310 | } |
311 | |
312 | if (connected && !chatter->quitting) { |
313 | focusable_hide(focusable); |
314 | return false; |
315 | } |
316 | |
317 | SLIST_FOREACH_SAFE(conn, &irc_connections_list, list, tconn) { |
318 | if (conn->chatter == chatter) { |
319 | irc_close_connection(conn); |
320 | /* this will kill any channels as well */ |
321 | irc_dealloc_connection(conn); |
322 | } |
323 | } |
324 | |
325 | if (chatter->tab_bar.baseAddr) |
326 | xfree(&chatter->tab_bar.baseAddr); |
327 | |
328 | DisposeWindow(focusable->win); |
329 | |
330 | return true; |
331 | } |
332 | |
333 | bool |
334 | chatter_quit(struct focusable *focusable) |
335 | { |
336 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
337 | |
338 | chatter->quitting = true; |
339 | focusable_close(focusable); |
340 | |
341 | return true; |
342 | } |
343 | |
344 | void |
345 | chatter_update_titlebar(struct chatter *chatter) |
346 | { |
347 | Str255 curtitle; |
348 | char title[64]; |
349 | struct chatter_tab *tab = chatter->current_tab; |
350 | |
351 | if (!tab->conn || tab->conn->state <= IRC_STATE_DISCONNECTED) |
352 | snprintf(title, sizeof(title), "%s: Disconnected", PROGRAM_NAME); |
353 | else if (tab->conn->state == IRC_STATE_CONNECTING) |
354 | snprintf(title, sizeof(title), "%s: Connecting to %s", |
355 | PROGRAM_NAME, tab->conn->hostname); |
356 | else if (tab->channel) |
357 | snprintf(title, sizeof(title), "%s: %s@%s: %s", PROGRAM_NAME, |
358 | tab->conn->nick, tab->conn->hostname, tab->channel->name); |
359 | else |
360 | snprintf(title, sizeof(title), "%s: %s@%s", PROGRAM_NAME, |
361 | tab->conn->nick, tab->conn->hostname); |
362 | |
363 | GetWTitle(chatter->win, &curtitle); |
364 | PtoCstr(curtitle); |
365 | |
366 | if (strcmp((char *)&curtitle, title) != 0) |
367 | SetWTitle(chatter->win, CtoPstr(title)); |
368 | } |
369 | |
370 | void |
371 | chatter_idle(struct focusable *focusable, EventRecord *event) |
372 | { |
373 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
374 | struct chatter_tab *tab; |
375 | short n; |
376 | |
377 | TEIdle(chatter->input_te); |
378 | |
379 | SLIST_FOREACH(tab, &chatter->tabs_list, list) { |
380 | irc_process(tab->conn); |
381 | } |
382 | } |
383 | |
384 | void |
385 | chatter_update(struct focusable *focusable, EventRecord *event) |
386 | { |
387 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
388 | struct chatter_tab *tab = chatter->current_tab; |
389 | GrafPtr old_port; |
390 | Rect r; |
391 | short what = -1; |
392 | |
393 | GetPort(&old_port); |
394 | |
395 | if (event != NULL) |
396 | what = event->what; |
397 | |
398 | switch (what) { |
399 | case -1: |
400 | case updateEvt: |
401 | TextFont(applFont); |
402 | TextSize(10); |
403 | |
404 | EraseRect(&chatter->win->portRect); |
405 | |
406 | if (tab->nick_list) { |
407 | r = (*(tab->nick_list))->rView; |
408 | LUpdate(chatter->win->visRgn, tab->nick_list); |
409 | InsetRect(&r, -1, -1); |
410 | FrameRect(&r); |
411 | } |
412 | |
413 | r = (*(tab->messages_te))->viewRect; |
414 | InsetRect(&r, -1, -1); |
415 | FrameRect(&r); |
416 | TEUpdate(&(*(tab->messages_te))->viewRect, tab->messages_te); |
417 | |
418 | r = (*(chatter->input_te))->viewRect; |
419 | InsetRect(&r, -1, -1); |
420 | FrameRect(&r); |
421 | TEUpdate(&(*(chatter->input_te))->viewRect, chatter->input_te); |
422 | |
423 | DrawControls(chatter->win); |
424 | chatter_draw_tab_bar(chatter); |
425 | break; |
426 | case activateEvt: |
427 | if (event->modifiers & activeFlag) { |
428 | if (tab->nick_list) |
429 | LActivate(true, tab->nick_list); |
430 | TEActivate(tab->messages_te); |
431 | TEActivate(chatter->input_te); |
432 | } else { |
433 | if (tab->nick_list) |
434 | LActivate(false, tab->nick_list); |
435 | TEDeactivate(tab->messages_te); |
436 | TEDeactivate(chatter->input_te); |
437 | } |
438 | break; |
439 | } |
440 | } |
441 | |
442 | void |
443 | chatter_draw_tab_bar(struct chatter *chatter) |
444 | { |
445 | Rect r, r2; |
446 | RgnHandle clip; |
447 | BitMap cur_bits; |
448 | short tab_width, tab_offset, n, width; |
449 | size_t len; |
450 | static const char no_connection[] = "Disconnected"; |
451 | struct chatter_tab *tab; |
452 | char *label; |
453 | |
454 | cur_bits = chatter->win->portBits; |
455 | |
456 | if (chatter->tab_bar.baseAddr == 0) { |
457 | width = chatter->win->portRect.right - chatter->win->portRect.left; |
458 | chatter->tab_bar.rowBytes = (((width - 1) / 16) + 1) * 2; |
459 | chatter->tab_bar.baseAddr = xmalloczero( |
460 | chatter->tab_bar.rowBytes * TAB_BAR_HEIGHT, "tab_bar"); |
461 | SetRect(&chatter->tab_bar.bounds, 0, 0, width, TAB_BAR_HEIGHT); |
462 | } |
463 | |
464 | SetPortBits(&chatter->tab_bar); |
465 | |
466 | TextFont(geneva); |
467 | TextSize(9); |
468 | |
469 | tab_offset = 5; |
470 | |
471 | tab_width = (chatter->win->portRect.right - |
472 | chatter->win->portRect.left - tab_offset - tab_offset) / |
473 | chatter->ntabs; |
474 | if (tab_width > MAX_TAB_WIDTH) |
475 | tab_width = MAX_TAB_WIDTH; |
476 | |
477 | r.top = 0; |
478 | r.bottom = TAB_BAR_HEIGHT; |
479 | r.left = 0; |
480 | r.right = chatter->win->portRect.right - chatter->win->portRect.left; |
481 | FillRect(&r, tab_bar_pattern); |
482 | |
483 | r.left--; |
484 | r.right++; |
485 | FrameRect(&r); |
486 | r.left++; |
487 | r.right--; |
488 | |
489 | r.left = tab_offset; |
490 | r.bottom -= 2; |
491 | r.right = r.left + tab_width; |
492 | |
493 | r2.left = r.left + 1; |
494 | r2.right = r.right - 1; |
495 | r2.top = r.top; |
496 | r2.bottom = r2.top + 1; |
497 | |
498 | GetClip(clip = NewRgn()); |
499 | |
500 | SLIST_FOREACH(tab, &chatter->tabs_list, list) { |
501 | tab->label_rect = r; |
502 | |
503 | FillRect(&r, white); |
504 | FrameRect(&r); |
505 | if (tab == chatter->current_tab) { |
506 | FrameRect(&r2); |
507 | FillRect(&r2, white); |
508 | tab->have_activity = false; |
509 | } |
510 | |
511 | ClipRect(&r); |
512 | |
513 | if (tab->conn) { |
514 | if (tab->channel) |
515 | label = tab->channel->name; |
516 | else |
517 | label = tab->conn->hostname; |
518 | } else |
519 | label = (char *)&no_connection; |
520 | |
521 | len = strlen(label); |
522 | if (tab->have_activity) |
523 | TextFace(bold | condense); |
524 | else |
525 | TextFace(0); |
526 | width = TextWidth(label, 0, len); |
527 | if (width > tab_width - 4) |
528 | width = tab_width - 4; |
529 | MoveTo(r.left + ((tab_width - width) / 2), r.bottom - 3); |
530 | DrawText(label, 0, len); |
531 | |
532 | SetClip(clip); |
533 | |
534 | r.left += tab_width + 2; |
535 | r.right += tab_width + 2; |
536 | r2.left += tab_width + 2; |
537 | r2.right += tab_width + 2; |
538 | } |
539 | |
540 | DisposeRgn(clip); |
541 | |
542 | TextFont(applFont); |
543 | TextSize(10); |
544 | TextFace(0); |
545 | |
546 | SetPortBits(&cur_bits); |
547 | |
548 | r = chatter->tab_bar.bounds; |
549 | r.bottom = (*(chatter->input_te))->viewRect.top; |
550 | r.top += r.bottom - TAB_BAR_HEIGHT; |
551 | |
552 | SLIST_FOREACH(tab, &chatter->tabs_list, list) { |
553 | tab->label_rect.top += r.top; |
554 | tab->label_rect.bottom += r.top; |
555 | } |
556 | |
557 | CopyBits(&chatter->tab_bar, &chatter->win->portBits, |
558 | &chatter->tab_bar.bounds, &r, srcCopy, nil); |
559 | |
560 | chatter_draw_grow_icon(chatter); |
561 | } |
562 | |
563 | void |
564 | chatter_draw_grow_icon(struct chatter *chatter) |
565 | { |
566 | Rect r, *te; |
567 | RgnHandle tmp; |
568 | WindowPtr win = chatter->win; |
569 | |
570 | /* |
571 | * Our input bar is taller than a scrollbar, so we can't use the |
572 | * normal DrawGrowIcon or our DrawGrowIconOnly |
573 | */ |
574 | HLock(*(chatter->input_te)); |
575 | te = &(*(chatter->input_te))->viewRect; |
576 | |
577 | r = win->portRect; |
578 | r.top = r.bottom - (te->bottom - te->top + 1); |
579 | r.left = r.right - SCROLLBAR_WIDTH + 1; |
580 | r.right += 1; |
581 | r.bottom += 1; |
582 | FrameRect(&r); |
583 | |
584 | r.bottom -= 2; |
585 | r.right -= 2; |
586 | r.top = r.bottom - 9; |
587 | r.left = r.right - 9; |
588 | FrameRect(&r); |
589 | |
590 | r.top -= 3; |
591 | r.left -= 2; |
592 | r.bottom -= 4; |
593 | r.right -= 4; |
594 | FillRect(&r, white); |
595 | FrameRect(&r); |
596 | |
597 | HUnlock(*(chatter->input_te)); |
598 | } |
599 | |
600 | void |
601 | chatter_mouse_down(struct focusable *focusable, EventRecord *event) |
602 | { |
603 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
604 | struct chatter_tab *tab = chatter->current_tab, *ttab; |
605 | Point p; |
606 | Cell selected = { 0 }, now = { 0 }, t = { 0 }; |
607 | ControlHandle control; |
608 | Rect r; |
609 | short val, adj, ret, part, n; |
610 | |
611 | p = event->where; |
612 | GlobalToLocal(&p); |
613 | |
614 | if (tab->nick_list) { |
615 | r = (*(tab->nick_list))->rView; |
616 | r.right += SCROLLBAR_WIDTH; |
617 | if (PtInRect(p, &r)) { |
618 | /* store what is selected now */ |
619 | ret = LGetSelect(true, &selected, tab->nick_list); |
620 | |
621 | /* possibly highlight a new cell */ |
622 | LClick(p, event->modifiers, tab->nick_list); |
623 | |
624 | if (selected.v != now.v) { |
625 | LSetSelect(false, selected, tab->nick_list); |
626 | /* TODO: detect double-click, open query window? */ |
627 | } |
628 | |
629 | return; |
630 | } |
631 | } |
632 | |
633 | r = (*(tab->messages_te))->viewRect; |
634 | if (PtInRect(p, &r)) { |
635 | TEClick(p, ((event->modifiers & shiftKey) != 0), tab->messages_te); |
636 | HLock(tab->messages_te); |
637 | if ((*(tab->messages_te))->selStart != |
638 | (*(tab->messages_te))->selEnd) |
639 | TESetSelect(0, 0, chatter->input_te); |
640 | HUnlock(tab->messages_te); |
641 | return; |
642 | } |
643 | |
644 | r = (*(chatter->input_te))->viewRect; |
645 | if (PtInRect(p, &r)) { |
646 | TEClick(p, ((event->modifiers & shiftKey) != 0), |
647 | chatter->input_te); |
648 | HLock(chatter->input_te); |
649 | if ((*(chatter->input_te))->selStart != |
650 | (*(chatter->input_te))->selEnd) |
651 | TESetSelect(0, 0, tab->messages_te); |
652 | HUnlock(chatter->input_te); |
653 | return; |
654 | } |
655 | |
656 | SLIST_FOREACH(ttab, &chatter->tabs_list, list) { |
657 | if (PtInRect(p, &ttab->label_rect)) { |
658 | chatter_focus_tab(chatter, ttab); |
659 | return; |
660 | } |
661 | } |
662 | |
663 | switch (part = FindControl(p, chatter->win, &control)) { |
664 | case inUpButton: |
665 | case inDownButton: |
666 | case inPageUp: |
667 | case inPageDown: |
668 | if (control != tab->messages_scroller) |
669 | break; |
670 | SetTrackControlTE(tab->messages_te); |
671 | TrackControl(control, p, TrackMouseDownInControl); |
672 | break; |
673 | case inThumb: |
674 | val = GetCtlValue(control); |
675 | if (TrackControl(control, p, 0L) == 0) |
676 | break; |
677 | adj = val - GetCtlValue(control); |
678 | if (adj != 0) { |
679 | val -= adj; |
680 | if (control == tab->messages_scroller) |
681 | TEScroll(0, adj * TEGetHeight(0, 0, tab->messages_te), |
682 | tab->messages_te); |
683 | SetCtlValue(control, val); |
684 | } |
685 | break; |
686 | } |
687 | } |
688 | |
689 | void |
690 | chatter_resize(struct focusable *focusable, EventRecord *event) |
691 | { |
692 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
693 | Rect bounds; |
694 | long newsize, width, height; |
695 | |
696 | bounds.left = 100; |
697 | bounds.top = 100; |
698 | bounds.right = screenBits.bounds.right; |
699 | bounds.bottom = screenBits.bounds.bottom; |
700 | |
701 | newsize = GrowWindow(focusable->win, event->where, &bounds); |
702 | |
703 | height = HiWord(newsize); |
704 | width = LoWord(newsize); |
705 | SizeWindow(focusable->win, width, height, true); |
706 | EraseRect(&chatter->win->portRect); |
707 | |
708 | /* chatter_draw_tab_bar will recreate this to the new size */ |
709 | if (chatter->tab_bar.baseAddr) |
710 | xfree(&chatter->tab_bar.baseAddr); |
711 | |
712 | chatter_layout(chatter, false, NULL); |
713 | } |
714 | |
715 | bool |
716 | chatter_menu(struct focusable *focusable, short menu, short item) |
717 | { |
718 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
719 | struct chatter_tab *tab = chatter->current_tab; |
720 | |
721 | switch (menu) { |
722 | case EDIT_MENU_ID: |
723 | switch (item) { |
724 | case EDIT_MENU_CUT_ID: |
725 | TECut(chatter->input_te); |
726 | return true; |
727 | case EDIT_MENU_COPY_ID: |
728 | HLock(chatter->input_te); |
729 | HLock(tab->messages_te); |
730 | if ((*(chatter->input_te))->selStart != |
731 | (*(chatter->input_te))->selEnd) |
732 | TECopy(chatter->input_te); |
733 | else if ((*(tab->messages_te))->selStart != |
734 | (*(tab->messages_te))->selEnd) |
735 | TECopy(tab->messages_te); |
736 | HUnlock(tab->messages_te); |
737 | HUnlock(chatter->input_te); |
738 | return true; |
739 | case EDIT_MENU_PASTE_ID: |
740 | TEPaste(chatter->input_te); |
741 | return true; |
742 | } |
743 | } |
744 | |
745 | return false; |
746 | } |
747 | |
748 | void |
749 | chatter_key_down(struct focusable *focusable, EventRecord *event) |
750 | { |
751 | struct chatter *chatter = (struct chatter *)(focusable->cookie); |
752 | struct chatter_tab *tab = chatter->current_tab; |
753 | TERec *te; |
754 | char *input, k; |
755 | |
756 | k = (event->message & charCodeMask); |
757 | if (k == '\r') { |
758 | HLock(chatter->input_te); |
759 | te = *(chatter->input_te); |
760 | HLock(te->hText); |
761 | (*(te->hText))[te->teLength] = '\0'; |
762 | input = xstrdup(*(te->hText), "input"); |
763 | TESetText(&k, 0, chatter->input_te); |
764 | EraseRect(&te->viewRect); |
765 | ValidRect(&te->viewRect); |
766 | TEIdle(chatter->input_te); |
767 | HUnlock(te->hText); |
768 | HUnlock(chatter->input_te); |
769 | irc_process_input(tab->conn, tab->channel, input); |
770 | xfree(&input); |
771 | } else { |
772 | TEKey(k, chatter->input_te); |
773 | TESelView(chatter->input_te); |
774 | } |
775 | } |
776 | |
777 | struct chatter_tab * |
778 | chatter_find_tab_for_conn_and_channel(struct chatter *chatter, |
779 | struct irc_connection *conn, struct irc_channel *channel) |
780 | { |
781 | short n; |
782 | struct chatter_tab *tab; |
783 | |
784 | if (conn == NULL) |
785 | return NULL; |
786 | |
787 | SLIST_FOREACH(tab, &chatter->tabs_list, list) { |
788 | if (tab->conn == conn && channel == tab->channel) |
789 | return tab; |
790 | } |
791 | |
792 | return NULL; |
793 | } |
794 | |
795 | size_t |
796 | chatter_printf(struct chatter *chatter, struct irc_connection *conn, |
797 | struct irc_channel *channel, const char *format, ...) |
798 | { |
799 | static char buf[600], buf_out[600]; |
800 | struct chatter_tab *tab = chatter->current_tab, *ttab; |
801 | StScrpRec *scrp_rec; |
802 | ScrpSTElement *scrp_ele, *prev_scrp_ele; |
803 | RgnHandle savergn; |
804 | Rect zerorect = { 0, 0, 0, 0 }; |
805 | va_list argptr; |
806 | size_t len, n, buf_out_len, in_this_style; |
807 | time_t now = Time; |
808 | short line_height = 0; |
809 | bool stop_formatting = false, had_activity; |
810 | |
811 | len = 0; |
812 | |
813 | if (conn != NULL) { |
814 | SLIST_FOREACH(ttab, &chatter->tabs_list, list) { |
815 | if (ttab->conn == conn && channel == ttab->channel) { |
816 | tab = ttab; |
817 | break; |
818 | } |
819 | } |
820 | } |
821 | |
822 | if (tab == NULL) |
823 | panic("no tab"); |
824 | |
825 | had_activity = tab->have_activity; |
826 | |
827 | if ((*(tab->messages_te))->teLength > 0) { |
828 | buf[0] = '\r'; |
829 | len++; |
830 | } |
831 | |
832 | len += strftime(buf + len, sizeof(buf) - len, "$B[%H:%M]$0 ", |
833 | localtime(&now)); |
834 | |
835 | va_start(argptr, format); |
836 | len += vsnprintf(buf + len, sizeof(buf) - len, format, argptr); |
837 | va_end(argptr); |
838 | |
839 | if (scrp_rec_h == NULL) { |
840 | scrp_rec_h = xNewHandle(4 + (20 * CHATTER_SCRAP_ELEMENTS)); |
841 | HLock(scrp_rec_h); |
842 | memset(*scrp_rec_h, 0, (4 + (20 * CHATTER_SCRAP_ELEMENTS))); |
843 | } else |
844 | HLock(scrp_rec_h); |
845 | |
846 | line_height = CHATTER_FONT_SIZE + 3; |
847 | |
848 | scrp_rec = (StScrpRec *)(*scrp_rec_h); |
849 | scrp_rec->scrpNStyles = 1; |
850 | scrp_ele = &scrp_rec->scrpStyleTab[scrp_rec->scrpNStyles - 1]; |
851 | scrp_ele->scrpStartChar = 0; |
852 | scrp_ele->scrpHeight = line_height; |
853 | scrp_ele->scrpAscent = CHATTER_FONT_SIZE; |
854 | scrp_ele->scrpFont = CHATTER_FONT; |
855 | scrp_ele->scrpSize = CHATTER_FONT_SIZE; |
856 | scrp_ele->scrpFace = 0; |
857 | |
858 | for (n = 0, buf_out_len = 0, in_this_style = 0; n < len; n++) { |
859 | if (!stop_formatting && buf[n] == '$') { |
860 | if (in_this_style > 0) { |
861 | scrp_rec->scrpNStyles++; |
862 | if (scrp_rec->scrpNStyles >= CHATTER_SCRAP_ELEMENTS) |
863 | panic("chatter_printf: too many elements"); |
864 | prev_scrp_ele = scrp_ele; |
865 | scrp_ele = &scrp_rec->scrpStyleTab[ |
866 | scrp_rec->scrpNStyles - 1]; |
867 | /* carry style forward */ |
868 | memcpy(scrp_ele, prev_scrp_ele, sizeof(ScrpSTElement)); |
869 | } |
870 | scrp_ele->scrpStartChar = buf_out_len; |
871 | |
872 | switch (buf[n + 1]) { |
873 | case 'B': |
874 | scrp_ele->scrpFace |= bold | condense; |
875 | break; |
876 | case 'U': |
877 | scrp_ele->scrpFace |= underline; |
878 | break; |
879 | case 's': |
880 | scrp_ele->scrpSize--; |
881 | break; |
882 | case 'S': |
883 | scrp_ele->scrpSize++; |
884 | break; |
885 | case '/': |
886 | stop_formatting = true; |
887 | /* FALLTHROUGH */ |
888 | case '0': |
889 | scrp_ele->scrpHeight = line_height; |
890 | scrp_ele->scrpAscent = CHATTER_FONT_SIZE; |
891 | scrp_ele->scrpFont = CHATTER_FONT; |
892 | scrp_ele->scrpSize = CHATTER_FONT_SIZE; |
893 | scrp_ele->scrpFace = 0; |
894 | break; |
895 | } |
896 | n++; |
897 | continue; |
898 | } |
899 | buf_out[buf_out_len++] = buf[n]; |
900 | in_this_style++; |
901 | } |
902 | |
903 | if (!buf_out_len) { |
904 | HUnlock(scrp_rec_h); |
905 | return 0; |
906 | } |
907 | |
908 | HLock(tab->messages_te); |
909 | |
910 | /* check for TE overflow */ |
911 | |
912 | /* too many lines */ |
913 | if ((*(tab->messages_te))->nLines >= |
914 | (nitems((*(tab->messages_te))->lineStarts) - 10)) |
915 | goto te_overflow; |
916 | |
917 | /* too many characters */ |
918 | if ((*(tab->messages_te))->teLength >= (SHRT_MAX - 500)) |
919 | goto te_overflow; |
920 | |
921 | /* rect of all lines is too tall */ |
922 | if ((*(tab->messages_te))->nLines * line_height >= (SHRT_MAX - 100)) |
923 | goto te_overflow; |
924 | |
925 | goto no_overflow; |
926 | |
927 | te_overflow: |
928 | savergn = NewRgn(); |
929 | GetClip(savergn); |
930 | /* create an empty clip region so all TE updates are hidden */ |
931 | ClipRect(&zerorect); |
932 | |
933 | /* select some lines at the start, delete them */ |
934 | TESetSelect(0, (*(tab->messages_te))->lineStarts[5], tab->messages_te); |
935 | TEDelete(tab->messages_te); |
936 | |
937 | /* scroll up, causing a repaint */ |
938 | TEPinScroll(0, INT_MAX, tab->messages_te); |
939 | |
940 | /* then scroll back down to what it looked like before we did anything */ |
941 | TEPinScroll(0, -INT_MAX, tab->messages_te); |
942 | |
943 | /* resume normal drawing */ |
944 | SetClip(savergn); |
945 | DisposeRgn(savergn); |
946 | |
947 | no_overflow: |
948 | if (chatter->current_tab != tab) { |
949 | savergn = NewRgn(); |
950 | GetClip(savergn); |
951 | /* create an empty clip region so all TE updates are hidden */ |
952 | ClipRect(&zerorect); |
953 | } |
954 | |
955 | TESetSelect(SHRT_MAX, SHRT_MAX, tab->messages_te); |
956 | TEStylInsert(buf_out, buf_out_len, scrp_rec_h, tab->messages_te); |
957 | HUnlock(scrp_rec_h); |
958 | HUnlock(tab->messages_te); |
959 | |
960 | chatter_autoscroll(chatter, tab->messages_te, tab->messages_scroller); |
961 | |
962 | if (chatter->current_tab == tab) |
963 | tab->have_activity = false; |
964 | else { |
965 | /* resume normal drawing */ |
966 | SetClip(savergn); |
967 | DisposeRgn(savergn); |
968 | |
969 | if (!had_activity) { |
970 | tab->have_activity = true; |
971 | chatter_draw_tab_bar(chatter); |
972 | } |
973 | } |
974 | |
975 | return buf_out_len; |
976 | } |
977 | |
978 | void |
979 | chatter_autoscroll(struct chatter *chatter, TEHandle te, |
980 | ControlHandle scroller) |
981 | { |
982 | TEPinScroll(0, -INT_MAX, te); |
983 | SetCtlValue(scroller, GetCtlMax(scroller)); |
984 | UpdateScrollbarForTE(chatter->win, scroller, te, false); |
985 | } |
986 | |
987 | void |
988 | chatter_remove_channel(struct chatter *chatter, |
989 | struct irc_channel *channel) |
990 | { |
991 | struct chatter_tab *tab = NULL, *ttab = NULL, *next_tab = NULL; |
992 | short n; |
993 | |
994 | SLIST_FOREACH(ttab, &chatter->tabs_list, list) { |
995 | if (ttab->conn == channel->connection) { |
996 | if (ttab->channel == channel) |
997 | tab = ttab; |
998 | else |
999 | next_tab = ttab; |
1000 | } |
1001 | } |
1002 | |
1003 | if (tab != NULL) { |
1004 | LDispose(tab->nick_list); |
1005 | DisposeControl(tab->messages_scroller); |
1006 | TEDispose(tab->messages_te); |
1007 | SLIST_REMOVE(&chatter->tabs_list, tab, chatter_tab, list); |
1008 | |
1009 | if (chatter->current_tab == tab) |
1010 | chatter->current_tab = NULL; |
1011 | } |
1012 | |
1013 | if (next_tab == NULL) |
1014 | chatter_draw_tab_bar(chatter); |
1015 | else |
1016 | chatter_focus_tab(chatter, next_tab); |
1017 | |
1018 | /* |
1019 | * Don't bother updating titlebar because we should be inside |
1020 | * irc_dealloc_channel which will then call chatter_update_titlebar |
1021 | */ |
1022 | } |
1023 | |
1024 | void |
1025 | chatter_sync_nick_list(struct chatter *chatter, struct irc_channel *channel, |
1026 | bool just_summary) |
1027 | { |
1028 | size_t n, i, j, tj, ops, voices; |
1029 | short ret, cellv; |
1030 | struct irc_channel_nick *nick = NULL; |
1031 | struct chatter_tab *tab; |
1032 | |
1033 | tab = chatter_find_tab_for_conn_and_channel(chatter, |
1034 | channel->connection, channel); |
1035 | if (!tab) |
1036 | return; |
1037 | |
1038 | if (!just_summary && tab == chatter->current_tab) { |
1039 | LDoDraw(false, tab->nick_list); |
1040 | LDelRow(0, 0, tab->nick_list); |
1041 | } |
1042 | |
1043 | if (channel) { |
1044 | cellv = 0; |
1045 | ops = voices = 0; |
1046 | nick = &channel->nicks[channel->first_nick]; |
1047 | while (nick) { |
1048 | if (nick->flags & IRC_NICK_FLAG_OP) |
1049 | ops++; |
1050 | else if (nick->flags & IRC_NICK_FLAG_VOICE) |
1051 | voices++; |
1052 | |
1053 | if (!just_summary) |
1054 | chatter_insert_to_nick_list(chatter, channel, nick, cellv); |
1055 | |
1056 | cellv++; |
1057 | |
1058 | if (nick->next_nick == -1) |
1059 | break; |
1060 | nick = &channel->nicks[nick->next_nick]; |
1061 | } |
1062 | |
1063 | if (just_summary) |
1064 | chatter_printf(chatter, channel->connection, channel, |
1065 | "$B%s$0: Total of $B%ld$0 nick%s $B(%ld$0 op%s, $B%ld$0 " |
1066 | "voice%s$B)$0", |
1067 | channel->name, channel->nnicks, |
1068 | channel->nnicks == 1 ? "" : "s", |
1069 | ops, ops == 1 ? "" : "s", |
1070 | voices, voices == 1 ? "" : "s"); |
1071 | } |
1072 | |
1073 | if (!just_summary) { |
1074 | LDoDraw(true, tab->nick_list); |
1075 | InvalRect(&(*(tab->nick_list))->rView); |
1076 | } |
1077 | } |
1078 | |
1079 | void |
1080 | chatter_insert_to_nick_list(struct chatter *chatter, |
1081 | struct irc_channel *channel, struct irc_channel_nick *nick, short pos) |
1082 | { |
1083 | Cell cell = { 0, 0 }; |
1084 | struct irc_channel_nick tnick; |
1085 | struct chatter_tab *tab; |
1086 | short j = 0; |
1087 | |
1088 | tab = chatter_find_tab_for_conn_and_channel(chatter, |
1089 | channel->connection, channel); |
1090 | if (!tab) |
1091 | return; |
1092 | |
1093 | if (nick->flags & IRC_NICK_FLAG_OP) { |
1094 | tnick.nick[0] = '@'; |
1095 | j++; |
1096 | } else if (nick->flags & IRC_NICK_FLAG_VOICE) { |
1097 | tnick.nick[0] = '+'; |
1098 | j++; |
1099 | } |
1100 | |
1101 | cell.v = pos; |
1102 | j += strlcpy(tnick.nick + j, nick->nick, sizeof(tnick.nick) - j); |
1103 | LAddRow(1, cell.v, tab->nick_list); |
1104 | LSetCell(&tnick.nick, j, cell, tab->nick_list); |
1105 | } |