Skip to content

Commit 7b2b0cf

Browse files
[#104] UI: Middle-click
1 parent b5bbb11 commit 7b2b0cf

3 files changed

Lines changed: 248 additions & 26 deletions

File tree

doc/man/hrmp-ui.1.rst

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ along with a playlist view and an output log window.
2323
The application launches the hrmp command-line player in the background and
2424
controls it via the keyboard commands that hrmp understands.
2525

26+
The playlist view supports double-click to start playback from a row, hover
27+
tooltips that reveal the full file path, middle-click to replace that row with
28+
supported files from the same directory sorted by filename without duplicating
29+
entries already present elsewhere in the playlist, and right-click to remove a
30+
row.
31+
2632
CONTROLS
2733
========
2834

@@ -59,11 +65,18 @@ Play mode
5965
Menu items
6066
----------
6167

62-
File → Add
63-
Add audio files to the playlist.
68+
File → Search
69+
Open a dialog for choosing a directory and filtering supported files with a
70+
regular expression. An empty search matches all supported files recursively
71+
below the selected directory. Double-clicking a result adds it to the
72+
playlist. The dialog remembers the last selected directory for the current
73+
hrmp-ui process.
74+
75+
File → Load
76+
Load a playlist file (``*.hrmp``) into the playlist view.
6477

65-
File → Clear
66-
Clear the playlist and stop any running hrmp instance.
78+
File → Save
79+
Save the current playlist view to a playlist file (``*.hrmp``).
6780

6881
File → Quit
6982
Quit hrmp-ui.

doc/manual/06-ui.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ You can add one file or multiple files at once, and the order shown reflects the
7878
You can select how the file names are presented in the preference window - either the full path or just the basename.
7979

8080
- **Double-click** a file to stop any existing playback (if necessary) and start playback from the file you double-clicked.
81+
- **Hover** a file to show its full path in a tooltip, even when the playlist is configured to show only basenames.
82+
- **Middle-click** a file to remove that row and replace it with the supported files from the same directory, sorted by filename. Files already present elsewhere in the playlist are not added again, and the clicked file participates in the sort order.
8183
- **Right-click** a file to remove it from the playlist, unless it is the file that is currently playing, in which case the playlist is left unchanged.
8284

8385
## Status Panel
@@ -123,6 +125,12 @@ These shortcuts complement the hrmp keyboard controls and work regardless of whi
123125

124126
### File Menu
125127

128+
- **Search**
129+
- Opens a dialog for browsing a directory and filtering the files shown in a scrollable list.
130+
- The directory defaults to the directory where `hrmp-ui` was started, and afterwards reuses the last directory chosen in the Search dialog for the rest of the current app session.
131+
- The **Search** field accepts a regular expression. An empty field matches all supported files recursively below the selected directory.
132+
- Double-clicking a result appends that file to the playlist.
133+
126134
- **Load**
127135
- Opens a file chooser restricted to playlist files (`*.hrmp`).
128136
- Replaces the current playlist view with the contents of the selected `.hrmp` file.

src/ui.c

Lines changed: 223 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ static void on_menu_debug(GtkWidget* widget, gpointer user_data);
5656
static void on_debug_clear_clicked(GtkWidget* widget, gpointer user_data);
5757
static void on_debug_close_clicked(GtkWidget* widget, gpointer user_data);
5858
static gboolean hrmp_gtk_append_playlist_file(struct App* app, const gchar* filename);
59+
static gint hrmp_gtk_compare_paths_by_name(gconstpointer a, gconstpointer b);
60+
static void hrmp_gtk_add_directory_files_to_playlist(struct App* app,
61+
const gchar* clicked_filepath,
62+
gint clicked_position);
5963
static gboolean hrmp_gtk_collect_search_matches(const gchar* directory,
6064
const gchar* relative_path,
6165
GRegex* regex,
@@ -421,6 +425,193 @@ hrmp_gtk_append_playlist_file(struct App* app, const gchar* filename)
421425
return TRUE;
422426
}
423427

428+
static void
429+
hrmp_gtk_add_unique_playlist_path(GPtrArray* paths, GHashTable* seen, const gchar* filepath)
430+
{
431+
if (filepath == NULL || filepath[0] == '\0' || g_hash_table_contains(seen, filepath))
432+
{
433+
return;
434+
}
435+
436+
g_hash_table_add(seen, g_strdup(filepath));
437+
g_ptr_array_add(paths, g_strdup(filepath));
438+
}
439+
440+
static gint
441+
hrmp_gtk_compare_paths_by_name(gconstpointer a, gconstpointer b)
442+
{
443+
const gchar* left = *((const gchar* const*)a);
444+
const gchar* right = *((const gchar* const*)b);
445+
gchar* left_base = g_path_get_basename(left);
446+
gchar* right_base = g_path_get_basename(right);
447+
gchar* left_key = g_utf8_collate_key_for_filename(left_base, -1);
448+
gchar* right_key = g_utf8_collate_key_for_filename(right_base, -1);
449+
gint cmp = g_strcmp0(left_key, right_key);
450+
451+
if (cmp == 0)
452+
{
453+
cmp = g_strcmp0(left, right);
454+
}
455+
456+
g_free(left_key);
457+
g_free(right_key);
458+
g_free(left_base);
459+
g_free(right_base);
460+
461+
return cmp;
462+
}
463+
464+
static void
465+
hrmp_gtk_add_directory_files_to_playlist(struct App* app,
466+
const gchar* clicked_filepath,
467+
gint clicked_position)
468+
{
469+
GDir* dir = NULL;
470+
GError* error = NULL;
471+
const gchar* name = NULL;
472+
GPtrArray* files = NULL;
473+
GtkTreeModel* model = NULL;
474+
GtkTreeIter iter;
475+
gboolean valid = FALSE;
476+
gchar* directory = NULL;
477+
GPtrArray* remaining_playlist = NULL;
478+
GPtrArray* final_playlist = NULL;
479+
GHashTable* seen = NULL;
480+
gchar* selected_filepath = NULL;
481+
gint insertion_index = 0;
482+
gint row_index = 0;
483+
484+
if (app == NULL || clicked_filepath == NULL || clicked_filepath[0] == '\0')
485+
{
486+
return;
487+
}
488+
489+
directory = g_path_get_dirname(clicked_filepath);
490+
if (directory == NULL || directory[0] == '\0')
491+
{
492+
g_free(directory);
493+
return;
494+
}
495+
496+
dir = g_dir_open(directory, 0, &error);
497+
if (dir == NULL)
498+
{
499+
g_warning("Failed to open directory '%s': %s", directory, error->message);
500+
g_error_free(error);
501+
g_free(directory);
502+
return;
503+
}
504+
505+
model = GTK_TREE_MODEL(app->list_store);
506+
{
507+
GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(app->list_view));
508+
GtkTreeIter selected_iter;
509+
if (gtk_tree_selection_get_selected(selection, NULL, &selected_iter))
510+
{
511+
gtk_tree_model_get(model, &selected_iter, 0, &selected_filepath, -1);
512+
}
513+
}
514+
515+
remaining_playlist = g_ptr_array_new_with_free_func(g_free);
516+
final_playlist = g_ptr_array_new_with_free_func(g_free);
517+
seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
518+
519+
valid = gtk_tree_model_get_iter_first(model, &iter);
520+
while (valid)
521+
{
522+
gchar* filepath = NULL;
523+
gtk_tree_model_get(model, &iter, 0, &filepath, -1);
524+
if (filepath != NULL && filepath[0] != '\0')
525+
{
526+
if (!(row_index == clicked_position && g_strcmp0(filepath, clicked_filepath) == 0))
527+
{
528+
g_ptr_array_add(remaining_playlist, g_strdup(filepath));
529+
g_hash_table_add(seen, g_strdup(filepath));
530+
}
531+
}
532+
g_free(filepath);
533+
row_index++;
534+
valid = gtk_tree_model_iter_next(model, &iter);
535+
}
536+
537+
files = g_ptr_array_new_with_free_func(g_free);
538+
while ((name = g_dir_read_name(dir)) != NULL)
539+
{
540+
gchar* full_path = g_build_filename(directory, name, NULL);
541+
542+
if (!g_file_test(full_path, G_FILE_TEST_IS_REGULAR) ||
543+
!hrmp_file_is_supported((char*)full_path))
544+
{
545+
g_free(full_path);
546+
continue;
547+
}
548+
549+
g_ptr_array_add(files, full_path);
550+
}
551+
552+
g_ptr_array_sort(files, hrmp_gtk_compare_paths_by_name);
553+
554+
insertion_index = clicked_position;
555+
if (insertion_index < 0)
556+
{
557+
insertion_index = 0;
558+
}
559+
if (insertion_index > (gint)remaining_playlist->len)
560+
{
561+
insertion_index = (gint)remaining_playlist->len;
562+
}
563+
564+
for (gint i = 0; i < insertion_index; ++i)
565+
{
566+
g_ptr_array_add(final_playlist, g_strdup(g_ptr_array_index(remaining_playlist, i)));
567+
}
568+
569+
for (guint i = 0; i < files->len; ++i)
570+
{
571+
hrmp_gtk_add_unique_playlist_path(final_playlist, seen, g_ptr_array_index(files, i));
572+
}
573+
574+
for (guint i = (guint)insertion_index; i < remaining_playlist->len; ++i)
575+
{
576+
g_ptr_array_add(final_playlist, g_strdup(g_ptr_array_index(remaining_playlist, i)));
577+
}
578+
579+
gtk_list_store_clear(app->list_store);
580+
for (guint i = 0; i < final_playlist->len; ++i)
581+
{
582+
hrmp_gtk_append_playlist_file(app, g_ptr_array_index(final_playlist, i));
583+
}
584+
585+
if (selected_filepath != NULL)
586+
{
587+
GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(app->list_view));
588+
589+
valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(app->list_store), &iter);
590+
while (valid)
591+
{
592+
gchar* filepath = NULL;
593+
gtk_tree_model_get(GTK_TREE_MODEL(app->list_store), &iter, 0, &filepath, -1);
594+
if (filepath != NULL && g_strcmp0(filepath, selected_filepath) == 0)
595+
{
596+
gtk_tree_selection_unselect_all(selection);
597+
gtk_tree_selection_select_iter(selection, &iter);
598+
g_free(filepath);
599+
break;
600+
}
601+
g_free(filepath);
602+
valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(app->list_store), &iter);
603+
}
604+
}
605+
606+
g_free(selected_filepath);
607+
g_hash_table_destroy(seen);
608+
g_ptr_array_free(remaining_playlist, TRUE);
609+
g_ptr_array_free(final_playlist, TRUE);
610+
g_ptr_array_free(files, TRUE);
611+
g_dir_close(dir);
612+
g_free(directory);
613+
}
614+
424615
static void
425616
hrmp_gtk_add_search_result(struct SearchDialog* search,
426617
const gchar* full_path,
@@ -3114,29 +3305,39 @@ static gboolean
31143305
on_playlist_button_press(GtkWidget* widget, GdkEventButton* event, gpointer user_data)
31153306
{
31163307
struct App* app = user_data;
3308+
GtkTreeView* tree_view = GTK_TREE_VIEW(widget);
3309+
GtkTreePath* path = NULL;
3310+
GtkTreeViewColumn* column = NULL;
31173311

3118-
if (event->type == GDK_BUTTON_PRESS && event->button == 3)
3119-
{
3120-
GtkTreeView* tree_view = GTK_TREE_VIEW(widget);
3121-
GtkTreePath* path = NULL;
3122-
GtkTreeViewColumn* column = NULL;
3312+
if (event->type == GDK_BUTTON_PRESS &&
3313+
(event->button == 2 || event->button == 3) &&
3314+
gtk_tree_view_get_path_at_pos(tree_view,
3315+
(gint)event->x,
3316+
(gint)event->y,
3317+
&path,
3318+
&column,
3319+
NULL,
3320+
NULL))
3321+
{
3322+
GtkTreeModel* model = gtk_tree_view_get_model(tree_view);
3323+
GtkTreeIter iter;
31233324

3124-
if (gtk_tree_view_get_path_at_pos(tree_view,
3125-
(gint)event->x,
3126-
(gint)event->y,
3127-
&path,
3128-
&column,
3129-
NULL,
3130-
NULL))
3325+
if (gtk_tree_model_get_iter(model, &iter, path))
31313326
{
3132-
GtkTreeModel* model = gtk_tree_view_get_model(tree_view);
3133-
GtkTreeIter iter;
3327+
gchar* filepath = NULL;
3328+
gtk_tree_model_get(model, &iter, 0, &filepath, -1);
31343329

3135-
if (gtk_tree_model_get_iter(model, &iter, path))
3330+
if (event->button == 2)
3331+
{
3332+
if (filepath != NULL && filepath[0] != '\0')
3333+
{
3334+
gint* indices = gtk_tree_path_get_indices(path);
3335+
gint clicked_position = (indices != NULL) ? indices[0] : 0;
3336+
hrmp_gtk_add_directory_files_to_playlist(app, filepath, clicked_position);
3337+
}
3338+
}
3339+
else
31363340
{
3137-
gchar* filepath = NULL;
3138-
gtk_tree_model_get(model, &iter, 0, &filepath, -1);
3139-
31403341
/* Do not remove the row corresponding to the currently playing file: treat the
31413342
* currently selected row as "playing" when hrmp is running. */
31423343
GtkTreeSelection* selection = gtk_tree_view_get_selection(tree_view);
@@ -3157,12 +3358,12 @@ on_playlist_button_press(GtkWidget* widget, GdkEventButton* event, gpointer user
31573358
{
31583359
gtk_tree_path_free(cur_path);
31593360
}
3160-
g_free(filepath);
31613361
}
3162-
3163-
gtk_tree_path_free(path);
3164-
return TRUE;
3362+
g_free(filepath);
31653363
}
3364+
3365+
gtk_tree_path_free(path);
3366+
return TRUE;
31663367
}
31673368

31683369
return FALSE;

0 commit comments

Comments
 (0)