Support running Big Picture as separate process
Enable Big Picture mode to run concurrently with Desktop mode by: - Implementing cross-platform process launching (LaunchSiblingExecutable) for Windows, macOS, and Linux - Adding app profile system to isolate CEF user data between modes - Making window class/title and mutex names profile-aware - Changing mode switching to launch sibling executable instead of in-process switching - Updating settings.js to use native bridge for Big Picture launch - Adding build.bat helper for Windows builds
This commit is contained in:
@@ -56,6 +56,7 @@ if(OS_WINDOWS)
|
||||
set(NEBULA_PLATFORM_SOURCES
|
||||
src/platform/win/default_browser_win.cpp
|
||||
src/platform/win/paths_win.cpp
|
||||
src/platform/win/process_win.cpp
|
||||
src/platform/win/startup_win.cpp
|
||||
src/platform/win/browser_host_win.cpp
|
||||
src/platform/win/nebula_window_win.cpp
|
||||
@@ -64,6 +65,7 @@ elseif(OS_MACOSX)
|
||||
set(NEBULA_PLATFORM_SOURCES
|
||||
src/platform/mac/default_browser_mac.mm
|
||||
src/platform/mac/paths_mac.cpp
|
||||
src/platform/mac/process_mac.cpp
|
||||
src/platform/mac/startup_mac.mm
|
||||
src/platform/mac/browser_host_mac.mm
|
||||
src/platform/mac/nebula_window_mac.mm
|
||||
@@ -79,6 +81,7 @@ elseif(OS_LINUX)
|
||||
set(NEBULA_PLATFORM_SOURCES
|
||||
src/platform/linux/default_browser_linux.cpp
|
||||
src/platform/linux/paths_linux.cpp
|
||||
src/platform/linux/process_linux.cpp
|
||||
src/platform/linux/startup_linux.cpp
|
||||
src/platform/linux/browser_host_linux.cpp
|
||||
src/platform/linux/nebula_window_linux.cpp
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
rem Build Nebula Browser (Windows).
|
||||
rem Usage:
|
||||
rem build.bat - configure + Release build
|
||||
rem build.bat Debug - configure + Debug build
|
||||
rem build.bat Release - configure + Release build
|
||||
rem build.bat configure - configure only
|
||||
rem build.bat clean - remove the build folder
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
set "CONFIG=Release"
|
||||
if /I "%~1"=="Debug" set "CONFIG=Debug"
|
||||
if /I "%~1"=="Release" set "CONFIG=Release"
|
||||
|
||||
if /I "%~1"=="clean" (
|
||||
echo Removing build\ ...
|
||||
if exist build rmdir /s /q build
|
||||
echo Done.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
if not exist "thirdparty\cef\cmake\FindCEF.cmake" (
|
||||
echo ERROR: CEF not found at thirdparty\cef\
|
||||
echo Download a Windows 64-bit CEF standard binary distribution and extract it
|
||||
echo so that thirdparty\cef\cmake\FindCEF.cmake exists.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Configuring CMake...
|
||||
cmake -B build
|
||||
if errorlevel 1 (
|
||||
echo Configure failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if /I "%~1"=="configure" (
|
||||
echo Configure complete.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo Building %CONFIG%...
|
||||
cmake --build build --config %CONFIG%
|
||||
if errorlevel 1 (
|
||||
echo Build failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build succeeded: build\%CONFIG%\NebulaBrowser.exe
|
||||
exit /b 0
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "platform/browser_host.h"
|
||||
#include "platform/default_browser.h"
|
||||
#include "platform/process.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
@@ -243,6 +244,13 @@ void NebulaController::OnWindowCloseRequested() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (launch_options_.mode == AppMode::BigPicture && !closing_) {
|
||||
if (!SwitchToSiblingMode(AppMode::Desktop)) {
|
||||
BeginShutdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
BeginShutdown();
|
||||
}
|
||||
|
||||
@@ -420,7 +428,8 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
|
||||
} else if (command == "big-picture") {
|
||||
EnterBigPictureMode();
|
||||
CloseMenuPopup();
|
||||
SwitchToSiblingMode(AppMode::BigPicture);
|
||||
} else if (command == "gpu-diagnostics") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
|
||||
@@ -488,7 +497,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
||||
BeginShutdown();
|
||||
} else if (command == "exit-bigpicture" && window_) {
|
||||
if (launch_options_.mode == AppMode::BigPicture) {
|
||||
BeginShutdown();
|
||||
if (!SwitchToSiblingMode(AppMode::Desktop)) {
|
||||
BeginShutdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ExitBigPictureMode();
|
||||
@@ -728,6 +739,23 @@ void NebulaController::ExitBigPictureMode() {
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
bool NebulaController::SwitchToSiblingMode(AppMode mode) {
|
||||
#if defined(_WIN32)
|
||||
const char* executable =
|
||||
mode == AppMode::BigPicture ? "NebulaBigPicture.exe" : "NebulaBrowser.exe";
|
||||
#else
|
||||
const char* executable =
|
||||
mode == AppMode::BigPicture ? "NebulaBigPicture" : "NebulaBrowser";
|
||||
#endif
|
||||
|
||||
if (!nebula::platform::LaunchSiblingExecutable(executable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginShutdown();
|
||||
return true;
|
||||
}
|
||||
|
||||
nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const {
|
||||
if (!window_) {
|
||||
return {};
|
||||
|
||||
@@ -53,6 +53,7 @@ private:
|
||||
void CreateContentBrowser();
|
||||
void EnterBigPictureMode();
|
||||
void ExitBigPictureMode();
|
||||
bool SwitchToSiblingMode(AppMode mode);
|
||||
nebula::window::BrowserLayout CurrentBrowserLayout() const;
|
||||
void ToggleMenuPopup();
|
||||
void CloseMenuPopup();
|
||||
|
||||
@@ -122,6 +122,10 @@ int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options
|
||||
return subprocess_exit_code;
|
||||
}
|
||||
|
||||
// Isolate Big Picture from the desktop browser so both can run together.
|
||||
nebula::ui::SetAppProfile(
|
||||
options.mode == AppMode::BigPicture ? "bigpicture" : "desktop");
|
||||
|
||||
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
||||
nebula::platform::InitCommandLine(command_line, startup);
|
||||
std::string initial_url = GetLaunchTarget(command_line);
|
||||
|
||||
@@ -111,6 +111,7 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
|
||||
const bool allowed_settings_command =
|
||||
IsSettingsFrame(frame) && (command == "navigate" ||
|
||||
command == "new-tab" ||
|
||||
command == "big-picture" ||
|
||||
command == "check-default-browser" ||
|
||||
command == "set-default-browser" ||
|
||||
command == "clear-site-history" ||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "platform/process.h"
|
||||
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name) {
|
||||
const auto directory = ExecutableDirectory();
|
||||
if (directory.empty() || executable_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = directory / executable_name;
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(path.c_str(), path.c_str(), static_cast<char*>(nullptr));
|
||||
_exit(127);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "platform/process.h"
|
||||
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path ResolveSiblingPath(const std::string& executable_name) {
|
||||
const auto directory = ExecutableDirectory();
|
||||
if (directory.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::filesystem::path same_dir = directory / executable_name;
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(same_dir, error)) {
|
||||
return same_dir;
|
||||
}
|
||||
|
||||
// NebulaBrowser.app/Contents/MacOS -> sibling NebulaBigPicture.app/Contents/MacOS
|
||||
const std::filesystem::path sibling_app =
|
||||
directory / ".." / ".." / ".." / (executable_name + ".app") / "Contents" / "MacOS" /
|
||||
executable_name;
|
||||
if (std::filesystem::exists(sibling_app, error)) {
|
||||
return sibling_app.lexically_normal();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name) {
|
||||
const std::filesystem::path path = ResolveSiblingPath(executable_name);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(path.c_str(), path.c_str(), static_cast<char*>(nullptr));
|
||||
_exit(127);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
// Launches an executable that lives next to the current process.
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name);
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -7,11 +7,22 @@
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::window {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
|
||||
constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
|
||||
const wchar_t* WindowClassName() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"NebulaBigPictureWindow"
|
||||
: L"NebulaBrowserWindow";
|
||||
}
|
||||
|
||||
const wchar_t* WindowTitle() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"Nebula Big Picture"
|
||||
: L"Nebula Browser";
|
||||
}
|
||||
constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc";
|
||||
constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
|
||||
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
|
||||
@@ -415,7 +426,7 @@ void nebula::window::NebulaWindowImpl::RegisterClass(HINSTANCE instance_handle)
|
||||
window_class.lpfnWndProc = StaticWndProc;
|
||||
window_class.hInstance = instance_handle;
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.lpszClassName = WindowClassName();
|
||||
|
||||
RegisterClassExW(&window_class);
|
||||
}
|
||||
@@ -522,8 +533,8 @@ bool NebulaWindow::Create(const platform::AppStartup& startup) {
|
||||
|
||||
impl_->hwnd = CreateWindowExW(
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WindowClassName(),
|
||||
WindowTitle(),
|
||||
WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN,
|
||||
x,
|
||||
y,
|
||||
@@ -654,7 +665,7 @@ void NebulaWindow::SetTitle(const std::string& title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::wstring wide = title.empty() ? kWindowTitle : Utf8ToWide(title);
|
||||
const std::wstring wide = title.empty() ? WindowTitle() : Utf8ToWide(title);
|
||||
SetWindowTextW(impl_->hwnd, wide.c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "platform/process.h"
|
||||
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& utf8) {
|
||||
if (utf8.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int size = MultiByteToWideChar(
|
||||
CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
|
||||
if (size <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(size), L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), result.data(), size);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name) {
|
||||
const auto directory = ExecutableDirectory();
|
||||
if (directory.empty() || executable_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring exe_name = Utf8ToWide(executable_name);
|
||||
if (exe_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = directory / exe_name;
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring path_wide = path.wstring();
|
||||
const std::wstring dir_wide = directory.wstring();
|
||||
|
||||
STARTUPINFOW startup_info = {};
|
||||
startup_info.cb = sizeof(startup_info);
|
||||
PROCESS_INFORMATION process_info = {};
|
||||
|
||||
// CreateProcess requires a writable command line buffer.
|
||||
std::wstring command_line = L"\"" + path_wide + L"\"";
|
||||
const BOOL created = CreateProcessW(
|
||||
path_wide.c_str(),
|
||||
command_line.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
dir_wide.c_str(),
|
||||
&startup_info,
|
||||
&process_info);
|
||||
if (!created) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(process_info.hProcess);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -10,10 +10,20 @@
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
|
||||
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
|
||||
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
|
||||
|
||||
const wchar_t* MainInstanceMutexName() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"Local\\NebulaBigPictureMainInstance"
|
||||
: L"Local\\NebulaBrowserMainInstance";
|
||||
}
|
||||
|
||||
const wchar_t* WindowClassName() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"NebulaBigPictureWindow"
|
||||
: L"NebulaBrowserWindow";
|
||||
}
|
||||
|
||||
class ScopedHandle {
|
||||
public:
|
||||
explicit ScopedHandle(HANDLE handle) : handle_(handle) {}
|
||||
@@ -54,7 +64,7 @@ void ForwardLaunchTargetToExistingWindow(const std::string& launch_target) {
|
||||
return;
|
||||
}
|
||||
|
||||
HWND existing_window = FindWindowW(kWindowClassName, nullptr);
|
||||
HWND existing_window = FindWindowW(WindowClassName(), nullptr);
|
||||
if (!existing_window) {
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +91,7 @@ void PrepareApp() {
|
||||
}
|
||||
|
||||
bool TryAcquireSingleInstance(const std::string& launch_target) {
|
||||
static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName));
|
||||
static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, MainInstanceMutexName()));
|
||||
const bool already_running = mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
if (already_running) {
|
||||
ForwardLaunchTargetToExistingWindow(launch_target);
|
||||
|
||||
@@ -109,8 +109,18 @@ const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string g_app_profile;
|
||||
|
||||
} // namespace
|
||||
|
||||
void SetAppProfile(std::string_view profile) {
|
||||
g_app_profile = std::string(profile);
|
||||
}
|
||||
|
||||
const std::string& GetAppProfile() {
|
||||
return g_app_profile;
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
return nebula::platform::ExecutableDirectory();
|
||||
}
|
||||
@@ -125,6 +135,9 @@ std::filesystem::path GetUserDataDirectory() {
|
||||
}
|
||||
|
||||
std::filesystem::path user_data = root / "Nebula" / "User Data";
|
||||
if (!g_app_profile.empty() && g_app_profile != "desktop") {
|
||||
user_data = root / "Nebula" / ("User Data " + g_app_profile);
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(user_data, ec);
|
||||
return user_data;
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace nebula::ui {
|
||||
|
||||
// Process-wide profile used to isolate desktop vs Big Picture CEF data.
|
||||
// Call before GetUserDataDirectory() / CEF initialize. Empty/"desktop" = default.
|
||||
void SetAppProfile(std::string_view profile);
|
||||
const std::string& GetAppProfile();
|
||||
|
||||
std::filesystem::path GetExecutableDirectory();
|
||||
std::filesystem::path GetUserDataDirectory();
|
||||
std::filesystem::path GetCacheDirectory();
|
||||
|
||||
+3
-3
@@ -382,11 +382,11 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
if (bigPictureBtn) {
|
||||
bigPictureBtn.addEventListener('click', async () => {
|
||||
bigPictureBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if (window.bigPictureAPI && typeof window.bigPictureAPI.launch === 'function') {
|
||||
if (hasNebulaNativeBridge()) {
|
||||
showStatus('Launching Big Picture Mode...');
|
||||
await window.bigPictureAPI.launch();
|
||||
window.nebulaNative.postMessage('big-picture');
|
||||
} else {
|
||||
showStatus('Big Picture Mode not available');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user