fix(linux): focus browser on external links
This commit is contained in:
parent
dba8ec3d63
commit
9eb47c43a9
5 changed files with 135 additions and 19 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
|
@ -17,6 +17,7 @@ name = "OpenCADStudio"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
|
"ashpd",
|
||||||
"bincode",
|
"bincode",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|
@ -423,6 +424,23 @@ dependencies = [
|
||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ashpd"
|
||||||
|
version = "0.13.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"serde",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,11 @@ naga = { version = "27", features = ["wgsl-in"] }
|
||||||
# "Open with" for the portable .exe.
|
# "Open with" for the portable .exe.
|
||||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
# Mint an xdg-activation token for external URL handlers on Wayland. The token
|
||||||
|
# is passed to xdg-open so an existing browser window may take focus.
|
||||||
|
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Track standard DWG object relationships used by the scene integration.
|
# Track standard DWG object relationships used by the scene integration.
|
||||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "a7b1e07" }
|
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "a7b1e07" }
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,28 @@ impl OpenCADStudio {
|
||||||
pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
match cmd {
|
match cmd {
|
||||||
"DONATE" => {
|
"DONATE" => {
|
||||||
crate::sys::open_url("https://patreon.com/HakanSeven12");
|
|
||||||
self.command_line.push_info("Opening Patreon page...");
|
self.command_line.push_info("Opening Patreon page...");
|
||||||
|
return Some(crate::sys::open_url(
|
||||||
|
"https://patreon.com/HakanSeven12",
|
||||||
|
self.main_window,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
"WEBVERSION" => {
|
"WEBVERSION" => {
|
||||||
crate::sys::open_url("https://hakanseven12.github.io/OpenCADStudio/");
|
|
||||||
self.command_line.push_info("Opening OCS Web...");
|
self.command_line.push_info("Opening OCS Web...");
|
||||||
|
return Some(crate::sys::open_url(
|
||||||
|
"https://hakanseven12.github.io/OpenCADStudio/",
|
||||||
|
self.main_window,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
"HELP" => {
|
"HELP" => {
|
||||||
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/discussions");
|
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info("Opening OCS Discussions for help and questions...");
|
.push_info("Opening OCS Discussions for help and questions...");
|
||||||
|
return Some(crate::sys::open_url(
|
||||||
|
"https://github.com/HakanSeven12/OpenCADStudio/discussions",
|
||||||
|
self.main_window,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DWGPROPS — print round-trip-only HeaderVariables ─────────
|
// ── DWGPROPS — print round-trip-only HeaderVariables ─────────
|
||||||
|
|
@ -204,8 +213,8 @@ impl OpenCADStudio {
|
||||||
"https://github.com/HakanSeven12/OpenCADStudio/issues/new?body={}",
|
"https://github.com/HakanSeven12/OpenCADStudio/issues/new?body={}",
|
||||||
crate::sys::percent_encode(&body)
|
crate::sys::percent_encode(&body)
|
||||||
);
|
);
|
||||||
crate::sys::open_url(&url);
|
|
||||||
self.command_line.push_info("Opening feedback page...");
|
self.command_line.push_info("Opening feedback page...");
|
||||||
|
return Some(crate::sys::open_url(&url, self.main_window));
|
||||||
}
|
}
|
||||||
|
|
||||||
"ABOUT" => {
|
"ABOUT" => {
|
||||||
|
|
@ -217,8 +226,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
"CHANGELOG" => {
|
"CHANGELOG" => {
|
||||||
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases");
|
|
||||||
self.command_line.push_info("Opening release notes...");
|
self.command_line.push_info("Opening release notes...");
|
||||||
|
return Some(crate::sys::open_url(
|
||||||
|
"https://github.com/HakanSeven12/OpenCADStudio/releases",
|
||||||
|
self.main_window,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ALIASEDIT — command-alias editor ───────────────────────────
|
// ── ALIASEDIT — command-alias editor ───────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -290,10 +290,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::OpenPathPicked(None) => Task::none(),
|
Message::OpenPathPicked(None) => Task::none(),
|
||||||
|
|
||||||
Message::OpenUrl(url) => {
|
Message::OpenUrl(url) => crate::sys::open_url(&url, self.main_window),
|
||||||
crate::sys::open_url(&url);
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::StartSectionSelect(section) => {
|
Message::StartSectionSelect(section) => {
|
||||||
self.start_section = section;
|
self.start_section = section;
|
||||||
|
|
@ -5040,9 +5037,12 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::UpdateNoticeOpenRelease => {
|
Message::UpdateNoticeOpenRelease => {
|
||||||
crate::sys::open_url(crate::io::update_check::RELEASES_PAGE);
|
let open = crate::sys::open_url(
|
||||||
|
crate::io::update_check::RELEASES_PAGE,
|
||||||
|
self.main_window,
|
||||||
|
);
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
Task::none()
|
open
|
||||||
}
|
}
|
||||||
Message::AssocPromptYes => {
|
Message::AssocPromptYes => {
|
||||||
self.file_assoc_enabled = true;
|
self.file_assoc_enabled = true;
|
||||||
|
|
|
||||||
97
src/sys.rs
97
src/sys.rs
|
|
@ -1,20 +1,101 @@
|
||||||
// Small platform shims for things the desktop build does natively but the web
|
// Small platform shims for things the desktop build does natively but the web
|
||||||
// (wasm) build must handle differently or skip.
|
// (wasm) build must handle differently or skip.
|
||||||
|
|
||||||
/// Open a URL in the user's browser. The desktop launches the default handler;
|
/// Open a URL in the user's browser.
|
||||||
/// the web opens a new tab (the button click is a user gesture, so it isn't
|
///
|
||||||
/// caught by the pop-up blocker). Focus of the opened page is left to the
|
/// Wayland requires an xdg-activation token from the source window before it
|
||||||
/// OS / browser.
|
/// will let an existing browser window take focus. Linux obtains that token
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
/// from the OCS surface and passes it to xdg-open.
|
||||||
pub fn open_url(url: &str) {
|
#[cfg(target_os = "linux")]
|
||||||
let _ = open::that(url);
|
pub fn open_url<Message: Send + 'static>(
|
||||||
|
url: &str,
|
||||||
|
parent: Option<iced::window::Id>,
|
||||||
|
) -> iced::Task<Message> {
|
||||||
|
let url = url.to_string();
|
||||||
|
match parent {
|
||||||
|
Some(parent) => iced::window::run(parent, move |window| {
|
||||||
|
open_url_linux(&url, linux_activation_token(window));
|
||||||
|
})
|
||||||
|
.discard(),
|
||||||
|
None => {
|
||||||
|
let _ = open::that_detached(url);
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn linux_activation_token(window: &dyn iced::window::Window) -> Option<String> {
|
||||||
|
use iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
|
|
||||||
|
let RawWindowHandle::Wayland(window_handle) =
|
||||||
|
window.window_handle().ok()?.as_raw()
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let RawDisplayHandle::Wayland(display_handle) =
|
||||||
|
window.display_handle().ok()?.as_raw()
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep the iced window borrowed while ashpd uses its raw Wayland surface.
|
||||||
|
// The returned token is an owned string and is immediately handed to the
|
||||||
|
// child process.
|
||||||
|
unsafe {
|
||||||
|
iced::futures::executor::block_on(
|
||||||
|
ashpd::ActivationToken::from_wayland_raw(
|
||||||
|
None,
|
||||||
|
window_handle.surface.as_ptr(),
|
||||||
|
display_handle.display.as_ptr(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map(String::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn open_url_linux(url: &str, activation_token: Option<String>) {
|
||||||
|
let Some(token) = activation_token else {
|
||||||
|
let _ = open::that_detached(url);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut command = std::process::Command::new("xdg-open");
|
||||||
|
command
|
||||||
|
.arg(url)
|
||||||
|
.env("XDG_ACTIVATION_TOKEN", &token)
|
||||||
|
.env("DESKTOP_STARTUP_ID", token);
|
||||||
|
|
||||||
|
if let Ok(mut child) = command.spawn() {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = child.wait();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let _ = open::that_detached(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "linux")))]
|
||||||
|
pub fn open_url<Message>(
|
||||||
|
url: &str,
|
||||||
|
_parent: Option<iced::window::Id>,
|
||||||
|
) -> iced::Task<Message> {
|
||||||
|
let _ = open::that_detached(url);
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web opens the tab synchronously so the browser still sees the click as a
|
||||||
|
/// user gesture and does not block it as a pop-up.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn open_url(url: &str) {
|
pub fn open_url<Message>(
|
||||||
|
url: &str,
|
||||||
|
_parent: Option<iced::window::Id>,
|
||||||
|
) -> iced::Task<Message> {
|
||||||
if let Some(window) = web_sys::window() {
|
if let Some(window) = web_sys::window() {
|
||||||
let _ = window.open_with_url_and_target(url, "_blank");
|
let _ = window.open_with_url_and_target(url, "_blank");
|
||||||
}
|
}
|
||||||
|
iced::Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue