Skip to content

Commit 3593e62

Browse files
authored
feat: attach/detach bound devices in tray context menu (#6)
1 parent 3996590 commit 3593e62

File tree

1 file changed

+135
-20
lines changed

1 file changed

+135
-20
lines changed

src/gui/usbipd_gui.rs

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use native_windows_gui as nwg;
99
use super::auto_attach_tab::AutoAttachTab;
1010
use super::connected_tab::ConnectedTab;
1111
use super::persisted_tab::PersistedTab;
12+
use crate::usbipd::{list_devices, UsbDevice};
1213
use crate::{
1314
auto_attach::AutoAttacher,
1415
win_utils::{self, DeviceNotification},
@@ -25,6 +26,7 @@ pub(super) trait GuiTab {
2526
#[derive(Default, NwgUi)]
2627
pub struct UsbipdGui {
2728
device_notification: Cell<DeviceNotification>,
29+
menu_tray_event_handler: Cell<Option<nwg::EventHandler>>,
2830

2931
#[nwg_resource]
3032
embed: nwg::EmbedResource,
@@ -75,24 +77,9 @@ pub struct UsbipdGui {
7577

7678
// Tray icon
7779
#[nwg_control(icon: Some(&data.app_icon), tip: Some("WSL USB Manager"))]
78-
#[nwg_events(OnContextMenu: [UsbipdGui::show_tray_menu], MousePressLeftUp: [UsbipdGui::show])]
80+
#[nwg_events(OnContextMenu: [UsbipdGui::show_menu_tray], MousePressLeftUp: [UsbipdGui::show])]
7981
tray: nwg::TrayNotification,
8082

81-
// Tray menu
82-
#[nwg_control(parent: window, popup: true)]
83-
menu_tray: nwg::Menu,
84-
85-
#[nwg_control(parent: menu_tray, text: "Open")]
86-
#[nwg_events(OnMenuItemSelected: [UsbipdGui::show])]
87-
menu_tray_open: nwg::MenuItem,
88-
89-
#[nwg_control(parent: menu_tray)]
90-
menu_tray_sep: nwg::MenuSeparator,
91-
92-
#[nwg_control(parent: menu_tray, text: "Exit")]
93-
#[nwg_events(OnMenuItemSelected: [UsbipdGui::exit])]
94-
menu_tray_exit: nwg::MenuItem,
95-
9683
// File menu
9784
#[nwg_control(parent: window, text: "File", popup: false)]
9885
menu_file: nwg::Menu,
@@ -105,7 +92,7 @@ pub struct UsbipdGui {
10592
menu_file_sep1: nwg::MenuSeparator,
10693

10794
#[nwg_control(parent: menu_file, text: "Exit")]
108-
#[nwg_events(OnMenuItemSelected: [UsbipdGui::exit])]
95+
#[nwg_events(OnMenuItemSelected: [UsbipdGui::exit()])]
10996
menu_file_exit: nwg::MenuItem,
11097
}
11198

@@ -154,9 +141,137 @@ impl UsbipdGui {
154141
self.window.set_visible(true);
155142
}
156143

157-
fn show_tray_menu(&self) {
144+
fn show_menu_tray(self: &Rc<UsbipdGui>) {
145+
// This prevents a memory leak in which the event handler closure is
146+
// kept alive after the menu is destroyed. An attempt was made to unbind
147+
// from the OnMenuExit event, but it seems to prevent the menu event
148+
// handlers from running at all.
149+
if let Some(handler) = self.menu_tray_event_handler.take() {
150+
nwg::unbind_event_handler(&handler);
151+
}
152+
153+
let mut menu_tray = nwg::Menu::default();
154+
nwg::Menu::builder()
155+
.popup(true)
156+
.parent(self.window.handle)
157+
.build(&mut menu_tray)
158+
.unwrap();
159+
160+
let devices = list_devices()
161+
.into_iter()
162+
.filter(|d| d.is_connected())
163+
.collect::<Vec<_>>();
164+
165+
let mut menu_items: Vec<(nwg::MenuItem, UsbDevice)> = Vec::with_capacity(devices.len());
166+
for device in devices {
167+
let device_name = device.description.as_deref();
168+
let vid_pid = device.vid_pid();
169+
let description = device_name.map(|s| s.to_string()).unwrap_or(
170+
vid_pid
171+
.clone()
172+
.unwrap_or_else(|| "Unknown Device".to_string()),
173+
);
174+
175+
if device.is_bound() {
176+
let menu_item = self
177+
.new_menu_item(menu_tray.handle, &description, false, device.is_attached())
178+
.unwrap();
179+
180+
menu_items.push((menu_item, device));
181+
}
182+
}
183+
184+
if menu_items.is_empty() {
185+
self.new_menu_item(menu_tray.handle, "No bound devices", true, false)
186+
.unwrap();
187+
};
188+
189+
self.new_menu_separator(menu_tray.handle).unwrap();
190+
let open_item = self
191+
.new_menu_item(menu_tray.handle, "Open", false, false)
192+
.unwrap();
193+
self.new_menu_separator(menu_tray.handle).unwrap();
194+
let exit_item = self
195+
.new_menu_item(menu_tray.handle, "Exit", false, false)
196+
.unwrap();
197+
198+
let rc_self_weak = Rc::downgrade(self);
199+
let handler =
200+
nwg::full_bind_event_handler(&self.window.handle, move |evt, _evt_data, handle| {
201+
// Ignore events that are not menu item selections
202+
if evt != nwg::Event::OnMenuItemSelected {
203+
return;
204+
}
205+
206+
// Retrieve the GUI instance
207+
let Some(rc_self) = rc_self_weak.upgrade() else {
208+
return;
209+
};
210+
211+
// Handle the menu item selection
212+
if handle == open_item.handle {
213+
// The open menu item was selected
214+
UsbipdGui::show(rc_self.as_ref());
215+
} else if handle == exit_item.handle {
216+
// The exit menu item was selected
217+
UsbipdGui::exit();
218+
} else {
219+
// A device menu item was selected
220+
let Some(device) = menu_items
221+
.iter()
222+
.find(|(item, _)| item.handle == handle)
223+
.map(|(_, d)| d)
224+
else {
225+
return;
226+
};
227+
228+
if device.is_attached() {
229+
// Silently ignore errors here as the device may have been unplugged
230+
device.detach().ok();
231+
} else {
232+
// TODO: this currently blocks the UI
233+
device.attach().unwrap_or_else(|err| {
234+
nwg::modal_error_message(
235+
rc_self.window.handle,
236+
"WSL USB Manager: Command Error",
237+
&err,
238+
);
239+
});
240+
}
241+
}
242+
});
243+
self.menu_tray_event_handler.set(Some(handler));
244+
158245
let (x, y) = nwg::GlobalCursor::position();
159-
self.menu_tray.popup(x, y);
246+
menu_tray.popup(x, y);
247+
}
248+
249+
fn new_menu_item(
250+
&self,
251+
parent: nwg::ControlHandle,
252+
text: &str,
253+
disabled: bool,
254+
check: bool,
255+
) -> Result<nwg::MenuItem, nwg::NwgError> {
256+
let mut menu_item = nwg::MenuItem::default();
257+
nwg::MenuItem::builder()
258+
.text(text)
259+
.disabled(disabled)
260+
.parent(parent)
261+
.check(check)
262+
.build(&mut menu_item)
263+
.map(|_| menu_item)
264+
}
265+
266+
fn new_menu_separator(
267+
&self,
268+
parent: nwg::ControlHandle,
269+
) -> Result<nwg::MenuSeparator, nwg::NwgError> {
270+
let mut sep = nwg::MenuSeparator::default();
271+
nwg::MenuSeparator::builder()
272+
.parent(parent)
273+
.build(&mut sep)
274+
.map(|_| sep)
160275
}
161276

162277
fn refresh(&self) {
@@ -165,7 +280,7 @@ impl UsbipdGui {
165280
self.auto_attach_tab_content.refresh();
166281
}
167282

168-
fn exit(&self) {
283+
fn exit() {
169284
nwg::stop_thread_dispatch();
170285
}
171286
}

0 commit comments

Comments
 (0)