use windows::Win32::Foundation::HWND;
use windows::Win32::System::Com::FORMATETC;
use windows::Win32::System::Ole::OleGetClipboard;
use windows::Win32::UI::Shell::Common::ITEMIDLIST;
use windows::Win32::UI::Shell::{
CMF_EXPLORE, CMF_EXTENDEDVERBS, CMF_NORMAL, GCS_VERBA, IContextMenu, IContextMenu2,
IContextMenu3,
};
use windows::Win32::UI::WindowsAndMessaging::*;
use windows::core::{Interface, PSTR};
use crate::error::{Error, Result};
use crate::hidden_window::HiddenWindow;
use crate::invoke::invoke_command;
use crate::menu_items::{InvokeParams, MenuItem, SelectedItem};
use crate::shell_item::ShellItems;
const ID_FIRST: u32 = 1;
const ID_LAST: u32 = 0x7FFF;
const ID_PASTE_INJECTED: u32 = 0x8000;
const CF_HDROP: u16 = 15;
pub struct ContextMenu {
items: ShellItems,
extended: bool,
owner_hwnd: Option<isize>,
}
impl ContextMenu {
pub fn new(items: ShellItems) -> Result<Self> {
Ok(Self {
items,
extended: false,
owner_hwnd: None,
})
}
pub fn extended(mut self, yes: bool) -> Self {
self.extended = yes;
self
}
pub fn owner(mut self, hwnd: isize) -> Self {
self.owner_hwnd = Some(hwnd);
self
}
pub fn show_at(self, x: i32, y: i32) -> Result<Option<SelectedItem>> {
let hidden_window = HiddenWindow::new()?;
let hwnd = if let Some(h) = self.owner_hwnd {
HWND(h as *mut _)
} else {
hidden_window.hwnd
};
let ctx_menu = self.get_context_menu_with_hwnd(hwnd)?;
let hmenu = unsafe { CreatePopupMenu().map_err(Error::Windows)? };
let flags = self.query_flags();
unsafe {
ctx_menu
.QueryContextMenu(hmenu, 0, ID_FIRST, ID_LAST, flags)
.map_err(Error::QueryContextMenu)?;
}
if self.items.is_background {
inject_clipboard_items(hmenu);
}
let ctx2: Option<IContextMenu2> = ctx_menu.cast().ok();
let ctx3: Option<IContextMenu3> = ctx_menu.cast().ok();
hidden_window.set_context_menu_handlers(ctx2, ctx3);
unsafe {
let _ = SetForegroundWindow(hwnd);
}
let cmd = unsafe {
TrackPopupMenu(
hmenu,
TPM_RETURNCMD | TPM_RIGHTBUTTON,
x,
y,
0,
hidden_window.hwnd,
None,
)
};
let selected = if cmd.as_bool() {
let command_id = cmd.0 as u32;
if command_id == ID_PASTE_INJECTED {
let ctx_menu_clone = ctx_menu.clone();
let hwnd_val = hwnd;
Some(SelectedItem {
menu_item: MenuItem {
id: ID_PASTE_INJECTED,
label: "Paste".to_string(),
command_string: Some("paste".to_string()),
is_separator: false,
is_disabled: false,
is_checked: false,
is_default: false,
submenu: None,
},
command_id: ID_PASTE_INJECTED,
invoker: Some(Box::new(move |_params: Option<InvokeParams>| {
crate::invoke::invoke_command_by_verb(&ctx_menu_clone, "paste", hwnd_val)
})),
_hidden_window: Some(hidden_window),
})
} else {
let item = get_menu_item_info_for_id(&ctx_menu, hmenu, command_id)?;
let ctx_menu_clone = ctx_menu.clone();
let hwnd_val = hwnd;
Some(SelectedItem {
menu_item: item,
command_id,
invoker: Some(Box::new(move |params: Option<InvokeParams>| {
invoke_command(&ctx_menu_clone, command_id - ID_FIRST, hwnd_val, params)
})),
_hidden_window: Some(hidden_window),
})
}
} else {
None
};
unsafe {
let _ = DestroyMenu(hmenu);
}
Ok(selected)
}
pub fn show(self) -> Result<Option<SelectedItem>> {
let mut point = windows::Win32::Foundation::POINT::default();
unsafe {
let _ = GetCursorPos(&mut point);
}
self.show_at(point.x, point.y)
}
pub fn enumerate(&self) -> Result<Vec<MenuItem>> {
let hidden_window = HiddenWindow::new()?;
let ctx_menu = self.get_context_menu_with_hwnd(hidden_window.hwnd)?;
let hmenu = unsafe { CreatePopupMenu().map_err(Error::Windows)? };
let flags = self.query_flags();
unsafe {
ctx_menu
.QueryContextMenu(hmenu, 0, ID_FIRST, ID_LAST, flags)
.map_err(Error::QueryContextMenu)?;
}
if self.items.is_background {
inject_clipboard_items(hmenu);
}
let items = enumerate_menu(&ctx_menu, hmenu)?;
unsafe {
let _ = DestroyMenu(hmenu);
}
Ok(items)
}
pub fn invoke_verb(&self, verb: &str) -> Result<()> {
let hidden_window = HiddenWindow::new()?;
let hwnd = if let Some(h) = self.owner_hwnd {
HWND(h as *mut _)
} else {
hidden_window.hwnd
};
let ctx_menu = self.get_context_menu_with_hwnd(hwnd)?;
let hmenu = unsafe { CreatePopupMenu().map_err(Error::Windows)? };
let flags = self.query_flags();
unsafe {
ctx_menu
.QueryContextMenu(hmenu, 0, ID_FIRST, ID_LAST, flags)
.map_err(Error::QueryContextMenu)?;
}
let result = crate::invoke::invoke_command_by_verb(&ctx_menu, verb, hwnd);
unsafe {
let _ = DestroyMenu(hmenu);
}
result
}
fn query_flags(&self) -> u32 {
let mut flags = CMF_NORMAL;
if !self.items.is_background {
flags |= CMF_EXPLORE;
}
if self.extended {
flags |= CMF_EXTENDEDVERBS;
}
flags
}
fn get_context_menu_with_hwnd(&self, hwnd: HWND) -> Result<IContextMenu> {
if self.items.is_background {
unsafe {
let menu: IContextMenu = self
.items
.parent
.CreateViewObject(hwnd)
.map_err(Error::GetContextMenu)?;
Ok(menu)
}
} else {
let pidl_ptrs: Vec<*const ITEMIDLIST> =
self.items.child_pidls.iter().map(|p| p.as_ptr()).collect();
unsafe {
let menu: IContextMenu = self
.items
.parent
.GetUIObjectOf(hwnd, &pidl_ptrs, None)
.map_err(Error::GetContextMenu)?;
Ok(menu)
}
}
}
}
fn enumerate_menu(ctx_menu: &IContextMenu, hmenu: HMENU) -> Result<Vec<MenuItem>> {
let count = unsafe { GetMenuItemCount(hmenu) };
if count < 0 {
return Ok(Vec::new());
}
let mut items = Vec::new();
for i in 0..count {
let mut mii = MENUITEMINFOW {
cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
fMask: MIIM_ID | MIIM_FTYPE | MIIM_STATE | MIIM_SUBMENU | MIIM_STRING,
..Default::default()
};
unsafe {
let _ = GetMenuItemInfoW(hmenu, i as u32, true, &mut mii);
}
if mii.fType.contains(MFT_SEPARATOR) {
items.push(MenuItem::separator());
continue;
}
let mut label_buf = vec![0u16; (mii.cch + 1) as usize];
mii.dwTypeData = windows::core::PWSTR(label_buf.as_mut_ptr());
mii.cch += 1;
unsafe {
let _ = GetMenuItemInfoW(hmenu, i as u32, true, &mut mii);
}
let label = String::from_utf16_lossy(&label_buf[..mii.cch as usize])
.replace('&', "");
let id = mii.wID;
let command_string = if (ID_FIRST..=ID_LAST).contains(&id) {
get_verb(ctx_menu, id - ID_FIRST)
} else {
None
};
let submenu = if !mii.hSubMenu.is_invalid() {
Some(enumerate_menu(ctx_menu, mii.hSubMenu)?)
} else {
None
};
items.push(MenuItem {
id,
label,
command_string,
is_separator: false,
is_disabled: mii.fState.contains(MFS_DISABLED),
is_checked: mii.fState.contains(MFS_CHECKED),
is_default: mii.fState.contains(MFS_DEFAULT),
submenu,
});
}
Ok(items)
}
fn get_verb(ctx_menu: &IContextMenu, offset: u32) -> Option<String> {
let mut buf = [0u8; 256];
unsafe {
ctx_menu
.GetCommandString(
offset as usize,
GCS_VERBA,
None,
PSTR(buf.as_mut_ptr()),
buf.len() as u32,
)
.ok()?;
}
let s = crate::util::ansi_buf_to_string(&buf);
if s.is_empty() {
None
} else {
Some(s)
}
}
fn get_menu_item_info_for_id(
ctx_menu: &IContextMenu,
hmenu: HMENU,
command_id: u32,
) -> Result<MenuItem> {
let mut mii = MENUITEMINFOW {
cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
fMask: MIIM_ID | MIIM_FTYPE | MIIM_STATE | MIIM_STRING,
..Default::default()
};
unsafe {
GetMenuItemInfoW(hmenu, command_id, false, &mut mii).map_err(Error::GetMenuItemInfo)?;
}
let mut label_buf = vec![0u16; (mii.cch + 1) as usize];
mii.dwTypeData = windows::core::PWSTR(label_buf.as_mut_ptr());
mii.cch += 1;
unsafe {
let _ = GetMenuItemInfoW(hmenu, command_id, false, &mut mii);
}
let label =
String::from_utf16_lossy(&label_buf[..mii.cch as usize]).replace('&', "");
let command_string = if command_id >= ID_FIRST {
get_verb(ctx_menu, command_id - ID_FIRST)
} else {
None
};
Ok(MenuItem {
id: command_id,
label,
command_string,
is_separator: false,
is_disabled: mii.fState.contains(MFS_DISABLED),
is_checked: mii.fState.contains(MFS_CHECKED),
is_default: mii.fState.contains(MFS_DEFAULT),
submenu: None,
})
}
fn inject_clipboard_items(hmenu: HMENU) {
let has_files = clipboard_has_files();
if has_files {
let paste_label: Vec<u16> = "貼り付け(V)\0".encode_utf16().collect();
let mii = MENUITEMINFOW {
cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
fMask: MIIM_ID | MIIM_STRING | MIIM_FTYPE,
fType: MFT_STRING,
wID: ID_PASTE_INJECTED,
dwTypeData: windows::core::PWSTR(paste_label.as_ptr() as *mut _),
cch: paste_label.len() as u32 - 1,
..Default::default()
};
unsafe {
let _ = InsertMenuItemW(hmenu, 0, true, &mii);
}
let sep = MENUITEMINFOW {
cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
fMask: MIIM_FTYPE,
fType: MFT_SEPARATOR,
..Default::default()
};
unsafe {
let _ = InsertMenuItemW(hmenu, 1, true, &sep);
}
}
}
fn clipboard_has_files() -> bool {
let data_obj = unsafe { OleGetClipboard() };
let data_obj = match data_obj {
Ok(d) => d,
Err(_) => return false,
};
let fmt = FORMATETC {
cfFormat: CF_HDROP,
ptd: std::ptr::null_mut(),
dwAspect: 1, lindex: -1,
tymed: 1, };
let result = unsafe { data_obj.QueryGetData(&fmt) };
result.is_ok()
}