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