Skip to content

Commit 353550d

Browse files
playback-history: Implement a GTK plugin. Closes: #1596
Based on the initial port of Jim Turner but cleaned-up, refactored to share most common code with the Qt plugin and with tooltip support.
1 parent 214bb9f commit 353550d

File tree

6 files changed

+328
-1
lines changed

6 files changed

+328
-1
lines changed

configure.ac

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ TRANSPORT_PLUGINS="gio"
8383

8484
if test "x$USE_GTK" = "xyes" ; then
8585
EFFECT_PLUGINS="$EFFECT_PLUGINS ladspa"
86-
GENERAL_PLUGINS="$GENERAL_PLUGINS albumart filebrowser lyrics-gtk playlist-manager search-tool statusicon"
86+
GENERAL_PLUGINS="$GENERAL_PLUGINS albumart filebrowser lyrics-gtk playback-history playlist-manager search-tool statusicon"
8787
GENERAL_PLUGINS="$GENERAL_PLUGINS gtkui skins"
8888
VISUALIZATION_PLUGINS="$VISUALIZATION_PLUGINS blur_scope cairo-spectrum vumeter"
8989
fi
@@ -884,6 +884,7 @@ if test "x$USE_GTK" = "xyes" ; then
884884
echo " Blur Scope: yes"
885885
echo " File Browser: yes"
886886
echo " OpenGL Spectrum Analyzer: $have_glspectrum"
887+
echo " Playback History: yes"
887888
echo " Playlist Manager: yes"
888889
echo " Search Tool: yes"
889890
echo " Spectrum Analyzer (2D): yes"

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ if meson.version().version_compare('>= 0.53')
340340
'Blur Scope': true,
341341
'File Browser': true,
342342
'OpenGL Spectrum Analyzer': get_variable('have_glspectrum', false),
343+
'Playback History': true,
343344
'Playlist Manager': true,
344345
'Search Tool': true,
345346
'Spectrum Analyzer (2D)': true,

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ if conf.has('USE_GTK')
128128
subdir('gtkui')
129129
subdir('ladspa')
130130
subdir('lyrics-gtk')
131+
subdir('playback-history')
131132
subdir('playlist-manager')
132133
subdir('search-tool')
133134
subdir('skins')

src/playback-history/Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
PLUGIN = playback-history${PLUGIN_SUFFIX}
2+
3+
SRCS = history-entry.cc \
4+
playback-history.cc
5+
6+
include ../../buildsys.mk
7+
include ../../extra.mk
8+
9+
plugindir := ${plugindir}/${GENERAL_PLUGIN_DIR}
10+
11+
LD = ${CXX}
12+
CPPFLAGS += -I../.. ${GTK_CFLAGS}
13+
CFLAGS += ${PLUGIN_CFLAGS}
14+
LIBS += ${GTK_LIBS} -laudgui

src/playback-history/meson.build

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
shared_module('playback-history',
2+
'history-entry.cc',
3+
'playback-history.cc',
4+
dependencies: [audacious_dep, audgui_dep, gtk_dep],
5+
name_prefix: '',
6+
install: true,
7+
install_dir: general_plugin_dir,
8+
)
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
* playback-history.cc
3+
*
4+
* Copyright (C) 2023-2024 Igor Kushnir <igorkuo@gmail.com>
5+
* Copyright (C) 2025 Jim Turner <turnerjw784@yahoo.com> (GTK version)
6+
* Copyright (C) 2026 Thomas Lange <thomas-lange2@gmx.de> (GTK version)
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License,
11+
* or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
22+
#include <cstring>
23+
#include <utility>
24+
25+
#include <gdk/gdkkeysyms.h>
26+
#include <gtk/gtk.h>
27+
28+
#define AUD_GLIB_INTEGRATION
29+
#include <libaudcore/audstrings.h>
30+
#include <libaudcore/hook.h>
31+
#include <libaudcore/i18n.h>
32+
#include <libaudcore/index.h>
33+
#include <libaudcore/objects.h>
34+
#include <libaudcore/plugin.h>
35+
#include <libaudcore/preferences.h>
36+
#include <libaudcore/runtime.h>
37+
#include <libaudgui/gtk-compat.h>
38+
#include <libaudgui/list.h>
39+
40+
#include "history-entry.h"
41+
#include "preferences.h"
42+
43+
class PlaybackHistory : public GeneralPlugin
44+
{
45+
private:
46+
static constexpr const char * about = aboutText;
47+
static const PluginPreferences prefs;
48+
49+
public:
50+
static constexpr PluginInfo info = {N_("Playback History"), PACKAGE,
51+
about, &prefs, PluginGLibOnly};
52+
53+
constexpr PlaybackHistory() : GeneralPlugin(info, false) {}
54+
55+
bool init() override;
56+
void * get_gtk_widget() override;
57+
int take_message(const char * code, const void *, int) override;
58+
};
59+
60+
EXPORT PlaybackHistory aud_plugin_instance;
61+
62+
const PluginPreferences PlaybackHistory::prefs = {{widgets}};
63+
64+
static GtkWidget * m_treeview = nullptr;
65+
static Index<HistoryEntry> m_entries;
66+
static int m_playing_position = -1;
67+
68+
static int pos_to_row(int position)
69+
{
70+
return m_entries.len() - 1 - position;
71+
}
72+
73+
static int row_to_pos(int row)
74+
{
75+
return pos_to_row(row); // self-inverse function
76+
}
77+
78+
static void get_value(void * user, int row, int column, GValue * value)
79+
{
80+
g_return_if_fail(row >= 0 && row < m_entries.len());
81+
g_return_if_fail(column == 0);
82+
83+
int pos = row_to_pos(row);
84+
HistoryEntry & entry = m_entries[pos];
85+
g_value_set_string(value, entry.text());
86+
}
87+
88+
static bool get_selected(void * user, int row)
89+
{
90+
return false; // true causes new entries to be highlighted ("selected")
91+
}
92+
93+
static void set_selected(void * user, int row, bool selected)
94+
{
95+
g_return_if_fail(row >= 0 && row < m_entries.len());
96+
97+
if (selected)
98+
m_entries[row_to_pos(row)].makeCurrent();
99+
}
100+
101+
static void select_all(void * user, bool selected)
102+
{
103+
// Required for set_selected() to work
104+
}
105+
106+
static void activate_row(void * user, int row)
107+
{
108+
g_return_if_fail(row >= 0 && row < m_entries.len());
109+
110+
int pos = row_to_pos(row);
111+
if (!m_entries[pos].play())
112+
return;
113+
114+
// Update m_playing_position here to prevent the imminent playback_started()
115+
// invocation from appending a copy of the activated entry to m_entries.
116+
// This does not prevent appending a different-type counterpart entry if the
117+
// type of the activated entry does not match the currently configured
118+
// History Item Granularity. Such a scenario is uncommon (happens only when
119+
// the user switches between the History modes), and so is not specially
120+
// handled or optimized for.
121+
if (m_playing_position != pos)
122+
{
123+
m_playing_position = pos;
124+
int highlight_row = pos_to_row(m_playing_position);
125+
audgui_list_set_highlight(m_treeview, highlight_row);
126+
audgui_list_set_focus(m_treeview, highlight_row);
127+
}
128+
}
129+
130+
static void remove_selected_rows()
131+
{
132+
GtkTreeModel * model;
133+
GtkTreeView * treeview = (GtkTreeView *)m_treeview;
134+
GtkTreeSelection * selection = gtk_tree_view_get_selection(treeview);
135+
GList * rows = gtk_tree_selection_get_selected_rows(selection, &model);
136+
137+
for (GList * l = g_list_last(rows); l; l = l->prev)
138+
{
139+
GtkTreeIter iter;
140+
GtkTreePath * path = (GtkTreePath *)l->data;
141+
142+
if (!gtk_tree_model_get_iter(model, &iter, path))
143+
continue;
144+
145+
int row = gtk_tree_path_get_indices(path)[0];
146+
int pos = row_to_pos(row);
147+
148+
if (pos == m_playing_position)
149+
m_playing_position = -1;
150+
else if (m_playing_position > pos)
151+
m_playing_position--;
152+
153+
audgui_list_delete_rows(m_treeview, row, 1);
154+
m_entries.remove(pos, 1);
155+
}
156+
157+
g_list_free_full(rows, (GDestroyNotify)gtk_tree_path_free);
158+
159+
int n_entries = m_entries.len();
160+
if (m_playing_position >= n_entries)
161+
m_playing_position = n_entries - 1;
162+
163+
if (m_playing_position >= 0)
164+
{
165+
int highlight_row = pos_to_row(m_playing_position);
166+
audgui_list_set_highlight(m_treeview, highlight_row);
167+
audgui_list_set_focus(m_treeview, highlight_row);
168+
}
169+
}
170+
171+
static void playback_started()
172+
{
173+
HistoryEntry entry;
174+
if (!entry.assignPlayingEntry())
175+
return;
176+
177+
int n_entries = m_entries.len();
178+
entry.debugPrint("Started playing ");
179+
AUDDBG("playing position=%d, entry count=%d\n",
180+
m_playing_position, n_entries);
181+
182+
if (m_playing_position >= 0 && m_playing_position < n_entries)
183+
{
184+
HistoryEntry & prev_playing_entry = m_entries[m_playing_position];
185+
if (!entry.shouldAppendEntry(prev_playing_entry))
186+
return;
187+
}
188+
189+
m_playing_position = n_entries;
190+
m_entries.append(std::move(entry));
191+
192+
// The last played entry appears at the top of the view.
193+
// Therefore, the new entry is inserted at row 0.
194+
audgui_list_insert_rows(m_treeview, 0, 1);
195+
audgui_list_set_highlight(m_treeview, 0);
196+
audgui_list_set_focus(m_treeview, 0);
197+
}
198+
199+
static char * escape_text_safe(const char * text)
200+
{
201+
return text ? g_markup_escape_text(text, -1) : g_strdup(text);
202+
}
203+
204+
static gboolean tooltip_cb(GtkWidget * widget, int x, int y,
205+
gboolean keyboard_mode, GtkTooltip * tooltip,
206+
void * data)
207+
{
208+
int row = audgui_list_row_at_point(widget, x, y);
209+
if (row < 0)
210+
return false;
211+
212+
int pos = row_to_pos(row);
213+
HistoryEntry & entry = m_entries[pos];
214+
215+
CharPtr type(escape_text_safe(entry.translatedTextDesignation()));
216+
CharPtr title(escape_text_safe(entry.text()));
217+
CharPtr playlist(escape_text_safe(entry.playlistTitle()));
218+
219+
StringBuf markup = str_printf(
220+
_("<b>%s:</b> %s\n"
221+
"<b>Playlist:</b> %s\n"
222+
"<b>Entry Number:</b> %d"),
223+
static_cast<const char *>(type), static_cast<const char *>(title),
224+
static_cast<const char *>(playlist), entry.entryNumber());
225+
226+
gtk_tooltip_set_markup(tooltip, markup);
227+
return true;
228+
}
229+
230+
static gboolean keypress_cb(GtkWidget * widget, GdkEventKey * event)
231+
{
232+
if ((event->state & GDK_CONTROL_MASK) == 0 &&
233+
event->keyval == GDK_KEY_Delete)
234+
{
235+
remove_selected_rows();
236+
return true;
237+
}
238+
239+
return false;
240+
}
241+
242+
static const AudguiListCallbacks callbacks = {
243+
get_value,
244+
get_selected,
245+
set_selected,
246+
select_all,
247+
activate_row
248+
};
249+
250+
static GtkWidget * build_widget()
251+
{
252+
m_treeview = audgui_list_new(&callbacks, nullptr, 0);
253+
audgui_list_add_column(m_treeview, nullptr, 0, G_TYPE_STRING, -1);
254+
gtk_widget_set_has_tooltip(m_treeview, true);
255+
gtk_tree_view_set_headers_visible((GtkTreeView *)m_treeview, false);
256+
gtk_tree_view_set_fixed_height_mode((GtkTreeView *)m_treeview, true);
257+
g_signal_connect(m_treeview, "query-tooltip", (GCallback)tooltip_cb, nullptr);
258+
g_signal_connect(m_treeview, "key-press-event", (GCallback)keypress_cb, nullptr);
259+
260+
GtkWidget * scrollview = gtk_scrolled_window_new(nullptr, nullptr);
261+
gtk_scrolled_window_set_shadow_type((GtkScrolledWindow *)scrollview, GTK_SHADOW_IN);
262+
gtk_scrolled_window_set_policy((GtkScrolledWindow *)scrollview,
263+
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
264+
gtk_container_add((GtkContainer *)scrollview, m_treeview);
265+
266+
GtkWidget * vbox = audgui_vbox_new(6);
267+
gtk_box_pack_start((GtkBox *)vbox, scrollview, true, true, 0);
268+
return vbox;
269+
}
270+
271+
static void destroy_cb()
272+
{
273+
hook_dissociate("playback ready", (HookFunction)playback_started);
274+
m_entries.clear();
275+
m_playing_position = -1;
276+
m_treeview = nullptr;
277+
}
278+
279+
bool PlaybackHistory::init()
280+
{
281+
aud_config_set_defaults(configSection, defaults);
282+
return true;
283+
}
284+
285+
void * PlaybackHistory::get_gtk_widget()
286+
{
287+
GtkWidget * vbox = build_widget();
288+
hook_associate("playback ready", (HookFunction)playback_started, nullptr);
289+
g_signal_connect(vbox, "destroy", (GCallback)destroy_cb, nullptr);
290+
return vbox;
291+
}
292+
293+
int PlaybackHistory::take_message(const char * code, const void *, int)
294+
{
295+
if (!strcmp(code, "grab focus") && m_treeview)
296+
{
297+
gtk_widget_grab_focus(m_treeview);
298+
return 0;
299+
}
300+
301+
return -1;
302+
}

0 commit comments

Comments
 (0)