diff --git a/.changes/0.7.0.md b/.changes/0.7.0.md new file mode 100644 index 0000000..5fbe8e3 --- /dev/null +++ b/.changes/0.7.0.md @@ -0,0 +1,6 @@ +--- +"tauri-plugin-context-menu": "minor" +--- + +- Add checked items support #10 +- Refactoring \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c6329e1..a8c1000 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-context-menu" -version = "0.6.2" +version = "0.7.0" authors = [ "c2r0b" ] description = "Handle native Context Menu in Tauri" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index a87b2f7..d0933f8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A Tauri plugin to display native context menu on Tauri v1.x. The Tauri API does not support native context menu out of the box, so this plugin is created to fill the gap. -![Screenshot](./assets/screenshot.png) +image Official context menu support has been added in Tauri v2.x (see [here](https://github.com/tauri-apps/tauri/issues/4338)), so this plugin is intended to be used with Tauri v1.x only. @@ -81,6 +81,7 @@ window.addEventListener("contextmenu", async (e) => { { label: "Subitem 2", disabled: false, + checked: true, event: "subitem2clicked", } ] @@ -127,6 +128,7 @@ List of options that can be passed to the plugin. | disabled | `boolean` | `optional` | `false` | Whether the menu item is disabled. | | event | `string` | `optional` | | Event name to be emitted when the menu item is clicked. | You can pass a function to be executed instead of an event name. | | payload | `string` | `optional` | | Payload to be passed to the event. | You can pass any type of data. | +| checked | `boolean` | `optional` | | Whether the menu item is checked. | | subitems | `MenuItem[]` | `optional` | `[]` | List of sub menu items to be displayed. | | shortcut | `string` | `optional` | | Keyboard shortcut displayed on the right. | | icon | `MenuItemIcon` | `optional` | | Icon to be displayed on the left. | @@ -230,4 +232,4 @@ import { listen } from "@tauri-apps/api/event"; listen("menu-did-close", () => { alert("menu closed"); }); -``` \ No newline at end of file +``` diff --git a/assets/screenshot.png b/assets/screenshot.png index 3739912..f2189e1 100644 Binary files a/assets/screenshot.png and b/assets/screenshot.png differ diff --git a/examples/ts-utility/package.json b/examples/ts-utility/package.json index 483fd29..7a9f0b8 100644 --- a/examples/ts-utility/package.json +++ b/examples/ts-utility/package.json @@ -1,6 +1,6 @@ { "name": "ts-utility-example", - "version": "0.6.2", + "version": "0.7.0", "main": "index.js", "type": "module", "scripts": { diff --git a/examples/ts-utility/src/index.ts b/examples/ts-utility/src/index.ts index df7fee8..2268ee6 100644 --- a/examples/ts-utility/src/index.ts +++ b/examples/ts-utility/src/index.ts @@ -32,6 +32,7 @@ onEventShowMenu('contextmenu', async (_e:MouseEvent) => { subitems: [ { label: "My first subitem", + checked: true, event: () => { alert('My first subitem clicked'); }, @@ -39,6 +40,7 @@ onEventShowMenu('contextmenu', async (_e:MouseEvent) => { }, { label: "My second subitem", + checked: false, disabled: true } ] diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json index 9cbcd7c..c0986e6 100644 --- a/examples/vanilla/package.json +++ b/examples/vanilla/package.json @@ -1,6 +1,6 @@ { "name": "vanilla-example", - "version": "0.6.2", + "version": "0.7.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/examples/vanilla/public/index.js b/examples/vanilla/public/index.js index 99a6995..2d60e65 100644 --- a/examples/vanilla/public/index.js +++ b/examples/vanilla/public/index.js @@ -65,10 +65,12 @@ window.addEventListener('contextmenu', (e) => { { label: "My first subitem", event: "my_first_subitem", + checked: true, shortcut: "ctrl+m" }, { label: "My second subitem", + checked: false, disabled: true } ] diff --git a/lerna.json b/lerna.json index 63093f9..70affe0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,4 +1,4 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.6.2" + "version": "0.7.0" } diff --git a/package.json b/package.json index d46bf3c..635d59d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tauri-plugin-context-menu", - "version": "0.6.2", + "version": "0.7.0", "author": "c2r0b", "description": "", "homepage": "https://github.com/c2r0b/tauri-plugin-context-menu", diff --git a/plugin/package.json b/plugin/package.json index c4d172a..6aeab16 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,6 +1,6 @@ { "name": "tauri-plugin-context-menu", - "version": "0.6.2", + "version": "0.7.0", "author": "c2r0b", "type": "module", "description": "", diff --git a/plugin/types.ts b/plugin/types.ts index 91d342f..20c7ae0 100644 --- a/plugin/types.ts +++ b/plugin/types.ts @@ -22,6 +22,7 @@ export interface Item { is_separator?: boolean event?: string|((e?:CallbackEvent) => any) payload?: any + checked?: boolean shortcut?: string icon?: Icon subitems?: Item[] diff --git a/src/lib.rs b/src/lib.rs index 24dfba9..d8e4e1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,5 @@ use serde::Deserialize; -use std::sync::Arc; -use tauri::{ - plugin::Builder, plugin::Plugin, plugin::TauriPlugin, Invoke, Manager, Runtime, State, Window, -}; +use tauri::{plugin::Builder, plugin::TauriPlugin, Runtime, Window}; mod menu_item; @@ -35,81 +32,16 @@ pub struct Position { is_absolute: Option, } -pub struct ContextMenu { - invoke_handler: Arc) + Send + Sync>, -} - -impl Default for ContextMenu { - fn default() -> Self { - Self { - invoke_handler: Arc::new(|_| {}), - } - } -} - -impl Clone for ContextMenu { - fn clone(&self) -> Self { - Self { - invoke_handler: Arc::clone(&self.invoke_handler), - } - } -} - -impl ContextMenu { - // Method to create a new ContextMenu - pub fn new) + Send + Sync>(handler: F) -> Self { - Self { - invoke_handler: Arc::new(handler), - } - } - - #[cfg(target_os = "linux")] - fn show_context_menu( - &self, - window: Window, - pos: Option, - items: Option>, - ) { - os::show_context_menu(window, pos, items); - } - - #[cfg(any(target_os = "macos", target_os = "windows"))] - fn show_context_menu( - &self, - window: Window, - pos: Option, - items: Option>, - ) { - let context_menu = Arc::new(self.clone()); - os::show_context_menu(context_menu, window, pos, items); - } -} - -impl Plugin for ContextMenu { - fn name(&self) -> &'static str { - "context_menu" - } - - fn extend_api(&mut self, invoke: Invoke) { - (self.invoke_handler)(invoke); - } -} - #[tauri::command] fn show_context_menu( - manager: State<'_, ContextMenu>, window: Window, pos: Option, items: Option>, ) { - manager.show_context_menu(window, pos, items); + os::show_context_menu(window, pos, items); } pub fn init() -> TauriPlugin { Builder::new("context_menu") .invoke_handler(tauri::generate_handler![show_context_menu]) - .setup(|app| { - app.manage(ContextMenu::::default()); - Ok(()) - }) .build() } diff --git a/src/linux.rs b/src/linux.rs index fcc5525..a86b3c3 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -1,5 +1,5 @@ use gdk::{keys::Key, Display, ModifierType}; -use gtk::{prelude::*, traits::WidgetExt, AccelFlags, AccelGroup, Menu, MenuItem as GtkMenuItem}; +use gtk::{prelude::*, traits::WidgetExt, AccelFlags, AccelGroup, Menu}; use std::{mem, thread::sleep, time}; use tauri::{Runtime, Window}; @@ -109,7 +109,18 @@ fn append_menu_item( if item.is_separator.unwrap_or(false) { menu.append(>k::SeparatorMenuItem::builder().visible(true).build()); } else { - let menu_item = GtkMenuItem::new(); + let menu_item = match item.checked { + Some(state) => { + // Create a CheckMenuItem for checkable items + let check_menu_item = gtk::CheckMenuItem::new(); + check_menu_item.set_active(state); + check_menu_item.upcast() + } + None => { + // Create a regular MenuItem for non-checkable items + gtk::MenuItem::new() + } + }; // Create a Box to hold the image and label let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 0); diff --git a/src/macos.rs b/src/macos.rs index 354660a..dd90943 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -9,7 +9,7 @@ use tauri::{Runtime, Window}; use crate::keymap::{get_key_map, get_modifier_map}; use crate::macos_window_holder::CURRENT_WINDOW; -use crate::{ContextMenu, MenuItem, Position}; +use crate::{MenuItem, Position}; extern "C" fn menu_item_action(_self: &Object, _cmd: Sel, _item: id) { // Get the window from the CURRENT_WINDOW static @@ -72,7 +72,7 @@ fn register_menu_item_action() -> Sel { selector(selector_name) } -fn create_custom_menu_item(context_menu: &ContextMenu, option: &MenuItem) -> id { +fn create_custom_menu_item(option: &MenuItem) -> id { // If the item is a separator, return a separator item if option.is_separator.unwrap_or(false) { let separator: id = unsafe { msg_send![class!(NSMenuItem), separatorItem] }; @@ -167,22 +167,26 @@ fn create_custom_menu_item(context_menu: &ContextMenu, option: &M let submenu: id = msg_send![class!(NSMenu), new]; let _: () = msg_send![submenu, setAutoenablesItems:NO]; for subitem in subitems.iter() { - let sub_menu_item: id = create_custom_menu_item(&context_menu, subitem); + let sub_menu_item: id = create_custom_menu_item::(subitem); let _: () = msg_send![submenu, addItem:sub_menu_item]; } let _: () = msg_send![item, setSubmenu:submenu]; } + // Handle checkable menu items + let state = match option.checked { + Some(true) => 1, + _ => 0, + }; + let _: () = msg_send![item, setState:state]; + item }; + menu_item } -fn create_context_menu( - context_menu: &ContextMenu, - options: &[MenuItem], - window: &Window, -) -> id { +fn create_context_menu(options: &[MenuItem], window: &Window) -> id { let _: () = CURRENT_WINDOW.set_window(window.clone()); unsafe { let title = NSString::alloc(nil).init_str("Menu"); @@ -192,7 +196,7 @@ fn create_context_menu( let _: () = msg_send![menu, setAutoenablesItems:NO]; for option in options.iter().cloned() { - let item: id = create_custom_menu_item(&context_menu, &option); + let item: id = create_custom_menu_item::(&option); let _: () = msg_send![menu, addItem:item]; } @@ -207,7 +211,6 @@ fn create_context_menu( } pub fn show_context_menu( - context_menu: Arc>, window: Window, pos: Option, items: Option>, @@ -215,7 +218,7 @@ pub fn show_context_menu( let main_queue = dispatch::Queue::main(); main_queue.exec_async(move || { let items_slice = items.as_ref().map(|v| v.as_slice()).unwrap_or(&[]); - let menu = create_context_menu(&*context_menu, items_slice, &window); + let menu = create_context_menu(items_slice, &window); let location = match pos { // Convert web page coordinates to screen coordinates Some(pos) if pos.x != 0.0 || pos.y != 0.0 => unsafe { diff --git a/src/menu_item.rs b/src/menu_item.rs index b56715f..ec7ca74 100644 --- a/src/menu_item.rs +++ b/src/menu_item.rs @@ -9,6 +9,7 @@ pub struct MenuItem { pub payload: Option, pub subitems: Option>, pub icon: Option, + pub checked: Option, pub is_separator: Option, } @@ -29,6 +30,7 @@ impl Default for MenuItem { payload: None, subitems: None, icon: None, + checked: Some(false), is_separator: Some(false), } } diff --git a/src/win.rs b/src/win.rs index b70fee7..3178d37 100644 --- a/src/win.rs +++ b/src/win.rs @@ -9,14 +9,14 @@ use winapi::{ um::winuser::{ AppendMenuW, ClientToScreen, CreatePopupMenu, DestroyMenu, DispatchMessageW, GetCursorPos, GetMessageW, PostQuitMessage, SetMenuItemBitmaps, TrackPopupMenu, TranslateMessage, - MF_BYCOMMAND, MF_DISABLED, MF_ENABLED, MF_POPUP, MF_SEPARATOR, MF_STRING, MSG, - TPM_LEFTALIGN, TPM_RIGHTBUTTON, TPM_TOPALIGN, WM_COMMAND, WM_HOTKEY, + MF_BYCOMMAND, MF_CHECKED, MF_DISABLED, MF_ENABLED, MF_POPUP, MF_SEPARATOR, MF_STRING, MSG, + TPM_LEFTALIGN, TPM_RIGHTBUTTON, TPM_TOPALIGN, WM_COMMAND, }, }; use crate::keymap::get_key_map; use crate::win_image_handler::{convert_to_hbitmap, load_bitmap_from_file}; -use crate::{ContextMenu, MenuItem, Position}; +use crate::{MenuItem, Position}; const ID_MENU_ITEM_BASE: u32 = 1000; @@ -74,6 +74,11 @@ fn append_menu_item(menu: HMENU, item: &MenuItem, counter: &mut u32) -> Result(id: u32, window: Window) { } pub fn show_context_menu( - _context_menu: Arc>, window: Window, pos: Option, items: Option>,