ListWindow: convert list_window_callback_fn_t to an abstract class
[ncmpc-debian.git] / src / screen_keydef.cxx
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2018 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19
20 #include "screen_keydef.hxx"
21 #include "screen_interface.hxx"
22 #include "ListPage.hxx"
23 #include "ListText.hxx"
24 #include "TextListRenderer.hxx"
25 #include "ProxyPage.hxx"
26 #include "screen_status.hxx"
27 #include "screen_find.hxx"
28 #include "screen.hxx"
29 #include "i18n.h"
30 #include "conf.hxx"
31 #include "screen_utils.hxx"
32 #include "options.hxx"
33 #include "Compiler.h"
34
35 #include <algorithm>
36
37 #include <assert.h>
38 #include <errno.h>
39 #include <string.h>
40 #include <glib.h>
41
42 class CommandKeysPage final : public ListPage, ListText {
43         ScreenManager &screen;
44
45         command_definition_t *cmds;
46
47         /**
48          * The command being edited, represented by a array subscript
49          * to @cmds, or -1, if no command is being edited
50          */
51         int subcmd = -1;
52
53         /** The number of keys assigned to the current command */
54         unsigned subcmd_n_keys = 0;
55
56 public:
57         CommandKeysPage(ScreenManager &_screen, WINDOW *w, Size size)
58                 :ListPage(w, size), screen(_screen) {}
59
60         void SetCommand(command_definition_t *_cmds, unsigned _cmd) {
61                 cmds = _cmds;
62                 subcmd = _cmd;
63                 lw.Reset();
64                 check_subcmd_length();
65         }
66
67 private:
68         /** The position of the up ("[..]") item */
69         static constexpr unsigned subcmd_item_up() {
70                 return 0;
71         }
72
73         /** The position of the "add a key" item */
74         gcc_pure
75         unsigned subcmd_item_add() const {
76                 return subcmd_n_keys + 1;
77         }
78
79         /** The number of items in the list_window, if there's a command being edited */
80         gcc_pure
81         unsigned subcmd_length() const {
82                 return subcmd_item_add() + 1;
83         }
84
85         /** Check whether a given item is a key */
86         gcc_pure
87         bool subcmd_item_is_key(unsigned i) const {
88                 return (i > subcmd_item_up() && i < subcmd_item_add());
89         }
90
91         /**
92          * Convert an item id (as in lw.selected) into a "key id", which is an array
93          * subscript to cmds[subcmd].keys.
94          */
95         static constexpr unsigned subcmd_item_to_key_id(unsigned i) {
96                 return i - 1;
97         }
98
99         /* TODO: rename to check_n_keys / subcmd_count_keys? */
100         void check_subcmd_length();
101
102         /**
103          * Delete a key from a given command's definition.
104          *
105          * @param cmd_index the command
106          * @param key_index the key (see below)
107          */
108         void DeleteKey(int cmd_index, int key_index);
109
110         /**
111          * Assigns a new key to a key slot.
112          */
113         void OverwriteKey(int cmd_index, int key_index);
114
115         /**
116          * Assign a new key to a new slot.
117          */
118         void AddKey(int cmd_index);
119
120 public:
121         /* virtual methods from class Page */
122         void OnOpen(struct mpdclient &c) override;
123         void Paint() const override;
124         bool OnCommand(struct mpdclient &c, command_t cmd) override;
125         const char *GetTitle(char *s, size_t size) const override;
126
127 private:
128         /* virtual methods from class ListText */
129         const char *GetListItemText(unsigned i) const override;
130 };
131
132 /* TODO: rename to check_n_keys / subcmd_count_keys? */
133 void
134 CommandKeysPage::check_subcmd_length()
135 {
136         unsigned i;
137
138         /* this loops counts the continous valid keys at the start of the the keys
139            array, so make sure you don't have gaps */
140         for (i = 0; i < MAX_COMMAND_KEYS; i++)
141                 if (cmds[subcmd].keys[i] == 0)
142                         break;
143         subcmd_n_keys = i;
144
145         lw.SetLength(subcmd_length());
146 }
147
148 void
149 CommandKeysPage::DeleteKey(int cmd_index, int key_index)
150 {
151         /* shift the keys to close the gap that appeared */
152         int i = key_index+1;
153         while (i < MAX_COMMAND_KEYS && cmds[cmd_index].keys[i])
154                 cmds[cmd_index].keys[key_index++] = cmds[cmd_index].keys[i++];
155
156         /* As key_index now holds the index of the last key slot that contained
157            a key, we use it to empty this slot, because this key has been copied
158            to the previous slot in the loop above */
159         cmds[cmd_index].keys[key_index] = 0;
160
161         cmds[cmd_index].flags |= COMMAND_KEY_MODIFIED;
162         check_subcmd_length();
163
164         screen_status_printf(_("Deleted"));
165
166         /* repaint */
167         SetDirty();
168
169         /* update key conflict flags */
170         check_key_bindings(cmds, nullptr, 0);
171 }
172
173 void
174 CommandKeysPage::OverwriteKey(int cmd_index, int key_index)
175 {
176         assert(key_index < MAX_COMMAND_KEYS);
177
178         char *buf = g_strdup_printf(_("Enter new key for %s: "),
179                                     cmds[cmd_index].name);
180         const int key = screen_getch(buf);
181         g_free(buf);
182
183         if (key == ERR) {
184                 screen_status_printf(_("Aborted"));
185                 return;
186         }
187
188         if (key == '\0') {
189                 screen_status_printf(_("Ctrl-Space can't be used"));
190                 return;
191         }
192
193         const command_t cmd = find_key_command(key, cmds);
194         if (cmd != CMD_NONE) {
195                 screen_status_printf(_("Error: key %s is already used for %s"),
196                                      key2str(key), get_key_command_name(cmd));
197                 screen_bell();
198                 return;
199         }
200
201         cmds[cmd_index].keys[key_index] = key;
202         cmds[cmd_index].flags |= COMMAND_KEY_MODIFIED;
203
204         screen_status_printf(_("Assigned %s to %s"),
205                              key2str(key),cmds[cmd_index].name);
206         check_subcmd_length();
207
208         /* repaint */
209         SetDirty();
210
211         /* update key conflict flags */
212         check_key_bindings(cmds, nullptr, 0);
213 }
214
215 void
216 CommandKeysPage::AddKey(int cmd_index)
217 {
218         if (subcmd_n_keys < MAX_COMMAND_KEYS)
219                 OverwriteKey(cmd_index, subcmd_n_keys);
220 }
221
222 const char *
223 CommandKeysPage::GetListItemText(unsigned idx) const
224 {
225         static char buf[256];
226
227         if (idx == subcmd_item_up())
228                 return "[..]";
229
230         if (idx == subcmd_item_add()) {
231                 g_snprintf(buf, sizeof(buf), "%d. %s",
232                            idx, _("Add new key"));
233                 return buf;
234         }
235
236         assert(subcmd_item_is_key(idx));
237
238         g_snprintf(buf, sizeof(buf),
239                    "%d. %-20s   (%d) ", idx,
240                    key2str(cmds[subcmd].keys[subcmd_item_to_key_id(idx)]),
241                    cmds[subcmd].keys[subcmd_item_to_key_id(idx)]);
242         return buf;
243 }
244
245 void
246 CommandKeysPage::OnOpen(gcc_unused struct mpdclient &c)
247 {
248         // TODO
249 }
250
251 const char *
252 CommandKeysPage::GetTitle(char *str, size_t size) const
253 {
254         g_snprintf(str, size, _("Edit keys for %s"), cmds[subcmd].name);
255         return str;
256 }
257
258 void
259 CommandKeysPage::Paint() const
260 {
261         lw.Paint(TextListRenderer(*this));
262 }
263
264 bool
265 CommandKeysPage::OnCommand(struct mpdclient &c, command_t cmd)
266 {
267         if (cmd == CMD_LIST_RANGE_SELECT)
268                 return false;
269
270         if (ListPage::OnCommand(c, cmd))
271                 return true;
272
273         switch(cmd) {
274         case CMD_PLAY:
275                 if (lw.selected == subcmd_item_up()) {
276                         screen.OnCommand(c, CMD_GO_PARENT_DIRECTORY);
277                 } else if (lw.selected == subcmd_item_add()) {
278                         AddKey(subcmd);
279                 } else {
280                         /* just to be sure ;-) */
281                         assert(subcmd_item_is_key(lw.selected));
282                         OverwriteKey(subcmd, subcmd_item_to_key_id(lw.selected));
283                 }
284                 return true;
285         case CMD_DELETE:
286                 if (subcmd_item_is_key(lw.selected))
287                         DeleteKey(subcmd, subcmd_item_to_key_id(lw.selected));
288
289                 return true;
290         case CMD_ADD:
291                 AddKey(subcmd);
292                 return true;
293         case CMD_LIST_FIND:
294         case CMD_LIST_RFIND:
295         case CMD_LIST_FIND_NEXT:
296         case CMD_LIST_RFIND_NEXT:
297                 screen_find(screen, &lw, cmd, *this);
298                 SetDirty();
299                 return true;
300
301         default:
302                 return false;
303         }
304
305         /* unreachable */
306         assert(0);
307         return false;
308 }
309
310 class CommandListPage final : public ListPage, ListText {
311         ScreenManager &screen;
312
313         command_definition_t *cmds = nullptr;
314
315         /** the number of commands */
316         unsigned command_n_commands = 0;
317
318 public:
319         CommandListPage(ScreenManager &_screen, WINDOW *w, Size size)
320                 :ListPage(w, size), screen(_screen) {}
321
322         ~CommandListPage() override {
323                 delete[] cmds;
324         }
325
326         command_definition_t *GetCommands() {
327                 return cmds;
328         }
329
330         int GetSelectedCommand() const {
331                 return lw.selected < command_n_commands
332                         ? (int)lw.selected
333                         : -1;
334         }
335
336 private:
337         /**
338          * the position of the "apply" item. It's the same as command_n_commands,
339          * because array subscripts start at 0, while numbers of items start at 1.
340          */
341         gcc_pure
342         unsigned command_item_apply() const {
343                 return command_n_commands;
344         }
345
346         /** the position of the "apply and save" item */
347         gcc_pure
348         unsigned command_item_save() const {
349                 return command_item_apply() + 1;
350         }
351
352         /** the number of items in the "command" view */
353         gcc_pure
354         unsigned command_length() const {
355                 return command_item_save() + 1;
356         }
357
358         /** The position of the up ("[..]") item */
359         static constexpr unsigned subcmd_item_up() {
360                 return 0;
361         }
362
363 public:
364         bool IsModified() const;
365
366         void Apply();
367         void Save();
368
369 public:
370         /* virtual methods from class Page */
371         void OnOpen(struct mpdclient &c) override;
372         void Paint() const override;
373         bool OnCommand(struct mpdclient &c, command_t cmd) override;
374         const char *GetTitle(char *s, size_t size) const override;
375
376 private:
377         /* virtual methods from class ListText */
378         const char *GetListItemText(unsigned i) const override;
379 };
380
381 bool
382 CommandListPage::IsModified() const
383 {
384         command_definition_t *orginal_cmds = get_command_definitions();
385         size_t size = command_n_commands * sizeof(command_definition_t);
386
387         return memcmp(orginal_cmds, cmds, size) != 0;
388 }
389
390 void
391 CommandListPage::Apply()
392 {
393         if (IsModified()) {
394                 command_definition_t *orginal_cmds = get_command_definitions();
395
396                 std::copy_n(cmds, command_n_commands, orginal_cmds);
397                 screen_status_printf(_("You have new key bindings"));
398         } else
399                 screen_status_printf(_("Keybindings unchanged."));
400 }
401
402 void
403 CommandListPage::Save()
404 {
405         char *allocated = nullptr;
406         const char *filename = options.key_file;
407         if (filename == nullptr) {
408                 if (!check_user_conf_dir()) {
409                         screen_status_printf(_("Error: Unable to create directory ~/.ncmpc - %s"),
410                                              strerror(errno));
411                         screen_bell();
412                         return;
413                 }
414
415                 filename = allocated = build_user_key_binding_filename();
416         }
417
418         FILE *f = fopen(filename, "w");
419         if (f == nullptr) {
420                 screen_status_printf(_("Error: %s - %s"), filename, strerror(errno));
421                 screen_bell();
422                 g_free(allocated);
423                 return;
424         }
425
426         if (write_key_bindings(f, KEYDEF_WRITE_HEADER))
427                 screen_status_printf(_("Wrote %s"), filename);
428         else
429                 screen_status_printf(_("Error: %s - %s"), filename, strerror(errno));
430
431         g_free(allocated);
432         fclose(f);
433 }
434
435 const char *
436 CommandListPage::GetListItemText(unsigned idx) const
437 {
438         static char buf[256];
439
440         if (idx == command_item_apply())
441                 return _("===> Apply key bindings ");
442         if (idx == command_item_save())
443                 return _("===> Apply & Save key bindings  ");
444
445         assert(idx < (unsigned) command_n_commands);
446
447         /*
448          * Format the lines in two aligned columnes for the key name and
449          * the description, like this:
450          *
451          *      this-command - do this
452          *      that-one     - do that
453          */
454         size_t len = strlen(cmds[idx].name);
455         strncpy(buf, cmds[idx].name, sizeof(buf));
456
457         if (len < get_cmds_max_name_width(cmds))
458                 memset(buf + len, ' ', get_cmds_max_name_width(cmds) - len);
459
460         g_snprintf(buf + get_cmds_max_name_width(cmds),
461                    sizeof(buf) - get_cmds_max_name_width(cmds),
462                    " - %s", _(cmds[idx].description));
463
464         return buf;
465 }
466
467 void
468 CommandListPage::OnOpen(gcc_unused struct mpdclient &c)
469 {
470         if (cmds == nullptr) {
471                 command_definition_t *current_cmds = get_command_definitions();
472                 command_n_commands = 0;
473                 while (current_cmds[command_n_commands].name)
474                         command_n_commands++;
475
476                 /* +1 for the terminator element */
477                 cmds = new command_definition_t[command_n_commands + 1];
478                 std::copy_n(current_cmds, command_n_commands + 1, cmds);
479         }
480
481         lw.SetLength(command_length());
482 }
483
484 const char *
485 CommandListPage::GetTitle(char *, size_t) const
486 {
487         return _("Edit key bindings");
488 }
489
490 void
491 CommandListPage::Paint() const
492 {
493         lw.Paint(TextListRenderer(*this));
494 }
495
496 bool
497 CommandListPage::OnCommand(struct mpdclient &c, command_t cmd)
498 {
499         if (cmd == CMD_LIST_RANGE_SELECT)
500                 return false;
501
502         if (ListPage::OnCommand(c, cmd))
503                 return true;
504
505         switch(cmd) {
506         case CMD_PLAY:
507                 if (lw.selected == command_item_apply()) {
508                         Apply();
509                         return true;
510                 } else if (lw.selected == command_item_save()) {
511                         Apply();
512                         Save();
513                         return true;
514                 }
515
516                 break;
517
518         case CMD_LIST_FIND:
519         case CMD_LIST_RFIND:
520         case CMD_LIST_FIND_NEXT:
521         case CMD_LIST_RFIND_NEXT:
522                 screen_find(screen, &lw, cmd, *this);
523                 SetDirty();
524                 return true;
525
526         default:
527                 break;
528         }
529
530         return false;
531 }
532
533 class KeyDefPage final : public ProxyPage {
534         ScreenManager &screen;
535
536         CommandListPage command_list_page;
537         CommandKeysPage command_keys_page;
538
539 public:
540         KeyDefPage(ScreenManager &_screen, WINDOW *_w, Size size)
541                 :ProxyPage(_w), screen(_screen),
542                  command_list_page(_screen, _w, size),
543                  command_keys_page(_screen, _w, size) {}
544
545 public:
546         /* virtual methods from class Page */
547         void OnOpen(struct mpdclient &c) override;
548         void OnClose() override;
549         bool OnCommand(struct mpdclient &c, command_t cmd) override;
550 };
551
552 static Page *
553 keydef_init(ScreenManager &screen, WINDOW *w, Size size)
554 {
555         return new KeyDefPage(screen, w, size);
556 }
557
558 void
559 KeyDefPage::OnOpen(struct mpdclient &c)
560 {
561         ProxyPage::OnOpen(c);
562
563         if (GetCurrentPage() == nullptr)
564                 SetCurrentPage(c, &command_list_page);
565 }
566
567 void
568 KeyDefPage::OnClose()
569 {
570         if (command_list_page.IsModified())
571                 screen_status_printf(_("Note: Did you forget to \'Apply\' your changes?"));
572
573         ProxyPage::OnClose();
574 }
575
576 bool
577 KeyDefPage::OnCommand(struct mpdclient &c, command_t cmd)
578 {
579         if (ProxyPage::OnCommand(c, cmd))
580                 return true;
581
582         switch(cmd) {
583         case CMD_PLAY:
584                 if (GetCurrentPage() == &command_list_page) {
585                         int s = command_list_page.GetSelectedCommand();
586                         if (s >= 0) {
587                                 command_keys_page.SetCommand(command_list_page.GetCommands(),
588                                                              s);
589                                 SetCurrentPage(c, &command_keys_page);
590                                 return true;
591                         }
592                 }
593
594                 break;
595
596         case CMD_GO_PARENT_DIRECTORY:
597         case CMD_GO_ROOT_DIRECTORY:
598                 if (GetCurrentPage() != &command_list_page) {
599                         SetCurrentPage(c, &command_list_page);
600                         return true;
601                 }
602
603                 break;
604
605         case CMD_SAVE_PLAYLIST:
606                 command_list_page.Apply();
607                 command_list_page.Save();
608                 return true;
609
610         default:
611                 return false;
612         }
613
614         /* unreachable */
615         assert(0);
616         return false;
617 }
618
619 const struct screen_functions screen_keydef = {
620         "keydef",
621         keydef_init,
622 };