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