From a184a04e5624ef078476a5722bb340a8fcef6d1b Mon Sep 17 00:00:00 2001 From: neebyA <126654084+neebyA@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:42:49 -0700 Subject: [PATCH] macOS: Minor UI improvements (#1575) --- src/gui/MainWindow.cpp | 21 ++++ src/gui/components/wxGameList.cpp | 183 +++++++++++++++++++++++++++++- src/gui/components/wxGameList.h | 2 - 3 files changed, 200 insertions(+), 6 deletions(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index b032a357..882c6eab 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -91,6 +91,7 @@ enum MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MAINFRAME_MENU_ID_OPTIONS_AUDIO, MAINFRAME_MENU_ID_OPTIONS_INPUT, + MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, // options -> account MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 = 20350, MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_12 = 20350 + 11, @@ -187,6 +188,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_AUDIO, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput) +EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, MainWindow::OnOptionsInput) // tools menu EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput) @@ -288,6 +290,11 @@ private: MainWindow::MainWindow() : wxFrame(nullptr, wxID_ANY, GetInitialWindowTitle(), wxDefaultPosition, wxSize(1280, 720), wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxCLIP_CHILDREN | wxRESIZE_BORDER) { +#ifdef __WXMAC__ + // Not necessary to set wxApp::s_macExitMenuItemId as automatically handled + wxApp::s_macAboutMenuItemId = MAINFRAME_MENU_ID_HELP_ABOUT; + wxApp::s_macPreferencesMenuItemId = MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS; +#endif gui_initHandleContextFromWxWidgetsWindow(g_window_info.window_main, this); g_mainFrame = this; CafeSystem::SetImplementation(this); @@ -911,6 +918,7 @@ void MainWindow::OnOptionsInput(wxCommandEvent& event) break; } + case MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS: case MAINFRAME_MENU_ID_OPTIONS_GENERAL2: { OpenSettings(); @@ -1940,6 +1948,16 @@ public: lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")"), 0); sizer->Add(lineSizer); } +#if BOOST_OS_MACOS + // MoltenVK + { + wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); + lineSizer->Add(new wxStaticText(parent, -1, "MoltenVK ("), 0); + lineSizer->Add(new wxHyperlinkCtrl(parent, -1, "https://github.com/KhronosGroup/MoltenVK", "https://github.com/KhronosGroup/MoltenVK"), 0); + lineSizer->Add(new wxStaticText(parent, -1, ")"), 0); + sizer->Add(lineSizer); + } +#endif // icons { wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); @@ -2165,6 +2183,9 @@ void MainWindow::RecreateMenu() m_padViewMenuItem = optionsMenu->AppendCheckItem(MAINFRAME_MENU_ID_OPTIONS_SECOND_WINDOW_PADVIEW, _("&Separate GamePad view"), wxEmptyString); m_padViewMenuItem->Check(GetConfig().pad_open); optionsMenu->AppendSeparator(); + #if BOOST_OS_MACOS + optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, _("&Settings..." "\tCtrl-,")); + #endif optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, _("&General settings")); optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_INPUT, _("&Input settings")); diff --git a/src/gui/components/wxGameList.cpp b/src/gui/components/wxGameList.cpp index e418ca0a..95711fef 100644 --- a/src/gui/components/wxGameList.cpp +++ b/src/gui/components/wxGameList.cpp @@ -82,6 +82,56 @@ std::list _getCachesPaths(const TitleId& titleId) return cachePaths; } +// Convert PNG to Apple icon image format +bool writeICNS(const fs::path& pngPath, const fs::path& icnsPath) { + // Read PNG file + std::ifstream pngFile(pngPath, std::ios::binary); + if (!pngFile) + return false; + + // Get PNG size + pngFile.seekg(0, std::ios::end); + uint32 pngSize = static_cast(pngFile.tellg()); + pngFile.seekg(0, std::ios::beg); + + // Calculate total file size (header + size + type + data) + uint32 totalSize = 8 + 8 + pngSize; + + // Create output file + std::ofstream icnsFile(icnsPath, std::ios::binary); + if (!icnsFile) + return false; + + // Write ICNS header + icnsFile.put(0x69); // 'i' + icnsFile.put(0x63); // 'c' + icnsFile.put(0x6e); // 'n' + icnsFile.put(0x73); // 's' + + // Write total file size (big endian) + icnsFile.put((totalSize >> 24) & 0xFF); + icnsFile.put((totalSize >> 16) & 0xFF); + icnsFile.put((totalSize >> 8) & 0xFF); + icnsFile.put(totalSize & 0xFF); + + // Write icon type (ic07 = 128x128 PNG) + icnsFile.put(0x69); // 'i' + icnsFile.put(0x63); // 'c' + icnsFile.put(0x30); // '0' + icnsFile.put(0x37); // '7' + + // Write PNG size (big endian) + icnsFile.put((pngSize >> 24) & 0xFF); + icnsFile.put((pngSize >> 16) & 0xFF); + icnsFile.put((pngSize >> 8) & 0xFF); + icnsFile.put(pngSize & 0xFF); + + // Copy PNG data + icnsFile << pngFile.rdbuf(); + + return true; +} + wxGameList::wxGameList(wxWindow* parent, wxWindowID id) : wxListCtrl(parent, id, wxDefaultPosition, wxDefaultSize, GetStyleFlags(Style::kList)), m_style(Style::kList) { @@ -596,9 +646,7 @@ void wxGameList::OnContextMenu(wxContextMenuEvent& event) menu.Append(kContextMenuEditGameProfile, _("&Edit game profile")); menu.AppendSeparator(); -#if BOOST_OS_LINUX || BOOST_OS_WINDOWS menu.Append(kContextMenuCreateShortcut, _("&Create shortcut")); -#endif menu.AppendSeparator(); menu.Append(kContextMenuCopyTitleName, _("&Copy Title Name")); menu.Append(kContextMenuCopyTitleId, _("&Copy Title ID")); @@ -724,9 +772,7 @@ void wxGameList::OnContextMenuSelected(wxCommandEvent& event) } case kContextMenuCreateShortcut: { -#if BOOST_OS_LINUX || BOOST_OS_WINDOWS CreateShortcut(gameInfo); -#endif break; } case kContextMenuCopyTitleName: @@ -1372,6 +1418,135 @@ void wxGameList::CreateShortcut(GameInfo2& gameInfo) } outputStream << desktopEntryString; } +#elif BOOST_OS_MACOS +void wxGameList::CreateShortcut(GameInfo2& gameInfo) +{ + const auto titleId = gameInfo.GetBaseTitleId(); + const auto titleName = wxString::FromUTF8(gameInfo.GetTitleName()); + auto exePath = ActiveSettings::GetExecutablePath(); + + const wxString appName = wxString::Format("%s.app", titleName); + wxFileDialog entryDialog(this, _("Choose shortcut location"), "~/Applications", appName, + "Application (*.app)|*.app", wxFD_SAVE | wxFD_CHANGE_DIR | wxFD_OVERWRITE_PROMPT); + const auto result = entryDialog.ShowModal(); + if (result == wxID_CANCEL) + return; + const auto output_path = entryDialog.GetPath(); + // Create .app folder + const fs::path appPath = output_path.utf8_string(); + if (!fs::create_directories(appPath)) + { + cemuLog_log(LogType::Force, "Failed to create app directory"); + return; + } + const fs::path infoPath = appPath / "Contents/Info.plist"; + const fs::path scriptPath = appPath / "Contents/MacOS/run.sh"; + const fs::path icnsPath = appPath / "Contents/Resources/shortcut.icns"; + if (!(fs::create_directories(scriptPath.parent_path()) && fs::create_directories(icnsPath.parent_path()))) + { + cemuLog_log(LogType::Force, "Failed to create app shortcut directories"); + return; + } + + std::optional iconPath; + // Obtain and convert icon + [&]() + { + int iconIndex, smallIconIndex; + + if (!QueryIconForTitle(titleId, iconIndex, smallIconIndex)) + { + cemuLog_log(LogType::Force, "Icon hasn't loaded"); + return; + } + const fs::path outIconDir = fs::temp_directory_path(); + + if (!fs::exists(outIconDir) && !fs::create_directories(outIconDir)) + { + cemuLog_log(LogType::Force, "Failed to create icon directory"); + return; + } + + iconPath = outIconDir / fmt::format("{:016x}.png", gameInfo.GetBaseTitleId()); + wxFileOutputStream pngFileStream(_pathToUtf8(iconPath.value())); + + auto image = m_image_list->GetIcon(iconIndex).ConvertToImage(); + wxPNGHandler pngHandler; + if (!pngHandler.SaveFile(&image, pngFileStream, false)) + { + iconPath = std::nullopt; + cemuLog_log(LogType::Force, "Icon failed to save"); + } + }(); + + std::string runCommand = fmt::format("#!/bin/zsh\n\n{0:?} --title-id {1:016x}", _pathToUtf8(exePath), titleId); + const std::string infoPlist = fmt::format( + "\n" + "\n" + "\n" + "\n" + " CFBundleDisplayName\n" + " {0}\n" + " CFBundleExecutable\n" + " run.sh\n" + " CFBundleIconFile\n" + " shortcut.icns\n" + " CFBundleName\n" + " {0}\n" + " CFBundlePackageType\n" + " APPL\n" + " CFBundleSignature\n" + " \?\?\?\?\n" + " LSApplicationCategoryType\n" + " public.app-category.games\n" + " CFBundleShortVersionString\n" + " {1}\n" + " CFBundleVersion\n" + " {1}\n" + "\n" + "\n", + gameInfo.GetTitleName(), + std::to_string(gameInfo.GetVersion()) + ); + // write Info.plist to infoPath + std::ofstream infoStream(infoPath); + std::ofstream scriptStream(scriptPath); + if (!infoStream.good() || !scriptStream.good()) + { + auto errorMsg = formatWxString(_("Failed to save app shortcut to {}"), output_path.utf8_string()); + wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + return; + } + infoStream << infoPlist; + scriptStream << runCommand; + scriptStream.close(); + + // Set execute permissions for script + fs::permissions( + scriptPath, + fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec, + fs::perm_options::add + ); + + // Return if iconPath is empty + if (!iconPath) + { + cemuLog_log(LogType::Force, "Icon not found"); + return; + } + + // Convert icon to icns, only works for 128x128 PNG + // Alternatively, can run the command "sips -s format icns {iconPath} --out '{icnsPath}'" + // using std::system() to handle images of any size + if (!writeICNS(*iconPath, icnsPath)) + { + cemuLog_log(LogType::Force, "Failed to convert icon to icns"); + return; + } + + // Remove temp file + fs::remove(*iconPath); +} #elif BOOST_OS_WINDOWS void wxGameList::CreateShortcut(GameInfo2& gameInfo) { diff --git a/src/gui/components/wxGameList.h b/src/gui/components/wxGameList.h index b285d259..a6bfa7f7 100644 --- a/src/gui/components/wxGameList.h +++ b/src/gui/components/wxGameList.h @@ -53,9 +53,7 @@ public: void ReloadGameEntries(bool cached = false); void DeleteCachedStrings(); -#if BOOST_OS_LINUX || BOOST_OS_WINDOWS void CreateShortcut(GameInfo2& gameInfo); -#endif long FindListItemByTitleId(uint64 title_id) const; void OnClose(wxCloseEvent& event);