1use windows::Win32::Foundation::HWND;
9use windows::Win32::System::Com::FORMATETC;
10use windows::Win32::System::Ole::OleGetClipboard;
11use windows::Win32::UI::Shell::Common::ITEMIDLIST;
12use windows::Win32::UI::Shell::{
13 CMF_EXPLORE, CMF_EXTENDEDVERBS, CMF_NORMAL, GCS_VERBA, IContextMenu, IContextMenu2,
14 IContextMenu3,
15};
16use windows::Win32::UI::WindowsAndMessaging::*;
17use windows::core::{Interface, PSTR};
18
19use crate::error::{Error, Result};
20use crate::hidden_window::HiddenWindow;
21use crate::invoke::invoke_command;
22use crate::menu_items::{InvokeParams, MenuItem, SelectedItem};
23use crate::shell_item::ShellItems;
24
25const ID_FIRST: u32 = 1;
28const ID_LAST: u32 = 0x7FFF;
30const ID_PASTE_INJECTED: u32 = 0x8000;
32
33const CF_HDROP: u16 = 15;
35
36pub struct ContextMenu {
58 items: ShellItems,
59 extended: bool,
60 owner_hwnd: Option<isize>,
61}
62
63impl ContextMenu {
64 pub fn new(items: ShellItems) -> Result<Self> {
66 Ok(Self {
67 items,
68 extended: false,
69 owner_hwnd: None,
70 })
71 }
72
73 pub fn extended(mut self, yes: bool) -> Self {
78 self.extended = yes;
79 self
80 }
81
82 pub fn owner(mut self, hwnd: isize) -> Self {
88 self.owner_hwnd = Some(hwnd);
89 self
90 }
91
92 pub fn show_at(self, x: i32, y: i32) -> Result<Option<SelectedItem>> {
97 let hidden_window = HiddenWindow::new()?;
98 let hwnd = if let Some(h) = self.owner_hwnd {
99 HWND(h as *mut _)
100 } else {
101 hidden_window.hwnd
102 };
103
104 let ctx_menu = self.get_context_menu_with_hwnd(hwnd)?;
105
106 let hmenu = unsafe { CreatePopupMenu().map_err(Error::Windows)? };
109
110 let flags = self.query_flags();
111 unsafe {
115 ctx_menu
116 .QueryContextMenu(hmenu, 0, ID_FIRST, ID_LAST, flags)
117 .map_err(Error::QueryContextMenu)?;
118 }
119
120 if self.items.is_background {
123 inject_clipboard_items(hmenu);
124 }
125
126 let ctx2: Option<IContextMenu2> = ctx_menu.cast().ok();
128 let ctx3: Option<IContextMenu3> = ctx_menu.cast().ok();
129
130 hidden_window.set_context_menu_handlers(ctx2, ctx3);
131
132 unsafe {
135 let _ = SetForegroundWindow(hwnd);
136 }
137
138 let cmd = unsafe {
142 TrackPopupMenu(
143 hmenu,
144 TPM_RETURNCMD | TPM_RIGHTBUTTON,
145 x,
146 y,
147 0,
148 hidden_window.hwnd,
149 None,
150 )
151 };
152
153 let selected = if cmd.as_bool() {
154 let command_id = cmd.0 as u32;
155 if command_id == ID_PASTE_INJECTED {
156 let ctx_menu_clone = ctx_menu.clone();
158 let hwnd_val = hwnd;
159 Some(SelectedItem {
160 menu_item: MenuItem {
161 id: ID_PASTE_INJECTED,
162 label: "Paste".to_string(),
163 command_string: Some("paste".to_string()),
164 is_separator: false,
165 is_disabled: false,
166 is_checked: false,
167 is_default: false,
168 submenu: None,
169 },
170 command_id: ID_PASTE_INJECTED,
171 invoker: Some(Box::new(move |_params: Option<InvokeParams>| {
172 crate::invoke::invoke_command_by_verb(&ctx_menu_clone, "paste", hwnd_val)
173 })),
174 })
175 } else {
176 let item = get_menu_item_info_for_id(&ctx_menu, hmenu, command_id)?;
177 let ctx_menu_clone = ctx_menu.clone();
178 let hwnd_val = hwnd;
179 Some(SelectedItem {
180 menu_item: item,
181 command_id,
182 invoker: Some(Box::new(move |params: Option<InvokeParams>| {
183 invoke_command(&ctx_menu_clone, command_id - ID_FIRST, hwnd_val, params)
184 })),
185 })
186 }
187 } else {
188 None
189 };
190
191 unsafe {
193 let _ = DestroyMenu(hmenu);
194 }
195
196 Ok(selected)
197 }
198
199 pub fn show(self) -> Result<Option<SelectedItem>> {
203 let mut point = windows::Win32::Foundation::POINT::default();
204 unsafe {
207 let _ = GetCursorPos(&mut point);
208 }
209 self.show_at(point.x, point.y)
210 }
211
212 pub fn enumerate(&self) -> Result<Vec<MenuItem>> {
217 let hidden_window = HiddenWindow::new()?;
218 let ctx_menu = self.get_context_menu_with_hwnd(hidden_window.hwnd)?;
219
220 let hmenu = unsafe { CreatePopupMenu().map_err(Error::Windows)? };
222
223 let flags = self.query_flags();
224 unsafe {
227 ctx_menu
228 .QueryContextMenu(hmenu, 0, ID_FIRST, ID_LAST, flags)
229 .map_err(Error::QueryContextMenu)?;
230 }
231
232 if self.items.is_background {
234 inject_clipboard_items(hmenu);
235 }
236
237 let items = enumerate_menu(&ctx_menu, hmenu)?;
238
239 unsafe {
241 let _ = DestroyMenu(hmenu);
242 }
243
244 Ok(items)
245 }
246
247 fn query_flags(&self) -> u32 {
248 let mut flags = CMF_NORMAL;
249 if !self.items.is_background {
250 flags |= CMF_EXPLORE;
251 }
252 if self.extended {
253 flags |= CMF_EXTENDEDVERBS;
254 }
255 flags
256 }
257
258 fn get_context_menu_with_hwnd(&self, hwnd: HWND) -> Result<IContextMenu> {
259 if self.items.is_background {
260 unsafe {
264 let menu: IContextMenu = self
265 .items
266 .parent
267 .CreateViewObject(hwnd)
268 .map_err(Error::GetContextMenu)?;
269 Ok(menu)
270 }
271 } else {
272 let pidl_ptrs: Vec<*const ITEMIDLIST> =
275 self.items.child_pidls.iter().map(|p| p.as_ptr()).collect();
276
277 unsafe {
281 let menu: IContextMenu = self
282 .items
283 .parent
284 .GetUIObjectOf(hwnd, &pidl_ptrs, None)
285 .map_err(Error::GetContextMenu)?;
286 Ok(menu)
287 }
288 }
289 }
290}
291
292fn enumerate_menu(ctx_menu: &IContextMenu, hmenu: HMENU) -> Result<Vec<MenuItem>> {
294 let count = unsafe { GetMenuItemCount(hmenu) };
296 if count < 0 {
297 return Ok(Vec::new());
298 }
299
300 let mut items = Vec::new();
301
302 for i in 0..count {
303 let mut mii = MENUITEMINFOW {
304 cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
305 fMask: MIIM_ID | MIIM_FTYPE | MIIM_STATE | MIIM_SUBMENU | MIIM_STRING,
306 ..Default::default()
307 };
308
309 unsafe {
314 let _ = GetMenuItemInfoW(hmenu, i as u32, true, &mut mii);
315 }
316
317 if mii.fType.contains(MFT_SEPARATOR) {
318 items.push(MenuItem::separator());
319 continue;
320 }
321
322 let mut label_buf = vec![0u16; (mii.cch + 1) as usize];
324 mii.dwTypeData = windows::core::PWSTR(label_buf.as_mut_ptr());
325 mii.cch += 1;
326 unsafe {
328 let _ = GetMenuItemInfoW(hmenu, i as u32, true, &mut mii);
329 }
330
331 let label = String::from_utf16_lossy(&label_buf[..mii.cch as usize])
332 .replace('&', "");
333
334 let id = mii.wID;
335
336 let command_string = if (ID_FIRST..=ID_LAST).contains(&id) {
337 get_verb(ctx_menu, id - ID_FIRST)
338 } else {
339 None
340 };
341
342 let submenu = if !mii.hSubMenu.is_invalid() {
343 Some(enumerate_menu(ctx_menu, mii.hSubMenu)?)
344 } else {
345 None
346 };
347
348 items.push(MenuItem {
349 id,
350 label,
351 command_string,
352 is_separator: false,
353 is_disabled: mii.fState.contains(MFS_DISABLED),
354 is_checked: mii.fState.contains(MFS_CHECKED),
355 is_default: mii.fState.contains(MFS_DEFAULT),
356 submenu,
357 });
358 }
359
360 Ok(items)
361}
362
363fn get_verb(ctx_menu: &IContextMenu, offset: u32) -> Option<String> {
365 let mut buf = [0u8; 256];
366 unsafe {
370 ctx_menu
371 .GetCommandString(
372 offset as usize,
373 GCS_VERBA,
374 None,
375 PSTR(buf.as_mut_ptr()),
376 buf.len() as u32,
377 )
378 .ok()?;
379 }
380 let s = crate::util::ansi_buf_to_string(&buf);
381 if s.is_empty() {
382 None
383 } else {
384 Some(s)
385 }
386}
387
388fn get_menu_item_info_for_id(
390 ctx_menu: &IContextMenu,
391 hmenu: HMENU,
392 command_id: u32,
393) -> Result<MenuItem> {
394 let mut mii = MENUITEMINFOW {
395 cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
396 fMask: MIIM_ID | MIIM_FTYPE | MIIM_STATE | MIIM_STRING,
397 ..Default::default()
398 };
399
400 unsafe {
404 GetMenuItemInfoW(hmenu, command_id, false, &mut mii).map_err(Error::GetMenuItemInfo)?;
405 }
406
407 let mut label_buf = vec![0u16; (mii.cch + 1) as usize];
409 mii.dwTypeData = windows::core::PWSTR(label_buf.as_mut_ptr());
410 mii.cch += 1;
411 unsafe {
413 let _ = GetMenuItemInfoW(hmenu, command_id, false, &mut mii);
414 }
415
416 let label =
417 String::from_utf16_lossy(&label_buf[..mii.cch as usize]).replace('&', "");
418
419 let command_string = if command_id >= ID_FIRST {
420 get_verb(ctx_menu, command_id - ID_FIRST)
421 } else {
422 None
423 };
424
425 Ok(MenuItem {
426 id: command_id,
427 label,
428 command_string,
429 is_separator: false,
430 is_disabled: mii.fState.contains(MFS_DISABLED),
431 is_checked: mii.fState.contains(MFS_CHECKED),
432 is_default: mii.fState.contains(MFS_DEFAULT),
433 submenu: None,
434 })
435}
436
437fn inject_clipboard_items(hmenu: HMENU) {
439 let has_files = clipboard_has_files();
440
441 if has_files {
442 let paste_label: Vec<u16> = "貼り付け(V)\0".encode_utf16().collect();
444 let mii = MENUITEMINFOW {
445 cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
446 fMask: MIIM_ID | MIIM_STRING | MIIM_FTYPE,
447 fType: MFT_STRING,
448 wID: ID_PASTE_INJECTED,
449 dwTypeData: windows::core::PWSTR(paste_label.as_ptr() as *mut _),
450 cch: paste_label.len() as u32 - 1,
451 ..Default::default()
452 };
453 unsafe {
455 let _ = InsertMenuItemW(hmenu, 0, true, &mii);
456 }
457
458 let sep = MENUITEMINFOW {
460 cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
461 fMask: MIIM_FTYPE,
462 fType: MFT_SEPARATOR,
463 ..Default::default()
464 };
465 unsafe {
467 let _ = InsertMenuItemW(hmenu, 1, true, &sep);
468 }
469 }
470}
471
472fn clipboard_has_files() -> bool {
474 let data_obj = unsafe { OleGetClipboard() };
476 let data_obj = match data_obj {
477 Ok(d) => d,
478 Err(_) => return false,
479 };
480
481 let fmt = FORMATETC {
482 cfFormat: CF_HDROP,
483 ptd: std::ptr::null_mut(),
484 dwAspect: 1, lindex: -1,
486 tymed: 1, };
488
489 let result = unsafe { data_obj.QueryGetData(&fmt) };
491 result.is_ok()
492}