Download
jcs
/subtext
/logger.c
(View History)
jcs logger: Fix text selection and copying | Latest amendment: 529 on 2023-11-08 |
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 "focusable.h" |
21 | #include "logger.h" |
22 | #include "subtext.h" |
23 | #include "user.h" |
24 | #include "util.h" |
25 | |
26 | struct logger *logger = NULL; |
27 | |
28 | void logger_layout(Rect *init_bounds); |
29 | void logger_key_down(struct focusable *focusable, EventRecord *event); |
30 | void logger_mouse_down(struct focusable *focusable, EventRecord *event); |
31 | bool logger_menu(struct focusable *focusable, short menu, short item); |
32 | void logger_resize(struct focusable *focusable, EventRecord *event); |
33 | void logger_update(struct focusable *focusable, EventRecord *event); |
34 | void logger_resume(struct focusable *focusable, EventRecord *event); |
35 | bool logger_quit(struct focusable *focusable); |
36 | void logger_flush_buffer(bool force); |
37 | |
38 | void |
39 | logger_init(void) |
40 | { |
41 | struct focusable *focusable; |
42 | Rect bounds = { 0 }; |
43 | short padding = 5; |
44 | |
45 | logger = xmalloczero(sizeof(struct logger)); |
46 | if (!logger) |
47 | panic("Can't allocate logger"); |
48 | logger->buffered_logs = xmalloc(LOGGER_BUFFERED_LOG_SIZE); |
49 | if (!logger->buffered_logs) |
50 | panic("Can't allocate logger buffer"); |
51 | logger->buffered_logs[0] = '\0'; |
52 | logger->autoflush = true; |
53 | |
54 | bounds.left = padding; |
55 | bounds.top = ((screenBits.bounds.bottom - |
56 | screenBits.bounds.top) / 3); |
57 | bounds.right = screenBits.bounds.right - padding - 1; |
58 | bounds.bottom = screenBits.bounds.bottom - padding - 1; |
59 | |
60 | logger->win = NewWindow(0L, &bounds, "\p", false, |
61 | documentProc, (WindowPtr)-1L, false, 0); |
62 | if (!logger->win) |
63 | panic("Can't create logger window"); |
64 | logger_update_title(); |
65 | |
66 | SetPort(logger->win); |
67 | |
68 | TextFont(LOGGER_FONT); |
69 | TextSize(LOGGER_FONT_SIZE); |
70 | |
71 | bounds.right -= bounds.left; |
72 | bounds.bottom -= bounds.top; |
73 | bounds.top = bounds.left = 0; |
74 | logger_layout(&bounds); |
75 | |
76 | focusable = xmalloczero(sizeof(struct focusable)); |
77 | if (!focusable) |
78 | panic("Can't create focusable"); |
79 | focusable->win = logger->win; |
80 | focusable->cookie = logger; |
81 | focusable->mouse_down = logger_mouse_down; |
82 | focusable->menu = logger_menu; |
83 | focusable->resize = logger_resize; |
84 | focusable->update = logger_update; |
85 | focusable->quit = logger_quit; |
86 | focusable->resume = logger_resume; |
87 | if (!add_focusable(focusable)) |
88 | panic("Can't add focusable"); |
89 | logger->focusable = focusable; |
90 | |
91 | DrawControls(logger->win); |
92 | DrawGrowIconOnly(logger->win); |
93 | } |
94 | |
95 | void |
96 | logger_layout(Rect *init_bounds) |
97 | { |
98 | Rect bounds, inset_bounds, win_bounds; |
99 | bool init = (init_bounds != NULL); |
100 | |
101 | if (init) |
102 | win_bounds = *init_bounds; |
103 | else |
104 | win_bounds = logger->win->portRect; |
105 | |
106 | /* messages scrollbar */ |
107 | bounds.top = -1; |
108 | bounds.right = win_bounds.right - win_bounds.left + 1; |
109 | bounds.bottom = win_bounds.bottom - win_bounds.top - 14; |
110 | bounds.left = bounds.right - SCROLLBAR_WIDTH; |
111 | if (init) { |
112 | logger->messages_scroller = NewControl(logger->win, &bounds, |
113 | "\p", true, 1, 1, 1, scrollBarProc, 0L); |
114 | if (!logger->messages_scroller) |
115 | panic("Can't create messages scroller"); |
116 | } else |
117 | (*(logger->messages_scroller))->contrlRect = bounds; |
118 | |
119 | /* messages */ |
120 | bounds.right = (*(logger->messages_scroller))->contrlRect.left; |
121 | bounds.left = 0; |
122 | bounds.top = 0; |
123 | bounds.bottom = win_bounds.bottom - win_bounds.top; |
124 | if (init) { |
125 | inset_bounds = bounds; |
126 | InsetRect(&inset_bounds, 4, 4); |
127 | logger->messages_te = TEStylNew(&inset_bounds, &bounds); |
128 | if (logger->messages_te == NULL) |
129 | panic("Can't create logger TE"); |
130 | (*(logger->messages_te))->caretHook = NullCaretHook; |
131 | TEActivate(logger->messages_te); |
132 | } else { |
133 | (*(logger->messages_te))->viewRect = bounds; |
134 | InsetRect(&bounds, 4, 4); |
135 | (*(logger->messages_te))->destRect = bounds; |
136 | TECalText(logger->messages_te); |
137 | TEUpdate(&(*(logger->messages_te))->viewRect, logger->messages_te); |
138 | } |
139 | |
140 | DrawControls(logger->win); |
141 | DrawGrowIconOnly(logger->win); |
142 | |
143 | if (!init) |
144 | logger_flush_buffer(true); |
145 | } |
146 | |
147 | void |
148 | logger_resume(struct focusable *focusable, EventRecord *event) |
149 | { |
150 | show_focusable(focusable); |
151 | InvalRect(logger->win->visRgn); |
152 | } |
153 | |
154 | bool |
155 | logger_quit(struct focusable *focusable) |
156 | { |
157 | destroy_focusable(focusable); |
158 | xfree(&logger->buffered_logs); |
159 | xfree(&logger); |
160 | |
161 | return true; |
162 | } |
163 | |
164 | void |
165 | logger_update(struct focusable *focusable, EventRecord *event) |
166 | { |
167 | GrafPtr old_port; |
168 | Rect r; |
169 | short what = -1; |
170 | |
171 | GetPort(&old_port); |
172 | SetPort(logger->win); |
173 | |
174 | if (event != NULL) |
175 | what = event->what; |
176 | |
177 | switch (what) { |
178 | case -1: |
179 | case updateEvt: |
180 | TextFont(applFont); |
181 | TextSize(10); |
182 | |
183 | EraseRect(&logger->win->portRect); |
184 | |
185 | r = (*(logger->messages_te))->viewRect; |
186 | InsetRect(&r, -1, -1); |
187 | FrameRect(&r); |
188 | |
189 | TEUpdate(&(*(logger->messages_te))->viewRect, logger->messages_te); |
190 | DrawControls(logger->win); |
191 | DrawGrowIconOnly(logger->win); |
192 | break; |
193 | case activateEvt: |
194 | if (event->modifiers & activeFlag) { |
195 | TEActivate(logger->messages_te); |
196 | } else { |
197 | TEDeactivate(logger->messages_te); |
198 | } |
199 | break; |
200 | } |
201 | |
202 | SetPort(old_port); |
203 | } |
204 | |
205 | void |
206 | logger_update_title(void) |
207 | { |
208 | static char logger_title[64]; |
209 | short n, uprinted = 0; |
210 | |
211 | snprintf(logger_title, sizeof(logger_title), |
212 | "%s | %luKB Free | %lu Call%s | ", db->config.name, |
213 | (FreeMem() / 1024), |
214 | session_today_tally.calls, session_today_tally.calls == 1 ? "" : "s"); |
215 | |
216 | for (n = 0; n < MAX_SESSIONS; n++) { |
217 | if (sessions[n] == NULL || !sessions[n]->logged_in) |
218 | continue; |
219 | |
220 | if (uprinted++) |
221 | strlcat(logger_title, ", ", sizeof(logger_title)); |
222 | else |
223 | strlcat(logger_title, "Logged in: ", sizeof(logger_title)); |
224 | |
225 | strlcat(logger_title, |
226 | sessions[n]->user ? sessions[n]->user->username : GUEST_USERNAME, |
227 | sizeof(logger_title)); |
228 | } |
229 | |
230 | if (uprinted == 0) |
231 | strlcat(logger_title, "0 connected", sizeof(logger_title)); |
232 | |
233 | SetWTitle(logger->win, CtoPstr(logger_title)); |
234 | } |
235 | |
236 | void |
237 | logger_mouse_down(struct focusable *focusable, EventRecord *event) |
238 | { |
239 | Point p; |
240 | ControlHandle control; |
241 | Rect r; |
242 | int val, adj; |
243 | |
244 | p = event->where; |
245 | GlobalToLocal(&p); |
246 | |
247 | r = (*(logger->messages_te))->viewRect; |
248 | if (PtInRect(p, &r)) { |
249 | TEClick(p, ((event->modifiers & shiftKey) != 0), |
250 | logger->messages_te); |
251 | return; |
252 | } |
253 | |
254 | switch (FindControl(p, logger->win, &control)) { |
255 | case inUpButton: |
256 | case inDownButton: |
257 | case inPageUp: |
258 | case inPageDown: |
259 | if (control != logger->messages_scroller) |
260 | break; |
261 | SetTrackControlTE(logger->messages_te); |
262 | TrackControl(control, p, TrackMouseDownInControl); |
263 | break; |
264 | case inThumb: |
265 | val = GetCtlValue(control); |
266 | if (TrackControl(control, p, 0L) == 0) |
267 | break; |
268 | adj = val - GetCtlValue(control); |
269 | if (adj != 0) { |
270 | val -= adj; |
271 | if (control == logger->messages_scroller) |
272 | TEScroll(0, adj * TEGetHeight(0, 0, logger->messages_te), |
273 | logger->messages_te); |
274 | SetCtlValue(control, val); |
275 | } |
276 | break; |
277 | } |
278 | } |
279 | |
280 | bool |
281 | logger_menu(struct focusable *focusable, short menu, short item) |
282 | { |
283 | switch (menu) { |
284 | case EDIT_MENU_ID: |
285 | switch (item) { |
286 | case EDIT_MENU_COPY_ID: |
287 | HLock(logger->messages_te); |
288 | if ((*(logger->messages_te))->selStart == |
289 | (*(logger->messages_te))->selEnd) |
290 | SysBeep(10); |
291 | else |
292 | TECopy(logger->messages_te); |
293 | HUnlock(logger->messages_te); |
294 | break; |
295 | default: |
296 | SysBeep(10); |
297 | break; |
298 | } |
299 | return true; |
300 | } |
301 | |
302 | return false; |
303 | } |
304 | |
305 | void |
306 | logger_resize(struct focusable *focusable, EventRecord *event) |
307 | { |
308 | Rect bounds; |
309 | long newsize, width, height; |
310 | |
311 | bounds.left = 100; |
312 | bounds.top = 100; |
313 | bounds.right = screenBits.bounds.right; |
314 | bounds.bottom = screenBits.bounds.bottom; |
315 | |
316 | newsize = GrowWindow(focusable->win, event->where, &bounds); |
317 | |
318 | height = HiWord(newsize); |
319 | width = LoWord(newsize); |
320 | SizeWindow(focusable->win, width, height, true); |
321 | logger_layout(NULL); |
322 | } |
323 | |
324 | size_t |
325 | logger_printf(const char *format, ...) |
326 | { |
327 | va_list va; |
328 | size_t len; |
329 | |
330 | if (!logger) |
331 | return 0; |
332 | |
333 | va_start(va, format); |
334 | len = logger_vprintf(format, va); |
335 | va_end(va); |
336 | |
337 | return len; |
338 | } |
339 | |
340 | size_t |
341 | logger_vprintf(const char *format, va_list ap) |
342 | { |
343 | static char buf[600]; |
344 | ssize_t len; |
345 | time_t now = Time; |
346 | |
347 | if (!logger) |
348 | return 0; |
349 | |
350 | blanker_unblank(); |
351 | |
352 | len = strftime(buf, sizeof(buf), "\r[%H:%M:%S] ", localtime(&now)); |
353 | len += vsnprintf(buf + len, sizeof(buf) - len, format, ap); |
354 | if (len > sizeof(buf)) { |
355 | buf[sizeof(buf) - 1] = ']'; |
356 | buf[sizeof(buf) - 2] = '…'; |
357 | buf[sizeof(buf) - 3] = '['; |
358 | len = sizeof(buf); |
359 | } |
360 | |
361 | while (buf[len - 1] == '\r') { |
362 | buf[len - 1] = '\0'; |
363 | len--; |
364 | } |
365 | |
366 | if (logger->buffered_logs_len + len > LOGGER_BUFFERED_LOG_SIZE) |
367 | logger_flush_buffer(false); |
368 | |
369 | len = strlcat(logger->buffered_logs, buf, LOGGER_BUFFERED_LOG_SIZE); |
370 | logger->buffered_logs_len = MIN(len, LOGGER_BUFFERED_LOG_SIZE); |
371 | |
372 | if (logger->autoflush) |
373 | logger_flush_buffer(false); |
374 | |
375 | return len; |
376 | } |
377 | |
378 | void |
379 | logger_flush_buffer(bool force) |
380 | { |
381 | RgnHandle savergn; |
382 | Rect zerorect = { 0, 0, 0, 0 }; |
383 | GrafPtr old_port; |
384 | short line_height = 0, new_lines = 1, n; |
385 | char *buf; |
386 | |
387 | if (!logger || (logger->buffered_logs_len == 0 && !force)) |
388 | return; |
389 | |
390 | for (n = 0; n < logger->buffered_logs_len; n++) { |
391 | if (logger->buffered_logs[n] == '\r') |
392 | new_lines++; |
393 | } |
394 | |
395 | line_height = LOGGER_FONT_SIZE + 3; |
396 | |
397 | GetPort(&old_port); |
398 | SetPort(logger->win); |
399 | |
400 | /* check for TE overflow */ |
401 | |
402 | HLock(logger->messages_te); |
403 | |
404 | /* too many lines */ |
405 | if ((unsigned long)(*(logger->messages_te))->nLines >= |
406 | (nitems((*(logger->messages_te))->lineStarts) - new_lines)) |
407 | goto te_overflow; |
408 | |
409 | /* too many characters */ |
410 | if ((unsigned long)(*(logger->messages_te))->teLength >= |
411 | (SHRT_MAX - logger->buffered_logs_len)) |
412 | goto te_overflow; |
413 | |
414 | /* destRect of all lines is too tall */ |
415 | if ((unsigned long)(*(logger->messages_te))->nLines * line_height >= |
416 | (SHRT_MAX - new_lines)) |
417 | goto te_overflow; |
418 | |
419 | goto no_overflow; |
420 | |
421 | te_overflow: |
422 | savergn = NewRgn(); |
423 | GetClip(savergn); |
424 | /* create an empty clip region so all TE updates are hidden */ |
425 | ClipRect(&zerorect); |
426 | |
427 | /* select some lines at the start, delete them */ |
428 | TESetSelect(0, (*(logger->messages_te))->lineStarts[new_lines * 2], |
429 | logger->messages_te); |
430 | TEDelete(logger->messages_te); |
431 | |
432 | /* scroll up, causing a repaint */ |
433 | TEPinScroll(0, SHRT_MAX, logger->messages_te); |
434 | |
435 | /* then scroll back down to what it looked like before we did anything */ |
436 | TEPinScroll(0, -SHRT_MAX, logger->messages_te); |
437 | |
438 | /* resume normal drawing */ |
439 | SetClip(savergn); |
440 | DisposeRgn(savergn); |
441 | |
442 | no_overflow: |
443 | TESetSelect(SHRT_MAX, SHRT_MAX, logger->messages_te); |
444 | |
445 | buf = logger->buffered_logs; |
446 | if ((*(logger->messages_te))->teLength == 0) { |
447 | /* skip leading \r */ |
448 | logger->buffered_logs_len--; |
449 | buf++; |
450 | } |
451 | |
452 | TEInsert(buf, logger->buffered_logs_len, logger->messages_te); |
453 | |
454 | if ((unsigned long)(*(logger->messages_te))->nLines >= |
455 | nitems((*(logger->messages_te))->lineStarts)) |
456 | warn("te overflow!"); |
457 | |
458 | TEPinScroll(0, -SHRT_MAX, logger->messages_te); |
459 | SetCtlValue(logger->messages_scroller, |
460 | GetCtlMax(logger->messages_scroller)); |
461 | UpdateScrollbarForTE(logger->win, logger->messages_scroller, |
462 | logger->messages_te, false); |
463 | HUnlock(logger->messages_te); |
464 | |
465 | SetPort(old_port); |
466 | |
467 | logger->buffered_logs_len = 0; |
468 | logger->buffered_logs[0] = '\0'; |
469 | } |