Skip to content

Minor macOS UI improvements #1575

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -288,6 +290,11 @@ class wxAmiiboDropTarget : public wxFileDropTarget
MainWindow::MainWindow()
: wxFrame(nullptr, -1, 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);
Expand Down Expand Up @@ -911,6 +918,7 @@ void MainWindow::OnOptionsInput(wxCommandEvent& event)
break;
}

case MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS:
case MAINFRAME_MENU_ID_OPTIONS_GENERAL2:
{
OpenSettings();
Expand Down Expand Up @@ -1940,6 +1948,16 @@ class CemuAboutDialog : public wxDialog
lineSizer->Add(new wxStaticText(parent, -1, ")"), 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);
Expand Down Expand Up @@ -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"));

Expand Down
183 changes: 179 additions & 4 deletions src/gui/components/wxGameList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,56 @@ std::list<fs::path> _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<uint32>(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)
{
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -724,9 +772,7 @@ void wxGameList::OnContextMenuSelected(wxCommandEvent& event)
}
case kContextMenuCreateShortcut:
{
#if BOOST_OS_LINUX || BOOST_OS_WINDOWS
CreateShortcut(gameInfo);
#endif
break;
}
case kContextMenuCopyTitleName:
Expand Down Expand Up @@ -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<fs::path> 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(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
"<plist version=\"1.0\">\n"
"<dict>\n"
" <key>CFBundleDisplayName</key>\n"
" <string>{0}</string>\n"
" <key>CFBundleExecutable</key>\n"
" <string>run.sh</string>\n"
" <key>CFBundleIconFile</key>\n"
" <string>shortcut.icns</string>\n"
" <key>CFBundleName</key>\n"
" <string>{0}</string>\n"
" <key>CFBundlePackageType</key>\n"
" <string>APPL</string>\n"
" <key>CFBundleSignature</key>\n"
" <string>\?\?\?\?</string>\n"
" <key>LSApplicationCategoryType</key>\n"
" <string>public.app-category.games</string>\n"
" <key>CFBundleShortVersionString</key>\n"
" <string>{1}</string>\n"
" <key>CFBundleVersion</key>\n"
" <string>{1}</string>\n"
"</dict>\n"
"</plist>\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)
{
Expand Down
2 changes: 0 additions & 2 deletions src/gui/components/wxGameList.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ class wxGameList : public wxListCtrl
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);
Expand Down
Loading