diff options
Diffstat (limited to 'src')
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)); } |