diff --git a/.github/workflows/Linux-libretro-common-samples.yml b/.github/workflows/Linux-libretro-common-samples.yml index 378e02809197..5f83fb78f7eb 100644 --- a/.github/workflows/Linux-libretro-common-samples.yml +++ b/.github/workflows/Linux-libretro-common-samples.yml @@ -80,6 +80,8 @@ jobs: task_queue_title_error_test tpool_wait_test retro_atomic_test + retro_atomic_extern_c_linkage_test + retro_atomic_extern_c_linkage_test_cxx retro_spsc_test ) @@ -212,6 +214,25 @@ jobs: # behavioural SPSC stress is already covered by the C test # binary above on this same host, and the C++11 backend bottoms # out through the same libstdc++ __atomic_* builtins. + # + # Two fixtures share the g++/clang++ x c++11/14/17 sweep: + # + # cxx_smoke.cpp : header included at C++ linkage, + # the "ordinary" C++ TU shape. + # cxx_smoke_extern_c.cpp : header included from inside an + # extern "C" { ... } wrap, the + # shape ui_qt.cpp uses under + # !CXX_BUILD. This is the + # regression case for the + # RETRO_BEGIN_DECLS_CXX shield in + # retro_atomic.h's C++11 backend. + # The standalone build of the + # sample at samples/atomic/ + # retro_atomic_extern_c_linkage/ + # covers the same case under the + # default toolchain; this matrix + # extends coverage to clang++ and + # the higher language standards. set -u set -o pipefail @@ -254,16 +275,41 @@ jobs: } EOF + # Regression fixture: include retro_atomic.h from inside an + # extern "C" wrap. Faithful mirror of ui_qt.cpp's pattern. + # Pre-fix retro_atomic.h would emit ~80 "template with C + # linkage" errors here; the RETRO_BEGIN_DECLS_CXX shield + # around makes this compile cleanly. + cat > "$tmpdir/cxx_smoke_extern_c.cpp" <<'EOF' + #include + #include + + extern "C" { + #include + } + + int main(void) { + retro_atomic_int_t ai; retro_atomic_int_init(&ai, 0); + retro_atomic_store_release_int(&ai, 7); + int v = retro_atomic_load_acquire_int(&ai); + std::printf("backend: %s\n", RETRO_ATOMIC_BACKEND_NAME); + std::puts(v == 7 ? "ALL OK" : "FAIL"); + return v == 7 ? 0 : 1; + } + EOF + for cxx in g++ clang++; do for std in c++11 c++14 c++17; do - echo "==> compile-test with $cxx -std=$std" - $cxx -std=$std -Wall -Wextra -pedantic -O2 \ - -I include \ - "$tmpdir/cxx_smoke.cpp" \ - -o "$tmpdir/cxx_smoke" \ - || { echo "::error title=C++ compile failed::$cxx -std=$std"; exit 1; } - "$tmpdir/cxx_smoke" \ - || { echo "::error title=C++ smoke failed::$cxx -std=$std"; exit 1; } + for fixture in cxx_smoke cxx_smoke_extern_c; do + echo "==> compile-test with $cxx -std=$std ($fixture)" + $cxx -std=$std -Wall -Wextra -pedantic -O2 \ + -I include \ + "$tmpdir/$fixture.cpp" \ + -o "$tmpdir/$fixture" \ + || { echo "::error title=C++ compile failed::$cxx -std=$std $fixture"; exit 1; } + "$tmpdir/$fixture" \ + || { echo "::error title=C++ smoke failed::$cxx -std=$std $fixture"; exit 1; } + done done done diff --git a/deps/switchres/custom_video_xrandr.cpp b/deps/switchres/custom_video_xrandr.cpp index 83b163e68112..4a17cc26cf24 100644 --- a/deps/switchres/custom_video_xrandr.cpp +++ b/deps/switchres/custom_video_xrandr.cpp @@ -676,13 +676,13 @@ bool xrandr_timing::add_mode(modeline *mode) // remove unlinked modeline if (mode->platform_data) { - log_error("XRANDR: <%d> (add_mode) [ERROR] remove mode [%04lx]\n", m_id, mode->platform_data); + log_error("XRANDR: <%d> (add_mode) [ERROR] remove mode [%04llx]\n", m_id, (unsigned long long)mode->platform_data); XRRDestroyMode(m_pdisplay, mode->platform_data); mode->platform_data = 0; } } else - log_verbose("XRANDR: <%d> (add_mode) mode %04lx %dx%d refresh %.6f added\n", m_id, mode->platform_data, mode->hactive, mode->vactive, mode->vfreq); + log_verbose("XRANDR: <%d> (add_mode) mode %04llx %dx%d refresh %.6f added\n", m_id, (unsigned long long)mode->platform_data, mode->hactive, mode->vactive, mode->vfreq); return ms_xerrors == 0; } diff --git a/deps/switchres/display_linux.cpp b/deps/switchres/display_linux.cpp index 470647f3e383..4b496abbbdac 100644 --- a/deps/switchres/display_linux.cpp +++ b/deps/switchres/display_linux.cpp @@ -163,7 +163,7 @@ int linux_display::get_available_video_modes() video_modes.push_back(mode); backup_modes.push_back(mode); - log_verbose("Switchres: [%3ld] %4dx%4d @%3d%s%s %s: ", video_modes.size(), mode.width, mode.height, mode.refresh, mode.interlace ? "i" : "p", mode.type & MODE_DESKTOP ? "*" : "", mode.type & MODE_ROTATED ? "rot" : ""); + log_verbose("Switchres: [%3zu] %4dx%4d @%3d%s%s %s: ", video_modes.size(), mode.width, mode.height, mode.refresh, mode.interlace ? "i" : "p", mode.type & MODE_DESKTOP ? "*" : "", mode.type & MODE_ROTATED ? "rot" : ""); log_mode(&mode); }; diff --git a/gfx/drivers/vulkan.c b/gfx/drivers/vulkan.c index 70bd211d6d45..9c3c25e84b94 100644 --- a/gfx/drivers/vulkan.c +++ b/gfx/drivers/vulkan.c @@ -6744,6 +6744,7 @@ static bool vulkan_frame(void *data, const void *frame, if ((vk->context->flags & VK_CTX_FLAG_HDR_ENABLE) && ((vk->flags & VK_FLAG_MENU_ENABLE) || (vk->flags & VK_FLAG_OVERLAY_ENABLE) || message_visible + || statistics_show #ifdef HAVE_GFX_WIDGETS || widgets_visible #endif @@ -6858,6 +6859,7 @@ static bool vulkan_frame(void *data, const void *frame, if ((vk->context->flags & VK_CTX_FLAG_HDR_ENABLE) && ((vk->flags & VK_FLAG_MENU_ENABLE) || (vk->flags & VK_FLAG_OVERLAY_ENABLE) || message_visible + || statistics_show #ifdef HAVE_GFX_WIDGETS || widgets_visible #endif diff --git a/gfx/drivers_shader/slang_cache.cpp b/gfx/drivers_shader/slang_cache.cpp index f6112f0ee321..5da8c953f0fa 100644 --- a/gfx/drivers_shader/slang_cache.cpp +++ b/gfx/drivers_shader/slang_cache.cpp @@ -32,8 +32,9 @@ static bool spirv_cache_get_dir(char *cache_dir_out, size_t cache_dir_out_len) return false; /* Build the spirv subdirectory path */ - snprintf(cache_dir_out, cache_dir_out_len, "%s/%s", - settings->paths.directory_cache, SPIRV_CACHE_SUBDIR); + fill_pathname_join_special(cache_dir_out, + settings->paths.directory_cache, SPIRV_CACHE_SUBDIR, + cache_dir_out_len); return true; } @@ -65,12 +66,14 @@ static bool spirv_cache_get_filename(const char *hash, char *cache_file_out, size_t cache_file_out_len) { char cache_dir[PATH_MAX_LENGTH]; + char hash_filename[128]; if (!spirv_cache_get_dir(cache_dir, sizeof(cache_dir))) return false; - snprintf(cache_file_out, cache_file_out_len, "%s/%s.spirv", - cache_dir, hash); + snprintf(hash_filename, sizeof(hash_filename), "%s.spirv", hash); + fill_pathname_join_special(cache_file_out, cache_dir, hash_filename, + cache_file_out_len); return true; } diff --git a/libretro-common/include/retro_atomic.h b/libretro-common/include/retro_atomic.h index f8d4be78f485..40923290fffb 100644 --- a/libretro-common/include/retro_atomic.h +++ b/libretro-common/include/retro_atomic.h @@ -23,6 +23,8 @@ #ifndef __LIBRETRO_SDK_ATOMIC_H #define __LIBRETRO_SDK_ATOMIC_H +#include + /* Minimal portable atomic operations for SPSC patterns. * * This header consolidates the ad-hoc atomic shims previously duplicated @@ -321,10 +323,12 @@ #endif /* The header contains only macros and integer typedefs; there are no - * function declarations and therefore no need for RETRO_BEGIN_DECLS / - * extern "C" wrapping. In particular the C++11 backend below - * #includes , whose templates cannot be declared with C - * linkage, so the wrapper would actively break that path. */ + * function declarations and therefore no need for RETRO_BEGIN_DECLS + * around the file. The C++11 backend below #includes , whose + * templates cannot be declared with C linkage; if a caller wraps its + * #include of this header in extern "C" { ... } (e.g. ui_qt.cpp under + * !CXX_BUILD), libstdc++ emits dozens of "template with C + * linkage" errors. RETRO_BEGIN_DECLS_CXX below escapes that. */ /* ---- C11 ------------------------------------------------- */ #if defined(RETRO_ATOMIC_BACKEND_C11) @@ -359,8 +363,10 @@ typedef atomic_size_t retro_atomic_size_t; /* ---- C++11 --------------------------------------------------- */ #elif defined(RETRO_ATOMIC_BACKEND_CXX11) +RETRO_BEGIN_DECLS_CXX #include #include +RETRO_END_DECLS_CXX /* This header is included by C++ TUs in C++11+ mode (gated on * __cplusplus >= 201103L or _MSVC_LANG >= 201103L). We use the * std::atomic_* free-function forms rather than the member-function diff --git a/libretro-common/include/retro_common_api.h b/libretro-common/include/retro_common_api.h index ace2ef6ac00a..a8b79e4310a4 100644 --- a/libretro-common/include/retro_common_api.h +++ b/libretro-common/include/retro_common_api.h @@ -40,6 +40,8 @@ special technique is called for. #define RETRO_BEGIN_DECLS #define RETRO_END_DECLS +#define RETRO_BEGIN_DECLS_CXX +#define RETRO_END_DECLS_CXX #ifdef __cplusplus @@ -50,6 +52,13 @@ special technique is called for. #undef RETRO_END_DECLS #define RETRO_BEGIN_DECLS extern "C" { #define RETRO_END_DECLS } +/* Force C++ linkage for a region inside a header that may be included + * from within a caller's extern "C" { ... } block -- needed when the + * region pulls in a C++ standard library header (e.g. ). */ +#undef RETRO_BEGIN_DECLS_CXX +#undef RETRO_END_DECLS_CXX +#define RETRO_BEGIN_DECLS_CXX extern "C++" { +#define RETRO_END_DECLS_CXX } #endif #else diff --git a/libretro-common/samples/atomic/retro_atomic_extern_c_linkage/Makefile b/libretro-common/samples/atomic/retro_atomic_extern_c_linkage/Makefile new file mode 100644 index 000000000000..25b90fabf2f3 --- /dev/null +++ b/libretro-common/samples/atomic/retro_atomic_extern_c_linkage/Makefile @@ -0,0 +1,50 @@ +TARGET := retro_atomic_extern_c_linkage_test +TARGET_TEST := retro_atomic_extern_c_linkage_test_cxx + +LIBRETRO_COMM_DIR := ../../.. + +# Two binaries from one source. The source mirrors ui_qt.cpp's pattern +# of wrapping a RetroArch header include in extern "C" { ... }, gated +# on !CXX_BUILD. We build it once each way: +# +# $(TARGET) : non-unity / separate-TU build (extern "C" wrap is +# active). This is the configuration that triggered +# the original "template with C linkage" failure on +# g++ before the retro_common_api.h fix. +# +# $(TARGET_TEST) : unity / griffin build (CXX_BUILD defined, extern +# "C" wrappers compile out). Validates that the +# fix did not regress the unity-build path. +# +# Both must compile and pass. Build failure on $(TARGET) is the +# regression signal for the original bug. + +SOURCE := retro_atomic_extern_c_linkage_test.cpp + +# C++11 is the minimum that engages retro_atomic.h's CXX11 backend. +# -Wall -Wextra to catch any incidental warning the test's macro +# expansions might produce on a future toolchain. +CXXFLAGS += -Wall -Wextra -pedantic -std=c++11 -g -O0 \ + -I$(LIBRETRO_COMM_DIR)/include + +ifneq ($(SANITIZER),) + CXXFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CXXFLAGS) + LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS) +endif + +# The unity-build target gets CXX_BUILD defined, exactly as +# Makefile.griffin does for griffin_cpp.cpp's TUs. +$(TARGET_TEST): CXXFLAGS += -DCXX_BUILD + +all: $(TARGET) $(TARGET_TEST) + +$(TARGET): $(SOURCE) + $(CXX) -o $@ $< $(CXXFLAGS) $(LDFLAGS) + +$(TARGET_TEST): $(SOURCE) + $(CXX) -o $@ $< $(CXXFLAGS) $(LDFLAGS) + +clean: + rm -f $(TARGET) $(TARGET_TEST) + +.PHONY: all clean diff --git a/libretro-common/samples/atomic/retro_atomic_extern_c_linkage/retro_atomic_extern_c_linkage_test.cpp b/libretro-common/samples/atomic/retro_atomic_extern_c_linkage/retro_atomic_extern_c_linkage_test.cpp new file mode 100644 index 000000000000..32ccd06b51e2 --- /dev/null +++ b/libretro-common/samples/atomic/retro_atomic_extern_c_linkage/retro_atomic_extern_c_linkage_test.cpp @@ -0,0 +1,224 @@ +/* Copyright (C) 2010-2026 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (retro_atomic_extern_c_linkage_test.cpp). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* Regression test for the linkage hazard in + * libretro-common/include/retro_atomic.h's C++11 backend. + * + * Background + * ---------- + * Several C++ TUs in RetroArch wrap their includes of in-tree C + * headers in extern "C" { ... } when CXX_BUILD is not defined. The + * canonical example is ui/drivers/ui_qt.cpp: + * + * #ifndef CXX_BUILD + * extern "C" { + * #endif + * #include "../../menu/menu_driver.h" // pulls in retro_atomic.h + * ... + * #ifndef CXX_BUILD + * } + * #endif + * + * On a C++11+ build this chain reaches retro_atomic.h's CXX11 backend, + * which #include's . libstdc++'s declares dozens of + * templates and template specializations; under C linkage every one of + * them is rejected with + * + * error: template with C linkage + * + * yielding ~80 errors on a single TU. See the build log on master at + * the time of the original report; same failure on g++ 9 through 13. + * + * Fix + * --- + * retro_common_api.h gained RETRO_BEGIN_DECLS_CXX / RETRO_END_DECLS_CXX + * macros that expand to extern "C++" { ... } in C++ mode (with + * CXX_BUILD off) and to nothing otherwise. retro_atomic.h's C++11 + * backend wraps its / include in those macros, so + * the standard headers parse at C++ linkage regardless of any + * extern "C" the caller imposes. + * + * What this test asserts + * ---------------------- + * 1. Compile-time: a C++ TU that wraps #include + * in extern "C" { ... } compiles without error. The include + * pattern below is a faithful mirror of ui_qt.cpp's; if the + * header's linkage shield breaks, this file fails to compile + * and that is the regression signal. + * + * 2. Runtime: the macro expansions inside the extern "C" wrap still + * produce live std::atomic objects whose load_acquire / + * store_release / fetch_add / fetch_sub / inc / dec round-trip + * correctly. This guards against a future "fix" that papered + * over the compile error by, say, falling back to a non-atomic + * plain-int typedef under C linkage. exit(0) on success, + * exit(1) on any property failure. + * + * 3. Both build configurations exercise the same source with no + * code-level difference (see Makefile). The Makefile builds + * two targets: + * retro_atomic_extern_c_linkage_test -- !CXX_BUILD path + * retro_atomic_extern_c_linkage_test_cxx -- CXX_BUILD path + * The second one defines CXX_BUILD before including any + * RetroArch headers, which makes the in-tree extern "C" wrappers + * compile out (because RETRO_BEGIN_DECLS / RETRO_END_DECLS expand + * to nothing). Both must build and pass. + * + * What this test does NOT assert + * ------------------------------ + * It does not exercise weakly-ordered SMP correctness or thread + * safety -- those are covered by retro_atomic_test (single-thread + * property + SPSC stress) and retro_spsc_test (lock-free SPSC) in + * neighbouring sample directories, and by the cross-arch qemu lane + * in .github/workflows/Linux-libretro-common-samples.yml. This + * test's job is purely to police the include-from-C++ hazard. + * + * Build standalone + * ---------------- + * make clean all + * ./retro_atomic_extern_c_linkage_test + * ./retro_atomic_extern_c_linkage_test_cxx + * + * Both binaries exit 0 on success. A pre-fix retro_atomic.h would + * fail at the build step rather than at runtime. + */ + +/* Mirror of ui_qt.cpp's include guard pattern. retro_common_api.h + * (transitively included from retro_atomic.h) keys on this: + * + * #ifdef CXX_BUILD + * -- no extern "C" wrappers anywhere in libretro-common headers -- + * #else + * -- normal RETRO_BEGIN_DECLS = extern "C" { wrapping -- + * #endif + * + * The Makefile builds this file twice, once with CXX_BUILD undefined + * (the non-unity build path that triggered the original bug) and once + * with CXX_BUILD defined (the griffin/unity-build path). Both must + * compile and pass the runtime checks. */ + +#include +#include + +/* This is the regression's trigger pattern: a C++ TU includes a + * RetroArch header from inside its own extern "C" block. On a stock + * (broken) retro_atomic.h, the transitive #include here would + * be parsed at C linkage and the build would die. */ +#ifndef CXX_BUILD +extern "C" { +#endif + +#include + +#ifndef CXX_BUILD +} +#endif + +/* Capability flags must be set after the include succeeds. If a + * future refactor accidentally compiles the header out under + * extern "C", these gates fire at preprocessing time with a clearer + * message than the libstdc++ template avalanche. */ +#if !defined(HAVE_RETRO_ATOMIC) +# error "retro_atomic.h: HAVE_RETRO_ATOMIC not set after include" +#endif +#if !defined(RETRO_ATOMIC_BACKEND_NAME) +# error "retro_atomic.h: RETRO_ATOMIC_BACKEND_NAME not set after include" +#endif + +/* On a C++11+ host the CXX11 backend is the expected selection. If + * something promotes a different backend in this TU we want to know; + * the whole point of the test is to police the C++11 path. */ +#if !defined(RETRO_ATOMIC_BACKEND_CXX11) +# error "retro_atomic_extern_c_linkage_test: expected the C++11 backend; if a different backend is intentional, update this gate" +#endif + +/* ---- Single-threaded property checks --------------------------------- */ +/* Each returns 0 on success, 1 on failure. Identical in shape to + * retro_atomic_test.c so a regression in either test points at the + * same place. */ + +static int check_init(void) +{ + retro_atomic_int_t ai; retro_atomic_int_init (&ai, 7); + retro_atomic_size_t as; retro_atomic_size_init(&as, (std::size_t)9); + + if (retro_atomic_load_acquire_int (&ai) != 7) return 1; + if (retro_atomic_load_acquire_size(&as) != (std::size_t)9) return 1; + return 0; +} + +static int check_store_release_load_acquire(void) +{ + retro_atomic_int_t ai; retro_atomic_int_init (&ai, 0); + retro_atomic_size_t as; retro_atomic_size_init(&as, 0); + + retro_atomic_store_release_int (&ai, 42); + retro_atomic_store_release_size(&as, (std::size_t)42); + + if (retro_atomic_load_acquire_int (&ai) != 42) return 1; + if (retro_atomic_load_acquire_size(&as) != (std::size_t)42) return 1; + return 0; +} + +static int check_fetch_add_sub_returns_previous(void) +{ + retro_atomic_int_t ai; retro_atomic_int_init (&ai, 10); + retro_atomic_size_t as; retro_atomic_size_init(&as, (std::size_t)10); + + /* fetch_add returns the previous value (POSIX convention). */ + if (retro_atomic_fetch_add_int (&ai, 5) != 10) return 1; + if (retro_atomic_fetch_add_size(&as, 5) != (std::size_t)10) return 1; + + if (retro_atomic_load_acquire_int (&ai) != 15) return 1; + if (retro_atomic_load_acquire_size(&as) != (std::size_t)15) return 1; + + if (retro_atomic_fetch_sub_int (&ai, 5) != 15) return 1; + if (retro_atomic_fetch_sub_size(&as, 5) != (std::size_t)15) return 1; + + if (retro_atomic_load_acquire_int (&ai) != 10) return 1; + if (retro_atomic_load_acquire_size(&as) != (std::size_t)10) return 1; + return 0; +} + +int main(void) +{ + int fails = 0; + + std::printf("backend: %s\n", RETRO_ATOMIC_BACKEND_NAME); +#ifdef CXX_BUILD + std::puts("config: CXX_BUILD defined (griffin/unity-build path)"); +#else + std::puts("config: CXX_BUILD undefined (separate-TU path, ui_qt.cpp shape)"); +#endif + + fails += check_init(); + fails += check_store_release_load_acquire(); + fails += check_fetch_add_sub_returns_previous(); + + if (fails) + { + std::printf("FAIL: %d check(s) failed\n", fails); + return 1; + } + std::puts("ALL OK"); + return 0; +} diff --git a/ui/drivers/ui_qt.cpp b/ui/drivers/ui_qt.cpp index ee504343f8ce..82afa79cc000 100644 --- a/ui/drivers/ui_qt.cpp +++ b/ui/drivers/ui_qt.cpp @@ -52,6 +52,7 @@ extern "C" { #include #include +#include #include #include #include @@ -135,6 +136,45 @@ static QPixmap pixmapFromPathRA(const QString &path) return QPixmap(path); } +/* Give btn a default action whose text comes from the localization + * tables, then size btn to fit. The action is parented to btn so + * Qt cleans it up automatically; the explicit parent is needed + * because QAction's default-NULL-parent overload only exists from + * Qt 5.7 onwards. */ +static void qt_button_set_action_label(QToolButton *btn, + enum msg_hash_enums label) +{ + btn->setDefaultAction(new QAction(msg_hash_to_str(label), btn)); + btn->setFixedSize(btn->sizeHint()); +} + +/* Add dock to win in the area stored as the dock's "default_area" + * dynamic property. The property is set elsewhere with a + * Qt::DockWidgetArea value; this just unpacks and routes it. + * Wraps a static_cast(... toInt()) idiom that + * otherwise repeats verbatim at every dock-attach site. */ +static void qt_dock_add_to(QMainWindow *win, QDockWidget *dock) +{ + win->addDockWidget(static_cast( + dock->property("default_area").toInt()), dock); +} + +/* Configure the four standard pieces of a QDockWidget in one call: + * the QObject name (for QSettings save/restore), the default + * docking area (read back later by qt_dock_add_to), the localized + * menu text shown in the View menu, and the widget the dock + * displays. Replaces a four-line setObjectName / setProperty / + * setProperty / setWidget block that recurs at every dock site. */ +static void qt_dock_configure(QDockWidget *dock, + const char *object_name, Qt::DockWidgetArea default_area, + enum msg_hash_enums menu_text, QWidget *widget) +{ + dock->setObjectName(object_name); + dock->setProperty("default_area", default_area); + dock->setProperty("menu_text", msg_hash_to_str(menu_text)); + dock->setWidget(widget); +} + /* %1 is a placeholder for palette(highlight) or the equivalent chosen by the user */ static const QString qt_theme_default_stylesheet = QString(R"( QPushButton[flat="true"] { @@ -1041,13 +1081,272 @@ static double exp_scale(double input_val, double mid_val, double max_val) return ret; } -TreeView::TreeView(QWidget *parent) : QTreeView(parent) { } +/* Status codes carried in string_list_elem_attr.i for the + * core-info value list returned by qt_core_info_collect(). */ +enum qt_core_info_row_status +{ + QT_CORE_INFO_ROW_NORMAL = 0, + /* Firmware section: header rows with a key but empty value. */ + QT_CORE_INFO_ROW_FIRMWARE_NOTE, + /* Firmware status rows that should render in green. */ + QT_CORE_INFO_ROW_FIRMWARE_PRESENT, + /* Firmware status rows that should render in red. */ + QT_CORE_INFO_ROW_FIRMWARE_MISSING, + /* Notes: no key, free-form value. */ + QT_CORE_INFO_ROW_NOTE_NO_KEY +}; + +/* Append a single (key, value) row. status applies to value->attr.i. */ +static void qt_core_info_append_row( + struct string_list *keys, + struct string_list *values, + const char *key, const char *value, + enum qt_core_info_row_status status) +{ + union string_list_elem_attr attr; + attr.i = 0; + string_list_append(keys, key ? key : "", attr); + attr.i = (int)status; + string_list_append(values, value ? value : "", attr); +} + +/* Append a row whose key is ":" and whose value is plain. */ +static void qt_core_info_append_kv( + struct string_list *keys, + struct string_list *values, + enum msg_hash_enums label_enum, const char *value) +{ + char key[256]; + size_t _len = strlcpy(key, msg_hash_to_str(label_enum), sizeof(key)); + strlcpy(key + _len, ":", sizeof(key) - _len); + qt_core_info_append_row(keys, values, key, value, + QT_CORE_INFO_ROW_NORMAL); +} + +/* Append a row built from a list of strings joined by ", ". */ +static void qt_core_info_append_joined( + struct string_list *keys, + struct string_list *values, + enum msg_hash_enums label_enum, + const struct string_list *src) +{ + char buf[NAME_MAX_LENGTH * 4]; + size_t i, _len = 0; + buf[0] = '\0'; + for (i = 0; i < src->size; i++) + { + _len += strlcpy(buf + _len, src->elems[i].data, sizeof(buf) - _len); + if (i < src->size - 1) + _len += strlcpy(buf + _len, ", ", sizeof(buf) - _len); + } + qt_core_info_append_kv(keys, values, label_enum, buf); +} + +/* Collect a list of human-readable rows describing the currently-selected + * core. Output: two parallel string_lists. values->elems[i].attr.i holds + * a qt_core_info_row_status for that row. + * + * Returns false if no core is selected or its info isn't loaded; in that + * case a single row with the "no info available" message is appended. */ +static bool qt_core_info_collect( + const char *current_core_path, + struct string_list *keys, + struct string_list *values) +{ + size_t i; + core_info_t *core_info = NULL; + + core_info_find(current_core_path, &core_info); + + if ( !current_core_path + || !*current_core_path + || !core_info + || !(core_info->flags & CORE_INFO_FLAG_HAS_INFO)) + { + qt_core_info_append_row(keys, values, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_CORE_INFORMATION_AVAILABLE), + "", QT_CORE_INFO_ROW_NORMAL); + return false; + } + + if (core_info->core_name) + qt_core_info_append_kv(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_CORE_NAME, core_info->core_name); + if (core_info->display_name) + qt_core_info_append_kv(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_CORE_LABEL, core_info->display_name); + if (core_info->systemname) + qt_core_info_append_kv(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_NAME, core_info->systemname); + if (core_info->system_manufacturer) + qt_core_info_append_kv(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_MANUFACTURER, + core_info->system_manufacturer); + + if (core_info->categories_list) + qt_core_info_append_joined(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_CATEGORIES, + core_info->categories_list); + if (core_info->authors_list) + qt_core_info_append_joined(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_AUTHORS, + core_info->authors_list); + if (core_info->permissions_list) + qt_core_info_append_joined(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_PERMISSIONS, + core_info->permissions_list); + if (core_info->licenses_list) + qt_core_info_append_joined(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_LICENSES, + core_info->licenses_list); + if (core_info->supported_extensions_list) + qt_core_info_append_joined(keys, values, + MENU_ENUM_LABEL_VALUE_CORE_INFO_SUPPORTED_EXTENSIONS, + core_info->supported_extensions_list); + + if (core_info->firmware_count > 0) + { + char tmp_path[PATH_MAX_LENGTH]; + core_info_ctx_firmware_t firmware_info; + bool update_missing_firmware = false; + settings_t *settings = config_get_ptr(); + uint8_t flags = content_get_flags(); + bool systemfiles_in_content_dir = settings->bools.systemfiles_in_content_dir; + bool content_is_inited = flags & CONTENT_ST_FLAG_IS_INITED; + + firmware_info.path = core_info->path; + + if (systemfiles_in_content_dir && content_is_inited) + { + fill_pathname_basedir(tmp_path, + path_get(RARCH_PATH_CONTENT), + sizeof(tmp_path)); + if (!*tmp_path) + firmware_info.directory.system = settings->paths.directory_system; + else + { + size_t _len = strlen(tmp_path); + if ( string_count_occurrences_single_character(tmp_path, PATH_DEFAULT_SLASH_C()) > 1 + && tmp_path[_len - 1] == PATH_DEFAULT_SLASH_C()) + tmp_path[_len - 1] = '\0'; + firmware_info.directory.system = tmp_path; + } + } + else + firmware_info.directory.system = settings->paths.directory_system; + + update_missing_firmware = core_info_list_update_missing_firmware(&firmware_info); + + if (update_missing_firmware) + { + char firmware_label[256]; + char tmp[PATH_MAX_LENGTH]; + size_t _len = strlcpy(firmware_label, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE), + sizeof(firmware_label)); + strlcpy(firmware_label + _len, ":", + sizeof(firmware_label) - _len); + + qt_core_info_append_row(keys, values, + firmware_label, "", QT_CORE_INFO_ROW_FIRMWARE_NOTE); + + if (systemfiles_in_content_dir) + qt_core_info_append_row(keys, values, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE_IN_CONTENT_DIRECTORY), + "", QT_CORE_INFO_ROW_FIRMWARE_NOTE); + + snprintf(tmp, sizeof(tmp), + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE_PATH), + firmware_info.directory.system); + qt_core_info_append_row(keys, values, + tmp, "", QT_CORE_INFO_ROW_FIRMWARE_NOTE); + + for (i = 0; i < core_info->firmware_count; i++) + { + char lbl_txt[256]; + const char *val_txt = NULL; + bool missing = false; + enum qt_core_info_row_status status; + size_t lbl_len = 0; + + if (!core_info->firmware[i].desc) + continue; + + lbl_len = strlcpy(lbl_txt, "(!) ", sizeof(lbl_txt)); + + if (core_info->firmware[i].missing) + { + missing = true; + if (core_info->firmware[i].optional) + strlcpy(lbl_txt + lbl_len, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MISSING_OPTIONAL), + sizeof(lbl_txt) - lbl_len); + else + strlcpy(lbl_txt + lbl_len, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MISSING_REQUIRED), + sizeof(lbl_txt) - lbl_len); + } + else + { + if (core_info->firmware[i].optional) + strlcpy(lbl_txt + lbl_len, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PRESENT_OPTIONAL), + sizeof(lbl_txt) - lbl_len); + else + strlcpy(lbl_txt + lbl_len, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PRESENT_REQUIRED), + sizeof(lbl_txt) - lbl_len); + } + + val_txt = core_info->firmware[i].desc + ? core_info->firmware[i].desc + : msg_hash_to_str(MENU_ENUM_LABEL_VALUE_RDB_ENTRY_NAME); + status = missing + ? QT_CORE_INFO_ROW_FIRMWARE_MISSING + : QT_CORE_INFO_ROW_FIRMWARE_PRESENT; + qt_core_info_append_row(keys, values, + lbl_txt, val_txt, status); + } + } + } + + if (core_info->notes && core_info->note_list) + { + for (i = 0; i < core_info->note_list->size; i++) + qt_core_info_append_row(keys, values, + "", core_info->note_list->elems[i].data, + QT_CORE_INFO_ROW_NOTE_NO_KEY); + } + + return true; +} + +/* Thumbnail widgets are addressed by a flat 0..3 index. The index order + * (boxart, title, screenshot, logo) matches the four QObject names set + * up in ui_companion_qt_init() and is *not* the same as ThumbnailType + * enum order (which has SCREENSHOT before TITLE_SCREEN). */ +static const char * const qt_thumbnail_widget_names[4] = { + "thumbnail", "thumbnail2", "thumbnail3", "thumbnail4" +}; -void TreeView::columnCountChanged(int oldCount, int newCount) +static const char * const qt_thumbnail_subdirs[4] = { + THUMBNAIL_BOXART, THUMBNAIL_TITLE, THUMBNAIL_SCREENSHOT, THUMBNAIL_LOGO +}; + +static int qt_thumbnail_type_to_widget_idx(ThumbnailType t) { - QTreeView::columnCountChanged(oldCount, newCount); + switch (t) + { + case THUMBNAIL_TYPE_BOXART: return 0; + case THUMBNAIL_TYPE_TITLE_SCREEN: return 1; + case THUMBNAIL_TYPE_SCREENSHOT: return 2; + case THUMBNAIL_TYPE_LOGO: return 3; + } + return 0; } +TreeView::TreeView(QWidget *parent) : QTreeView(parent) { } + void TreeView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QModelIndexList list = selected.indexes(); @@ -1066,11 +1365,6 @@ bool TableView::isEditorOpen() ListWidget::ListWidget(QWidget *parent) : QListWidget(parent) { } -bool ListWidget::isEditorOpen() -{ - return (state() == QAbstractItemView::EditingState); -} - void ListWidget::keyPressEvent(QKeyEvent *event) { int key = event->key(); @@ -1121,12 +1415,26 @@ void LogTextEdit::appendMessage(const QString& text) } /* Only accept indexes from current path. - * https://www.qtcentre.org/threads/50700-QFileSystemModel-and-QSortFilterProxyModel-don-t-work-well-together */ + * https://www.qtcentre.org/threads/50700-QFileSystemModel-and-QSortFilterProxyModel-don-t-work-well-together + * + * The root index is cached because this method is invoked once per + * (row, parent) pair the proxy considers - which can be tens of + * thousands per directory load on a populous tree - while the + * QFileSystemModel root path changes only when the user navigates. + * Resolving the root path to an index via sm->index(...) walks the + * model on every call without the cache. */ bool FileSystemProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - QFileSystemModel *sm = qobject_cast(sourceModel()); - QModelIndex rootIndex = sm->index(sm->rootPath()); - if (sourceParent == rootIndex) + QFileSystemModel *sm = qobject_cast(sourceModel()); + const QString currentRoot = sm->rootPath(); + + if (currentRoot != m_cachedRootPath) + { + m_cachedRootPath = currentRoot; + m_cachedRootIndex = sm->index(currentRoot); + } + + if (sourceParent == m_cachedRootIndex) return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); return true; } @@ -1163,10 +1471,6 @@ MainWindow::MainWindow(QWidget *parent) : ,m_stopPushButton(new QToolButton(this)) ,m_browserAndPlaylistTabWidget(new QTabWidget(this)) ,m_pendingRun(false) - ,m_thumbnailPixmap(NULL) - ,m_thumbnailPixmap2(NULL) - ,m_thumbnailPixmap3(NULL) - ,m_thumbnailPixmap4(NULL) ,m_settings(NULL) ,m_viewOptionsDialog(NULL) ,m_coreInfoDialog(new CoreInfoDialog(this, NULL)) @@ -1190,7 +1494,6 @@ MainWindow::MainWindow(QWidget *parent) : ,m_thumbnailType(THUMBNAIL_TYPE_BOXART) ,m_gridProgressBar(NULL) ,m_gridProgressWidget(NULL) - ,m_currentGridHash() ,m_currentGridWidget(NULL) ,m_allPlaylistsListMaxCount(0) ,m_allPlaylistsGridMaxCount(0) @@ -1226,30 +1529,24 @@ MainWindow::MainWindow(QWidget *parent) : ,m_itemsCountLabel(new QLabel(this)) { settings_t *settings = config_get_ptr(); - const char *path_dir_playlist = settings->paths.directory_playlist; const char *path_dir_assets = settings->paths.directory_assets; - const char *path_dir_menu_content = settings->paths.directory_menu_content; - QDir playlistDir(path_dir_playlist); QString configDir = QFileInfo(path_get(RARCH_PATH_CONFIG)).dir().absolutePath(); - QToolButton *searchResetButton = NULL; - QHBoxLayout *zoomLayout = new QHBoxLayout(); - QLabel *zoomLabel = new QLabel(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_ZOOM), m_zoomWidget); - QPushButton *thumbnailTypePushButton = new QPushButton(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_MENU_VIEW_OPTIONS_THUMBNAIL_TYPE), m_zoomWidget); - QMenu *thumbnailTypeMenu = new QMenu(thumbnailTypePushButton); - QAction *thumbnailTypeBoxartAction = NULL; - QAction *thumbnailTypeScreenshotAction = NULL; - QAction *thumbnailTypeTitleAction = NULL; - QAction *thumbnailTypeLogoAction = NULL; - QPushButton *viewTypePushButton = new QPushButton(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_VIEW), m_zoomWidget); - QMenu *viewTypeMenu = new QMenu(viewTypePushButton); - QAction *viewTypeIconsAction = NULL; - QAction *viewTypeListAction = NULL; - QHBoxLayout *gridProgressLayout = new QHBoxLayout(); - QLabel *gridProgressLabel = NULL; - QHBoxLayout *gridFooterLayout = NULL; qRegisterMetaType >("ThumbnailWidget"); qRegisterMetaType("retro_task_callback_t"); + qRegisterMetaType("PlaylistEntry"); + + memset(m_thumbnailPixmaps, 0, sizeof(m_thumbnailPixmaps)); + + /* Background loader for the file-browser preview pane. Decoding + * happens off the UI thread; results arrive on the imageLoaded + * signal and are filtered by m_pendingPreviewPath so that rapid + * selection changes don't flicker stale images in. */ + m_previewLoader = new ThumbnailLoader(this); + connect(m_previewLoader, + SIGNAL(imageLoaded(QImage,QModelIndex,QString)), this, + SLOT(onPreviewImageLoaded(QImage,QModelIndex,QString))); + m_previewLoader->start(); /* Cancel all progress dialogs immediately since * they show as soon as they're constructed. */ @@ -1258,70 +1555,159 @@ MainWindow::MainWindow(QWidget *parent) : m_thumbnailPackDownloadProgressDialog->cancel(); m_playlistThumbnailDownloadProgressDialog->cancel(); - m_gridProgressWidget = new QWidget(); - gridProgressLabel = new QLabel( + /* m_playlistViewsAndFooter holds the playlist/grid views on top + * and the footer toolbar (zoom, view-type, thumbnail-type) on + * the bottom. Set up its layout, add the views, then call + * setupPlaylistFooter() to build and attach the toolbar. */ + m_playlistViewsAndFooter->setLayout(new QVBoxLayout()); + + m_gridView->setSelectionMode(QAbstractItemView::SingleSelection); + m_gridView->setEditTriggers(QAbstractItemView::NoEditTriggers); + + m_playlistViews->addWidget(m_gridView); + m_playlistViews->addWidget(m_tableView); + m_centralWidget->setObjectName("centralWidget"); + + m_playlistViewsAndFooter->layout()->addWidget(m_playlistViews); + m_playlistViewsAndFooter->layout()->setAlignment(Qt::AlignCenter); + m_playlistViewsAndFooter->layout()->setContentsMargins(0, 0, 0, 0); + + setupPlaylistFooter(); + + setupModels(); + + m_logWidget->setObjectName("logWidget"); + + m_folderIcon = QIcon(QString(path_dir_assets) + GENERIC_FOLDER_ICON); + m_defaultStyle = QApplication::style(); + m_defaultPalette = QApplication::palette(); + + /* ViewOptionsDialog needs m_settings set before it's constructed */ + m_settings = new QSettings(configDir + + QString("/retroarch_qt.cfg"), QSettings::IniFormat, this); + m_viewOptionsDialog = new ViewOptionsDialog(this, 0); + m_playlistEntryDialog = new PlaylistEntryDialog(this, 0); + + /* default NULL parameter for parent wasn't added until 5.7 */ + qt_button_set_action_label(m_startCorePushButton, MENU_ENUM_LABEL_VALUE_START_CORE); + qt_button_set_action_label(m_runPushButton, MENU_ENUM_LABEL_VALUE_RUN); + qt_button_set_action_label(m_stopPushButton, MENU_ENUM_LABEL_VALUE_QT_STOP); + qt_button_set_action_label(m_coreInfoPushButton, MENU_ENUM_LABEL_VALUE_QT_INFO); + + setupFileSystemBrowser(); + + reloadPlaylists(); + + setupDockWidgets(); + + m_dirTree->setContextMenuPolicy(Qt::CustomContextMenu); + m_listWidget->setContextMenuPolicy(Qt::CustomContextMenu); + + setupSignalConnections(); + + m_timer->start(TIMER_MSEC); + + statusBar()->addPermanentWidget(m_statusLabel); + + setCurrentCoreLabel(); + setCoreActions(); + + /* Both of these are necessary to get the folder to scroll + * to the top of the view */ + qApp->processEvents(); + QTimer::singleShot(0, this, SLOT(onBrowserStartClicked())); + + m_searchLineEdit->setFocus(); + m_loadCoreWindow->setWindowModality(Qt::ApplicationModal); + + m_statusMessageElapsedTimer.start(); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + resizeDocks(QList() << m_searchDock, + QList() << 1, Qt::Vertical); +#endif +} + +/* Build the footer toolbar - zoom slider, view-type and thumbnail- + * type push buttons, items count label, and the always-hidden grid + * progress widget - and attach it under the playlist views. Owns + * all its widget locals so they don't pollute the constructor. + * Mutates several MainWindow members (m_gridProgressWidget, + * m_gridProgressBar, m_zoomSlider, m_lastZoomSliderValue) and + * appends to m_playlistViewsAndFooter's layout, which the caller + * is expected to have created. */ +void MainWindow::setupPlaylistFooter() +{ + QHBoxLayout *zoomLayout = new QHBoxLayout(); + QLabel *zoomLabel = new QLabel( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_ZOOM), m_zoomWidget); + QPushButton *thumbnailTypePushButton = new QPushButton( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_MENU_VIEW_OPTIONS_THUMBNAIL_TYPE), + m_zoomWidget); + QMenu *thumbnailTypeMenu = new QMenu(thumbnailTypePushButton); + QPushButton *viewTypePushButton = new QPushButton( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_VIEW), m_zoomWidget); + QMenu *viewTypeMenu = new QMenu(viewTypePushButton); + QHBoxLayout *gridProgressLayout = new QHBoxLayout(); + QHBoxLayout *gridFooterLayout = NULL; + QLabel *gridProgressLabel = NULL; + + m_gridProgressWidget = new QWidget(); + gridProgressLabel = new QLabel( msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_PROGRESS), m_gridProgressWidget); thumbnailTypePushButton->setObjectName("thumbnailTypePushButton"); thumbnailTypePushButton->setFlat(true); - thumbnailTypeBoxartAction = thumbnailTypeMenu->addAction( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_BOXART)); - thumbnailTypeScreenshotAction = thumbnailTypeMenu->addAction( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_SCREENSHOT)); - thumbnailTypeTitleAction = thumbnailTypeMenu->addAction( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_TITLE_SCREEN)); - thumbnailTypeLogoAction = thumbnailTypeMenu->addAction( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_LOGO)); + connect(thumbnailTypeMenu->addAction( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_BOXART)), + SIGNAL(triggered()), this, SLOT(onBoxartThumbnailClicked())); + connect(thumbnailTypeMenu->addAction( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_SCREENSHOT)), + SIGNAL(triggered()), this, SLOT(onScreenshotThumbnailClicked())); + connect(thumbnailTypeMenu->addAction( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_TITLE_SCREEN)), + SIGNAL(triggered()), this, SLOT(onTitleThumbnailClicked())); + connect(thumbnailTypeMenu->addAction( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_LOGO)), + SIGNAL(triggered()), this, SLOT(onLogoThumbnailClicked())); thumbnailTypePushButton->setMenu(thumbnailTypeMenu); viewTypePushButton->setObjectName("viewTypePushButton"); viewTypePushButton->setFlat(true); - viewTypeIconsAction = viewTypeMenu->addAction( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_VIEW_TYPE_ICONS)); - viewTypeListAction = viewTypeMenu->addAction( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_VIEW_TYPE_LIST)); + connect(viewTypeMenu->addAction( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_VIEW_TYPE_ICONS)), + SIGNAL(triggered()), this, SLOT(onIconViewClicked())); + connect(viewTypeMenu->addAction( + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_VIEW_TYPE_LIST)), + SIGNAL(triggered()), this, SLOT(onListViewClicked())); viewTypePushButton->setMenu(viewTypeMenu); gridProgressLabel->setObjectName("gridProgressLabel"); - m_gridProgressBar = new QProgressBar( - m_gridProgressWidget); - - m_gridProgressBar->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred)); + m_gridProgressBar = new QProgressBar(m_gridProgressWidget); + m_gridProgressBar->setSizePolicy( + QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred)); zoomLabel->setObjectName("zoomLabel"); - m_zoomSlider = new QSlider( - Qt::Horizontal, m_zoomWidget); - + m_zoomSlider = new QSlider(Qt::Horizontal, m_zoomWidget); m_zoomSlider->setMinimum(0); m_zoomSlider->setMaximum(100); m_zoomSlider->setValue(50); - m_zoomSlider->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred)); + m_zoomSlider->setSizePolicy( + QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred)); m_lastZoomSliderValue = m_zoomSlider->value(); - m_playlistViewsAndFooter->setLayout(new QVBoxLayout()); - - m_gridView->setSelectionMode(QAbstractItemView::SingleSelection); - m_gridView->setEditTriggers(QAbstractItemView::NoEditTriggers); - - m_playlistViews->addWidget(m_gridView); - m_playlistViews->addWidget(m_tableView); - m_centralWidget->setObjectName("centralWidget"); - - m_playlistViewsAndFooter->layout()->addWidget(m_playlistViews); - m_playlistViewsAndFooter->layout()->setAlignment(Qt::AlignCenter); - m_playlistViewsAndFooter->layout()->setContentsMargins(0, 0, 0, 0); - m_gridProgressWidget->setLayout(gridProgressLayout); gridProgressLayout->setContentsMargins(0, 0, 0, 0); - gridProgressLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Preferred)); + gridProgressLayout->addSpacerItem(new QSpacerItem( + 0, 0, QSizePolicy::Expanding, QSizePolicy::Preferred)); gridProgressLayout->addWidget(gridProgressLabel); gridProgressLayout->addWidget(m_gridProgressBar); @@ -1336,18 +1722,27 @@ MainWindow::MainWindow(QWidget *parent) : gridFooterLayout = new QHBoxLayout(); gridFooterLayout->addWidget(m_itemsCountLabel); - gridFooterLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Preferred)); + gridFooterLayout->addSpacerItem(new QSpacerItem( + 0, 0, QSizePolicy::Expanding, QSizePolicy::Preferred)); gridFooterLayout->addWidget(m_gridProgressWidget); gridFooterLayout->addWidget(m_zoomWidget); gridFooterLayout->addWidget(thumbnailTypePushButton); gridFooterLayout->addWidget(viewTypePushButton); - static_cast(m_playlistViewsAndFooter->layout())->addLayout(gridFooterLayout); + static_cast(m_playlistViewsAndFooter->layout()) + ->addLayout(gridFooterLayout); m_gridProgressWidget->hide(); +} - m_playlistModel = new PlaylistModel(this); - m_proxyModel = new QSortFilterProxyModel(this); +/* Configure the playlist + filesystem proxy models and the table / + * file-table / grid views that consume them. The init list creates + * the views; this fills in their behaviour (sort, selection, etc.) + * and hooks them up to their models. */ +void MainWindow::setupModels() +{ + m_playlistModel = new PlaylistModel(this); + m_proxyModel = new QSortFilterProxyModel(this); m_proxyModel->setSourceModel(m_playlistModel); m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); @@ -1361,7 +1756,8 @@ MainWindow::MainWindow(QWidget *parent) : m_tableView->verticalHeader()->setVisible(false); m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); m_tableView->setSelectionMode(QAbstractItemView::SingleSelection); - m_tableView->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); + m_tableView->setEditTriggers( + QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); m_tableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_tableView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); m_tableView->horizontalHeader()->setStretchLastSection(true); @@ -1380,57 +1776,24 @@ MainWindow::MainWindow(QWidget *parent) : m_gridView->setItemDelegate(new ThumbnailDelegate(m_gridItem, this)); m_gridView->setModel(m_proxyModel); - m_gridView->setSelectionModel(m_tableView->selectionModel()); +} - m_logWidget->setObjectName("logWidget"); - - m_folderIcon = QIcon(QString(path_dir_assets) + GENERIC_FOLDER_ICON); - m_defaultStyle = QApplication::style(); - m_defaultPalette = QApplication::palette(); - - /* ViewOptionsDialog needs m_settings set before it's constructed */ - m_settings = new QSettings(configDir - + QString("/retroarch_qt.cfg"), QSettings::IniFormat, this); - m_viewOptionsDialog = new ViewOptionsDialog(this, 0); - m_playlistEntryDialog = new PlaylistEntryDialog(this, 0); +/* Configure the QFileSystemModels and the directory tree view used + * by the file browser. Sets entry filters (respecting the user's + * "show hidden files" preference), points both models at the root + * of the filesystem, and hides the size/type/date columns on the + * tree so only names are shown. */ +void MainWindow::setupFileSystemBrowser() +{ + const bool show_hidden = m_settings->value("show_hidden_files", true).toBool(); + const QDir::Filters hidden_filters = show_hidden + ? (QDir::Hidden | QDir::System) + : static_cast(0); - /* default NULL parameter for parent wasn't added until 5.7 */ - m_startCorePushButton->setDefaultAction(new QAction(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_START_CORE), m_startCorePushButton)); - m_startCorePushButton->setFixedSize(m_startCorePushButton->sizeHint()); - - m_runPushButton->setDefaultAction(new QAction(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_RUN), m_runPushButton)); - m_runPushButton->setFixedSize(m_runPushButton->sizeHint()); - - m_stopPushButton->setDefaultAction(new QAction(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_STOP), m_stopPushButton)); - m_stopPushButton->setFixedSize(m_stopPushButton->sizeHint()); - - m_coreInfoPushButton->setDefaultAction(new QAction(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_INFO), m_coreInfoPushButton)); - m_coreInfoPushButton->setFixedSize(m_coreInfoPushButton->sizeHint()); - - searchResetButton = new QToolButton(m_searchWidget); - searchResetButton->setDefaultAction(new QAction(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_MENU_SEARCH_CLEAR), searchResetButton)); - searchResetButton->setFixedSize(searchResetButton->sizeHint()); - - connect(searchResetButton, SIGNAL(clicked()), this, SLOT(onSearchResetClicked())); - - m_dirModel->setFilter( QDir::NoDotAndDotDot - | QDir::AllDirs - | QDir::Drives - | (m_settings->value("show_hidden_files", true).toBool() - ? (QDir::Hidden | QDir::System) - : static_cast(0))); - - m_fileModel->setFilter( QDir::NoDot - | QDir::AllEntries - | (m_settings->value("show_hidden_files", true).toBool() - ? (QDir::Hidden | QDir::System) - : static_cast(0))); + m_dirModel->setFilter(QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Drives + | hidden_filters); + m_fileModel->setFilter(QDir::NoDot | QDir::AllEntries | hidden_filters); #if defined(Q_OS_WIN) m_dirModel->setRootPath(""); @@ -1452,60 +1815,66 @@ MainWindow::MainWindow(QWidget *parent) : m_dirTree->hideColumn(2); /* type */ m_dirTree->hideColumn(3); /* date modified */ } +} - reloadPlaylists(); +/* Build the search / core-info / log dock widgets and their + * contents, register them with the main window, and hide the log + * dock so it stays out of the way until the user opens it. Pulled + * out of the constructor for readability. */ +void MainWindow::setupDockWidgets() +{ + QToolButton *searchResetButton = new QToolButton(m_searchWidget); + qt_button_set_action_label(searchResetButton, + MENU_ENUM_LABEL_VALUE_QT_MENU_SEARCH_CLEAR); + connect(searchResetButton, SIGNAL(clicked()), this, + SLOT(onSearchResetClicked())); m_searchWidget->setLayout(new QHBoxLayout()); m_searchWidget->layout()->addWidget(m_searchLineEdit); m_searchWidget->layout()->addWidget(searchResetButton); - m_searchDock->setObjectName("searchDock"); - m_searchDock->setProperty("default_area", Qt::LeftDockWidgetArea); - m_searchDock->setProperty("menu_text", - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SEARCH)); - m_searchDock->setWidget(m_searchWidget); + qt_dock_configure(m_searchDock, "searchDock", Qt::LeftDockWidgetArea, + MENU_ENUM_LABEL_VALUE_SEARCH, m_searchWidget); m_searchDock->setFixedHeight(m_searchDock->minimumSizeHint().height()); - addDockWidget(static_cast( - m_searchDock->property("default_area").toInt()), m_searchDock); + qt_dock_add_to(this, m_searchDock); m_coreInfoLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_coreInfoLabel->setTextFormat(Qt::RichText); m_coreInfoLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); m_coreInfoLabel->setOpenExternalLinks(true); - m_coreInfoDock->setObjectName("coreInfoDock"); - m_coreInfoDock->setProperty("default_area", Qt::RightDockWidgetArea); - m_coreInfoDock->setProperty("menu_text", - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CORE_INFO)); - m_coreInfoDock->setWidget(m_coreInfoWidget); + qt_dock_configure(m_coreInfoDock, "coreInfoDock", Qt::RightDockWidgetArea, + MENU_ENUM_LABEL_VALUE_QT_CORE_INFO, m_coreInfoWidget); - addDockWidget(static_cast( - m_coreInfoDock->property("default_area").toInt()), m_coreInfoDock); + qt_dock_add_to(this, m_coreInfoDock); m_logWidget->setLayout(new QVBoxLayout()); m_logWidget->layout()->addWidget(m_logTextEdit); m_logWidget->layout()->setContentsMargins(0, 0, 0, 0); - m_logDock->setObjectName("logDock"); - m_logDock->setProperty("default_area", Qt::BottomDockWidgetArea); - m_logDock->setProperty("menu_text", msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_LOG)); - m_logDock->setWidget(m_logWidget); + qt_dock_configure(m_logDock, "logDock", Qt::BottomDockWidgetArea, + MENU_ENUM_LABEL_VALUE_QT_LOG, m_logWidget); - addDockWidget(static_cast( - m_logDock->property("default_area").toInt()), m_logDock); + qt_dock_add_to(this, m_logDock); /* Hide the log by default. If user has saved their dock positions * with the log visible, then this hide() call will be reversed * later by restoreState(). * * FIXME: If user unchecks "save dock positions", the log will - * not be unhidden even if it was previously saved in the config. - */ + * not be unhidden even if it was previously saved in the config. */ m_logDock->hide(); +} - m_dirTree->setContextMenuPolicy(Qt::CustomContextMenu); - m_listWidget->setContextMenuPolicy(Qt::CustomContextMenu); +/* Wire up signal/slot connections for the main window's child + * widgets and self-emitted signals. Pulled out of the constructor + * to keep that body readable. All connections operate on already- + * constructed members of MainWindow; no parameters needed. */ +void MainWindow::setupSignalConnections() +{ + settings_t *settings = config_get_ptr(); + const char *path_dir_menu_content = settings->paths.directory_menu_content; connect(m_searchLineEdit, SIGNAL(returnPressed()), this, SLOT(onSearchEnterPressed())); @@ -1535,18 +1904,6 @@ MainWindow::MainWindow(QWidget *parent) : SLOT(onLaunchWithComboBoxIndexChanged(int))); connect(m_zoomSlider, SIGNAL(valueChanged(int)), this, SLOT(onZoomValueChanged(int))); - connect(thumbnailTypeBoxartAction, SIGNAL(triggered()), this, - SLOT(onBoxartThumbnailClicked())); - connect(thumbnailTypeScreenshotAction, SIGNAL(triggered()), this, - SLOT(onScreenshotThumbnailClicked())); - connect(thumbnailTypeTitleAction, SIGNAL(triggered()), this, - SLOT(onTitleThumbnailClicked())); - connect(thumbnailTypeLogoAction, SIGNAL(triggered()), this, - SLOT(onLogoThumbnailClicked())); - connect(viewTypeIconsAction, SIGNAL(triggered()), this, - SLOT(onIconViewClicked())); - connect(viewTypeListAction, SIGNAL(triggered()), this, - SLOT(onListViewClicked())); connect(m_dirModel, SIGNAL(directoryLoaded(const QString&)), this, SLOT(onFileSystemDirLoaded(const QString&))); connect(m_fileModel, SIGNAL(directoryLoaded(const QString&)), this, @@ -1565,7 +1922,7 @@ MainWindow::MainWindow(QWidget *parent) : connect(m_playlistThumbnailDownloadProgressDialog, SIGNAL(canceled()), m_playlistThumbnailDownloadProgressDialog, SLOT(cancel())); connect(m_playlistThumbnailDownloadProgressDialog, SIGNAL(canceled()), - this,SLOT(onPlaylistThumbnailDownloadCanceled())); + this, SLOT(onPlaylistThumbnailDownloadCanceled())); connect(m_thumbnailDownloadProgressDialog, SIGNAL(canceled()), m_thumbnailDownloadProgressDialog, SLOT(cancel())); @@ -1578,12 +1935,9 @@ MainWindow::MainWindow(QWidget *parent) : this, SLOT(onThumbnailPackDownloadCanceled())); connect(this, SIGNAL(itemChanged()), this, SLOT(onItemChanged())); - connect(this, SIGNAL(gotThumbnailDownload(QString,QString)), - this, SLOT(onDownloadThumbnail(QString,QString))); m_thumbnailTimer->setSingleShot(true); connect(m_thumbnailTimer, SIGNAL(timeout()), this, SLOT(updateVisibleItems())); - connect(this, SIGNAL(updateThumbnails()), this, SLOT(updateVisibleItems())); /* TODO: Handle scroll and resize differently. */ connect(m_gridView, SIGNAL(visibleItemsChangedMaybe()), @@ -1624,52 +1978,20 @@ MainWindow::MainWindow(QWidget *parent) : SLOT(onGotReloadShaderParams()), Qt::AutoConnection); #endif #endif - connect(this, SIGNAL(gotReloadCoreOptions()), this, - SLOT(onGotReloadCoreOptions()), Qt::AutoConnection); /* These are always queued */ connect(this, SIGNAL(showErrorMessageDeferred(QString)), this, SLOT(onShowErrorMessage(QString)), Qt::QueuedConnection); connect(this, SIGNAL(showInfoMessageDeferred(QString)), this, SLOT(onShowInfoMessage(QString)), Qt::QueuedConnection); - connect(this, SIGNAL(extractArchiveDeferred(QString,QString, - QString,retro_task_callback_t)), this, - SLOT(onExtractArchive(QString,QString,QString,retro_task_callback_t)), - Qt::QueuedConnection); - - m_timer->start(TIMER_MSEC); - - statusBar()->addPermanentWidget(m_statusLabel); - - setCurrentCoreLabel(); - setCoreActions(); - - /* Both of these are necessary to get the folder to scroll - * to the top of the view */ - qApp->processEvents(); - QTimer::singleShot(0, this, SLOT(onBrowserStartClicked())); - - m_searchLineEdit->setFocus(); - m_loadCoreWindow->setWindowModality(Qt::ApplicationModal); - - m_statusMessageElapsedTimer.start(); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - resizeDocks(QList() << m_searchDock, - QList() << 1, Qt::Vertical); -#endif } MainWindow::~MainWindow() { - if (m_thumbnailPixmap) - delete m_thumbnailPixmap; - if (m_thumbnailPixmap2) - delete m_thumbnailPixmap2; - if (m_thumbnailPixmap3) - delete m_thumbnailPixmap3; - if (m_thumbnailPixmap4) - delete m_thumbnailPixmap4; + size_t i; + for (i = 0; i < 4; i++) + if (m_thumbnailPixmaps[i]) + delete m_thumbnailPixmaps[i]; if (m_proxyFileModel) delete m_proxyFileModel; } @@ -1859,6 +2181,11 @@ const QString& MainWindow::customThemeString() const bool MainWindow::setCustomThemeFile(QString filePath) { + QByteArray pathArray; + const char *path_data; + void *buf = NULL; + int64_t len = 0; + if (filePath.isEmpty()) { QMessageBox::critical(this, @@ -1867,45 +2194,37 @@ bool MainWindow::setCustomThemeFile(QString filePath) return false; } - QFile file(filePath); + pathArray = filePath.toUtf8(); + path_data = pathArray.constData(); - if (file.exists()) + if (!filestream_exists(path_data)) { - bool opened = file.open(QIODevice::ReadOnly); - - if (!opened) - { - QMessageBox::critical(this, - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CUSTOM_THEME), - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FILE_READ_OPEN_FAILED)); - return false; - } - - { - QByteArray fileArray = file.readAll(); - QString fileStr = QString::fromUtf8(fileArray); + QMessageBox::critical(this, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CUSTOM_THEME), + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FILE_DOES_NOT_EXIST)); + return false; + } - file.close(); - - if (fileStr.isEmpty()) - { - QMessageBox::critical(this, - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CUSTOM_THEME), - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FILE_IS_EMPTY)); - return false; - } - - setCustomThemeString(fileStr); - } + if (!filestream_read_file(path_data, &buf, &len)) + { + QMessageBox::critical(this, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CUSTOM_THEME), + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FILE_READ_OPEN_FAILED)); + return false; } - else + + if (len <= 0) { + free(buf); QMessageBox::critical(this, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CUSTOM_THEME), - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FILE_DOES_NOT_EXIST)); + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FILE_IS_EMPTY)); return false; } + setCustomThemeString(QString::fromUtf8(static_cast(buf), + static_cast(len))); + free(buf); return true; } @@ -1984,28 +2303,24 @@ bool MainWindow::showMessageBox(QString msg, MessageBoxType msgType, void MainWindow::onFileBrowserTreeContextMenuRequested(const QPoint&) { #ifdef HAVE_LIBRETRODB - QDir dir; - QByteArray dirArray; QPointer action; QList actions; QScopedPointer scanAction; QString currentDirString = QDir::toNativeSeparators( m_dirModel->filePath(m_dirTree->currentIndex())); settings_t *settings = config_get_ptr(); - const char *fullpath = NULL; const char *path_dir_playlist = settings->paths.directory_playlist; const char *path_content_db = settings->paths.path_content_database; + QByteArray dirArray; + const char *fullpath = NULL; if (currentDirString.isEmpty()) return; -#if (QT_VERSION > QT_VERSION_CHECK(6, 0, 0)) - dir.setPath(currentDirString); -#else - dir = currentDirString; -#endif + dirArray = currentDirString.toUtf8(); + fullpath = dirArray.constData(); - if (!dir.exists()) + if (!path_is_directory(fullpath)) return; /* Default NULL parameter for parent wasn't added until 5.7 */ @@ -2017,9 +2332,6 @@ void MainWindow::onFileBrowserTreeContextMenuRequested(const QPoint&) if (!(action = QMenu::exec(actions, QCursor::pos(), NULL, m_dirTree))) return; - dirArray = currentDirString.toUtf8(); - fullpath = dirArray.constData(); - task_push_dbscan( path_dir_playlist, path_content_db, @@ -2178,20 +2490,19 @@ MainWindow::Theme MainWindow::getThemeFromString(QString themeString) return THEME_SYSTEM_DEFAULT; } -QString MainWindow::getThemeString(Theme theme) +const char *MainWindow::getThemeString(Theme theme) { switch (theme) { - case THEME_SYSTEM_DEFAULT: - return QString("default"); case THEME_DARK: - return QString("dark"); + return "dark"; case THEME_CUSTOM: - return QString("custom"); + return "custom"; + case THEME_SYSTEM_DEFAULT: default: break; } - return QString("default"); + return "default"; } MainWindow::Theme MainWindow::theme() { return m_currentTheme; } @@ -2242,24 +2553,23 @@ void MainWindow::changeThumbnailType(ThumbnailType type) QString MainWindow::changeThumbnail(const QImage &image, QString type) { - QHash hash = getCurrentContentHash(); + PlaylistEntry entry = getCurrentContentEntry(); QString dirString = m_playlistModel->getPlaylistThumbnailsDir( - hash["db_name"]) + entry.dbName) + QString("/") + type; QString thumbPath = m_playlistModel->getSanitizedThumbnailName( dirString + QString("/"), - hash["label_noext"]); + entry.labelNoExt); QByteArray dirArray = QDir::toNativeSeparators(dirString).toUtf8(); const char *dirData = dirArray.constData(); QByteArray thumbArray = QDir::toNativeSeparators(thumbPath).toUtf8(); const char *thumbData = thumbArray.constData(); int quality = -1; - QDir dir(dirString); QImage scaledImage(image); - if (!dir.exists()) + if (!path_is_directory(dirData)) { - if (!dir.mkpath(".")) + if (!path_mkdir(dirData)) { RARCH_ERR("[Qt] Could not create directory: \"%s\".\n", dirData); return QString(); @@ -2295,404 +2605,71 @@ QString MainWindow::changeThumbnail(const QImage &image, QString type) void MainWindow::onThumbnailDropped(const QImage &image, ThumbnailType thumbnailType) { - switch (thumbnailType) - { - case THUMBNAIL_TYPE_BOXART: - { - QString path = changeThumbnail(image, THUMBNAIL_BOXART); - - if (path.isNull()) - return; - - if (m_thumbnailPixmap) - delete m_thumbnailPixmap; - - m_thumbnailPixmap = new QPixmap(pixmapFromPathRA(path)); - - onResizeThumbnailOne(*m_thumbnailPixmap, true); - break; - } - - case THUMBNAIL_TYPE_TITLE_SCREEN: - { - QString path = changeThumbnail(image, THUMBNAIL_TITLE); + int idx = qt_thumbnail_type_to_widget_idx(thumbnailType); + QString path = changeThumbnail(image, qt_thumbnail_subdirs[idx]); + QPixmap *new_pix = NULL; - if (path.isNull()) - return; - - if (m_thumbnailPixmap2) - delete m_thumbnailPixmap2; - - m_thumbnailPixmap2 = new QPixmap(pixmapFromPathRA(path)); - - onResizeThumbnailTwo(*m_thumbnailPixmap2, true); - break; - } - - case THUMBNAIL_TYPE_SCREENSHOT: - { - QString path = changeThumbnail(image, THUMBNAIL_SCREENSHOT); - - if (path.isNull()) - return; - - if (m_thumbnailPixmap3) - delete m_thumbnailPixmap3; - - m_thumbnailPixmap3 = new QPixmap(pixmapFromPathRA(path)); - - onResizeThumbnailThree(*m_thumbnailPixmap3, true); - break; - } - - case THUMBNAIL_TYPE_LOGO: - { - QString path = changeThumbnail(image, THUMBNAIL_LOGO); - - if (path.isNull()) - return; + if (path.isNull()) + return; - if (m_thumbnailPixmap4) - delete m_thumbnailPixmap4; + if (m_thumbnailPixmaps[idx]) + delete m_thumbnailPixmaps[idx]; - m_thumbnailPixmap4 = new QPixmap(pixmapFromPathRA(path)); + new_pix = new QPixmap(pixmapFromPathRA(path)); + m_thumbnailPixmaps[idx] = new_pix; - onResizeThumbnailFour(*m_thumbnailPixmap4, true); - break; - } - } + setThumbnail(qt_thumbnail_widget_names[idx], *new_pix, true); } QVector > MainWindow::getCoreInfo() { size_t i; QVector > infoList; - runloop_state_t *runloop_st = runloop_state_get_ptr(); - QHash currentCore = getSelectedCore(); - core_info_t *core_info = NULL; - QByteArray currentCorePathArray = currentCore["core_path"].toUtf8(); + QByteArray currentCorePathArray = getSelectedCorePath().toUtf8(); const char *current_core_path_data = currentCorePathArray.constData(); + struct string_list *keys = string_list_new(); + struct string_list *values = string_list_new(); - /* Search for current core */ - core_info_find(current_core_path_data, &core_info); - - if ( currentCore["core_path"].isEmpty() - || !core_info - || !(core_info->flags & CORE_INFO_FLAG_HAS_INFO)) + if (!keys || !values) { - QHash hash; - - hash["key"] = msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_NO_CORE_INFORMATION_AVAILABLE); - hash["value"] = QLatin1String(""); - - infoList.append(hash); - + string_list_free(keys); + string_list_free(values); return infoList; } - if (core_info->core_name) - { - QHash hash; + qt_core_info_collect(current_core_path_data, keys, values); - hash["key"] = QString( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_CORE_NAME)) - + QString(":"); - hash["value"] = core_info->core_name; - - infoList.append(hash); - } - - if (core_info->display_name) + for (i = 0; i < keys->size; i++) { QHash hash; + enum qt_core_info_row_status status = + (enum qt_core_info_row_status)values->elems[i].attr.i; - hash["key"] = QString( - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_CORE_LABEL)) - + QString(":"); - hash["value"] = core_info->display_name; + hash["key"] = keys->elems[i].data; + hash["value"] = values->elems[i].data; - infoList.append(hash); - } - - if (core_info->systemname) - { - QHash hash; - - hash["key"] = QString(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_NAME)) - + QString(":"); - hash["value"] = core_info->systemname; - - infoList.append(hash); - } - - if (core_info->system_manufacturer) - { - QHash hash; - - hash["key"] = QString(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_MANUFACTURER)) - + QString(":"); - hash["value"] = core_info->system_manufacturer; - - infoList.append(hash); - } - - if (core_info->categories_list) - { - QString categories; - QHash hash; - - for (i = 0; i < core_info->categories_list->size; i++) - { - categories += core_info->categories_list->elems[i].data; - if (i < core_info->categories_list->size - 1) - categories += QString(", "); - } - - hash["key"] = QString( - msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_CATEGORIES)) - + QString(":"); - hash["value"] = categories; - - infoList.append(hash); - } - - if (core_info->authors_list) - { - QString authors; - QHash hash; - - for (i = 0; i < core_info->authors_list->size; i++) + if ( status == QT_CORE_INFO_ROW_FIRMWARE_PRESENT + || status == QT_CORE_INFO_ROW_FIRMWARE_MISSING) { - authors += core_info->authors_list->elems[i].data; - if (i < core_info->authors_list->size - 1) - authors += QString(", "); + const char *css_color = (status == QT_CORE_INFO_ROW_FIRMWARE_MISSING) + ? "#ff0000" : "#00af00"; + const char *style_rgb = (status == QT_CORE_INFO_ROW_FIRMWARE_MISSING) + ? "color: #ff0000" : "color: rgb(0, 175, 0)"; + QString style = QString("font-weight: bold; ") + style_rgb; + + hash["label_style"] = style; + hash["value_style"] = style; + hash["html_key"] = QString("" + hash["key"] + ""; + hash["html_value"] = QString("" + hash["value"] + ""; } - hash["key"] = QString( - msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_AUTHORS)) - + QString(":"); - hash["value"] = authors; - infoList.append(hash); } - if (core_info->permissions_list) - { - QString permissions; - QHash hash; - - for (i = 0; i < core_info->permissions_list->size; i++) - { - permissions += core_info->permissions_list->elems[i].data; - if (i < core_info->permissions_list->size - 1) - permissions += QString(", "); - } - - hash["key"] = QString( - msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_PERMISSIONS)) - + QString(":"); - hash["value"] = permissions; - - infoList.append(hash); - } - - if (core_info->licenses_list) - { - QString licenses; - QHash hash; - - for (i = 0; i < core_info->licenses_list->size; i++) - { - licenses += core_info->licenses_list->elems[i].data; - if (i < core_info->licenses_list->size - 1) - licenses += QString(", "); - } - - hash["key"] = QString( - msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_LICENSES)) - + QString(":"); - hash["value"] = licenses; - - infoList.append(hash); - } - - if (core_info->supported_extensions_list) - { - QString supported_extensions; - QHash hash; - - for (i = 0; i < core_info->supported_extensions_list->size; i++) - { - supported_extensions += core_info->supported_extensions_list->elems[i].data; - if (i < core_info->supported_extensions_list->size - 1) - supported_extensions += QString(", "); - } - - hash["key"] = QString( - msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_SUPPORTED_EXTENSIONS)) - + QString(":"); - hash["value"] = supported_extensions; - - infoList.append(hash); - } - - if (core_info->firmware_count > 0) - { - char tmp_path[PATH_MAX_LENGTH]; - core_info_ctx_firmware_t firmware_info; - bool update_missing_firmware = false; - settings_t *settings = config_get_ptr(); - uint8_t flags = content_get_flags(); - bool systemfiles_in_content_dir = settings->bools.systemfiles_in_content_dir; - bool content_is_inited = flags & CONTENT_ST_FLAG_IS_INITED; - - firmware_info.path = core_info->path; - - /* If 'System Files are in Content Directory' is enabled and content is inited, - * adjust the path to check for firmware files */ - if (systemfiles_in_content_dir && content_is_inited) - { - fill_pathname_basedir(tmp_path, - path_get(RARCH_PATH_CONTENT), - sizeof(tmp_path)); - - /* If content path is empty, fall back to global system dir path */ - if (!*tmp_path) - firmware_info.directory.system = settings->paths.directory_system; - else - { - size_t _len = strlen(tmp_path); - - /* Removes trailing slash (unless root dir), doesn't really matter - * but it's more consistent with how the path is stored and - * displayed without 'System Files are in Content Directory' */ - if ( string_count_occurrences_single_character(tmp_path, PATH_DEFAULT_SLASH_C()) > 1 - && tmp_path[_len - 1] == PATH_DEFAULT_SLASH_C()) - tmp_path[_len - 1] = '\0'; - - firmware_info.directory.system = tmp_path; - } - } - else - firmware_info.directory.system = settings->paths.directory_system; - - update_missing_firmware = core_info_list_update_missing_firmware(&firmware_info); - - if (update_missing_firmware) - { - char tmp[PATH_MAX_LENGTH]; - QHash hash; - - hash["key"] = QString(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE)) - + QString(":"); - hash["value"] = QLatin1String(""); - - infoList.append(hash); - - /* If 'System Files are in Content Directory' is enabled, - * let's add a note about it. */ - if (systemfiles_in_content_dir) - { - hash["key"] = QString(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE_IN_CONTENT_DIRECTORY)); - hash["value"] = QLatin1String(""); - - infoList.append(hash); - } - - /* Show the path that was checked */ - snprintf(tmp, sizeof(tmp), - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE_PATH), - firmware_info.directory.system); - - hash["key"] = QString(tmp); - hash["value"] = QLatin1String(""); - - infoList.append(hash); - - /* FIXME: This looks hacky and probably - * needs to be improved for good translation support. */ - - for (i = 0; i < core_info->firmware_count; i++) - { - if (core_info->firmware[i].desc) - { - QString val_txt; - QHash hash; - QString lbl_txt = QString("(!) "); - bool missing = false; - - if (core_info->firmware[i].missing) - { - missing = true; - if (core_info->firmware[i].optional) - lbl_txt += msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_MISSING_OPTIONAL); - else - lbl_txt += msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_MISSING_REQUIRED); - } - else - if (core_info->firmware[i].optional) - lbl_txt += msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_PRESENT_OPTIONAL); - else - lbl_txt += msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_PRESENT_REQUIRED); - - if (core_info->firmware[i].desc) - val_txt = core_info->firmware[i].desc; - else - val_txt = msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_RDB_ENTRY_NAME); - - hash["key"] = lbl_txt; - hash["value"] = val_txt; - - if (missing) - { - QString style = QString("font-weight: bold; color: #ff0000"); - hash["label_style"] = style; - hash["value_style"] = style; - hash["html_key"] = "" + hash["key"] + QString(""); - hash["html_value"] = "" + hash["value"] + QString(""); - } - else - { - QString style = QString("font-weight: bold; color: rgb(0, 175, 0)"); - hash["label_style"] = style; - hash["value_style"] = style; - hash["html_key"] = "" + hash["key"] + QString(""); - hash["html_value"] = "" + hash["value"] + QString(""); - } - - infoList.append(hash); - } - } - } - } - - if (core_info->notes) - { - for (i = 0; i < core_info->note_list->size; i++) - { - QHash hash; - - hash["key"] = QLatin1String(""); - hash["value"] = core_info->note_list->elems[i].data; - - infoList.append(hash); - } - } + string_list_free(keys); + string_list_free(values); return infoList; } @@ -2723,7 +2700,7 @@ void MainWindow::onFileDoubleClicked(const QModelIndex &proxyIndex) if (m_fileModel->isDir(index)) m_dirTree->setCurrentIndex(m_dirModel->index(m_fileModel->filePath(index))); else - loadContent(getFileContentHash(index)); + loadContent(getFileContentEntry(index)); } void MainWindow::selectBrowserDir(QString path) @@ -2785,22 +2762,22 @@ QModelIndex MainWindow::getCurrentContentIndex() return QModelIndex(); } -QHash MainWindow::getCurrentContentHash() +PlaylistEntry MainWindow::getCurrentContentEntry() { - return getCurrentContentIndex().data(PlaylistModel::HASH).value >(); + return getCurrentContentIndex().data(PlaylistModel::ENTRY).value(); } -QHash MainWindow::getFileContentHash(const QModelIndex &index) +PlaylistEntry MainWindow::getFileContentEntry(const QModelIndex &index) { - QHash hash; + PlaylistEntry entry; QFileInfo fileInfo = m_fileModel->fileInfo(index); - hash["path"] = QDir::toNativeSeparators(m_fileModel->filePath(index)); - hash["label"] = hash["path"]; - hash["label_noext"] = fileInfo.completeBaseName(); - hash["db_name"] = fileInfo.dir().dirName(); + entry.path = QDir::toNativeSeparators(m_fileModel->filePath(index)); + entry.label = entry.path; + entry.labelNoExt = fileInfo.completeBaseName(); + entry.dbName = fileInfo.dir().dirName(); - return hash; + return entry; } void MainWindow::onContentItemDoubleClicked(const QModelIndex &index) @@ -2825,47 +2802,44 @@ void MainWindow::onStartCoreClicked() msg_hash_to_str(MSG_FAILED_TO_LOAD_CONTENT)); } -QHash MainWindow::getSelectedCore() +/* Resolve which core path the user has currently selected, given the + * UI's combo-box mode and the currently-highlighted content row. The + * result is the empty string if no core can be resolved (e.g. unknown + * mode, no current item, or no default core for the playlist). */ +QString MainWindow::getSelectedCorePath() { - QHash coreHash; - QHash contentHash; + PlaylistEntry entry; QVariantMap coreMap = m_launchWithComboBox->currentData( Qt::UserRole).value(); core_selection coreSelection = static_cast( coreMap.value("core_selection").toInt()); ViewType viewType = getCurrentViewType(); - if (viewType == VIEW_TYPE_LIST) - contentHash = m_tableView->currentIndex().data( - PlaylistModel::HASH).value >(); - else if (viewType == VIEW_TYPE_ICONS) - contentHash = m_gridView->currentIndex().data( - PlaylistModel::HASH).value >(); + /* The content row only matters for the two playlist branches — + * CORE_SELECTION_CURRENT just hands back whatever core is loaded. + * Original behaviour: an "other" view type (e.g. while transitioning) + * returned an empty entry from all three branches. */ + if (viewType == VIEW_TYPE_LIST || viewType == VIEW_TYPE_ICONS) + entry = getCurrentContentIndex().data(PlaylistModel::ENTRY) + .value(); else - return coreHash; + return QString(); - switch(coreSelection) + switch (coreSelection) { case CORE_SELECTION_CURRENT: - coreHash["core_path"] = path_get(RARCH_PATH_CORE); - break; + return QString::fromUtf8(path_get(RARCH_PATH_CORE)); case CORE_SELECTION_PLAYLIST_SAVED: - if ( contentHash.isEmpty() - || contentHash["core_path"].isEmpty()) - break; - - coreHash["core_path"] = contentHash["core_path"]; + if (!entry.corePath.isEmpty()) + return entry.corePath; break; case CORE_SELECTION_PLAYLIST_DEFAULT: { QString plName; QString defaultCorePath; - if (contentHash.isEmpty()) - break; - - plName = contentHash["pl_name"].isEmpty() - ? contentHash["db_name"] : contentHash["pl_name"]; + plName = entry.plName.isEmpty() + ? entry.dbName : entry.plName; if (plName.isEmpty()) break; @@ -2873,14 +2847,14 @@ QHash MainWindow::getSelectedCore() defaultCorePath = getPlaylistDefaultCore(plName); if (!defaultCorePath.isEmpty()) - coreHash["core_path"] = defaultCorePath; + return defaultCorePath; break; } default: break; } - return coreHash; + return QString(); } /* the hash typically has the following keys: @@ -2894,7 +2868,7 @@ core_name - The display name of the core, or "DETECT" if unknown label_noext - The display name of the content that is guaranteed not to contain a file extension */ -void MainWindow::loadContent(const QHash &contentHash) +void MainWindow::loadContent(const PlaylistEntry &entry) { content_ctx_info_t content_info; QByteArray corePathArray; @@ -2927,18 +2901,14 @@ void MainWindow::loadContent(const QHash &contentHash) { QStringList extensionFilters; - if (contentHash.contains("path")) + if (!entry.path.isEmpty()) { - int last_index = contentHash["path"].lastIndexOf('.'); - QByteArray pathArray = contentHash["path"].toUtf8(); + QByteArray pathArray = entry.path.toUtf8(); const char *pathData = pathArray.constData(); + const char *ext = path_get_extension(pathData); - if (last_index >= 0) - { - QString ext_str = contentHash["path"].mid(last_index + 1); - if (!ext_str.isEmpty()) - extensionFilters.append(ext_str.toLower()); - } + if (ext && *ext) + extensionFilters.append(QString(ext).toLower()); if (path_is_compressed_file(pathData)) { @@ -2951,7 +2921,7 @@ void MainWindow::loadContent(const QHash &contentHash) size_t i; for (i = 0; i < list->size; i++) { - const char *filePath = list->elems[i].data; + const char *filePath = list->elems[i].data; const char *extension = path_get_extension(filePath); if (!extensionFilters.contains(extension, Qt::CaseInsensitive)) @@ -2974,26 +2944,26 @@ void MainWindow::loadContent(const QHash &contentHash) { case CORE_SELECTION_CURRENT: corePathArray = path_get(RARCH_PATH_CORE); - contentPathArray = contentHash["path"].toUtf8(); - contentLabelArray = contentHash["label_noext"].toUtf8(); + contentPathArray = entry.path.toUtf8(); + contentLabelArray = entry.labelNoExt.toUtf8(); break; case CORE_SELECTION_PLAYLIST_SAVED: - corePathArray = contentHash["core_path"].toUtf8(); - contentPathArray = contentHash["path"].toUtf8(); - contentLabelArray = contentHash["label_noext"].toUtf8(); + corePathArray = entry.corePath.toUtf8(); + contentPathArray = entry.path.toUtf8(); + contentLabelArray = entry.labelNoExt.toUtf8(); break; case CORE_SELECTION_PLAYLIST_DEFAULT: { - QString plName = contentHash["pl_name"].isEmpty() - ? contentHash["db_name"] : contentHash["pl_name"]; + QString plName = entry.plName.isEmpty() + ? entry.dbName : entry.plName; QString defaultCorePath = getPlaylistDefaultCore(plName); if (!defaultCorePath.isEmpty()) { corePathArray = defaultCorePath.toUtf8(); - contentPathArray = contentHash["path"].toUtf8(); - contentLabelArray = contentHash["label_noext"].toUtf8(); + contentPathArray = entry.path.toUtf8(); + contentLabelArray = entry.labelNoExt.toUtf8(); } break; @@ -3002,8 +2972,8 @@ void MainWindow::loadContent(const QHash &contentHash) return; } - contentDbNameArray = contentHash["db_name"].toUtf8(); - contentCrc32Array = contentHash["crc32"].toUtf8(); + contentDbNameArray = entry.dbName.toUtf8(); + contentCrc32Array = entry.crc32.toUtf8(); core_path = corePathArray.constData(); content_path = contentPathArray.constData(); @@ -3062,21 +3032,21 @@ void MainWindow::loadContent(const QHash &contentHash) void MainWindow::onRunClicked() { - QHash contentHash; + PlaylistEntry entry; switch (m_currentBrowser) { case BROWSER_TYPE_FILES: - contentHash = getFileContentHash( + entry = getFileContentEntry( m_proxyFileModel->mapToSource(m_fileTableView->currentIndex())); break; case BROWSER_TYPE_PLAYLISTS: - contentHash = getCurrentContentHash(); + entry = getCurrentContentEntry(); break; } - if (!contentHash.isEmpty()) - loadContent(contentHash); + if (!entry.path.isEmpty()) + loadContent(entry); } PlaylistEntryDialog* MainWindow::playlistEntryDialog() @@ -3089,8 +3059,7 @@ ViewOptionsDialog* MainWindow::viewOptionsDialog() {return m_viewOptionsDialog;} void MainWindow::setCoreActions() { QListWidgetItem *currentPlaylistItem = m_listWidget->currentItem(); - ViewType viewType = getCurrentViewType(); - QHash hash = getCurrentContentHash(); + PlaylistEntry entry = getCurrentContentEntry(); QString currentPlaylistFileName = QString(); rarch_system_info_t *sys_info = &runloop_state_get_ptr()->system; @@ -3117,47 +3086,37 @@ void MainWindow::setCoreActions() if (m_currentBrowser == BROWSER_TYPE_PLAYLISTS) { - if (!hash.isEmpty()) - { - QString coreName = hash["core_name"]; + const QString &coreName = entry.coreName; - if (coreName.isEmpty()) - coreName = QString(""); - else + if (!coreName.isEmpty() && coreName != QLatin1String("DETECT")) + { + if (m_launchWithComboBox->findText(coreName) == -1) { - const char *detect_str = "DETECT"; + int i; + bool found_existing = false; - if (coreName != detect_str) + for (i = 0; i < m_launchWithComboBox->count(); i++) { - if (m_launchWithComboBox->findText(coreName) == -1) - { - int i; - bool found_existing = false; - - for (i = 0; i < m_launchWithComboBox->count(); i++) - { - QVariantMap map = m_launchWithComboBox->itemData( - i, Qt::UserRole).toMap(); + QVariantMap map = m_launchWithComboBox->itemData( + i, Qt::UserRole).toMap(); - if ( map.value("core_path").toString() == hash["core_path"] - || map.value("core_name").toString() == coreName) - { - found_existing = true; - break; - } - } - - if (!found_existing) - { - QVariantMap comboBoxMap; - comboBoxMap["core_name"] = coreName; - comboBoxMap["core_path"] = hash["core_path"]; - comboBoxMap["core_selection"] = CORE_SELECTION_PLAYLIST_SAVED; - m_launchWithComboBox->addItem(coreName, - QVariant::fromValue(comboBoxMap)); - } + if ( map.value("core_path").toString() == entry.corePath + || map.value("core_name").toString() == coreName) + { + found_existing = true; + break; } } + + if (!found_existing) + { + QVariantMap comboBoxMap; + comboBoxMap["core_name"] = coreName; + comboBoxMap["core_path"] = entry.corePath; + comboBoxMap["core_selection"] = CORE_SELECTION_PLAYLIST_SAVED; + m_launchWithComboBox->addItem(coreName, + QVariant::fromValue(comboBoxMap)); + } } } } @@ -3165,8 +3124,8 @@ void MainWindow::setCoreActions() switch(m_currentBrowser) { case BROWSER_TYPE_PLAYLISTS: - currentPlaylistFileName = hash["pl_name"].isEmpty() - ? hash["db_name"] : hash["pl_name"]; + currentPlaylistFileName = entry.plName.isEmpty() + ? entry.dbName : entry.plName; break; case BROWSER_TYPE_FILES: currentPlaylistFileName = m_fileModel->rootDirectory().dirName(); @@ -3199,13 +3158,10 @@ void MainWindow::setCoreActions() if (allPlaylists) { - QFileInfo info; QListWidgetItem *listItem = m_listWidget->item(row); QString listItemString = listItem->data( Qt::UserRole).toString(); - info.setFile(listItemString); - if (listItemString == ALL_PLAYLISTS_TOKEN) continue; } @@ -3467,14 +3423,12 @@ void MainWindow::onShowHiddenDockWidgetAction() if (!dock->isVisible()) { - addDockWidget(static_cast( - dock->property("default_area").toInt()), dock); + qt_dock_add_to(this, dock); dock->setVisible(true); dock->setFloating(false); } } -QWidget* MainWindow::searchWidget() { return m_searchWidget; } QLineEdit* MainWindow::searchLineEdit() { return m_searchLineEdit; } void MainWindow::onSearchEnterPressed() { @@ -3484,16 +3438,16 @@ void MainWindow::onSearchEnterPressed() void MainWindow::onCurrentTableItemDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { - QHash hash; + PlaylistEntry entry; if (!roles.contains(Qt::EditRole)) return; if (topLeft != bottomRight) return; - hash = topLeft.data(PlaylistModel::HASH).value>(); + entry = topLeft.data(PlaylistModel::ENTRY).value(); - updateCurrentPlaylistEntry(hash); + updateCurrentPlaylistEntry(entry); onCurrentItemChanged(topLeft); } @@ -3505,33 +3459,41 @@ void MainWindow::onCurrentListItemDataChanged(QListWidgetItem *item) void MainWindow::renamePlaylistItem(QListWidgetItem *item, QString newName) { - QString oldPath; - QString newPath; - QString extension; + char old_path[PATH_MAX_LENGTH]; + char new_path[PATH_MAX_LENGTH]; + char old_basedir[PATH_MAX_LENGTH]; + char dir_playlist[PATH_MAX_LENGTH]; + char old_name_buf[PATH_MAX_LENGTH]; + const char *ext = NULL; QString oldName; - QFile file; - QFileInfo info; - QFileInfo playlistInfo; - QString playlistPath; + QString newPath; settings_t *settings = config_get_ptr(); const char *path_dir_playlist = settings->paths.directory_playlist; - QDir playlistDir(path_dir_playlist); if (!item) return; - playlistPath = item->data(Qt::UserRole).toString(); - playlistInfo = QFileInfo(playlistPath); - oldName = playlistInfo.completeBaseName(); + strlcpy(old_path, + item->data(Qt::UserRole).toString().toUtf8().constData(), + sizeof(old_path)); + + /* completeBaseName(): strip directory and extension */ + fill_pathname(old_name_buf, path_basename(old_path), "", + sizeof(old_name_buf)); + oldName = QString::fromUtf8(old_name_buf); + + /* Compare the playlist's directory with path_dir_playlist + * case-insensitively to match Qt's QDir == QDir behaviour + * on Windows. */ + strlcpy(old_basedir, old_path, sizeof(old_basedir)); + path_basedir(old_basedir); + strlcpy(dir_playlist, path_dir_playlist, sizeof(dir_playlist)); + fill_pathname_slash(dir_playlist, sizeof(dir_playlist)); - /* Don't just compare strings in case there are - * case differences on Windows that should be ignored. */ - /* special playlists like history etc. can't have an association */ - if (QDir(playlistInfo.absoluteDir()) != QDir(playlistDir)) + if (!string_is_equal_case_insensitive(old_basedir, dir_playlist)) { - /* Special playlists shouldn't be editable already, - * but just in case, set the old name back and - * early return if they rename it */ + /* Special playlists (history etc.) can't have an association. + * Set the old name back if user tried to rename one. */ item->setText(oldName); return; } @@ -3541,25 +3503,24 @@ void MainWindow::renamePlaylistItem(QListWidgetItem *item, QString newName) disconnect(m_listWidget, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCurrentListItemDataChanged(QListWidgetItem*))); - oldPath = item->data(Qt::UserRole).toString(); - - file.setFileName(oldPath); - info = QFileInfo(file); - - extension = info.suffix(); - - newPath = info.absolutePath(); - - /* absolutePath() will always use / even on Windows */ - if (newPath.at(newPath.size() - 1) != '/') - /* add trailing slash if the path doesn't have one */ - newPath += '/'; - - newPath += newName + QString(".") + extension; + /* Build new path: basedir + newName + "." + extension */ + ext = path_get_extension(old_path); + { + QByteArray newNameUtf8 = newName.toUtf8(); + size_t _len = strlcpy(new_path, old_basedir, sizeof(new_path)); + _len += strlcpy(new_path + _len, newNameUtf8.constData(), + sizeof(new_path) - _len); + if (ext && *ext) + { + _len += strlcpy(new_path + _len, ".", sizeof(new_path) - _len); + strlcpy(new_path + _len, ext, sizeof(new_path) - _len); + } + } + newPath = QString::fromUtf8(new_path); item->setData(Qt::UserRole, newPath); - if (!file.rename(newPath)) + if (filestream_rename(old_path, new_path) != 0) { RARCH_ERR("[Qt] Could not rename playlist.\n"); item->setText(oldName); @@ -3572,70 +3533,106 @@ void MainWindow::renamePlaylistItem(QListWidgetItem *item, QString newName) void MainWindow::onCurrentItemChanged(const QModelIndex &index) { onCurrentItemChanged(index.data( - PlaylistModel::HASH).value>()); + PlaylistModel::ENTRY).value()); } void MainWindow::onCurrentFileChanged(const QModelIndex &index) { - onCurrentItemChanged(getFileContentHash( + onCurrentItemChanged(getFileContentEntry( m_proxyFileModel->mapToSource(index))); } -void MainWindow::onCurrentItemChanged(const QHash &hash) +void MainWindow::onCurrentItemChanged(const PlaylistEntry &entry) { - QString path = hash["path"]; - bool acceptDrop = false; + size_t i; + const QString &path = entry.path; + bool acceptDrop = false; - if (m_thumbnailPixmap) - delete m_thumbnailPixmap; - if (m_thumbnailPixmap2) - delete m_thumbnailPixmap2; - if (m_thumbnailPixmap3) - delete m_thumbnailPixmap3; - if (m_thumbnailPixmap4) - delete m_thumbnailPixmap4; + for (i = 0; i < 4; i++) + { + if (m_thumbnailPixmaps[i]) + delete m_thumbnailPixmaps[i]; + m_thumbnailPixmaps[i] = NULL; + } if (m_playlistModel->isSupportedImage(path)) { - /* use thumbnail widgets to show regular image files */ - m_thumbnailPixmap = new QPixmap(pixmapFromPathRA(path)); - m_thumbnailPixmap2 = new QPixmap(*m_thumbnailPixmap); - m_thumbnailPixmap3 = new QPixmap(*m_thumbnailPixmap); - m_thumbnailPixmap4 = new QPixmap(*m_thumbnailPixmap); + /* Use thumbnail widgets to show regular image files. These can + * be very large (multi-GiB ARGB32 bitmaps after decoding a + * high-resolution PNG), so do the decode on the loader thread + * and update the panes when it arrives. Until then the panes + * show blank, which also clears any image left from a previous + * selection. */ + QPixmap blank; + + m_pendingPreviewPath = path; + + for (i = 0; i < 4; i++) + setThumbnail(qt_thumbnail_widget_names[i], blank, false); + + m_previewLoader->request(QModelIndex(), path); + + setCoreActions(); + return; } else { QString thumbnailsDir = m_playlistModel->getPlaylistThumbnailsDir( - hash["db_name"]); - QString thumbnailName1 = m_playlistModel->getSanitizedThumbnailName( - thumbnailsDir + QString("/") + THUMBNAIL_BOXART + QString("/"), - hash["label_noext"]); - QString thumbnailName2 = m_playlistModel->getSanitizedThumbnailName( - thumbnailsDir + QString("/") + THUMBNAIL_TITLE + QString("/"), - hash["label_noext"]); - QString thumbnailName3 = m_playlistModel->getSanitizedThumbnailName( - thumbnailsDir + QString("/") + THUMBNAIL_SCREENSHOT + QString("/"), - hash["label_noext"]); - QString thumbnailName4 = m_playlistModel->getSanitizedThumbnailName( - thumbnailsDir + QString("/") + THUMBNAIL_LOGO + QString("/"), - hash["label_noext"]); - - m_thumbnailPixmap = new QPixmap(pixmapFromPathRA(thumbnailName1)); - m_thumbnailPixmap2 = new QPixmap(pixmapFromPathRA(thumbnailName2)); - m_thumbnailPixmap3 = new QPixmap(pixmapFromPathRA(thumbnailName3)); - m_thumbnailPixmap4 = new QPixmap(pixmapFromPathRA(thumbnailName4)); + entry.dbName); + + /* Clear any pending file-browser preview request: this code + * path serves the playlist views, not the file browser, so a + * preview result arriving now would be unwanted. */ + m_pendingPreviewPath = QString(); + + for (i = 0; i < 4; i++) + { + QString name = m_playlistModel->getSanitizedThumbnailName( + thumbnailsDir + QString("/") + + qt_thumbnail_subdirs[i] + QString("/"), + entry.labelNoExt); + m_thumbnailPixmaps[i] = new QPixmap(pixmapFromPathRA(name)); + } if ( m_currentBrowser == BROWSER_TYPE_PLAYLISTS && !currentPlaylistIsSpecial()) acceptDrop = true; } - onResizeThumbnailOne(*m_thumbnailPixmap, acceptDrop); - onResizeThumbnailTwo(*m_thumbnailPixmap2, acceptDrop); - onResizeThumbnailThree(*m_thumbnailPixmap3, acceptDrop); - onResizeThumbnailFour(*m_thumbnailPixmap4, acceptDrop); + for (i = 0; i < 4; i++) + setThumbnail(qt_thumbnail_widget_names[i], + *m_thumbnailPixmaps[i], acceptDrop); + + setCoreActions(); +} + +void MainWindow::onPreviewImageLoaded(const QImage image, + const QModelIndex & /* index */, const QString &path) +{ + size_t i; + + /* Drop stale results: if the user moved selection while we were + * decoding, the path we just got back isn't what's currently + * showing. */ + if (path != m_pendingPreviewPath) + return; + if (image.isNull()) + return; + + for (i = 0; i < 4; i++) + { + if (m_thumbnailPixmaps[i]) + delete m_thumbnailPixmaps[i]; + m_thumbnailPixmaps[i] = NULL; + } + + m_thumbnailPixmaps[0] = new QPixmap(QPixmap::fromImage(image)); + for (i = 1; i < 4; i++) + m_thumbnailPixmaps[i] = new QPixmap(*m_thumbnailPixmaps[0]); - setCoreActions(); + for (i = 0; i < 4; i++) + setThumbnail(qt_thumbnail_widget_names[i], + *m_thumbnailPixmaps[i], false); } void MainWindow::setThumbnail(QString widgetName, @@ -3646,26 +3643,6 @@ void MainWindow::setThumbnail(QString widgetName, thumbnail->setPixmap(pixmap, acceptDrop); } -void MainWindow::onResizeThumbnailOne(QPixmap &pixmap, bool acceptDrop) -{ - setThumbnail("thumbnail", pixmap, acceptDrop); -} - -void MainWindow::onResizeThumbnailTwo(QPixmap &pixmap, bool acceptDrop) -{ - setThumbnail("thumbnail2", pixmap, acceptDrop); -} - -void MainWindow::onResizeThumbnailThree(QPixmap &pixmap, bool acceptDrop) -{ - setThumbnail("thumbnail3", pixmap, acceptDrop); -} - -void MainWindow::onResizeThumbnailFour(QPixmap &pixmap, bool acceptDrop) -{ - setThumbnail("thumbnail4", pixmap, acceptDrop); -} - void MainWindow::setCurrentViewType(ViewType viewType) { m_viewType = viewType; @@ -3707,12 +3684,10 @@ void MainWindow::onCurrentListItemChanged( setCoreActions(); } -TableView* MainWindow::contentTableView() { return m_tableView; } QTableView* MainWindow::fileTableView() { return m_fileTableView; } QStackedWidget* MainWindow::centralWidget() { return m_centralWidget; } FileDropWidget* MainWindow::playlistViews() { return m_playlistViews; } QWidget* MainWindow::playlistViewsAndFooter() {return m_playlistViewsAndFooter;} -GridView* MainWindow::contentGridView() { return m_gridView; } void MainWindow::onBrowserDownloadsClicked() { @@ -3930,8 +3905,6 @@ void MainWindow::initContentTableWidget() if (!item) return; - m_currentGridHash.clear(); - if (m_currentGridWidget) { m_currentGridWidget->setObjectName("thumbnailWidget"); @@ -3986,36 +3959,28 @@ void MainWindow::keyPressEvent(QKeyEvent *event) QSettings* MainWindow::settings() { return m_settings; } -QString MainWindow::getCurrentViewTypeString() +const char *MainWindow::getCurrentViewTypeString() { - switch (m_viewType) - { - case VIEW_TYPE_ICONS: - return QString("icons"); - case VIEW_TYPE_LIST: - default: - break; - } - - return QString("list"); + if (m_viewType == VIEW_TYPE_ICONS) + return "icons"; + return "list"; } -QString MainWindow::getCurrentThumbnailTypeString() +const char *MainWindow::getCurrentThumbnailTypeString() { switch (m_thumbnailType) { case THUMBNAIL_TYPE_SCREENSHOT: - return QString("screenshot"); + return "screenshot"; case THUMBNAIL_TYPE_TITLE_SCREEN: - return QString("title"); + return "title"; case THUMBNAIL_TYPE_LOGO: - return QString("logo"); + return "logo"; case THUMBNAIL_TYPE_BOXART: default: - return QString("boxart"); + break; } - - return QString("list"); + return "boxart"; } ThumbnailType MainWindow::getThumbnailTypeFromString(QString thumbnailType) @@ -4154,8 +4119,10 @@ int MainWindow::onExtractArchive(QString path, QString extractionDir, struct archive_extract_userdata userdata; QByteArray pathArray = path.toUtf8(); QByteArray dirArray = extractionDir.toUtf8(); + QByteArray tmpExtArray = tempExtension.toUtf8(); const char *file = pathArray.constData(); const char *dir = dirArray.constData(); + const char *temp_ext = tmpExtArray.constData(); struct string_list *file_list = file_archive_get_file_list(file, NULL); retro_task_t *decompress_task = NULL; @@ -4169,37 +4136,45 @@ int MainWindow::onExtractArchive(QString path, QString extractionDir, for (i = 0; i < file_list->size; i++) { - QFile fileObj(file_list->elems[i].data); + const char *target_file = file_list->elems[i].data; - if (fileObj.exists()) - { - if (!fileObj.remove()) - { - /* If we cannot delete the existing file to update it, - * rename it for now and delete later */ - QFile fileTemp(fileObj.fileName() + tempExtension); + if (!filestream_exists(target_file)) + continue; - if (fileTemp.exists()) - { - if (!fileTemp.remove()) - { - showMessageBox(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_DELETE_FILE), - MainWindow::MSGBOX_TYPE_ERROR, Qt::ApplicationModal, false); - RARCH_ERR("[Qt] Could not delete file: \"%s\".\n", file_list->elems[i].data); - return -1; - } - } + if (filestream_delete(target_file) == 0) + continue; + + /* If we cannot delete the existing file to update it, + * rename it out of the way for later cleanup. */ + { + char temp_path[PATH_MAX_LENGTH]; + size_t _len = strlcpy(temp_path, target_file, sizeof(temp_path)); + strlcpy(temp_path + _len, temp_ext, sizeof(temp_path) - _len); - if (!fileObj.rename(fileTemp.fileName())) + if (filestream_exists(temp_path)) + { + if (filestream_delete(temp_path) != 0) { showMessageBox(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_RENAME_FILE), + MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_DELETE_FILE), MainWindow::MSGBOX_TYPE_ERROR, Qt::ApplicationModal, false); - RARCH_ERR("[Qt] Could not rename file: \"%s\".\n", file_list->elems[i].data); + RARCH_ERR("[Qt] Could not delete file: \"%s\".\n", + target_file); + string_list_free(file_list); return -1; } } + + if (filestream_rename(target_file, temp_path) != 0) + { + showMessageBox(msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_RENAME_FILE), + MainWindow::MSGBOX_TYPE_ERROR, Qt::ApplicationModal, false); + RARCH_ERR("[Qt] Could not rename file: \"%s\".\n", + target_file); + string_list_free(file_list); + return -1; + } } } @@ -4233,17 +4208,6 @@ int MainWindow::onExtractArchive(QString path, QString extractionDir, return 1; } -QString MainWindow::getScrubbedString(QString str) -{ - const QString chars("&*/:`\"<>?\\|"); - int i; - - for (i = 0; i < chars.size(); i++) - str.replace(chars.at(i), '_'); - - return str; -} - static void* ui_window_qt_init(void) { ui_window.qtWindow = new MainWindow(); @@ -4423,8 +4387,6 @@ static void* ui_application_qt_initialize(void) ui_application.app->setOrganizationName("libretro"); ui_application.app->setApplicationName("RetroArch"); ui_application.app->setApplicationVersion(PACKAGE_VERSION); - ui_application.app->connect(ui_application.app, SIGNAL(lastWindowClosed()), - app_handler, SLOT(onLastWindowClosed())); #ifdef Q_OS_UNIX setlocale(LC_NUMERIC, "C"); @@ -4490,7 +4452,6 @@ static ui_application_t ui_application_qt = { AppHandler::AppHandler(QObject *parent) : QObject(parent) { } AppHandler::~AppHandler() { } -void AppHandler::onLastWindowClosed() { } void AppHandler::exit() { @@ -4665,11 +4626,6 @@ void ThumbnailLabel::paintEvent(QPaintEvent *event) QWidget::paintEvent(event); } -void ThumbnailLabel::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); -} - static void ui_companion_qt_deinit(void *data) { ui_companion_qt_t *handle = (ui_companion_qt_t*)data; @@ -4683,110 +4639,23 @@ static void ui_companion_qt_deinit(void *data) free(handle); } -static void* ui_companion_qt_init(void) -{ - int i = 0; - QString initialPlaylist; - QRect desktopRect; - ui_companion_qt_t *handle = (ui_companion_qt_t*) - calloc(1, sizeof(*handle)); - MainWindow *mainwindow = NULL; - QHBoxLayout *browserButtonsHBoxLayout = NULL; - QVBoxLayout *layout = NULL; - QVBoxLayout *launchWithWidgetLayout = NULL; - QHBoxLayout *coreComboBoxLayout = NULL; - QMenuBar *menu = NULL; - QScreen *screen = NULL; - QMenu *fileMenu = NULL; - QMenu *editMenu = NULL; - QMenu *viewMenu = NULL; - QMenu *viewClosedDocksMenu = NULL; - QMenu *helpMenu = NULL; - QDockWidget *thumbnailDock = NULL; - QDockWidget *thumbnail2Dock = NULL; - QDockWidget *thumbnail3Dock = NULL; - QDockWidget *thumbnail4Dock = NULL; - QDockWidget *browserAndPlaylistTabDock = NULL; - QDockWidget *coreSelectionDock = NULL; - QTabWidget *browserAndPlaylistTabWidget = NULL; - QStackedWidget *centralWidget = NULL; - QStackedWidget *widget = NULL; - QFrame *browserWidget = NULL; - QFrame *playlistWidget = NULL; - QWidget *coreSelectionWidget = NULL; - QWidget *launchWithWidget = NULL; - ThumbnailWidget *thumbnailWidget = NULL; - ThumbnailWidget *thumbnail2Widget = NULL; - ThumbnailWidget *thumbnail3Widget = NULL; - ThumbnailWidget *thumbnail4Widget = NULL; - QPushButton *browserDownloadsButton = NULL; - QPushButton *browserUpButton = NULL; - QPushButton *browserStartButton = NULL; - ThumbnailLabel *thumbnail = NULL; - ThumbnailLabel *thumbnail2 = NULL; - ThumbnailLabel *thumbnail3 = NULL; - ThumbnailLabel *thumbnail4 = NULL; - QAction *editSearchAction = NULL; - QAction *loadCoreAction = NULL; - QAction *unloadCoreAction = NULL; - QAction *exitAction = NULL; - QComboBox *launchWithComboBox = NULL; - QSettings *qsettings = NULL; - QListWidget *listWidget = NULL; - bool foundPlaylist = false; - - if (!handle) - return NULL; - - handle->app = static_cast - (ui_application_qt.initialize()); - handle->window = static_cast(ui_window_qt.init()); - - screen = qApp->primaryScreen(); - desktopRect = screen->availableGeometry(); - - mainwindow = handle->window->qtWindow; - - qsettings = mainwindow->settings(); - - initialPlaylist = qsettings->value("initial_playlist", - mainwindow->getSpecialPlaylistPath(SPECIAL_PLAYLIST_HISTORY)).toString(); - - mainwindow->resize(((desktopRect.width()) < (INITIAL_WIDTH) ? (desktopRect.width()) : (INITIAL_WIDTH)), - ((desktopRect.height()) < (INITIAL_HEIGHT) ? (desktopRect.height()) : (INITIAL_HEIGHT))); - mainwindow->setGeometry(QStyle::alignedRect(Qt::LeftToRight, - Qt::AlignCenter, mainwindow->size(), desktopRect)); - - mainwindow->setWindowTitle("RetroArch"); - mainwindow->setDockOptions(QMainWindow::AnimatedDocks - | QMainWindow::AllowNestedDocks - | QMainWindow::AllowTabbedDocks - | GROUPED_DRAGGING); - - listWidget = mainwindow->playlistListWidget(); - - widget = mainwindow->playlistViews(); - widget->setContextMenuPolicy(Qt::CustomContextMenu); - - QObject::connect(widget, SIGNAL(filesDropped(QStringList)), - mainwindow, SLOT(onPlaylistFilesDropped(QStringList))); - QObject::connect(widget, SIGNAL(enterPressed()), mainwindow, - SLOT(onDropWidgetEnterPressed())); - QObject::connect(widget, SIGNAL(deletePressed()), mainwindow, - SLOT(deleteCurrentPlaylistItem())); - QObject::connect(widget, SIGNAL(customContextMenuRequested(const QPoint&)), - mainwindow, SLOT(onFileDropWidgetContextMenuRequested(const QPoint&))); - - centralWidget = mainwindow->centralWidget(); - - centralWidget->addWidget(mainwindow->playlistViewsAndFooter()); - centralWidget->addWidget(mainwindow->fileTableView()); +/* ---------------------------------------------------------------- */ +/* Helpers split out of ui_companion_qt_init() for readability. */ +/* No functional changes from the original monolithic flow. */ +/* ---------------------------------------------------------------- */ - mainwindow->setCentralWidget(centralWidget); - - menu = mainwindow->menuBar(); - - fileMenu = menu->addMenu(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_MENU_FILE)); +static void qt_companion_build_menubar(MainWindow *mainwindow) +{ + QMenuBar *menu = mainwindow->menuBar(); + QMenu *fileMenu = menu->addMenu(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_MENU_FILE)); + QMenu *editMenu; + QMenu *viewMenu; + QMenu *viewClosedDocksMenu; + QMenu *helpMenu; + QAction *loadCoreAction; + QAction *unloadCoreAction; + QAction *exitAction; + QAction *editSearchAction; loadCoreAction = fileMenu->addAction(msg_hash_to_str( MENU_ENUM_LABEL_VALUE_QT_MENU_FILE_LOAD_CORE), mainwindow, @@ -4850,26 +4719,33 @@ static void* ui_companion_qt_init(void) MENU_ENUM_LABEL_VALUE_QT_MENU_HELP_ABOUT)) + QString("..."), mainwindow, SLOT(showAbout())); helpMenu->addAction(QString("About Qt..."), qApp, SLOT(aboutQt())); +} + +/* Build the playlist + file-browser tab dock. Returns the dock so the + * caller can splitDockWidget() the core-selection dock against it. */ +static QDockWidget *qt_companion_build_browser_dock(MainWindow *mainwindow) +{ + QFrame *playlistWidget = new QFrame(); + QFrame *browserWidget = new QFrame(); + QPushButton *browserDownloadsButton = new QPushButton(msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_CORE_ASSETS_DIRECTORY)); + QPushButton *browserUpButton = new QPushButton(msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_QT_TAB_FILE_BROWSER_UP)); + QPushButton *browserStartButton = new QPushButton(msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_FAVORITES)); + QHBoxLayout *browserButtonsHBoxLayout = new QHBoxLayout(); + QTabWidget *browserAndPlaylistTabWidget = mainwindow->browserAndPlaylistTabWidget(); + QDockWidget *browserAndPlaylistTabDock; - playlistWidget = new QFrame(); playlistWidget->setLayout(new QVBoxLayout()); playlistWidget->setObjectName("playlistWidget"); playlistWidget->layout()->setContentsMargins(0, 0, 0, 0); - playlistWidget->layout()->addWidget(mainwindow->playlistListWidget()); - browserWidget = new QFrame(); browserWidget->setLayout(new QVBoxLayout()); browserWidget->setObjectName("browserWidget"); browserWidget->layout()->setContentsMargins(0, 0, 0, 0); - browserDownloadsButton = new QPushButton(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_CORE_ASSETS_DIRECTORY)); - browserUpButton = new QPushButton(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_TAB_FILE_BROWSER_UP)); - browserStartButton = new QPushButton(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_FAVORITES)); - QObject::connect(browserDownloadsButton, SIGNAL(clicked()), mainwindow, SLOT(onBrowserDownloadsClicked())); QObject::connect(browserUpButton, SIGNAL(clicked()), mainwindow, @@ -4877,7 +4753,6 @@ static void* ui_companion_qt_init(void) QObject::connect(browserStartButton, SIGNAL(clicked()), mainwindow, SLOT(onBrowserStartClicked())); - browserButtonsHBoxLayout = new QHBoxLayout(); browserButtonsHBoxLayout->addWidget(browserUpButton); browserButtonsHBoxLayout->addWidget(browserStartButton); browserButtonsHBoxLayout->addWidget(browserDownloadsButton); @@ -4885,15 +4760,13 @@ static void* ui_companion_qt_init(void) qobject_cast(browserWidget->layout())->addLayout(browserButtonsHBoxLayout); browserWidget->layout()->addWidget(mainwindow->dirTreeView()); - browserAndPlaylistTabWidget = mainwindow->browserAndPlaylistTabWidget(); browserAndPlaylistTabWidget->setObjectName("browserAndPlaylistTabWidget"); /* Several functions depend on the same tab title strings here, - * so if you change these, make sure to change those too - * setCoreActions() - * onTabWidgetIndexChanged() - * onCurrentListItemChanged() - */ + * so if you change these, make sure to change those too: + * setCoreActions() + * onTabWidgetIndexChanged() + * onCurrentListItemChanged() */ browserAndPlaylistTabWidget->addTab(playlistWidget, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_TAB_PLAYLISTS)); browserAndPlaylistTabWidget->addTab(browserWidget, @@ -4901,108 +4774,79 @@ static void* ui_companion_qt_init(void) browserAndPlaylistTabDock = new QDockWidget(msg_hash_to_str( MENU_ENUM_LABEL_VALUE_QT_MENU_DOCK_CONTENT_BROWSER), mainwindow); - browserAndPlaylistTabDock->setObjectName("browserAndPlaylistTabDock"); - browserAndPlaylistTabDock->setProperty("default_area", Qt::LeftDockWidgetArea); - browserAndPlaylistTabDock->setProperty("menu_text", - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_MENU_DOCK_CONTENT_BROWSER)); - browserAndPlaylistTabDock->setWidget(browserAndPlaylistTabWidget); - - mainwindow->addDockWidget(static_cast( - browserAndPlaylistTabDock->property("default_area").toInt()), - browserAndPlaylistTabDock); + qt_dock_configure(browserAndPlaylistTabDock, + "browserAndPlaylistTabDock", Qt::LeftDockWidgetArea, + MENU_ENUM_LABEL_VALUE_QT_MENU_DOCK_CONTENT_BROWSER, + browserAndPlaylistTabWidget); + + qt_dock_add_to(mainwindow, browserAndPlaylistTabDock); browserButtonsHBoxLayout->addItem(new QSpacerItem( browserAndPlaylistTabWidget->tabBar()->width(), 20, QSizePolicy::Expanding, QSizePolicy::Minimum)); - thumbnailWidget = new ThumbnailWidget(THUMBNAIL_TYPE_BOXART); - thumbnailWidget->setObjectName("thumbnail"); - - thumbnail2Widget = new ThumbnailWidget(THUMBNAIL_TYPE_TITLE_SCREEN); - thumbnail2Widget->setObjectName("thumbnail2"); - - thumbnail3Widget = new ThumbnailWidget(THUMBNAIL_TYPE_SCREENSHOT); - thumbnail3Widget->setObjectName("thumbnail3"); - - thumbnail4Widget = new ThumbnailWidget(THUMBNAIL_TYPE_LOGO); - thumbnail4Widget->setObjectName("thumbnail4"); - - QObject::connect(thumbnailWidget, SIGNAL(filesDropped(const QImage&, - ThumbnailType)), mainwindow, - SLOT(onThumbnailDropped(const QImage&, ThumbnailType))); - QObject::connect(thumbnail2Widget, SIGNAL(filesDropped(const QImage&, - ThumbnailType)), mainwindow, - SLOT(onThumbnailDropped(const QImage&, ThumbnailType))); - QObject::connect(thumbnail3Widget, SIGNAL(filesDropped(const QImage&, - ThumbnailType)), mainwindow, - SLOT(onThumbnailDropped(const QImage&, ThumbnailType))); - QObject::connect(thumbnail4Widget, SIGNAL(filesDropped(const QImage&, - ThumbnailType)), mainwindow, - SLOT(onThumbnailDropped(const QImage&, ThumbnailType))); - - thumbnailDock = new QDockWidget(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_BOXART), mainwindow); - thumbnailDock->setObjectName("thumbnailDock"); - thumbnailDock->setProperty("default_area", Qt::RightDockWidgetArea); - thumbnailDock->setProperty("menu_text", msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_BOXART)); - thumbnailDock->setWidget(thumbnailWidget); - - mainwindow->addDockWidget(static_cast( - thumbnailDock->property("default_area").toInt()), thumbnailDock); - - thumbnail2Dock = new QDockWidget(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_TITLE_SCREEN), mainwindow); - thumbnail2Dock->setObjectName("thumbnail2Dock"); - thumbnail2Dock->setProperty("default_area", Qt::RightDockWidgetArea); - thumbnail2Dock->setProperty("menu_text", msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_TITLE_SCREEN)); - thumbnail2Dock->setWidget(thumbnail2Widget); - - mainwindow->addDockWidget(static_cast( - thumbnail2Dock->property("default_area").toInt()), thumbnail2Dock); - - thumbnail3Dock = new QDockWidget(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_SCREENSHOT), mainwindow); - thumbnail3Dock->setObjectName("thumbnail3Dock"); - thumbnail3Dock->setProperty("default_area", Qt::RightDockWidgetArea); - thumbnail3Dock->setProperty("menu_text", msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_SCREENSHOT)); - thumbnail3Dock->setWidget(thumbnail3Widget); - - mainwindow->addDockWidget(static_cast( - thumbnail3Dock->property("default_area").toInt()), thumbnail3Dock); - - thumbnail4Dock = new QDockWidget(msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_LOGO), mainwindow); - thumbnail4Dock->setObjectName("thumbnail4Dock"); - thumbnail4Dock->setProperty("default_area", Qt::RightDockWidgetArea); - thumbnail4Dock->setProperty("menu_text", msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_LOGO)); - thumbnail4Dock->setWidget(thumbnail4Widget); - - mainwindow->addDockWidget(static_cast( - thumbnail4Dock->property("default_area").toInt()), thumbnail4Dock); - - mainwindow->tabifyDockWidget(thumbnailDock, thumbnail2Dock); - mainwindow->tabifyDockWidget(thumbnailDock, thumbnail3Dock); - mainwindow->tabifyDockWidget(thumbnailDock, thumbnail4Dock); + return browserAndPlaylistTabDock; +} + +/* Build the four boxart/title/screenshot/logo thumbnail docks. + * The four docks are tabbed against the first one. */ +static void qt_companion_build_thumbnail_docks(MainWindow *mainwindow) +{ + /* Maps widget index -> ThumbnailType + display label hash + dock obj name. */ + static const ThumbnailType types[4] = { + THUMBNAIL_TYPE_BOXART, THUMBNAIL_TYPE_TITLE_SCREEN, + THUMBNAIL_TYPE_SCREENSHOT, THUMBNAIL_TYPE_LOGO + }; + static const msg_hash_enums labels[4] = { + MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_BOXART, + MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_TITLE_SCREEN, + MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_SCREENSHOT, + MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_LOGO + }; + static const char * const dock_obj_names[4] = { + "thumbnailDock", "thumbnail2Dock", "thumbnail3Dock", "thumbnail4Dock" + }; + QDockWidget *docks[4]; + int i; - /* When tabifying the dock widgets, the last tab added is selected - * by default, so we need to re-select the first tab */ - thumbnailDock->raise(); + for (i = 0; i < 4; i++) + { + ThumbnailWidget *tw = new ThumbnailWidget(types[i]); + tw->setObjectName(qt_thumbnail_widget_names[i]); - coreSelectionWidget = new QWidget(); - coreSelectionWidget->setLayout(new QVBoxLayout()); + QObject::connect(tw, SIGNAL(filesDropped(const QImage&, + ThumbnailType)), mainwindow, + SLOT(onThumbnailDropped(const QImage&, ThumbnailType))); - launchWithComboBox = mainwindow->launchWithComboBox(); + docks[i] = new QDockWidget(msg_hash_to_str(labels[i]), mainwindow); + qt_dock_configure(docks[i], dock_obj_names[i], + Qt::RightDockWidgetArea, labels[i], tw); - launchWithWidgetLayout = new QVBoxLayout(); + qt_dock_add_to(mainwindow, docks[i]); + } + + for (i = 1; i < 4; i++) + mainwindow->tabifyDockWidget(docks[0], docks[i]); + + /* When tabifying the dock widgets, the last tab added is selected + * by default, so we re-select the first tab here. */ + docks[0]->raise(); +} - launchWithWidget = new QWidget(); - launchWithWidget->setLayout(launchWithWidgetLayout); +/* Build the core-selection dock (combo box + run/stop/start buttons) + * and split it vertically against the existing browser/playlist dock. */ +static void qt_companion_build_core_selection_dock(MainWindow *mainwindow, + QDockWidget *browserAndPlaylistTabDock) +{ + QWidget *coreSelectionWidget = new QWidget(); + QVBoxLayout *launchWithLayout = new QVBoxLayout(); + QWidget *launchWithWidget = new QWidget(); + QHBoxLayout *coreComboBoxLayout = new QHBoxLayout(); + QComboBox *launchWithComboBox = mainwindow->launchWithComboBox(); + QDockWidget *coreSelectionDock; - coreComboBoxLayout = new QHBoxLayout(); + coreSelectionWidget->setLayout(new QVBoxLayout()); + launchWithWidget->setLayout(launchWithLayout); mainwindow->runPushButton()->setSizePolicy( QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding)); @@ -5021,25 +4865,21 @@ static void* ui_companion_qt_init(void) coreComboBoxLayout->setStretchFactor(launchWithComboBox, 1); - launchWithWidgetLayout->addLayout(coreComboBoxLayout); + launchWithLayout->addLayout(coreComboBoxLayout); coreSelectionWidget->layout()->addWidget(launchWithWidget); - coreSelectionWidget->layout()->addItem(new QSpacerItem(20, - browserAndPlaylistTabWidget->height(), + mainwindow->browserAndPlaylistTabWidget()->height(), QSizePolicy::Minimum, QSizePolicy::Expanding)); coreSelectionDock = new QDockWidget(msg_hash_to_str( MENU_ENUM_LABEL_VALUE_QT_CORE), mainwindow); - coreSelectionDock->setObjectName("coreSelectionDock"); - coreSelectionDock->setProperty("default_area", Qt::LeftDockWidgetArea); - coreSelectionDock->setProperty("menu_text", msg_hash_to_str( - MENU_ENUM_LABEL_VALUE_QT_CORE)); - coreSelectionDock->setWidget(coreSelectionWidget); + qt_dock_configure(coreSelectionDock, "coreSelectionDock", + Qt::LeftDockWidgetArea, MENU_ENUM_LABEL_VALUE_QT_CORE, + coreSelectionWidget); coreSelectionDock->setFixedHeight(coreSelectionDock->minimumSizeHint().height()); - mainwindow->addDockWidget(static_cast( - coreSelectionDock->property("default_area").toInt()), coreSelectionDock); + qt_dock_add_to(mainwindow, coreSelectionDock); mainwindow->splitDockWidget(browserAndPlaylistTabDock, coreSelectionDock, Qt::Vertical); @@ -5047,7 +4887,12 @@ static void* ui_companion_qt_init(void) mainwindow->resizeDocks(QList() << coreSelectionDock, QList() << 1, Qt::Vertical); #endif +} +/* Restore persistent state (limits, geometry, theme, view type, last tab). */ +static void qt_companion_restore_settings(MainWindow *mainwindow, + QSettings *qsettings) +{ if (qsettings->contains("all_playlists_list_max_count")) mainwindow->setAllPlaylistsListMaxCount(qsettings->value( "all_playlists_list_max_count", 0).toInt()); @@ -5088,16 +4933,13 @@ static void* ui_companion_qt_init(void) if (qsettings->contains("theme")) { - QString themeStr = qsettings->value("theme").toString(); + QString themeStr = qsettings->value("theme").toString(); MainWindow::Theme theme = mainwindow->getThemeFromString(themeStr); if ( qsettings->contains("custom_theme") && theme == MainWindow::THEME_CUSTOM) - { - QString customThemeFilePath = - qsettings->value("custom_theme").toString(); - mainwindow->setCustomThemeFile(customThemeFilePath); - } + mainwindow->setCustomThemeFile( + qsettings->value("custom_theme").toString()); mainwindow->setTheme(theme); } @@ -5108,9 +4950,7 @@ static void* ui_companion_qt_init(void) { QString viewType = qsettings->value("view_type", "list").toString(); - if (viewType == QLatin1String("list")) - mainwindow->setCurrentViewType(MainWindow::VIEW_TYPE_LIST); - else if (viewType == QLatin1String("icons")) + if (viewType == QLatin1String("icons")) mainwindow->setCurrentViewType(MainWindow::VIEW_TYPE_ICONS); else mainwindow->setCurrentViewType(MainWindow::VIEW_TYPE_LIST); @@ -5123,9 +4963,7 @@ static void* ui_companion_qt_init(void) QString thumbnailType = qsettings->value("icon_view_thumbnail_type", "boxart").toString(); - if (thumbnailType == QLatin1String("boxart")) - mainwindow->setCurrentThumbnailType(THUMBNAIL_TYPE_BOXART); - else if (thumbnailType == QLatin1String("screenshot")) + if (thumbnailType == QLatin1String("screenshot")) mainwindow->setCurrentThumbnailType(THUMBNAIL_TYPE_SCREENSHOT); else if (thumbnailType == QLatin1String("title")) mainwindow->setCurrentThumbnailType(THUMBNAIL_TYPE_TITLE_SCREEN); @@ -5134,15 +4972,125 @@ static void* ui_companion_qt_init(void) else mainwindow->setCurrentThumbnailType(THUMBNAIL_TYPE_BOXART); } +} + +/* Set the initial playlist row to match the user's saved choice; if not + * found, fall back to the first non-hidden row. */ +static void qt_companion_select_initial_playlist(QListWidget *listWidget, + const QString &initialPlaylist) +{ + int i; + bool found = false; + + for (i = 0; i < listWidget->count(); i++) + { + QListWidgetItem *item = listWidget->item(i); + QString path; + + if (!item) + continue; + + path = item->data(Qt::UserRole).toString(); + + if (path == initialPlaylist) + { + found = true; + listWidget->setRowHidden(i, false); + listWidget->setCurrentRow(i); + break; + } + } + + if (found) + return; + + /* Couldn't find the user's initial playlist, just find anything. */ + for (i = 0; i < listWidget->count(); i++) + { + if (!listWidget->isRowHidden(i)) + { + listWidget->setCurrentRow(i); + break; + } + } +} + +static void* ui_companion_qt_init(void) +{ + QString initialPlaylist; + QRect desktopRect; + ui_companion_qt_t *handle = (ui_companion_qt_t*) + calloc(1, sizeof(*handle)); + MainWindow *mainwindow = NULL; + QScreen *screen = NULL; + QStackedWidget *centralWidget = NULL; + QStackedWidget *widget = NULL; + QTabWidget *browserAndPlaylistTabWidget = NULL; + QDockWidget *browserAndPlaylistTabDock = NULL; + QSettings *qsettings = NULL; + QListWidget *listWidget = NULL; + + if (!handle) + return NULL; + + handle->app = static_cast + (ui_application_qt.initialize()); + handle->window = static_cast(ui_window_qt.init()); + + screen = qApp->primaryScreen(); + desktopRect = screen->availableGeometry(); + + mainwindow = handle->window->qtWindow; + qsettings = mainwindow->settings(); + + initialPlaylist = qsettings->value("initial_playlist", + mainwindow->getSpecialPlaylistPath(SPECIAL_PLAYLIST_HISTORY)).toString(); + + mainwindow->resize(((desktopRect.width()) < (INITIAL_WIDTH) ? (desktopRect.width()) : (INITIAL_WIDTH)), + ((desktopRect.height()) < (INITIAL_HEIGHT) ? (desktopRect.height()) : (INITIAL_HEIGHT))); + mainwindow->setGeometry(QStyle::alignedRect(Qt::LeftToRight, + Qt::AlignCenter, mainwindow->size(), desktopRect)); + + mainwindow->setWindowTitle("RetroArch"); + mainwindow->setDockOptions(QMainWindow::AnimatedDocks + | QMainWindow::AllowNestedDocks + | QMainWindow::AllowTabbedDocks + | GROUPED_DRAGGING); + + listWidget = mainwindow->playlistListWidget(); + widget = mainwindow->playlistViews(); + widget->setContextMenuPolicy(Qt::CustomContextMenu); + + QObject::connect(widget, SIGNAL(filesDropped(QStringList)), + mainwindow, SLOT(onPlaylistFilesDropped(QStringList))); + QObject::connect(widget, SIGNAL(enterPressed()), mainwindow, + SLOT(onDropWidgetEnterPressed())); + QObject::connect(widget, SIGNAL(deletePressed()), mainwindow, + SLOT(deleteCurrentPlaylistItem())); + QObject::connect(widget, SIGNAL(customContextMenuRequested(const QPoint&)), + mainwindow, SLOT(onFileDropWidgetContextMenuRequested(const QPoint&))); + + centralWidget = mainwindow->centralWidget(); + centralWidget->addWidget(mainwindow->playlistViewsAndFooter()); + centralWidget->addWidget(mainwindow->fileTableView()); + mainwindow->setCentralWidget(centralWidget); + + qt_companion_build_menubar(mainwindow); + browserAndPlaylistTabDock = qt_companion_build_browser_dock(mainwindow); + qt_companion_build_thumbnail_docks(mainwindow); + qt_companion_build_core_selection_dock(mainwindow, + browserAndPlaylistTabDock); + qt_companion_restore_settings(mainwindow, qsettings); + + browserAndPlaylistTabWidget = mainwindow->browserAndPlaylistTabWidget(); /* We make sure to hook up the tab widget callback only after the tabs * themselves have been added, but before changing to a specific one, - * to avoid the callback firing before the view type is set. - */ + * to avoid the callback firing before the view type is set. */ QObject::connect(browserAndPlaylistTabWidget, SIGNAL(currentChanged(int)), mainwindow, SLOT(onTabWidgetIndexChanged(int))); - /* Setting the last tab must come after setting the view type */ + /* Setting the last tab must come after setting the view type. */ if (qsettings->contains("save_last_tab")) { int lastTabIndex = qsettings->value("last_tab", 0).toInt(); @@ -5160,40 +5108,7 @@ static void* ui_companion_qt_init(void) mainwindow->onTabWidgetIndexChanged(0); } - /* The initial playlist that is selected is based on - * the user's setting (initialPlaylist) */ - for (i = 0; listWidget->count() && i < listWidget->count(); i++) - { - QString path; - QListWidgetItem *item = listWidget->item(i); - - if (!item) - continue; - - path = item->data(Qt::UserRole).toString(); - - if (path == initialPlaylist) - { - foundPlaylist = true; - listWidget->setRowHidden(i, false); - listWidget->setCurrentRow(i); - break; - } - } - - /* couldn't find the user's initial playlist, just find anything */ - if (!foundPlaylist) - { - for (i = 0; listWidget->count() && i < listWidget->count(); i++) - { - /* select the first non-hidden row */ - if (!listWidget->isRowHidden(i)) - { - listWidget->setCurrentRow(i); - break; - } - } - } + qt_companion_select_initial_playlist(listWidget, initialPlaylist); mainwindow->initContentTableWidget(); @@ -5321,29 +5236,6 @@ ui_companion_driver_t ui_companion_qt = { "qt", }; -QStringList string_split_to_qt(QString str, char delim) -{ - int at = 0; - QStringList list = QStringList(); - - for (;;) - { - /* Find next split */ - int spl = str.indexOf(delim, at); - - /* Store split into list of extensions */ - list << str.mid(at, (spl < 0 ? -1 : spl - at)); - - /* No more splits */ - if (spl < 0) - break; - - at = spl + 1; - } - - return list; -} - #define CORE_NAME_COLUMN 0 #define CORE_VERSION_COLUMN 1 @@ -5463,22 +5355,13 @@ void LoadCoreWindow::loadCore(const char *path) void LoadCoreWindow::onCoreEnterPressed() { - QByteArray pathArray; - const char *pathData = NULL; QTableWidgetItem *selectedCoreItem = m_table->item(m_table->currentRow(), CORE_NAME_COLUMN); QVariantHash hash = selectedCoreItem->data( Qt::UserRole).toHash(); QString path = hash["path"].toString(); -#if (QT_VERSION > QT_VERSION_CHECK(6, 0, 0)) - pathArray.append(path.toStdString()); -#else - pathArray.append(path); -#endif - pathData = pathArray.constData(); - - loadCore(pathData); + loadCore(path.toUtf8().constData()); } void LoadCoreWindow::onLoadCustomCoreClicked() @@ -5547,8 +5430,7 @@ void LoadCoreWindow::initCoreList(const QStringList &extensionFilters) name_item = new QTableWidgetItem(name); hash["path"] = QByteArray(core->path); - hash["extensions"] = string_split_to_qt( - QString(core->supported_extensions), '|'); + hash["extensions"] = QString(core->supported_extensions).split('|'); name_item->setData(Qt::UserRole, hash); name_item->setFlags(name_item->flags() & ~Qt::ItemIsEditable); diff --git a/ui/drivers/ui_qt.h b/ui/drivers/ui_qt.h index 6f31a3a6859f..771e7e1df6f6 100644 --- a/ui/drivers/ui_qt.h +++ b/ui/drivers/ui_qt.h @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -37,7 +36,6 @@ #include #include #include -#include #include #include #include @@ -98,15 +96,7 @@ class QSlider; class QDragEnterEvent; class QDropEvent; class QProgressDialog; -class MainWindow; -class ThumbnailWidget; class ThumbnailLabel; -class GridView; -class ShaderParamsDialog; -class CoreOptionsDialog; -class CoreInfoDialog; -class PlaylistEntryDialog; -class ViewOptionsDialog; enum SpecialPlaylist { @@ -201,7 +191,7 @@ class PlaylistModel : public QAbstractListModel public: enum Roles { - HASH = Qt::UserRole + 1, + ENTRY = Qt::UserRole + 1, THUMBNAIL }; @@ -232,14 +222,14 @@ private slots: void onImageLoaded(const QImage image, const QModelIndex &index, const QString &path); private: - QVector > m_contents; + QVector m_contents; QCache m_cache; QSet m_pendingImages; QRegularExpression m_fileSanitizerRegex; ThumbnailType m_thumbnailType = THUMBNAIL_TYPE_BOXART; ThumbnailLoader *m_thumbnailLoader; QString getThumbnailPath(const QModelIndex &index, QString type) const; - QString getThumbnailPath(const QHash &hash, QString type) const; + QString getThumbnailPath(const PlaylistEntry &entry, QString type) const; QString getCurrentTypeThumbnailPath(const QModelIndex &index) const; void getPlaylistItems(QString path); }; @@ -277,7 +267,6 @@ public slots: void setPixmap(const QPixmap &pixmap); protected: void paintEvent(QPaintEvent *event); - void resizeEvent(QResizeEvent *event); private: void updateMargins(); @@ -294,7 +283,6 @@ class TreeView : public QTreeView signals: void itemsSelected(QModelIndexList selectedIndexes); protected slots: - void columnCountChanged(int oldCount, int newCount); void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); }; @@ -311,7 +299,6 @@ class ListWidget : public QListWidget Q_OBJECT public: ListWidget(QWidget *parent = 0); - bool isEditorOpen(); signals: void enterPressed(); void deletePressed(); @@ -327,9 +314,6 @@ class AppHandler : public QObject AppHandler(QObject *parent = 0); ~AppHandler(); void exit(); - -private slots: - void onLastWindowClosed(); }; class CoreInfoLabel : public QLabel @@ -386,6 +370,16 @@ class FileSystemProxyModel : public QSortFilterProxyModel protected: virtual bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; void sort(int column, Qt::SortOrder order = Qt::AscendingOrder); +private: + /* Cache for filterAcceptsRow's "is this row a child of the + * source model's current root?" check. The QFileSystemModel + * root path rarely changes, but filterAcceptsRow can fire + * thousands of times per directory load / sort / filter, and + * resolving rootPath() to a QModelIndex via sm->index(...) + * is not free. Compare the cached path string against the + * current one on each call; refresh both members on mismatch. */ + mutable QString m_cachedRootPath; + mutable QModelIndex m_cachedRootIndex; }; class LoadCoreTableWidget : public QTableWidget @@ -463,12 +457,9 @@ class MainWindow : public QMainWindow PlaylistModel* playlistModel(); ListWidget* playlistListWidget(); QStackedWidget* centralWidget(); - TableView* contentTableView(); QTableView* fileTableView(); FileDropWidget* playlistViews(); - GridView* contentGridView(); QWidget* playlistViewsAndFooter(); - QWidget* searchWidget(); QLineEdit* searchLineEdit(); QComboBox* launchWithComboBox(); QToolButton* startCorePushButton(); @@ -483,18 +474,18 @@ class MainWindow : public QMainWindow void setTheme(Theme theme = THEME_SYSTEM_DEFAULT); Theme theme(); Theme getThemeFromString(QString themeString); - QString getThemeString(Theme theme); - QHash getSelectedCore(); + const char *getThemeString(Theme theme); + QString getSelectedCorePath(); void showStatusMessage(QString msg, unsigned priority, unsigned duration, bool flush); bool showMessageBox(QString msg, MessageBoxType msgType = MSGBOX_TYPE_INFO, Qt::WindowModality modality = Qt::ApplicationModal, bool showDontAsk = true, bool *dontAsk = NULL); bool setCustomThemeFile(QString filePath); void setCustomThemeString(QString qss); const QString& customThemeString() const; void setCurrentViewType(ViewType viewType); - QString getCurrentViewTypeString(); + const char *getCurrentViewTypeString(); ViewType getCurrentViewType(); void setCurrentThumbnailType(ThumbnailType thumbnailType); - QString getCurrentThumbnailTypeString(); + const char *getCurrentThumbnailTypeString(); ThumbnailType getCurrentThumbnailType(); ThumbnailType getThumbnailTypeFromString(QString thumbnailType); void setAllPlaylistsListMaxCount(int count); @@ -504,31 +495,21 @@ class MainWindow : public QMainWindow void addFilesToPlaylist(QStringList files); QString getCurrentPlaylistPath(); QModelIndex getCurrentContentIndex(); - QHash getCurrentContentHash(); - QHash getFileContentHash(const QModelIndex &index); + PlaylistEntry getCurrentContentEntry(); + PlaylistEntry getFileContentEntry(const QModelIndex &index); QString getSpecialPlaylistPath(SpecialPlaylist playlist); QVector > getPlaylists(); - QString getScrubbedString(QString str); void setDefaultCustomProperties(); void setIconViewZoom(int zoomValue); signals: - void thumbnailChanged(const QPixmap &pixmap); - void thumbnail2Changed(const QPixmap &pixmap); - void thumbnail3Changed(const QPixmap &pixmap); - void thumbnail4Changed(const QPixmap &pixmap); void gotLogMessage(const QString &msg); void gotStatusMessage(QString msg, unsigned priority, unsigned duration, bool flush); void gotReloadPlaylists(); void gotReloadShaderParams(); - void gotReloadCoreOptions(); void showErrorMessageDeferred(QString msg); void showInfoMessageDeferred(QString msg); - void extractArchiveDeferred(QString path, QString extractionDir, QString tempExtension, retro_task_callback_t cb); void itemChanged(); - void updateThumbnails(); - void gridItemChanged(QString title); - void gotThumbnailDownload(QString system, QString title); void scrollToDownloads(QString path); void scrollToDownloadsAgain(QString path); @@ -541,15 +522,11 @@ public slots: void onShowHiddenDockWidgetAction(); void setCoreActions(); void onRunClicked(); - void loadContent(const QHash &contentHash); + void loadContent(const PlaylistEntry &entry); void onStartCoreClicked(); void onDropWidgetEnterPressed(); void selectBrowserDir(QString path); void setThumbnail(QString widgetName, QPixmap &pixmap, bool acceptDrop); - void onResizeThumbnailOne(QPixmap &pixmap, bool acceptDrop); - void onResizeThumbnailTwo(QPixmap &pixmap, bool acceptDrop); - void onResizeThumbnailThree(QPixmap &pixmap, bool acceptDrop); - void onResizeThumbnailFour(QPixmap &pixmap, bool acceptDrop); void appendLogMessage(const QString &msg); void onGotLogMessage(const QString &msg); void onGotStatusMessage(QString msg, unsigned priority, unsigned duration, bool flush); @@ -590,8 +567,9 @@ private slots: void onCurrentListItemChanged(QListWidgetItem *current, QListWidgetItem *previous); void onCurrentListItemDataChanged(QListWidgetItem *item); void onCurrentItemChanged(const QModelIndex &index); - void onCurrentItemChanged(const QHash &hash); + void onCurrentItemChanged(const PlaylistEntry &entry); void onCurrentFileChanged(const QModelIndex &index); + void onPreviewImageLoaded(const QImage image, const QModelIndex &index, const QString &path); void onSearchEnterPressed(); void onSearchLineEditEdited(const QString &text); void onContentItemDoubleClicked(const QModelIndex &index); @@ -630,7 +608,7 @@ private slots: private: void setCurrentCoreLabel(); void getPlaylistFiles(); - bool updateCurrentPlaylistEntry(const QHash &contentHash); + bool updateCurrentPlaylistEntry(const PlaylistEntry &entry); bool addDirectoryFilesToList(QProgressDialog *dialog, QStringList &list, QDir &dir, QStringList &extensions); void renamePlaylistItem(QListWidgetItem *item, QString newName); bool currentPlaylistIsSpecial(); @@ -638,6 +616,15 @@ private slots: void applySearch(); void updateItemsCount(); QString changeThumbnail(const QImage &image, QString type); + /* Constructor helpers - keep MainWindow::MainWindow readable by + * pulling self-contained chunks of setup out into their own + * methods. None take parameters; everything operates on already- + * initialised members. */ + void setupPlaylistFooter(); + void setupModels(); + void setupFileSystemBrowser(); + void setupDockWidgets(); + void setupSignalConnections(); PlaylistModel *m_playlistModel; QSortFilterProxyModel *m_proxyModel; @@ -666,10 +653,15 @@ private slots: QToolButton *m_stopPushButton; QTabWidget *m_browserAndPlaylistTabWidget; bool m_pendingRun; - QPixmap *m_thumbnailPixmap; - QPixmap *m_thumbnailPixmap2; - QPixmap *m_thumbnailPixmap3; - QPixmap *m_thumbnailPixmap4; + QPixmap *m_thumbnailPixmaps[4]; + /* Background loader for the file-browser preview pane. The + * preview is decoded asynchronously so that selecting a large + * image in the file table does not block the UI thread on a + * full-resolution PNG decode. m_pendingPreviewPath is the path + * we last requested; results for any other path are stale and + * dropped on arrival. */ + ThumbnailLoader *m_previewLoader; + QString m_pendingPreviewPath; QSettings *m_settings; ViewOptionsDialog *m_viewOptionsDialog; CoreInfoDialog *m_coreInfoDialog; @@ -694,7 +686,6 @@ private slots: ThumbnailType m_thumbnailType; QProgressBar *m_gridProgressBar; QWidget *m_gridProgressWidget; - QHash m_currentGridHash; QPointer m_currentGridWidget; int m_allPlaylistsListMaxCount; int m_allPlaylistsGridMaxCount; @@ -754,8 +745,6 @@ typedef struct ui_window_qt MainWindow *qtWindow; } ui_window_qt_t; -QStringList string_split_to_qt(QString str, char delim); - RETRO_END_DECLS #endif diff --git a/ui/drivers/ui_qt_widgets.cpp b/ui/drivers/ui_qt_widgets.cpp index fb26805e0c2a..b209747fe412 100644 --- a/ui/drivers/ui_qt_widgets.cpp +++ b/ui/drivers/ui_qt_widgets.cpp @@ -109,6 +109,18 @@ static inline QString sanitize_ampersand(QString input) return input.replace("&", "&&"); } +/* Replace characters unsafe in URLs / file names with '_' */ +static QString scrub_qstring(QString str) +{ + static const char chars[] = "&*/:`\"<>?\\|"; + QByteArray buf = str.toUtf8(); + char *s = buf.data(); + size_t i; + for (i = 0; i < sizeof(chars) - 1; i++) + string_replace_all_chars(s, chars[i], '_'); + return QString::fromUtf8(s); +} + static inline QString form_label(rarch_setting_t *setting) { return QString(sanitize_ampersand(setting->short_description)) + ":"; @@ -463,7 +475,7 @@ StringComboBox::StringComboBox(rarch_setting_t *setting, QWidget *parent) : ,m_setting(setting) ,m_value(setting->value.target.string) { - addItems(string_split_to_qt(QString(setting->values), '|')); + addItems(QString(setting->values).split('|')); connect(this, SIGNAL(currentTextChanged(const QString&)), this, SLOT(onCurrentTextChanged(const QString&))); @@ -1201,7 +1213,7 @@ void MainWindow::onFileDropWidgetContextMenuRequested(const QPoint &pos) QScopedPointer deleteAction; QPointer selectedAction; QPoint cursorPos = QCursor::pos(); - QHash contentHash = getCurrentContentHash(); + PlaylistEntry currentEntry = getCurrentContentEntry(); bool specialPlaylist = currentPlaylistIsSpecial(); bool allPlaylist = currentPlaylistIsAll(); bool actionsAdded = false; @@ -1230,7 +1242,7 @@ void MainWindow::onFileDropWidgetContextMenuRequested(const QPoint &pos) editAction.reset(new QAction(QString(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_EDIT)), this)); deleteAction.reset(new QAction(QString(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_DELETE)), this)); - if (!contentHash.isEmpty()) + if (!currentEntry.path.isEmpty()) { menu->addAction(editAction.data()); menu->addAction(deleteAction.data()); @@ -1249,9 +1261,9 @@ void MainWindow::onFileDropWidgetContextMenuRequested(const QPoint &pos) { if (selectedAction == downloadThumbnailAction.data()) { - QHash hash = getCurrentContentHash(); + PlaylistEntry entry = getCurrentContentEntry(); QString system = QFileInfo(getCurrentPlaylistPath()).completeBaseName(); - QString title = hash.value("label"); + QString title = entry.label; if (!title.isEmpty()) { @@ -1305,7 +1317,7 @@ void MainWindow::onFileDropWidgetContextMenuRequested(const QPoint &pos) PlaylistEntryDialog *playlistDialog = playlistEntryDialog(); QString currentPlaylistPath = getCurrentPlaylistPath(); - if (!playlistDialog->showDialog(contentHash)) + if (!playlistDialog->showDialog(currentEntry)) return; selectedName = m_playlistEntryDialog->getSelectedName(); @@ -1322,13 +1334,13 @@ void MainWindow::onFileDropWidgetContextMenuRequested(const QPoint &pos) if (selectedDatabase.isEmpty()) selectedDatabase = QFileInfo(currentPlaylistPath).fileName().remove(".lpl"); - contentHash["label"] = selectedName; - contentHash["path"] = selectedPath; - contentHash["core_name"] = selectedCore.value("core_name"); - contentHash["core_path"] = selectedCore.value("core_path"); - contentHash["db_name"] = selectedDatabase; + currentEntry.label = selectedName; + currentEntry.path = selectedPath; + currentEntry.coreName = selectedCore.value("core_name"); + currentEntry.corePath = selectedCore.value("core_path"); + currentEntry.dbName = selectedDatabase; - if (!updateCurrentPlaylistEntry(contentHash)) + if (!updateCurrentPlaylistEntry(currentEntry)) { showMessageBox(msg_hash_to_str( MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_UPDATE_PLAYLIST_ENTRY), @@ -1999,7 +2011,7 @@ void PlaylistEntryDialog::loadPlaylistOptions() QString ui_display_name; QHash hash; const core_info_t *core = &core_info_list->list[i]; - QStringList databases = string_split_to_qt(QString(core->databases), '|'); + QStringList databases = QString(core->databases).split('|'); hash["core_name"] = core->core_name; hash["core_display_name"] = core->display_name; @@ -2058,16 +2070,16 @@ bool PlaylistEntryDialog::nameFieldEnabled() } void PlaylistEntryDialog::setEntryValues( - const QHash &contentHash) + const PlaylistEntry &entry) { QString db; - QString coreName = contentHash.value("core_name"); + const QString &coreName = entry.coreName; int foundDB = 0; int i = 0; loadPlaylistOptions(); - if (contentHash.isEmpty()) + if (entry.path.isEmpty()) { m_nameLineEdit->setText( msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_FIELD_MULTIPLE)); @@ -2078,8 +2090,8 @@ void PlaylistEntryDialog::setEntryValues( } else { - m_nameLineEdit->setText(contentHash.value("label")); - m_pathLineEdit->setText(contentHash.value("path")); + m_nameLineEdit->setText(entry.label); + m_pathLineEdit->setText(entry.path); m_nameLineEdit->setEnabled(true); m_pathLineEdit->setEnabled(true); } @@ -2100,7 +2112,7 @@ void PlaylistEntryDialog::setEntryValues( } } - db = contentHash.value("db_name"); + db = entry.dbName; if (!db.isEmpty()) { @@ -2139,7 +2151,7 @@ const QStringList PlaylistEntryDialog::getSelectedExtensions() /* Otherwise it would create a QStringList with a single blank entry... */ if (!text.isEmpty()) - list = string_split_to_qt(text, ' '); + list = text.split(' '); return list; } @@ -2147,10 +2159,10 @@ void PlaylistEntryDialog::onAccepted() { } void PlaylistEntryDialog::onRejected() { } void PlaylistEntryDialog::hideDialog() { reject(); } -bool PlaylistEntryDialog::showDialog(const QHash &hash) +bool PlaylistEntryDialog::showDialog(const PlaylistEntry &entry) { loadPlaylistOptions(); - setEntryValues(hash); + setEntryValues(entry); return (exec() == QDialog::Accepted); } @@ -2910,8 +2922,6 @@ void CoreOptionsDialog::buildLayout() if (!contentLabel.isEmpty()) { - uint32_t flags = runloop_st->flags; - if (!label.isEmpty()) { QHBoxLayout *gameOptionsLayout = new QHBoxLayout(); @@ -5156,7 +5166,7 @@ void MainWindow::downloadThumbnail(QString system, QString title, QUrl url) if (!settings || m_pendingThumbnailDownloadTypes.isEmpty()) return; - title = getScrubbedString(title); + title = scrub_qstring(title); downloadType = m_pendingThumbnailDownloadTypes.takeFirst(); urlString = QString(THUMBNAIL_URL_HEADER) + system + "/" + downloadType + "/" + title + THUMBNAIL_IMAGE_EXTENSION; @@ -5305,7 +5315,7 @@ void MainWindow::downloadNextPlaylistThumbnail( if (!settings) return; - title = getScrubbedString(title); + title = scrub_qstring(title); urlString = QString(THUMBNAIL_URL_HEADER) + system + "/" + type + "/" + title + THUMBNAIL_IMAGE_EXTENSION; @@ -5411,12 +5421,12 @@ void MainWindow::downloadPlaylistThumbnails(QString playlistPath) QHash hash2; QHash hash3; QHash hash4; - const QHash &itemHash = + const PlaylistEntry itemEntry = m_playlistModel->index(i, 0).data( - PlaylistModel::HASH).value< QHash >(); + PlaylistModel::ENTRY).value(); - hash["db_name"] = itemHash.value("db_name"); - hash["label_noext"] = itemHash.value("label_noext"); + hash["db_name"] = itemEntry.dbName; + hash["label_noext"] = itemEntry.labelNoExt; hash["type"] = THUMBNAIL_BOXART; hash2 = hash; @@ -5566,7 +5576,8 @@ QVector AudioCategory::pages() QVector pages; pages << new AudioPage(this); - pages << new MenuSoundsPage(this); + pages << new SimplePage(DISPLAYLIST_MENU_SOUNDS_LIST, + MENU_ENUM_LABEL_VALUE_MENU_SOUNDS, this); return pages; } @@ -5643,17 +5654,6 @@ QWidget *AudioPage::widget() return widget; } -MenuSoundsPage::MenuSoundsPage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_MENU_SOUNDS); -} - -QWidget *MenuSoundsPage::widget() -{ - return create_widget(DISPLAYLIST_MENU_SOUNDS_LIST); -} - InputCategory::InputCategory(QWidget *parent) : OptionsCategory(parent) { @@ -5857,7 +5857,8 @@ QVector NetworkCategory::pages() QVector pages; pages << new NetplayPage(this); - pages << new UpdaterPage(this); + pages << new SimplePage(DISPLAYLIST_UPDATER_SETTINGS_LIST, + MENU_ENUM_LABEL_VALUE_UPDATER_SETTINGS, this); return pages; } @@ -6011,17 +6012,6 @@ void NetplayPage::onRadioButtonClicked(int id) #endif } -UpdaterPage::UpdaterPage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_UPDATER_SETTINGS); -} - -QWidget *UpdaterPage::widget() -{ - return create_widget(DISPLAYLIST_UPDATER_SETTINGS_LIST); -} - OnscreenDisplayCategory::OnscreenDisplayCategory(QWidget *parent) : OptionsCategory(parent) { @@ -6429,7 +6419,9 @@ QWidget *ViewsPage::widget() FormLayout *leftLayout = new FormLayout; QVBoxLayout *rightLayout = new QVBoxLayout; SettingsGroup *quickMenu = new SettingsGroup("Quick Menu"); - QuickMenuPage *quickPage = new QuickMenuPage(this); + OptionsPage *quickPage = new SimplePage( + DISPLAYLIST_QUICK_MENU_VIEWS_SETTINGS_LIST, + MENU_ENUM_LABEL_VALUE_QUICK_MENU_VIEWS_SETTINGS, this); SettingsGroup *mainMenu = new SettingsGroup("Main Menu"); SettingsGroup *settings = new SettingsGroup("Settings"); SettingsGroup *tabs = new SettingsGroup("Tabs"); @@ -6531,17 +6523,6 @@ QWidget *ViewsPage::widget() return widget; } -QuickMenuPage::QuickMenuPage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_QUICK_MENU_VIEWS_SETTINGS); -} - -QWidget *QuickMenuPage::widget() -{ - return create_widget(DISPLAYLIST_QUICK_MENU_VIEWS_SETTINGS_LIST); -} - AppearancePage::AppearancePage(QObject *parent) : OptionsPage(parent) { @@ -7065,23 +7046,11 @@ DriversCategory::DriversCategory(QWidget *parent) : QVector DriversCategory::pages() { QVector pages; - - pages << new DriversPage(this); - + pages << new SimplePage(DISPLAYLIST_DRIVER_SETTINGS_LIST, + MENU_ENUM_LABEL_VALUE_DRIVER_SETTINGS, this); return pages; } -DriversPage::DriversPage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_DRIVER_SETTINGS); -} - -QWidget *DriversPage::widget() -{ - return create_widget(DISPLAYLIST_DRIVER_SETTINGS_LIST); -} - /* DIRECTORY */ DirectoryCategory::DirectoryCategory(QWidget *parent) : @@ -7094,22 +7063,10 @@ DirectoryCategory::DirectoryCategory(QWidget *parent) : QVector DirectoryCategory::pages() { QVector pages; - - pages << new DirectoryPage(this); - + pages << new SimplePage(DISPLAYLIST_DIRECTORY_SETTINGS_LIST, this); return pages; } -DirectoryPage::DirectoryPage(QObject *parent) : - OptionsPage(parent) -{ -} - -QWidget *DirectoryPage::widget() -{ - return create_widget(DISPLAYLIST_DIRECTORY_SETTINGS_LIST); -} - /* CONFIGURATION */ ConfigurationCategory::ConfigurationCategory(QWidget *parent) : @@ -7122,22 +7079,10 @@ ConfigurationCategory::ConfigurationCategory(QWidget *parent) : QVector ConfigurationCategory::pages() { QVector pages; - - pages << new ConfigurationPage(this); - + pages << new SimplePage(DISPLAYLIST_CONFIGURATION_SETTINGS_LIST, this); return pages; } -ConfigurationPage::ConfigurationPage(QObject *parent) : - OptionsPage(parent) -{ -} - -QWidget *ConfigurationPage::widget() -{ - return create_widget(DISPLAYLIST_CONFIGURATION_SETTINGS_LIST); -} - /* CORE */ CoreCategory::CoreCategory(QWidget *parent) : @@ -7150,22 +7095,10 @@ CoreCategory::CoreCategory(QWidget *parent) : QVector CoreCategory::pages() { QVector pages; - - pages << new CorePage(this); - + pages << new SimplePage(DISPLAYLIST_CORE_SETTINGS_LIST, this); return pages; } -CorePage::CorePage(QObject *parent) : - OptionsPage(parent) -{ -} - -QWidget *CorePage::widget() -{ - return create_widget(DISPLAYLIST_CORE_SETTINGS_LIST); -} - /* LOGGING */ LoggingCategory::LoggingCategory(QWidget *parent) : @@ -7178,18 +7111,10 @@ LoggingCategory::LoggingCategory(QWidget *parent) : QVector LoggingCategory::pages() { QVector pages; - pages << new LoggingPage(this); + pages << new SimplePage(DISPLAYLIST_LOGGING_SETTINGS_LIST, this); return pages; } -LoggingPage::LoggingPage(QObject *parent) : - OptionsPage(parent) { } - -QWidget *LoggingPage::widget() -{ - return create_widget(DISPLAYLIST_LOGGING_SETTINGS_LIST); -} - /* AI SERVICE */ AIServiceCategory::AIServiceCategory(QWidget *parent) : @@ -7202,23 +7127,11 @@ AIServiceCategory::AIServiceCategory(QWidget *parent) : QVector AIServiceCategory::pages() { QVector pages; - - pages << new AIServicePage(this); - + pages << new SimplePage(DISPLAYLIST_AI_SERVICE_SETTINGS_LIST, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_SETTINGS, this); return pages; } -AIServicePage::AIServicePage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_AI_SERVICE_SETTINGS); -} - -QWidget *AIServicePage::widget() -{ - return create_widget(DISPLAYLIST_AI_SERVICE_SETTINGS_LIST); -} - /* FRAME THROTTLE */ FrameThrottleCategory::FrameThrottleCategory(QWidget *parent) : @@ -7231,35 +7144,13 @@ FrameThrottleCategory::FrameThrottleCategory(QWidget *parent) : QVector FrameThrottleCategory::pages() { QVector pages; - - pages << new FrameThrottlePage(this); - pages << new RewindPage(this); - + pages << new SimplePage(DISPLAYLIST_FRAME_THROTTLE_SETTINGS_LIST, + MENU_ENUM_LABEL_VALUE_FRAME_THROTTLE_SETTINGS, this); + pages << new SimplePage(DISPLAYLIST_REWIND_SETTINGS_LIST, + MENU_ENUM_LABEL_VALUE_REWIND_SETTINGS, this); return pages; } -FrameThrottlePage::FrameThrottlePage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_FRAME_THROTTLE_SETTINGS); -} - -QWidget *FrameThrottlePage::widget() -{ - return create_widget(DISPLAYLIST_FRAME_THROTTLE_SETTINGS_LIST); -} - -RewindPage::RewindPage(QObject *parent) : - OptionsPage(parent) -{ - setDisplayName(MENU_ENUM_LABEL_VALUE_REWIND_SETTINGS); -} - -QWidget *RewindPage::widget() -{ - return create_widget(DISPLAYLIST_REWIND_SETTINGS_LIST); -} - PlaylistModel::PlaylistModel(QObject *parent) : QAbstractListModel(parent) { @@ -7293,8 +7184,8 @@ QVariant PlaylistModel::data(const QModelIndex &index, int role) const case Qt::DisplayRole: case Qt::EditRole: case Qt::ToolTipRole: - return m_contents.at(index.row())["label_noext"]; - case HASH: + return m_contents.at(index.row()).labelNoExt; + case ENTRY: return QVariant::fromValue(m_contents.at(index.row())); case THUMBNAIL: { @@ -7320,12 +7211,12 @@ bool PlaylistModel::setData(const QModelIndex &index, const QVariant &value, int { if (index.isValid() && role == Qt::EditRole) { - QHash hash = m_contents.at(index.row()); + PlaylistEntry rowEntry = m_contents.at(index.row()); - hash["label"] = value.toString(); - hash["label_noext"] = QFileInfo(value.toString()).completeBaseName(); + rowEntry.label = value.toString(); + rowEntry.labelNoExt = QFileInfo(value.toString()).completeBaseName(); - m_contents.replace(index.row(), hash); + m_contents.replace(index.row(), rowEntry); emit dataChanged(index, index, { role }); return true; } @@ -7392,18 +7283,18 @@ QString PlaylistModel::getSanitizedThumbnailName(QString dir, QString label) con } -QString PlaylistModel::getThumbnailPath(const QHash &hash, QString type) const +QString PlaylistModel::getThumbnailPath(const PlaylistEntry &entry, QString type) const { /* use thumbnail widgets to show regular image files */ - if (isSupportedImage(hash["path"])) - return hash["path"]; + if (isSupportedImage(entry.path)) + return entry.path; return getSanitizedThumbnailName( - getPlaylistThumbnailsDir(hash.value("db_name")) + getPlaylistThumbnailsDir(entry.dbName) + QString("/") + type + QString("/"), - hash["label_noext"]); + entry.labelNoExt); } QString PlaylistModel::getCurrentTypeThumbnailPath(const QModelIndex &index) const @@ -7570,7 +7461,8 @@ bool MainWindow::addDirectoryFilesToList(QProgressDialog *dialog, * inside the archive * doesn't have one of the chosen extensions, * then we skip it. */ - if (extensions.contains(QFileInfo(pathData).suffix())) + if (extensions.contains( + QString::fromUtf8(path_get_extension(pathData)))) add = true; } { @@ -7579,7 +7471,8 @@ bool MainWindow::addDirectoryFilesToList(QProgressDialog *dialog, * inside the archive * doesn't have one of the chosen extensions, * then we skip it. */ - if (extensions.contains(QFileInfo(pathData).suffix())) + if (extensions.contains( + QString::fromUtf8(path_get_extension(pathData)))) add = true; } } @@ -7614,7 +7507,7 @@ void MainWindow::addFilesToPlaylist(QStringList files) QByteArray currentPlaylistArray; QScopedPointer dialog(NULL); QHash selectedCore; - QHash itemToAdd; + PlaylistEntry itemToAdd; QString selectedDatabase; QString selectedName; QString selectedPath; @@ -7635,10 +7528,8 @@ void MainWindow::addFilesToPlaylist(QStringList files) /* Assume a blank list means we will manually enter in all fields. */ if (files.isEmpty()) { - /* Make sure hash isn't blank, that would mean there's - * multiple entries to add at once. */ - itemToAdd["label"] = QLatin1String(""); - itemToAdd["path"] = QLatin1String(""); + /* Leave fields default-empty: the dialog will offer the + * "Multiple" placeholder when it sees an empty path. */ } else if (files.count() == 1) { @@ -7647,8 +7538,8 @@ void MainWindow::addFilesToPlaylist(QStringList files) if (info.isFile()) { - itemToAdd["label"] = info.completeBaseName(); - itemToAdd["path"] = path; + itemToAdd.label = info.completeBaseName(); + itemToAdd.path = path; } } @@ -7877,7 +7768,7 @@ void MainWindow::addFilesToPlaylist(QStringList files) * doesn't have one of the chosen extensions, * then we skip it. */ if (!selectedExtensions.contains( - QFileInfo(pathData).suffix())) + QString::fromUtf8(path_get_extension(pathData)))) { string_list_free(list); continue; @@ -7912,14 +7803,8 @@ void MainWindow::addFilesToPlaylist(QStringList files) } bool MainWindow::updateCurrentPlaylistEntry( - const QHash &contentHash) -{ - QString path; - QString label; - QString corePath; - QString coreName; - QString dbName; - QString crc32; + const PlaylistEntry &contentEntry) +{ QByteArray playlistPathArray; QByteArray pathArray; QByteArray labelArray; @@ -7937,8 +7822,6 @@ bool MainWindow::updateCurrentPlaylistEntry( const char *dbNameData = NULL; const char *crc32Data = NULL; playlist_t *playlist = NULL; - unsigned index = 0; - bool ok = false; settings_t *settings = config_get_ptr(); playlist_config.capacity = COLLECTION_SIZE; @@ -7949,39 +7832,22 @@ bool MainWindow::updateCurrentPlaylistEntry( settings->bools.playlist_portable_paths ? settings->paths.directory_menu_content : NULL); - if ( playlistPath.isEmpty() - || contentHash.isEmpty() - || !contentHash.contains("index")) - return false; - - index = contentHash.value("index").toUInt(&ok); - - if (!ok) - return false; - - path = contentHash.value("path"); - label = contentHash.value("label"); - coreName = contentHash.value("core_name"); - corePath = contentHash.value("core_path"); - dbName = contentHash.value("db_name"); - crc32 = contentHash.value("crc32"); - - if ( path.isEmpty() - || label.isEmpty() - || coreName.isEmpty() - || corePath.isEmpty() - ) + if ( playlistPath.isEmpty() + || contentEntry.path.isEmpty() + || contentEntry.label.isEmpty() + || contentEntry.coreName.isEmpty() + || contentEntry.corePath.isEmpty()) return false; playlistPathArray = playlistPath.toUtf8(); - pathArray = QDir::toNativeSeparators(path).toUtf8(); - labelArray = label.toUtf8(); - coreNameArray = coreName.toUtf8(); - corePathArray = QDir::toNativeSeparators(corePath).toUtf8(); + pathArray = QDir::toNativeSeparators(contentEntry.path).toUtf8(); + labelArray = contentEntry.label.toUtf8(); + coreNameArray = contentEntry.coreName.toUtf8(); + corePathArray = QDir::toNativeSeparators(contentEntry.corePath).toUtf8(); - if (!dbName.isEmpty()) + if (!contentEntry.dbName.isEmpty()) { - dbNameArray = (dbName + ".lpl").toUtf8(); + dbNameArray = (contentEntry.dbName + ".lpl").toUtf8(); dbNameData = dbNameArray.constData(); } @@ -7991,9 +7857,9 @@ bool MainWindow::updateCurrentPlaylistEntry( coreNameData = coreNameArray.constData(); corePathData = corePathArray.constData(); - if (!crc32.isEmpty()) + if (!contentEntry.crc32.isEmpty()) { - crc32Array = crc32.toUtf8(); + crc32Array = contentEntry.crc32.toUtf8(); crc32Data = crc32Array.constData(); } @@ -8031,7 +7897,7 @@ bool MainWindow::updateCurrentPlaylistEntry( entry.crc32 = const_cast(crc32Data); entry.db_name = const_cast(dbNameData); - playlist_update(playlist, index, &entry); + playlist_update(playlist, contentEntry.index, &entry); } playlist_write_file(playlist); @@ -8129,13 +7995,8 @@ void MainWindow::onPlaylistWidgetContextMenuRequested(const QPoint&) for (j = 0; j < m_listWidget->count(); j++) { QListWidgetItem *item = m_listWidget->item(j); -#if (QT_VERSION > QT_VERSION_CHECK(6, 0, 0)) - bool hidden = item->isHidden(); -#else - bool hidden = m_listWidget->isItemHidden(item); -#endif - if (hidden) + if (item->isHidden()) { QAction *action = hiddenPlaylistsMenu->addAction(item->text()); action->setProperty("row", j); @@ -8619,11 +8480,9 @@ void MainWindow::deleteCurrentPlaylistItem() QByteArray playlistArray; playlist_config_t playlist_config; QString playlistPath = getCurrentPlaylistPath(); - QHash contentHash = getCurrentContentHash(); + PlaylistEntry contentEntry = getCurrentContentEntry(); playlist_t *playlist = NULL; const char *playlistData = NULL; - unsigned index = 0; - bool ok = false; bool isAllPlaylist = currentPlaylistIsAll(); settings_t *settings = config_get_ptr(); @@ -8641,24 +8500,19 @@ void MainWindow::deleteCurrentPlaylistItem() if (playlistPath.isEmpty()) return; - if (contentHash.isEmpty()) + if (contentEntry.path.isEmpty()) return; playlistArray = playlistPath.toUtf8(); playlistData = playlistArray.constData(); - index = contentHash.value("index").toUInt(&ok); - - if (!ok) - return; - - if (!showMessageBox(QString(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CONFIRM_DELETE_PLAYLIST_ITEM)).arg(contentHash["label"]), MainWindow::MSGBOX_TYPE_QUESTION_YESNO, Qt::ApplicationModal, false)) + if (!showMessageBox(QString(msg_hash_to_str(MENU_ENUM_LABEL_VALUE_QT_CONFIRM_DELETE_PLAYLIST_ITEM)).arg(contentEntry.label), MainWindow::MSGBOX_TYPE_QUESTION_YESNO, Qt::ApplicationModal, false)) return; playlist_config_set_path(&playlist_config, playlistData); playlist = playlist_init(&playlist_config); - playlist_delete_index(playlist, index); + playlist_delete_index(playlist, contentEntry.index); playlist_write_file(playlist); playlist_free(playlist); @@ -8769,50 +8623,50 @@ void PlaylistModel::getPlaylistItems(QString path) for (i = 0; i < playlistSize; i++) { - QHash hash; - const struct playlist_entry *entry = NULL; + PlaylistEntry rowEntry; + const struct playlist_entry *pl_row = NULL; - playlist_get_index(playlist, i, &entry); + playlist_get_index(playlist, i, &pl_row); - if (!entry->path || !*entry->path) + if (!pl_row->path || !*pl_row->path) continue; - hash["path"] = entry->path; - hash["index"] = QString::number(i); + rowEntry.path = pl_row->path; + rowEntry.index = (unsigned)i; - if (!entry->label || !*entry->label) + if (!pl_row->label || !*pl_row->label) { - hash["label"] = entry->path; - hash["label_noext"] = entry->path; + rowEntry.label = pl_row->path; + rowEntry.labelNoExt = pl_row->path; } else { - hash["label"] = entry->label; - hash["label_noext"] = entry->label; + rowEntry.label = pl_row->label; + rowEntry.labelNoExt = pl_row->label; } - if (entry->core_path && *entry->core_path) - hash["core_path"] = entry->core_path; + if (pl_row->core_path && *pl_row->core_path) + rowEntry.corePath = pl_row->core_path; - if (entry->core_name && *entry->core_name) - hash["core_name"] = entry->core_name; + if (pl_row->core_name && *pl_row->core_name) + rowEntry.coreName = pl_row->core_name; - if (entry->crc32 && *entry->crc32) - hash["crc32"] = entry->crc32; + if (pl_row->crc32 && *pl_row->crc32) + rowEntry.crc32 = pl_row->crc32; - if (entry->db_name && *entry->db_name) + if (pl_row->db_name && *pl_row->db_name) { - hash["db_name"] = entry->db_name; - hash["db_name"].remove(".lpl"); + rowEntry.dbName = pl_row->db_name; + rowEntry.dbName.remove(".lpl"); } if (playlistName && *playlistName) { - hash["pl_name"] = playlistName; - hash["pl_name"].remove(".lpl"); + rowEntry.plName = playlistName; + rowEntry.plName.remove(".lpl"); } - m_contents.append(hash); + m_contents.append(rowEntry); } playlist_free(playlist); @@ -8856,18 +8710,18 @@ void PlaylistModel::addDir(QString path, QFlags showHidden) for (i = 0; i < dirList.count(); i++) { - QHash hash; + PlaylistEntry rowEntry; QString fileName = dirList.at(i); QString filePath( QDir::toNativeSeparators(dir.absoluteFilePath(fileName))); QFileInfo fileInfo(filePath); - hash["path"] = filePath; - hash["label"] = hash["path"]; - hash["label_noext"] = fileInfo.completeBaseName(); - hash["db_name"] = fileInfo.dir().dirName(); + rowEntry.path = filePath; + rowEntry.label = filePath; + rowEntry.labelNoExt = fileInfo.completeBaseName(); + rowEntry.dbName = fileInfo.dir().dirName(); - m_contents.append(hash); + m_contents.append(rowEntry); } endResetModel(); diff --git a/ui/drivers/ui_qt_widgets.h b/ui/drivers/ui_qt_widgets.h index 15550426aefa..e3eec32277f0 100644 --- a/ui/drivers/ui_qt_widgets.h +++ b/ui/drivers/ui_qt_widgets.h @@ -18,8 +18,8 @@ #include #include #include +#include #include -#include #include #include @@ -57,6 +57,27 @@ class QListWidget; class QStackedLayout; #endif +/* Content schema describing a single playlist row. Replaces the + * QHash previously stored in PlaylistModel and + * passed around as the playlist row payload. The field set is the + * same keys the QHash carried; making it a struct removes a class + * of stringly-typed lookup bugs and a lot of toUtf8/from-key + * round trips at call sites. */ +struct PlaylistEntry +{ + QString path; + QString label; + QString labelNoExt; + QString corePath; + QString coreName; + QString crc32; + QString dbName; + QString plName; + QString plPath; + unsigned index = 0; +}; +Q_DECLARE_METATYPE(PlaylistEntry) + class FormLayout : public QFormLayout { public: @@ -841,9 +862,9 @@ class PlaylistEntryDialog : public QDialog const QStringList getSelectedExtensions(); bool filterInArchive(); bool nameFieldEnabled(); - void setEntryValues(const QHash &contentHash); + void setEntryValues(const PlaylistEntry &entry); public slots: - bool showDialog(const QHash &hash = QHash()); + bool showDialog(const PlaylistEntry &entry = PlaylistEntry()); void hideDialog(); void onAccepted(); void onRejected(); @@ -895,6 +916,28 @@ class OptionsPage : public QObject QString m_displayName = "General"; }; +/* SimplePage: + * Trivial OptionsPage subclass whose widget() is a one-shot + * create_widget(displaylist) call. Consolidates ~11 near-identical + * leaf Page classes that only differ in display name and displaylist + * enum. Use this for any page that does not need custom layout, + * custom slots, or its own load()/apply(). */ +class SimplePage : public OptionsPage +{ + Q_OBJECT +public: + SimplePage(enum menu_displaylist_ctl_state state, + QObject *parent = nullptr) + : OptionsPage(parent), m_state(state) {} + SimplePage(enum menu_displaylist_ctl_state state, + msg_hash_enums name, + QObject *parent = nullptr) + : OptionsPage(parent), m_state(state) { setDisplayName(name); } + QWidget *widget(); /* defined inline at end of header, after create_widget() */ +private: + enum menu_displaylist_ctl_state m_state; +}; + class OptionsCategory : public QObject { Q_OBJECT @@ -936,14 +979,6 @@ class DriversCategory : public OptionsCategory QVector pages(); }; -class DriversPage : public OptionsPage -{ - Q_OBJECT -public: - DriversPage(QObject *parent = nullptr); - QWidget *widget(); -}; - /*********************************************************** AI Service ************************************************************/ @@ -954,14 +989,6 @@ class AIServiceCategory : public OptionsCategory QVector pages(); }; -class AIServicePage : public OptionsPage -{ - Q_OBJECT -public: - AIServicePage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Video ************************************************************/ @@ -1039,14 +1066,6 @@ class AudioPage : public OptionsPage QWidget *widget(); }; -class MenuSoundsPage : public OptionsPage -{ - Q_OBJECT -public: - MenuSoundsPage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Input ************************************************************/ @@ -1109,14 +1128,6 @@ class CoreCategory : public OptionsCategory QVector pages(); }; -class CorePage : public OptionsPage -{ - Q_OBJECT -public: - CorePage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Configuration ************************************************************/ @@ -1127,14 +1138,6 @@ class ConfigurationCategory : public OptionsCategory QVector pages(); }; -class ConfigurationPage : public OptionsPage -{ - Q_OBJECT -public: - ConfigurationPage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Saving ************************************************************/ @@ -1163,14 +1166,6 @@ class LoggingCategory : public OptionsCategory QVector pages(); }; -class LoggingPage : public OptionsPage -{ - Q_OBJECT -public: - LoggingPage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Frame Throttle ************************************************************/ @@ -1181,22 +1176,6 @@ class FrameThrottleCategory : public OptionsCategory QVector pages(); }; -class FrameThrottlePage : public OptionsPage -{ - Q_OBJECT -public: - FrameThrottlePage(QObject *parent = nullptr); - QWidget *widget(); -}; - -class RewindPage : public OptionsPage -{ - Q_OBJECT -public: - RewindPage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Recording ************************************************************/ @@ -1244,14 +1223,6 @@ class ViewsPage : public OptionsPage QWidget *widget(); }; -class QuickMenuPage : public OptionsPage -{ - Q_OBJECT -public: - QuickMenuPage(QObject *parent = nullptr); - QWidget *widget(); -}; - class AppearancePage : public OptionsPage { Q_OBJECT @@ -1344,14 +1315,6 @@ private slots: QGroupBox* createMitmServerGroup(); }; -class UpdaterPage : public OptionsPage -{ - Q_OBJECT -public: - UpdaterPage(QObject *parent = nullptr); - QWidget *widget(); -}; - /************************************************************ Playlists ************************************************************/ @@ -1406,14 +1369,6 @@ class DirectoryCategory : public OptionsCategory QVector pages(); }; -class DirectoryPage : public OptionsPage -{ - Q_OBJECT -public: - DirectoryPage(QObject *parent = nullptr); - QWidget *widget(); -}; - static inline QWidget *create_widget(enum menu_displaylist_ctl_state name) { unsigned i; @@ -1439,4 +1394,9 @@ static inline QWidget *create_widget(enum menu_displaylist_ctl_state name) return widget; } +inline QWidget *SimplePage::widget() +{ + return create_widget(m_state); +} + #endif