]> git.neil.brown.name Git - edlib.git/blob - lib-popup.c
imageview: don't measure image until first refresh.
[edlib.git] / lib-popup.c
1 /*
2  * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * popup
6  *
7  * A 'popup' dialogue pane can be used to enter a file name or
8  * probably lots of other things.
9  * It gets a high 'z' value so it obscures whatever is behind.
10  *
11  * As well a interacting with its own buffer, a popup can pass events
12  * on to other panes, and it can disappear.
13  * For now these are combined - the <ENTER> key will make the window
14  * disappear and will pass a message with the content of the text
15  * as a string.
16  * The target pane must not disappear while the popup is active.
17  * I need to find a way to control that.
18  *
19  * A popup is created by "PopupTile"
20  * A prefix to be displayed can be added by setting "prefix" on
21  * the popup pane.
22  * A default value can be given with attr "default" which is displated
23  * after prefix
24  * The event sent when the popup is closed can be set by setting
25  * attribute "done-key"
26  * otherwise "PopupDone" is used.
27  *
28  * The "Style" of a popup is a string of characters:
29  * D - parent is whole display (window) rather than single pane
30  * P - position w.r.t another popup - Currently always 'under'
31  * M - multiple lines of text - default is one line
32  * 1 - 1/4 width of parent
33  * 2 - 1/2 width of parent (default)
34  * 3 - 3/4 width of parent
35  * 4 - full width
36  * T - at top of parent (default is centred)
37  * B - at bottom of parent
38  * L - at left of parent (default is centred)
39  * R - at right of parent
40  * x - x,y passed with PopupTile set location of top-left.
41  * s - border at bottom to show document status
42  * a - allow recursive popups
43  * r - permit this popup even inside non-recursive popups
44  * t - temporary - auto-close when focus leaves
45  */
46 #define _GNU_SOURCE /*  for asprintf */
47 #include <unistd.h>
48 #include <stdlib.h>
49 #include <stdio.h>
50 #include <memory.h>
51
52 #define PANE_DATA_TYPE struct popup_info
53 #include "core.h"
54
55 struct popup_info {
56         struct pane     *target safe;
57         struct pane     *parent_popup;
58         char            *style safe;
59         struct command  *done;
60 };
61 #include "core-pane.h"
62
63 static struct map *popup_map;
64 DEF_LOOKUP_CMD(popup_handle, popup_map);
65
66 static int line_height(struct pane *p safe, int scale)
67 {
68         struct call_return cr =
69                 call_ret(all, "Draw:text-size", p, -1, NULL, "x",
70                          scale, NULL, "");
71         return cr.y;
72 }
73
74 static void popup_resize(struct pane *p safe, const char *style safe,
75                          short cix, short ciy)
76 {
77         struct popup_info *ppi = p->data;
78         int x,y,w,h;
79         int lh, bh = 0, bw = 0;
80         char *bhs, *bws;
81         struct xy xyscale = pane_scale(p);
82
83         /* First find the size */
84         lh = line_height(p, xyscale.x);
85         bhs = pane_attr_get(pane_focus(p), "border-height");
86         if (bhs)
87                 bh = atoi(bhs);
88         if (bh <= 0)
89                 bh = line_height(p, 0); /* border height */
90         bws = pane_attr_get(pane_focus(p), "border-width");
91         if (bws)
92                 bw = atoi(bhs);
93         if (bw <= 0)
94                 bw = bh;
95         if (strchr(style, 'M')) {
96                 h = p->parent->h/2 + bh;
97                 attr_set_str(&p->attrs, "render-one-line", "no");
98         } else {
99                 h = bh + lh + bh;
100                 attr_set_str(&p->attrs, "render-one-line", "yes");
101         }
102         if (ppi->parent_popup) {
103                 w = ppi->parent_popup->w;
104                 h = ppi->parent_popup->h;
105                 x = ppi->parent_popup->x;
106                 y = ppi->parent_popup->y + ppi->parent_popup->h;
107         } else {
108                 w = p->parent->w - 2*bw;
109                 if (strchr(style, '1'))
110                         w = w / 4;
111                 else if (strchr(style, '3'))
112                         w = 3 * w / 4;
113                 else if (strchr(style, '4')) {
114                         w = w;
115                         h = p->parent->h - 2 * bh;
116                 } else
117                         w = w / 2;
118
119                 x = p->parent->w/2 - w/2;
120                 y = p->parent->h/2 - h/2;
121                 if (strchr(style, 'T')) { y = 0; h -= bh; }
122                 if (strchr(style, 'B')) { h -= bh; y = p->parent->h - h; }
123                 if (strchr(style, 'L')) x = 0;
124                 if (strchr(style, 'R')) x = p->parent->w - w;
125                 if (strchr(style, 'x')) {
126                         x = cix; y = ciy;
127                         if (p->w > 0)
128                                 w = p->w;
129                         if (p->h > 0)
130                                 h = p->h;
131                 }
132         }
133         pane_resize(p, x, y, w, h);
134 }
135
136 DEF_CMD_CLOSED(popup_close)
137 {
138         struct popup_info *ppi = ci->home->data;
139
140         if (ci->num)
141                 /* Pane had focus, so give to target */
142                 pane_take_focus(ppi->target);
143         command_put(ppi->done);
144         ppi->done = NULL;
145         free(ppi->style);
146         ppi->style = "";
147         return 1;
148 }
149
150 DEF_CMD(popup_notify_close)
151 {
152         struct popup_info *ppi = ci->home->data;
153
154         if (ci->focus == ppi->target) {
155                 /* target is closing, so we close too */
156                 ppi->target = safe_cast NULL;
157                 pane_close(ci->home);
158         }
159         return 1;
160 }
161
162 static void popup_finished(struct pane *focus safe, struct pane *home safe,
163                            const char *result)
164 {
165         struct popup_info *ppi = home->data;
166         struct pane *target = ppi->target;
167         const char *key;
168         const char *aux;
169         struct command *done = ppi->done;
170
171         pane_take_focus(target);
172         key = pane_attr_get(focus, "done-key");
173         if (!key)
174                 key = "PopupDone";
175         aux = pane_attr_get(focus, "popup-aux");
176
177         ppi->done = NULL;
178         pane_close(home);
179         /* home is now closed, so ppi cannot be touched */
180         if (done)
181                 comm_call(done, key, target, 1, NULL, result, 0, NULL, aux);
182         else
183                 call(key, target, 1, NULL, result, 0, NULL, aux);
184 }
185
186 DEF_CMD(popup_abort)
187 {
188         /* A NULL 'result' signals the abort */
189         popup_finished(ci->focus, ci->home, NULL);
190         return 1;
191 }
192
193 static bool popup_set_style(struct pane *p safe)
194 {
195         struct popup_info *ppi = p->data;
196         char *orig_border = attr_find(p->attrs, "borders");
197         bool changed = False;
198
199         if (ppi->parent_popup) {
200                 char *border = pane_attr_get(ppi->parent_popup, "borders");
201                 attr_set_str(&p->attrs, "borders", border);
202         } else {
203                 char border[6];
204                 int i, j;
205
206                 for (i = 0, j = 0; i < 4; i++) {
207                         if (strchr(ppi->style, "TLBR"[i]) == NULL)
208                                 border[j++] = "TLBR"[i];
209                 }
210                 if (strchr(ppi->style, 's'))
211                         /* Force a status line */
212                         border[j++] = 's';
213                 border[j] = 0;
214                 if (!orig_border || strcmp(orig_border, border) != 0) {
215                         attr_set_str(&p->attrs, "borders", border);
216                         changed = True;
217                 }
218         }
219
220         if (strchr(ppi->style, 'a'))
221                 /* allow recursion */
222                 attr_set_str(&p->attrs, "Popup", "ignore");
223         else
224                 attr_set_str(&p->attrs, "Popup", "true");
225         return changed;
226 }
227
228 DEF_CMD(popup_style)
229 {
230         struct popup_info *ppi = ci->home->data;
231
232         if (!ci->str)
233                 return Enoarg;
234
235         free(ppi->style);
236         ppi->style = strdup(ci->str);
237         if (popup_set_style(ci->home))
238                 call("view:changed", ci->focus);
239         popup_resize(ci->home, ppi->style, ci->home->x, ci->home->y);
240         return 1;
241 }
242
243 DEF_CMD(popup_notify_refresh_size)
244 {
245         pane_damaged(ci->home, DAMAGED_SIZE);
246         return 1;
247 }
248
249 DEF_CMD(popup_refresh_size)
250 {
251         struct popup_info *ppi = ci->home->data;
252         char *prompt, *dflt, *prefix;
253         struct pane *focus = pane_focus(ci->home);
254
255         prefix = pane_attr_get(focus, "prefix");
256         prompt = pane_attr_get(focus, "prompt");
257         if (!prefix && prompt) {
258                 char *t = NULL;
259                 dflt = pane_attr_get(focus, "default");
260                 if (!prompt)
261                         prompt = "";
262                 if (dflt)
263                         asprintf(&t, "%s(%s): ", prompt, dflt);
264                 else
265                         asprintf(&t, "%s: ", prompt);
266                 attr_set_str(&focus->attrs, "prefix", t);
267                 free(t);
268         }
269
270         popup_set_style(ci->home);
271         popup_resize(ci->home, ppi->style, ci->home->x, ci->home->y);
272         return 0;
273 }
274
275 DEF_CMD(popup_get_target)
276 {
277         struct popup_info *ppi = ci->home->data;
278         return comm_call(ci->comm2, "callback:get-target", ppi->target);
279 }
280
281 DEF_CMD(popup_ignore)
282 {
283         return 1;
284 }
285
286 DEF_CMD(popup_close_others)
287 {
288         /* For some popups, like search or find-file, it doesn't make sense
289          * to maximize the popup.  For others line email-compose it does.
290          * For now, allow it on multi-line popups.
291          */
292         struct popup_info *ppi = ci->home->data;
293         struct pane *p;
294
295         if (strchr(ppi->style, 'M') == NULL)
296                 return 1;
297         p = call_ret(pane, "OtherPane", ci->focus);
298         if (p) {
299                 home_call(ci->home->focus, "doc:attach-view", p);
300                 pane_take_focus(p);
301         }
302         return 1;
303 }
304
305 DEF_CMD(popup_split)
306 {
307         /* Rather than 'split', this moves the popup to an 'other' pane.
308          * For some popups, like search or find-file, it doesn't make sense
309          * to allow this.  For others line email-compose it does.
310          * For now, allow it on multi-line popups.
311          */
312         struct popup_info *ppi = ci->home->data;
313         struct pane *p;
314
315         if (strchr(ppi->style, 'M') == NULL)
316                 return 1;
317         p = call_ret(pane, "OtherPane", ci->focus);
318         if (p)
319                 p = call_ret(pane, "OtherPane", p);
320         if (p) {
321                 home_call(ci->home->focus, "doc:attach-view", p);
322                 pane_take_focus(p);
323         }
324         return 1;
325 }
326
327 DEF_CMD(popup_set_callback)
328 {
329         struct popup_info *ppi = ci->home->data;
330
331         if (ppi->done)
332                 command_put(ppi->done);
333         ppi->done = NULL;
334         if (ci->comm2)
335                 ppi->done = command_get(ci->comm2);
336         return 1;
337 }
338
339 DEF_CMD(popup_delayed_close)
340 {
341         /* nothing should be using this pane any more */
342         pane_close(ci->focus);
343         return 1;
344 }
345
346 DEF_CMD(popup_defocus)
347 {
348         struct popup_info *ppi = ci->home->data;
349
350         if (strchr(ppi->style, 't') == NULL) {
351                 /* Not interested, target might be though */
352                 home_call(ppi->target, "pane:defocus", ci->focus);
353                 return Efallthrough;
354         }
355
356         if (pane_has_focus(ci->home))
357                 /* We are still on the focal-path from display
358                  * Maybe we focussed in to a sub-popup
359                  */
360                 return Efallthrough;
361         if (call_ret(pane, "ThisPopup", ci->focus))
362                 /* New focus is a popup, so stay for now */
363                 return Efallthrough;
364
365         call_comm("event:on-idle", ci->home, &popup_delayed_close, 1);
366
367         return Efallthrough;
368 }
369
370 DEF_CMD(popup_this)
371 {
372         struct popup_info *ppi = ci->home->data;
373
374         if (strchr(ppi->style, 'a') == NULL &&
375             strcmp(ci->key, "ThisPopup") != 0)
376                 return Efallthrough;
377         return comm_call(ci->comm2, "callback:pane", ci->home,
378                          0, NULL, "Popup");
379 }
380
381 DEF_CMD(popup_other)
382 {
383         /* If a popup is asked for 'Other', return the 'This'
384          * of the target
385          */
386         struct popup_info *ppi = ci->home->data;
387
388         return home_call(ppi->target, "ThisPane", ci->focus,
389                          ci->num, ci->mark, ci->str,
390                          ci->num2, ci->mark2, ci->str2,
391                          ci->x, ci->y,
392                          ci->comm2);
393 }
394
395 DEF_CMD(popup_child_notify)
396 {
397         /* Anything that reponds to ThisPane needs to discard
398          * any children when new are registered.
399          * If none are left, we need to go ourselves.
400          */
401         struct pane *p = ci->home;
402         struct pane *c = ci->focus;
403         struct pane *old;
404
405         if (c->z != 0)
406                 return 1;
407         if (ci->num == -2)
408                 /* When a pane is moved away, not closed, we assume someone will
409                  * move something better in.
410                  */
411                 return 1;
412 restart:
413         list_for_each_entry(old, &p->children, siblings) {
414                 if (c->z != 0)
415                         /* Ignore */
416                         continue;
417                 if (old == c)
418                         /* This pane is under control... */
419                         continue;
420                 if (old->damaged & DAMAGED_CLOSED)
421                         continue;
422                 if (ci->num > 0) {
423                         /* Not the pane we just added, so close it */
424                         pane_close(old);
425                         goto restart;
426                 }
427                 if (ci->num < 0)
428                         /* Not the pane we removed, so not empty yet,
429                          * so nothing to do
430                          */
431                         return 1;
432         }
433         if (ci->num >= 0)
434                 p->focus = c;
435         else
436                 /* Completely empty, so close */
437                 pane_close(p);
438         return 1;
439 }
440
441 DEF_CMD(popup_do_close)
442 {
443         const char *str;
444
445         str = ci->str;
446         if (!str || !str[0])
447                 str = pane_attr_get(ci->focus, "default");
448         if (!str)
449                 str = "";
450         popup_finished(ci->focus, ci->home, str);
451         return 1;
452 }
453
454 DEF_CMD(popup_attach)
455 {
456         /* attach a popup.  It can be attach to the view or the display,
457          * can be in a corner, in a side, or central, and be 1 line or
458          * multi line, and can have controlled width.
459          * These are set with individual character in ci->str as follows.
460          * D  - attach to display, otherwise is on focus.
461          * TBLR - 0, 1, or 2 can be given for center, side, or corner
462          * M  - multi line, else one line
463          * 1234 - how many quarters of width to use.(default 2);
464          * r  - allow recursive popup
465          * t  - temp pane, disappears when it loses focus
466          */
467         struct pane *root, *p, *parent;
468         struct popup_info *ppi;
469         const char *style = ci->str;
470         char *in_popup;
471         struct xy xy;
472         int z = 1;
473
474         if (!style)
475                 style = "D3";
476
477         if (!strchr(style, 'r') && !strchr(style, 'P') &&
478             (in_popup = pane_attr_get(ci->focus, "Popup")) != NULL &&
479             strcmp(in_popup, "ignore") != 0)
480                 /* No recusive popups without permission */
481                 return Efallthrough;
482
483         if (strchr(style, 'D'))
484                 root = call_ret(pane, "RootPane", ci->focus);
485         else if (strchr(style, 'P'))
486                 root = call_ret(pane, "ThisPopup", ci->focus);
487         else
488                 root = call_ret(pane, "ThisPane", ci->focus);
489
490         if (!root)
491                 return Efallthrough;
492
493         /* If focus is already a popup, make this popup higher */
494         p = pane_my_child(root, ci->focus);
495         if (p && p->z > 0)
496                 z = p->z + 1;
497
498         parent = NULL;
499         if (strchr(style, 'P')) {
500                 parent = root;
501                 root = root->parent;
502         }
503
504         p = pane_register(root, z + 1, &popup_handle.c);
505         if (!p)
506                 return Efail;
507         ppi = p->data;
508         ppi->done = NULL;
509         ppi->target = ci->focus;
510
511         ppi->parent_popup = parent;
512
513         ppi->style = strdup(style);
514         popup_set_style(p);
515         xy = pane_mapxy(ci->focus, root, ci->x, ci->y, True);
516         popup_resize(p, style, xy.x, xy.y);
517         attr_set_str(&p->attrs, "render-wrap", "no");
518
519         pane_add_notify(p, ppi->target, "Notify:Close");
520         if (ppi->parent_popup)
521                 pane_add_notify(p, ppi->parent_popup, "Notify:resize");
522
523         pane_take_focus(p);
524
525         if (ci->str2) {
526                 struct pane *doc =
527                         call_ret(pane, "doc:from-text", p, 0, NULL,
528                                  "*popup*", 0, NULL, ci->str2);
529                 if (doc &&
530                     (p = home_call_ret(pane, doc, "doc:attach-view",
531                                        p, -1)) != NULL) {
532                         call("doc:file", p, 1);
533                         call("doc:set:autoclose", p, 1);
534                 }
535         }
536
537         if (!p)
538                 return Efail;
539
540         return comm_call(ci->comm2, "callback:attach", p);
541 }
542
543 void edlib_init(struct pane *ed safe)
544 {
545         call_comm("global-set-command", ed, &popup_attach,
546                   0, NULL, "PopupTile");
547
548         popup_map = key_alloc();
549
550         key_add(popup_map, "Close", &popup_close);
551         key_add(popup_map, "Notify:Close", &popup_notify_close);
552         key_add(popup_map, "Abort", &popup_abort);
553         key_add(popup_map, "popup:style", &popup_style);
554         key_add(popup_map, "Refresh:size", &popup_refresh_size);
555         key_add(popup_map, "view:changed", &popup_refresh_size);
556         key_add(popup_map, "Notify:resize", &popup_notify_refresh_size);
557         key_add(popup_map, "popup:get-target", &popup_get_target);
558         key_add(popup_map, "popup:close", &popup_do_close);
559         key_add(popup_map, "popup:set-callback", &popup_set_callback);
560         key_add(popup_map, "Child-Notify", &popup_child_notify);
561         key_add(popup_map, "ThisPane", &popup_this);
562         key_add(popup_map, "OtherPane", &popup_other);
563         key_add(popup_map, "ThisPopup", &popup_this);
564
565         key_add(popup_map, "Window:bury", &popup_do_close);
566         key_add(popup_map, "Window:close", &popup_abort);
567         key_add(popup_map, "Window:split-x", &popup_split);
568         key_add(popup_map, "Window:split-y", &popup_split);
569         key_add(popup_map, "Window:x+", &popup_ignore);
570         key_add(popup_map, "Window:x-", &popup_ignore);
571         key_add(popup_map, "Window:y+", &popup_ignore);
572         key_add(popup_map, "Window:y-", &popup_ignore);
573         key_add(popup_map, "Window:close-others", &popup_close_others);
574         key_add(popup_map, "pane:defocus", &popup_defocus);
575 }