changes in popup menu is now preserved
[taskasaur.git] / menu.c
1
2 #include <stdlib.h>
3 #include <stddef.h>
4 #include <string.h>
5 #include <stdbool.h>
6 #include <ncurses.h>
7
8 #include "headers/menu.h"
9 #include "headers/render.h"
10 #include "headers/utils.h"
11
12 #define MENU_PAD_TOP 2
13 #define MENU_PAD_BOTTOM 2
14 #define MENU_PAD_LEFT 2
15 #define MENU_PAD_RIGHT 1
16
17 #define MAX_CONTENTS_LENGTH 256
18
19 typedef struct MenuItem {
20     char* title;
21     char* description;
22     void* userdata;
23 } MenuItem;
24
25 typedef struct Menu {
26     char* menu_name;
27     MenuItem** menu_items;
28     int menu_length;
29     int selected_item;
30     int scroll_offset;
31     bool focused;
32     WINDOW* menu_win;
33     int max_height;
34     int max_width;
35     void* userdata;
36     void (*render_item)(Menu*, int, int);
37     int (*item_height)(MenuItem*);
38 } Menu;
39
40 int swap_item(Menu* menu, int src_index, int dest_index);
41
42 /* insert mode */
43 int menu_insert_mode(Menu* menu, int insert_index);
44
45 /* prob temp for now */
46 MenuItem* create_blank_menuitem(void);
47
48 /* rendering stuff */
49 void default_render_item(Menu* menu, int item_index, int start_y);
50 int default_item_height(MenuItem* menuitem);
51 int items_visible(Menu* menu, int offset);
52 int items_visible_rev(Menu* menu, int offset);
53
54 MenuItem*
55 create_menuitem(char* title)
56 {
57     MenuItem* new_menuitem;
58     TodoItem* new_userdata;
59
60     new_menuitem = malloc(sizeof(MenuItem));
61     new_menuitem->title = title;
62     new_menuitem->description = strdup(""); //TEMP FOR NOW
63
64     new_userdata = malloc(sizeof(TodoItem));
65     new_userdata->item_name = title;
66     new_userdata->description = strdup("");
67     new_userdata->due = strdup("");
68     new_userdata->subtask_list = malloc(0);
69     new_userdata->subtask_count = 0;
70     
71     new_menuitem->userdata = new_userdata;
72
73     return new_menuitem;
74 }
75
76 MenuItem*
77 create_blank_menuitem(void)
78 {
79     return create_menuitem(strdup(""));
80 }
81
82 Menu* 
83 create_menu(char* menu_name, MenuItem** item_list)
84 {
85     Menu* new_menu;
86
87     new_menu = malloc(sizeof(Menu));
88     new_menu->menu_name = menu_name;
89     new_menu->menu_items = item_list;
90     new_menu->menu_length = array_length(MenuItem*, item_list);
91     new_menu->selected_item = 0;
92     new_menu->scroll_offset = 0;
93     new_menu->focused = false;
94     new_menu->userdata = NULL;
95     new_menu->render_item = *default_render_item;
96     new_menu->item_height = *default_item_height;
97     set_menu_win(new_menu, stdscr);
98
99     return new_menu;
100 }
101
102 /* getters */
103 WINDOW*
104 get_menu_win(Menu* menu)
105 {
106     return menu->menu_win;
107 }
108
109 MenuItem*
110 get_menu_item(Menu* menu, int index)
111 {
112     if (index < 0 || index >= menu->menu_length) return NULL;
113
114     return menu->menu_items[index];
115 }
116
117 MenuItem**
118 get_menu_itemlist(Menu* menu)
119 {
120     return menu->menu_items;
121 }
122
123 int
124 get_selected_item(Menu* menu)
125 {
126     return menu->selected_item;
127 }
128
129 MenuItem*
130 get_selected_menuitem(Menu* menu)
131 {
132     return get_menu_item(menu, get_selected_item(menu));
133 }
134
135 bool
136 get_menu_focused(Menu* menu)
137 {
138     return menu->focused;
139 }
140
141 int
142 get_menu_length(Menu* menu)
143 {
144     return menu->menu_length;
145 }
146
147 char*
148 get_menu_name(Menu* menu)
149 {
150     return menu->menu_name;
151 }
152
153 void*
154 get_menu_userdata(Menu* menu)
155 {
156     return menu->userdata;
157 }
158
159 char*
160 get_menuitem_title(MenuItem* menuitem)
161 {
162     return menuitem->title;
163 }
164
165 char*
166 get_menuitem_descrip(MenuItem* menuitem)
167 {
168     return menuitem->description;
169 }
170
171 void*
172 get_menuitem_userdata(MenuItem* menuitem)
173 {
174     return menuitem->userdata;
175 }
176
177
178 /* setters */
179 int
180 set_menu_win(Menu* menu, WINDOW* win)
181 {
182     int height, width;
183
184     menu->menu_win = win;
185     getmaxyx(menu->menu_win, height, width);
186
187     menu->max_height = height-MENU_PAD_TOP-MENU_PAD_BOTTOM;
188     menu->max_width = width-MENU_PAD_LEFT-MENU_PAD_RIGHT;
189
190     return 0;
191 }
192
193 int
194 set_selected_item(Menu* menu, int selected_item)
195 {
196     menu->selected_item = selected_item;
197     return 0;
198 }
199
200 int
201 set_menu_focus(Menu* menu, bool focus)
202 {
203     menu->focused = focus;
204     return 0;
205 }
206
207 int
208 set_menu_userdata(Menu* menu, void* userdata)
209 {
210     menu->userdata = userdata;
211     return 0;
212 }
213
214 int
215 set_menu_renderitem(Menu* menu, void (*render_item)(Menu*, int, int))
216 {
217     menu->render_item = render_item;
218     return 0;
219 }
220
221 int
222 set_menu_itemheight(Menu* menu, int (*item_height)(MenuItem*))
223 {
224     menu->item_height = item_height;
225     return 0;
226 }
227
228 int
229 set_menuitem_title(MenuItem* menuitem, char* title)
230 {
231     menuitem->title = title;
232     return 0;
233 }
234
235 int
236 set_menuitem_descrip(MenuItem* menuitem, char* descrip)
237 {
238     menuitem->description = descrip;
239     return 0;
240 }
241
242 int
243 set_menuitem_userdata(MenuItem* menuitem, void* userdata)
244 {
245     menuitem->userdata = userdata;
246     return 0;
247 }
248
249
250 int
251 swap_item(Menu* menu, int src_index, int dest_index)
252 {
253     ar_swap_item((void**)menu->menu_items, src_index, dest_index);
254
255     return 0;
256 }
257
258 int
259 delete_item(Menu* menu, int index)
260 {
261     if (index < 0 || index > menu->menu_length-1) return -1;
262
263     for (int i = index; i <= menu->menu_length-1; i++) {
264         menu->menu_items[i] = menu->menu_items[i+1];
265     }
266
267     menu->menu_items = realloc(menu->menu_items, menu->menu_length*sizeof(MenuItem*)); 
268     menu->menu_items[menu->menu_length-1] = 0; // preserve null at end
269
270     menu->menu_length -= 1;
271
272     /* also move the current selected position if it's last */
273     if (menu->selected_item > menu->menu_length-1) {
274         menu->selected_item = menu->menu_length-1;
275     }
276
277     return 0;
278 }
279
280 int 
281 insert_item(Menu* menu, MenuItem* menuitem, int index)
282 { // note, this func does not validate index
283
284     /* resize array and insert */
285     menu->menu_items = realloc(menu->menu_items, (menu->menu_length+2)*sizeof(MenuItem*));
286
287     for (int i = menu->menu_length; i > index; i--) {
288         menu->menu_items[i] = menu->menu_items[i-1];
289     }
290
291     menu->menu_items[index] = menuitem;
292     menu->menu_items[menu->menu_length+1] = 0; // remember null at end   
293     menu->menu_length += 1;
294
295     /* move cursor pos */
296     menu->selected_item = index;
297
298     return 0;
299 }
300
301 int
302 menu_insert_mode(Menu* menu, int insert_index)
303 {
304     char temp[MAX_CONTENTS_LENGTH+1]; // remember null
305     char* new_contents;
306     int insert_pos;
307
308     curs_on();
309
310     // account for multiline items
311     insert_pos = menu->scroll_offset;
312     for (int i = 0; i < insert_index; i++) {
313         insert_pos += menu->item_height(menu->menu_items[i]);
314     }
315
316     /* move cursor to right spot */
317     ungetstr(menu->menu_items[insert_index]->title);
318     mvwgetnstr(menu->menu_win,
319         insert_pos, 
320         0,
321         temp,
322         MAX_CONTENTS_LENGTH
323     );
324     curs_off();
325
326     /* copy out */
327     new_contents = strdup(temp);
328     menu->menu_items[insert_index]->title = new_contents;
329
330     /* delete if empty - maybe move this to a cleanup stage */
331     if (strlen(new_contents) == 0) {
332         delete_item(menu, insert_index);
333     }
334
335     return 0;
336 }
337
338 int
339 menu_driver(Menu* menu, MenuAction action)
340 {
341
342     switch (action) {
343         case MENU_UP:
344             menu->selected_item = menu->selected_item-1 >= 0 ? menu->selected_item-1 : 0;
345             break;
346
347         case MENU_DOWN:
348             menu->selected_item = menu->selected_item+1 <= menu->menu_length-1 ? menu->selected_item+1 : menu->menu_length-1;
349             break;
350
351         case MENU_TOP:
352             menu->selected_item = 0;
353             break;
354
355         case MENU_BOTTOM:
356             menu->selected_item = menu->menu_length-1;
357             break;
358
359         case MENU_MOVE_UP:
360             if (menu->selected_item <= 0) break;
361             swap_item(menu, menu->selected_item, menu->selected_item-1);
362             menu->selected_item -= 1;
363             break;
364
365         case MENU_MOVE_DOWN:
366             if (menu->selected_item >= menu->menu_length-1) break;
367             swap_item(menu, menu->selected_item, menu->selected_item+1);
368             menu->selected_item += 1;
369             break;
370
371         case MENU_DELETE:
372             delete_item(menu, menu->selected_item);
373             break;
374
375         case MENU_APPEND:
376             insert_item(menu, create_blank_menuitem(), menu->menu_length);
377             render_menu(menu); // refresh after inserting
378             menu_insert_mode(menu, menu->selected_item);
379             break;
380
381         case MENU_INSERT_ABOVE:
382             insert_item(menu, create_blank_menuitem(), menu->selected_item);
383             render_menu(menu);
384             menu_insert_mode(menu, menu->selected_item);
385             break;
386
387         case MENU_INSERT_BELOW:
388             insert_item(menu, create_blank_menuitem(), menu->selected_item+1);
389             render_menu(menu);
390             menu_insert_mode(menu, menu->selected_item); // inserted item is cur now
391             break;
392
393         case MENU_EDIT:
394             menu_insert_mode(menu, menu->selected_item);
395             break;
396
397         default: // This is here for debug, disable later
398             fprintf(stderr, "Invalid menu action");
399     }
400
401     return 0;
402 }
403
404 int
405 render_menu(Menu* menu)
406 {
407     wclear(menu->menu_win);
408
409     /* calculate scroll */
410     int visible;
411
412     visible = items_visible(menu, menu->scroll_offset);
413
414     if (menu->selected_item >= menu->scroll_offset+visible) {
415         menu->scroll_offset = clamp(
416             menu->selected_item-items_visible_rev(menu, menu->selected_item)+1,
417             0,
418             floorzero(menu->menu_length-1)
419         );
420
421     } else if (menu->selected_item < menu->scroll_offset) {
422         menu->scroll_offset = clamp(
423             menu->selected_item,
424             0,
425             floorzero(menu->menu_length-1)
426         );
427     }
428
429     /* render menu items */
430     int curline = 0;
431     for (int i = menu->scroll_offset; i < menu->menu_length; i++) {
432         menu->render_item(menu, i, curline);
433         curline += menu->item_height(menu->menu_items[i]);
434     }
435
436     wrefresh(menu->menu_win);
437
438     return 0;
439 }
440
441 void
442 default_render_item(Menu* menu, int item_index, int start_y)
443 {
444     MenuItem* curitem;
445     int hlcolor;
446
447     curitem = menu->menu_items[item_index];
448
449     /* color selected item */
450     hlcolor = COLOR_PAIR((item_index == menu->selected_item && menu->focused == true) ? TS_SELECTED : TS_NONSELECTED);
451     wattron(menu->menu_win, hlcolor);
452     mvwprintw(menu->menu_win, start_y, 0, curitem->title);
453     wattroff(menu->menu_win, hlcolor);
454 }
455
456
457 int
458 default_item_height(MenuItem* menuitem)
459
460     return 1; // maybe account for line wrap by default
461 }
462
463 int
464 items_visible(Menu* menu, int offset)
465 {
466     int maxheight;
467     int maxwidth; // unused
468
469     getmaxyx(menu->menu_win, maxheight, maxwidth);
470
471     int vis = 0;
472     int lines = 0;
473     for (int i = offset ; i < menu->menu_length; i++) {
474
475         lines += menu->item_height(menu->menu_items[i]);
476         if (lines > maxheight) break;
477         vis += 1;
478
479     }
480
481     return vis;
482 }
483
484 int
485 items_visible_rev(Menu* menu, int offset)
486 {
487     int maxheight;
488     int maxwidth; // unused
489
490     getmaxyx(menu->menu_win, maxheight, maxwidth);
491
492     int vis = 0;
493     int lines = 0;
494     for (int i = offset; i > 0; i--) {
495
496         lines += menu->item_height(menu->menu_items[i]);
497         if (lines > maxheight) break;
498         vis +=1;
499
500     }
501
502     return vis;
503 }
504
505 int
506 free_menu(Menu* menu)
507 {
508     return 0;
509 }
510