From 94bd430b183758b7713c66e3e3f65c6c03b4ffdd Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Mon, 3 Mar 2025 12:01:32 +0100 Subject: [PATCH] filebrowser/tagtree/playlistsviewer: Add multiselection operations This new feature allows to do mass operations to provide much more flexibility making selected mixes and reorganizing/reordering the playlist directly on your Rockbox device. There's also new related hotkeys to make usability even better. Compatible actions : Playlists viewer -> mass move, delete, add next / add to playlist Tagtree/filebrowser -> mass add next / add to playlist Notes: - Voicing may sound strange/partially lacking, because untested. - Touchscreens have additional codes about list rendering. There's a function to update. I could not do it yet as I don't have the hardware to do tests. I added a FIXME accordingly. Change-Id: Ia57b59fb5646ba3ec47aa16c8db802f3946a0008 # Conflicts: # apps/gui/list.h # apps/menu.c # apps/playlist.c # apps/playlist.h # apps/plugin.h # apps/plugins/random_folder_advance_config.c # Conflicts: # apps/gui/bitmap/list-skinned.c --- apps/bookmark.c | 2 +- apps/cuesheet.c | 2 +- apps/filetypes.h | 1 + apps/gui/bitmap/list-skinned.c | 9 +- apps/gui/bitmap/list.c | 19 +- apps/gui/list.c | 90 +++-- apps/gui/list.h | 7 +- apps/gui/option_select.c | 2 +- apps/gui/wps.c | 4 +- apps/lang/english.lang | 56 +++ apps/lang/francais.lang | 56 +++ apps/menu.c | 2 +- apps/menus/recording_menu.c | 2 +- apps/onplay.c | 129 ++++++- apps/onplay.h | 21 +- apps/playlist.c | 24 +- apps/playlist.h | 2 +- apps/playlist_catalog.c | 87 ++++- apps/playlist_catalog.h | 9 +- apps/playlist_viewer.c | 398 ++++++++++++++++---- apps/plugin.c | 1 + apps/plugin.h | 6 +- apps/plugins/calendar.c | 2 +- apps/plugins/chessbox/chessbox_pgn.c | 2 +- apps/plugins/keybox.c | 2 +- apps/plugins/keyremap.c | 2 +- apps/plugins/lastfm_scrobbler.c | 2 +- apps/plugins/lastfm_scrobbler_viewer.c | 2 +- apps/plugins/lrcplayer.c | 2 +- apps/plugins/main_menu_config.c | 2 +- apps/plugins/open_plugins.c | 2 +- apps/plugins/playing_time.c | 4 +- apps/plugins/properties.c | 2 +- apps/plugins/puzzles/rockbox.c | 6 +- apps/plugins/random_folder_advance_config.c | 2 +- apps/plugins/rb_info.c | 2 +- apps/plugins/shopper.c | 2 +- apps/plugins/shortcuts/shortcuts_view.c | 2 +- apps/plugins/tagcache/tagcache.c | 2 +- apps/plugins/text_editor.c | 2 +- apps/plugins/zxbox/tapefile.c | 62 +-- apps/radio/presets.c | 2 +- apps/recorder/recording.c | 2 +- apps/screens.c | 2 +- apps/settings_list.c | 10 +- apps/tagtree.c | 71 +++- apps/tagtree.h | 2 + apps/tree.c | 256 ++++++++++++- apps/tree.h | 4 + manual/working_with_playlists/main.tex | 14 + 50 files changed, 1138 insertions(+), 258 deletions(-) diff --git a/apps/bookmark.c b/apps/bookmark.c index f00ace7c41..f4bbd1c7df 100644 --- a/apps/bookmark.c +++ b/apps/bookmark.c @@ -988,7 +988,7 @@ static int select_bookmark(const char* bookmark_file_name, = (strcmp(bookmark_file_name, RECENT_BOOKMARK_FILE) == 0); gui_synclist_init(&list, &get_bookmark_info, - (void*) bookmarks, false, 2, NULL); + (void*) bookmarks, false, 2, NULL, false); if(global_settings.talk_menu) gui_synclist_set_voice_callback(&list, bookmark_list_voice_cb); diff --git a/apps/cuesheet.c b/apps/cuesheet.c index c5a1aacad9..48c340739a 100644 --- a/apps/cuesheet.c +++ b/apps/cuesheet.c @@ -464,7 +464,7 @@ void browse_cuesheet(struct cuesheet *cue) title[sizeof(title) - 2] = '~'; /* give indication of truncation */ - gui_synclist_init(&lists, list_get_name_cb, cue, false, 2, NULL); + gui_synclist_init(&lists, list_get_name_cb, cue, false, 2, NULL, false); gui_synclist_set_nb_items(&lists, 2*cue->track_count); gui_synclist_set_title(&lists, title, 0); diff --git a/apps/filetypes.h b/apps/filetypes.h index a20a2f82ee..fd477544c4 100644 --- a/apps/filetypes.h +++ b/apps/filetypes.h @@ -50,6 +50,7 @@ #define FILE_ATTR_OPX 0x1400 /* open plugins shortcut */ #define FILE_ATTR_LOG 0x1500 /* log file */ #define FILE_ATTR_MASK 0xFF00 /* which bits tree.c uses for file types */ +#define FILE_ATTR_MULTISELECTION 0x40000000 /* is file a part of a multiselection ? */ long tree_get_filetype_voiceclip(int attr); diff --git a/apps/gui/bitmap/list-skinned.c b/apps/gui/bitmap/list-skinned.c index 454010275d..9a13dd2c64 100644 --- a/apps/gui/bitmap/list-skinned.c +++ b/apps/gui/bitmap/list-skinned.c @@ -67,7 +67,7 @@ static bool skinlist_is_configured(enum screen_type screen, struct gui_synclist *list) { return (listcfg[screen] != NULL) && - (!list || (list && list->selected_size == 1)); + (!list || (list && (list->selected_size == 1 || list->multiple_selection_force_single_entry_scroll))); } static int current_drawing_line; static int offset_to_item(int offset, bool wrap) @@ -214,7 +214,8 @@ bool skinlist_draw(struct screen *display, struct gui_synclist *list) display->set_background(parent->bg_pattern); #endif display->clear_viewport(); - current_item = list->selected_item; + int selected_item = list->selected_item; + current_item = selected_item; current_nbitems = list->nb_items; needs_scrollbar[screen] = list->nb_items > display_lines; @@ -225,7 +226,7 @@ bool skinlist_draw(struct screen *display, struct gui_synclist *list) if (list_start_item+cur_line+1 > list->nb_items) break; current_drawing_line = list_start_item+cur_line; - is_selected = list_start_item+cur_line == list->selected_item; + is_selected = list_start_item+cur_line == selected_item; for (viewport = SKINOFFSETTOPTR(get_skin_buffer(wps.data), listcfg[screen]->data->tree); viewport; @@ -315,6 +316,6 @@ bool skinlist_draw(struct screen *display, struct gui_synclist *list) } else display->update_viewport(); - current_drawing_line = list->selected_item; + current_drawing_line = selected_item; return true; } diff --git a/apps/gui/bitmap/list.c b/apps/gui/bitmap/list.c index 17db15b8bd..4a47cf5f3d 100644 --- a/apps/gui/bitmap/list.c +++ b/apps/gui/bitmap/list.c @@ -248,7 +248,16 @@ void list_draw(struct screen *display, struct gui_synclist *list) const int nb_lines = list_get_nb_lines(list, screen); linedes.height = list->line_height[screen]; - linedes.nlines = list->selected_size; + int selected_item = list->selected_item; + int selected_size = list->selected_size; + if (selected_size <= 0) + { + selected_item += selected_size; + while (selected_item < 0) + selected_item += list->nb_items; + selected_size = ((- selected_size) + 1); + } + linedes.nlines = selected_size; #if LCD_DEPTH > 1 /* XXX: Do we want to support the separator on remote displays? */ if (display->screen_type == SCREEN_MAIN) @@ -388,8 +397,8 @@ void list_draw(struct screen *display, struct gui_synclist *list) /* don't draw it during scrolling */ list->scroll_mode == SCROLL_NONE && #endif - i >= list->selected_item - && i < list->selected_item + list->selected_size) + ((i >= selected_item && i < selected_item + selected_size) || + (i < (selected_item + selected_size - list->nb_items)))) {/* The selected item must be displayed scrolling */ #ifdef HAVE_LCD_COLOR if (list->selection_color) @@ -451,7 +460,7 @@ void list_draw(struct screen *display, struct gui_synclist *list) #endif linedes.style = style; linedes.scroll = is_selected ? true : list->scroll_all; - linedes.line = i % list->selected_size; + linedes.line = i % selected_size; icon = list->callback_get_item_icon ? list->callback_get_item_icon(i, list->data) : Icon_NOICON; @@ -484,6 +493,8 @@ void list_draw(struct screen *display, struct gui_synclist *list) #if defined(HAVE_TOUCHSCREEN) /* This needs to be fixed if we ever get more than 1 touchscreen on a target. */ +/* FIXME: Multiple selection (with multiple_selection_force_single_entry_scroll) + is not coded (yet) for touchscreen devices */ static void do_touch_scroll(struct gui_synclist *gui_list, int new_y_pos) { diff --git a/apps/gui/list.c b/apps/gui/list.c index 25ef70b76f..710639ddff 100644 --- a/apps/gui/list.c +++ b/apps/gui/list.c @@ -160,15 +160,19 @@ void gui_synclist_init(struct gui_synclist * gui_list, list_get_name callback_get_item_name, void * data, bool scroll_all, - int selected_size, struct viewport list_parent[NB_SCREENS] + int selected_size, struct viewport list_parent[NB_SCREENS], + bool multiple_selection_force_single_entry_scroll ) { gui_list->callback_get_item_icon = NULL; gui_list->callback_get_item_name = callback_get_item_name; gui_list->callback_speak_item = NULL; gui_list->callback_draw_item = NULL; + gui_list->multiple_selection_force_single_entry_scroll = multiple_selection_force_single_entry_scroll; gui_list->nb_items = 0; gui_list->selected_item = 0; + gui_list->min_index_forbid_limit = 0; + gui_list->end_index_forbid_limit = 0; gui_synclist_init_display_settings(gui_list); #ifdef HAVE_TOUCHSCREEN gui_list->y_pos = 0; @@ -256,30 +260,34 @@ static void gui_list_put_selection_on_screen(struct gui_synclist * gui_list, int nb_lines = list_get_nb_lines(gui_list, screen); int bottom = MAX(0, gui_list->nb_items - nb_lines); int new_start_item = gui_list->start_item[screen]; - int difference = gui_list->selected_item - gui_list->start_item[screen]; - const int scroll_limit_up = (nb_lines < gui_list->selected_size+2 ? 0:1); - const int scroll_limit_down = (scroll_limit_up+gui_list->selected_size); - - if (gui_list->selected_size >= nb_lines) + int selected_item = gui_list->selected_item; + int selected_size = gui_list->selected_size; + if (gui_list->multiple_selection_force_single_entry_scroll) + selected_size = 1; + int difference = selected_item - gui_list->start_item[screen]; + const int scroll_limit_up = (nb_lines < selected_size + 2 ? 0:1); + const int scroll_limit_down = (scroll_limit_up+selected_size); + + if (selected_size >= nb_lines) { - new_start_item = gui_list->selected_item; + new_start_item = selected_item; } else if (gui_list->scroll_paginated) { - nb_lines -= nb_lines%gui_list->selected_size; + nb_lines -= nb_lines%selected_size; if (difference < 0 || difference >= nb_lines) { - new_start_item = gui_list->selected_item - - (gui_list->selected_item%nb_lines); + new_start_item = selected_item - + (selected_item%nb_lines); } } else if (difference <= scroll_limit_up) /* list moved up */ { - new_start_item = gui_list->selected_item - scroll_limit_up; + new_start_item = selected_item - scroll_limit_up; } else if (difference > nb_lines - scroll_limit_down) /* list moved down */ { - new_start_item = gui_list->selected_item + scroll_limit_down - nb_lines; + new_start_item = selected_item + scroll_limit_down - nb_lines; } if (new_start_item < 0) gui_list->start_item[screen] = 0; @@ -389,22 +397,31 @@ void gui_synclist_select_item(struct gui_synclist * gui_list, int item_number) static void gui_list_select_at_offset(struct gui_synclist * gui_list, int offset, bool allow_wrap) { - if (gui_list->selected_size > 1) + int selected_item = gui_list->selected_item; + int selected_size = gui_list->selected_size; + int max_index = gui_list->nb_items - gui_list->end_index_forbid_limit; + if (max_index < gui_list->min_index_forbid_limit) + max_index = gui_list->min_index_forbid_limit; + if (gui_list->multiple_selection_force_single_entry_scroll) + selected_size = 1; + else if (selected_size > 1) + offset *= selected_size; + + int new_selection = selected_item + offset; + int remain = (max_index - selected_size); + if (remain < gui_list->min_index_forbid_limit) + remain = gui_list->min_index_forbid_limit; + else if (remain > max_index) + remain = max_index; + + if (new_selection >= max_index) { - offset *= gui_list->selected_size; - } - - int new_selection = gui_list->selected_item + offset; - int remain = (gui_list->nb_items - gui_list->selected_size); - - if (new_selection >= gui_list->nb_items) - { - new_selection = allow_wrap ? 0 : remain; + new_selection = (allow_wrap ? gui_list->min_index_forbid_limit : remain); edge_beep(gui_list, allow_wrap); } - else if (new_selection < 0) + else if (new_selection < gui_list->min_index_forbid_limit) { - new_selection = allow_wrap ? remain : 0; + new_selection = (allow_wrap ? remain : gui_list->min_index_forbid_limit); edge_beep(gui_list, allow_wrap); } @@ -517,7 +534,7 @@ static void gui_synclist_select_next_page(struct gui_synclist * lists, bool allow_wrap) { int nb_lines = list_get_nb_lines(lists, screen); - if (lists->selected_size > 1) + if (lists->selected_size > 1 && !lists->multiple_selection_force_single_entry_scroll) nb_lines = MAX(1, nb_lines/lists->selected_size); gui_list_select_at_offset(lists, nb_lines, allow_wrap); @@ -528,7 +545,7 @@ static void gui_synclist_select_previous_page(struct gui_synclist * lists, bool allow_wrap) { int nb_lines = list_get_nb_lines(lists, screen); - if (lists->selected_size > 1) + if (lists->selected_size > 1 && !lists->multiple_selection_force_single_entry_scroll) nb_lines = MAX(1, nb_lines/lists->selected_size); gui_list_select_at_offset(lists, -nb_lines, allow_wrap); @@ -578,8 +595,11 @@ bool gui_synclist_keyclick_callback(int action, void* data) if (action == ACTION_STD_PREV && !lists->wraparound) return false; } + int selected_size = lists->selected_size; + if (lists->multiple_selection_force_single_entry_scroll) + selected_size = 1; - if (lists->selected_item == lists->nb_items - lists->selected_size) + if (lists->selected_item == lists->nb_items - selected_size) { if (action == ACTION_STD_NEXTREPEAT) return false; @@ -709,6 +729,7 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr) #ifndef HAVE_WHEEL_ACCELERATION if (button_queue_count() < FRAMEDROP_TRIGGER) #endif + if (!lists->multiple_selection_force_single_entry_scroll) gui_synclist_draw(lists); yield(); *actionptr = ACTION_STD_PREV; @@ -722,6 +743,7 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr) #ifndef HAVE_WHEEL_ACCELERATION if (button_queue_count() < FRAMEDROP_TRIGGER) #endif + if (!lists->multiple_selection_force_single_entry_scroll) gui_synclist_draw(lists); yield(); *actionptr = ACTION_STD_NEXT; @@ -729,7 +751,8 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr) case ACTION_TREE_PGRIGHT: gui_synclist_scroll_right(lists); - gui_synclist_draw(lists); + if (!lists->multiple_selection_force_single_entry_scroll) + gui_synclist_draw(lists); yield(); return true; case ACTION_TREE_ROOT_INIT: @@ -755,7 +778,8 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr) return false; } gui_synclist_scroll_left(lists); - gui_synclist_draw(lists); + if (!lists->multiple_selection_force_single_entry_scroll) + gui_synclist_draw(lists); pgleft_allow_cancel = false; /* stop ACTION_TREE_PAGE_LEFT skipping to root */ yield(); @@ -772,7 +796,8 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr) #endif SCREEN_MAIN; gui_synclist_select_previous_page(lists, screen, false); - gui_synclist_draw(lists); + if (!lists->multiple_selection_force_single_entry_scroll) + gui_synclist_draw(lists); yield(); *actionptr = ACTION_STD_NEXT; } @@ -787,7 +812,8 @@ bool gui_synclist_do_button(struct gui_synclist * lists, int *actionptr) #endif SCREEN_MAIN; gui_synclist_select_next_page(lists, screen, false); - gui_synclist_draw(lists); + if (!lists->multiple_selection_force_single_entry_scroll) + gui_synclist_draw(lists); yield(); *actionptr = ACTION_STD_PREV; } @@ -938,7 +964,7 @@ bool simplelist_show_list(struct simplelist_info *info) } gui_synclist_init(&lists, getname, info->callback_data, - info->scroll_all, info->selection_size, NULL); + info->scroll_all, info->selection_size, NULL, false); if (info->title) gui_synclist_set_title(&lists, info->title, info->title_icon); diff --git a/apps/gui/list.h b/apps/gui/list.h index 814a383e9a..fb0968cdad 100644 --- a/apps/gui/list.h +++ b/apps/gui/list.h @@ -155,10 +155,12 @@ struct gui_synclist bool scroll_all; int nb_items; int selected_item; - + int min_index_forbid_limit; /* don't let the user scroll under it */ + int end_index_forbid_limit; /* don't let the user scroll past (number_items-this var) */ int start_item[NB_SCREENS]; /* the item that is displayed at the top of the screen */ /* the number of lines that are selected at the same time */ int selected_size; + bool multiple_selection_force_single_entry_scroll; /* the number of pixels each line occupies (including optional padding on touchscreen */ int line_height[NB_SCREENS]; int offset_position[NB_SCREENS]; /* the list's screen scroll placement in pixels */ @@ -199,7 +201,8 @@ extern void gui_synclist_init( void * data, bool scroll_all, int selected_size, - struct viewport parent[NB_SCREENS] /* NOTE: new screens should NOT set this to NULL */ + struct viewport parent[NB_SCREENS], /* NOTE: new screens should NOT set this to NULL */ + bool multiple_selection_force_single_entry_scroll ); extern void gui_synclist_set_nb_items(struct gui_synclist * lists, int nb_items); extern void gui_synclist_set_icon_callback(struct gui_synclist * lists, list_get_icon icon_callback); diff --git a/apps/gui/option_select.c b/apps/gui/option_select.c index c7eab6aba1..236c0b8fc4 100644 --- a/apps/gui/option_select.c +++ b/apps/gui/option_select.c @@ -506,7 +506,7 @@ bool option_screen(const struct settings_list *setting, else return false; /* only int/bools can go here */ push_current_activity(ACTIVITY_OPTIONSELECT); gui_synclist_init(&lists, value_setting_get_name_cb, - (void*)setting, false, 1, parent); + (void*)setting, false, 1, parent, false); if (setting->lang_id == -1) { title = setting_get_cfgvals(setting); diff --git a/apps/gui/wps.c b/apps/gui/wps.c index 7bbeba0049..89d08a0a8a 100644 --- a/apps/gui/wps.c +++ b/apps/gui/wps.c @@ -803,7 +803,7 @@ long gui_wps_show(void) theme_enabled = false; gwps_leave_wps(theme_enabled); onplay(state->id3->path, - FILE_ATTR_AUDIO, CONTEXT_WPS, hotkey, ONPLAY_NO_CUSTOMACTION); + FILE_ATTR_AUDIO, CONTEXT_WPS, hotkey, ONPLAY_NO_CUSTOMACTION, false); if (!audio_status()) { /* re-enable theme since we're returning to SBS */ @@ -820,7 +820,7 @@ long gui_wps_show(void) { gwps_leave_wps(true); int retval = onplay(state->id3->path, - FILE_ATTR_AUDIO, CONTEXT_WPS, hotkey, ONPLAY_NO_CUSTOMACTION); + FILE_ATTR_AUDIO, CONTEXT_WPS, hotkey, ONPLAY_NO_CUSTOMACTION, false); /* if music is stopped in the context menu we want to exit the wps */ if (retval == ONPLAY_MAINMENU || !audio_status()) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 56044bfcbf..d5263480e3 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -5985,6 +5985,62 @@ *: "Disk size" + + id: LANG_MULTISELECT_ENABLE + desc: enable multiselection (in lists) + user: core + + *: "Enable Multiselection" + + + *: "Enable Multiselection" + + + *: "Enable Multiselection" + + + + id: LANG_MULTISELECT_DISABLE + desc: disable multiselection (in lists) + user: core + + *: "Disable Multiselection" + + + *: "Disable Multiselection" + + + *: "Disable Multiselection" + + + + id: LANG_MULTISELECT_TOGGLE + desc: enable or disable multiselection in lists (toggle enable/disable) + user: core + + *: "Toggle Multiselection" + + + *: "Toggle Multiselection" + + + *: "Toggle Multiselection" + + + + id: LANG_ALL_FOLDERS_FROM_CURRENT_SELECTION + desc: ask yes/no popup + user: core + + *: "All folders from current selection" + + + *: "All folders from current selection" + + + *: "All folders from current selection" + + id: LANG_DISK_FREE_INFO desc: disk size info diff --git a/apps/lang/francais.lang b/apps/lang/francais.lang index 316c3eef6c..60761784f2 100644 --- a/apps/lang/francais.lang +++ b/apps/lang/francais.lang @@ -5930,6 +5930,62 @@ *: "Disque" + + id: LANG_MULTISELECT_ENABLE + desc: enable multiselection (in lists) + user: core + + *: "Enable Multiselection" + + + *: "Activer la Multisélection" + + + *: "Activer la Multisélection" + + + + id: LANG_MULTISELECT_DISABLE + desc: disable multiselection (in lists) + user: core + + *: "Disable Multiselection" + + + *: "Désactiver la Multisélection" + + + *: "Désactiver la Multisélection" + + + + id: LANG_MULTISELECT_TOGGLE + desc: enable or disable multiselection in lists (toggle enable/disable) + user: core + + *: "Toggle Multiselection" + + + *: "Activer/Désactiver la Multisélection" + + + *: "Activer ou Désactiver la Multisélection" + + + + id: LANG_ALL_FOLDERS_FROM_CURRENT_SELECTION + desc: ask yes/no popup + user: core + + *: "All folders from current selection" + + + *: "Tous les dossiers de la sélection courante" + + + *: "Tous les dossiers de la sélection courante" + + id: LANG_DISK_FREE_INFO desc: disk size info diff --git a/apps/menu.c b/apps/menu.c index ce96c64d48..93c2e58a59 100644 --- a/apps/menu.c +++ b/apps/menu.c @@ -265,7 +265,7 @@ static int init_menu_lists(const struct menu_item_ex *menu, current_submenus_menu = (struct menu_item_ex *)menu; - gui_synclist_init(lists,get_menu_item_name,(void*)menu,false,1, parent); + gui_synclist_init(lists,get_menu_item_name,(void*)menu,false,1, parent, false); title = init_title(menu, &icon, buf, buf_sz); gui_synclist_set_title(lists, title, icon); gui_synclist_set_icon_callback(lists, global_settings.show_icons?menu_get_icon:NULL); diff --git a/apps/menus/recording_menu.c b/apps/menus/recording_menu.c index 9ef0eb6bac..dfc5e1b177 100644 --- a/apps/menus/recording_menu.c +++ b/apps/menus/recording_menu.c @@ -489,7 +489,7 @@ int rectrigger(void) settings[STOP_THRESHOLD] = find_setting(&global_settings.rec_stop_thres_linear); } - gui_synclist_init(&lists, trigger_get_name, settings, false, 2, vp); + gui_synclist_init(&lists, trigger_get_name, settings, false, 2, vp, false); gui_synclist_set_nb_items(&lists, TRIG_OPTION_COUNT*2); gui_synclist_set_icon_callback(&lists, global_settings.show_icons?trigger_get_icon:NULL); /* restart trigger with new values */ diff --git a/apps/onplay.c b/apps/onplay.c index 0c176d9038..96794a49ec 100644 --- a/apps/onplay.c +++ b/apps/onplay.c @@ -237,16 +237,6 @@ MAKE_ONPLAYMENU( wps_playlist_menu, ID2P(LANG_CURRENT_PLAYLIST), &search_playlist_item, &reshuffle_item, &playing_time_item ); -/* argument for add_to_playlist (for use by menu callbacks) */ -#define PL_NONE 0x00 -#define PL_QUEUE 0x01 -#define PL_REPLACE 0x02 -struct add_to_pl_param -{ - int8_t position; - uint8_t flags; -}; - static struct add_to_pl_param addtopl_insert = {PLAYLIST_INSERT, PL_NONE}; static struct add_to_pl_param addtopl_insert_first = {PLAYLIST_INSERT_FIRST, PL_NONE}; static struct add_to_pl_param addtopl_insert_last = {PLAYLIST_INSERT_LAST, PL_NONE}; @@ -262,6 +252,13 @@ static struct add_to_pl_param addtopl_queue_last_shuf = {PLAYLIST_INSERT_LAST_S static struct add_to_pl_param addtopl_replace = {PLAYLIST_INSERT, PL_REPLACE}; static struct add_to_pl_param addtopl_replace_shuffled = {PLAYLIST_INSERT_LAST_SHUFFLED, PL_REPLACE}; +static bool addto_pl_return_only_choice = false; +static struct add_to_pl_param *addto_pl_current_choice = NULL; + +static bool show_playlist_cat_menu_return_only_choice = false; +static char *show_playlist_cat_menu_current_choice = NULL; +static bool show_playlist_cat_menu_current_choice_is_new_playlist = false; + static void op_playlist_insert_selected(int position, bool queue) { #ifdef HAVE_TAGCACHE @@ -311,6 +308,12 @@ static int add_to_playlist(void* arg) /* warn if replacing the playlist */ if (new_playlist && !warn_on_pl_erase()) return 1; + + if (addto_pl_return_only_choice) + { + addto_pl_current_choice = param; + return 1; + } splash(0, ID2P(LANG_WAIT)); @@ -519,16 +522,50 @@ void onplay_show_playlist_menu(const char* path, int attr, void (*playlist_inser do_menu(&tree_playlist_menu, NULL, NULL, false); } +struct add_to_pl_param* onplay_show_playlist_menu_get_choice(void) +{ + addto_pl_current_choice = NULL; + addto_pl_return_only_choice = true; + in_queue_submenu = false; + do_menu(&tree_playlist_menu, NULL, NULL, false); + addto_pl_return_only_choice = false; + return addto_pl_current_choice; +} + +struct add_to_pl_param* onplay_show_playlist_menu_get_current_choice(void) +{ + return addto_pl_current_choice; +} + /* playlist catalog options */ static bool cat_add_to_a_playlist(void) { - return catalog_add_to_a_playlist(selected_file.path, selected_file.attr, + if (show_playlist_cat_menu_return_only_choice) + { + show_playlist_cat_menu_current_choice = catalog_add_to_a_playlist_get_choice(selected_file.path, selected_file.attr, false, NULL); + return true; + } + else + return catalog_add_to_a_playlist(selected_file.path, selected_file.attr, false, NULL, ctx_add_to_playlist); } +char* onplay_show_playlist_cat_menu_get_current_choice(void) +{ + return show_playlist_cat_menu_current_choice; +} + static bool cat_add_to_a_new_playlist(void) { - return catalog_add_to_a_playlist(selected_file.path, selected_file.attr, + if (show_playlist_cat_menu_return_only_choice) + { + show_playlist_cat_menu_current_choice = catalog_add_to_a_playlist_get_choice(selected_file.path, selected_file.attr, + true, NULL); + show_playlist_cat_menu_current_choice_is_new_playlist = true; + return true; + } + else + return catalog_add_to_a_playlist(selected_file.path, selected_file.attr, true, NULL, ctx_add_to_playlist); } @@ -544,6 +581,21 @@ MAKE_ONPLAYMENU(cat_playlist_menu, ID2P(LANG_ADD_TO_PL), cat_playlist_callback, Icon_Playlist, &cat_add_to_list, &cat_add_to_new); +bool onplay_show_playlist_cat_menu_current_choice_is_new_playlist(void) +{ + return show_playlist_cat_menu_current_choice_is_new_playlist; +} + +char* onplay_show_playlist_cat_menu_get_choice(void) +{ + show_playlist_cat_menu_current_choice = NULL; + show_playlist_cat_menu_current_choice_is_new_playlist = false; + show_playlist_cat_menu_return_only_choice = true; + do_menu(&cat_playlist_menu, NULL, NULL, false); + show_playlist_cat_menu_return_only_choice = false; + return show_playlist_cat_menu_current_choice; +} + void onplay_show_playlist_cat_menu(const char* track_name, int attr, void (*add_to_pl_cb)) { ctx_add_to_playlist = add_to_pl_cb; @@ -837,6 +889,12 @@ static bool list_viewers(void) return false; } +static bool set_multiselect(void) +{ + onplay_result = ONPLAY_MULTISELECT; + return true; +} + #ifdef HAVE_TAGCACHE static bool prepare_database_sel(void *param) { @@ -905,6 +963,10 @@ MENUITEM_FUNCTION_W_PARAM(properties_item, 0, ID2P(LANG_PROPERTIES), MENUITEM_FUNCTION_W_PARAM(track_info_item, 0, ID2P(LANG_MENU_SHOW_ID3_INFO), onplay_load_plugin, (void *)"properties", clipboard_callback, Icon_NOICON); +MENUITEM_FUNCTION(multiselect_item_enable, 0, ID2P(LANG_MULTISELECT_ENABLE), + set_multiselect, clipboard_callback, Icon_NOICON); +MENUITEM_FUNCTION(multiselect_item_disable, 0, ID2P(LANG_MULTISELECT_DISABLE), + set_multiselect, clipboard_callback, Icon_NOICON); #ifdef HAVE_TAGCACHE MENUITEM_FUNCTION_W_PARAM(pictureflow_item, 0, ID2P(LANG_ONPLAY_PICTUREFLOW), onplay_load_plugin, (void *)"pictureflow", @@ -1017,9 +1079,17 @@ static int clipboard_callback(int action, struct gui_synclist *this_list) { (void)this_list; + bool multiselection_enabled = tree_get_context()->multiselection_enabled; switch (action) { case ACTION_REQUEST_MENUITEM: + if (this_item == &multiselect_item_disable) + return (multiselection_enabled ? action : ACTION_EXIT_MENUITEM); + if (this_item == &multiselect_item_enable) + return (!multiselection_enabled ? action : ACTION_EXIT_MENUITEM); + if (multiselection_enabled && + this_item != &tree_playlist_menu && this_item != &cat_playlist_menu) + return ACTION_EXIT_MENUITEM; #ifdef HAVE_MULTIVOLUME /* no rename+delete for volumes */ if ((selected_file.attr & ATTR_VOLUME) && @@ -1162,6 +1232,8 @@ MAKE_ONPLAYMENU( tree_onplay_menu, ID2P(LANG_ONPLAY_MENU_TITLE), &rename_file_item, &clipboard_cut_item, &clipboard_copy_item, &clipboard_paste_item, &delete_file_item, &delete_dir_item, &list_viewers_item, &create_dir_item, &properties_item, &track_info_item, + &multiselect_item_enable, + &multiselect_item_disable, #ifdef HAVE_TAGCACHE &pictureflow_item, #endif @@ -1189,7 +1261,7 @@ static int onplaymenu_callback(int action, } break; case ACTION_REQUEST_MENUITEM: - if (this_item == &view_playlist_item) + if (this_item == &view_playlist_item && !tree_get_context()->multiselection_enabled) { if ((selected_file.attr & FILE_ATTR_MASK) == FILE_ATTR_M3U && selected_file.context == CONTEXT_TREE) @@ -1335,6 +1407,11 @@ static const struct hotkey_assignment hotkey_items[] = { .func = HOTKEY_FUNC(bookmark_load_menu, NULL), .return_code = ONPLAY_START_PLAY, .flags = HOTKEY_FLAG_WPS }, + { .action = HOTKEY_MULTISELECTION, + .lang_id = LANG_MULTISELECT_TOGGLE, + .func = HOTKEY_FUNC(NULL, NULL), + .return_code = ONPLAY_MULTISELECT, + .flags = HOTKEY_FLAG_TREE }, { .action = HOTKEY_PROPERTIES, .lang_id = LANG_PROPERTIES, .func = HOTKEY_FUNC(hotkey_tree_run_plugin, (void *)"properties"), @@ -1387,11 +1464,17 @@ static int execute_hotkey(bool is_wps) } #endif /* HOTKEY */ -int onplay(char* file, int attr, int from_context, bool hotkey, int customaction) +int onplay(char* file, int attr, int from_context, bool hotkey, int customaction, bool getchoice) { + addto_pl_return_only_choice = getchoice; + show_playlist_cat_menu_return_only_choice = getchoice; + show_playlist_cat_menu_current_choice_is_new_playlist = false; + addto_pl_current_choice = NULL; + show_playlist_cat_menu_current_choice = NULL; const struct menu_item_ex *menu; onplay_result = ONPLAY_OK; ctx_current_playlist_insert = NULL; + selected_file_set(from_context, NULL, attr); #ifdef HAVE_TAGCACHE @@ -1401,13 +1484,13 @@ int onplay(char* file, int attr, int from_context, bool hotkey, int customaction if (file != NULL) { /* add a leading slash so that catalog_add_to_a_playlist - later prefills the name when creating a new playlist */ + later prefills the name when creating a new playlist */ snprintf(selected_file.buf, MAX_PATH, "/%s", file); selected_file.path = selected_file.buf; } } - else -#endif + else + #endif { ctx_add_to_playlist = NULL; if (file != NULL) @@ -1421,13 +1504,20 @@ int onplay(char* file, int attr, int from_context, bool hotkey, int customaction #ifdef HAVE_HOTKEY if (hotkey) - return execute_hotkey(from_context == CONTEXT_WPS); + { + int result = execute_hotkey(from_context == CONTEXT_WPS); + addto_pl_return_only_choice = false; + show_playlist_cat_menu_return_only_choice = false; + return result; + } #else (void)hotkey; #endif if (customaction == ONPLAY_CUSTOMACTION_SHUFFLE_SONGS) { int returnCode = add_to_playlist(&addtopl_replace_shuffled); + addto_pl_return_only_choice = false; + show_playlist_cat_menu_return_only_choice = false; if (returnCode == 1) // User did not want to erase his current playlist, so let's show again the database main menu return ONPLAY_RELOAD_DIR; @@ -1444,7 +1534,8 @@ int onplay(char* file, int attr, int from_context, bool hotkey, int customaction if (get_current_activity() == ACTIVITY_CONTEXTMENU) /* Activity may have been */ pop_current_activity(); /* popped already by menu item */ - + addto_pl_return_only_choice = false; + show_playlist_cat_menu_return_only_choice = false; if (menu_selection == GO_TO_WPS) return ONPLAY_START_PLAY; if (menu_selection == GO_TO_ROOT) diff --git a/apps/onplay.h b/apps/onplay.h index 26b1247f7b..d63bf1fe13 100644 --- a/apps/onplay.h +++ b/apps/onplay.h @@ -25,13 +25,24 @@ #include "menu.h" #endif +/* argument for add_to_playlist (for use by menu callbacks) */ +#define PL_NONE 0x00 +#define PL_QUEUE 0x01 +#define PL_REPLACE 0x02 + +struct add_to_pl_param +{ + int8_t position; + uint8_t flags; +}; + enum { ONPLAY_NO_CUSTOMACTION, ONPLAY_CUSTOMACTION_SHUFFLE_SONGS, ONPLAY_CUSTOMACTION_FIRSTLETTER, }; -int onplay(char* file, int attr, int from_context, bool hotkey, int customaction); +int onplay(char* file, int attr, int from_context, bool hotkey, int customaction, bool getchoice); int get_onplay_context(void); enum { @@ -41,6 +52,7 @@ enum { ONPLAY_START_PLAY, ONPLAY_PLAYLIST, ONPLAY_PLUGIN, + ONPLAY_MULTISELECT, #ifdef HAVE_HOTKEY ONPLAY_FUNC_RETURN, /* for use in hotkey_assignment only */ #endif @@ -62,6 +74,7 @@ enum hotkey_action { HOTKEY_INSERT, HOTKEY_INSERT_SHUFFLED, HOTKEY_BOOKMARK_LIST, + HOTKEY_MULTISELECTION, }; enum hotkey_flags { HOTKEY_FLAG_NONE = 0x0, @@ -84,6 +97,10 @@ const struct hotkey_assignment *get_hotkey(int action); /* needed for the playlist viewer.. eventually clean this up */ void onplay_show_playlist_cat_menu(const char* track_name, int attr, void (*add_to_pl_cb)); +bool onplay_show_playlist_cat_menu_current_choice_is_new_playlist(void); +char* onplay_show_playlist_cat_menu_get_choice(void); void onplay_show_playlist_menu(const char* path, int attr, void (*playlist_insert_cb)); - +struct add_to_pl_param* onplay_show_playlist_menu_get_choice(void); +struct add_to_pl_param* onplay_show_playlist_menu_get_current_choice(void); +char* onplay_show_playlist_cat_menu_get_current_choice(void); #endif diff --git a/apps/playlist.c b/apps/playlist.c index 1543fad262..4a27036000 100644 --- a/apps/playlist.c +++ b/apps/playlist.c @@ -2507,29 +2507,25 @@ void playlist_insert_context_release(struct playlist_insert_context *context) */ int playlist_insert_directory(struct playlist_info* playlist, const char *dirname, int position, bool queue, - bool recurse, struct playlist_insert_context* context) + bool recurse, struct playlist_insert_context* context_to_use) { - bool context_provided = context; - int result = 0; - struct playlist_insert_context c; - - if (!context_provided) - { - context = &c; - result = playlist_insert_context_create(playlist, &c, - position, queue, true); - } + int result = -1; + struct playlist_insert_context context; + if (context_to_use == NULL) + result = playlist_insert_context_create(playlist, &context, position, queue, true); + else + result = 0; if (result >= 0) { cpu_boost(true); result = playlist_directory_tracksearch(dirname, recurse, - directory_search_callback, context); + directory_search_callback, context_to_use == NULL ? &context : context_to_use); cpu_boost(false); } - if (!context_provided) - playlist_insert_context_release(&c); + if (context_to_use == NULL) + playlist_insert_context_release(&context); return result; } diff --git a/apps/playlist.h b/apps/playlist.h index 02e1262cd3..f145669573 100644 --- a/apps/playlist.h +++ b/apps/playlist.h @@ -157,7 +157,7 @@ int playlist_insert_context_add(struct playlist_insert_context *context, void playlist_insert_context_release(struct playlist_insert_context *context); int playlist_insert_directory(struct playlist_info* playlist, const char *dirname, int position, bool queue, - bool recurse, struct playlist_insert_context *context); + bool recurse, struct playlist_insert_context* context_to_use); int playlist_insert_playlist(struct playlist_info* playlist, const char *filename, int position, bool queue); bool playlist_entries_iterate(const char *filename, diff --git a/apps/playlist_catalog.c b/apps/playlist_catalog.c index 736e5595dc..64b799f02b 100644 --- a/apps/playlist_catalog.c +++ b/apps/playlist_catalog.c @@ -288,7 +288,7 @@ static int add_track_to_playlist(char* filename, void* context) /* Add "sel" file into specified "playlist". How to insert depends on type of file */ int catalog_insert_into(const char* playlist, bool new_playlist, - const char* sel, int sel_attr) + const char* sel, int sel_attr, bool* forced_recurse_dir) { int fd; int result = -1; @@ -349,21 +349,25 @@ int catalog_insert_into(const char* playlist, bool new_playlist, else if (sel_attr & ATTR_DIRECTORY) { /* search directory for tracks and append to playlist */ - bool recurse; - const char *lines[] = { - ID2P(LANG_RECURSE_DIRECTORY_QUESTION), sel}; - const struct text_message message={lines, 2}; struct add_track_context context; - - - if (sel[1] == '\0' && sel[0] == PATH_ROOTCHR) - recurse = true; - else if (global_settings.recursive_dir_insert != RECURSE_ASK) - recurse = (bool)global_settings.recursive_dir_insert; + bool recurse; + if (forced_recurse_dir != NULL) + recurse = *forced_recurse_dir; else { - /* Ask if user wants to recurse directory */ - recurse = (gui_syncyesno_run(&message, NULL, NULL)==YESNO_YES); + const char *lines[] = { + ID2P(LANG_RECURSE_DIRECTORY_QUESTION), sel}; + const struct text_message message={lines, 2}; + + if (sel[1] == '\0' && sel[0] == PATH_ROOTCHR) + recurse = true; + else if (global_settings.recursive_dir_insert != RECURSE_ASK) + recurse = (bool)global_settings.recursive_dir_insert; + else + { + /* Ask if user wants to recurse directory */ + recurse = (gui_syncyesno_run(&message, NULL, NULL)==YESNO_YES); + } } context.fd = fd; @@ -457,13 +461,14 @@ bool catalog_pick_new_playlist_name(char *pl_name, size_t buf_size, return do_save; } +static char playlist[MAX_PATH + 7]; /* room for /.m3u8\0*/ + static int (*ctx_add_to_playlist)(const char* playlist, bool new_playlist); bool catalog_add_to_a_playlist(const char* sel, int sel_attr, bool new_playlist, char *m3u8name, void (*add_to_pl_cb)) { int result; - char playlist[MAX_PATH + 7]; /* room for /.m3u8\0*/ size_t basename_start; if ((browser_status & CATBROWSE_PLAYLIST) == CATBROWSE_PLAYLIST) return false; @@ -515,7 +520,59 @@ bool catalog_add_to_a_playlist(const char* sel, int sel_attr, result = ctx_add_to_playlist(playlist, new_playlist); } else - result = catalog_insert_into(playlist, new_playlist, sel, sel_attr); + result = catalog_insert_into(playlist, new_playlist, sel, sel_attr, NULL); return (result == 0); } + +char* catalog_add_to_a_playlist_get_choice(const char* sel, int sel_attr, + bool new_playlist, char *m3u8name) +{ + size_t basename_start; + if ((browser_status & CATBROWSE_PLAYLIST) == CATBROWSE_PLAYLIST) + return NULL; + + if (initialize_catalog_buf(playlist, sizeof(playlist)) < 0) + return NULL; + + if (new_playlist) + { + if (m3u8name == NULL) + { + const char *name; + /* If sel is empty, root, or playlist directory we use 'all' */ + if (!sel || !strcmp(sel, "/") || !strcmp(sel, playlist)) + { + sel = "/"; + name = "/all"; + } + else /*If sel is a folder, we prefill the text field with its name*/ + name = strrchr(sel, '/'); + + if (name == NULL || ((sel_attr & ATTR_DIRECTORY) != ATTR_DIRECTORY) || + ((sel_attr & FILE_ATTR_MULTISELECTION) == FILE_ATTR_MULTISELECTION)) + create_numbered_filename(playlist, playlist, PLAYLIST_UNTITLED_PREFIX, + ".m3u8", 1 IF_CNFN_NUM_(, NULL)); + else + { + basename_start = strlen(playlist) + 1; + strlcat(playlist, name, sizeof(playlist)); + fix_path_part(playlist, basename_start, + sizeof(playlist) - 1 - basename_start) ; + apply_playlist_extension(playlist, sizeof(playlist)); + } + } + else + strmemccpy(playlist, m3u8name, sizeof(playlist)); + + if (!catalog_pick_new_playlist_name(playlist, sizeof(playlist), NULL)) + return NULL; + } + else + { + if (display_playlists(playlist, CATBROWSE_PLAYLIST) < 0) + return NULL; + } + + return playlist; +} \ No newline at end of file diff --git a/apps/playlist_catalog.h b/apps/playlist_catalog.h index add8d23293..49ead1f28b 100644 --- a/apps/playlist_catalog.h +++ b/apps/playlist_catalog.h @@ -36,9 +36,6 @@ bool catalog_view_playlists(void); bool catalog_pick_new_playlist_name(char *pl_name, size_t buf_size, const char* curr_pl_name); -int catalog_insert_into(const char* playlist, bool new_playlist, - const char* sel, int sel_attr); - /* * Add something to a playlist (new or select from list of playlists in * catalog). @@ -56,4 +53,10 @@ bool catalog_add_to_a_playlist(const char* sel, int sel_attr, bool new_playlist, char* m3u8name, void (*add_to_pl_cb)); +char* catalog_add_to_a_playlist_get_choice(const char* sel, int sel_attr, + bool new_playlist, char* m3u8name); + +int catalog_insert_into(const char* playlist, bool new_playlist, + const char* sel, int sel_attr, bool* forced_recurse_dir); + #endif diff --git a/apps/playlist_viewer.c b/apps/playlist_viewer.c index d2971981e3..0f10055fb7 100644 --- a/apps/playlist_viewer.c +++ b/apps/playlist_viewer.c @@ -120,10 +120,13 @@ struct playlist_viewer { int *initial_selection; /* The initially selected track */ int current_playing_track; /* Index of current playing track */ int selected_track; /* The selected track, relative (first is 0) */ + int selected_size; /* Multiselection. Offset is the selected track. */ + int min_index_forbid_limit; /* don't let the user scroll under it */ + int end_index_forbid_limit; /* don't let the user scroll past (number_items-this var) */ + bool multiselection_enabled; + int multiselection_offset_track; int moving_track; /* The track to move, relative (first is 0) or -1 if nothing is currently being moved */ - int moving_playlist_index; /* Playlist-relative index (as opposed to - viewer-relative index) of moving track */ struct playlist_buffer buffer; struct mp3entry *id3; bool allow_view_text_plugin; @@ -477,8 +480,11 @@ static bool playlist_viewer_init(struct playlist_viewer * viewer, } playlist_buffer_init(&viewer->buffer, buffer, buffer_size); + viewer->min_index_forbid_limit = 0; + viewer->end_index_forbid_limit = 0; + viewer->selected_size = 1; viewer->moving_track = -1; - viewer->moving_playlist_index = -1; + viewer->multiselection_enabled = false; viewer->initial_selection = most_recent_selection; if (!reload) @@ -673,21 +679,98 @@ static enum pv_context_result open_pictureflow(const struct playlist_entry *curr #endif #endif /*defined(HAVE_HOTKEY) || defined(HAVE_TAGCACHE)*/ -static enum pv_context_result delete_track(int current_track_index, - int index, bool current_was_playing) +static enum pv_context_result add_tracks_to_current_playlist(int index, struct add_to_pl_param *choice) { - playlist_delete(viewer.playlist, current_track_index); + bool modified = false; + struct playlist_entry *current_track; + if (choice != NULL) + { + int selected_size = viewer.selected_size; + if (selected_size < 0) + { + index -= (- selected_size); + selected_size = ((- selected_size) + 1); + } + struct playlist_insert_context pl_context; + bool queue = (choice->flags & PL_QUEUE) == PL_QUEUE; + bool new_playlist = (choice->flags & PL_REPLACE) == PL_REPLACE; + if (new_playlist && global_settings.keep_current_track_on_replace_playlist) + { + if (audio_status() & AUDIO_STATUS_PLAY) + { + modified = true; + playlist_remove_all_tracks(NULL); + new_playlist = false; + } + } + if (new_playlist) + { + modified = true; + playlist_create(NULL, NULL); + } + + if (playlist_insert_context_create(NULL, &pl_context, choice->position, queue, true) >= 0) + { + for (int i = 0; i < selected_size; i++) + { + current_track = playlist_buffer_get_track(&viewer.buffer, index + i); + if (playlist_insert_context_add(&pl_context, current_track->name) < 0) + { + break; + } + modified = true; + } + } + playlist_insert_context_release(&pl_context); + if (new_playlist && (playlist_amount() > 0)) + { + if (global_settings.playlist_shuffle) + playlist_shuffle(current_tick, -1); + playlist_start(0, 0, 0); + } + } + return modified ? PV_CONTEXT_MODIFIED : PV_CONTEXT_UNCHANGED; +} + +static enum pv_context_result delete_tracks(int index) +{ + if (viewer.selected_size < 0) + { + index -= (- viewer.selected_size); + viewer.selected_size = ((- viewer.selected_size) + 1); + } + bool current_was_playing = false; + int pl_track_info_index; + + for (int i = 0; i < viewer.selected_size; i++) + { + /* Playlist viewer orders songs based on display index. We need to + convert to real playlist index to access track */ + pl_track_info_index = (index + playlist_get_first_index(viewer.playlist)) + % viewer.num_tracks; + if (!current_was_playing) + current_was_playing = (audio_status() & AUDIO_STATUS_PLAY) && /* or paused */ + (pl_track_info_index == viewer.current_playing_track); + playlist_delete(viewer.playlist, pl_track_info_index); + if (i + 1 < viewer.selected_size) + { + if (!viewer.playlist) + playlist_get_resume_info(&viewer.current_playing_track); + else + viewer.current_playing_track = -1; + + viewer.num_tracks = playlist_amount_ex(viewer.playlist); + } + } if (current_was_playing) { if (playlist_amount_ex(viewer.playlist) <= 0) audio_stop(); else { - /* Start playing new track except if it's the lasttrack - track in the playlist and repeat mode is disabled */ - struct playlist_entry *current_track = - playlist_buffer_get_track(&viewer.buffer, index); - if (current_track->display_index != viewer.num_tracks || + /* Start playing new track except if it's the lasttrack + track in the playlist and repeat mode is disabled */ + if ((index + 1) != playlist_amount_ex(viewer.playlist) || global_settings.repeat_mode == REPEAT_ALL) { audio_play(0, 0); @@ -700,14 +783,11 @@ static enum pv_context_result delete_track(int current_track_index, static enum pv_context_result context_menu(int index) { - struct playlist_entry *current_track = playlist_buffer_get_track(&viewer.buffer, - index); - bool current_was_playing = (audio_status() & AUDIO_STATUS_PLAY) && /* or paused */ - (current_track->index == viewer.current_playing_track); + struct playlist_entry *current_track; MENUITEM_STRINGLIST(menu_items, ID2P(LANG_PLAYLIST), NULL, ID2P(LANG_PLAYING_NEXT), ID2P(LANG_ADD_TO_PL), - ID2P(LANG_REMOVE), ID2P(LANG_MOVE), + ID2P(LANG_MULTISELECT_ENABLE), ID2P(LANG_REMOVE), ID2P(LANG_MOVE), ID2P(LANG_MENU_SHOW_ID3_INFO), ID2P(LANG_SHUFFLE), ID2P(LANG_SAVE), ID2P(LANG_PLAYLISTVIEWER_SETTINGS) @@ -715,35 +795,95 @@ static enum pv_context_result context_menu(int index) ,ID2P(LANG_ONPLAY_PICTUREFLOW) #endif ); - int sel = do_menu(&menu_items, NULL, NULL, false); + + MENUITEM_STRINGLIST(menu_items_multiselection_enabled, ID2P(LANG_PLAYLIST), NULL, + ID2P(LANG_PLAYING_NEXT), ID2P(LANG_ADD_TO_PL), + ID2P(LANG_MULTISELECT_DISABLE), ID2P(LANG_REMOVE), ID2P(LANG_MOVE) + ); + int sel; + if (viewer.multiselection_enabled) + sel = do_menu(&menu_items_multiselection_enabled, NULL, NULL, false); + else + sel = do_menu(&menu_items, NULL, NULL, false); if (sel == MENU_ATTACHED_USB) return PV_CONTEXT_USB; else if (sel >= 0) { /* Abort current move */ viewer.moving_track = -1; - viewer.moving_playlist_index = -1; switch (sel) { case 0: + { /* Playing Next... menu */ - onplay_show_playlist_menu(current_track->name, FILE_ATTR_AUDIO, NULL); - return PV_CONTEXT_UNCHANGED; + struct add_to_pl_param *choice = onplay_show_playlist_menu_get_choice(); + return add_tracks_to_current_playlist(index, choice); + } case 1: + { /* Add to Playlist... menu */ - onplay_show_playlist_cat_menu(current_track->name, FILE_ATTR_AUDIO, NULL); + char* choice = onplay_show_playlist_cat_menu_get_choice(); + if (viewer.selected_size < 0) + { + index -= (- viewer.selected_size); + viewer.selected_size = ((- viewer.selected_size) + 1); + } + if (choice != NULL) + { + bool new_playlist = onplay_show_playlist_cat_menu_current_choice_is_new_playlist(); + for (int i = 0; i < viewer.selected_size; i++) + { + current_track = playlist_buffer_get_track(&viewer.buffer, index + i); + if (catalog_insert_into(choice, new_playlist, current_track->name, FILE_ATTR_AUDIO, NULL) != 0) + { + splash(HZ*2, ID2P(LANG_PLAYLIST_ACCESS_ERROR)); + break; + } + new_playlist = false; + } + viewer.selected_size = 1; + viewer.multiselection_enabled = false; + } return PV_CONTEXT_UNCHANGED; + } case 2: - return delete_track(current_track->index, index, current_was_playing); + /* Make a multiselection */ + if (!viewer.multiselection_enabled) + { + viewer.multiselection_enabled = true; + viewer.multiselection_offset_track = index; + return PV_CONTEXT_UNCHANGED; + } + else + { + /* Will be disabled later bacause of PV_CONTEXT_MODIFIED */ + return PV_CONTEXT_MODIFIED; + } case 3: + return delete_tracks(index); + case 4: /* move track */ viewer.moving_track = index; - viewer.moving_playlist_index = current_track->index; + if (viewer.multiselection_enabled) + { + int selected_size = viewer.selected_size; + if (selected_size < 0) + { + viewer.min_index_forbid_limit = (- selected_size); + viewer.end_index_forbid_limit = 0; + } + else + { + viewer.min_index_forbid_limit = 0; + viewer.end_index_forbid_limit = (selected_size - 1); + } + } return PV_CONTEXT_UNCHANGED; - case 4: - return show_track_info(current_track); case 5: + current_track = playlist_buffer_get_track(&viewer.buffer, index); + return show_track_info(current_track); + case 6: /* shuffle */ if (!yesno_pop_confirm(ID2P(LANG_SHUFFLE))) return PV_CONTEXT_UNCHANGED; @@ -751,11 +891,11 @@ static enum pv_context_result context_menu(int index) playlist_randomise(viewer.playlist, current_tick, !viewer.playlist); viewer.selected_track = 0; return PV_CONTEXT_MODIFIED; - case 6: + case 7: save_playlist_screen(viewer.playlist); /* playlist indices of current playlist may have changed */ return viewer.playlist ? PV_CONTEXT_UNCHANGED : PV_CONTEXT_PL_UPDATE; - case 7: + case 8: { /* playlist viewer settings */ sel = global_settings.playlist_viewer_track_display; @@ -766,7 +906,8 @@ static enum pv_context_result context_menu(int index) PV_CONTEXT_UNCHANGED : PV_CONTEXT_PL_UPDATE; } #ifdef HAVE_TAGCACHE - case 8: + case 9: + current_track = playlist_buffer_get_track(&viewer.buffer, index); return open_pictureflow(current_track); #endif } @@ -777,24 +918,40 @@ static enum pv_context_result context_menu(int index) static int get_track_num(struct playlist_viewer *local_viewer, int selected_item) { + int result = selected_item; if (local_viewer->moving_track >= 0) { - if (local_viewer->selected_track == selected_item) - { - return local_viewer->moving_track; - } - else if (local_viewer->selected_track > selected_item - && selected_item >= local_viewer->moving_track) + int selected_size = viewer.selected_size; + int moving_track = local_viewer->moving_track; + int cur_selected_track = local_viewer->selected_track; + if (selected_size < 0) { - return selected_item+1; /* move down */ + moving_track -= (- selected_size); + cur_selected_track -= (- selected_size); + selected_size = ((- selected_size) + 1); } - else if (local_viewer->selected_track < selected_item - && selected_item <= local_viewer->moving_track) + for (int i = 0; i < selected_size; i++) { - return selected_item-1; /* move up */ + if ((cur_selected_track + i) == selected_item) + { + result = moving_track + i; + break; + } + else if ((cur_selected_track + (selected_size - 1 - i)) > selected_item + && selected_item >= (moving_track + (selected_size - 1 - i))) + { + /* move down */ + result = selected_item + i + 1; + } + else if ((cur_selected_track + i) < selected_item + && selected_item <= (moving_track + i)) + { + /* move up */ + result = selected_item - i - 1; + } } } - return selected_item; + return result; } static struct playlist_entry* pv_get_track(struct playlist_viewer *local_viewer, int selected_item) @@ -821,15 +978,24 @@ static enum themable_icons playlist_callback_icons(int selected_item, { struct playlist_viewer *local_viewer = (struct playlist_viewer *)data; struct playlist_entry *track = pv_get_track(local_viewer, selected_item); + int moving_track = local_viewer->moving_track; + int selected_size = local_viewer->selected_size; + if (moving_track >= 0 && selected_size < 0) + { + moving_track -= (- selected_size); + selected_size = ((- selected_size) + 1); + } if (track->index == local_viewer->current_playing_track) { /* Current playing track */ return Icon_Audio; } - else if (track->index == local_viewer->moving_playlist_index) + else if (moving_track >= 0 + && track->index >= moving_track + && track->index < (moving_track + selected_size)) { - /* Track we are moving */ + /* Tracks we are moving */ return Icon_Moving; } else if (track->attr & PLAYLIST_ATTR_QUEUED) @@ -885,7 +1051,10 @@ static void update_gui(struct gui_synclist * playlist_lists, bool init) { if (init) gui_synclist_init(playlist_lists, playlist_callback_name, - &viewer, false, 1, NULL); + &viewer, false, 1, NULL, true); + playlist_lists->selected_size = viewer.selected_size; + playlist_lists->min_index_forbid_limit = viewer.min_index_forbid_limit; + playlist_lists->end_index_forbid_limit = viewer.end_index_forbid_limit; gui_synclist_set_nb_items(playlist_lists, viewer.num_tracks); gui_synclist_set_voice_callback(playlist_lists, global_settings.talk_file? @@ -903,7 +1072,11 @@ static bool update_viewer(struct gui_synclist *playlist_lists, enum pv_context_r { bool exit = false; if (res == PV_CONTEXT_MODIFIED) + { + viewer.selected_size = 1; + viewer.multiselection_enabled = false; playlist_set_modified(viewer.playlist, true); + } if (res == PV_CONTEXT_MODIFIED || res == PV_CONTEXT_PL_UPDATE) { @@ -977,17 +1150,73 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, /* during moving, another redraw is going to be needed, * since viewer.selected_track is updated too late (after the first draw) * drawing the moving item needs it */ - viewer.selected_track=gui_synclist_get_sel_pos(&playlist_lists); + bool list_redraw_needed = false; + if (viewer.selected_track != gui_synclist_get_sel_pos(&playlist_lists)) + { + viewer.selected_track = gui_synclist_get_sel_pos(&playlist_lists); + list_redraw_needed = true; + } + if (viewer.multiselection_enabled) + { + if (viewer.moving_track < 0) + { + int offsetdiff = viewer.selected_track - viewer.multiselection_offset_track; + if (offsetdiff < 0) + { + viewer.selected_size = (- offsetdiff) + 1; + } + else if (offsetdiff == 0) + { + viewer.selected_size = 1; + } + else + { + viewer.selected_size = - (offsetdiff); + } + viewer.min_index_forbid_limit = 0; + viewer.end_index_forbid_limit = 0; + } + } + else + { + viewer.selected_size = 1; + viewer.min_index_forbid_limit = 0; + viewer.end_index_forbid_limit = 0; + } + if (playlist_lists.selected_size != viewer.selected_size) + { + playlist_lists.selected_size = viewer.selected_size; + list_redraw_needed = true; + } + if (playlist_lists.min_index_forbid_limit != viewer.min_index_forbid_limit) + { + playlist_lists.min_index_forbid_limit = viewer.min_index_forbid_limit; + list_redraw_needed = true; + } + if (playlist_lists.end_index_forbid_limit != viewer.end_index_forbid_limit) + { + playlist_lists.end_index_forbid_limit = viewer.end_index_forbid_limit; + list_redraw_needed = true; + } if (res) { bool reload = playlist_buffer_needs_reload(&viewer.buffer, viewer.selected_track); if (reload) + { playlist_buffer_load_entries_screen(&viewer.buffer, button == ACTION_STD_NEXT ? FORWARD : BACKWARD, viewer.selected_track); - if (reload || viewer.moving_track >= 0) - gui_synclist_draw(&playlist_lists); + list_redraw_needed = true; + } + } + if (list_redraw_needed) + /* it's important to refresh conditionally only when something changed, + or all texts scrolling will be broken */ + gui_synclist_draw(&playlist_lists); + if (viewer.moving_track < 0 && button == ACTION_STD_OK && viewer.selected_size != 1) + { + button = ACTION_STD_CONTEXT; } switch (button) { @@ -999,7 +1228,15 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, viewer.selected_track = viewer.moving_track; gui_synclist_select_item(&playlist_lists, viewer.moving_track); viewer.moving_track = -1; - viewer.moving_playlist_index = -1; + viewer.selected_size = 1; + viewer.multiselection_enabled = false; + gui_synclist_draw(&playlist_lists); + gui_synclist_speak_item(&playlist_lists); + } + else if (viewer.multiselection_enabled) + { + viewer.selected_size = 1; + viewer.multiselection_enabled = false; gui_synclist_draw(&playlist_lists); gui_synclist_speak_item(&playlist_lists); } @@ -1019,22 +1256,40 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, int ret_val, start_index = current_track->index; if (viewer.moving_track >= 0) { - /* Move track */ - ret_val = playlist_move(viewer.playlist, - viewer.moving_playlist_index, - current_track->index); - if (ret_val < 0) + /* Move tracks */ + int moving_track = viewer.moving_track; + int distance = (viewer.selected_track - moving_track); + if (viewer.selected_size < 0) { - cond_talk_ids_fq(LANG_MOVE, LANG_FAILED); - splashf(HZ, (unsigned char *)"%s %s", str(LANG_MOVE), - str(LANG_FAILED)); + moving_track -= (- viewer.selected_size); + viewer.selected_size = ((- viewer.selected_size) + 1); + } + for (int i = 0; i < viewer.selected_size; i++) + { + int start_index = moving_track; + int dest_index = moving_track + distance; + if (start_index > dest_index) + { + start_index = start_index + i; + dest_index = dest_index + i; + } + else if (viewer.selected_size > 1 && distance > 0) + dest_index = (dest_index + (viewer.selected_size - 1)); + ret_val = playlist_move(viewer.playlist, + (playlist_buffer_get_track(&viewer.buffer, start_index)->index), + (playlist_buffer_get_track(&viewer.buffer, dest_index)->index)); + if (ret_val < 0) + { + cond_talk_ids_fq(LANG_MOVE, LANG_FAILED); + splashf(HZ, (unsigned char *)"%s %s", str(LANG_MOVE), + str(LANG_FAILED)); + break; + } } - playlist_set_modified(viewer.playlist, true); - update_playlist(true); + viewer.multiselection_enabled = false; viewer.moving_track = -1; - viewer.moving_playlist_index = -1; } else if (global_settings.party_mode) { @@ -1150,6 +1405,22 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, goto exit; } } + else if (global_settings.hotkey_tree == HOTKEY_MULTISELECTION) + { + /* Make a multiselection */ + if (!viewer.multiselection_enabled) + { + viewer.multiselection_enabled = true; + viewer.multiselection_offset_track = current_track->index; + /* Reset current text scrollings to avoid graphical glitches */ + gui_synclist_draw(&playlist_lists); + } + else + { + /* Will be disabled later bacause of PV_CONTEXT_MODIFIED */ + update_viewer(&playlist_lists, PV_CONTEXT_MODIFIED); + } + } else if (global_settings.hotkey_tree == HOTKEY_PROPERTIES) { if (show_track_info(current_track) == PV_CONTEXT_USB) @@ -1161,17 +1432,18 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, } else if (global_settings.hotkey_tree == HOTKEY_DELETE) { - if (update_viewer(&playlist_lists, - delete_track(current_track->index, - viewer.selected_track, - (current_track->index == viewer.current_playing_track)))) + if (update_viewer(&playlist_lists, delete_tracks(viewer.selected_track))) { ret = PLAYLIST_VIEWER_CANCEL; exit = true; } } else - onplay(current_track->name, FILE_ATTR_AUDIO, CONTEXT_STD, true, ONPLAY_NO_CUSTOMACTION); + { + onplay(current_track->name, FILE_ATTR_AUDIO, CONTEXT_STD, true, ONPLAY_NO_CUSTOMACTION, true); + struct add_to_pl_param *choice = onplay_show_playlist_menu_get_current_choice(); + update_viewer(&playlist_lists, add_tracks_to_current_playlist(viewer.selected_track, choice)); + } break; } #endif /* HAVE_HOTKEY */ @@ -1284,7 +1556,7 @@ bool search_playlist(void) backlight_on(); struct playlist_search_data s_data = {.track = &track, .found_indicies = found_indicies}; gui_synclist_init(&playlist_lists, playlist_search_callback_name, - &s_data, false, 1, NULL); + &s_data, false, 1, NULL, true); gui_synclist_set_title(&playlist_lists, str(LANG_SEARCH_RESULTS), NOICON); if(global_settings.talk_file) gui_synclist_set_voice_callback(&playlist_lists, diff --git a/apps/plugin.c b/apps/plugin.c index 9b6a446cec..7832fac39b 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -468,6 +468,7 @@ static const struct plugin_api rockbox_api = { set_current_file, set_dirfilter, onplay_show_playlist_menu, + onplay_show_playlist_menu_get_choice, onplay_show_playlist_cat_menu, browse_id3, diff --git a/apps/plugin.h b/apps/plugin.h index 46b13b07fc..c8b1311798 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -395,7 +395,8 @@ struct plugin_api { void (*gui_synclist_init)(struct gui_synclist * lists, list_get_name callback_get_item_name, void * data, bool scroll_all,int selected_size, - struct viewport parent[NB_SCREENS]); + struct viewport parent[NB_SCREENS], + bool multiple_selection_force_single_entry_scroll); void (*gui_synclist_set_nb_items)(struct gui_synclist * lists, int nb_items); void (*gui_synclist_set_voice_callback)(struct gui_synclist * lists, list_speak_item voice_callback); void (*gui_synclist_set_icon_callback)(struct gui_synclist * lists, @@ -539,6 +540,7 @@ struct plugin_api { void (*set_dirfilter)(int l_dirfilter); void (*onplay_show_playlist_menu)(const char* path, int attr, void (*playlist_insert_cb)); + struct add_to_pl_param* (*onplay_show_playlist_menu_get_choice)(void); void (*onplay_show_playlist_cat_menu)(const char* track_name, int attr, void (*add_to_pl_cb)); bool (*browse_id3)(struct mp3entry *id3, @@ -868,7 +870,7 @@ struct plugin_api { const char *filename, int position, bool queue, bool sync); int (*playlist_insert_directory)(struct playlist_info* playlist, const char *dirname, int position, bool queue, - bool recurse, struct playlist_insert_context *context); + bool recurse, struct playlist_insert_context* context_to_use); int (*playlist_insert_playlist)(struct playlist_info* playlist, const char *filename, int position, bool queue); int (*playlist_insert_context_create)(struct playlist_info* playlist, diff --git a/apps/plugins/calendar.c b/apps/plugins/calendar.c index d1213367ae..819bddca43 100644 --- a/apps/plugins/calendar.c +++ b/apps/plugins/calendar.c @@ -974,7 +974,7 @@ static bool view_events(int selected, struct shown *shown) bool exit=false; int button; - rb->gui_synclist_init(&gui_memos, &get_event_text, shown, false, 1, NULL); + rb->gui_synclist_init(&gui_memos, &get_event_text, shown, false, 1, NULL, false); rb->gui_synclist_set_title(&gui_memos, "Events (" CALENDAR_EVENT_MENU_NAME " : menu)", NOICON); rb->gui_synclist_set_nb_items(&gui_memos, memos_in_shown_memory); rb->gui_synclist_select_item(&gui_memos, selected); diff --git a/apps/plugins/chessbox/chessbox_pgn.c b/apps/plugins/chessbox/chessbox_pgn.c index d6da369b9e..80fa6c63e7 100644 --- a/apps/plugins/chessbox/chessbox_pgn.c +++ b/apps/plugins/chessbox/chessbox_pgn.c @@ -672,7 +672,7 @@ struct pgn_game_node* pgn_show_game_list(struct pgn_game_node* first_game){ } - rb->gui_synclist_init(&games_list, &get_game_text, first_game, false, 1, NULL); + rb->gui_synclist_init(&games_list, &get_game_text, first_game, false, 1, NULL, false); rb->gui_synclist_set_title(&games_list, rb->str(LANG_CHESSBOX_GAMES), NOICON); if (rb->global_settings->talk_menu) rb->gui_synclist_set_voice_callback(&games_list, speak_game_selection); diff --git a/apps/plugins/keybox.c b/apps/plugins/keybox.c index 749d76af74..a2e6b44e1a 100644 --- a/apps/plugins/keybox.c +++ b/apps/plugins/keybox.c @@ -654,7 +654,7 @@ enum plugin_status plugin_start(const void *parameter) (void)parameter; int ret; - rb->gui_synclist_init(&kb_list, &kb_list_cb, NULL, false, 1, NULL); + rb->gui_synclist_init(&kb_list, &kb_list_cb, NULL, false, 1, NULL, false); rb->gui_synclist_set_title(&kb_list, "Keybox", NOICON); rb->gui_synclist_set_nb_items(&kb_list, 0); diff --git a/apps/plugins/keyremap.c b/apps/plugins/keyremap.c index bb7e965963..0dc5f5f46e 100644 --- a/apps/plugins/keyremap.c +++ b/apps/plugins/keyremap.c @@ -2043,7 +2043,7 @@ static void synclist_set(int id, int selected_item, int items, int sel_size) list_voice_cb(0, menu_id); rb->gui_synclist_init(&lists,list_get_name_cb, - menu_id, false, sel_size, NULL); + menu_id, false, sel_size, NULL, false); rb->gui_synclist_set_icon_callback(&lists,NULL); rb->gui_synclist_set_voice_callback(&lists, list_voice_cb); diff --git a/apps/plugins/lastfm_scrobbler.c b/apps/plugins/lastfm_scrobbler.c index 906888c957..2c364973da 100644 --- a/apps/plugins/lastfm_scrobbler.c +++ b/apps/plugins/lastfm_scrobbler.c @@ -398,7 +398,7 @@ static void clear_display(void) if (!lists.title) /* initialize the list, only used to display the title..*/ { - rb->gui_synclist_init(&lists, NULL, NULL, false,1, NULL); + rb->gui_synclist_init(&lists, NULL, NULL, false, 1, NULL, false); rb->gui_synclist_set_title(&lists, rb->str(LANG_AUDIOSCROBBLER), Icon_Moving); } diff --git a/apps/plugins/lastfm_scrobbler_viewer.c b/apps/plugins/lastfm_scrobbler_viewer.c index c7bf2bab8b..fb488830b7 100644 --- a/apps/plugins/lastfm_scrobbler_viewer.c +++ b/apps/plugins/lastfm_scrobbler_viewer.c @@ -936,7 +936,7 @@ static void synclist_set(int selected_item, int items, int sel_size, struct prin selected_item = 0; rb->gui_synclist_init(&lists,list_get_name_cb, - pc_data, false, sel_size, NULL); + pc_data, false, sel_size, NULL, false); rb->gui_synclist_set_icon_callback(&lists, list_icon_cb); rb->gui_synclist_set_voice_callback(&lists, list_voice_cb); diff --git a/apps/plugins/lrcplayer.c b/apps/plugins/lrcplayer.c index 9102f03264..67a422917d 100644 --- a/apps/plugins/lrcplayer.c +++ b/apps/plugins/lrcplayer.c @@ -2051,7 +2051,7 @@ static int timetag_editor(void) selected = idx; } - rb->gui_synclist_init(&gui_editor, &get_lrc_timeline, NULL, false, 1, NULL); + rb->gui_synclist_init(&gui_editor, &get_lrc_timeline, NULL, false, 1, NULL, false); rb->gui_synclist_set_nb_items(&gui_editor, current.nlrcline); rb->gui_synclist_set_icon_callback(&gui_editor, get_icon); rb->gui_synclist_set_title(&gui_editor, "Timetag Editor", diff --git a/apps/plugins/main_menu_config.c b/apps/plugins/main_menu_config.c index 94e7740fbe..0bc461ff06 100644 --- a/apps/plugins/main_menu_config.c +++ b/apps/plugins/main_menu_config.c @@ -183,7 +183,7 @@ enum plugin_status plugin_start(const void* parameter) menu_table = rb->root_menu_get_options(&menu_item_count); load_from_cfg(); - rb->gui_synclist_init(&list, menu_get_name, NULL, false, 1, NULL); + rb->gui_synclist_init(&list, menu_get_name, NULL, false, 1, NULL, false); if (rb->global_settings->talk_menu) rb->gui_synclist_set_voice_callback(&list, menu_speak_item); rb->gui_synclist_set_icon_callback(&list, menu_get_icon); diff --git a/apps/plugins/open_plugins.c b/apps/plugins/open_plugins.c index e90d2d95de..6373331572 100644 --- a/apps/plugins/open_plugins.c +++ b/apps/plugins/open_plugins.c @@ -650,7 +650,7 @@ static void synclist_set(char* menu_id, int selection, int items, int sel_size) selection = 0; rb->gui_synclist_init(&lists,list_get_name_cb, - menu_id, false, sel_size, NULL); + menu_id, false, sel_size, NULL, false); rb->gui_synclist_set_voice_callback(&lists, list_voice_cb); rb->gui_synclist_set_nb_items(&lists,items); diff --git a/apps/plugins/playing_time.c b/apps/plugins/playing_time.c index 6504988026..f2a8bb7904 100644 --- a/apps/plugins/playing_time.c +++ b/apps/plugins/playing_time.c @@ -317,7 +317,7 @@ static int pt_speak_info(int selected_item, void * data) static bool pt_display_stats(struct playing_time_info *pti) { struct gui_synclist pt_lists; - rb->gui_synclist_init(&pt_lists, &pt_get_info, pti, true, 2, NULL); + rb->gui_synclist_init(&pt_lists, &pt_get_info, pti, true, 2, NULL, false); if (rb->global_settings->talk_menu) rb->gui_synclist_set_voice_callback(&pt_lists, pt_speak_info); rb->gui_synclist_set_nb_items(&pt_lists, pti->remaining_only ? 2 : 8*2); @@ -367,7 +367,7 @@ static int pt_options_speak(int selected_item, void * data) static int pt_options(struct playing_time_info *pti) { struct gui_synclist pt_options; - rb->gui_synclist_init(&pt_options, &pt_options_name, NULL, true, 1, NULL); + rb->gui_synclist_init(&pt_options, &pt_options_name, NULL, true, 1, NULL, false); if (rb->global_settings->talk_menu) rb->gui_synclist_set_voice_callback(&pt_options, pt_options_speak); rb->gui_synclist_set_nb_items(&pt_options, *pti->single_mode_tag ? 3 : 2); diff --git a/apps/plugins/properties.c b/apps/plugins/properties.c index 316b33292a..fc1dc070fc 100644 --- a/apps/plugins/properties.c +++ b/apps/plugins/properties.c @@ -162,7 +162,7 @@ static void setup_properties_list(struct dir_stats *stats) else nb_props = NUM_DIR_PROPERTIES; - rb->gui_synclist_init(&properties_lists, &get_props, stats, false, 2, NULL); + rb->gui_synclist_init(&properties_lists, &get_props, stats, false, 2, NULL, false); rb->gui_synclist_set_title(&properties_lists, rb->str(props_type == PROPS_DIR ? LANG_PROPERTIES_DIRECTORY_PROPERTIES : diff --git a/apps/plugins/puzzles/rockbox.c b/apps/plugins/puzzles/rockbox.c index 172207b63b..5f22344253 100644 --- a/apps/plugins/puzzles/rockbox.c +++ b/apps/plugins/puzzles/rockbox.c @@ -2138,7 +2138,7 @@ static int list_choose(const char *list_str, const char *title, int sel) struct gui_synclist list; - rb->gui_synclist_init(&list, &config_choices_formatter, (void*)list_str, false, 1, NULL); + rb->gui_synclist_init(&list, &config_choices_formatter, (void*)list_str, false, 1, NULL, false); rb->gui_synclist_set_nb_items(&list, n); rb->gui_synclist_select_item(&list, sel); @@ -2350,7 +2350,7 @@ static bool config_menu_core(int which) /* display a list */ struct gui_synclist list; - rb->gui_synclist_init(&list, &config_formatter, config, false, 1, NULL); + rb->gui_synclist_init(&list, &config_formatter, config, false, 1, NULL, false); rb->gui_synclist_set_nb_items(&list, n); rb->gui_synclist_select_item(&list, 0); @@ -2444,7 +2444,7 @@ static int do_preset_menu(struct preset_menu *menu, char *title, int selected) /* display a list */ struct gui_synclist list; - rb->gui_synclist_init(&list, &preset_formatter, menu, false, 1, NULL); + rb->gui_synclist_init(&list, &preset_formatter, menu, false, 1, NULL, false); rb->gui_synclist_set_nb_items(&list, menu->n_entries); rb->gui_synclist_select_item(&list, selected); diff --git a/apps/plugins/random_folder_advance_config.c b/apps/plugins/random_folder_advance_config.c index 7f932aabd8..31fb147670 100644 --- a/apps/plugins/random_folder_advance_config.c +++ b/apps/plugins/random_folder_advance_config.c @@ -370,7 +370,7 @@ static int edit(struct rfa_scan *scan) } scan->dirty = false; rb->gui_synclist_init(&lists, list_get_name_cb, (void *) scan, - false, 1, NULL); + false, 1, NULL, false); rb->gui_synclist_set_nb_items(&lists, scan->dirs->count); rb->gui_synclist_select_item(&lists, 0); set_title(&lists, false, scan->dirs->count); diff --git a/apps/plugins/rb_info.c b/apps/plugins/rb_info.c index 31b06d6f75..39d158d5f6 100644 --- a/apps/plugins/rb_info.c +++ b/apps/plugins/rb_info.c @@ -508,7 +508,7 @@ static void synclist_set(char* menu_id, int selected_item, int items, int sel_si list_voice_cb(0, menu_id); rb->gui_synclist_init(&lists,list_get_name_cb, - menu_id, false, sel_size, NULL); + menu_id, false, sel_size, NULL, false); if (menu_id == MENU_ID(M_TESTPUT)) { testput_cols = printcell_set_columns(&lists, NULL, diff --git a/apps/plugins/shopper.c b/apps/plugins/shopper.c index 25a484a31e..f584aa7807 100644 --- a/apps/plugins/shopper.c +++ b/apps/plugins/shopper.c @@ -302,7 +302,7 @@ enum plugin_status plugin_start(const void* parameter) rb->cpu_boost(0); #endif /* now dump it in the list */ - rb->gui_synclist_init(&lists,list_get_name_cb,0, false, 1, NULL); + rb->gui_synclist_init(&lists,list_get_name_cb,0, false, 1, NULL, false); rb->gui_synclist_set_icon_callback(&lists, list_get_icon_cb); create_view(&lists); rb->gui_synclist_set_nb_items(&lists,view_item_count); diff --git a/apps/plugins/shortcuts/shortcuts_view.c b/apps/plugins/shortcuts/shortcuts_view.c index fadf111c6d..08fd5b569e 100644 --- a/apps/plugins/shortcuts/shortcuts_view.c +++ b/apps/plugins/shortcuts/shortcuts_view.c @@ -105,7 +105,7 @@ int list_sc(int *selected_item) /* Setup the GUI list object, draw it to the screen, * and then handle the user input to it */ - rb->gui_synclist_init(&gui_sc, &build_sc_list, &sc_file, false, 1, NULL); + rb->gui_synclist_init(&gui_sc, &build_sc_list, &sc_file, false, 1, NULL, false); rb->gui_synclist_set_title(&gui_sc, (user_file?"Shortcuts (sealed)":"Shortcuts (editable)"), NOICON); rb->gui_synclist_set_nb_items(&gui_sc, sc_file.entry_cnt); diff --git a/apps/plugins/tagcache/tagcache.c b/apps/plugins/tagcache/tagcache.c index c5ee21e870..26925c0274 100644 --- a/apps/plugins/tagcache/tagcache.c +++ b/apps/plugins/tagcache/tagcache.c @@ -760,7 +760,7 @@ static int commit_menu(void) int button,i; int selection, ret = 0; - rb->gui_synclist_init(&lists,list_get_name_cb,0, false, 1, NULL); + rb->gui_synclist_init(&lists,list_get_name_cb,0, false, 1, NULL, false); rb->gui_synclist_set_nb_items(&lists, 9); rb->gui_synclist_select_item(&lists, 0); diff --git a/apps/plugins/text_editor.c b/apps/plugins/text_editor.c index b4f99447ae..73f4d4daa6 100644 --- a/apps/plugins/text_editor.c +++ b/apps/plugins/text_editor.c @@ -211,7 +211,7 @@ static bool save_changes(int overwrite) static void setup_lists(struct gui_synclist *lists, int sel) { - rb->gui_synclist_init(lists,list_get_name_cb,0, false, 1, NULL); + rb->gui_synclist_init(lists,list_get_name_cb,0, false, 1, NULL, false); rb->gui_synclist_set_nb_items(lists,line_count); rb->gui_synclist_select_item(lists, sel); rb->gui_synclist_draw(lists); diff --git a/apps/plugins/zxbox/tapefile.c b/apps/plugins/zxbox/tapefile.c index 054cae1aba..1830c77d5a 100644 --- a/apps/plugins/zxbox/tapefile.c +++ b/apps/plugins/zxbox/tapefile.c @@ -66,18 +66,18 @@ static dbyte callctr, callbeg; #define ST_DIRE 2 #define ST_MISC 3 -#define PL_NONE 0 -#define PL_PAUSE 1 -#define PL_LEADER 2 -#define PL_DATA 3 -#define PL_END 4 -#define PL_PSEQ 5 -#define PL_DIRE 6 +#define TFPL_NONE 0 +#define TFPL_PAUSE 1 +#define TFPL_LEADER 2 +#define TFPL_DATA 3 +#define TFPL_END 4 +#define TFPL_PSEQ 5 +#define TFPL_DIRE 6 #define IMP_1MS 3500 static dbyte lead_pause; -static int playstate = PL_NONE; +static int playstate = TFPL_NONE; static int currlev; #define DEF_LEAD_PAUSE 2000 @@ -339,7 +339,7 @@ static int end_seg(struct seginfo *csp) segi++; isbeg(); } - playstate = PL_NONE; + playstate = TFPL_NONE; return 1; } @@ -717,7 +717,7 @@ byte *tf_get_block(int i) int next_byte(void) { - playstate = PL_NONE; + playstate = TFPL_NONE; return next_data(); } @@ -737,7 +737,7 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) while(impbuf < impbufend - 1 && timelen > 0) { switch(playstate) { - case PL_PAUSE: + case TFPL_PAUSE: if(currlev && lead_pause) { PULSE(IMP_1MS); lead_pause --; @@ -758,15 +758,15 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) tf_cseg.ptr != tf_cseg.len) finished = 0; switch (tf_cseg.type) { - case ST_NORM: playstate = PL_LEADER; break; - case ST_DIRE: playstate = PL_DIRE; dirpulse = 0; break; - case ST_PSEQ: playstate = PL_PSEQ; break; - default: playstate = PL_NONE; + case ST_NORM: playstate = TFPL_LEADER; break; + case ST_DIRE: playstate = TFPL_DIRE; dirpulse = 0; break; + case ST_PSEQ: playstate = TFPL_PSEQ; break; + default: playstate = TFPL_NONE; } } break; - case PL_LEADER: + case TFPL_LEADER: if(tf_cseg.num >= 2) { DPULSE(tf_cseg.pulse, tf_cseg.pulse); tf_cseg.num -= 2; @@ -780,15 +780,15 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) if(tf_cseg.sync1p || tf_cseg.sync2p) DPULSE(tf_cseg.sync1p, tf_cseg.sync2p); bitrem = 0; - playstate = PL_DATA; + playstate = TFPL_DATA; } break; - case PL_DATA: + case TFPL_DATA: if(!bitrem) { toput = next_data(); if(toput < 0) { - playstate = PL_END; + playstate = TFPL_END; break; } if(tf_cseg.ptr != tf_cseg.len) { @@ -822,14 +822,14 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) bitrem--, toput <<= 1; break; - case PL_PSEQ: + case TFPL_PSEQ: { int b1, b2; dbyte pulse1, pulse2; b1 = next_data(); b2 = next_data(); if(b1 < 0 || b2 < 0) { - playstate = PL_END; + playstate = TFPL_END; break; } pulse1 = b1 + (b2 << 8); @@ -838,7 +838,7 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) b2 = next_data(); if(b1 < 0 || b2 < 0) { PULSE(pulse1); - playstate = PL_END; + playstate = TFPL_END; break; } pulse2 = b1 + (b2 << 8); @@ -846,12 +846,12 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) } break; - case PL_DIRE: + case TFPL_DIRE: for(;;) { if(!bitrem) { toput = next_data(); if(toput < 0) { - playstate = PL_END; + playstate = TFPL_END; DPULSE(dirpulse, 0); break; } @@ -877,17 +877,17 @@ int next_imps(unsigned short *impbuf, int buflen, long timelen) } break; - case PL_END: + case TFPL_END: if(tf_cseg.pause) { PULSE(IMP_1MS); tf_cseg.pause--; if(currlev) PULSE(0); finished = 1; } - playstate = PL_NONE; + playstate = TFPL_NONE; break; - case PL_NONE: + case TFPL_NONE: default: return PTRDIFF(impbuf, impbufstart); } @@ -916,10 +916,10 @@ int next_segment(void) } if(tf_cseg.segtype >= SEG_DATA) { - playstate = PL_PAUSE; + playstate = TFPL_PAUSE; if(lead_pause) finished = 1; } - else playstate = PL_NONE; + else playstate = TFPL_NONE; if(tf_cseg.segtype <= SEG_STOP && !finished) { endnext = 1; @@ -930,7 +930,7 @@ int next_segment(void) tf_cseg.pause = 1; tf_cseg.segtype = SEG_VIRTUAL; - playstate = PL_END; + playstate = TFPL_END; } return tf_cseg.segtype; @@ -954,7 +954,7 @@ unsigned segment_pos(void) void close_tapefile(void) { if(tapefd != -1) { - playstate = PL_NONE; + playstate = TFPL_NONE; rb->close(tapefd); tapefd = -1; } diff --git a/apps/radio/presets.c b/apps/radio/presets.c index 9ac4b75ee3..2d97f7ba0b 100644 --- a/apps/radio/presets.c +++ b/apps/radio/presets.c @@ -503,7 +503,7 @@ int handle_radio_presets(void) if(presets_loaded == false) return result; - gui_synclist_init(&lists, presets_get_name, NULL, false, 1, NULL); + gui_synclist_init(&lists, presets_get_name, NULL, false, 1, NULL, false); gui_synclist_set_title(&lists, str(LANG_PRESET), NOICON); if(global_settings.talk_file) gui_synclist_set_voice_callback(&lists, presets_speak_name); diff --git a/apps/recorder/recording.c b/apps/recorder/recording.c index 253142bd97..0a339e70f3 100644 --- a/apps/recorder/recording.c +++ b/apps/recorder/recording.c @@ -1087,7 +1087,7 @@ bool recording_screen(bool no_source) } /* init the bottom list */ - gui_synclist_init(&lists, reclist_get_name, NULL, false, 1, vp_list); + gui_synclist_init(&lists, reclist_get_name, NULL, false, 1, vp_list, false); gui_synclist_set_title(&lists, NULL, Icon_NOICON); send_event(GUI_EVENT_ACTIONUPDATE, (void*)1); /* force a redraw */ diff --git a/apps/screens.c b/apps/screens.c index 5e19db7edd..2487bc0da5 100644 --- a/apps/screens.c +++ b/apps/screens.c @@ -807,7 +807,7 @@ bool browse_id3_ex(struct mp3entry *id3, struct playlist_info *playlist, info.info_id[info.count++] = i; } - gui_synclist_init(&id3_lists, &id3_get_name_cb, &info, true, 2, NULL); + gui_synclist_init(&id3_lists, &id3_get_name_cb, &info, true, 2, NULL, false); if(global_settings.talk_menu) gui_synclist_set_voice_callback(&id3_lists, id3_speak_item); gui_synclist_set_nb_items(&id3_lists, info.count*2); diff --git a/apps/settings_list.c b/apps/settings_list.c index 4ac206b5be..f7e5494ec2 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -2367,21 +2367,21 @@ const struct settings_list settings[] = { TABLE_SETTING(0, hotkey_tree, LANG_HOTKEY_FILE_BROWSER, HOTKEY_OFF, "hotkey tree", #ifdef HAVE_TAGCACHE - "off,properties,pictureflow,open with,delete,insert,insert shuffled", + "off,properties,pictureflow,open with,delete,insert,insert shuffled,multiselection", #else - "off,properties,open with,delete,insert,insert shuffled", + "off,properties,open with,delete,insert,insert shuffled,multiselection", #endif UNIT_INT, hotkey_formatter, hotkey_getlang, NULL, #ifdef HAVE_TAGCACHE - 7, + 8, #else - 6, + 7, #endif HOTKEY_OFF,HOTKEY_PROPERTIES, #ifdef HAVE_TAGCACHE HOTKEY_PICTUREFLOW, #endif - HOTKEY_OPEN_WITH, HOTKEY_DELETE, HOTKEY_INSERT, HOTKEY_INSERT_SHUFFLED), + HOTKEY_OPEN_WITH, HOTKEY_DELETE, HOTKEY_INSERT, HOTKEY_INSERT_SHUFFLED, HOTKEY_MULTISELECTION), #endif /* HAVE_HOTKEY */ INT_SETTING(F_TIME_SETTING, resume_rewind, LANG_RESUME_REWIND, 0, diff --git a/apps/tagtree.c b/apps/tagtree.c index 67ff646205..b10b9398f8 100644 --- a/apps/tagtree.c +++ b/apps/tagtree.c @@ -2415,7 +2415,7 @@ static bool* fill_random_playlist_indexes(bool *bool_array, size_t arr_sz, static bool insert_all_playlist(struct tree_context *c, const char* playlist, bool new_playlist, - int position, bool queue) + int position, bool queue, struct playlist_insert_context* context_to_use) { struct tagcache_search tcs; int n; @@ -2438,7 +2438,7 @@ static bool insert_all_playlist(struct tree_context *c, if (playlist == NULL) { - if (playlist_insert_context_create(NULL, &context, position, queue, false) < 0) + if (context_to_use == NULL && playlist_insert_context_create(NULL, &context, position, queue, false) < 0) { tagcache_search_finish(&tcs); cpu_boost(false); @@ -2554,7 +2554,7 @@ static bool insert_all_playlist(struct tree_context *c, } } - if (playlist_insert_context_add(&context, buf) < 0) { + if (playlist_insert_context_add(context_to_use == NULL ? &context : context_to_use, buf) < 0) { logf("playlist_insert_track failed"); exit_loop_now = true; break; @@ -2572,7 +2572,10 @@ static bool insert_all_playlist(struct tree_context *c, } if (playlist == NULL) - playlist_insert_context_release(&context); + { + if (context_to_use == NULL) + playlist_insert_context_release(&context); + } else close(fd); @@ -2604,11 +2607,15 @@ static void reset_tc_to_prev(int dirlevel, int selected_item) tagtree_load(tc); } -static bool tagtree_insert_selection(int position, bool queue, - const char* playlist, bool new_playlist) +bool tagtree_insert_selection(int position, bool queue, + const char* playlist, bool new_playlist, + struct playlist_insert_context* context_to_use, + int selected_size) { char buf[MAX_PATH]; int dirlevel = tc->dirlevel; + /* goto_allsubentries will modify tc->selected_item so we need to store a copy here to restore + the position at the end of the function with reset_tc_to_prev */ int selected_item = tc->selected_item; int newtable; int ret; @@ -2621,19 +2628,47 @@ static bool tagtree_insert_selection(int position, bool queue, #endif , 0, 0, 0); - newtable = tagtree_get_entry(tc, tc->selected_item)->newtable; + newtable = tagtree_get_entry(tc, selected_item)->newtable; if (newtable == TABLE_PLAYTRACK) /* Insert a single track? */ { - if (tagtree_get_filename(tc, buf, sizeof buf) < 0) + struct tagcache_search tcs; + if (!tagcache_search(&tcs, tag_filename)) return false; - if (!playlist) - playlist_insert_track(NULL, buf, position, queue, true); - else - catalog_insert_into(playlist, new_playlist, buf, FILE_ATTR_AUDIO); - - return true; + for (int i = 0; i < selected_size; i++) + { + if (i > 0 && action_userabort(TIMEOUT_NOBLOCK)) + { + tagcache_search_finish(&tcs); + return false; + } + int extraseek = tagtree_get_entry(tc, selected_item + i)->extraseek; + + if (!tagcache_retrieve(&tcs, extraseek, tcs.type, buf, sizeof buf)) + { + tagcache_search_finish(&tcs); + return false; + } + + if (context_to_use != NULL) + { + if (playlist_insert_context_add(context_to_use, buf) < 0) + { + tagcache_search_finish(&tcs); + return false; + } + } + else if (playlist != NULL) + { + catalog_insert_into(playlist, new_playlist, buf, FILE_ATTR_AUDIO, NULL); + new_playlist = false; + } + else + playlist_insert_track(NULL, buf, position, queue, true); + } + tagcache_search_finish(&tcs); + return (selected_size == 1); } ret = goto_allsubentries(newtable); @@ -2641,7 +2676,7 @@ static bool tagtree_insert_selection(int position, bool queue, { if (tc->filesindir <= 0) splash(HZ, ID2P(LANG_END_PLAYLIST)); - else if (!insert_all_playlist(tc, playlist, new_playlist, position, queue)) + else if (!insert_all_playlist(tc, playlist, new_playlist, position, queue, context_to_use)) splash(HZ*2, ID2P(LANG_FAILED)); } @@ -2723,7 +2758,7 @@ bool tagtree_get_subentry_filename(char *buf, size_t bufsize) bool tagtree_current_playlist_insert(int position, bool queue) { - return tagtree_insert_selection(position, queue, NULL, false); + return tagtree_insert_selection(position, queue, NULL, false, NULL, 1); } @@ -2731,7 +2766,7 @@ int tagtree_add_to_playlist(const char* playlist, bool new_playlist) { if (!new_playlist) tagtree_load(tc); /* because display_playlists was called */ - return tagtree_insert_selection(0, false, playlist, new_playlist) ? 0 : -1; + return tagtree_insert_selection(0, false, playlist, new_playlist, NULL, 1) ? 0 : -1; } static int tagtree_play_folder(struct tree_context* c) @@ -2745,7 +2780,7 @@ static int tagtree_play_folder(struct tree_context* c) return -1; } - if (!insert_all_playlist(c, NULL, false, PLAYLIST_INSERT_LAST, false)) + if (!insert_all_playlist(c, NULL, false, PLAYLIST_INSERT_LAST, false, NULL)) return -2; int n = c->filesindir; diff --git a/apps/tagtree.h b/apps/tagtree.h index a57a5c2f80..80dd71d58d 100644 --- a/apps/tagtree.h +++ b/apps/tagtree.h @@ -25,6 +25,7 @@ #include "config.h" #include "tagcache.h" #include "tree.h" +#include "playlist.h" #define TAGNAVI_VERSION "#! rockbox/tagbrowser/2.0" #define TAGMENU_MAX_ITEMS 64 @@ -48,6 +49,7 @@ int tagtree_get_filename(struct tree_context* c, char *buf, int buflen); int tagtree_get_custom_action(struct tree_context* c); bool tagtree_get_subentry_filename(char *buf, size_t bufsize); bool tagtree_subentries_do_action(bool (*action_cb)(const char *file_name)); +bool tagtree_insert_selection(int position, bool queue, const char* playlist, bool new_playlist, struct playlist_insert_context* context_to_use, int selected_size); #endif #endif diff --git a/apps/tree.c b/apps/tree.c index 5df204bbf2..f63c34f047 100644 --- a/apps/tree.c +++ b/apps/tree.c @@ -394,6 +394,28 @@ static int tree_get_file_position(char * filename) return(ret); } +static void set_multiselection_selected_size(void) +{ + if (tc.multiselection_enabled) + { + int offsetdiff = tc.selected_item - tc.multiselection_offset_item; + if (offsetdiff < 0) + { + tc.multiselection_selected_size = (- offsetdiff) + 1; + } + else if (offsetdiff == 0) + { + tc.multiselection_selected_size = 1; + } + else + { + tc.multiselection_selected_size = - (offsetdiff); + } + } + else + tc.multiselection_selected_size = 1; +} + /* * Called when a new dir is loaded (for example when returning from other apps ...) * also completely redraws the tree @@ -461,7 +483,7 @@ static int update_dir(void) } } - gui_synclist_init(list, &tree_get_filename, &tc, false, 1, NULL); + gui_synclist_init(list, &tree_get_filename, &tc, false, 1, NULL, true); #ifdef HAVE_TAGCACHE if (id3db) @@ -526,8 +548,10 @@ static int update_dir(void) gui_synclist_set_color_callback(list, &tree_get_filecolor); #endif if( tc.selected_item >= tc.filesindir) - tc.selected_item=tc.filesindir-1; + tc.selected_item = tc.filesindir-1; + set_multiselection_selected_size(); + list->selected_size = tc.multiselection_selected_size; gui_synclist_select_item(list, tc.selected_item); gui_synclist_draw(list); gui_synclist_speak_item(list); @@ -702,6 +726,79 @@ static void set_current_file_ex(const char *path, const char *filename) } } +static bool tree_insert_selection_is_dir_recurse(void) +{ + bool recurse = (global_settings.recursive_dir_insert == RECURSE_ON); + if (global_settings.recursive_dir_insert == RECURSE_ASK) + { + + const char *lines[] = { + ID2P(LANG_RECURSE_DIRECTORY_QUESTION), + ID2P(LANG_ALL_FOLDERS_FROM_CURRENT_SELECTION) + }; + const struct text_message message={lines, 2}; + /* Ask if user wants to recurse directory */ + recurse = (gui_syncyesno_run(&message, NULL, NULL)==YESNO_YES); + } + return recurse == RECURSE_ON; +} + +static bool tree_insert_selection(int position, bool queue, + const char* playlist, bool new_playlist, + struct playlist_insert_context* context_to_use, char* buf, size_t bufsize, char* currdir, + bool* dir_recurse_asked, bool* dir_recurse, int selected_size) +{ + bool result = false; + #ifdef HAVE_TAGCACHE + bool id3db = *tc.dirfilter == SHOW_ID3DB; + if (id3db) + { + result = tagtree_insert_selection(position, queue, playlist, + new_playlist, context_to_use, selected_size); + } + else + #else + (void) selected_size; + #endif + { + struct entry *entry = get_valid_entry(__func__, &tc, tc.selected_item); + ft_assemble_path(buf, bufsize, currdir, entry->name); + if ((entry->attr & FILE_ATTR_MASK) == FILE_ATTR_AUDIO) + { + if (context_to_use == NULL) + result = catalog_insert_into(playlist, new_playlist, buf, FILE_ATTR_AUDIO, NULL) >= 0; + else + result = playlist_insert_context_add(context_to_use, buf) >= 0; + } + else if ((entry->attr & FILE_ATTR_MASK) == FILE_ATTR_M3U) + { + if (context_to_use == NULL) + result = catalog_insert_into(playlist, new_playlist, buf, FILE_ATTR_M3U, NULL) >= 0; + else + result = playlist_entries_iterate(buf, context_to_use, NULL); + } + else if (entry->attr & ATTR_DIRECTORY) + { + if (!(*dir_recurse_asked)) + { + /* We ask the user only once for all the selection */ + *dir_recurse = tree_insert_selection_is_dir_recurse(); + *dir_recurse_asked = true; + } + if (context_to_use == NULL) + result = catalog_insert_into(playlist, new_playlist, buf, ATTR_DIRECTORY, dir_recurse) >= 0; + else + result = playlist_insert_directory(NULL, buf, position, queue, *dir_recurse, context_to_use) >= 0; + if (ft_load(&tc, currdir) < 0) + { + /* Reload current dir */ + result = -1; + } + } + } + return result; +} + /* Selects a file and update tree context properly */ void set_current_file(const char *path) { @@ -711,6 +808,7 @@ void set_current_file(const char *path) static int exit_to_new_screen(int screen) { + tc.multiselection_enabled = false; gui_synclist_scroll_stop(&tree_lists); return screen; } @@ -766,7 +864,23 @@ static int dirbrowse(void) list_do_action_timeout(&tree_lists, HZ/2)); oldbutton = button; gui_synclist_do_button(&tree_lists, &button); - tc.selected_item = gui_synclist_get_sel_pos(&tree_lists); + struct gui_synclist * const list = &tree_lists; + bool list_redraw_needed = false; + if (tc.selected_item != gui_synclist_get_sel_pos(list)) + { + tc.selected_item = gui_synclist_get_sel_pos(list); + list_redraw_needed = true; + } + set_multiselection_selected_size(); + if (list->selected_size != tc.multiselection_selected_size) + { + list->selected_size = tc.multiselection_selected_size; + list_redraw_needed = true; + } + if (list_redraw_needed) + /* it's important to refresh conditionally only when something changed, + or all texts scrolling will be broken */ + gui_synclist_draw(list); int customaction = ONPLAY_NO_CUSTOMACTION; bool do_restore_display = true; #ifdef HAVE_TAGCACHE @@ -781,11 +895,18 @@ static int dirbrowse(void) } } #endif + if (button == ACTION_STD_OK && tc.multiselection_selected_size != 1) + { + button = ACTION_STD_CONTEXT; + } switch ( button ) { case ACTION_STD_OK: /* nothing to do if no files to display */ if ( numentries == 0 ) break; + if (tc.multiselection_enabled && tc.multiselection_selected_size != 1) + break; + tc.multiselection_enabled = false; if (tc.browse->flags & BROWSE_SELECTONLY) { struct entry *entry = @@ -821,6 +942,12 @@ static int dirbrowse(void) break; case ACTION_STD_CANCEL: + if (tc.multiselection_enabled) + { + tc.multiselection_enabled = false; + restore = do_restore_display; + break; + } exit_to_new_screen(0); if (*tc.dirfilter > NUM_FILTER_MODES && tc.dirlevel < 1) { exit_func = true; @@ -912,12 +1039,13 @@ static int dirbrowse(void) bool hotkey = button == ACTION_TREE_HOTKEY; int onplay_result; int attr = 0; + size_t bufsize; if (tc.browse->flags & BROWSE_NO_CONTEXT_MENU) break; if(!numentries) - onplay_result = onplay(NULL, 0, curr_context, hotkey, customaction); + onplay_result = onplay(NULL, 0, curr_context, hotkey, customaction, false); else { #ifdef HAVE_TAGCACHE if (id3db) @@ -959,15 +1087,101 @@ static int dirbrowse(void) else #endif { - struct entry *entry = - get_valid_entry(__func__, &tc, tc.selected_item); - + struct entry *entry = get_valid_entry(__func__, &tc, tc.selected_item); attr = entry->attr; - - ft_assemble_path(buf, sizeof(buf), currdir, entry->name); - + bufsize = sizeof(buf); + ft_assemble_path(buf, bufsize, currdir, entry->name); + } + if (tc.multiselection_selected_size != 1) + attr |= FILE_ATTR_MULTISELECTION; + onplay_result = onplay(buf, attr, curr_context, hotkey, customaction, true); + struct add_to_pl_param *choice = onplay_show_playlist_menu_get_current_choice(); + char* cat_menu_choice = onplay_show_playlist_cat_menu_get_current_choice(); + int initial_selected_item = tc.selected_item; + int selected_item = tc.selected_item; + int selected_size = tc.multiselection_selected_size; + if (selected_size < 0) + { + selected_item -= (- selected_size); + selected_size = ((- selected_size) + 1); + } + struct playlist_insert_context pl_context; + bool new_playlist; + bool dir_recurse_asked = false; + bool dir_recurse = false; + if (choice != NULL) + { + bool queue = (choice->flags & PL_QUEUE) == PL_QUEUE; + new_playlist = (choice->flags & PL_REPLACE) == PL_REPLACE; + if (new_playlist && global_settings.keep_current_track_on_replace_playlist) + { + if (audio_status() & AUDIO_STATUS_PLAY) + { + playlist_remove_all_tracks(NULL); + new_playlist = false; + } + } + if (new_playlist) + { + playlist_create(NULL, NULL); + } + if (playlist_insert_context_create(NULL, &pl_context, choice->position, queue, true) >= 0) + { + for (int i = 0; i < selected_size; i++) + { + if (i > 0 && action_userabort(TIMEOUT_NOBLOCK)) + break; + tc.selected_item = selected_item + i; + if (!tree_insert_selection(choice->position, queue, NULL, + new_playlist, &pl_context, buf, bufsize, currdir, + &dir_recurse_asked, &dir_recurse, selected_size)) + { + break; + } + } + tc.selected_item = initial_selected_item; + } + playlist_insert_context_release(&pl_context); + if (new_playlist && (playlist_amount() > 0)) + { + if (global_settings.playlist_shuffle) + playlist_shuffle(current_tick, -1); + playlist_start(0, 0, 0); + onplay_result = ONPLAY_START_PLAY; + } + tc.multiselection_enabled = false; + } + else if (cat_menu_choice != NULL) + { + new_playlist = onplay_show_playlist_cat_menu_current_choice_is_new_playlist(); + #ifdef HAVE_TAGCACHE + if (id3db) + { + if (!new_playlist) + tagtree_load(&tc); /* because display_playlists was called */ + } + else + #endif + { + if (!new_playlist) + ft_load(&tc, currdir); /* because display_playlists was called */ + } + for (int i = 0; i < selected_size; i++) + { + if (i > 0 && action_userabort(TIMEOUT_NOBLOCK)) + break; + tc.selected_item = selected_item + i; + if (!tree_insert_selection(PLAYLIST_INSERT_LAST, false, cat_menu_choice, + new_playlist, NULL, buf, bufsize, currdir, + &dir_recurse_asked, &dir_recurse, selected_size)) + { + break; + } + new_playlist = false; + } + tc.selected_item = initial_selected_item; + tc.multiselection_enabled = false; } - onplay_result = onplay(buf, attr, curr_context, hotkey, customaction); } switch (onplay_result) { @@ -983,6 +1197,19 @@ static int dirbrowse(void) reload_dir = true; break; + case ONPLAY_MULTISELECT: + restore = do_restore_display; + if (!tc.multiselection_enabled) + { + tc.multiselection_enabled = true; + tc.multiselection_offset_item = tc.selected_item; + } + else + { + tc.multiselection_enabled = false; + } + break; + case ONPLAY_START_PLAY: return exit_to_new_screen(GO_TO_WPS); break; @@ -1045,9 +1272,12 @@ static int dirbrowse(void) if (!reload_dir) { + tc.multiselection_enabled = false; + tc.selected_item = 0; + set_multiselection_selected_size(); + list->selected_size = tc.multiselection_selected_size; gui_synclist_select_item(&tree_lists, 0); gui_synclist_draw(&tree_lists); - tc.selected_item = 0; lastdir[0] = 0; } @@ -1107,6 +1337,8 @@ int rockbox_browse(struct browse_context *browse) backups[backup_count] = tc; backup_count++; int *prev_dirfilter = tc.dirfilter; + tc.multiselection_enabled = false; + tc.multiselection_selected_size = 1; tc.dirfilter = &dirfilter; tc.sort_dir = global_settings.sort_dir; diff --git a/apps/tree.h b/apps/tree.h index 4c7c2e8fe7..63b19eb4c9 100644 --- a/apps/tree.h +++ b/apps/tree.h @@ -102,6 +102,10 @@ struct tree_context { bool is_browsing; /* valid browse context? */ struct browse_context *browse; + + bool multiselection_enabled; + int multiselection_offset_item; + int multiselection_selected_size; }; /* diff --git a/manual/working_with_playlists/main.tex b/manual/working_with_playlists/main.tex index bdb595b19c..d992921dd5 100644 --- a/manual/working_with_playlists/main.tex +++ b/manual/working_with_playlists/main.tex @@ -144,3 +144,17 @@ \subsubsection{Through \setting{Playlists}} It can be used like the \setting{File Browser} but will display the content of a playlist when one is selected. +\subsection{Multiselection} +This option is available through the \setting{File Browser}, the \setting{Database Browser}, +and also through the \setting{Playlist catalogue} to make playlists management way more flexible, +should you ever need to move/add or delete complete CDs/albums (or part of them). + +You can enable \setting{Multiselection} from the \setting{Context Menu} to perform certain +actions to multiple files in a row. +To cancel multiselection, select the setting again or press the \ActionWpsSkipPrev{} button. + +\note{Once you are in the multiselection state, navigating on the list + will automatically select all items relative to the initially selected element. + To perform your desired action in mass, just open a \setting{Context Menu} + when all your desired entries are selected according to your needs.} +