]> Pileus Git - lackey/blob - src/form.c
cb17dabb9393565fc6fa24eeeb60c79287e94ab7
[lackey] / src / form.c
1 /*
2  * Copyright (C) 2015 Andy Spencer <andy753421@gmail.com>
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 #define _XOPEN_SOURCE
19 #define _XOPEN_SOURCE_EXTENDED
20
21 #include <ctype.h>
22 #include <string.h>
23 #include <ncurses.h>
24
25 #include "util.h"
26 #include "date.h"
27 #include "cal.h"
28 #include "view.h"
29 #include "form.h"
30
31 /* Constants */
32 #define DATE_WIDTH   (4+1+2+1+2 +1+ 2+1+2)
33 #define TEXT_WIDTH   (DATE_WIDTH)
34 #define NUMBER_WIDTH (10)
35
36 /* Widget accessors */
37 #define FF_TEXT(f)   (((form_text_t  *)f)->text)
38 #define FF_DATE(f)   (((form_date_t  *)f)->date)
39 #define FF_NUMBER(f) (((form_number_t*)f)->number)
40 #define FF_BUTTON(f) (((form_button_t*)f)->button)
41 #define FF_LIST(f)   (((form_list_t  *)f))
42
43 /* Local variables */
44 static form_t *form;    // current form
45 static WINDOW *win;     // current window
46 static int     arow;    // active row
47 static int     acol;    // active row
48
49 /* Helper functions */
50 static int label_width(const char *label)
51 {
52         int len = 0;
53         for (int i = 0; label[i]; i++)
54                 if (label[i] != '_')
55                         len++;
56         return len;
57 }
58
59 static void label_print(const char *label)
60 {
61         for (int i = 0; label[i]; i++) {
62                 if (label[i] == '_' && label[i+1])
63                         waddch(win, label[++i]
64                                 | A_UNDERLINE | A_BOLD);
65                 else
66                         waddch(win, label[i]);
67         }
68 }
69
70 static int field_width(form_field_t *field)
71 {
72         // Calculate list width
73         int list_size = 0;
74         if (field->type == FORM_LIST) {
75                 for (int i = 0; i < FF_LIST(field)->num; i++) {
76                         int width = strlen(FF_LIST(field)->map[i]);
77                         if (width > list_size)
78                                 list_size = width;
79                 }
80         }
81
82         // Calculate field size
83         int width = 0;
84         if (field->before)
85                 width += strlen(field->before);
86         switch (field->type) {
87                 case FORM_LABEL:  width += label_width(field->label); break;
88                 case FORM_TEXT:   width += TEXT_WIDTH;                break;
89                 case FORM_DATE:   width += DATE_WIDTH;                break;
90                 case FORM_NUMBER: width += NUMBER_WIDTH;              break;
91                 case FORM_BUTTON: width += label_width(field->label); break;
92                 case FORM_LIST:   width += list_size;                 break;
93         }
94         if (field->after)
95                 width += strlen(field->after);
96         return width;
97 }
98
99 static void field_sizes(form_t *form, int *col_size)
100 {
101         // Do this by column, and right to left so that we can add
102         // in the extra space available for blank spaces.
103         for (int c = form->cols-1; c >= 0; c--) {
104                 col_size[c] = 0;
105                 for (int r = 0; r < form->rows; r++) {
106                         form_field_t *field = form->fields[r][c];
107                         if (form->fields[r][c]) {
108                                 int width = field_width(field);
109                                 for (int i = c+1; i < form->cols; i++) {
110                                         if (form->fields[r][i])
111                                                 break;
112                                         width -= col_size[i];
113                                 }
114                                 if (width > col_size[c])
115                                         col_size[c] = width;
116                         }
117                 }
118         }
119 }
120
121 static void field_draw(form_field_t *field, int width, int hover)
122 {
123         char **map   = FF_LIST(field)->map;
124         int    idx   = FF_LIST(field)->idx;
125
126         char *before = field->before ?: "";
127         char *after  = field->after  ?: "";
128
129         int boxed    = field->type == FORM_TEXT ||
130                        field->type == FORM_NUMBER;
131         int bold     = field->attr.bold;
132         int under    = 0;
133
134         int begin    = getcurx(win);
135         int maxstr   = width - strlen(before) - strlen(after);
136
137         if (bold)   wattron(win, A_BOLD);
138         if (hover)  wattron(win, A_REVERSE);
139         if (under)  wattron(win, A_UNDERLINE);
140         if (boxed)  waddch(win, '[');
141         if (boxed)  maxstr -= 2;
142
143         wprintw(win, "%s", before);
144         switch (field->type) {
145                 case FORM_LABEL:
146                         label_print(field->label);
147                         break;
148                 case FORM_TEXT:
149                         if (FF_TEXT(field))
150                                 wprintw(win, "%-.*s",
151                                                 maxstr, FF_TEXT(field));
152                         break;
153                 case FORM_DATE:
154                         if (no_date(&FF_DATE(field)))
155                                 wprintw(win, "%s", "{undefined}");
156                         else
157                                 wprintw(win, "%04d-%02d-%02d %02d:%02d",
158                                         FF_DATE(field).year,  FF_DATE(field).month+1,
159                                         FF_DATE(field).day+1, FF_DATE(field).hour,
160                                         FF_DATE(field).min);
161                         break;
162                 case FORM_NUMBER:
163                         wprintw(win, "%d", FF_NUMBER(field));
164                         break;
165                 case FORM_BUTTON:
166                         wprintw(win, "%s", field->label);
167                         break;
168                 case FORM_LIST:
169                         if (map)
170                                 wprintw(win, "%s", map[idx]);
171                         else
172                                 wprintw(win, "%s", "{undefined}");
173                         break;
174         }
175         int pad = width-(getcurx(win)-begin)-boxed;
176         wprintw(win, "%-*s", pad, after);
177
178         if (boxed)  waddch(win, ']');
179         if (under)  wattroff(win, A_UNDERLINE);
180         if (hover)  wattroff(win, A_REVERSE);
181         if (bold)   wattroff(win, A_BOLD);
182 }
183
184 static int is_active(form_t *form, int r, int c)
185 {
186         for (int i = c; i >= 0; i--) {
187                 if (r == arow && i == acol)
188                         return 1;
189                 if (form->fields[r][i])
190                         break;
191         }
192         for (int i = c+1; i < form->cols; i++) {
193                 if (form->fields[r][i])
194                         break;
195                 if (r == arow && i == acol)
196                         return 1;
197         }
198         return 0;
199 }
200
201 static int can_active(form_t *form, int r, int c)
202 {
203         for (int i = c; i >= 0; i--) {
204                 if (!form->fields[r][i])
205                         continue;
206                 if (form->fields[r][i]->type == FORM_LABEL)
207                         return 0;
208                 return 1;
209         }
210         return 0;
211 }
212
213 static int set_active(form_t *form, int r, int c)
214 {
215         if (can_active(form, r, c)) {
216                 arow = r;
217                 acol = c;
218                 return 1;
219         }
220         return 0;
221 }
222
223 static void move_active(form_t *form, int ro, int co)
224 {
225         // Set first field
226         if (ro==0 && co==0)
227                 for (int r = 0; r < form->rows; r++)
228                 for (int c = 0; c < form->cols; c++)
229                         if (set_active(form, r, c))
230                                 return;
231
232         // Move up/down
233         if (ro) {
234                 for (int ri = arow+ro; ri>=0 && ri<form->rows; ri+=ro) {
235                         if (is_active(form, ri, acol))
236                                 continue;
237                         // Search for a row
238                         for (int ci = acol; ci < form->cols; ci++)
239                                 if (set_active(form, ri, ci))
240                                         return;
241                         for (int ci = acol; ci >= 0; ci--)
242                                 if (set_active(form, ri, ci))
243                                         return;
244                 }
245         }
246
247         // Move left/right
248         if (co) {
249                 for (int ci = acol+co; ci>=0 && ci<form->cols; ci+=co) {
250                         if (is_active(form, arow, ci))
251                                 continue;
252                         // Simple move
253                         if (set_active(form, arow, ci))
254                                 return;
255                 }
256         }
257 }
258
259 static void form_edit(void)
260 {
261         int cur=0, len=0;
262         char buf[512] = {};
263         int col_size[form->cols];
264
265         field_sizes(form, col_size);
266
267         int rrow = arow;
268         int rcol = 0;
269         int rlen = 0;
270         for (int c = 0; c < form->cols; c++) {
271                 if (is_active(form, arow, c))
272                         rlen += col_size[c];
273                 else if (rlen == 0)
274                         rcol += col_size[c];
275                 else
276                         break;
277         }
278
279         int done = 0;
280         curs_set(true);
281         while (!done) {
282                 // Draw
283                 int idx = MAX(0, len-(rlen-2));
284                 wmove(win, rrow, rcol);
285                 wprintw(win, "[%-*.*s]", rlen-2, rlen-2, &buf[idx]);
286                 wmove(win, rrow, rcol+1+cur-idx);
287                 wrefresh(win);
288
289                 // Read next character
290                 MEVENT btn;
291                 int chr = getch();
292                 date_sync();
293                 if (chr == KEY_MOUSE)
294                         if (getmouse(&btn) != OK)
295                                 continue;
296
297                 // Line editing
298                 switch (chr) {
299                         case ERR:
300                         case KEY_RESIZE:
301                         case '\14':
302                         case '\7':
303                                 //view_run(chr, btn.bstate, btn.y, btn.x);
304                                 continue;
305                         case '\012': // Enter
306                                 done = 1;
307                                 break;
308                         case '\033': // Escape
309                                 done = 2;
310                                 break;
311                         case KEY_LEFT:
312                                 if (cur > 0)
313                                         cur--;
314                                 break;
315                         case KEY_RIGHT:
316                                 if (cur < len)
317                                         cur++;
318                                 break;
319                         case KEY_BACKSPACE:
320                                 if (cur > 0) {
321                                         memmove(&buf[cur-1], &buf[cur], (len-cur)+1);
322                                         cur--;
323                                         len--;
324                                 }
325                                 break;
326                         case KEY_DC:
327                                 if (cur < len) {
328                                         memmove(&buf[cur], &buf[cur+1], (len-cur)+1);
329                                         len--;
330                                 }
331                                 break;
332                 }
333
334                 // Text input
335                 if (isprint(chr)) {
336                         if (len+1 < sizeof(buf)) {
337                                 memmove(&buf[cur+1], &buf[cur], (len-cur)+1);
338                                 buf[cur] = chr;
339                                 cur++;
340                                 len++;
341                                 //debug("form: cur=%d, len=%d", cur, len);
342                         } else {
343                                 debug("form: out of space");
344                         }
345                 } else {
346                         debug("form: Unhandled key - Dec %3d,  Hex %02x,  Oct %03o,  Chr <%c>",
347                                         chr, chr, chr, chr);
348                 }
349         }
350         curs_set(false);
351 }
352
353 /* Form functions */
354 void form_show(form_t *_form)
355 {
356         // Save form
357         form = _form;
358 }
359
360 void form_draw(WINDOW *_win)
361 {
362         // Save window
363         win = _win;
364
365         // Validate everything
366         if (!win || !form)
367                 return;
368
369         // Calculate column width
370         int col_size[form->cols];
371         field_sizes(form, col_size);
372
373         // Make sure we have an active field
374         if (!can_active(form, arow, acol))
375                 move_active(form, 0, 0);
376
377         // Display form
378         for (int r = 0; r < form->rows; r++) {
379                 for (int c = 0; c < form->cols; c++) {
380                         form_field_t *field = form->fields[r][c];
381                         // Calculate form field size
382                         int width = col_size[c];
383                         for (int i = c+1; i < form->cols; i++) {
384                                 if (form->fields[r][i])
385                                         break;
386                                 width += col_size[i];
387                         }
388                         // Draw the field
389                         if (field)
390                                 field_draw(field, width, is_active(form, r, c));
391                         else if (c == 0)
392                                 wprintw(win, "%*s", width, "");
393                 }
394                 wprintw(win, "\n");
395         }
396 }
397
398 int form_run(int key, mmask_t btn, int row, int col)
399 {
400         // Validate everything
401         if (!form)
402                 return 0;
403
404         // Check movement keys
405         switch (key) {
406                 case 'h': move_active(form,  0, -1); goto redraw;
407                 case 'j': move_active(form,  1,  0); goto redraw;
408                 case 'k': move_active(form, -1,  0); goto redraw;
409                 case 'l': move_active(form,  0,  1); goto redraw;
410         }
411
412         // Handle mouse movement
413         if (key == KEY_MOUSE && row < form->rows) {
414                 int pos=0, col_size[form->cols];
415                 field_sizes(form, col_size);
416                 for (int c = 0; c < form->cols; c++) {
417                         if (pos < col && col < pos+col_size[c]) {
418                                 if (can_active(form, row, c)) {
419                                         arow = row;
420                                         acol = c;
421                                         goto redraw;
422                                 }
423                         }
424                         pos += col_size[c];
425                 }
426         }
427
428         // Search for hotkeys
429         for (int r = 0; r < form->rows; r++)
430         for (int c = 0; c < form->cols; c++)
431                 if (form->fields[r][c] &&
432                     form->fields[r][c]->hotkey == key) {
433                         arow = r;
434                         acol = c;
435                         goto redraw;
436                 }
437
438         // Check editing (enter)
439         if (key == '\012') {
440                 form_edit();
441                 goto redraw;
442         }
443
444         return 0;
445
446 redraw:
447         werase(win);
448         form_draw(win);
449         wrefresh(win);
450         return 1;
451 }
452
453 /* Test functions */
454 static form_text_t   title     = TEXT('t');
455 static form_text_t   location  = TEXT('o');
456 static form_date_t   start     = DATE('s');
457 static form_date_t   end       = DATE('e');
458 static form_date_t   due_date  = DATE('u');
459 static form_number_t completed = NUMBER('p', .f.after="%");
460 static form_list_t   calendar  = LIST('c');
461 static form_list_t   category  = LIST('g');
462 static form_list_t   repeat    = LIST('r');
463 static form_number_t frequency = NUMBER(0);
464 static form_button_t weekdays  = BUTTONS("Su Mo Tu We Th Fr Sa");
465 static form_text_t   details   = TEXT('d');
466
467 static form_t edit = { 12, 4, {
468         { LABEL("_Title: "),    &title.f                                           },
469         { LABEL("L_ocation: "), &location.f                                        },
470         {                                                                          },
471         { LABEL("_Start: "),    &start.f,    LABEL("  _End: "),       &end.f       },
472         { LABEL("D_ue Date: "), &due_date.f, LABEL("  Com_pleted: "), &completed.f },
473         { LABEL("_Calendar: "), &calendar.f, LABEL("  Cate_gory: "),  &category.f  },
474         {                                                                          },
475         { LABEL("_Repeat: "),   &repeat.f,   LABEL("  Every: "),      &frequency.f },
476         {                                                                          },
477         { NULL,                 &weekdays.f                                        },
478         {                                                                          },
479         { LABEL("_Details: "),  &details.f                                         },
480 } };
481
482 void form_test(void)
483 {
484         /* Setup Curses */
485         initscr();
486         cbreak();
487         noecho();
488         keypad(stdscr, TRUE);
489         start_color();
490         curs_set(false);
491         timeout(100);
492         use_default_colors();
493         mousemask(ALL_MOUSE_EVENTS, NULL);
494
495         /* Init */
496         win = newwin(LINES, COLS, 0, 0);
497         form_show(&edit);
498
499         /* Run */
500         while (1) {
501                 MEVENT btn;
502                 int chr = getch();
503                 if (chr == 'q')
504                         break;
505                 if (chr == KEY_MOUSE)
506                         if (getmouse(&btn) != OK)
507                                 continue;
508                 switch (chr) {
509                         case KEY_RESIZE:
510                                 mvwin(win, 0, 0);
511                                 wresize(win, LINES, COLS);
512                                 refresh();
513                                 werase(win);
514                                 form_draw(win);
515                                 wrefresh(win);
516                                 continue;
517                         case '\14': // Ctrl-L
518                                 clear();
519                         case '\7':  // Ctrl-G
520                                 refresh();
521                                 werase(win);
522                                 form_draw(win);
523                                 wrefresh(win);
524                                 continue;
525                 }
526                 if (form_run(chr, btn.bstate, btn.y, btn.x))
527                         continue;
528                 if (chr == ERR) // timeout
529                         continue;
530         }
531
532         /* Finish */
533         endwin();
534 }