1 /* ncdc - NCurses Direct Connect client
2
3 Copyright (c) 2011-2019 Yoran Heling
4
5 Permission is hereby granted, free of charge, to any person obtaining
6 a copy of this software and associated documentation files (the
7 "Software"), to deal in the Software without restriction, including
8 without limitation the rights to use, copy, modify, merge, publish,
9 distribute, sublicense, and/or sell copies of the Software, and to
10 permit persons to whom the Software is furnished to do so, subject to
11 the following conditions:
12
13 The above copyright notice and this permission notice shall be included
14 in all copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 */
25
26 #include "ncdc.h"
27 #include "ui_listing.h"
28
29 // Generic listing "widget".
30 // This widget allows easy listing, selecting and paging of (dynamic) GSequence
31 // lists. The list is managed by the user, but the widget does need to be
32 // notified of insertions and deletions.
33
34 #if INTERFACE
35
36 struct ui_listing_t {
37 GSequence *list;
38 GSequenceIter *sel;
39 GSequenceIter *top;
40 gboolean topisbegin;
41 gboolean selisbegin;
42 gboolean (*skip)(ui_listing_t *, GSequenceIter *, void *);
43 void *dat;
44
45 // fields needed for searching
46 ui_textinput_t *search_box;
47 gchar *query;
48 gint match_start;
49 gint match_end;
50 const char *(*to_string)(GSequenceIter *);
51 }
52
53 #endif
54
55 // error values for ui_listing_t.match_start
56 #define REGEX_NO_MATCH -1
57 #define REGEX_ERROR -2
58
59
60 // TODO: This can be relatively slow (linear search), is used often but rarely
61 // changes. Cache this in the struct if it becomes a problem.
ui_listing_getbegin(ui_listing_t * ul)62 static GSequenceIter *ui_listing_getbegin(ui_listing_t *ul) {
63 GSequenceIter *i = g_sequence_get_begin_iter(ul->list);
64 while(!g_sequence_iter_is_end(i) && ul->skip && ul->skip(ul, i, ul->dat))
65 i = g_sequence_iter_next(i);
66 return i;
67 }
68
69
ui_listing_next(ui_listing_t * ul,GSequenceIter * i)70 static GSequenceIter *ui_listing_next(ui_listing_t *ul, GSequenceIter *i) {
71 do
72 i = g_sequence_iter_next(i);
73 while(!g_sequence_iter_is_end(i) && ul->skip && ul->skip(ul, i, ul->dat));
74 return i;
75 }
76
77
ui_listing_prev(ui_listing_t * ul,GSequenceIter * i)78 static GSequenceIter *ui_listing_prev(ui_listing_t *ul, GSequenceIter *i) {
79 GSequenceIter *begin = ui_listing_getbegin(ul);
80 do
81 i = g_sequence_iter_prev(i);
82 while(!g_sequence_iter_is_begin(i) && i != begin && ul->skip && ul->skip(ul, i, ul->dat));
83 if(g_sequence_iter_is_begin(i))
84 i = begin;
85 return i;
86 }
87
88
89 // update top/sel in case they used to be the start of the list but aren't anymore
ui_listing_inserted(ui_listing_t * ul)90 void ui_listing_inserted(ui_listing_t *ul) {
91 GSequenceIter *begin = ui_listing_getbegin(ul);
92 if(!!ul->topisbegin != !!(ul->top == begin))
93 ul->top = ui_listing_getbegin(ul);
94 if(!!ul->selisbegin != !!(ul->sel == begin))
95 ul->sel = ui_listing_getbegin(ul);
96 }
97
98
99 // called after the order of the list has changed
100 // update sel in case it used to be the start of the list but isn't anymore
ui_listing_sorted(ui_listing_t * ul)101 void ui_listing_sorted(ui_listing_t *ul) {
102 if(!!ul->selisbegin != !!(ul->sel == ui_listing_getbegin(ul)))
103 ul->sel = ui_listing_getbegin(ul);
104 }
105
106
ui_listing_updateisbegin(ui_listing_t * ul)107 static void ui_listing_updateisbegin(ui_listing_t *ul) {
108 GSequenceIter *begin = ui_listing_getbegin(ul);
109 ul->topisbegin = ul->top == begin;
110 ul->selisbegin = ul->sel == begin;
111 }
112
113
114 // update top/sel in case one of them is removed.
115 // call this before using g_sequence_remove()
ui_listing_remove(ui_listing_t * ul,GSequenceIter * iter)116 void ui_listing_remove(ui_listing_t *ul, GSequenceIter *iter) {
117 if(ul->top == iter)
118 ul->top = ui_listing_prev(ul, iter);
119 if(ul->top == iter)
120 ul->top = ui_listing_next(ul, iter);
121 if(ul->sel == iter) {
122 ul->sel = ui_listing_next(ul, iter);
123 if(g_sequence_iter_is_end(ul->sel))
124 ul->sel = ui_listing_prev(ul, iter);
125 if(ul->sel == iter)
126 ul->sel = g_sequence_get_end_iter(ul->list);
127 }
128 ui_listing_updateisbegin(ul);
129 }
130
131
132 // called when the skip() function changes behaviour (i.e. some items that were
133 // skipped aren't now or the other way around).
ui_listing_skipchanged(ui_listing_t * ul)134 void ui_listing_skipchanged(ui_listing_t *ul) {
135 // sel got hidden? oops!
136 if(!g_sequence_iter_is_end(ul->sel) && ul->skip(ul, ul->sel, ul->dat)) {
137 ul->sel = ui_listing_next(ul, ul->sel);
138 if(g_sequence_iter_is_end(ul->sel))
139 ul->sel = ui_listing_prev(ul, ul->sel);
140 }
141 // top got hidden? oops as well
142 if(!g_sequence_iter_is_end(ul->top) && ul->skip(ul, ul->top, ul->dat))
143 ul->top = ui_listing_prev(ul, ul->top);
144 ui_listing_updateisbegin(ul);
145 }
146
147
ui_listing_create(GSequence * list,gboolean (* skip)(ui_listing_t *,GSequenceIter *,void *),void * dat,const char * (* to_string)(GSequenceIter *))148 ui_listing_t *ui_listing_create(GSequence *list, gboolean (*skip)(ui_listing_t *, GSequenceIter *, void *), void *dat, const char *(*to_string)(GSequenceIter *)) {
149 ui_listing_t *ul = g_slice_new0(ui_listing_t);
150 ul->list = list;
151 ul->sel = ul->top = ui_listing_getbegin(ul);
152 ul->topisbegin = ul->selisbegin = TRUE;
153 ul->skip = skip;
154 ul->dat = dat;
155
156 ul->search_box = NULL;
157 ul->query = NULL;
158 ul->match_start = REGEX_NO_MATCH;
159 ul->to_string = to_string;
160
161 return ul;
162 }
163
164
ui_listing_free(ui_listing_t * ul)165 void ui_listing_free(ui_listing_t *ul) {
166 if(ul->search_box)
167 ui_textinput_free(ul->search_box);
168 if(ul->query)
169 g_free(ul->query);
170 g_slice_free(ui_listing_t, ul);
171 }
172
173
174 // search next/previous
ui_listing_search_advance(ui_listing_t * ul,GSequenceIter * startpos,gboolean prev)175 static void ui_listing_search_advance(ui_listing_t *ul, GSequenceIter *startpos, gboolean prev) {
176 if(g_sequence_iter_is_end(startpos) && g_sequence_iter_is_end((startpos = ui_listing_getbegin(ul))))
177 return;
178 GRegex *regex = ul->query ? g_regex_new(ul->query, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, 0, NULL) : NULL;
179 if(!regex) {
180 ul->match_start = REGEX_ERROR;
181 return;
182 }
183 ul->match_start = REGEX_NO_MATCH;
184
185 GSequenceIter *pos = startpos;
186 do {
187 const char *candidate = ul->to_string(pos);
188 GMatchInfo *match_info;
189 if(g_regex_match(regex, candidate, 0, &match_info)) {
190 g_match_info_fetch_pos(match_info, 0, &ul->match_start, &ul->match_end);
191 g_match_info_free(match_info);
192 ul->sel = pos;
193 break;
194 }
195 g_match_info_free(match_info);
196
197 pos = (prev ? ui_listing_prev : ui_listing_next)(ul, pos);
198 if(g_sequence_iter_is_begin(pos))
199 pos = ui_listing_prev(ul, g_sequence_get_end_iter(ul->list));
200 else if(g_sequence_iter_is_end(pos))
201 pos = ui_listing_getbegin(ul);
202 } while(ul->match_start == REGEX_NO_MATCH && pos != startpos);
203
204 g_regex_unref(regex);
205 }
206
207
208 // handle keys in search mode
ui_listing_search(ui_listing_t * ul,guint64 key)209 static void ui_listing_search(ui_listing_t *ul, guint64 key) {
210 char *completed = NULL;
211 ui_textinput_key(ul->search_box, key, &completed);
212
213 g_free(ul->query);
214 ul->query = completed ? completed : ui_textinput_get(ul->search_box);
215
216 if(completed) {
217 if(ul->match_start < 0) {
218 g_free(ul->query);
219 ul->query = NULL;
220 }
221 ui_textinput_free(ul->search_box);
222 ul->search_box = NULL;
223 ul->match_start = -1;
224 ul->match_end = -1;
225 } else
226 ui_listing_search_advance(ul, ul->sel, FALSE);
227 }
228
229
ui_listing_key(ui_listing_t * ul,guint64 key,int page)230 gboolean ui_listing_key(ui_listing_t *ul, guint64 key, int page) {
231 if(ul->search_box) {
232 ui_listing_search(ul, key);
233 return TRUE;
234 }
235
236 // stop highlighting
237 ul->match_start = REGEX_NO_MATCH;
238
239 switch(key) {
240 case INPT_CHAR('/'): // start search mode
241 if(ul->to_string) {
242 if(ul->query) {
243 g_free(ul->query);
244 ul->query = NULL;
245 }
246 g_assert(!ul->search_box);
247 ul->search_box = ui_textinput_create(FALSE, NULL);
248 }
249 break;
250 case INPT_CHAR(','): // find next
251 ui_listing_search_advance(ul, ui_listing_next(ul, ul->sel), FALSE);
252 break;
253 case INPT_CHAR('.'): // find previous
254 ui_listing_search_advance(ul, ui_listing_prev(ul, ul->sel), TRUE);
255 break;
256 case INPT_KEY(KEY_NPAGE): { // page down
257 int i = page;
258 while(i-- && !g_sequence_iter_is_end(ul->sel))
259 ul->sel = ui_listing_next(ul, ul->sel);
260 if(g_sequence_iter_is_end(ul->sel))
261 ul->sel = ui_listing_prev(ul, ul->sel);
262 break;
263 }
264 case INPT_KEY(KEY_PPAGE): { // page up
265 int i = page;
266 GSequenceIter *begin = ui_listing_getbegin(ul);
267 while(i-- && ul->sel != begin)
268 ul->sel = ui_listing_prev(ul, ul->sel);
269 break;
270 }
271 case INPT_KEY(KEY_DOWN): // item down
272 case INPT_CHAR('j'):
273 ul->sel = ui_listing_next(ul, ul->sel);
274 if(g_sequence_iter_is_end(ul->sel))
275 ul->sel = ui_listing_prev(ul, ul->sel);
276 break;
277 case INPT_KEY(KEY_UP): // item up
278 case INPT_CHAR('k'):
279 ul->sel = ui_listing_prev(ul, ul->sel);
280 break;
281 case INPT_KEY(KEY_HOME): // home
282 ul->sel = ui_listing_getbegin(ul);
283 break;
284 case INPT_KEY(KEY_END): // end
285 ul->sel = ui_listing_prev(ul, g_sequence_get_end_iter(ul->list));
286 break;
287 default:
288 return FALSE;
289 }
290
291 ui_listing_updateisbegin(ul);
292 return TRUE;
293 }
294
295
ui_listing_fixtop(ui_listing_t * ul,int height)296 static void ui_listing_fixtop(ui_listing_t *ul, int height) {
297 // sel before top? top = sel!
298 if(g_sequence_iter_compare(ul->top, ul->sel) > 0)
299 ul->top = ul->sel;
300
301 // does sel still fit on the screen?
302 int i = height;
303 GSequenceIter *n = ul->top;
304 while(n != ul->sel && i > 0) {
305 n = ui_listing_next(ul, n);
306 i--;
307 }
308
309 // Nope? Make sure it fits
310 if(i <= 0) {
311 n = ul->sel;
312 for(i=0; i<height-1; i++)
313 n = ui_listing_prev(ul, n);
314 ul->top = n;
315 }
316
317 // Make sure there's no empty space if we have enough rows to fill the screen
318 i = height;
319 n = ul->top;
320 GSequenceIter *begin = ui_listing_getbegin(ul);
321 while(!g_sequence_iter_is_end(n) && i-- > 0)
322 n = ui_listing_next(ul, n);
323 while(ul->top != begin && i-- > 0)
324 ul->top = ui_listing_prev(ul, ul->top);
325 }
326
327
328 // Every item is assumed to occupy exactly one line.
329 // Returns the relative position of the current page (in percent).
330 // The selected row number is written to *cur, to be used with move(cur, 0).
331 // TODO: The return value is only correct if no skip function is used or if
332 // there are otherwise no hidden rows. It'll give a blatantly wrong number if
333 // there are.
ui_listing_draw(ui_listing_t * ul,int top,int bottom,ui_cursor_t * cur,void (* cb)(ui_listing_t *,GSequenceIter *,int,void *))334 int ui_listing_draw(ui_listing_t *ul, int top, int bottom, ui_cursor_t *cur, void (*cb)(ui_listing_t *, GSequenceIter *, int, void *)) {
335 int search_box_height = !!ul->search_box;
336 int listing_height = 1 + bottom - top - search_box_height;
337 ui_listing_fixtop(ul, listing_height);
338
339 if(cur) {
340 cur->x = 0;
341 cur->y = top;
342 }
343
344 // draw
345 GSequenceIter *n = ul->top;
346 while(top <= bottom - search_box_height && !g_sequence_iter_is_end(n)) {
347 if(cur && n == ul->sel)
348 cur->y = top;
349 cb(ul, n, top, ul->dat);
350 n = ui_listing_next(ul, n);
351 top++;
352 }
353 if(ul->search_box) {
354 const char *status;
355 switch(ul->match_start) {
356 case REGEX_NO_MATCH:
357 status = "no match>";
358 break;
359 case REGEX_ERROR:
360 status = " invalid>";
361 break;
362 default:
363 status = " search>";
364 }
365 mvaddstr(bottom, 0, status);
366 ui_textinput_draw(ul->search_box, bottom, 10, wincols - 10, cur);
367 }
368
369 ui_listing_updateisbegin(ul);
370
371 int last = g_sequence_iter_get_position(g_sequence_get_end_iter(ul->list));
372 return MIN(100, last ? (g_sequence_iter_get_position(ul->top)+listing_height)*100/last : 0);
373 }
374
375
ui_listing_draw_match(ui_listing_t * ul,GSequenceIter * iter,int y,int x,int max)376 void ui_listing_draw_match(ui_listing_t *ul, GSequenceIter *iter, int y, int x, int max) {
377 const char *str = ul->to_string(iter);
378 if(ul->sel == iter && ul->match_start >= 0) {
379 int ofs1 = 0,
380 ofs2 = ul->match_start,
381 ofs3 = ul->match_end,
382 width1 = substr_columns(str + ofs1, ofs2 - ofs1),
383 width2 = substr_columns(str + ofs2, ofs3 - ofs2);
384 mvaddnstr(y, x, str + ofs1, str_offset_from_columns(str + ofs1, MIN(width1, max)));
385 x += width1, max -= width1;
386 attron(A_REVERSE);
387 mvaddnstr(y, x, str + ofs2, str_offset_from_columns(str + ofs2, MIN(width2, max)));
388 x += width2, max -= width2;
389 attroff(A_REVERSE);
390 mvaddnstr(y, x, str + ofs3, str_offset_from_columns(str + ofs3, max));
391 } else {
392 mvaddnstr(y, x, str, max);
393 }
394 }
395