summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/audacious/dbus-server.cc17
-rw-r--r--src/audacious/main.cc2
-rw-r--r--src/audtool/audtool.h6
-rw-r--r--src/audtool/handlers_general.c50
-rw-r--r--src/audtool/main.c2
-rw-r--r--src/dbus/aud-dbus.xml12
-rw-r--r--src/libaudcore/Makefile14
-rw-r--r--src/libaudcore/adder.cc86
-rw-r--r--src/libaudcore/audstrings.cc112
-rw-r--r--src/libaudcore/audstrings.h6
-rw-r--r--src/libaudcore/charset.cc4
-rw-r--r--src/libaudcore/config.cc26
-rw-r--r--src/libaudcore/equalizer.cc14
-rw-r--r--src/libaudcore/history.cc9
-rw-r--r--src/libaudcore/index.h13
-rw-r--r--src/libaudcore/interface.cc4
-rw-r--r--src/libaudcore/internal.h4
-rw-r--r--src/libaudcore/mainloop.cc330
-rw-r--r--src/libaudcore/mainloop.h15
-rw-r--r--src/libaudcore/multihash.cc8
-rw-r--r--src/libaudcore/multihash.h16
-rw-r--r--src/libaudcore/objects.h82
-rw-r--r--src/libaudcore/output.cc4
-rw-r--r--src/libaudcore/playback.cc10
-rw-r--r--src/libaudcore/playlist-data.cc36
-rw-r--r--src/libaudcore/playlist-data.h3
-rw-r--r--src/libaudcore/playlist-utils.cc45
-rw-r--r--src/libaudcore/playlist.cc28
-rw-r--r--src/libaudcore/playlist.h3
-rw-r--r--src/libaudcore/probe.cc26
-rw-r--r--src/libaudcore/ringbuf.h13
-rw-r--r--src/libaudcore/runtime.cc30
-rw-r--r--src/libaudcore/runtime.h1
-rw-r--r--src/libaudcore/stringbuf.cc191
-rw-r--r--src/libaudcore/templates.h17
-rw-r--r--src/libaudcore/tests/test-mainloop.cc76
-rw-r--r--src/libaudcore/tests/test.cc64
-rw-r--r--src/libaudcore/tuple-compiler.cc10
-rw-r--r--src/libaudcore/tuple.cc42
-rw-r--r--src/libaudcore/tuple.h12
-rw-r--r--src/libaudcore/vfs.cc46
-rw-r--r--src/libaudcore/vfs.h9
-rw-r--r--src/libaudcore/vis-runner.cc10
-rw-r--r--src/libaudgui/Makefile10
-rw-r--r--src/libaudgui/about.cc27
-rw-r--r--src/libaudgui/images.gresource.xml66
-rw-r--r--src/libaudgui/infopopup.cc23
-rw-r--r--src/libaudgui/init.cc211
-rw-r--r--src/libaudgui/pixbufs.cc9
-rw-r--r--src/libaudgui/prefs-window.cc59
-rw-r--r--src/libaudgui/url-opener.cc29
-rw-r--r--src/libaudqt/Makefile47
-rw-r--r--src/libaudqt/about-qt.cc (renamed from src/libaudqt/about.cc)22
-rw-r--r--src/libaudqt/art-qt.cc (renamed from src/libaudqt/art.cc)51
-rw-r--r--src/libaudqt/audqt.cc (renamed from src/libaudqt/util.cc)37
-rw-r--r--src/libaudqt/equalizer-qt.cc (renamed from src/libaudqt/equalizer.cc)0
-rw-r--r--src/libaudqt/fileopener.cc59
-rw-r--r--src/libaudqt/images.qrc65
-rw-r--r--src/libaudqt/info-widget.cc42
-rw-r--r--src/libaudqt/infopopup-qt.cc212
-rw-r--r--src/libaudqt/infowin-qt.cc (renamed from src/libaudqt/infowin.cc)81
-rw-r--r--src/libaudqt/libaudqt-internal.h18
-rw-r--r--src/libaudqt/libaudqt.h14
-rw-r--r--src/libaudqt/log-inspector.cc2
-rw-r--r--src/libaudqt/menu-qt.cc (renamed from src/libaudqt/menu.cc)6
-rw-r--r--src/libaudqt/playlist-management.cc4
-rw-r--r--src/libaudqt/plugin-menu-qt.cc (renamed from src/libaudqt/plugin-menu.cc)2
-rw-r--r--src/libaudqt/prefs-pluginlist-model.cc5
-rw-r--r--src/libaudqt/prefs-pluginlist-model.h1
-rw-r--r--src/libaudqt/prefs-widget-qt.cc (renamed from src/libaudqt/prefs-widget.cc)0
-rw-r--r--src/libaudqt/prefs-window-qt.cc (renamed from src/libaudqt/prefs-window.cc)54
-rw-r--r--src/libaudqt/queue-manager-qt.cc (renamed from src/libaudqt/queue-manager.cc)0
-rw-r--r--src/libaudqt/url-opener-qt.cc (renamed from src/libaudqt/url-opener.cc)28
-rw-r--r--src/libaudqt/util-qt.cc97
-rw-r--r--src/libaudqt/volumebutton.cc8
75 files changed, 2075 insertions, 722 deletions
diff --git a/src/audacious/dbus-server.cc b/src/audacious/dbus-server.cc
index ce0f1e8..7602ace 100644
--- a/src/audacious/dbus-server.cc
+++ b/src/audacious/dbus-server.cc
@@ -110,6 +110,21 @@ static gboolean do_clear (Obj * obj, Invoc * invoc)
return true;
}
+static gboolean do_config_get (Obj * obj, Invoc * invoc, const char * section, const char * name)
+{
+ String value = aud_get_str (section[0] ? section : nullptr, name);
+ FINISH2 (config_get, value);
+ return true;
+}
+
+static gboolean do_config_set (Obj * obj, Invoc * invoc, const char * section,
+ const char * name, const char * value)
+{
+ aud_set_str (section[0] ? section : nullptr, name, value);
+ FINISH (config_set);
+ return true;
+}
+
static gboolean do_delete (Obj * obj, Invoc * invoc, unsigned pos)
{
CURRENT.remove_entry (pos);
@@ -742,6 +757,8 @@ handlers[] =
{"handle-auto-advance", (GCallback) do_auto_advance},
{"handle-balance", (GCallback) do_balance},
{"handle-clear", (GCallback) do_clear},
+ {"handle-config-get", (GCallback) do_config_get},
+ {"handle-config-set", (GCallback) do_config_set},
{"handle-delete", (GCallback) do_delete},
{"handle-delete-active-playlist", (GCallback) do_delete_active_playlist},
{"handle-eject", (GCallback) do_eject},
diff --git a/src/audacious/main.cc b/src/audacious/main.cc
index d8fbbf0..52ad234 100644
--- a/src/audacious/main.cc
+++ b/src/audacious/main.cc
@@ -368,7 +368,7 @@ int main (int argc, char * * argv)
return EXIT_SUCCESS;
}
-#if USE_DBUS
+#ifdef USE_DBUS
do_remote (); /* may exit */
#endif
diff --git a/src/audtool/audtool.h b/src/audtool/audtool.h
index 51bde99..b3aa711 100644
--- a/src/audtool/audtool.h
+++ b/src/audtool/audtool.h
@@ -25,9 +25,9 @@
struct commandhandler
{
- char * name;
+ const char * name;
void (* handler) (int argc, char * * argv);
- char * desc;
+ const char * desc;
int args;
};
@@ -129,6 +129,8 @@ void show_about_window (int, char * *);
void get_version (int argc, char * * argv);
void plugin_is_enabled (int argc, char * * argv);
void plugin_enable (int argc, char * * argv);
+void config_get (int argc, char * * argv);
+void config_set (int argc, char * * argv);
void equalizer_get_eq (int argc, char * * argv);
void equalizer_get_eq_preamp (int argc, char * * argv);
diff --git a/src/audtool/handlers_general.c b/src/audtool/handlers_general.c
index 68c9da5..886e7ac 100644
--- a/src/audtool/handlers_general.c
+++ b/src/audtool/handlers_general.c
@@ -19,6 +19,7 @@
*/
#include <stdlib.h>
+#include <string.h>
#include "audtool.h"
#include "wrappers.h"
@@ -103,7 +104,7 @@ void get_handlers_list (int argc, char * * argv)
audtool_report ("");
audtool_report ("Commands may be prefixed with '--' (GNU-style long options) or not, your choice.");
audtool_report ("Show/hide and enable/disable commands take an optional 'on' or 'off' argument.");
- audtool_report ("Report bugs to http://redmine.audacious-media-player.org/projects/audacious");
+ audtool_report ("Report bugs to https://redmine.audacious-media-player.org/projects/audacious");
}
void get_version (int argc, char * * argv)
@@ -150,3 +151,50 @@ void plugin_enable (int argc, char * * argv)
obj_audacious_call_plugin_enable_sync (dbus_proxy, argv[1], enable, NULL, NULL);
}
+
+void config_get (int argc, char * * argv)
+{
+ if (argc != 2)
+ {
+ audtool_whine_args (argv[0], "[<section>:]<name>");
+ exit (1);
+ }
+
+ const char * section = "";
+ const char * name = argv[1];
+ char * colon = strchr (argv[1], ':');
+
+ if (colon)
+ {
+ * colon = 0;
+ section = argv[1];
+ name = colon + 1;
+ }
+
+ char * value = NULL;
+ obj_audacious_call_config_get_sync (dbus_proxy, section, name, & value, NULL, NULL);
+ audtool_report (value);
+ g_free (value);
+}
+
+void config_set (int argc, char * * argv)
+{
+ if (argc != 3)
+ {
+ audtool_whine_args (argv[0], "[<section>:]<name> <value>");
+ exit (1);
+ }
+
+ const char * section = "";
+ const char * name = argv[1];
+ char * colon = strchr (argv[1], ':');
+
+ if (colon)
+ {
+ * colon = 0;
+ section = argv[1];
+ name = colon + 1;
+ }
+
+ obj_audacious_call_config_set_sync (dbus_proxy, section, name, argv[2], NULL, NULL);
+}
diff --git a/src/audtool/main.c b/src/audtool/main.c
index 62894e5..345dcb1 100644
--- a/src/audtool/main.c
+++ b/src/audtool/main.c
@@ -128,6 +128,8 @@ const struct commandhandler handlers[] =
{"version", get_version, "print Audacious version", 0},
{"plugin-is-enabled", plugin_is_enabled, "exit code = 0 if plugin is enabled", 1},
{"plugin-enable", plugin_enable, "enable/disable plugin", 2},
+ {"config-get", config_get, "DO NOT USE", 1},
+ {"config-set", config_set, "DO NOT USE", 2},
{"shutdown", shutdown_audacious_server, "shut down Audacious", 0},
{"help", get_handlers_list, "print this help", 0},
diff --git a/src/dbus/aud-dbus.xml b/src/dbus/aud-dbus.xml
index 0172d98..960c910 100644
--- a/src/dbus/aud-dbus.xml
+++ b/src/dbus/aud-dbus.xml
@@ -41,6 +41,18 @@
<arg type="b" direction="in" name="enable" />
</method>
+ <method name="ConfigGet">
+ <arg type="s" direction="in" name="section" />
+ <arg type="s" direction="in" name="name" />
+ <arg type="s" direction="out" name="value" />
+ </method>
+
+ <method name="ConfigSet">
+ <arg type="s" direction="in" name="section" />
+ <arg type="s" direction="in" name="name" />
+ <arg type="s" direction="in" name="value" />
+ </method>
+
<!-- Quit Audacious -->
<method name="Quit" />
diff --git a/src/libaudcore/Makefile b/src/libaudcore/Makefile
index a90d5dc..e969f93 100644
--- a/src/libaudcore/Makefile
+++ b/src/libaudcore/Makefile
@@ -1,6 +1,6 @@
SHARED_LIB = ${LIB_PREFIX}audcore${LIB_SUFFIX}
LIB_MAJOR = 5
-LIB_MINOR = 0
+LIB_MINOR = 1
SRCS = adder.cc \
art.cc \
@@ -95,12 +95,12 @@ CPPFLAGS := -I.. -I../.. \
${GLIB_CFLAGS} \
${GMODULE_CFLAGS} \
${QTCORE_CFLAGS} \
- -DHARDCODE_BINDIR=\"${bindir}\" \
- -DHARDCODE_DATADIR=\"${datadir}/audacious\" \
- -DHARDCODE_PLUGINDIR=\"${plugindir}\" \
- -DHARDCODE_LOCALEDIR=\"${localedir}\" \
- -DHARDCODE_DESKTOPFILE=\"${datarootdir}/applications/audacious.desktop\" \
- -DHARDCODE_ICONFILE=\"${datarootdir}/icons/hicolor/48x48/apps/audacious.png\" \
+ -DINSTALL_BINDIR=\"${bindir}\" \
+ -DINSTALL_DATADIR=\"${datadir}/audacious\" \
+ -DINSTALL_PLUGINDIR=\"${plugindir}\" \
+ -DINSTALL_LOCALEDIR=\"${localedir}\" \
+ -DINSTALL_DESKTOPFILE=\"${datarootdir}/applications/audacious.desktop\" \
+ -DINSTALL_ICONFILE=\"${datarootdir}/icons/hicolor/48x48/apps/audacious.png\" \
-DLIBAUDCORE_BUILD
CFLAGS += ${LIB_CFLAGS}
diff --git a/src/libaudcore/adder.cc b/src/libaudcore/adder.cc
index f9d9bca..888b321 100644
--- a/src/libaudcore/adder.cc
+++ b/src/libaudcore/adder.cc
@@ -136,27 +136,63 @@ static void status_done_locked ()
}
static void add_file (PlaylistAddItem && item, Playlist::FilterFunc filter,
- void * user, AddResult * result, bool validate)
+ void * user, AddResult * result, bool skip_invalid)
{
AUDINFO ("Adding file: %s\n", (const char *) item.filename);
status_update (item.filename, result->items.len ());
- /* If the item doesn't already have a valid tuple, and isn't a subtune
- * itself, then probe it to expand any subtunes. The "validate" check (used
- * to skip non-audio files when adding folders) is also nested within this
- * block; note that "validate" is always false for subtunes. */
+ /*
+ * If possible, we'll wait until the file is added to the playlist to probe
+ * it. There are a couple of reasons why we might need to probe it now:
+ *
+ * 1. We're adding a folder, and need to skip over non-audio files (the
+ * "skip invalid" flag indicates this case).
+ * 2. The file might have subtunes, which we need to expand in order to add
+ * them to the playlist correctly.
+ *
+ * If we already have metadata, or the file is itself a subtune, then
+ * neither of these reasons apply.
+ */
if (! item.tuple.valid () && ! is_subtune (item.filename))
{
+ /* If we open the file to identify the decoder, we can re-use the same
+ * handle to read metadata. */
VFSFile file;
if (! item.decoder)
{
- bool fast = ! aud_get_bool (nullptr, "slow_probe");
- item.decoder = aud_file_find_decoder (item.filename, fast, file);
- if (validate && ! item.decoder)
- return;
+ if (aud_get_bool (nullptr, "slow_probe"))
+ {
+ /* The slow path. User settings dictate that we should try to
+ * find a decoder even if we don't recognize the file extension. */
+ item.decoder = aud_file_find_decoder (item.filename, false, file);
+ if (skip_invalid && ! item.decoder)
+ return;
+ }
+ else
+ {
+ /* The fast path. First see whether any plugins recognize the
+ * file extension. Note that it's possible for multiple plugins
+ * to recognize the same extension (.ogg, for example). */
+ int flags = probe_by_filename (item.filename);
+ if (skip_invalid && ! (flags & PROBE_FLAG_HAS_DECODER))
+ return;
+
+ if ((flags & PROBE_FLAG_MIGHT_HAVE_SUBTUNES))
+ {
+ /* At least one plugin recognized the file extension and
+ * indicated that there might be subtunes. Figure out for
+ * sure which decoder we need to use for this file. */
+ item.decoder = aud_file_find_decoder (item.filename, true, file);
+ if (skip_invalid && ! item.decoder)
+ return;
+ }
+ }
}
+ /* At this point we've either identified the decoder or determined that
+ * the file doesn't have any subtunes. If the former, read the tag so
+ * so we can expand any subtunes we find. */
if (item.decoder && input_plugin_has_subtunes (item.decoder))
aud_file_read_tag (item.filename, item.decoder, file, item.tuple);
}
@@ -183,9 +219,9 @@ static void add_file (PlaylistAddItem && item, Playlist::FilterFunc filter,
/* To prevent infinite recursion, we currently allow adding a folder from within
* a playlist, but not a playlist from within a folder, nor a second playlist
* from within a playlist (this last rule is enforced by setting
- * <allow_playlist> to false from within add_playlist()). */
+ * <from_playlist> to true from within add_playlist()). */
static void add_generic (PlaylistAddItem && item, Playlist::FilterFunc filter,
- void * user, AddResult * result, bool save_title, bool allow_playlist);
+ void * user, AddResult * result, bool save_title, bool from_playlist);
static void add_playlist (const char * filename, Playlist::FilterFunc filter,
void * user, AddResult * result, bool save_title)
@@ -203,7 +239,7 @@ static void add_playlist (const char * filename, Playlist::FilterFunc filter,
result->title = title;
for (auto & item : items)
- add_generic (std::move (item), filter, user, result, false, false);
+ add_generic (std::move (item), filter, user, result, false, true);
}
static void add_cuesheets (Index<String> & files, Playlist::FilterFunc filter,
@@ -311,14 +347,21 @@ static void add_folder (const char * filename, Playlist::FilterFunc filter,
if (mode & VFS_IS_REGULAR)
add_file ({String (file)}, filter, user, result, true);
- else if (mode & VFS_IS_DIR)
+ else if ((mode & VFS_IS_DIR) && aud_get_bool (nullptr, "recurse_folders"))
add_folder (file, filter, user, result, false);
}
}
static void add_generic (PlaylistAddItem && item, Playlist::FilterFunc filter,
- void * user, AddResult * result, bool save_title, bool allow_playlist)
+ void * user, AddResult * result, bool save_title, bool from_playlist)
{
+ if (! strstr (item.filename, "://"))
+ {
+ /* Let's not add random junk to the playlist. */
+ AUDERR ("Invalid URI: %s\n", (const char *) item.filename);
+ return;
+ }
+
if (filter && ! filter (item.filename, user))
{
result->filtered = true;
@@ -331,11 +374,16 @@ static void add_generic (PlaylistAddItem && item, Playlist::FilterFunc filter,
add_file (std::move (item), filter, user, result, false);
else
{
+ int tests = 0;
+ if (! from_playlist)
+ tests |= VFS_NO_ACCESS;
+ if (! from_playlist || aud_get_bool (nullptr, "folders_in_playlist"))
+ tests |= VFS_IS_DIR;
+
String error;
- VFSFileTest mode = VFSFile::test_file (item.filename,
- VFSFileTest (VFS_IS_DIR | VFS_NO_ACCESS), error);
+ VFSFileTest mode = VFSFile::test_file (item.filename, (VFSFileTest) tests, error);
- if (mode & VFS_NO_ACCESS)
+ if ((mode & VFS_NO_ACCESS))
aud_ui_show_error (str_printf (_("Error reading %s:\n%s"),
(const char *) item.filename, (const char *) error));
else if (mode & VFS_IS_DIR)
@@ -343,7 +391,7 @@ static void add_generic (PlaylistAddItem && item, Playlist::FilterFunc filter,
add_folder (item.filename, filter, user, result, save_title);
result->saw_folder = true;
}
- else if (allow_playlist && Playlist::filename_is_playlist (item.filename))
+ else if ((! from_playlist) && Playlist::filename_is_playlist (item.filename))
add_playlist (item.filename, filter, user, result, save_title);
else
add_file (std::move (item), filter, user, result, false);
@@ -474,7 +522,7 @@ static void * add_worker (void * unused)
bool save_title = (task->items.len () == 1);
for (auto & item : task->items)
- add_generic (std::move (item), task->filter, task->user, result, save_title, true);
+ add_generic (std::move (item), task->filter, task->user, result, save_title, false);
delete task;
diff --git a/src/libaudcore/audstrings.cc b/src/libaudcore/audstrings.cc
index 6d971b9..a53f8e7 100644
--- a/src/libaudcore/audstrings.cc
+++ b/src/libaudcore/audstrings.cc
@@ -160,6 +160,14 @@ EXPORT StringBuf str_printf (const char * format, ...)
return str;
}
+EXPORT void str_append_printf (StringBuf & str, const char * format, ...)
+{
+ va_list args;
+ va_start (args, format);
+ str_append_vprintf (str, format, args);
+ va_end (args);
+}
+
EXPORT StringBuf str_vprintf (const char * format, va_list args)
{
StringBuf str (-1);
@@ -168,6 +176,14 @@ EXPORT StringBuf str_vprintf (const char * format, va_list args)
return str;
}
+EXPORT void str_append_vprintf (StringBuf & str, const char * format, va_list args)
+{
+ int len0 = str.len ();
+ str.resize (-1);
+ int len1 = vsnprintf (str + len0, str.len () - len0, format, args);
+ str.resize (len0 + len1);
+}
+
EXPORT bool str_has_prefix_nocase (const char * str, const char * prefix)
{
return ! g_ascii_strncasecmp (str, prefix, strlen (prefix));
@@ -585,27 +601,30 @@ EXPORT StringBuf filename_to_uri (const char * name)
* 1) system locale is not UTF-8, and
* 2) filename is not already valid UTF-8 */
if (! g_get_charset (nullptr) && ! g_utf8_validate (name, -1, nullptr))
- buf.steal (str_from_locale (name));
-
- if (! buf)
- buf.steal (str_copy (name));
+ buf = str_from_locale (name);
#endif
- buf.steal (str_encode_percent (buf));
+ buf = str_encode_percent (buf ? buf : name);
buf.insert (0, URI_PREFIX);
- return buf;
+ return buf.settle ();
}
-/* Like g_filename_from_uri, but converts the filename from UTF-8 to the system
- * locale after percent-decoding (except on Windows, where filenames are assumed
- * to be UTF-8). On Windows, strips the leading '/' and replaces '/' with '\'. */
+/* Like g_filename_from_uri, but optionally converts the filename from UTF-8 to
+ * the system locale after percent-decoding (except on Windows, where filenames
+ * are assumed to be UTF-8). On Windows, strips the leading '/' and replaces
+ * '/' with '\'. If the input is not a valid URI, it is assumed to be a local
+ * filename already and is not percent-decoded. */
EXPORT StringBuf uri_to_filename (const char * uri, bool use_locale)
{
- if (strncmp (uri, URI_PREFIX, URI_PREFIX_LEN))
- return StringBuf ();
+ StringBuf buf;
- StringBuf buf = str_decode_percent (uri + URI_PREFIX_LEN);
+ if (! strncmp (uri, URI_PREFIX, URI_PREFIX_LEN))
+ buf = str_decode_percent (uri + URI_PREFIX_LEN);
+ else if (! strstr (uri, "://")) /* already a local filename? */
+ buf = str_copy (uri);
+ else
+ return StringBuf ();
#ifndef _WIN32
/* convert to locale if:
@@ -616,19 +635,19 @@ EXPORT StringBuf uri_to_filename (const char * uri, bool use_locale)
{
StringBuf locale = str_to_locale (buf);
if (locale)
- buf.steal (std::move (locale));
+ buf = std::move (locale);
}
#endif
/* if UTF-8 was requested, make sure the result is valid */
if (! use_locale)
{
- buf.steal (str_to_utf8 (std::move (buf)));
+ buf = str_to_utf8 (std::move (buf));
if (! buf)
return StringBuf ();
}
- return filename_normalize (std::move (buf));
+ return filename_normalize (buf.settle ());
}
/* Formats a URI for human-readable display. Percent-decodes and, for file://
@@ -739,14 +758,45 @@ EXPORT StringBuf uri_construct (const char * path, const char * reference)
StringBuf buf = str_to_utf8 (path, -1);
if (! buf)
- return buf;
+ return StringBuf ();
if (aud_get_bool (nullptr, "convert_backslash"))
str_replace_char (buf, '\\', '/');
- buf.steal (str_encode_percent (buf));
+ buf = str_encode_percent (buf);
buf.insert (0, reference, slash + 1 - reference);
- return buf;
+ return buf.settle ();
+}
+
+/* Basically the reverse of uri_construct().
+ * First try to split off a relative path (if so configured).
+ * Failing that, try to convert to a local filename.
+ * Failing that, return the URI as-is.
+ *
+ * All output is UTF-8 for portability.
+ *
+ * Parameters:
+ * 1. uri: the full URI of a song file
+ * 2. reference: the full URI of the playlist being written */
+
+EXPORT StringBuf uri_deconstruct (const char * uri, const char * reference)
+{
+ if (aud_get_bool (nullptr, "export_relative_paths"))
+ {
+ const char * slash = strrchr (reference, '/');
+ if (slash && ! strncmp (uri, reference, slash + 1 - reference))
+ {
+ StringBuf path = str_to_utf8 (str_decode_percent (uri + (slash + 1 - reference)));
+ if (path)
+ return path;
+ }
+ }
+
+ StringBuf filename = uri_to_filename (uri, false);
+ if (filename)
+ return filename;
+
+ return str_copy (uri);
}
/* Like strcasecmp, but orders numbers correctly (2 before 10). */
@@ -991,24 +1041,22 @@ EXPORT double str_to_double (const char * string)
return neg ? -val : val;
}
-EXPORT StringBuf int_to_str (int val)
+EXPORT void str_insert_int (StringBuf & string, int pos, int val)
{
bool neg = (val < 0);
unsigned absval = neg ? -val : val;
int digits = digits_for (absval);
- StringBuf buf ((neg ? 1 : 0) + digits);
+ int len = (neg ? 1 : 0) + digits;
+ char * set = string.insert (pos, nullptr, len);
- char * set = buf;
if (neg)
* (set ++) = '-';
uint_to_str (absval, set, digits);
-
- return buf;
}
-EXPORT StringBuf double_to_str (double val)
+EXPORT void str_insert_double (StringBuf & string, int pos, double val)
{
bool neg = (val < 0);
if (neg)
@@ -1028,9 +1076,9 @@ EXPORT StringBuf double_to_str (double val)
decimals --;
int digits = digits_for (i);
- StringBuf buf ((neg ? 1 : 0) + digits + (decimals ? 1 : 0) + decimals);
+ int len = (neg ? 1 : 0) + digits + (decimals ? 1 : 0) + decimals;
+ char * set = string.insert (pos, nullptr, len);
- char * set = buf;
if (neg)
* (set ++) = '-';
@@ -1042,7 +1090,19 @@ EXPORT StringBuf double_to_str (double val)
* (set ++) = '.';
uint_to_str (f, set, decimals);
}
+}
+EXPORT StringBuf int_to_str (int val)
+{
+ StringBuf buf;
+ str_insert_int (buf, 0, val);
+ return buf;
+}
+
+EXPORT StringBuf double_to_str (double val)
+{
+ StringBuf buf;
+ str_insert_double (buf, 0, val);
return buf;
}
diff --git a/src/libaudcore/audstrings.h b/src/libaudcore/audstrings.h
index 8c956f0..e32f00a 100644
--- a/src/libaudcore/audstrings.h
+++ b/src/libaudcore/audstrings.h
@@ -36,10 +36,13 @@ StringBuf str_copy (const char * s, int len = -1);
StringBuf str_concat (const std::initializer_list<const char *> & strings);
#ifdef _WIN32
StringBuf str_printf (const char * format, ...) __attribute__ ((__format__ (gnu_printf, 1, 2)));
+void str_append_printf (StringBuf & str, const char * format, ...) __attribute__ ((__format__ (gnu_printf, 2, 3)));
#else
StringBuf str_printf (const char * format, ...) __attribute__ ((__format__ (__printf__, 1, 2)));
+void str_append_printf (StringBuf & str, const char * format, ...) __attribute__ ((__format__ (__printf__, 2, 3)));
#endif
StringBuf str_vprintf (const char * format, va_list args);
+void str_append_vprintf (StringBuf & str, const char * format, va_list args);
bool str_has_prefix_nocase (const char * str, const char * prefix);
bool str_has_suffix_nocase (const char * str, const char * suffix);
@@ -90,6 +93,7 @@ StringBuf uri_get_extension (const char * uri);
/* Requires: aud_init() */
StringBuf uri_construct (const char * path, const char * reference);
+StringBuf uri_deconstruct (const char * uri, const char * reference);
int str_compare (const char * a, const char * b);
int str_compare_encoded (const char * a, const char * b);
@@ -99,6 +103,8 @@ StringBuf index_to_str_list (const Index<String> & index, const char * sep);
int str_to_int (const char * string);
double str_to_double (const char * string);
+void str_insert_int (StringBuf & string, int pos, int val);
+void str_insert_double (StringBuf & string, int pos, double val);
StringBuf int_to_str (int val);
StringBuf double_to_str (double val);
diff --git a/src/libaudcore/charset.cc b/src/libaudcore/charset.cc
index 5a4dba6..d77d38f 100644
--- a/src/libaudcore/charset.cc
+++ b/src/libaudcore/charset.cc
@@ -185,9 +185,9 @@ EXPORT StringBuf str_to_utf8 (StringBuf && str)
return std::move (str);
tiny_lock_read (& settings_lock);
- str.steal (convert_to_utf8_locked (str, str.len ()));
+ str = convert_to_utf8_locked (str, str.len ());
tiny_unlock_read (& settings_lock);
- return std::move (str);
+ return str.settle ();
}
static void chardet_update (void * = nullptr, void * = nullptr)
diff --git a/src/libaudcore/config.cc b/src/libaudcore/config.cc
index 84bd0f4..5046914 100644
--- a/src/libaudcore/config.cc
+++ b/src/libaudcore/config.cc
@@ -39,6 +39,7 @@ static const char * const core_defaults[] = {
"always_resume_paused", "TRUE",
"clear_playlist", "TRUE",
"open_to_temporary", "TRUE",
+ "recurse_folders", "TRUE",
"resume_playback_on_startup", "TRUE",
"show_interface", "TRUE",
@@ -46,7 +47,6 @@ static const char * const core_defaults[] = {
"eqpreset_default_file", "",
"eqpreset_extension", "",
"equalizer_active", "FALSE",
- "equalizer_autoload", "FALSE",
"equalizer_bands", "0,0,0,0,0,0,0,0,0,0",
"equalizer_preamp", "0",
@@ -62,6 +62,7 @@ static const char * const core_defaults[] = {
/* network */
"net_buffer_kb", "128",
+ "save_url_history", "TRUE",
"use_proxy", "FALSE",
"use_proxy_auth", "FALSE",
@@ -71,6 +72,7 @@ static const char * const core_defaults[] = {
"enable_clipping_prevention", "TRUE",
"output_bit_depth", "-1",
"output_buffer_size", "500",
+ "record", "FALSE",
"record_stream", aud::numeric_string<(int) OutputStream::AfterReplayGain>::str,
"replay_gain_mode", aud::numeric_string<(int) ReplayGainMode::Track>::str,
"replay_gain_preamp", "0",
@@ -93,6 +95,8 @@ static const char * const core_defaults[] = {
#else
"convert_backslash", "FALSE",
#endif
+ "export_relative_paths", "TRUE",
+ "folders_in_playlist", "FALSE",
"generic_title_format", "${?artist:${artist} - }${?album:${album} - }${title}",
"leading_zero", "FALSE",
"show_hours", "TRUE",
@@ -238,9 +242,7 @@ private:
void config_load ()
{
- StringBuf path = filename_to_uri (aud_get_path (AudPath::UserDir));
- path.insert (-1, "/config");
-
+ StringBuf path = filename_build ({aud_get_path (AudPath::UserDir), "config"});
if (VFSFile::test_file (path, VFS_EXISTS))
{
VFSFile file (path, "r");
@@ -265,12 +267,15 @@ void config_save ()
Index<ConfigItem> list;
- s_config.iterate ([&] (ConfigNode * node) {
+ auto add_to_list = [&] (ConfigNode * node) {
list.append (* node);
-
- s_modified = false; // must be inside MultiHash lock
return false;
- });
+ };
+ auto finish = [] () {
+ s_modified = false; // must be inside MultiHash lock
+ };
+
+ s_config.iterate (add_to_list, finish);
list.sort ([] (const ConfigItem & a, const ConfigItem & b) {
if (a.section == b.section)
@@ -279,12 +284,9 @@ void config_save ()
return strcmp (a.section, b.section);
});
- StringBuf path = filename_to_uri (aud_get_path (AudPath::UserDir));
- path.insert (-1, "/config");
-
String current_heading;
- VFSFile file (path, "w");
+ VFSFile file (filename_build ({aud_get_path (AudPath::UserDir), "config"}), "w");
if (! file)
goto FAILED;
diff --git a/src/libaudcore/equalizer.cc b/src/libaudcore/equalizer.cc
index 23dae12..d19c031 100644
--- a/src/libaudcore/equalizer.cc
+++ b/src/libaudcore/equalizer.cc
@@ -38,14 +38,14 @@
/* Q value for band-pass filters 1.2247 = (3/2)^(1/2)
* Gives 4 dB suppression at Fc*2 and Fc/2 */
-#define Q 1.2247449
+#define Q 1.2247449f
/* Center frequencies for band-pass filters (Hz) */
/* These are not the historical WinAmp frequencies, because the IIR filters used
* here are designed for each frequency to be twice the previous. Using WinAmp
* frequencies leads to too much gain in some bands and too little in others. */
-static const float CF[AUD_EQ_NBANDS] = {31.25, 62.5, 125, 250, 500, 1000, 2000,
- 4000, 8000, 16000};
+static const float CF[AUD_EQ_NBANDS] = {31.25f, 62.5f, 125, 250, 500, 1000,
+ 2000, 4000, 8000, 16000};
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static bool active;
@@ -59,13 +59,13 @@ static int K; /* Number of used EQ bands */
/* 2nd order band-pass filter design */
static void bp2 (float *a, float *b, float fc)
{
- float th = 2 * M_PI * fc;
+ float th = 2 * (float)M_PI * fc;
float C = (1 - tanf (th * Q / 2)) / (1 + tanf (th * Q / 2));
a[0] = (1 + C) * cosf (th);
a[1] = -C;
b[0] = (1 - C) / 2;
- b[1] = -1.005;
+ b[1] = -1.005f;
}
void eq_set_format (int new_channels, int new_rate)
@@ -79,7 +79,7 @@ void eq_set_format (int new_channels, int new_rate)
* than rate/2Q to avoid singularities in the tangent used in bp2() */
K = AUD_EQ_NBANDS;
- while (K > 0 && CF[K - 1] > (float) rate / (2.005 * Q))
+ while (K > 0 && CF[K - 1] > (float) rate / (2.005f * Q))
K --;
/* Generate filter taps */
@@ -102,7 +102,7 @@ static void eq_set_bands_real (double preamp, double *values)
for (int c = 0; c < AUD_MAX_CHANNELS; c ++)
{
for (int i = 0; i < AUD_EQ_NBANDS; i ++)
- gv[c][i] = pow (10, adj[i] / 20) - 1;
+ gv[c][i] = powf (10, adj[i] / 20) - 1;
}
}
diff --git a/src/libaudcore/history.cc b/src/libaudcore/history.cc
index 4dab916..b8cfdbd 100644
--- a/src/libaudcore/history.cc
+++ b/src/libaudcore/history.cc
@@ -47,3 +47,12 @@ EXPORT void aud_history_add (const char * path)
add = old;
}
}
+
+EXPORT void aud_history_clear ()
+{
+ for (int i = 0; i < MAX_ENTRIES; i ++)
+ {
+ StringBuf name = str_printf ("entry%d", i);
+ aud_set_str ("history", name, "");
+ }
+}
diff --git a/src/libaudcore/index.h b/src/libaudcore/index.h
index 92be2d9..a97a4b1 100644
--- a/src/libaudcore/index.h
+++ b/src/libaudcore/index.h
@@ -53,15 +53,6 @@ public:
b.m_size = 0;
}
- void steal (IndexBase && b, aud::EraseFunc erase_func)
- {
- if (this != & b)
- {
- clear (erase_func);
- new (this) IndexBase (std::move (b));
- }
- }
-
void * begin ()
{ return m_data; }
const void * begin () const
@@ -118,8 +109,8 @@ public:
Index (Index && b) :
IndexBase (std::move (b)) {}
- void operator= (Index && b)
- { steal (std::move (b), aud::erase_func<T> ()); }
+ Index & operator= (Index && b)
+ { return aud::move_assign (* this, std::move (b)); }
T * begin ()
{ return (T *) IndexBase::begin (); }
diff --git a/src/libaudcore/interface.cc b/src/libaudcore/interface.cc
index f83a53e..ae44093 100644
--- a/src/libaudcore/interface.cc
+++ b/src/libaudcore/interface.cc
@@ -167,6 +167,10 @@ void interface_run ()
EXPORT void aud_quit ()
{
+ // Qt is very sensitive to things being deleted in the correct order
+ // to avoid upsetting it, we'll stop all queued callbacks right now
+ QueuedFunc::inhibit_all ();
+
if (current_interface)
current_interface->quit ();
else
diff --git a/src/libaudcore/internal.h b/src/libaudcore/internal.h
index bf2216c..7035a20 100644
--- a/src/libaudcore/internal.h
+++ b/src/libaudcore/internal.h
@@ -103,6 +103,10 @@ bool open_input_file (const char * filename, const char * mode,
InputPlugin * ip, VFSFile & file, String * error = nullptr);
InputPlugin * load_input_plugin (PluginHandle * decoder, String * error = nullptr);
+#define PROBE_FLAG_HAS_DECODER (1 << 0)
+#define PROBE_FLAG_MIGHT_HAVE_SUBTUNES (1 << 1)
+int probe_by_filename (const char * filename);
+
/* runtime.cc */
extern size_t misc_bytes_allocated;
diff --git a/src/libaudcore/mainloop.cc b/src/libaudcore/mainloop.cc
index 397106f..090aeca 100644
--- a/src/libaudcore/mainloop.cc
+++ b/src/libaudcore/mainloop.cc
@@ -1,6 +1,6 @@
/*
* mainloop.cc
- * Copyright 2014-2015 John Lindgren
+ * Copyright 2014-2017 John Lindgren
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -19,9 +19,6 @@
#include "mainloop.h"
-#include <pthread.h>
-#include <stdlib.h>
-
#include <glib.h>
#ifdef USE_QT
@@ -32,13 +29,6 @@
#include "multihash.h"
#include "runtime.h"
-static pthread_mutex_t mainloop_mutex = PTHREAD_MUTEX_INITIALIZER;
-static GMainLoop * glib_mainloop;
-
-#ifdef USE_QT
-static QCoreApplication * qt_mainloop;
-#endif
-
struct QueuedFuncParams {
QueuedFunc::Func func;
void * data;
@@ -46,33 +36,33 @@ struct QueuedFuncParams {
bool repeat;
};
-// Helper class that is created when the QueuedFunc is activated. The base
-// class contains the GLib implementation. The Qt implementation, which is more
-// complex, is contained in the QueuedFuncEvent and QueuedFuncTimer subclasses.
+// QueuedFunc itself is a tiny handle/identifier object; most of the related
+// code lives in various "helper" objects. This is the base class inherited
+// by all the different types of helpers.
struct QueuedFuncHelper
{
+ QueuedFunc * queued;
QueuedFuncParams params;
- QueuedFunc * queued = nullptr;
-
- QueuedFuncHelper (const QueuedFuncParams & params) :
- params (params) {}
- virtual ~QueuedFuncHelper () {}
+ // Creates an appropriate helper subclass for the given parameters and
+ // schedules it to run the QueuedFunc
+ static QueuedFuncHelper * create (QueuedFunc * queued, const QueuedFuncParams & params);
+ // Callback which runs the QueuedFunc, if still active
void run ();
- void start_for (QueuedFunc * queued_);
- virtual bool can_stop ()
- { return true; }
+ // Cancels any scheduled run of the QueuedFunc and marks the helper for
+ // deletion (it may not deleted immediately, but should not be accessed
+ // again after calling this function)
+ virtual void cancel () = 0;
- virtual void start ();
- virtual void stop ();
+ virtual ~QueuedFuncHelper () {}
-private:
- struct RunCheck;
+protected:
+ QueuedFuncHelper (QueuedFunc * queued, const QueuedFuncParams & params) :
+ queued (queued), params (params) {}
- int glib_source = 0;
};
// The following hash table implements a thread-safe "registry" of active
@@ -82,46 +72,66 @@ private:
struct QueuedFuncNode : public MultiHash::Node
{
- QueuedFunc * queued;
- QueuedFuncHelper * helper;
- bool can_stop;
+ // Creates a helper to be registered in the hash
+ QueuedFuncNode (QueuedFunc * queued, const QueuedFuncParams & params) :
+ helper (QueuedFuncHelper::create (queued, params)) {}
+
+ // Cancels a helper when it is unregistered from the hash
+ ~QueuedFuncNode ()
+ { helper->cancel (); }
- bool match (const QueuedFunc * q) const
- { return q == queued; }
+ // Replaces the registration of one helper with another
+ void reset (QueuedFunc * queued, const QueuedFuncParams & params)
+ {
+ helper->cancel ();
+ helper = QueuedFuncHelper::create (queued, params);
+ }
+
+ // Checks whether a helper is still registered
+ bool is_current (QueuedFuncHelper * test_helper) const
+ { return test_helper == helper; }
+
+ // Hash comparison function
+ bool match (const QueuedFunc * queued) const
+ { return queued == helper->queued; }
+
+private:
+ QueuedFuncHelper * helper;
};
static MultiHash_T<QueuedFuncNode, QueuedFunc> func_table;
+static bool in_lockdown = false;
// Helper logic common between GLib and Qt
// "run" logic executed within the hash table lock
-struct QueuedFuncHelper::RunCheck
+struct RunCheck
{
QueuedFuncHelper * helper;
- bool valid;
+ bool okay_to_run;
+ // Called if the QueuedFunc is not registered in the hash
+ // This indicates a "stale" event that should not be processed
QueuedFuncNode * add (const QueuedFunc *)
{ return nullptr; }
bool found (const QueuedFuncNode * node)
{
- // Check that the same helper is installed as when the function was
- // first queued. If a new helper is installed, then we are processing
- // a "stale" event and should not run the callback.
- valid = (node->helper == helper);
-
- // Uninstall the QueuedFunc and helper if this is a one-time (non-
- // periodic) callback. Do NOT uninstall the QueuedFunc if has already
- // been reinstalled with a new helper (see previous comment).
- bool remove = valid && ! helper->params.repeat;
-
- // Clean up the helper if this is a one-time (non-periodic) callback OR
- // a new helper has already been installed. No fields or methods of the
- // helper may be accessed after this point.
- if (! valid || ! helper->params.repeat)
- helper->stop ();
-
- return remove;
+ // Check whether a different helper has been registered
+ // This also indicates a "stale" event that should not be processed
+ if (! node->is_current (helper))
+ return false;
+
+ // We are still registered and good to go
+ okay_to_run = true;
+
+ // Leave a periodic timer registered
+ if (helper->params.repeat)
+ return false;
+
+ // Unregister and cancel a one-time callback
+ delete node;
+ return true;
}
};
@@ -132,36 +142,38 @@ void QueuedFuncHelper::run ()
RunCheck r = {this, false};
func_table.lookup (queued, ptr_hash (queued), r);
- if (r.valid)
+ if (r.okay_to_run)
params.func (params.data);
}
-void QueuedFuncHelper::start_for (QueuedFunc * queued_)
-{
- queued = queued_;
- queued->_running = params.repeat;
-
- start (); // branch to GLib or Qt implementation
-}
-
// GLib implementation -- simple wrapper around g_timeout_add_full()
-void QueuedFuncHelper::start ()
+class HelperGLib : public QueuedFuncHelper
{
- auto callback = [] (void * me) -> gboolean {
- (static_cast<QueuedFuncHelper *> (me))->run ();
+public:
+ HelperGLib (QueuedFunc * queued, const QueuedFuncParams & params) :
+ QueuedFuncHelper (queued, params)
+ {
+ glib_source = g_timeout_add_full (G_PRIORITY_HIGH, params.interval_ms,
+ run_cb, this, aud::delete_obj<HelperGLib>);
+ }
+
+ void cancel ()
+ {
+ // GLib will delete the helper after we return to the main loop
+ g_source_remove (glib_source);
+ }
+
+private:
+ static gboolean run_cb (void * me)
+ {
+ (static_cast<HelperGLib *> (me))->run ();
return G_SOURCE_CONTINUE;
- };
+ }
- glib_source = g_timeout_add_full (G_PRIORITY_HIGH, params.interval_ms,
- callback, this, aud::delete_obj<QueuedFuncHelper>);
-}
+ int glib_source = 0;
+};
-void QueuedFuncHelper::stop ()
-{
- if (glib_source)
- g_source_remove (glib_source); // deletes the QueuedFuncHelper
-}
#ifdef USE_QT
@@ -175,32 +187,29 @@ void QueuedFuncHelper::stop ()
// QEvents cannot be cancelled once posted; therefore it's necessary to check
// on our side that the QueuedFunc is still active when the event is received.
-class QueuedFuncEvent : public QueuedFuncHelper, public QEvent
-{
-public:
- QueuedFuncEvent (const QueuedFuncParams & params) :
- QueuedFuncHelper (params),
- QEvent (User) {}
-
- bool can_stop ()
- { return false; }
-
- void start ();
-};
-
-class QueuedFuncRouter : public QObject
+class EventRouter : public QObject
{
protected:
- void customEvent (QEvent * event)
- { dynamic_cast<QueuedFuncEvent *> (event)->run (); }
+ void customEvent (QEvent * event);
};
-static QueuedFuncRouter router;
+static EventRouter router;
-void QueuedFuncEvent::start ()
+class HelperQEvent : public QueuedFuncHelper, public QEvent
{
- QCoreApplication::postEvent (& router, this, Qt::HighEventPriority);
-}
+public:
+ HelperQEvent (QueuedFunc * queued, const QueuedFuncParams & params) :
+ QueuedFuncHelper (queued, params),
+ QEvent (User)
+ {
+ QCoreApplication::postEvent (& router, this, Qt::HighEventPriority);
+ }
+
+ void cancel () {} // Qt will delete the event after it fires
+};
+
+void EventRouter::customEvent (QEvent * event)
+ { dynamic_cast<HelperQEvent *> (event)->run (); }
// Periodic callbacks are implemented through QObject's timer capability. In
// this case, the QueuedFuncHelper is a QObject that is re-associated with the
@@ -211,19 +220,17 @@ void QueuedFuncEvent::start ()
// that the QApplication is up. We then start the timer and run our own
// callback from the QTimerEvents we receive.
-class QueuedFuncTimer : public QueuedFuncHelper, public QObject
+class HelperQTimer : public QueuedFuncHelper, public QObject
{
public:
- QueuedFuncTimer (const QueuedFuncParams & params) :
- QueuedFuncHelper (params) {}
-
- void start ()
+ HelperQTimer (QueuedFunc * queued, const QueuedFuncParams & params) :
+ QueuedFuncHelper (queued, params)
{
moveToThread (router.thread ()); // main thread
QCoreApplication::postEvent (this, new QEvent (QEvent::User), Qt::HighEventPriority);
}
- void stop ()
+ void cancel ()
{ deleteLater (); }
protected:
@@ -236,160 +243,129 @@ protected:
#endif // USE_QT
// creates the appropriate helper subclass
-static QueuedFuncHelper * create_helper (const QueuedFuncParams & params)
+QueuedFuncHelper * QueuedFuncHelper::create (QueuedFunc * queued, const QueuedFuncParams & params)
{
#ifdef USE_QT
if (aud_get_mainloop_type () == MainloopType::Qt)
{
if (params.interval_ms > 0)
- return new QueuedFuncTimer (params);
+ return new HelperQTimer (queued, params);
else
- return new QueuedFuncEvent (params);
+ return new HelperQEvent (queued, params);
}
#endif
- return new QueuedFuncHelper (params);
+ return new HelperGLib (queued, params);
}
// "start" logic executed within the hash table lock
-struct QueuedFunc::Starter
+struct Starter
{
QueuedFunc * queued;
- QueuedFuncHelper * helper;
+ const QueuedFuncParams & params;
- // QueuedFunc not yet installed
- // install, then create a helper and start it
+ // register a new helper for this QueuedFunc
QueuedFuncNode * add (const QueuedFunc *)
- {
- auto node = new QueuedFuncNode;
- node->queued = queued;
- node->helper = helper;
- node->can_stop = helper->can_stop ();
-
- helper->start_for (queued);
+ { return in_lockdown ? nullptr : new QueuedFuncNode (queued, params); }
- return node;
- }
-
- // QueuedFunc already installed
- // first clean up the existing helper
- // then create a new helper and start it
+ // cancel the old helper and register a replacement
bool found (QueuedFuncNode * node)
- {
- if (node->can_stop)
- node->helper->stop ();
-
- node->helper = helper;
- node->can_stop = helper->can_stop ();
-
- helper->start_for (queued);
-
- return false; // do not remove
- }
+ { node->reset (queued, params); return false; }
};
// common entry point used by all queue() and start() variants
-void QueuedFunc::start (const QueuedFuncParams & params)
+static void start_func (QueuedFunc * queued, const QueuedFuncParams & params)
{
- Starter s = {this, create_helper (params)};
- func_table.lookup (this, ptr_hash (this), s);
+ Starter s = {queued, params};
+ func_table.lookup (queued, ptr_hash (queued), s);
}
EXPORT void QueuedFunc::queue (Func func, void * data)
{
- start ({func, data, 0, false});
+ start_func (this, {func, data, 0, false});
+ _running = false;
}
EXPORT void QueuedFunc::queue (int delay_ms, Func func, void * data)
{
g_return_if_fail (delay_ms >= 0);
- start ({func, data, delay_ms, false});
+ start_func (this, {func, data, delay_ms, false});
+ _running = false;
}
EXPORT void QueuedFunc::start (int interval_ms, Func func, void * data)
{
g_return_if_fail (interval_ms > 0);
- start ({func, data, interval_ms, true});
+ start_func (this, {func, data, interval_ms, true});
+ _running = true;
}
// "stop" logic executed within the hash table lock
-struct QueuedFunc::Stopper
+struct Stopper
{
- // not installed, do nothing
+ // not registered, do nothing
QueuedFuncNode * add (const QueuedFunc *)
{ return nullptr; }
- // installed, clean up the helper and uninstall
+ // unregister and cancel helper
bool found (QueuedFuncNode * node)
- {
- if (node->can_stop)
- node->helper->stop ();
-
- node->queued->_running = false;
- delete node;
-
- return true; // remove
- }
+ { delete node; return true; }
};
EXPORT void QueuedFunc::stop ()
{
Stopper s;
func_table.lookup (this, ptr_hash (this), s);
+ _running = false;
+}
+
+// unregister a pending callback at shutdown
+static bool cleanup_node (QueuedFuncNode * node)
+ { delete node; return true; }
+// inhibit all future callbacks at shutdown
+static void enter_lockdown ()
+ { in_lockdown = true; }
+
+EXPORT void QueuedFunc::inhibit_all ()
+{
+ func_table.iterate (cleanup_node, enter_lockdown);
}
// main loop implementation follows
+static GMainLoop * glib_mainloop;
+
EXPORT void mainloop_run ()
{
- pthread_mutex_lock (& mainloop_mutex);
-
#ifdef USE_QT
if (aud_get_mainloop_type () == MainloopType::Qt)
{
- if (! qt_mainloop)
- {
- static char app_name[] = "audacious";
- static int dummy_argc = 1;
- static char * dummy_argv[] = {app_name, nullptr};
-
- qt_mainloop = new QCoreApplication (dummy_argc, dummy_argv);
- atexit ([] () { delete qt_mainloop; });
- }
-
- pthread_mutex_unlock (& mainloop_mutex);
- qt_mainloop->exec ();
+ static char app_name[] = "audacious";
+ static int dummy_argc = 1;
+ static char * dummy_argv[] = {app_name, nullptr};
+
+ QCoreApplication (dummy_argc, dummy_argv).exec ();
}
else
#endif
{
- if (! glib_mainloop)
- {
- glib_mainloop = g_main_loop_new (nullptr, true);
- atexit ([] () { g_main_loop_unref (glib_mainloop); });
- }
-
- pthread_mutex_unlock (& mainloop_mutex);
+ glib_mainloop = g_main_loop_new (nullptr, true);
g_main_loop_run (glib_mainloop);
+ g_main_loop_unref (glib_mainloop);
+ glib_mainloop = nullptr;
}
}
EXPORT void mainloop_quit ()
{
- pthread_mutex_lock (& mainloop_mutex);
-
#ifdef USE_QT
if (aud_get_mainloop_type () == MainloopType::Qt)
{
- if (qt_mainloop)
- qt_mainloop->quit ();
+ qApp->quit ();
}
else
#endif
{
- if (glib_mainloop)
- g_main_loop_quit (glib_mainloop);
+ g_main_loop_quit (glib_mainloop);
}
-
- pthread_mutex_unlock (& mainloop_mutex);
}
diff --git a/src/libaudcore/mainloop.h b/src/libaudcore/mainloop.h
index ee78717..4c38eba 100644
--- a/src/libaudcore/mainloop.h
+++ b/src/libaudcore/mainloop.h
@@ -24,13 +24,8 @@
#ifndef LIBAUDCORE_MAINLOOP_H
#define LIBAUDCORE_MAINLOOP_H
-struct QueuedFuncHelper;
-struct QueuedFuncParams;
-
class QueuedFunc
{
- friend struct QueuedFuncHelper;
-
public:
typedef void (* Func) (void * data);
@@ -59,13 +54,13 @@ public:
~QueuedFunc ()
{ stop (); }
-private:
- struct Starter;
- struct Stopper;
+ // cancels any pending callbacks
+ // inhibits all future callbacks
+ // needed to allow safe shutdown of some (Qt!) main loops
+ static void inhibit_all ();
+private:
bool _running = false;
-
- void start (const QueuedFuncParams & params);
};
void mainloop_run ();
diff --git a/src/libaudcore/multihash.cc b/src/libaudcore/multihash.cc
index 160239c..81f9c83 100644
--- a/src/libaudcore/multihash.cc
+++ b/src/libaudcore/multihash.cc
@@ -162,12 +162,20 @@ EXPORT int MultiHash::lookup (const void * data, unsigned hash, AddFunc add,
EXPORT void MultiHash::iterate (FoundFunc func, void * state)
{
+ iterate (func, state, nullptr, nullptr);
+}
+
+EXPORT void MultiHash::iterate (FoundFunc func, void * state, FinalFunc final, void * fstate)
+{
for (TinyLock & lock : locks)
tiny_lock (& lock);
for (HashBase & channel : channels)
channel.iterate (func, state);
+ if (final)
+ final (fstate);
+
for (TinyLock & lock : locks)
tiny_unlock (& lock);
}
diff --git a/src/libaudcore/multihash.h b/src/libaudcore/multihash.h
index 663b756..e5bf35b 100644
--- a/src/libaudcore/multihash.h
+++ b/src/libaudcore/multihash.h
@@ -107,6 +107,7 @@ public:
typedef HashBase::Node Node;
typedef HashBase::MatchFunc MatchFunc;
typedef HashBase::FoundFunc FoundFunc;
+ typedef void (* FinalFunc) (void * state);
/* Callback. May create a new node representing <data> to be added to the
* table. Returns the new node or null. */
@@ -136,6 +137,11 @@ public:
* remove the node from the table. */
void iterate (FoundFunc func, void * state);
+ /* Variant of iterate() which runs a second callback after the iteration
+ * is complete, while the table is still locked. This is useful when some
+ * operation needs to be performed with the table in a known state. */
+ void iterate (FoundFunc func, void * state, FinalFunc final, void * fstate);
+
private:
static constexpr int Channels = 16; /* must be a power of two */
static constexpr int Shift = 24; /* bit shift for channel selection */
@@ -177,6 +183,10 @@ public:
void iterate (F func)
{ MultiHash::iterate (WrapIterate<F>::run, & func); }
+ template<class F, class Final>
+ void iterate (F func, Final final)
+ { MultiHash::iterate (WrapIterate<F>::run, & func, WrapFinal<Final>::run, & final); }
+
private:
static bool match_cb (const Node * node, const void * data)
{ return (static_cast<const Node_T *> (node))->match
@@ -204,6 +214,12 @@ private:
{ return (* static_cast<F *> (func))
(static_cast<Node_T *> (node)); }
};
+
+ template<class Final>
+ struct WrapFinal {
+ static void run (void * func)
+ { (* static_cast<Final *> (func)) (); }
+ };
};
/* Simpler single-thread hash table. */
diff --git a/src/libaudcore/objects.h b/src/libaudcore/objects.h
index facc0ba..214254a 100644
--- a/src/libaudcore/objects.h
+++ b/src/libaudcore/objects.h
@@ -90,14 +90,7 @@ public:
}
SmartPtr & operator= (SmartPtr && b)
- {
- if (this != & b)
- {
- capture (b.ptr);
- b.ptr = nullptr;
- }
- return * this;
- }
+ { return aud::move_assign (* this, std::move (b)); }
explicit operator bool () const
{ return (bool) ptr; }
@@ -172,15 +165,7 @@ public:
}
String & operator= (String && b)
- {
- if (this != & b)
- {
- raw_unref (raw);
- raw = b.raw;
- b.raw = nullptr;
- }
- return * this;
- }
+ { return aud::move_assign (* this, std::move (b)); }
bool operator== (const String & b) const
{ return raw_equal (raw, b.raw); }
@@ -208,21 +193,21 @@ private:
struct StringStack;
-// Mutable string buffer, allocated on a stack to allow fast allocation. The
-// price for this speed is that only the top string in the stack (i.e. the one
-// most recently allocated) can be resized or deleted. The string is always
-// null-terminated (i.e. str[str.len ()] == 0). Rules for the correct use of
-// StringBuf can be summarized as follows:
+// Mutable string buffer, allocated on a stack-like structure. The intent is
+// to provide fast allocation/deallocation for strings with a short lifespan.
+// Note that some usage patterns (for example, repeatedly appending to multiple
+// strings) can rapidly cause memory fragmentation and eventually lead to an
+// out-of-memory exception.
+//
+// Some usage guidelines:
//
// 1. Always declare StringBufs within function or block scope, never at file
// or class scope. Do not attempt to create a StringBuf with new or
// malloc().
-// 2. Only the first StringBuf declared in a function can be used as the
-// return value. It is possible to create a second StringBuf and then
-// transfer its contents to the first with steal(), but doing so carries
-// a performance penalty.
-// 3. Do not truncate the StringBuf by inserting null characters manually;
-// instead, use resize().
+// 2. If you need to return a StringBuf from a function that uses several
+// different strings internally, make sure all the other strings go out of
+// scope first and then call settle() on the string to be returned.
+// 3. Never transfer StringBuf objects across thread boundaries.
class StringBuf
{
@@ -232,10 +217,6 @@ public:
m_data (nullptr),
m_len (0) {}
- // A length of -1 means to use all available space. This can be useful when
- // the final length of the string is not known in advance, but keep in mind
- // that you will not be able to create any further StringBufs until you call
- // resize(). Also, the string will not be null-terminated in this case.
explicit StringBuf (int len) :
stack (nullptr),
m_data (nullptr),
@@ -254,17 +235,33 @@ public:
other.m_len = 0;
}
- // only allowed for top (or null) string
- ~StringBuf () noexcept (false);
+ StringBuf & operator= (StringBuf && other)
+ { return aud::move_assign (* this, std::move (other)); }
- // only allowed for top (or null) string
- void resize (int size);
- void insert (int pos, const char * s, int len = -1);
+ ~StringBuf ();
+
+ // Resizes to <len> bytes (not counting the terminating null byte) by
+ // appended uninitialized bytes or truncating. The resized string will be
+ // null-terminated unless <len> is -1. A length of -1 means to make the
+ // string as large as possible. This can be useful when the required length
+ // is not known in advance. However, it will be impossible to create any
+ // further StringBufs until resize() is called again.
+ void resize (int len);
+
+ // Inserts the substring <s> at the given position, or appends it if <pos>
+ // is -1. If <len> is -1, <s> is assumed to be null-terminated; otherwise,
+ // <len> indicates the number of bytes to insert. If <s> is a null pointer,
+ // uninitialized bytes are inserted and <len> must not be -1. A pointer to
+ // the inserted substring is returned for convenience.
+ char * insert (int pos, const char * s, int len = -1);
+
+ // Removes <len> bytes at the given position.
void remove (int pos, int len);
- // only allowed for top two strings (or when one string is null)
- void steal (StringBuf && other);
- void combine (StringBuf && other);
+ // Collapses any unused space preceding this string.
+ // Judicious use can combat memory fragmentation.
+ // Returns a move reference to allow e.g. "return str.settle();"
+ StringBuf && settle ();
int len () const
{ return m_len; }
@@ -272,6 +269,11 @@ public:
operator char * ()
{ return m_data; }
+ // deprecated, use assignment
+ void steal (StringBuf && other) __attribute__((deprecated));
+ // deprecated, use insert()
+ void combine (StringBuf && other) __attribute__((deprecated));
+
private:
StringStack * stack;
char * m_data;
diff --git a/src/libaudcore/output.cc b/src/libaudcore/output.cc
index e1a1015..191884d 100644
--- a/src/libaudcore/output.cc
+++ b/src/libaudcore/output.cc
@@ -449,12 +449,12 @@ bool output_open_audio (const String & filename, const Tuple & tuple,
void output_set_tuple (const Tuple & tuple)
{
- LOCK_ALL;
+ LOCK_MINOR;
if (s_input)
in_tuple = tuple.ref ();
- UNLOCK_ALL;
+ UNLOCK_MINOR;
}
void output_set_replay_gain (const ReplayGainInfo & info)
diff --git a/src/libaudcore/playback.cc b/src/libaudcore/playback.cc
index aea3b1b..ce8be6c 100644
--- a/src/libaudcore/playback.cc
+++ b/src/libaudcore/playback.cc
@@ -79,6 +79,7 @@ struct PlaybackInfo {
int stop_time = -1;
ReplayGainInfo gain {};
+ bool gain_valid = false;
int bitrate = 0;
int samplerate = 0;
@@ -263,7 +264,9 @@ static void end_cb (void *)
}
else
{
- if (failed_entries < 10)
+ // if 10 songs in a row have failed, or if the entire playlist
+ // (for playlists less than 10 songs) has failed, stop trying
+ if (failed_entries < aud::min (playlist.n_entries (), 10))
do_next ();
else
do_stop ();
@@ -314,6 +317,7 @@ static void run_playback ()
pb_info.time_offset = aud::max (0, pb_info.tuple.get_int (Tuple::StartTime));
pb_info.stop_time = aud::max (-1, pb_info.tuple.get_int (Tuple::EndTime) - pb_info.time_offset);
pb_info.gain = pb_info.tuple.get_replay_gain ();
+ pb_info.gain_valid = pb_info.tuple.has_replay_gain ();
// force initial seek if we are playing a segmented track
if (pb_info.time_offset > 0 && pb_control.seek < 0)
@@ -503,7 +507,8 @@ EXPORT void InputPlugin::open_audio (int format, int rate, int channels)
return;
}
- output_set_replay_gain (pb_info.gain);
+ if (pb_info.gain_valid)
+ output_set_replay_gain (pb_info.gain);
if (pb_control.paused)
output_pause (true);
@@ -524,6 +529,7 @@ EXPORT void InputPlugin::set_replay_gain (const ReplayGainInfo & gain)
{
lock ();
pb_info.gain = gain;
+ pb_info.gain_valid = true;
if (is_ready ())
output_set_replay_gain (gain);
diff --git a/src/libaudcore/playlist-data.cc b/src/libaudcore/playlist-data.cc
index 274a3e5..dd3a3d5 100644
--- a/src/libaudcore/playlist-data.cc
+++ b/src/libaudcore/playlist-data.cc
@@ -680,13 +680,13 @@ int PlaylistData::queue_get_entry (int at) const
int PlaylistData::queue_find_entry (int entry_num) const
{
auto entry = entry_at (entry_num);
- return entry->queued ? m_queued.find ((PlaylistEntry *) entry) : -1;
+ return (entry && entry->queued) ? m_queued.find ((PlaylistEntry *) entry) : -1;
}
void PlaylistData::queue_insert (int at, int entry_num)
{
auto entry = entry_at (entry_num);
- if (entry->queued)
+ if (! entry || entry->queued)
return;
if (at < 0 || at > m_queued.len ())
@@ -910,6 +910,38 @@ void PlaylistData::shuffle_reset ()
entry->shuffle_num = 0;
}
+Index<int> PlaylistData::shuffle_history () const
+{
+ Index<int> history;
+
+ // create a list of all entries in the shuffle list
+ for (auto & entry : m_entries)
+ {
+ if (entry->shuffle_num)
+ history.append (entry->number);
+ }
+
+ // sort by shuffle order
+ history.sort ([this] (int entry_a, int entry_b) {
+ return m_entries[entry_a]->shuffle_num - m_entries[entry_b]->shuffle_num;
+ });
+
+ return history;
+}
+
+void PlaylistData::shuffle_replay (const Index<int> & history)
+{
+ shuffle_reset ();
+
+ // replay the given history, entry by entry
+ for (int entry_num : history)
+ {
+ auto entry = entry_at (entry_num);
+ if (entry)
+ entry->shuffle_num = ++ m_last_shuffle_num;
+ }
+}
+
bool PlaylistData::prev_song ()
{
if (aud_get_bool (nullptr, "shuffle"))
diff --git a/src/libaudcore/playlist-data.h b/src/libaudcore/playlist-data.h
index c94a94c..663c565 100644
--- a/src/libaudcore/playlist-data.h
+++ b/src/libaudcore/playlist-data.h
@@ -94,6 +94,9 @@ public:
void set_position (int entry_num);
+ Index<int> shuffle_history () const;
+ void shuffle_replay (const Index<int> & history);
+
bool prev_song ();
bool next_song (bool repeat);
diff --git a/src/libaudcore/playlist-utils.cc b/src/libaudcore/playlist-utils.cc
index c017261..9492f29 100644
--- a/src/libaudcore/playlist-utils.cc
+++ b/src/libaudcore/playlist-utils.cc
@@ -239,9 +239,8 @@ static StringBuf make_playlist_path (int playlist)
if (! playlist)
return filename_build ({aud_get_path (AudPath::UserDir), "playlist.xspf"});
- StringBuf name = str_printf ("playlist_%02d.xspf", 1 + playlist);
- name.steal (filename_build ({aud_get_path (AudPath::UserDir), name}));
- return name;
+ return filename_build ({aud_get_path (AudPath::UserDir),
+ str_printf ("playlist_%02d.xspf", 1 + playlist)});
}
static void load_playlists_real ()
@@ -265,33 +264,23 @@ static void load_playlists_real ()
/* unique ID-based naming scheme */
StringBuf order_path = filename_build ({folder, "order"});
- char * order_string;
- Index<String> order;
-
- g_file_get_contents (order_path, & order_string, nullptr, nullptr);
- if (! order_string)
- goto DONE;
-
- order = str_list_to_index (order_string, " ");
- g_free (order_string);
+ auto order_string = VFSFile::read_file (order_path,
+ VFSReadOptions (VFS_APPEND_NULL | VFS_IGNORE_MISSING));
+ auto order = str_list_to_index (order_string.begin (), " ");
for (int i = 0; i < order.len (); i ++)
{
- const String & number = order[i];
+ const char * number = order[i];
- StringBuf name1 = str_concat ({number, ".audpl"});
- StringBuf name2 = str_concat ({number, ".xspf"});
-
- StringBuf path = filename_build ({folder, name1});
+ StringBuf path = filename_build ({folder, str_concat ({number, ".audpl"})});
if (! g_file_test (path, G_FILE_TEST_EXISTS))
- path.steal (filename_build ({folder, name2}));
+ path = filename_build ({folder, str_concat ({number, ".xspf"})});
PlaylistEx playlist = PlaylistEx::insert_with_stamp (count + i, atoi (number));
playlist.insert_flat_playlist (filename_to_uri (path));
playlist.set_modified (g_str_has_suffix (path, ".xspf"));
}
-DONE:
if (! Playlist::n_playlists ())
Playlist::insert_playlist (0);
}
@@ -325,21 +314,11 @@ static void save_playlists_real ()
StringBuf order_string = index_to_str_list (order, " ");
StringBuf order_path = filename_build ({folder, "order"});
+ auto old_order_string = VFSFile::read_file (order_path,
+ VFSReadOptions (VFS_APPEND_NULL | VFS_IGNORE_MISSING));
- char * old_order_string;
- g_file_get_contents (order_path, & old_order_string, nullptr, nullptr);
-
- if (! old_order_string || strcmp (old_order_string, order_string))
- {
- GError * error = nullptr;
- if (! g_file_set_contents (order_path, order_string, -1, & error))
- {
- AUDERR ("Cannot write to %s: %s\n", (const char *) order_path, error->message);
- g_error_free (error);
- }
- }
-
- g_free (old_order_string);
+ if (strcmp (old_order_string.begin (), order_string))
+ VFSFile::write_file (order_path, (const char *) order_string, order_string.len ());
/* clean up deleted playlists and files from old naming scheme */
diff --git a/src/libaudcore/playlist.cc b/src/libaudcore/playlist.cc
index fff37e1..6b64808 100644
--- a/src/libaudcore/playlist.cc
+++ b/src/libaudcore/playlist.cc
@@ -264,6 +264,11 @@ EXPORT bool Playlist::update_pending_any ()
RETURN (pending);
}
+EXPORT void Playlist::process_pending_update ()
+{
+ update (nullptr);
+}
+
EXPORT bool Playlist::scan_in_progress () const
{
ENTER_GET_PLAYLIST (false);
@@ -1152,6 +1157,16 @@ void playlist_save_state ()
fprintf (handle, "position %d\n", playlist->position ());
+ /* save shuffle history */
+ auto history = playlist->shuffle_history ();
+
+ for (int i = 0; i < history.len (); i += 16)
+ {
+ int count = aud::min (16, history.len () - i);
+ auto list = int_array_to_str (& history[i], count);
+ fprintf (handle, "shuffle %s\n", (const char *) list);
+ }
+
/* resume state is stored per-playlist for historical reasons */
bool is_playing = (playlist->id () == playing_id);
fprintf (handle, "resume-state %d\n", (is_playing && paused) ? ResumePause : ResumePlay);
@@ -1205,6 +1220,19 @@ void playlist_load_state ()
parser.next ();
}
+ /* restore shuffle history */
+ Index<int> history;
+
+ for (String list; (list = parser.get_str ("shuffle")); parser.next ())
+ {
+ auto split = str_list_to_index (list, ", ");
+ for (auto & str : split)
+ history.append (str_to_int (str));
+ }
+
+ if (history.len ())
+ playlist->shuffle_replay (history);
+
/* resume state is stored per-playlist for historical reasons */
int resume_state = ResumePlay;
if (parser.get_int ("resume-state", resume_state))
diff --git a/src/libaudcore/playlist.h b/src/libaudcore/playlist.h
index 18292dc..e9270c1 100644
--- a/src/libaudcore/playlist.h
+++ b/src/libaudcore/playlist.h
@@ -329,6 +329,9 @@ public:
bool update_pending () const;
static bool update_pending_any ();
+ /* Immediately calls any pending "playlist update" hook. Use cautiously. */
+ static void process_pending_update ();
+
/* May be called within the "playlist update" hook to determine the update
* level and number of entries changed in a playlist. */
Update update_detail () const;
diff --git a/src/libaudcore/probe.cc b/src/libaudcore/probe.cc
index 81bac55..5c57ef8 100644
--- a/src/libaudcore/probe.cc
+++ b/src/libaudcore/probe.cc
@@ -56,6 +56,32 @@ InputPlugin * load_input_plugin (PluginHandle * decoder, String * error)
return ip;
}
+/* figure out some basic info without opening the file */
+int probe_by_filename (const char * filename)
+{
+ int flags = 0;
+ auto & list = aud_plugin_list (PluginType::Input);
+
+ StringBuf scheme = uri_get_scheme (filename);
+ StringBuf ext = uri_get_extension (filename);
+
+ for (PluginHandle * plugin : list)
+ {
+ if (! aud_plugin_get_enabled (plugin))
+ continue;
+
+ if ((scheme && input_plugin_has_key (plugin, InputKey::Scheme, scheme)) ||
+ (ext && input_plugin_has_key (plugin, InputKey::Ext, ext)))
+ {
+ flags |= PROBE_FLAG_HAS_DECODER;
+ if (input_plugin_has_subtunes (plugin))
+ flags |= PROBE_FLAG_MIGHT_HAVE_SUBTUNES;
+ }
+ }
+
+ return flags;
+}
+
EXPORT PluginHandle * aud_file_find_decoder (const char * filename, bool fast,
VFSFile & file, String * error)
{
diff --git a/src/libaudcore/ringbuf.h b/src/libaudcore/ringbuf.h
index 5fd6aef..5206588 100644
--- a/src/libaudcore/ringbuf.h
+++ b/src/libaudcore/ringbuf.h
@@ -56,15 +56,6 @@ public:
b.m_len = 0;
}
- void steal (RingBufBase && b, aud::EraseFunc erase_func)
- {
- if (this != & b)
- {
- destroy (erase_func);
- new (this) RingBufBase (std::move (b));
- }
- }
-
// allocated size of the buffer
int size () const
{ return m_size; }
@@ -119,8 +110,8 @@ public:
RingBuf (RingBuf && b) :
RingBufBase (std::move (b)) {}
- void operator= (RingBuf && b)
- { steal (std::move (b), aud::erase_func<T> ()); }
+ RingBuf & operator= (RingBuf && b)
+ { return aud::move_assign (* this, std::move (b)); }
int size () const
{ return cooked (RingBufBase::size ()); }
diff --git a/src/libaudcore/runtime.cc b/src/libaudcore/runtime.cc
index 7eb8e5e..382e773 100644
--- a/src/libaudcore/runtime.cc
+++ b/src/libaudcore/runtime.cc
@@ -126,8 +126,8 @@ static StringBuf get_path_to_self ()
throw std::bad_alloc ();
buf.resize (lenw * sizeof (wchar_t));
- buf.steal (str_convert (buf, buf.len (), UTF16_NATIVE, "UTF-8"));
- return buf;
+ buf = str_convert (buf, buf.len (), UTF16_NATIVE, "UTF-8");
+ return buf.settle ();
#elif defined __APPLE__
@@ -169,22 +169,22 @@ static String relocate_path (const char * path, const char * from, const char *
static void set_default_paths ()
{
- aud_paths[AudPath::BinDir] = String (HARDCODE_BINDIR);
- aud_paths[AudPath::DataDir] = String (HARDCODE_DATADIR);
- aud_paths[AudPath::PluginDir] = String (HARDCODE_PLUGINDIR);
- aud_paths[AudPath::LocaleDir] = String (HARDCODE_LOCALEDIR);
- aud_paths[AudPath::DesktopFile] = String (HARDCODE_DESKTOPFILE);
- aud_paths[AudPath::IconFile] = String (HARDCODE_ICONFILE);
+ aud_paths[AudPath::BinDir] = String (INSTALL_BINDIR);
+ aud_paths[AudPath::DataDir] = String (INSTALL_DATADIR);
+ aud_paths[AudPath::PluginDir] = String (INSTALL_PLUGINDIR);
+ aud_paths[AudPath::LocaleDir] = String (INSTALL_LOCALEDIR);
+ aud_paths[AudPath::DesktopFile] = String (INSTALL_DESKTOPFILE);
+ aud_paths[AudPath::IconFile] = String (INSTALL_ICONFILE);
}
static void set_install_paths ()
{
- StringBuf bindir = filename_normalize (str_copy (HARDCODE_BINDIR));
- StringBuf datadir = filename_normalize (str_copy (HARDCODE_DATADIR));
- StringBuf plugindir = filename_normalize (str_copy (HARDCODE_PLUGINDIR));
- StringBuf localedir = filename_normalize (str_copy (HARDCODE_LOCALEDIR));
- StringBuf desktopfile = filename_normalize (str_copy (HARDCODE_DESKTOPFILE));
- StringBuf iconfile = filename_normalize (str_copy (HARDCODE_ICONFILE));
+ StringBuf bindir = filename_normalize (str_copy (INSTALL_BINDIR));
+ StringBuf datadir = filename_normalize (str_copy (INSTALL_DATADIR));
+ StringBuf plugindir = filename_normalize (str_copy (INSTALL_PLUGINDIR));
+ StringBuf localedir = filename_normalize (str_copy (INSTALL_LOCALEDIR));
+ StringBuf desktopfile = filename_normalize (str_copy (INSTALL_DESKTOPFILE));
+ StringBuf iconfile = filename_normalize (str_copy (INSTALL_ICONFILE));
StringBuf from = str_copy (bindir);
@@ -197,7 +197,7 @@ static void set_install_paths ()
return;
}
- to.steal (filename_normalize (std::move (to)));
+ to = filename_normalize (std::move (to));
const char * base = last_path_element (to);
diff --git a/src/libaudcore/runtime.h b/src/libaudcore/runtime.h
index 30ad950..1a1a352 100644
--- a/src/libaudcore/runtime.h
+++ b/src/libaudcore/runtime.h
@@ -128,6 +128,7 @@ void aud_leak_check ();
String aud_history_get (int entry);
void aud_history_add (const char * path);
+void aud_history_clear ();
void aud_output_reset (OutputReset type);
diff --git a/src/libaudcore/stringbuf.cc b/src/libaudcore/stringbuf.cc
index fc646f6..dd923ca 100644
--- a/src/libaudcore/stringbuf.cc
+++ b/src/libaudcore/stringbuf.cc
@@ -19,6 +19,7 @@
#include <pthread.h>
#include <stdlib.h>
+#include <stdint.h>
#include <string.h>
#include <new>
@@ -34,18 +35,34 @@
#endif
#endif
+struct StringHeader
+{
+ StringHeader * next, * prev;
+ int len;
+};
+
struct StringStack
{
static constexpr int Size = 1048576; // 1 MB
- char * top;
+ StringHeader * top;
char buf[Size - sizeof top];
};
-// adds one byte for null character and rounds up to word boundary
-static constexpr int align (int len)
+static constexpr intptr_t align (intptr_t ptr, intptr_t size)
{
- return (len + sizeof (void *)) & ~(sizeof (void *) - 1);
+ return (ptr + (size - 1)) / size * size;
+}
+
+static StringHeader * align_after (StringStack * stack, StringHeader * prev_header)
+{
+ char * base;
+ if (prev_header)
+ base = (char *) prev_header + sizeof (StringHeader) + prev_header->len + 1;
+ else
+ base = stack->buf;
+
+ return (StringHeader *) align ((intptr_t) base, alignof (StringHeader));
}
static pthread_key_t key;
@@ -99,7 +116,7 @@ static StringStack * get_stack ()
throw std::bad_alloc ();
#endif
- stack->top = stack->buf;
+ stack->top = nullptr;
pthread_setspecific (key, stack);
}
@@ -109,82 +126,117 @@ static StringStack * get_stack ()
EXPORT void StringBuf::resize (int len)
{
if (! stack)
- {
stack = get_stack ();
- m_data = stack->top;
- }
- else
- {
- if (m_data + align (m_len) != stack->top)
- throw std::bad_alloc ();
- }
- if (len < 0)
+ StringHeader * header = nullptr;
+ bool need_alloc = true;
+
+ if (m_data)
{
- stack->top = stack->buf + sizeof stack->buf;
- m_len = stack->top - m_data - 1;
+ header = (StringHeader *) (m_data - sizeof (StringHeader));
- if (m_len < 0)
- throw std::bad_alloc ();
+ /* check if there is enough space in the current location */
+ char * limit = header->next ? (char *) header->next : (char *) stack + sizeof (StringStack);
+ int max_len = limit - 1 - m_data;
+
+ if ((len < 0 && ! header->next) || (len >= 0 && len < max_len))
+ {
+ m_len = header->len = (len < 0) ? max_len : len;
+ need_alloc = false;
+ }
}
- else
+
+ if (need_alloc)
{
- stack->top = m_data + align (len);
+ /* allocate a new string at the top of the stack */
+ StringHeader * new_header = align_after (stack, stack->top);
+ char * new_data = (char *) new_header + sizeof (StringHeader);
+ char * limit = (char *) stack + sizeof (StringStack);
+ int max_len = limit - 1 - new_data;
- if (stack->top - stack->buf > (int) sizeof stack->buf)
+ if (max_len < aud::max (len, 0))
throw std::bad_alloc ();
- m_data[len] = 0;
- m_len = len;
+ int new_len = (len < 0) ? max_len : len;
+
+ if (stack->top)
+ stack->top->next = new_header;
+
+ new_header->prev = stack->top;
+ new_header->next = nullptr;
+ new_header->len = new_len;
+
+ stack->top = new_header;
+
+ /* move the old data, if any */
+ if (m_data)
+ {
+ int bytes_to_copy = aud::min (m_len, new_len);
+ memcpy (new_data, m_data, bytes_to_copy);
+
+ if (header->prev)
+ header->prev->next = header->next;
+
+ /* we know header != stack->top */
+ header->next->prev = header->prev;
+ }
+
+ m_data = new_data;
+ m_len = new_len;
}
+
+ /* Null-terminate the string except when the maximum length was requested
+ * (to avoid paging in the entire 1 MB stack prematurely). The caller is
+ * expected to follow up with a more realistic resize() in this case. */
+ if (len >= 0)
+ m_data[len] = 0;
}
-EXPORT StringBuf::~StringBuf () noexcept (false)
+EXPORT StringBuf::~StringBuf ()
{
if (m_data)
{
- if (m_data + align (m_len) != stack->top)
- throw std::bad_alloc ();
+ auto header = (StringHeader *) (m_data - sizeof (StringHeader));
+
+ if (header->prev)
+ header->prev->next = header->next;
- stack->top = m_data;
+ if (header == stack->top)
+ stack->top = header->prev;
+ else
+ header->next->prev = header->prev;
}
}
EXPORT void StringBuf::steal (StringBuf && other)
{
- if (other.m_data)
+ (* this = std::move (other)).settle ();
+}
+
+EXPORT StringBuf && StringBuf::settle ()
+{
+ if (m_data)
{
- if (m_data)
- {
- if (m_data + align (m_len) != other.m_data ||
- other.m_data + align (other.m_len) != stack->top)
- throw std::bad_alloc ();
+ /* collapse any space preceding this string */
+ auto header = (StringHeader *) (m_data - sizeof (StringHeader));
+ StringHeader * new_header = align_after (stack, header->prev);
- m_len = other.m_len;
- memmove (m_data, other.m_data, m_len + 1);
- stack->top = m_data + align (m_len);
- }
- else
+ if (new_header != header)
{
- stack = other.stack;
- m_data = other.m_data;
- m_len = other.m_len;
- }
+ if (header->prev)
+ header->prev->next = new_header;
- other.stack = nullptr;
- other.m_data = nullptr;
- other.m_len = 0;
- }
- else
- {
- if (m_data)
- {
- this->~StringBuf ();
- stack = nullptr;
- m_data = nullptr;
- m_len = 0;
+ if (header == stack->top)
+ stack->top = new_header;
+ else
+ header->next->prev = new_header;
+
+ memmove (new_header, header, sizeof (StringHeader) + m_len + 1);
+ m_data = (char *) new_header + sizeof (StringHeader);
}
}
+
+ return std::move (* this);
}
EXPORT void StringBuf::combine (StringBuf && other)
@@ -192,29 +244,12 @@ EXPORT void StringBuf::combine (StringBuf && other)
if (! other.m_data)
return;
- if (m_data)
- {
- if (m_data + align (m_len) != other.m_data ||
- other.m_data + align (other.m_len) != stack->top)
- throw std::bad_alloc ();
-
- memmove (m_data + m_len, other.m_data, other.m_len + 1);
- m_len += other.m_len;
- stack->top = m_data + align (m_len);
- }
- else
- {
- stack = other.stack;
- m_data = other.m_data;
- m_len = other.m_len;
- }
-
- other.stack = nullptr;
- other.m_data = nullptr;
- other.m_len = 0;
+ insert (m_len, other.m_data, other.m_len);
+ other = StringBuf ();
+ settle ();
}
-EXPORT void StringBuf::insert (int pos, const char * s, int len)
+EXPORT char * StringBuf::insert (int pos, const char * s, int len)
{
int len0 = m_len;
@@ -225,7 +260,11 @@ EXPORT void StringBuf::insert (int pos, const char * s, int len)
resize (len0 + len);
memmove (m_data + pos + len, m_data + pos, len0 - pos);
- memcpy (m_data + pos, s, len);
+
+ if (s)
+ memcpy (m_data + pos, s, len);
+
+ return m_data + pos;
}
EXPORT void StringBuf::remove (int pos, int len)
diff --git a/src/libaudcore/templates.h b/src/libaudcore/templates.h
index d14c3d8..cc1d533 100644
--- a/src/libaudcore/templates.h
+++ b/src/libaudcore/templates.h
@@ -98,6 +98,20 @@ inline T from_ptr (void * v)
return u.t;
}
+// Move-assignment implemented via move-constructor
+// ================================================
+
+template<class T>
+T & move_assign (T & a, T && b)
+{
+ if (& a != & b)
+ {
+ a.~T ();
+ new (& a) T (std::move (b));
+ }
+ return a;
+}
+
// Function wrappers (or "casts") for interaction with C-style APIs
// ================================================================
@@ -116,6 +130,9 @@ void typed_func (T * obj)
template<class T, void (T::* func) ()>
static void obj_member (void * obj)
{ (((T *) obj)->* func) (); }
+template<class T, void (T::* func) () const>
+static void obj_member (void * obj)
+ { (((T *) obj)->* func) (); }
// Wrapper class allowing enumerations to be used as array indexes;
// the enumeration must begin with zero and have a "count" constant
diff --git a/src/libaudcore/tests/test-mainloop.cc b/src/libaudcore/tests/test-mainloop.cc
index 3eef667..384c533 100644
--- a/src/libaudcore/tests/test-mainloop.cc
+++ b/src/libaudcore/tests/test-mainloop.cc
@@ -33,12 +33,17 @@ MainloopType aud_get_mainloop_type ()
}
static QueuedFunc counters[70];
-static QueuedFunc timer, delayed, restart;
+static QueuedFunc timer, delayed;
static int count;
-static int restart_count;
static pthread_t main_thread;
+static void never_called (void * data)
+{
+ bool called = true;
+ assert (! called);
+}
+
static void count_up (void * data)
{
assert (pthread_self () == main_thread);
@@ -54,14 +59,6 @@ static void count_up (void * data)
printf ("%d%c", count, (count % 10) ? ' ' : '\n');
}
-static void count_restart (void * data)
-{
- assert (pthread_self () == main_thread);
- assert (data == nullptr);
-
- restart_count ++;
-}
-
static void count_down (void * data)
{
assert (pthread_self () == main_thread);
@@ -76,11 +73,12 @@ static void count_down (void * data)
if (! count)
{
- timer.stop ();
+ // stop the timer
+ // queue up an idle call so it's pending at shutdown
+ // initiate the shutdown sequence
+ timer.queue (never_called, nullptr);
+ QueuedFunc::inhibit_all ();
mainloop_quit ();
-
- // check queueing an event while main loop is restarting
- restart.queue (count_restart, nullptr);
}
}
@@ -94,12 +92,6 @@ static void check_count (void * data)
printf ("CHECK: %d\n", count);
}
-static void never_called (void * data)
-{
- bool called = true;
- assert (! called);
-}
-
static void * worker (void * data)
{
// queue some more idle calls from a secondary thread
@@ -119,39 +111,33 @@ int main (int argc, const char * * argv)
main_thread = pthread_self ();
- for (int j = 0; j < 2; j ++)
- {
- // queue up a bunch of idle calls
- for (int i = 0; i < 50; i ++)
- counters[i].queue (count_up, (void *) (size_t) (i - 30));
+ // queue up a bunch of idle calls
+ for (int i = 0; i < 50; i ++)
+ counters[i].queue (count_up, (void *) (size_t) (i - 30));
- // stop some of them
- for (int i = 10; i < 30; i ++)
- counters[i].stop ();
+ // stop some of them
+ for (int i = 10; i < 30; i ++)
+ counters[i].stop ();
- // restart some that were stopped and some that weren't
- for (int i = 0; i < 20; i ++)
- counters[i].queue (count_up, (void *) (size_t) (20 + i));
+ // restart some that were stopped and some that weren't
+ for (int i = 0; i < 20; i ++)
+ counters[i].queue (count_up, (void *) (size_t) (20 + i));
- // start a countdown timer at 10 Hz
- timer.start (100, count_down, & count);
+ // start a countdown timer at 10 Hz
+ timer.start (100, count_down, & count);
- // queue up a call and then immediately delete the QueuedFunc
- QueuedFunc ().queue (never_called, nullptr);
+ // queue up a call and then immediately delete the QueuedFunc
+ QueuedFunc ().queue (never_called, nullptr);
- pthread_t thread;
- pthread_create (& thread, nullptr, worker, nullptr);
+ pthread_t thread;
+ pthread_create (& thread, nullptr, worker, nullptr);
- mainloop_run ();
+ mainloop_run ();
- pthread_join (thread, nullptr);
-
- // check that the timer reports being stopped
- assert (! timer.running ());
- }
+ pthread_join (thread, nullptr);
- // check that events queued during restart are processed
- assert (restart_count == 1);
+ // check that the timer reports being stopped
+ assert (! timer.running ());
return 0;
}
diff --git a/src/libaudcore/tests/test.cc b/src/libaudcore/tests/test.cc
index a7a6ba8..d0ed8fe 100644
--- a/src/libaudcore/tests/test.cc
+++ b/src/libaudcore/tests/test.cc
@@ -454,6 +454,68 @@ static void test_ringbuf ()
string_leak_check ();
}
+static StringBuf str_recursive_insert (const char * str, int level)
+{
+ StringBuf buf = str_copy (str);
+ buf.insert (buf.len () / 2, str);
+
+ if (level == 1)
+ return buf;
+
+ // intentionally causing fragmentation here
+ return str_recursive_insert (buf, level - 1);
+}
+
+static StringBuf str_repeated_nest (const char * str, int level)
+{
+ StringBuf buf1 = str_copy (str);
+ StringBuf buf2 = str_copy (str);
+
+ while (level -- > 0)
+ {
+ buf1.insert (buf1.len () / 2, buf2);
+ buf2.insert (buf2.len () / 2, buf1);
+ }
+
+ // intentionally causing fragmentation here
+ return buf2;
+}
+
+static void test_stringbuf ()
+{
+ char expect[262145];
+
+ StringBuf str1 = str_recursive_insert ("ab", 17).settle ();
+
+ memset (expect, 'a', 121393);
+ memset (expect + 121393, 'b', 121393);
+ expect[242786] = 0;
+
+ assert (! strcmp (str_repeated_nest ("ab", 12), expect));
+
+ memset (expect, 'a', 131072);
+ memset (expect + 131072, 'b', 131072);
+ expect[262144] = 0;
+
+ assert (! strcmp (str1, expect));
+}
+
+static void test_str_printf ()
+{
+ StringBuf problem = str_printf ("%d", 6);
+ const char * loc1 = problem;
+ str_append_printf (problem, " * %d", 7);
+ const char * loc2 = problem;
+
+ assert (loc1 == loc2);
+ assert (! strcmp (problem, "6 * 7"));
+
+ StringBuf answer = str_printf ("%d", 6 * 7);
+ str_append_printf (problem, " = %s", (const char *) answer);
+
+ assert (! strcmp (problem, "6 * 7 = 42"));
+}
+
int main ()
{
test_audio_conversion ();
@@ -462,6 +524,8 @@ int main ()
test_filename_split ();
test_tuple_formats ();
test_ringbuf ();
+ test_stringbuf ();
+ test_str_printf ();
return 0;
}
diff --git a/src/libaudcore/tuple-compiler.cc b/src/libaudcore/tuple-compiler.cc
index 895cb9c..b8d50cd 100644
--- a/src/libaudcore/tuple-compiler.cc
+++ b/src/libaudcore/tuple-compiler.cc
@@ -147,7 +147,7 @@ static StringBuf get_item (const char * & str, char endch, bool & literal)
{
if (! literal)
{
- buf.steal (StringBuf ());
+ buf = StringBuf (); // release space before AUDWARN
AUDWARN ("Unexpected string literal at '%s'.\n", s);
return StringBuf ();
}
@@ -166,7 +166,7 @@ static StringBuf get_item (const char * & str, char endch, bool & literal)
if (! * s)
{
- buf.steal (StringBuf ());
+ buf = StringBuf (); // release space before AUDWARN
AUDWARN ("Unterminated string literal.\n");
return StringBuf ();
}
@@ -192,7 +192,7 @@ static StringBuf get_item (const char * & str, char endch, bool & literal)
if (* s != endch)
{
- buf.steal (StringBuf ());
+ buf = StringBuf (); // release space before AUDWARN
AUDWARN ("Expected '%c' at '%s'.\n", endch, s);
return StringBuf ();
}
@@ -361,7 +361,7 @@ static bool compile_expression (Index<Node> & nodes, const char * & expression)
if (! * c)
{
- buf.steal (StringBuf ());
+ buf = StringBuf (); // release space before AUDWARN
AUDWARN ("Incomplete escaped character.\n");
return false;
}
@@ -428,7 +428,7 @@ static void eval_expression (const Index<Node> & nodes, const Tuple & tuple, Str
break;
case Tuple::Int:
- out.combine (int_to_str (tmpi));
+ str_insert_int (out, -1, tmpi);
break;
default:
diff --git a/src/libaudcore/tuple.cc b/src/libaudcore/tuple.cc
index 4cd29b6..603a475 100644
--- a/src/libaudcore/tuple.cc
+++ b/src/libaudcore/tuple.cc
@@ -547,18 +547,18 @@ EXPORT void Tuple::set_format (const char * format, int chans, int rate, int bra
if (chans > 0)
{
if (chans == 1)
- buf.insert (-1, _("Mono"));
+ buf = str_copy (_("Mono"));
else if (chans == 2)
- buf.insert (-1, _("Stereo"));
+ buf = str_copy (_("Stereo"));
else
- buf.combine (str_printf (dngettext (PACKAGE, "%d channel", "%d channels", chans), chans));
+ buf = str_printf (dngettext (PACKAGE, "%d channel", "%d channels", chans), chans);
if (rate > 0)
buf.insert (-1, ", ");
}
if (rate > 0)
- buf.combine (str_printf ("%d kHz", rate / 1000));
+ str_append_printf (buf, "%d kHz", rate / 1000);
if (buf[0])
set_str (Quality, buf);
@@ -592,6 +592,14 @@ EXPORT void Tuple::set_gain (Field field, Field unit_field, const char * str)
set_int (unit_field, 1000000);
}
+/* combining this with get_replay_gain() would be cleaner but would
+ * require adding a validity flag to ReplayGainInfo, breaking ABI */
+EXPORT bool Tuple::has_replay_gain () const
+{
+ return get_int (GainDivisor) > 0 &&
+ (data->is_set (AlbumGain) || data->is_set (TrackGain));
+}
+
EXPORT ReplayGainInfo Tuple::get_replay_gain () const
{
ReplayGainInfo gain {};
@@ -604,18 +612,36 @@ EXPORT ReplayGainInfo Tuple::get_replay_gain () const
if (gain_unit > 0)
{
- if (data->is_set (AlbumGain))
+ bool have_album = data->is_set (AlbumGain);
+ bool have_track = data->is_set (TrackGain);
+
+ if (have_album)
gain.album_gain = get_int (AlbumGain) / (float) gain_unit;
- if (data->is_set (TrackGain))
+ if (have_track)
gain.track_gain = get_int (TrackGain) / (float) gain_unit;
+
+ /* fill in missing information if we can */
+ if (! have_album && have_track)
+ gain.album_gain = gain.track_gain;
+ if (have_album && ! have_track)
+ gain.track_gain = gain.album_gain;
}
if (peak_unit > 0)
{
- if (data->is_set (AlbumPeak))
+ bool have_album = data->is_set (AlbumPeak);
+ bool have_track = data->is_set (TrackPeak);
+
+ if (have_album)
gain.album_peak = get_int (AlbumPeak) / (float) peak_unit;
- if (data->is_set (TrackPeak))
+ if (have_track)
gain.track_peak = get_int (TrackPeak) / (float) peak_unit;
+
+ /* fill in missing information if we can */
+ if (! have_album && have_track)
+ gain.album_peak = gain.track_peak;
+ if (have_album && ! have_track)
+ gain.track_peak = gain.album_peak;
}
return gain;
diff --git a/src/libaudcore/tuple.h b/src/libaudcore/tuple.h
index 6d45d73..b7766d6 100644
--- a/src/libaudcore/tuple.h
+++ b/src/libaudcore/tuple.h
@@ -125,15 +125,7 @@ public:
}
Tuple & operator= (Tuple && b)
- {
- if (this != & b)
- {
- this->~Tuple ();
- data = b.data;
- b.data = nullptr;
- }
- return * this;
- }
+ { return aud::move_assign (* this, std::move (b)); }
bool operator== (const Tuple & b) const;
bool operator!= (const Tuple & b) const
@@ -202,6 +194,8 @@ public:
/* Sets a Replay Gain field pair from a decimal string. */
void set_gain (Field field, Field unit_field, const char * str);
+ /* Returns true if minimal ReplayGainInfo is present. */
+ bool has_replay_gain () const;
/* Fills ReplayGainInfo struct from various fields. */
ReplayGainInfo get_replay_gain () const;
diff --git a/src/libaudcore/vfs.cc b/src/libaudcore/vfs.cc
index 8748fd4..ccbd1aa 100644
--- a/src/libaudcore/vfs.cc
+++ b/src/libaudcore/vfs.cc
@@ -41,14 +41,8 @@ static TransportPlugin * lookup_transport (const char * filename,
String & error, bool * custom_input = nullptr)
{
StringBuf scheme = uri_get_scheme (filename);
- if (! scheme)
- {
- AUDERR ("Invalid URI: %s\n", filename);
- error = String (_("Invalid URI"));
- return nullptr;
- }
- if (! strcmp (scheme, "file"))
+ if (! scheme || ! strcmp (scheme, "file"))
return & local_transport;
if (! strcmp (scheme, "stdin"))
return & stdin_transport;
@@ -177,7 +171,7 @@ EXPORT int VFSFile::fseek (int64_t offset, VFSSeekType whence)
whence == VFS_SEEK_CUR ? "current" : whence == VFS_SEEK_SET ? "beginning" :
whence == VFS_SEEK_END ? "end" : "invalid");
- if (! m_impl->fseek (offset, whence))
+ if (m_impl->fseek (offset, whence) == 0)
return 0;
AUDDBG ("<%p> seek failed!\n", m_impl.get ());
@@ -226,7 +220,7 @@ EXPORT int VFSFile::ftruncate (int64_t length)
{
AUDDBG ("<%p> truncate to %" PRId64 "\n", m_impl.get (), length);
- if (! m_impl->ftruncate (length))
+ if (m_impl->ftruncate (length) == 0)
return 0;
AUDDBG ("<%p> truncate failed!\n", m_impl.get ());
@@ -238,7 +232,7 @@ EXPORT int VFSFile::fflush ()
{
AUDDBG ("<%p> flush\n", m_impl.get ());
- if (! m_impl->fflush ())
+ if (m_impl->fflush () == 0)
return 0;
AUDDBG ("<%p> flush failed!\n", m_impl.get ());
@@ -391,3 +385,35 @@ EXPORT Index<String> VFSFile::read_folder (const char * filename, String & error
auto tp = lookup_transport (filename, error);
return tp ? tp->read_folder (filename, error) : Index<String> ();
}
+
+EXPORT Index<char> VFSFile::read_file (const char * filename, VFSReadOptions options)
+{
+ Index<char> text;
+
+ if (! (options & VFS_IGNORE_MISSING) || test_file (filename, VFS_EXISTS))
+ {
+ VFSFile file (filename, "r");
+ if (file)
+ text = file.read_all ();
+ else
+ AUDERR ("Cannot open %s for reading: %s\n", filename, file.error ());
+ }
+
+ if ((options & VFS_APPEND_NULL))
+ text.append (0);
+
+ return text;
+}
+
+EXPORT bool VFSFile::write_file (const char * filename, const void * data, int64_t len)
+{
+ bool written = false;
+
+ VFSFile file (filename, "w");
+ if (file)
+ written = (file.fwrite (data, 1, len) == len && file.fflush () == 0);
+ else
+ AUDERR ("Cannot open %s for writing: %s\n", filename, file.error ());
+
+ return written;
+}
diff --git a/src/libaudcore/vfs.h b/src/libaudcore/vfs.h
index e51cf84..e7f9ecd 100644
--- a/src/libaudcore/vfs.h
+++ b/src/libaudcore/vfs.h
@@ -42,6 +42,11 @@ enum VFSFileTest {
VFS_NO_ACCESS = (1 << 5)
};
+enum VFSReadOptions {
+ VFS_APPEND_NULL = (1 << 0),
+ VFS_IGNORE_MISSING = (1 << 1)
+};
+
enum VFSSeekType {
VFS_SEEK_SET = 0,
VFS_SEEK_CUR = 1,
@@ -163,6 +168,10 @@ public:
/* returns a sorted list of folder entries (as full URIs) */
static Index<String> read_folder (const char * filename, String & error);
+ /* convenience functions to read/write entire files */
+ static Index<char> read_file (const char * filename, VFSReadOptions options);
+ static bool write_file (const char * filename, const void * data, int64_t len);
+
private:
String m_filename, m_error;
SmartPtr<VFSImpl> m_impl;
diff --git a/src/libaudcore/vis-runner.cc b/src/libaudcore/vis-runner.cc
index 8574a96..4f142ca 100644
--- a/src/libaudcore/vis-runner.cc
+++ b/src/libaudcore/vis-runner.cc
@@ -34,8 +34,9 @@
struct VisNode : public ListNode
{
- explicit VisNode (int channels) :
+ VisNode (int channels, int time) :
channels (channels),
+ time (time),
data (new float[channels * FRAMES_PER_NODE]) {}
~VisNode ()
@@ -112,7 +113,8 @@ static void flush_locked ()
vis_list.clear ();
vis_pool.clear ();
- queued_clear.queue (send_clear, nullptr);
+ if (enabled)
+ queued_clear.queue (send_clear, nullptr);
}
void vis_runner_flush ()
@@ -193,11 +195,11 @@ void vis_runner_pass_audio (int time, const Index<float> & data, int channels, i
{
assert (current_node->channels == channels);
vis_pool.remove (current_node);
+ current_node->time = node_time;
}
else
- current_node = new VisNode (channels);
+ current_node = new VisNode (channels, node_time);
- current_node->time = node_time;
current_frames = 0;
}
diff --git a/src/libaudgui/Makefile b/src/libaudgui/Makefile
index b082d02..c0589ce 100644
--- a/src/libaudgui/Makefile
+++ b/src/libaudgui/Makefile
@@ -7,6 +7,7 @@ SRCS = about.cc \
eq-preset.cc \
equalizer.cc \
file-opener.cc \
+ images.c \
infopopup.cc \
infowin.cc \
init.cc \
@@ -35,6 +36,8 @@ INCLUDES = libaudgui.h \
list.h \
menu.h
+CLEAN = images.c images.h
+
include ../../buildsys.mk
include ../../extra.mk
@@ -53,3 +56,10 @@ LIBS := -L../libaudcore -laudcore \
${LIBS} -lm \
${GLIB_LIBS} \
${GTK_LIBS}
+
+pre-depend: images.c images.h
+
+images.h: images.gresource.xml
+ glib-compile-resources --sourcedir=../../images --generate-header --target=images.h images.gresource.xml
+images.c: images.gresource.xml
+ glib-compile-resources --sourcedir=../../images --generate-source --target=images.c images.gresource.xml
diff --git a/src/libaudgui/about.cc b/src/libaudgui/about.cc
index dcc025b..9787871 100644
--- a/src/libaudgui/about.cc
+++ b/src/libaudgui/about.cc
@@ -22,13 +22,14 @@
#include <libaudcore/audstrings.h>
#include <libaudcore/i18n.h>
#include <libaudcore/runtime.h>
+#include <libaudcore/vfs.h>
#include "internal.h"
#include "libaudgui.h"
#include "libaudgui-gtk.h"
static const char about_text[] = "<big><b>Audacious " VERSION "</b></big>\n" COPYRIGHT;
-static const char website[] = "http://audacious-media-player.org";
+static const char website[] = "https://audacious-media-player.org";
static GtkWidget * create_credits_notebook (const char * credits, const char * license)
{
@@ -77,8 +78,9 @@ static GtkWidget * create_about_window ()
GtkWidget * vbox = gtk_vbox_new (false, 6);
gtk_container_add ((GtkContainer *) about_window, vbox);
- StringBuf logo_path = filename_build ({data_dir, "images", "about-logo.png"});
- GtkWidget * image = gtk_image_new_from_file (logo_path);
+ AudguiPixbuf logo (gdk_pixbuf_new_from_resource_at_scale
+ ("/org/audacious/about-logo.svg", 4 * dpi, 2 * dpi, true, nullptr));
+ GtkWidget * image = gtk_image_new_from_pixbuf (logo.get ());
gtk_box_pack_start ((GtkBox *) vbox, image, false, false, 0);
GtkWidget * label = gtk_label_new (nullptr);
@@ -92,26 +94,13 @@ static GtkWidget * create_about_window ()
GtkWidget * button = gtk_link_button_new (website);
gtk_container_add ((GtkContainer *) align, button);
- char * credits, * license;
+ auto credits = VFSFile::read_file (filename_build ({data_dir, "AUTHORS"}), VFS_APPEND_NULL);
+ auto license = VFSFile::read_file (filename_build ({data_dir, "COPYING"}), VFS_APPEND_NULL);
- StringBuf credits_path = filename_build ({data_dir, "AUTHORS"});
- if (! g_file_get_contents (credits_path, & credits, nullptr, nullptr))
- credits = g_strdup_printf ("Unable to load %s; check your installation.", (const char *) credits_path);
-
- StringBuf license_path = filename_build ({data_dir, "COPYING"});
- if (! g_file_get_contents (license_path, & license, nullptr, nullptr))
- license = g_strdup_printf ("Unable to load %s; check your installation.", (const char *) license_path);
-
- g_strchomp (credits);
- g_strchomp (license);
-
- GtkWidget * notebook = create_credits_notebook (credits, license);
+ GtkWidget * notebook = create_credits_notebook (credits.begin (), license.begin ());
gtk_widget_set_size_request (notebook, 6 * dpi, 2 * dpi);
gtk_box_pack_start ((GtkBox *) vbox, notebook, true, true, 0);
- g_free (credits);
- g_free (license);
-
return about_window;
}
diff --git a/src/libaudgui/images.gresource.xml b/src/libaudgui/images.gresource.xml
new file mode 100644
index 0000000..2a5dacf
--- /dev/null
+++ b/src/libaudgui/images.gresource.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/audacious">
+ <file>about-logo.svg</file>
+ <file>application-exit.svg</file>
+ <file>applications-graphics.svg</file>
+ <file>applications-internet.svg</file>
+ <file>applications-system.svg</file>
+ <file>appointment-new.svg</file>
+ <file>audacious.svg</file>
+ <file>audio-card.svg</file>
+ <file>audio-volume-high.svg</file>
+ <file>audio-volume-low.svg</file>
+ <file>audio-volume-medium.svg</file>
+ <file>audio-volume-muted.svg</file>
+ <file>audio-x-generic.svg</file>
+ <file>dialog-error.svg</file>
+ <file>dialog-information.svg</file>
+ <file>dialog-question.svg</file>
+ <file>dialog-warning.svg</file>
+ <file>document-new.svg</file>
+ <file>document-open-recent.svg</file>
+ <file>document-open.svg</file>
+ <file>document-save.svg</file>
+ <file>edit-clear.svg</file>
+ <file>edit-copy.svg</file>
+ <file>edit-cut.svg</file>
+ <file>edit-delete.svg</file>
+ <file>edit-find.svg</file>
+ <file>edit-paste.svg</file>
+ <file>edit-select-all.svg</file>
+ <file>face-smile.svg</file>
+ <file>folder-remote.svg</file>
+ <file>folder.svg</file>
+ <file>go-down.svg</file>
+ <file>go-jump.svg</file>
+ <file>go-next.svg</file>
+ <file>go-previous.svg</file>
+ <file>go-up.svg</file>
+ <file>help-about.svg</file>
+ <file>insert-text.svg</file>
+ <file>list-add.svg</file>
+ <file>list-remove.svg</file>
+ <file>media-optical.svg</file>
+ <file>media-playback-pause.svg</file>
+ <file>media-playback-start.svg</file>
+ <file>media-playback-stop.svg</file>
+ <file>media-playlist-repeat.svg</file>
+ <file>media-playlist-shuffle.svg</file>
+ <file>media-record.svg</file>
+ <file>media-skip-backward.svg</file>
+ <file>media-skip-forward.svg</file>
+ <file>multimedia-volume-control.svg</file>
+ <file>preferences-system.svg</file>
+ <file>process-stop.svg</file>
+ <file>system-run.svg</file>
+ <file>text-x-generic.svg</file>
+ <file>user-desktop.svg</file>
+ <file>user-home.svg</file>
+ <file>user-trash.svg</file>
+ <file>view-refresh.svg</file>
+ <file>view-sort-ascending.svg</file>
+ <file>view-sort-descending.svg</file>
+ <file>window-close.svg</file>
+ </gresource>
+</gresources>
diff --git a/src/libaudgui/infopopup.cc b/src/libaudgui/infopopup.cc
index 680a0b1..bdefdd5 100644
--- a/src/libaudgui/infopopup.cc
+++ b/src/libaudgui/infopopup.cc
@@ -112,15 +112,28 @@ static void infopopup_realized (GtkWidget * widget)
/* borrowed from the gtkui infoarea */
static gboolean infopopup_draw_bg (GtkWidget * widget)
{
+ double r = 1, g = 1, b = 1;
+
+ /* In a dark theme, try to match the tone of the base color */
+ auto & c = (gtk_widget_get_style (widget))->base[GTK_STATE_NORMAL];
+ int v = aud::max (aud::max (c.red, c.green), c.blue);
+
+ if (v >= 10*256 && v < 80*256)
+ {
+ r = (double)c.red / v;
+ g = (double)c.green / v;
+ b = (double)c.blue / v;
+ }
+
GtkAllocation alloc;
gtk_widget_get_allocation (widget, & alloc);
cairo_t * cr = gdk_cairo_create (gtk_widget_get_window (widget));
cairo_pattern_t * gradient = cairo_pattern_create_linear (0, 0, 0, alloc.height);
- cairo_pattern_add_color_stop_rgb (gradient, 0, 0.25, 0.25, 0.25);
- cairo_pattern_add_color_stop_rgb (gradient, 0.5, 0.15, 0.15, 0.15);
- cairo_pattern_add_color_stop_rgb (gradient, 0.5, 0.1, 0.1, 0.1);
+ cairo_pattern_add_color_stop_rgb (gradient, 0, 0.25 * r, 0.25 * g, 0.25 * b);
+ cairo_pattern_add_color_stop_rgb (gradient, 0.5, 0.15 * r, 0.15 * g, 0.15 * b);
+ cairo_pattern_add_color_stop_rgb (gradient, 0.5, 0.1 * r, 0.1 * g, 0.1 * b);
cairo_pattern_add_color_stop_rgb (gradient, 1, 0, 0, 0);
cairo_set_source (cr, gradient);
@@ -210,10 +223,6 @@ static GtkWidget * infopopup_create ()
/* override background drawing */
gtk_widget_set_app_paintable (infopopup, true);
- GtkStyle * style = gtk_style_new ();
- gtk_widget_set_style (infopopup, style);
- g_object_unref (style);
-
g_signal_connect (infopopup, "realize", (GCallback) infopopup_realized, nullptr);
g_signal_connect (infopopup, "expose-event", (GCallback) infopopup_draw_bg, nullptr);
diff --git a/src/libaudgui/init.cc b/src/libaudgui/init.cc
index aa5b93c..c3d3a76 100644
--- a/src/libaudgui/init.cc
+++ b/src/libaudgui/init.cc
@@ -30,6 +30,10 @@
#include "libaudgui.h"
#include "libaudgui-gtk.h"
+extern "C" {
+#include "images.h"
+}
+
static const char * const audgui_defaults[] = {
"clear_song_fields", "TRUE",
"close_dialog_add", "FALSE",
@@ -127,6 +131,196 @@ void audgui_hide_unique_window (int id)
gtk_widget_destroy (windows[id]);
}
+#ifdef _WIN32
+/* On Windows, the default icon sizes are fixed.
+ * Adjust them for varying screen resolutions. */
+void adjust_icon_sizes (void)
+{
+ struct Mapping {
+ GtkIconSize size;
+ const char * name;
+ };
+
+ static const Mapping mappings[] = {
+ {GTK_ICON_SIZE_MENU, "gtk-menu"},
+ {GTK_ICON_SIZE_SMALL_TOOLBAR, "gtk-small-toolbar"},
+ {GTK_ICON_SIZE_LARGE_TOOLBAR, "gtk-large-toolbar"},
+ {GTK_ICON_SIZE_BUTTON, "gtk-button"},
+ {GTK_ICON_SIZE_DND, "gtk-dnd"},
+ {GTK_ICON_SIZE_DIALOG, "gtk-dialog"}
+ };
+
+ StringBuf value;
+
+ for (auto & m : mappings)
+ {
+ int width, height;
+ if (gtk_icon_size_lookup (m.size, & width, & height))
+ {
+ width = audgui_to_native_dpi (width);
+ height = audgui_to_native_dpi (height);
+
+ const char * sep = value.len () ? ":" : "";
+ str_append_printf (value, "%s%s=%d,%d", sep, m.name, width, height);
+ }
+ }
+
+ GtkSettings * settings = gtk_settings_get_default ();
+ g_object_set ((GObject *) settings, "gtk-icon-sizes", (const char *) value, nullptr);
+}
+#endif
+
+static int get_icon_size (GtkIconSize size)
+{
+ int width, height;
+ if (gtk_icon_size_lookup (size, & width, & height))
+ return (width + height) / 2;
+
+ return audgui_to_native_dpi (16);
+}
+
+static void load_fallback_icon (const char * icon, int size)
+{
+ StringBuf resource = str_concat ({"/org/audacious/", icon, ".svg"});
+ auto pixbuf = gdk_pixbuf_new_from_resource_at_scale (resource, size, size, true, nullptr);
+
+ if (pixbuf)
+ {
+ gtk_icon_theme_add_builtin_icon (icon, size, pixbuf);
+ g_object_unref (pixbuf);
+ }
+}
+
+static void load_fallback_icons ()
+{
+ static const char * const all_icons[] = {
+ "application-exit",
+ "applications-graphics",
+ "applications-internet",
+ "applications-system",
+ "appointment-new",
+ "audacious",
+ "audio-card",
+ "audio-volume-high",
+ "audio-volume-low",
+ "audio-volume-medium",
+ "audio-volume-muted",
+ "audio-x-generic",
+ "dialog-error",
+ "dialog-information",
+ "dialog-question",
+ "dialog-warning",
+ "document-new",
+ "document-open-recent",
+ "document-open",
+ "document-save",
+ "edit-clear",
+ "edit-copy",
+ "edit-cut",
+ "edit-delete",
+ "edit-find",
+ "edit-paste",
+ "edit-select-all",
+ "face-smile",
+ "folder-remote",
+ "folder",
+ "go-down",
+ "go-jump",
+ "go-next",
+ "go-previous",
+ "go-up",
+ "help-about",
+ "insert-text",
+ "list-add",
+ "list-remove",
+ "media-optical",
+ "media-playback-pause",
+ "media-playback-start",
+ "media-playback-stop",
+ "media-playlist-repeat",
+ "media-playlist-shuffle",
+ "media-record",
+ "media-skip-backward",
+ "media-skip-forward",
+ "multimedia-volume-control",
+ "preferences-system",
+ "process-stop",
+ "system-run",
+ "text-x-generic",
+ "user-desktop",
+ "user-home",
+ "user-trash",
+ "view-refresh",
+ "view-sort-ascending",
+ "view-sort-descending",
+ "window-close"
+ };
+
+ static const char * const toolbar_icons[] = {
+ "audacious",
+ "audio-volume-high",
+ "audio-volume-low",
+ "audio-volume-medium",
+ "audio-volume-muted",
+ "document-open",
+ "edit-find",
+ "list-add",
+ "media-playback-pause",
+ "media-playback-start",
+ "media-playback-stop",
+ "media-playlist-repeat",
+ "media-playlist-shuffle",
+ "media-record",
+ "media-skip-backward",
+ "media-skip-forward"
+ };
+
+ static const char * const dialog_icons[] = {
+ "dialog-error",
+ "dialog-information",
+ "dialog-question",
+ "dialog-warning"
+ };
+
+ /* keep this in sync with the list in prefs-window.cc */
+ static const char * const category_icons[] = {
+ "applications-graphics",
+ "applications-internet",
+ "applications-system",
+ "audacious", /* for window icons */
+ "audio-volume-medium",
+ "audio-x-generic", /* also used for fallback album art */
+ "dialog-information",
+ "preferences-system"
+ };
+
+ g_resources_register (images_get_resource ());
+
+#ifdef _WIN32
+ adjust_icon_sizes ();
+#endif
+
+ int menu_size = get_icon_size (GTK_ICON_SIZE_MENU);
+ for (const char * icon : all_icons)
+ load_fallback_icon (icon, menu_size);
+
+ GtkIconSize icon_size;
+ GtkSettings * settings = gtk_settings_get_default ();
+ g_object_get (settings, "gtk-toolbar-icon-size", & icon_size, NULL);
+
+ int toolbar_size = get_icon_size (icon_size);
+ for (const char * icon : toolbar_icons)
+ load_fallback_icon (icon, toolbar_size);
+
+ int dialog_size = get_icon_size (GTK_ICON_SIZE_DIALOG);
+ for (const char * icon : dialog_icons)
+ load_fallback_icon (icon, dialog_size);
+
+ int category_size = audgui_to_native_dpi (48);
+ for (const char * icon : category_icons)
+ load_fallback_icon (icon, category_size);
+}
+
static void playlist_set_playing_cb (void *, void *)
{
audgui_pixbuf_uncache ();
@@ -140,12 +334,25 @@ static void playlist_position_cb (void * list, void *)
EXPORT void audgui_init ()
{
+ static bool icons_loaded = false;
assert (aud_get_mainloop_type () == MainloopType::GLib);
if (init_count ++)
return;
- gtk_init (nullptr, nullptr);
+ static char app_name[] = "audacious";
+ static char * app_args[] = {app_name, nullptr};
+
+ int dummy_argc = 1;
+ char * * dummy_argv = app_args;
+
+ gtk_init (& dummy_argc, & dummy_argv);
+
+ if (! icons_loaded)
+ {
+ load_fallback_icons ();
+ icons_loaded = true;
+ }
aud_config_set_defaults ("audgui", audgui_defaults);
@@ -154,9 +361,7 @@ EXPORT void audgui_init ()
hook_associate ("playlist set playing", playlist_set_playing_cb, nullptr);
hook_associate ("playlist position", playlist_position_cb, nullptr);
-#ifndef _WIN32
gtk_window_set_default_icon_name ("audacious");
-#endif
}
EXPORT void audgui_cleanup ()
diff --git a/src/libaudgui/pixbufs.cc b/src/libaudgui/pixbufs.cc
index bb12c93..23d5fee 100644
--- a/src/libaudgui/pixbufs.cc
+++ b/src/libaudgui/pixbufs.cc
@@ -34,8 +34,13 @@ EXPORT AudguiPixbuf audgui_pixbuf_fallback ()
static AudguiPixbuf fallback;
if (! fallback)
- fallback.capture (gdk_pixbuf_new_from_file (filename_build
- ({aud_get_path (AudPath::DataDir), "images", "album.png"}), nullptr));
+ {
+ GtkIconTheme * icon_theme = gtk_icon_theme_get_default ();
+ int icon_size = audgui_to_native_dpi (48);
+
+ fallback.capture (gtk_icon_theme_load_icon (icon_theme,
+ "audio-x-generic", icon_size, (GtkIconLookupFlags) 0, nullptr));
+ }
return fallback.ref ();
}
diff --git a/src/libaudgui/prefs-window.cc b/src/libaudgui/prefs-window.cc
index e126745..bf839a2 100644
--- a/src/libaudgui/prefs-window.cc
+++ b/src/libaudgui/prefs-window.cc
@@ -46,7 +46,7 @@ enum CategoryViewCols {
};
struct Category {
- const char * icon_path;
+ const char * icon;
const char * name;
};
@@ -73,16 +73,19 @@ enum {
CATEGORY_NETWORK,
CATEGORY_PLAYLIST,
CATEGORY_SONG_INFO,
- CATEGORY_PLUGINS
+ CATEGORY_PLUGINS,
+ CATEGORY_ADVANCED
};
+/* keep this in sync with the list in load_fallback_icons (init.cc) */
static const Category categories[] = {
- { "appearance.png", N_("Appearance") },
- { "audio.png", N_("Audio") },
- { "connectivity.png", N_("Network") },
- { "playlist.png", N_("Playlist")} ,
- { "info.png", N_("Song Info") },
- { "plugins.png", N_("Plugins") }
+ { "applications-graphics", N_("Appearance") },
+ { "audio-volume-medium", N_("Audio") },
+ { "applications-internet", N_("Network") },
+ { "audio-x-generic", N_("Playlist")} ,
+ { "dialog-information", N_("Song Info") },
+ { "applications-system", N_("Plugins") },
+ { "preferences-system", N_("Advanced") }
};
static const PluginCategory plugin_categories[] = {
@@ -300,10 +303,9 @@ static const PreferencesWidget playlist_page_widgets[] = {
WidgetCheck (N_("Show hours separately (1:30:00 vs. 90:00)"),
WidgetBool (0, "show_hours", send_title_change)),
WidgetCustomGTK (create_titlestring_table),
- WidgetLabel (N_("<b>Compatibility</b>")),
- WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"),
- WidgetBool (0, "convert_backslash")),
- WidgetTable ({{chardet_elements}})
+ WidgetLabel (N_("<b>Export</b>")),
+ WidgetCheck (N_("Use relative paths when possible"),
+ WidgetBool (0, "export_relative_paths"))
};
static const PreferencesWidget song_info_page_widgets[] = {
@@ -329,8 +331,20 @@ static const PreferencesWidget song_info_page_widgets[] = {
WIDGET_CHILD),
WidgetCheck (N_("Show time scale for current song"),
WidgetBool (0, "filepopup_showprogressbar"),
- WIDGET_CHILD),
- WidgetLabel (N_("<b>Advanced</b>")),
+ WIDGET_CHILD)
+};
+
+static const PreferencesWidget advanced_page_widgets[] = {
+ WidgetLabel (N_("<b>Compatibility</b>")),
+ WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"),
+ WidgetBool (0, "convert_backslash")),
+ WidgetTable ({{chardet_elements}}),
+ WidgetLabel (N_("<b>Playlist</b>")),
+ WidgetCheck (N_("Add folders recursively"),
+ WidgetBool (0, "recurse_folders")),
+ WidgetCheck (N_("Add folders nested within playlist files"),
+ WidgetBool (0, "folders_in_playlist")),
+ WidgetLabel (N_("<b>Metadata</b>")),
WidgetCheck (N_("Guess missing metadata from file path"),
WidgetBool (0, "metadata_fallbacks")),
WidgetCheck (N_("Do not load metadata for songs until played"),
@@ -470,7 +484,8 @@ static void fill_category_list (GtkTreeView * treeview, GtkNotebook * notebook)
GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_INT);
gtk_tree_view_set_model (treeview, (GtkTreeModel *) store);
- const char * data_dir = aud_get_path (AudPath::DataDir);
+ GtkIconTheme * icon_theme = gtk_icon_theme_get_default ();
+ int icon_size = audgui_to_native_dpi (48);
for (const Category & category : categories)
{
@@ -482,8 +497,8 @@ static void fill_category_list (GtkTreeView * treeview, GtkNotebook * notebook)
gtk_list_store_set (store, & iter, CATEGORY_VIEW_COL_NAME,
gettext (category.name), -1);
- StringBuf path = filename_build ({data_dir, "images", category.icon_path});
- AudguiPixbuf img (gdk_pixbuf_new_from_file (path, nullptr));
+ AudguiPixbuf img (gtk_icon_theme_load_icon (icon_theme,
+ category.icon, icon_size, (GtkIconLookupFlags) 0, nullptr));
if (img)
gtk_list_store_set (store, & iter, CATEGORY_VIEW_COL_ICON, img.get (), -1);
@@ -747,7 +762,7 @@ static void record_update (void * = nullptr, void * = nullptr)
{
gtk_widget_set_sensitive (record_checkbox, false);
gtk_button_set_label ((GtkButton *) record_checkbox,
- str_printf (_("No audio recording plugin available")));
+ _("No audio recording plugin available"));
gtk_toggle_button_set_active ((GtkToggleButton *) record_checkbox, false);
gtk_widget_set_sensitive (record_config_button, false);
gtk_widget_set_sensitive (record_about_button, false);
@@ -782,6 +797,13 @@ static void create_plugin_category ()
plugin_view_new (category.type), gtk_label_new (_(category.name)));
}
+static void create_advanced_category ()
+{
+ GtkWidget * advanced_page_vbox = gtk_vbox_new (false, 0);
+ audgui_create_widgets (advanced_page_vbox, advanced_page_widgets);
+ gtk_container_add ((GtkContainer *) category_notebook, advanced_page_vbox);
+}
+
static void destroy_cb ()
{
hook_dissociate ("enable record", record_update);
@@ -834,6 +856,7 @@ static void create_prefs_window ()
create_playlist_category ();
create_song_info_category ();
create_plugin_category ();
+ create_advanced_category ();
GtkWidget * hseparator = gtk_hseparator_new ();
gtk_box_pack_start ((GtkBox *) vbox, hseparator, false, false, 6);
diff --git a/src/libaudgui/url-opener.cc b/src/libaudgui/url-opener.cc
index 2733bca..6c5c5cd 100644
--- a/src/libaudgui/url-opener.cc
+++ b/src/libaudgui/url-opener.cc
@@ -21,6 +21,7 @@
#include <libaudcore/drct.h>
#include <libaudcore/i18n.h>
+#include <libaudcore/preferences.h>
#include <libaudcore/runtime.h>
#include "internal.h"
@@ -37,11 +38,24 @@ static void open_cb (void * entry)
else
aud_drct_pl_add (text, -1);
- aud_history_add (text);
+ if (aud_get_bool (nullptr, "save_url_history"))
+ aud_history_add (text);
+}
+
+static void clear_cb (void * combo)
+{
+ /* no gtk_combo_box_text_clear()? */
+ gtk_list_store_clear ((GtkListStore *) gtk_combo_box_get_model ((GtkComboBox *) combo));
+ aud_history_clear ();
}
static GtkWidget * create_url_opener (bool open)
{
+ static const PreferencesWidget widgets[] = {
+ WidgetCheck (N_("_Save to history"),
+ WidgetBool (0, "save_url_history"))
+ };
+
const char * title, * verb, * icon;
if (open)
@@ -72,13 +86,24 @@ static GtkWidget * create_url_opener (bool open)
g_object_set_data ((GObject *) entry, "open", GINT_TO_POINTER (open));
+ GtkWidget * hbox = gtk_hbox_new (false, 6);
+ audgui_create_widgets (hbox, widgets);
+
+ GtkWidget * clear_button = audgui_button_new (_("C_lear history"),
+ "edit-clear", clear_cb, combo);
+ gtk_box_pack_end ((GtkBox *) hbox, clear_button, false, false, 0);
+
+ GtkWidget * vbox = gtk_vbox_new (false, 6);
+ gtk_box_pack_start ((GtkBox *) vbox, combo, false, false, 0);
+ gtk_box_pack_start ((GtkBox *) vbox, hbox, false, false, 0);
+
GtkWidget * button1 = audgui_button_new (verb, icon, open_cb, entry);
GtkWidget * button2 = audgui_button_new (_("_Cancel"), "process-stop", nullptr, nullptr);
GtkWidget * dialog = audgui_dialog_new (GTK_MESSAGE_OTHER, title,
_("Enter URL:"), button1, button2);
gtk_widget_set_size_request (dialog, 4 * audgui_get_dpi (), -1);
- audgui_dialog_add_widget (dialog, combo);
+ audgui_dialog_add_widget (dialog, vbox);
return dialog;
}
diff --git a/src/libaudqt/Makefile b/src/libaudqt/Makefile
index 7417d4e..fa9e7bf 100644
--- a/src/libaudqt/Makefile
+++ b/src/libaudqt/Makefile
@@ -1,25 +1,28 @@
SHARED_LIB = ${LIB_PREFIX}audqt${LIB_SUFFIX}
LIB_MAJOR = 2
-LIB_MINOR = 0
-
-SRCS = about.cc \
- art.cc \
- equalizer.cc \
- fileopener.cc \
- infowin.cc \
- info-widget.cc \
- log-inspector.cc \
- menu.cc \
- playlist-management.cc \
- plugin-menu.cc \
- prefs-builder.cc \
- prefs-plugin.cc \
- prefs-widget.cc \
- prefs-window.cc \
- prefs-pluginlist-model.cc \
- queue-manager.cc \
- url-opener.cc \
- util.cc \
+LIB_MINOR = 1
+
+SRCS = about-qt.cc \
+ art-qt.cc \
+ audqt.cc \
+ equalizer-qt.cc \
+ fileopener.cc \
+ images.cc \
+ infopopup-qt.cc \
+ infowin-qt.cc \
+ info-widget.cc \
+ log-inspector.cc \
+ menu-qt.cc \
+ playlist-management.cc \
+ plugin-menu-qt.cc \
+ prefs-builder.cc \
+ prefs-plugin.cc \
+ prefs-widget-qt.cc \
+ prefs-window-qt.cc \
+ prefs-pluginlist-model.cc \
+ queue-manager-qt.cc \
+ url-opener-qt.cc \
+ util-qt.cc \
volumebutton.cc
INCLUDES = export.h \
@@ -46,5 +49,5 @@ LIBS := -L../libaudcore -laudcore \
${LIBS} -lm \
${QT_LIBS}
-%.moc: %.h
- moc $< -o $@
+images.cc: images.qrc
+ ${QT_BINPATH}/rcc images.qrc -o images.cc
diff --git a/src/libaudqt/about.cc b/src/libaudqt/about-qt.cc
index 2fdccbd..fe0a9de 100644
--- a/src/libaudqt/about.cc
+++ b/src/libaudqt/about-qt.cc
@@ -18,16 +18,15 @@
*/
#include <QDialog>
-#include <QFile>
#include <QLabel>
#include <QPlainTextEdit>
#include <QTabWidget>
-#include <QTextStream>
#include <QVBoxLayout>
#include <libaudcore/audstrings.h>
#include <libaudcore/i18n.h>
#include <libaudcore/runtime.h>
+#include <libaudcore/vfs.h>
#include "libaudqt.h"
@@ -43,18 +42,11 @@ static QTabWidget * buildCreditsNotebook (QWidget * parent)
for (int i = 0; i < 2; i ++)
{
- QFile f (QString (filename_build ({data_dir, filenames[i]})));
- if (! f.open (QIODevice::ReadOnly))
- continue;
-
- QTextStream in (& f);
-
- auto edit = new QPlainTextEdit (in.readAll ().trimmed (), parent);
+ auto text = VFSFile::read_file (filename_build ({data_dir, filenames[i]}), VFS_APPEND_NULL);
+ auto edit = new QPlainTextEdit (text.begin (), parent);
edit->setReadOnly (true);
edit->setFrameStyle (QFrame::NoFrame);
tabs->addTab (edit, _(titles[i]));
-
- f.close ();
}
return tabs;
@@ -62,16 +54,15 @@ static QTabWidget * buildCreditsNotebook (QWidget * parent)
static QDialog * buildAboutWindow ()
{
- const char * data_dir = aud_get_path (AudPath::DataDir);
- const char * logo_path = filename_build ({data_dir, "images", "about-logo.png"});
const char * about_text = "<big><b>Audacious " VERSION "</b></big><br>" COPYRIGHT;
- const char * website = "http://audacious-media-player.org";
+ const char * website = "https://audacious-media-player.org";
auto window = new QDialog;
window->setWindowTitle (_("About Audacious"));
auto logo = new QLabel (window);
- logo->setPixmap (QPixmap (logo_path));
+ int logo_size = audqt::to_native_dpi (400);
+ logo->setPixmap (QIcon (":/about-logo.svg").pixmap (logo_size, logo_size));
logo->setAlignment (Qt::AlignHCenter);
auto text = new QLabel (about_text, window);
@@ -83,6 +74,7 @@ static QDialog * buildAboutWindow ()
link_label->setOpenExternalLinks (true);
auto layout = audqt::make_vbox (window);
+ layout->addSpacing (audqt::sizes.EightPt);
layout->addWidget (logo);
layout->addWidget (text);
layout->addWidget (link_label);
diff --git a/src/libaudqt/art.cc b/src/libaudqt/art-qt.cc
index c691992..3327d91 100644
--- a/src/libaudqt/art.cc
+++ b/src/libaudqt/art-qt.cc
@@ -19,55 +19,48 @@
#include <QApplication>
#include <QPixmap>
+#include <QIcon>
#include <QImage>
#include <libaudcore/audstrings.h>
#include <libaudcore/drct.h>
#include <libaudcore/probe.h>
#include <libaudcore/runtime.h>
+#include <libaudqt/libaudqt.h>
namespace audqt {
-static QImage load_fallback ()
+EXPORT QImage art_request (const char * filename, bool * queued)
{
- static QImage fallback;
- static bool loaded = false;
+ AudArtPtr art = aud_art_request (filename, AUD_ART_DATA, queued);
- if (! loaded)
- fallback.load ((const char *) filename_build
- ({aud_get_path (AudPath::DataDir), "images", "album.png"}));
-
- return fallback; // shallow copy
+ auto data = art.data ();
+ return data ? QImage::fromData ((const uchar *) data->begin (), data->len ()) : QImage ();
}
-EXPORT QPixmap art_request (const char * filename, unsigned int w, unsigned int h, bool want_hidpi)
+EXPORT QPixmap art_scale (const QImage & image, unsigned int w, unsigned int h, bool want_hidpi)
{
- AudArtPtr art = aud_art_request (filename, AUD_ART_DATA);
-
- auto data = art.data ();
- auto img = data ? QImage::fromData ((const uchar *) data->begin (), data->len ()) : QImage ();
-
- if (img.isNull ())
- {
- img = load_fallback ();
- if (img.isNull ())
- return QPixmap ();
- }
-
// return original image if requested size is zero,
// or original size is smaller than requested size
- if ((w == 0 && h == 0) || ((unsigned) img.width () <= w && (unsigned) img.height () <= h))
- return QPixmap::fromImage (img);
+ if ((w == 0 && h == 0) || ((unsigned) image.width () <= w && (unsigned) image.height () <= h))
+ return QPixmap::fromImage (image);
- if (! want_hidpi)
- return QPixmap::fromImage (img.scaled (w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation));
+ qreal r = want_hidpi ? qApp->devicePixelRatio () : 1;
+ auto pixmap = QPixmap::fromImage (image.scaled (w * r, h * r,
+ Qt::KeepAspectRatio, Qt::SmoothTransformation));
- qreal r = qApp->devicePixelRatio ();
+ pixmap.setDevicePixelRatio (r);
+ return pixmap;
+}
- QPixmap pm = QPixmap::fromImage (img.scaled (w * r, h * r, Qt::KeepAspectRatio, Qt::SmoothTransformation));
- pm.setDevicePixelRatio (r);
+EXPORT QPixmap art_request (const char * filename, unsigned int w, unsigned int h, bool want_hidpi)
+{
+ auto img = art_request (filename);
+ if (! img.isNull ())
+ return art_scale (img, w, h, want_hidpi);
- return pm;
+ unsigned size = to_native_dpi (48);
+ return get_icon ("audio-x-generic").pixmap (aud::min (w, size), aud::min (h, size));
}
EXPORT QPixmap art_request_current (unsigned int w, unsigned int h, bool want_hidpi)
diff --git a/src/libaudqt/util.cc b/src/libaudqt/audqt.cc
index 14ed33c..d44df33 100644
--- a/src/libaudqt/util.cc
+++ b/src/libaudqt/audqt.cc
@@ -34,7 +34,6 @@
namespace audqt {
static int init_count;
-static QApplication * qapp;
static PixelSizes sizes_local;
static PixelMargins margins_local;
@@ -44,23 +43,28 @@ EXPORT const PixelMargins & margins = margins_local;
EXPORT void init ()
{
- if (init_count ++ || qapp)
+ if (init_count ++)
return;
static char app_name[] = "audacious";
static int dummy_argc = 1;
static char * dummy_argv[] = {app_name, nullptr};
- qapp = new QApplication (dummy_argc, dummy_argv);
- atexit ([] () { delete qapp; });
+ auto qapp = new QApplication (dummy_argc, dummy_argv);
qapp->setAttribute (Qt::AA_UseHighDpiPixmaps);
#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0)
qapp->setAttribute (Qt::AA_ForceRasterWidgets);
#endif
+#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
+ qapp->setAttribute (Qt::AA_UseStyleSheetPropagationInWidgetStyles);
+#endif
qapp->setApplicationName (_("Audacious"));
- qapp->setWindowIcon (QIcon::fromTheme (app_name));
+ if (qapp->windowIcon ().isNull ())
+ qapp->setWindowIcon (audqt::get_icon (app_name));
+
+ qapp->setQuitOnLastWindowClosed (false);
auto desktop = qapp->desktop ();
sizes_local.OneInch = aud::max (96, (desktop->logicalDpiX () + desktop->logicalDpiY ()) / 2);
@@ -72,17 +76,23 @@ EXPORT void init ()
margins_local.FourPt = QMargins (sizes.FourPt, sizes.FourPt, sizes.FourPt, sizes.FourPt);
margins_local.EightPt = QMargins (sizes.EightPt, sizes.EightPt, sizes.EightPt, sizes.EightPt);
+#ifdef Q_OS_MAC // Mac-specific font tweaks
+ QApplication::setFont (QApplication::font ("QSmallFont"), "QDialog");
+ QApplication::setFont (QApplication::font ("QSmallFont"), "QTreeView");
+ QApplication::setFont (QApplication::font ("QTipLabel"), "QStatusBar");
+#endif
+
log_init ();
}
EXPORT void run ()
{
- qapp->exec ();
+ qApp->exec ();
}
EXPORT void quit ()
{
- qapp->quit ();
+ qApp->quit ();
}
EXPORT void cleanup ()
@@ -92,12 +102,25 @@ EXPORT void cleanup ()
aboutwindow_hide ();
equalizer_hide ();
+ infopopup_hide ();
infowin_hide ();
log_inspector_hide ();
prefswin_hide ();
queue_manager_hide ();
log_cleanup ();
+
+ delete qApp;
+}
+
+EXPORT QIcon get_icon (const char * name)
+{
+ auto icon = QIcon::fromTheme (name);
+
+ if (icon.isNull ())
+ icon = QIcon (QString (":/") + name + ".svg");
+
+ return icon;
}
EXPORT QHBoxLayout * make_hbox (QWidget * parent, int spacing)
diff --git a/src/libaudqt/equalizer.cc b/src/libaudqt/equalizer-qt.cc
index 01907ce..01907ce 100644
--- a/src/libaudqt/equalizer.cc
+++ b/src/libaudqt/equalizer-qt.cc
diff --git a/src/libaudqt/fileopener.cc b/src/libaudqt/fileopener.cc
index c4d6b5c..dc5691f 100644
--- a/src/libaudqt/fileopener.cc
+++ b/src/libaudqt/fileopener.cc
@@ -21,6 +21,7 @@
#include <libaudcore/drct.h>
#include <libaudcore/i18n.h>
+#include <libaudcore/playlist.h>
#include <libaudcore/runtime.h>
#include <libaudqt/libaudqt.h>
@@ -29,6 +30,23 @@ namespace audqt {
static aud::array<FileMode, QFileDialog *> s_dialogs;
+static void import_playlist (Playlist playlist, const String & filename)
+{
+ playlist.set_filename (filename);
+ playlist.remove_all_entries ();
+ playlist.insert_entry (0, filename, Tuple (), false);
+}
+
+static void export_playlist (Playlist playlist, const String & filename)
+{
+ Playlist::GetMode mode = Playlist::Wait;
+ if (aud_get_bool (nullptr, "metadata_on_play"))
+ mode = Playlist::NoWait;
+
+ playlist.set_filename (filename);
+ playlist.save_to_file (filename, mode);
+}
+
EXPORT void fileopener_show (FileMode mode)
{
QFileDialog * & dialog = s_dialogs[mode];
@@ -39,21 +57,27 @@ EXPORT void fileopener_show (FileMode mode)
N_("Open Files"),
N_("Open Folder"),
N_("Add Files"),
- N_("Add Folder")
+ N_("Add Folder"),
+ N_("Import Playlist"),
+ N_("Export Playlist")
};
static constexpr aud::array<FileMode, const char *> labels {
N_("Open"),
N_("Open"),
N_("Add"),
- N_("Add")
+ N_("Add"),
+ N_("Import"),
+ N_("Export")
};
static constexpr aud::array<FileMode, QFileDialog::FileMode> modes {
QFileDialog::ExistingFiles,
QFileDialog::Directory,
QFileDialog::ExistingFiles,
- QFileDialog::Directory
+ QFileDialog::Directory,
+ QFileDialog::ExistingFile,
+ QFileDialog::AnyFile
};
String path = aud_get_str ("audgui", "filesel_path");
@@ -63,19 +87,42 @@ EXPORT void fileopener_show (FileMode mode)
dialog->setFileMode (modes[mode]);
dialog->setLabelText (QFileDialog::Accept, _(labels[mode]));
+ if (mode == FileMode::ExportPlaylist)
+ dialog->setAcceptMode (QFileDialog::AcceptSave);
+
QObject::connect (dialog, & QFileDialog::directoryEntered, [] (const QString & path)
{ aud_set_str ("audgui", "filesel_path", path.toUtf8 ().constData ()); });
- QObject::connect (dialog, & QFileDialog::accepted, [dialog, mode] ()
+ auto playlist = Playlist::active_playlist ();
+
+ QObject::connect (dialog, & QFileDialog::accepted, [dialog, mode, playlist] ()
{
Index<PlaylistAddItem> files;
for (const QUrl & url : dialog->selectedUrls ())
files.append (String (url.toEncoded ().constData ()));
- if (mode == FileMode::Add || mode == FileMode::AddFolder)
+ switch (mode)
+ {
+ case FileMode::Add:
+ case FileMode::AddFolder:
aud_drct_pl_add_list (std::move (files), -1);
- else
+ break;
+ case FileMode::Open:
+ case FileMode::OpenFolder:
aud_drct_pl_open_list (std::move (files));
+ break;
+ case FileMode::ImportPlaylist:
+ if (files.len () == 1)
+ import_playlist (playlist, files[0].filename);
+ break;
+ case FileMode::ExportPlaylist:
+ if (files.len () == 1)
+ export_playlist (playlist, files[0].filename);
+ break;
+ default:
+ /* not reached */
+ break;
+ }
});
QObject::connect (dialog, & QObject::destroyed, [& dialog] ()
diff --git a/src/libaudqt/images.qrc b/src/libaudqt/images.qrc
new file mode 100644
index 0000000..93c2ebd
--- /dev/null
+++ b/src/libaudqt/images.qrc
@@ -0,0 +1,65 @@
+<RCC>
+<qresource>
+ <file alias="about-logo.svg">../../images/about-logo.svg</file>
+ <file alias="application-exit.svg">../../images/application-exit.svg</file>
+ <file alias="applications-graphics.svg">../../images/applications-graphics.svg</file>
+ <file alias="applications-internet.svg">../../images/applications-internet.svg</file>
+ <file alias="applications-system.svg">../../images/applications-system.svg</file>
+ <file alias="appointment-new.svg">../../images/appointment-new.svg</file>
+ <file alias="audacious.svg">../../images/audacious.svg</file>
+ <file alias="audio-card.svg">../../images/audio-card.svg</file>
+ <file alias="audio-volume-high.svg">../../images/audio-volume-high.svg</file>
+ <file alias="audio-volume-low.svg">../../images/audio-volume-low.svg</file>
+ <file alias="audio-volume-medium.svg">../../images/audio-volume-medium.svg</file>
+ <file alias="audio-volume-muted.svg">../../images/audio-volume-muted.svg</file>
+ <file alias="audio-x-generic.svg">../../images/audio-x-generic.svg</file>
+ <file alias="dialog-error.svg">../../images/dialog-error.svg</file>
+ <file alias="dialog-information.svg">../../images/dialog-information.svg</file>
+ <file alias="dialog-question.svg">../../images/dialog-question.svg</file>
+ <file alias="dialog-warning.svg">../../images/dialog-warning.svg</file>
+ <file alias="document-new.svg">../../images/document-new.svg</file>
+ <file alias="document-open-recent.svg">../../images/document-open-recent.svg</file>
+ <file alias="document-open.svg">../../images/document-open.svg</file>
+ <file alias="document-save.svg">../../images/document-save.svg</file>
+ <file alias="edit-clear.svg">../../images/edit-clear.svg</file>
+ <file alias="edit-copy.svg">../../images/edit-copy.svg</file>
+ <file alias="edit-cut.svg">../../images/edit-cut.svg</file>
+ <file alias="edit-delete.svg">../../images/edit-delete.svg</file>
+ <file alias="edit-find.svg">../../images/edit-find.svg</file>
+ <file alias="edit-paste.svg">../../images/edit-paste.svg</file>
+ <file alias="edit-select-all.svg">../../images/edit-select-all.svg</file>
+ <file alias="face-smile.svg">../../images/face-smile.svg</file>
+ <file alias="folder-remote.svg">../../images/folder-remote.svg</file>
+ <file alias="folder.svg">../../images/folder.svg</file>
+ <file alias="go-down.svg">../../images/go-down.svg</file>
+ <file alias="go-jump.svg">../../images/go-jump.svg</file>
+ <file alias="go-next.svg">../../images/go-next.svg</file>
+ <file alias="go-previous.svg">../../images/go-previous.svg</file>
+ <file alias="go-up.svg">../../images/go-up.svg</file>
+ <file alias="help-about.svg">../../images/help-about.svg</file>
+ <file alias="insert-text.svg">../../images/insert-text.svg</file>
+ <file alias="list-add.svg">../../images/list-add.svg</file>
+ <file alias="list-remove.svg">../../images/list-remove.svg</file>
+ <file alias="media-optical.svg">../../images/media-optical.svg</file>
+ <file alias="media-playback-pause.svg">../../images/media-playback-pause.svg</file>
+ <file alias="media-playback-start.svg">../../images/media-playback-start.svg</file>
+ <file alias="media-playback-stop.svg">../../images/media-playback-stop.svg</file>
+ <file alias="media-playlist-repeat.svg">../../images/media-playlist-repeat.svg</file>
+ <file alias="media-playlist-shuffle.svg">../../images/media-playlist-shuffle.svg</file>
+ <file alias="media-record.svg">../../images/media-record.svg</file>
+ <file alias="media-skip-backward.svg">../../images/media-skip-backward.svg</file>
+ <file alias="media-skip-forward.svg">../../images/media-skip-forward.svg</file>
+ <file alias="multimedia-volume-control.svg">../../images/multimedia-volume-control.svg</file>
+ <file alias="preferences-system.svg">../../images/preferences-system.svg</file>
+ <file alias="process-stop.svg">../../images/process-stop.svg</file>
+ <file alias="system-run.svg">../../images/system-run.svg</file>
+ <file alias="text-x-generic.svg">../../images/text-x-generic.svg</file>
+ <file alias="user-desktop.svg">../../images/user-desktop.svg</file>
+ <file alias="user-home.svg">../../images/user-home.svg</file>
+ <file alias="user-trash.svg">../../images/user-trash.svg</file>
+ <file alias="view-refresh.svg">../../images/view-refresh.svg</file>
+ <file alias="view-sort-ascending.svg">../../images/view-sort-ascending.svg</file>
+ <file alias="view-sort-descending.svg">../../images/view-sort-descending.svg</file>
+ <file alias="window-close.svg">../../images/window-close.svg</file>
+</qresource>
+</RCC>
diff --git a/src/libaudqt/info-widget.cc b/src/libaudqt/info-widget.cc
index ef9b218..c1eb083 100644
--- a/src/libaudqt/info-widget.cc
+++ b/src/libaudqt/info-widget.cc
@@ -1,7 +1,7 @@
/*
* info-widget.h
- * Copyright 2006-2014 William Pitcock, Tomasz Moń, Eugene Zagidullin,
- * John Lindgren, and Thomas Lange
+ * Copyright 2006-2017 René Bertin, Thomas Lange, John Lindgren,
+ * William Pitcock, Tomasz Moń, and Eugene Zagidullin
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -20,6 +20,7 @@
#include "info-widget.h"
#include "libaudqt.h"
+#include "libaudqt-internal.h"
#include <QHeaderView>
@@ -97,6 +98,17 @@ EXPORT InfoWidget::InfoWidget (QWidget * parent) :
header ()->hide ();
setIndentation (0);
resizeColumnToContents (0);
+ setContextMenuPolicy (Qt::CustomContextMenu);
+
+ connect (this, & QWidget::customContextMenuRequested, [this] (const QPoint & pos)
+ {
+ auto index = indexAt (pos);
+ if (index.column () != 1)
+ return;
+ auto text = m_model->data (index, Qt::DisplayRole).toString ();
+ if (! text.isEmpty ())
+ show_copy_context_menu (this, mapToGlobal (pos), text);
+ });
}
EXPORT InfoWidget::~InfoWidget ()
@@ -136,20 +148,17 @@ bool InfoModel::setData (const QModelIndex & index, const QVariant & value, int
m_dirty = true;
auto t = Tuple::field_get_type (field_id);
- if (t == Tuple::String)
- {
- m_tuple.set_str (field_id, value.toString ().toUtf8 ());
- emit dataChanged (index, index, {role});
- return true;
- }
- else if (t == Tuple::Int)
- {
- m_tuple.set_int (field_id, value.toInt ());
- emit dataChanged (index, index, {role});
- return true;
- }
+ auto str = value.toString ();
+
+ if (str.isEmpty ())
+ m_tuple.unset (field_id);
+ else if (t == Tuple::String)
+ m_tuple.set_str (field_id, str.toUtf8 ());
+ else /* t == Tuple::Int */
+ m_tuple.set_int (field_id, str.toInt ());
- return false;
+ emit dataChanged (index, index, {role});
+ return true;
}
QVariant InfoModel::data (const QModelIndex & index, int role) const
@@ -170,7 +179,8 @@ QVariant InfoModel::data (const QModelIndex & index, int role) const
case Tuple::String:
return QString (m_tuple.get_str (field_id));
case Tuple::Int:
- return m_tuple.get_int (field_id);
+ /* convert to string so Qt allows clearing the field */
+ return QString::number (m_tuple.get_int (field_id));
default:
return QVariant ();
}
diff --git a/src/libaudqt/infopopup-qt.cc b/src/libaudqt/infopopup-qt.cc
new file mode 100644
index 0000000..fdbba41
--- /dev/null
+++ b/src/libaudqt/infopopup-qt.cc
@@ -0,0 +1,212 @@
+/*
+ * infopopup-qt.cc
+ * Copyright 2018 John Lindgren
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions, and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions, and the following disclaimer in the documentation
+ * provided with the distribution.
+ *
+ * This software is provided "as is" and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising from
+ * the use of this software.
+ */
+
+#include <libaudcore/audstrings.h>
+#include <libaudcore/hook.h>
+#include <libaudcore/i18n.h>
+#include <libaudcore/playlist.h>
+#include <libaudcore/tuple.h>
+
+#include "libaudqt.h"
+#include "libaudqt-internal.h"
+
+#include <QBoxLayout>
+#include <QGridLayout>
+#include <QLabel>
+#include <QPainter>
+
+namespace audqt {
+
+class InfoPopup : public PopupWidget
+{
+public:
+ InfoPopup (const String & filename, const Tuple & tuple);
+
+private:
+ void add_field (int row, const char * field, const char * value);
+ void add_fields (const Tuple & tuple);
+ void art_ready (const char * filename);
+ void finish_loading ();
+
+ void paintEvent (QPaintEvent *) override;
+
+ HookReceiver<InfoPopup, const char *> art_ready_hook
+ {"art ready", this, & InfoPopup::art_ready};
+
+ const String m_filename;
+ const QGradientStops m_stops;
+
+ QHBoxLayout m_hbox;
+ QGridLayout m_grid;
+ bool m_queued = false;
+};
+
+static QGradientStops get_stops (const QColor & base)
+{
+ QColor mid = QColor (64, 64, 64);
+ QColor dark = QColor (38, 38, 38);
+ QColor darker = QColor (26, 26, 26);
+
+ /* In a dark theme, try to match the tone of the base color */
+ int v = base.value ();
+ if (v >= 10 && v < 80)
+ {
+ int r = base.red (), g = base.green (), b = base.blue ();
+ mid = QColor (r * 64 / v, g * 64 / v, b * 64 / v);
+ dark = QColor (r * 38 / v, g * 38 / v, b * 38 / v);
+ darker = QColor (r * 26 / v, g * 26 / v, b * 26 / v);
+ }
+
+ return {
+ {0, mid},
+ {0.499, dark},
+ {0.5, darker},
+ {1, Qt::black}
+ };
+}
+
+InfoPopup::InfoPopup (const String & filename, const Tuple & tuple) :
+ m_filename (filename),
+ m_stops (get_stops (palette ().color (QPalette::Window)))
+{
+ setWindowFlags (Qt::ToolTip);
+
+ m_hbox.setMargin (sizes.TwoPt);
+ m_hbox.setSpacing (sizes.FourPt);
+ setLayout (& m_hbox);
+
+ m_grid.setMargin (0);
+ m_grid.setHorizontalSpacing (sizes.FourPt);
+ m_grid.setVerticalSpacing (0);
+ m_hbox.addLayout (& m_grid);
+
+ add_fields (tuple);
+ finish_loading ();
+}
+
+void InfoPopup::add_fields (const Tuple & tuple)
+{
+ String title = tuple.get_str (Tuple::Title);
+ String artist = tuple.get_str (Tuple::Artist);
+ String album = tuple.get_str (Tuple::Album);
+ String genre = tuple.get_str (Tuple::Genre);
+
+ int year = tuple.get_int (Tuple::Year);
+ int track = tuple.get_int (Tuple::Track);
+ int length = tuple.get_int (Tuple::Length);
+ int row = 0;
+
+ if (title)
+ add_field (row ++, _("Title"), title);
+ if (artist)
+ add_field (row ++, _("Artist"), artist);
+ if (album)
+ add_field (row ++, _("Album"), album);
+ if (genre)
+ add_field (row ++, _("Genre"), genre);
+ if (year > 0)
+ add_field (row ++, _("Year"), int_to_str (year));
+ if (track > 0)
+ add_field (row ++, _("Track"), int_to_str (track));
+ if (length > 0)
+ add_field (row ++, _("Length"), str_format_time (length));
+}
+
+void InfoPopup::add_field (int row, const char * field, const char * value)
+{
+ auto header = new QLabel (this);
+ header->setTextFormat (Qt::RichText);
+ header->setText (QString ("<i><font color=\"#a0a0a0\">%1</font></i>").arg (field));
+ m_grid.addWidget (header, row, 0, Qt::AlignRight);
+
+ auto label = new QLabel (this);
+ header->setTextFormat (Qt::RichText);
+ auto html = QString (value).toHtmlEscaped ();
+ label->setText (QString ("<font color=\"#ffffff\">%1</font>").arg (html));
+ m_grid.addWidget (label, row, 1, Qt::AlignLeft);
+}
+
+void InfoPopup::art_ready (const char * filename)
+{
+ if (m_queued && strcmp (filename, m_filename) == 0)
+ finish_loading ();
+}
+
+void InfoPopup::finish_loading ()
+{
+ QImage image = art_request (m_filename, & m_queued);
+
+ if (! image.isNull ())
+ {
+ auto label = new QLabel (this);
+ label->setPixmap (art_scale (image, sizes.OneInch, sizes.OneInch));
+ m_hbox.insertWidget (0, label);
+ }
+
+ if (! m_queued)
+ show ();
+}
+
+void InfoPopup::paintEvent (QPaintEvent *)
+{
+ QLinearGradient grad (0, 0, 0, height ());
+ grad.setStops (m_stops);
+
+ QPainter p (this);
+ p.fillRect (rect (), grad);
+}
+
+static InfoPopup * s_infopopup;
+
+static void infopopup_show (const String & filename, const Tuple & tuple)
+{
+ delete s_infopopup;
+ s_infopopup = new InfoPopup (filename, tuple);
+
+ QObject::connect (s_infopopup, & QObject::destroyed, [] () {
+ s_infopopup = nullptr;
+ });
+}
+
+EXPORT void infopopup_show (Playlist playlist, int entry)
+{
+ String filename = playlist.entry_filename (entry);
+ Tuple tuple = playlist.entry_tuple (entry);
+
+ if (filename && tuple.valid ())
+ infopopup_show (filename, tuple);
+}
+
+EXPORT void infopopup_show_current ()
+{
+ auto playlist = Playlist::playing_playlist ();
+ if (playlist == Playlist ())
+ playlist = Playlist::active_playlist ();
+
+ int position = playlist.get_position ();
+ if (position >= 0)
+ infopopup_show (playlist, position);
+}
+
+EXPORT void infopopup_hide ()
+{
+ delete s_infopopup;
+}
+
+} // namespace audqt
diff --git a/src/libaudqt/infowin.cc b/src/libaudqt/infowin-qt.cc
index e51159a..2f4161c 100644
--- a/src/libaudqt/infowin.cc
+++ b/src/libaudqt/infowin-qt.cc
@@ -18,13 +18,18 @@
* the use of this software.
*/
+#include <math.h>
+
#include <QDialog>
#include <QDialogButtonBox>
+#include <QEvent>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QPixmap>
+#include <QPainter>
#include <QPushButton>
+#include <QTextDocument>
#include <QVBoxLayout>
#include <libaudcore/audstrings.h>
@@ -36,9 +41,63 @@
#include "info-widget.h"
#include "libaudqt.h"
+#include "libaudqt-internal.h"
namespace audqt {
+/* This class remedies some of the deficiencies of QLabel (such as lack
+ * of proper wrapping). It can be expanded and/or made more visible if
+ * it turns out to be useful outside InfoWindow. */
+class TextWidget : public QWidget
+{
+public:
+ TextWidget ()
+ {
+ m_doc.setDefaultFont (font ());
+ }
+
+ void setText (const QString & text)
+ {
+ m_doc.setPlainText (text);
+ updateGeometry ();
+ }
+
+ void setWidth (int width)
+ {
+ m_doc.setTextWidth (width);
+ updateGeometry ();
+ }
+
+protected:
+ QSize sizeHint () const override
+ {
+ qreal width = m_doc.idealWidth ();
+ qreal height = m_doc.size ().height ();
+ return QSize (ceil (width), ceil (height));
+ }
+
+ QSize minimumSizeHint () const override
+ { return sizeHint (); }
+
+ void changeEvent (QEvent * event) override
+ {
+ if (event->type () == QEvent::FontChange)
+ {
+ m_doc.setDefaultFont (font ());
+ updateGeometry ();
+ }
+ }
+
+ void paintEvent (QPaintEvent * event) override
+ {
+ QPainter painter (this);
+ m_doc.drawContents (& painter);
+ }
+
+private:
+ QTextDocument m_doc;
+};
+
class InfoWindow : public QDialog
{
public:
@@ -50,6 +109,7 @@ public:
private:
String m_filename;
QLabel m_image;
+ TextWidget m_uri_label;
InfoWidget m_infowidget;
void displayImage (const char * filename);
@@ -63,8 +123,22 @@ InfoWindow::InfoWindow (QWidget * parent) : QDialog (parent)
setWindowTitle (_("Song Info"));
setContentsMargins (margins.TwoPt);
+ m_image.setAlignment (Qt::AlignCenter);
+ m_uri_label.setWidth (2 * audqt::sizes.OneInch);
+ m_uri_label.setContextMenuPolicy (Qt::CustomContextMenu);
+
+ connect (& m_uri_label, & QWidget::customContextMenuRequested, [this] (const QPoint & pos) {
+ show_copy_context_menu (this, m_uri_label.mapToGlobal (pos), QString (m_filename));
+ });
+
+ auto left_vbox = make_vbox (nullptr);
+ left_vbox->addWidget (& m_image);
+ left_vbox->addWidget (& m_uri_label);
+ left_vbox->setStretch (0, 1);
+ left_vbox->setStretch (1, 0);
+
auto hbox = make_hbox (nullptr);
- hbox->addWidget (& m_image);
+ hbox->addLayout (left_vbox);
hbox->addWidget (& m_infowidget);
auto vbox = make_vbox (this);
@@ -75,18 +149,19 @@ InfoWindow::InfoWindow (QWidget * parent) : QDialog (parent)
bbox->button (QDialogButtonBox::Close)->setText (translate_str (N_("_Close")));
vbox->addWidget (bbox);
- QObject::connect (bbox, & QDialogButtonBox::accepted, [this] () {
+ connect (bbox, & QDialogButtonBox::accepted, [this] () {
m_infowidget.updateFile ();
deleteLater ();
});
- QObject::connect (bbox, & QDialogButtonBox::rejected, this, & QObject::deleteLater);
+ connect (bbox, & QDialogButtonBox::rejected, this, & QObject::deleteLater);
}
void InfoWindow::fillInfo (const char * filename, const Tuple & tuple,
PluginHandle * decoder, bool updating_enabled)
{
m_filename = String (filename);
+ m_uri_label.setText ((QString) uri_to_display (filename));
displayImage (filename);
m_infowidget.fillInfo (filename, tuple, decoder, updating_enabled);
}
diff --git a/src/libaudqt/libaudqt-internal.h b/src/libaudqt/libaudqt-internal.h
index ea00ed9..2aec88f 100644
--- a/src/libaudqt/libaudqt-internal.h
+++ b/src/libaudqt/libaudqt-internal.h
@@ -1,6 +1,6 @@
/*
* libaudqt-internal.h
- * Copyright 2016 John Lindgren
+ * Copyright 2016-2017 John Lindgren
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -20,12 +20,28 @@
#ifndef LIBAUDQT_INTERNAL_H
#define LIBAUDQT_INTERNAL_H
+#include <QWidget>
+
+class QPoint;
+class QScreen;
+class QString;
+
namespace audqt {
/* log-inspector.cc */
void log_init ();
void log_cleanup ();
+/* util-qt.cc */
+class PopupWidget : public QWidget
+{
+protected:
+ void showEvent (QShowEvent *) override;
+};
+
+void show_copy_context_menu (QWidget * parent, const QPoint & global_pos,
+ const QString & text_to_copy);
+
} // namespace audqt
#endif // LIBAUDQT_INTERNAL_H
diff --git a/src/libaudqt/libaudqt.h b/src/libaudqt/libaudqt.h
index 3f242c2..e2a23de 100644
--- a/src/libaudqt/libaudqt.h
+++ b/src/libaudqt/libaudqt.h
@@ -25,6 +25,7 @@
#include <QString>
#include <libaudcore/objects.h>
+class QIcon;
class QLayout;
class QBoxLayout;
class QHBoxLayout;
@@ -46,6 +47,8 @@ enum class FileMode {
OpenFolder,
Add,
AddFolder,
+ ImportPlaylist,
+ ExportPlaylist,
count
};
@@ -97,6 +100,8 @@ void run ();
void quit ();
void cleanup ();
+QIcon get_icon (const char * name);
+
QHBoxLayout * make_hbox (QWidget * parent, int spacing = sizes.FourPt);
QVBoxLayout * make_vbox (QWidget * parent, int spacing = sizes.FourPt);
@@ -129,10 +134,17 @@ void prefswin_show_plugin_page (PluginType type);
void log_inspector_show ();
void log_inspector_hide ();
-/* art.cc */
+/* art-qt.cc */
+QImage art_request (const char * filename, bool * queued = nullptr);
+QPixmap art_scale (const QImage & image, unsigned int w, unsigned int h, bool want_hidpi = true);
QPixmap art_request (const char * filename, unsigned int w, unsigned int h, bool want_hidpi = true);
QPixmap art_request_current (unsigned int w, unsigned int h, bool want_hidpi = true);
+/* infopopup-qt.cc */
+void infopopup_show (Playlist playlist, int entry);
+void infopopup_show_current ();
+void infopopup_hide ();
+
/* infowin.cc */
void infowin_show (Playlist playlist, int entry);
void infowin_show_current ();
diff --git a/src/libaudqt/log-inspector.cc b/src/libaudqt/log-inspector.cc
index bcaefd5..e582a29 100644
--- a/src/libaudqt/log-inspector.cc
+++ b/src/libaudqt/log-inspector.cc
@@ -217,7 +217,7 @@ LogEntryInspector::LogEntryInspector (QWidget * parent) :
auto btnbox = new QDialogButtonBox (this);
auto btn1 = btnbox->addButton (translate_str (N_("Cl_ear")), QDialogButtonBox::ActionRole);
- btn1->setIcon (QIcon::fromTheme ("edit-clear-all"));
+ btn1->setIcon (audqt::get_icon ("edit-clear-all"));
btn1->setAutoDefault (false);
QObject::connect (btn1, & QPushButton::clicked, [] () {
s_model.get ()->cleanup ();
diff --git a/src/libaudqt/menu.cc b/src/libaudqt/menu-qt.cc
index 8f405b5..2ce34de 100644
--- a/src/libaudqt/menu.cc
+++ b/src/libaudqt/menu-qt.cc
@@ -74,7 +74,7 @@ MenuAction::MenuAction (const MenuItem & item, const char * domain, QWidget * pa
#ifndef Q_OS_MAC
if (item.text.icon && QIcon::hasThemeIcon (item.text.icon))
- setIcon (QIcon::fromTheme (item.text.icon));
+ setIcon (audqt::get_icon (item.text.icon));
#endif
if (item.text.shortcut)
@@ -117,8 +117,12 @@ EXPORT QMenu * menu_build (ArrayRef<MenuItem> menu_items, const char * domain, Q
EXPORT QMenuBar * menubar_build (ArrayRef<MenuItem> menu_items, const char * domain, QWidget * parent)
{
+#ifdef Q_OS_MAC
+ QMenuBar * m = new QMenuBar (nullptr);
+#else
QMenuBar * m = new QMenuBar (parent);
m->setContextMenuPolicy (Qt::PreventContextMenu);
+#endif
for (auto & it : menu_items)
m->addAction (new MenuAction (it, domain, parent));
diff --git a/src/libaudqt/playlist-management.cc b/src/libaudqt/playlist-management.cc
index 60b3120..c6e0899 100644
--- a/src/libaudqt/playlist-management.cc
+++ b/src/libaudqt/playlist-management.cc
@@ -82,8 +82,8 @@ static QDialog * buildDeleteDialog (Playlist playlist)
dialog->addButton (remove, QMessageBox::AcceptRole);
dialog->addButton (cancel, QMessageBox::RejectRole);
- remove->setIcon (QIcon::fromTheme ("edit-delete"));
- cancel->setIcon (QIcon::fromTheme ("process-stop"));
+ remove->setIcon (audqt::get_icon ("edit-delete"));
+ cancel->setIcon (audqt::get_icon ("process-stop"));
QObject::connect (skip_prompt, & QCheckBox::stateChanged, [] (int state) {
aud_set_bool ("audgui", "no_confirm_playlist_delete", (state == Qt::Checked));
diff --git a/src/libaudqt/plugin-menu.cc b/src/libaudqt/plugin-menu-qt.cc
index 935d83a..344ad7e 100644
--- a/src/libaudqt/plugin-menu.cc
+++ b/src/libaudqt/plugin-menu-qt.cc
@@ -42,7 +42,7 @@ static void show_prefs ()
}
MenuItem default_menu_items[] = {
- MenuCommand ({N_("Plugins ..."), "preferences-system"}, show_prefs),
+ MenuCommand ({N_("_Plugins ..."), "preferences-system"}, show_prefs),
};
void menu_rebuild (AudMenuID id)
diff --git a/src/libaudqt/prefs-pluginlist-model.cc b/src/libaudqt/prefs-pluginlist-model.cc
index 969cad7..ea82f43 100644
--- a/src/libaudqt/prefs-pluginlist-model.cc
+++ b/src/libaudqt/prefs-pluginlist-model.cc
@@ -24,6 +24,7 @@
#include <libaudcore/i18n.h>
#include <libaudcore/plugins.h>
#include <libaudcore/runtime.h>
+#include <libaudqt/libaudqt.h>
namespace audqt {
@@ -155,13 +156,13 @@ QVariant PluginListModel::data (const QModelIndex & index, int role) const
case AboutColumn:
if (role == Qt::DecorationRole && enabled && aud_plugin_has_about (p))
- return QIcon::fromTheme ("dialog-information");
+ return audqt::get_icon ("dialog-information");
break;
case SettingsColumn:
if (role == Qt::DecorationRole && enabled && aud_plugin_has_configure (p))
- return QIcon::fromTheme ("preferences-system");
+ return audqt::get_icon ("preferences-system");
break;
}
diff --git a/src/libaudqt/prefs-pluginlist-model.h b/src/libaudqt/prefs-pluginlist-model.h
index aaf1874..adb0130 100644
--- a/src/libaudqt/prefs-pluginlist-model.h
+++ b/src/libaudqt/prefs-pluginlist-model.h
@@ -34,6 +34,7 @@ public:
NameColumn,
AboutColumn,
SettingsColumn,
+ SpacerColumn,
NumColumns
};
diff --git a/src/libaudqt/prefs-widget.cc b/src/libaudqt/prefs-widget-qt.cc
index c67280f..c67280f 100644
--- a/src/libaudqt/prefs-widget.cc
+++ b/src/libaudqt/prefs-widget-qt.cc
diff --git a/src/libaudqt/prefs-window.cc b/src/libaudqt/prefs-window-qt.cc
index 19f5dc1..42f5798 100644
--- a/src/libaudqt/prefs-window.cc
+++ b/src/libaudqt/prefs-window-qt.cc
@@ -112,7 +112,7 @@ PrefsWindow * PrefsWindow::instance = nullptr;
int PrefsWindow::output_combo_selected;
struct Category {
- const char * icon_path;
+ const char * icon;
const char * name;
};
@@ -128,22 +128,24 @@ enum {
CATEGORY_PLAYLIST,
CATEGORY_SONG_INFO,
CATEGORY_PLUGINS,
+ CATEGORY_ADVANCED,
CATEGORY_COUNT
};
static const Category categories[] = {
- { "appearance.png", N_("Appearance") },
- { "audio.png", N_("Audio") },
- { "connectivity.png", N_("Network") },
- { "playlist.png", N_("Playlist")} ,
- { "info.png", N_("Song Info") },
- { "plugins.png", N_("Plugins") }
+ { "applications-graphics", N_("Appearance") },
+ { "audio-volume-medium", N_("Audio") },
+ { "applications-internet", N_("Network") },
+ { "audio-x-generic", N_("Playlist")} ,
+ { "dialog-information", N_("Song Info") },
+ { "applications-system", N_("Plugins") },
+ { "preferences-system", N_("Advanced") }
};
static const TitleFieldTag title_field_tags[] = {
{ N_("Artist") , "${artist}" },
{ N_("Album") , "${album}" },
- { N_("Album Artist"), "${album-artist}" },
+ { N_("Album artist"), "${album-artist}" },
{ N_("Title") , "${title}" },
{ N_("Track number"), "${track-number}" },
{ N_("Genre") , "${genre}" },
@@ -331,10 +333,9 @@ static const PreferencesWidget playlist_page_widgets[] = {
WidgetCheck (N_("Show hours separately (1:30:00 vs. 90:00)"),
WidgetBool (0, "show_hours", send_title_change)),
WidgetCustomQt (create_titlestring_table),
- WidgetLabel (N_("<b>Compatibility</b>")),
- WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"),
- WidgetBool (0, "convert_backslash")),
- WidgetTable ({{chardet_elements}})
+ WidgetLabel (N_("<b>Export</b>")),
+ WidgetCheck (N_("Use relative paths when possible"),
+ WidgetBool (0, "export_relative_paths"))
};
static const PreferencesWidget song_info_page_widgets[] = {
@@ -360,8 +361,20 @@ static const PreferencesWidget song_info_page_widgets[] = {
WIDGET_CHILD),
WidgetCheck (N_("Show time scale for current song"),
WidgetBool (0, "filepopup_showprogressbar"),
- WIDGET_CHILD),
- WidgetLabel (N_("<b>Advanced</b>")),
+ WIDGET_CHILD)
+};
+
+static const PreferencesWidget advanced_page_widgets[] = {
+ WidgetLabel (N_("<b>Compatibility</b>")),
+ WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"),
+ WidgetBool (0, "convert_backslash")),
+ WidgetTable ({{chardet_elements}}),
+ WidgetLabel (N_("<b>Playlist</b>")),
+ WidgetCheck (N_("Add folders recursively"),
+ WidgetBool (0, "recurse_folders")),
+ WidgetCheck (N_("Add folders nested within playlist files"),
+ WidgetBool (0, "folders_in_playlist")),
+ WidgetLabel (N_("<b>Metadata</b>")),
WidgetCheck (N_("Guess missing metadata from file path"),
WidgetBool (0, "metadata_fallbacks")),
WidgetCheck (N_("Do not load metadata for songs until played"),
@@ -439,7 +452,7 @@ static void * create_titlestring_table ()
/* build menu */
QPushButton * btn_mnu = new QPushButton (w);
btn_mnu->setFixedWidth (btn_mnu->sizeHint ().height ());
- btn_mnu->setIcon (QIcon::fromTheme ("list-add"));
+ btn_mnu->setIcon (audqt::get_icon ("list-add"));
l->addWidget (btn_mnu, 1, 2);
QMenu * mnu_fields = new QMenu (w);
@@ -547,19 +560,20 @@ static void create_plugin_category (QStackedWidget * parent)
s_plugin_view->setModel (s_plugin_model);
s_plugin_view->setSelectionMode (QTreeView::NoSelection);
+ s_plugin_view->setAlternatingRowColors (true);
auto header = s_plugin_view->header ();
header->hide ();
header->setSectionResizeMode (header->ResizeToContents);
- header->setStretchLastSection (false);
+ header->setStretchLastSection (true);
parent->addWidget (s_plugin_view);
QObject::connect (s_plugin_view, & QAbstractItemView::clicked, [] (const QModelIndex & index)
{
auto p = s_plugin_model->pluginForIndex (index);
- if (! p)
+ if (! p || ! aud_plugin_get_enabled (p))
return;
switch (index.column ())
@@ -614,6 +628,7 @@ PrefsWindow::PrefsWindow () :
create_category (s_category_notebook, playlist_page_widgets);
create_category (s_category_notebook, song_info_page_widgets);
create_plugin_category (s_category_notebook);
+ create_category (s_category_notebook, advanced_page_widgets);
QDialogButtonBox * bbox = new QDialogButtonBox (QDialogButtonBox::Close);
bbox->button (QDialogButtonBox::Close)->setText (translate_str (N_("_Close")));
@@ -622,15 +637,14 @@ PrefsWindow::PrefsWindow () :
QObject::connect (bbox, & QDialogButtonBox::rejected, this, & QObject::deleteLater);
QSignalMapper * mapper = new QSignalMapper (this);
- const char * data_dir = aud_get_path (AudPath::DataDir);
QObject::connect (mapper, static_cast <void (QSignalMapper::*)(int)>(&QSignalMapper::mapped),
s_category_notebook, static_cast <void (QStackedWidget::*)(int)>(&QStackedWidget::setCurrentIndex));
for (int i = 0; i < CATEGORY_COUNT; i ++)
{
- QIcon ico (QString (filename_build ({data_dir, "images", categories[i].icon_path})));
- QAction * a = new QAction (ico, translate_str (categories[i].name), toolbar);
+ auto a = new QAction (get_icon (categories[i].icon),
+ translate_str (categories[i].name), toolbar);
toolbar->addAction (a);
mapper->setMapping (a, i);
diff --git a/src/libaudqt/queue-manager.cc b/src/libaudqt/queue-manager-qt.cc
index 11301b3..11301b3 100644
--- a/src/libaudqt/queue-manager.cc
+++ b/src/libaudqt/queue-manager-qt.cc
diff --git a/src/libaudqt/url-opener.cc b/src/libaudqt/url-opener-qt.cc
index 49645f0..b5bcebf 100644
--- a/src/libaudqt/url-opener.cc
+++ b/src/libaudqt/url-opener-qt.cc
@@ -26,6 +26,7 @@
#include <libaudcore/drct.h>
#include <libaudcore/i18n.h>
+#include <libaudcore/preferences.h>
#include <libaudcore/runtime.h>
#include "libaudqt.h"
@@ -34,6 +35,11 @@ namespace audqt {
static QDialog * buildUrlDialog (bool open)
{
+ static const PreferencesWidget widgets[] = {
+ WidgetCheck (N_("_Save to history"),
+ WidgetBool (0, "save_url_history"))
+ };
+
const char * title, * verb, * icon;
if (open)
@@ -59,11 +65,19 @@ static QDialog * buildUrlDialog (bool open)
combobox->setEditable (true);
combobox->setMinimumContentsLength (50);
+ auto clear_button = new QPushButton (translate_str (N_("C_lear history")), dialog);
+ clear_button->setIcon (audqt::get_icon ("edit-clear"));
+
+ auto hbox = make_hbox (nullptr);
+ prefs_populate (hbox, widgets, PACKAGE);
+ hbox->addStretch (1);
+ hbox->addWidget (clear_button);
+
auto button1 = new QPushButton (translate_str (verb), dialog);
- button1->setIcon (QIcon::fromTheme (icon));
+ button1->setIcon (audqt::get_icon (icon));
auto button2 = new QPushButton (translate_str (N_("_Cancel")), dialog);
- button2->setIcon (QIcon::fromTheme ("process-stop"));
+ button2->setIcon (audqt::get_icon ("process-stop"));
auto buttonbox = new QDialogButtonBox (dialog);
buttonbox->addButton (button1, QDialogButtonBox::AcceptRole);
@@ -72,6 +86,7 @@ static QDialog * buildUrlDialog (bool open)
auto layout = make_vbox (dialog);
layout->addWidget (label);
layout->addWidget (combobox);
+ layout->addLayout (hbox);
layout->addStretch (1);
layout->addWidget (buttonbox);
@@ -85,6 +100,11 @@ static QDialog * buildUrlDialog (bool open)
}
combobox->setCurrentIndex (-1);
+ QObject::connect (clear_button, & QPushButton::pressed, [combobox] () {
+ combobox->clear ();
+ aud_history_clear ();
+ });
+
QObject::connect (buttonbox, & QDialogButtonBox::rejected, dialog, & QDialog::close);
QObject::connect (buttonbox, & QDialogButtonBox::accepted, [dialog, combobox, open] () {
@@ -95,7 +115,9 @@ static QDialog * buildUrlDialog (bool open)
else
aud_drct_pl_add (url, -1);
- aud_history_add (url);
+ if (aud_get_bool (nullptr, "save_url_history"))
+ aud_history_add (url);
+
dialog->close ();
});
diff --git a/src/libaudqt/util-qt.cc b/src/libaudqt/util-qt.cc
new file mode 100644
index 0000000..213e983
--- /dev/null
+++ b/src/libaudqt/util-qt.cc
@@ -0,0 +1,97 @@
+/*
+ * util-qt.cc
+ * Copyright 2017 René Bertin and John Lindgren
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions, and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions, and the following disclaimer in the documentation
+ * provided with the distribution.
+ *
+ * This software is provided "as is" and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising from
+ * the use of this software.
+ */
+
+#include "libaudqt.h"
+#include "libaudqt-internal.h"
+
+#include <QAction>
+#include <QApplication>
+#include <QClipboard>
+#include <QCursor>
+#include <QMenu>
+#include <QMimeData>
+#include <QWindow>
+#include <QScreen>
+
+#include <libaudcore/i18n.h>
+
+namespace audqt {
+
+void PopupWidget::showEvent (QShowEvent *)
+{
+ auto pos = QCursor::pos ();
+ auto geom = QApplication::primaryScreen ()->geometry ();
+
+ /* find the screen the cursor is on */
+ if (! geom.contains (pos))
+ {
+ for (auto screen : QApplication::screens ())
+ {
+ auto geom2 = screen->geometry ();
+ if (geom2.contains (pos))
+ {
+ geom = geom2;
+ break;
+ }
+ }
+ }
+
+ int x = pos.x ();
+ int y = pos.y ();
+ int w = width ();
+ int h = height ();
+
+ /* If we show the popup right under the cursor, the underlying window gets
+ * a leaveEvent and immediately hides the popup again. So, we offset the
+ * popup slightly. */
+ if (x + w > geom.x () + geom.width ())
+ x -= w + 3;
+ else
+ x += 3;
+
+ if (y + h > geom.y () + geom.height ())
+ y -= h + 3;
+ else
+ y += 3;
+
+ move (x, y);
+}
+
+void show_copy_context_menu (QWidget * parent, const QPoint & global_pos,
+ const QString & text_to_copy)
+{
+ auto menu = new QMenu (parent);
+ auto action = new QAction (audqt::get_icon ("edit-copy"), N_("Copy"), menu);
+
+ QObject::connect (action, & QAction::triggered, action, [text_to_copy] () {
+ auto data = new QMimeData;
+ data->setText (text_to_copy);
+ QApplication::clipboard ()->setMimeData (data);
+ });
+
+ /* delete the menu as soon as it's closed */
+ QObject::connect (menu, & QMenu::aboutToHide, [menu] () {
+ menu->deleteLater ();
+ });
+
+ menu->addAction (action);
+ menu->popup (global_pos);
+}
+
+} // namespace audqt
diff --git a/src/libaudqt/volumebutton.cc b/src/libaudqt/volumebutton.cc
index 2668263..673002d 100644
--- a/src/libaudqt/volumebutton.cc
+++ b/src/libaudqt/volumebutton.cc
@@ -86,13 +86,13 @@ VolumeButton::VolumeButton (QWidget * parent) :
void VolumeButton::updateIcon (int val)
{
if (val == 0)
- setIcon (QIcon::fromTheme ("audio-volume-muted"));
+ setIcon (audqt::get_icon ("audio-volume-muted"));
else if (val < 34)
- setIcon (QIcon::fromTheme ("audio-volume-low"));
+ setIcon (audqt::get_icon ("audio-volume-low"));
else if (val < 67)
- setIcon (QIcon::fromTheme ("audio-volume-medium"));
+ setIcon (audqt::get_icon ("audio-volume-medium"));
else
- setIcon (QIcon::fromTheme ("audio-volume-high"));
+ setIcon (audqt::get_icon ("audio-volume-high"));
setToolTip (QString ("%1 %").arg (val));
}