1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 //    * Redistributions of source code must retain the above copyright notice,
11 //      this list of conditions and the following disclaimer.
12 //
13 //    * Redistributions in binary form must reproduce the above copyright
14 //      notice, this list of conditions and the following disclaimer in the
15 //      documentation and/or other materials provided with the distribution.
16 //
17 //    * Neither the name of the author nor the names of other contributors may
18 //      be used to endorse or promote products derived from this software
19 //      without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34 
35 #include <stdafx.h>
36 #include "Hotkeys.h"
37 #include <musikcore/support/Preferences.h>
38 #include <app/util/Playback.h>
39 #include <unordered_map>
40 #include <unordered_set>
41 #include <iostream>
42 
43 using namespace musik::cube;
44 using namespace musik::core;
45 
46 #define ENSURE_LOADED() { if (!preferences) { loadPreferences(); } }
47 
48 using Id = Hotkeys::Id;
49 
50 /* sigh: http://stackoverflow.com/a/24847480 */
51 struct EnumHasher {
52     template <typename T>
operator ()EnumHasher53     std::size_t operator()(T t) const noexcept {
54         return static_cast<std::size_t>(t);
55     }
56 };
57 
58 /* map from internal ID to user-friendly JSON key name */
59 static std::unordered_map<std::string, Id> NAME_TO_ID = {
60     { "key_up", Id::Up },
61     { "key_down", Id::Down },
62     { "key_left", Id::Left },
63     { "key_right", Id::Right },
64     { "key_page_up", Id::PageUp },
65     { "key_page_down", Id::PageDown },
66     { "key_home", Id::Home },
67     { "key_end", Id::End },
68 
69     { "navigate_library", Id::NavigateLibrary },
70     { "navigate_library_browse", Id::NavigateLibraryBrowse },
71     { "navigate_library_browse_artists", Id::NavigateLibraryBrowseArtists },
72     { "navigate_library_browse_albums", Id::NavigateLibraryBrowseAlbums },
73     { "navigate_library_browse_genres", Id::NavigateLibraryBrowseGenres },
74     { "navigate_library_album_artists", Id::NavigateLibraryBrowseAlbumArtists },
75     { "navigate_library_playlists", Id::NavigateLibraryBrowsePlaylists },
76     { "navigate_library_choose_category", Id::NavigateLibraryBrowseChooseCategory },
77     { "navigate_library_browse_directories", Id::NavigateLibraryBrowseDirectories },
78     { "navigate_library_filter", Id::NavigateLibraryFilter },
79     { "navigate_library_tracks", Id::NavigateLibraryTracks },
80     { "navigate_library_play_queue", Id::NavigateLibraryPlayQueue },
81     { "navigate_settings", Id::NavigateSettings },
82     { "navigate_console", Id::NavigateConsole },
83     { "navigate_lyrics", Id::NavigateLyrics },
84     { "navigate_hotkeys", Id::NavigateHotkeys},
85     { "navigate_jump_to_playing", Id::NavigateJumpToPlaying },
86 
87     { "play_queue_move_up", Id::PlayQueueMoveUp },
88     { "play_queue_move_down", Id::PlayQueueMoveDown },
89     { "play_queue_delete", Id::PlayQueueDelete },
90     { "play_queue_playlist_load", Id::PlayQueuePlaylistLoad },
91     { "play_queue_playlist_save", Id::PlayQueuePlaylistSave },
92     { "play_queue_playlist_rename", Id::PlayQueuePlaylistRename },
93     { "play_queue_playlist_delete", Id::PlayQueuePlaylistDelete },
94     { "play_queue_hot_swap", Id::PlayQueueHotSwap },
95     { "play_queue_clear", Id::PlayQueueClear },
96 
97     { "browse_playlists_new", Id::BrowsePlaylistsNew },
98     { "browse_playlists_save", Id::BrowsePlaylistsSave },
99     { "browse_playlists_rename", Id::BrowsePlaylistsRename },
100     { "browse_playlists_delete", Id::BrowsePlaylistsDelete },
101 
102     { "track_list_rate_track", Id::TrackListRateTrack },
103     { "track_list_change_sort_order", Id::TrackListChangeSortOrder },
104     { "track_list_next_group", Id::TrackListNextGroup },
105     { "track_list_previous_group", Id::TrackListPreviousGroup },
106     { "track_list_play_from_top", Id::TrackListPlayFromTop },
107 
108     { "search_input_toggle_match_type", Id::SearchInputToggleMatchType },
109 
110     { "lyrics_retry", Id::LyricsRetry },
111 
112     { "playback_toggle_mute", Id::ToggleMute },
113     { "playback_toggle_pause", Id::TogglePause },
114     { "playback_next", Id::Next },
115     { "playback_previous", Id::Previous },
116     { "playback_volume_up", Id::VolumeUp },
117     { "playback_volume_down", Id::VolumeDown },
118     { "playback_seek_forward", Id::SeekForward },
119     { "playback_seek_forward_proportional", Id::SeekForwardProportional },
120     { "playback_seek_back_proportional", Id::SeekBackProportional },
121     { "playback_seek_back", Id::SeekBack },
122     { "playback_toggle_repeat", Id::ToggleRepeat },
123     { "playback_toggle_shuffle", Id::ToggleShuffle },
124     { "playback_stop", Id::Stop },
125 
126     { "view_refresh", Id::ViewRefresh },
127 
128     { "toggle_visualizer", Id::ToggleVisualizer },
129 
130     { "show_equalizer", Id::ShowEqualizer },
131 
132     { "metadata_rescan", Id::RescanMetadata },
133 
134     { "hotkeys_reset_to_default", Id::HotkeysResetToDefault },
135     { "hotkeys_backup", Id::HotkeysBackup },
136 
137     { "context_menu", Id::ContextMenu }
138 };
139 
140 /* default hotkeys */
141 static std::unordered_map<Id, std::string, EnumHasher> ID_TO_DEFAULT = {
142     { Id::Up, "KEY_UP" },
143     { Id::Down, "KEY_DOWN" },
144     { Id::Left, "KEY_LEFT" },
145     { Id::Right, "KEY_RIGHT" },
146     { Id::PageUp, "KEY_PPAGE" },
147     { Id::PageDown, "KEY_NPAGE" },
148     { Id::Home, "KEY_HOME" },
149     { Id::End, "KEY_END" },
150 
151     { Id::NavigateLibrary, "a" },
152     { Id::NavigateLibraryBrowse, "b" },
153     { Id::NavigateLibraryBrowseArtists, "1" },
154     { Id::NavigateLibraryBrowseAlbums, "2" },
155     { Id::NavigateLibraryBrowseGenres, "3" },
156     { Id::NavigateLibraryBrowseAlbumArtists, "4" },
157     { Id::NavigateLibraryBrowsePlaylists, "5" },
158     { Id::NavigateLibraryBrowseChooseCategory, "6" },
159     { Id::NavigateLibraryBrowseDirectories, "d" },
160 
161     { Id::NavigateLibraryFilter, "f" },
162     { Id::NavigateLibraryTracks, "t" },
163     { Id::NavigateLibraryPlayQueue, "n" },
164     { Id::NavigateSettings, "s" },
165     { Id::NavigateConsole, "`" },
166     { Id::NavigateLyrics, "^L" },
167     { Id::NavigateHotkeys, "?" },
168     { Id::NavigateJumpToPlaying, "x" },
169 
170 #ifdef __APPLE__
171     /* M-up, M-down don't seem to work on iTerm2, and the delete
172     key doesn't exist on most macbooks. ugly special mappings here. */
173     { Id::PlayQueueMoveUp, "CTL_UP" },
174     { Id::PlayQueueMoveDown, "CTL_DOWN" },
175     { Id::PlayQueueDelete, "KEY_BACKSPACE" },
176 #else
177     { Id::PlayQueueMoveUp, "M-up" },
178     { Id::PlayQueueMoveDown, "M-down" },
179     { Id::PlayQueueDelete, "KEY_DC" },
180 #endif
181     { Id::PlayQueuePlaylistLoad, "M-l" },
182     { Id::PlayQueuePlaylistSave, "M-s" },
183     { Id::PlayQueuePlaylistRename, "M-r" },
184     { Id::PlayQueuePlaylistDelete, "M-x" },
185     { Id::PlayQueueHotSwap, "M-a" },
186     { Id::PlayQueueClear, "X" },
187 
188     { Id::BrowsePlaylistsSave, "M-s" },
189     { Id::BrowsePlaylistsNew, "M-n" },
190     { Id::BrowsePlaylistsRename, "M-r" },
191 #ifdef __APPLE__
192     { Id::BrowsePlaylistsDelete, "KEY_BACKSPACE" },
193 #else
194     { Id::BrowsePlaylistsDelete, "KEY_DC" },
195 #endif
196 
197     { Id::TrackListRateTrack, "r" },
198     { Id::TrackListChangeSortOrder, "M-s" },
199     { Id::TrackListNextGroup, "]" },
200     { Id::TrackListPreviousGroup, "[" },
201     { Id::TrackListPlayFromTop, "M-P" },
202 
203     { Id::SearchInputToggleMatchType, "M-m" },
204 
205     { Id::LyricsRetry, "r" },
206 
207     { Id::ToggleMute, "m" },
208     { Id::TogglePause, "^P" },
209     { Id::Next, "l" },
210     { Id::Previous, "j" },
211     { Id::VolumeUp, "i" },
212     { Id::VolumeDown, "k" },
213     { Id::SeekForward, "o" },
214     { Id::SeekBack, "u" },
215     { Id::SeekForwardProportional, "p" },
216     { Id::SeekBackProportional, "y" },
217     { Id::ToggleRepeat, "." },
218     { Id::ToggleShuffle, "," },
219     { Id::Stop, "^X" },
220 
221     { Id::ViewRefresh, "KEY_F(5)" },
222 
223     { Id::ToggleVisualizer, "v" },
224 
225     { Id::ShowEqualizer, "^E" },
226 
227     { Id::RescanMetadata, "^R"},
228 
229     { Id::HotkeysResetToDefault, "M-r" },
230     { Id::HotkeysBackup, "M-b" },
231 
232     { Id::ContextMenu, "M-enter" }
233 };
234 
235 /* custom keys */
236 static std::unordered_map<Id, std::string, EnumHasher> customIdToKey;
237 
238 /* preferences file */
239 static std::shared_ptr<Preferences> preferences;
240 
savePreferences()241 static void savePreferences() {
242     for (const auto& pair : NAME_TO_ID) {
243         preferences->SetString(
244             pair.first.c_str(),
245             Hotkeys::Get(pair.second).c_str());
246     }
247 
248     preferences->Save();
249 }
250 
loadPreferences()251 static void loadPreferences() {
252     preferences = Preferences::ForComponent("hotkeys", Preferences::ModeReadWrite);
253 
254     try {
255         if (preferences) {
256             std::vector<std::string> names;
257             preferences->GetKeys(names);
258             for (auto n : names) {
259                 const auto it = NAME_TO_ID.find(n);
260                 if (it != NAME_TO_ID.end()) {
261                     customIdToKey[it->second] = preferences->GetString(n);
262                 }
263             }
264         }
265 
266         /* write back to disk; this way any new hotkey defaults are picked
267         up and saved so the user can edit them easily. */
268         savePreferences();
269     }
270     catch (...) {
271         std::cerr << "failed to load hotkeys.json! default hotkeys selected.";
272         customIdToKey.clear();
273     }
274 }
275 
Is(Id id,const std::string & kn)276 bool Hotkeys::Is(Id id, const std::string& kn) {
277     ENSURE_LOADED()
278 
279     /* see if the user has specified a custom value for this hotkey. if
280     they have, compare it to the custom value. */
281     const auto custom = customIdToKey.find(id);
282     if (custom != customIdToKey.end()) {
283         return (custom->second == kn);
284     }
285 
286     /* otherwise, let's compare against the default key */
287     const auto it = ID_TO_DEFAULT.find(id);
288     if (it != ID_TO_DEFAULT.end() && it->second == kn) {
289         return true;
290     }
291 
292     return false;
293 }
294 
295 template <typename T>
find(Id id,T & map)296 std::string find(Id id, T& map) {
297     const auto it = map.find(id);
298     if (it != map.end()) {
299         return it->second;
300     }
301     return "";
302 }
303 
304 template <typename T>
find(const std::string & kn,T & map)305 Hotkeys::Id find(const std::string& kn, T& map) {
306     for (auto it : map) {
307         if (it.second == kn) {
308             return it.first;
309         }
310     }
311     return Hotkeys::COUNT;
312 }
313 
Default(Id id)314 std::string Hotkeys::Default(Id id) {
315     ENSURE_LOADED()
316     return find(id, ID_TO_DEFAULT);
317 }
318 
Custom(Id id)319 std::string Hotkeys::Custom(Id id) {
320     ENSURE_LOADED()
321     return find(id, customIdToKey);
322 }
323 
Get(Id id)324 std::string Hotkeys::Get(Id id) {
325     auto kn = Custom(id);
326     return kn.size() ? kn : Default(id);
327 }
328 
Set(Id id,const std::string & kn)329 void Hotkeys::Set(Id id, const std::string& kn) {
330     customIdToKey[id] = kn;
331     savePreferences();
332 }
333 
Reset()334 void Hotkeys::Reset() {
335     customIdToKey.clear();
336     savePreferences();
337     loadPreferences();
338 }
339 
Existing(const std::string & kn)340 std::string Hotkeys::Existing(const std::string& kn) {
341     auto id = find(kn, customIdToKey);
342     if (id == Hotkeys::COUNT) {
343         id = find(kn, ID_TO_DEFAULT);
344         if (customIdToKey.find(id) != customIdToKey.end()) {
345             /* we found a default key for this one, but that default
346             binding has already been overridden! ensure we return
347             that it's available. */
348             id = Hotkeys::COUNT;
349         }
350     }
351     return id != Hotkeys::COUNT ? Name(id) : "";
352 }
353 
Name(Id id)354 std::string Hotkeys::Name(Id id) {
355     for (auto entry : NAME_TO_ID) {
356         if (entry.second == id) {
357             return entry.first;
358         }
359     }
360     return "<error>";
361 }
362 
363 class NavigationKeysImpl : public cursespp::INavigationKeys {
364     public:
Up(const std::string & key)365         bool Up(const std::string& key) override { return Up() == key; }
Down(const std::string & key)366         bool Down(const std::string& key) override { return Down() == key; }
Left(const std::string & key)367         bool Left(const std::string& key) override { return Left() == key; }
Right(const std::string & key)368         bool Right(const std::string& key) override { return Right() == key; }
PageUp(const std::string & key)369         bool PageUp(const std::string& key) override { return PageUp() == key; }
PageDown(const std::string & key)370         bool PageDown(const std::string& key) override { return PageDown() == key; }
Home(const std::string & key)371         bool Home(const std::string& key) override { return Home() == key; }
End(const std::string & key)372         bool End(const std::string& key) override { return End() == key; }
Next(const std::string & key)373         bool Next(const std::string& key) override { return Next() == key; }
Prev(const std::string & key)374         bool Prev(const std::string& key) override { return Prev() == key; }
Mode(const std::string & key)375         bool Mode(const std::string& key) override { return Mode() == key; }
376 
Up()377         std::string Up() override { return Hotkeys::Get(Hotkeys::Up); }
Down()378         std::string Down() override { return Hotkeys::Get(Hotkeys::Down); }
Left()379         std::string Left() override { return Hotkeys::Get(Hotkeys::Left); }
Right()380         std::string Right() override { return Hotkeys::Get(Hotkeys::Right); }
PageUp()381         std::string PageUp() override { return Hotkeys::Get(Hotkeys::PageUp); }
PageDown()382         std::string PageDown() override { return Hotkeys::Get(Hotkeys::PageDown); }
Home()383         std::string Home() override { return Hotkeys::Get(Hotkeys::Home); }
End()384         std::string End() override { return Hotkeys::Get(Hotkeys::End); }
Next()385         std::string Next() override { return "KEY_TAB"; }
Prev()386         std::string Prev() override { return "KEY_BTAB"; }
Mode()387         std::string Mode() override { return "^["; }
388 };
389 
NavigationKeys()390 std::shared_ptr<cursespp::INavigationKeys> Hotkeys::NavigationKeys() {
391     return std::make_shared<NavigationKeysImpl>();
392 }
393