Skip to content

Commit

Permalink
Implements a copy to clipboard if opened without shortcut
Browse files Browse the repository at this point in the history
Implements as well a shared state
  • Loading branch information
Maeeen committed Sep 1, 2024
1 parent bf3d1ec commit f25b378
Show file tree
Hide file tree
Showing 16 changed files with 386 additions and 193 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ winresource = "0.1.17"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_System_Threading", "Win32_Graphics_Gdi", "Win32_UI_Accessibility", "Win32_System_Com"
"Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", # Hooking
"Win32_Graphics_Gdi", "Win32_UI_Accessibility", "Win32_System_Com", # Caret locator and various monitor informations
"Win32_System_Memory", "Win32_System_DataExchange", "Win32_System_Ole" # Clipboard
]}
raw-window-handle = "0.6.2"
emoji-picker-hooker = { path = "./emoji-picker-hooker", optional = true }
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ Multi-platform features:
* [ ] Add a *tooltip* to see the name of the emojis
* [ ] Better name everything (notably, features)
* [x] Emoji groups
* [ ] Should copy to clipboard if no text box is focused
* [x] Should copy to clipboard if no text box is focused
* Copies to clipboard if opened without the shortcut.
* [x] Skin tones
* [ ] More support for multiple-skin tone emojis
* [x] A tray-icon (maybe?) Not a good idea to have a process floating around without showing its existence to the user.
Expand Down
6 changes: 1 addition & 5 deletions src/emoji.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use std::path::PathBuf;

// These one may be required when implementing a better search.
// pub type EmojiMap<Internal = EmojiWrapper> = BTreeMap<String, Internal>;
// pub type EmojiGrouppedMap<Internal> = HashMap<emojis::Group, EmojiMap<Internal>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EmojiWrapper(pub &'static emojis::Emoji);

Expand Down Expand Up @@ -51,12 +48,11 @@ impl EmojiWrapper {
emojis::SkinTone::DarkAndMediumLight => Some(23),
emojis::SkinTone::DarkAndMedium => Some(24),
emojis::SkinTone::DarkAndMediumDark => Some(25),
_ => None
_ => None,
}
}
}


#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EmojiGroupWrapper(pub emojis::Group);

Expand Down
27 changes: 15 additions & 12 deletions src/emoji_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::cell::RefCell;

use crate::{
emoji::{EmojiGroupWrapper, EmojiWrapper},
EmojiGroupModel, EmojiSkinToneModel, EmojiModel,
EmojiGroupModel, EmojiModel, EmojiSkinToneModel,
};
use slint::{Model, ModelNotify, ModelRc, VecModel};

Expand All @@ -17,12 +17,13 @@ impl From<EmojiWrapper> for EmojiModel {
name: e.name().into(),
code: e.code().into(),
image: image.unwrap_or_default(),
skin_tones: ModelRc::new(VecModel::from(
match e.skin_tones() {
Some(iterator) => iterator.map(EmojiSkinToneModel::try_from).map(|f| f.ok()).flatten().collect(),
None => vec![],
},
)),
skin_tones: ModelRc::new(VecModel::from(match e.skin_tones() {
Some(iterator) => iterator
.map(EmojiSkinToneModel::try_from)
.flat_map(|f| f.ok())
.collect(),
None => vec![],
})),
}
}
}
Expand All @@ -36,11 +37,13 @@ impl TryFrom<EmojiWrapper> for EmojiSkinToneModel {
let filename = e.get_filename_path();
let image = slint::Image::load_from_path(&filename);

e.skin_tone().map(|skin_tone| EmojiSkinToneModel {
code: e.code().into(),
image: image.unwrap_or_default(),
skin_tone: skin_tone.into(),
}).ok_or(())
e.skin_tone()
.map(|skin_tone| EmojiSkinToneModel {
code: e.code().into(),
image: image.unwrap_or_default(),
skin_tone: skin_tone.into(),
})
.ok_or(())
}
}

Expand Down
62 changes: 39 additions & 23 deletions src/handlers/back_click.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ use windows::{
},
};

use crate::handler::{Handler, MpscNotifier, Notifier};
use crate::EmojiPickerWindow;
use crate::{
handler::{Handler, MpscNotifier},
SharedApp,
};

use super::utils::ToHWND;
use super::{
utils::ToHWND, CloserNotifier, NotifierReason, NotifiersArgs, OnCloseHandler, OnOpenHandler,
};

pub struct OutsideClickHandlers<'a> {
pub on_open_handler: Handler<'a, EmojiPickerWindow>,
pub on_close_handler: Handler<'a, EmojiPickerWindow>,
pub closer: Box<dyn Notifier<()>>,
pub on_open_handler: OnOpenHandler<'a>,
pub on_close_handler: OnCloseHandler<'a>,
pub closer: CloserNotifier,
}

unsafe extern "system" fn transparent_window_proc(
Expand All @@ -55,7 +60,7 @@ unsafe extern "system" fn transparent_window_proc(
WM_LBUTTONUP..=WM_MBUTTONDBLCLK => {
// Any mouse event
if let Some(tx) = TX.as_ref() {
let _ = tx.send(());
let _ = tx.send(NotifierReason::Backclick);
}
// Hide window (despite the name, it does not destroy the window.)
if let Err(e) = CloseWindow(hwnd) {
Expand Down Expand Up @@ -106,10 +111,13 @@ unsafe fn setup_transp_window_dimensions(hwnd: HWND) -> Option<()> {
}

// Yeah, this is not pretty. But it's a way to send a message to the event loop.
static mut TX: Option<mpsc::SyncSender<()>> = None;
static mut TX: Option<mpsc::SyncSender<NotifierReason>> = None;

fn generate_transparent_window(app: &EmojiPickerWindow, tx: mpsc::SyncSender<()>) -> Option<HWND> {
let hwnd = app.window().to_hwnd()?;
fn generate_transparent_window(
window: &slint::Window,
tx: mpsc::SyncSender<NotifiersArgs>,
) -> Option<HWND> {
let hwnd = window.to_hwnd()?;
let hinstance: HINSTANCE =
unsafe { HINSTANCE(GetWindowLongPtrW(hwnd, GWL_HINSTANCE) as *mut _) };
const CLASS_NAME: PCWSTR = w!("EmojiPickerTransparentWindow");
Expand Down Expand Up @@ -163,23 +171,31 @@ fn generate_transparent_window(app: &EmojiPickerWindow, tx: mpsc::SyncSender<()>
}
}

pub fn generate_handlers<'a>(app: &EmojiPickerWindow) -> Option<OutsideClickHandlers<'a>> {
let (tx, rx) = mpsc::sync_channel::<()>(1);
pub fn generate_handlers<'a>(ui: &EmojiPickerWindow) -> Option<OutsideClickHandlers<'a>> {
let (tx, rx) = mpsc::sync_channel::<NotifiersArgs>(1);
// The cast to isize is to send the HWND.
// TODO: wrap it and mark it as Send.
let transp_win = generate_transparent_window(app, tx)?.0 as isize;

let on_open_handler = Handler::new(move |app: &EmojiPickerWindow| unsafe {
if let Some(win) = app.window().to_hwnd() {
let transp_win = HWND(transp_win as *mut _);
let _ = setup_transp_window_dimensions(transp_win);
// This is a bit of a hack, but we set the main window of the emoji
// picker to be the child window of the transparent window. This way,
// the emoji picker will be above the transparent window.
let _ = SetWindowLongPtrW(win, GWL_HWNDPARENT, transp_win.0 as isize);

let _ = ShowWindow(transp_win, SW_NORMAL);
let transp_win = generate_transparent_window(ui.window(), tx)?.0 as isize;

let on_open_handler = Handler::new(move |args: &(SharedApp, NotifiersArgs)| {
let (app, reason) = args;

if *reason != NotifierReason::Shortcut {
return;
}

let _ = app.weak_ui().upgrade_in_event_loop(move |ui| unsafe {
if let Some(win) = ui.window().to_hwnd() {
let transp_win = HWND(transp_win as *mut _);
let _ = setup_transp_window_dimensions(transp_win);
// This is a bit of a hack, but we set the main window of the emoji
// picker to be the child window of the transparent window. This way,
// the emoji picker will be above the transparent window.
let _ = SetWindowLongPtrW(win, GWL_HWNDPARENT, transp_win.0 as isize);

let _ = ShowWindow(transp_win, SW_NORMAL);
}
});
});
let on_close_handler = Handler::new(move |_| unsafe {
let _ = ShowWindow(HWND(transp_win as *mut _), SW_HIDE);
Expand Down
23 changes: 15 additions & 8 deletions src/handlers/caret_locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ use windows::{
},
};

use crate::handler::Handler;
use crate::EmojiPickerWindow;
use crate::{handler::Handler, SharedApp};

use super::utils::ToHWND;
use super::{utils::ToHWND, BeforeOpenHandler, NotifierReason};

struct Position {
x: i32,
Expand Down Expand Up @@ -144,11 +143,19 @@ fn get_caret_position() -> Option<CaretPosition> {
}
}

pub fn get_handler<'a>() -> Handler<'a, EmojiPickerWindow> {
Handler::new(|app: &EmojiPickerWindow| {
if let Some(caret_location) = get_caret_position() {
let window_pos = get_window_position(app.window(), caret_location);
app.window().set_position(window_pos)
pub fn get_handler<'a>() -> BeforeOpenHandler<'a> {
Handler::new(|args: &(SharedApp, NotifierReason)| {
let (app, reason) = args;

if *reason != NotifierReason::Shortcut {
return;
}

let _ = app.weak_ui().upgrade_in_event_loop(move |ui| {
if let Some(caret_location) = get_caret_position() {
let window_pos = get_window_position(ui.window(), caret_location);
ui.window().set_position(window_pos)
}
});
})
}
10 changes: 0 additions & 10 deletions src/handlers/close_shortcut.rs

This file was deleted.

126 changes: 101 additions & 25 deletions src/handlers/emoji_selected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,110 @@ use windows::Win32::UI::{
WindowsAndMessaging::GetMessageExtraInfo,
};

use crate::handler::Handler;
use crate::{handler::Handler, SharedApp};

use super::EmojiSelectedHandler;

// On a Windows system, the emoji picker will send the requested String to the active window.
pub fn get_handler<'a>() -> Handler<'a, String> {
Handler::new(|code: &String| {
let encoded = str::encode_utf16(code);
let extra_info = unsafe { GetMessageExtraInfo() };
let input_struct_kd = encoded.into_iter().map(|c| INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
dwExtraInfo: extra_info.0 as usize,
wVk: VIRTUAL_KEY(0),
wScan: c,
dwFlags: KEYEVENTF_UNICODE,
time: 0,
},
pub fn get_handler<'a>() -> EmojiSelectedHandler<'a> {
Handler::new(|args: &(SharedApp, String)| {
let (app, code) = args;

if cfg!(target_os = "windows") && app.get_reason().should_type_emoji() {
type_emoji(code);
} else {
clipboard(code);
}
})
}

/// Types the given String as Unicode characters.
#[cfg(target_os = "windows")]
pub fn type_emoji(code: &str) {
let encoded = str::encode_utf16(code);
let extra_info = unsafe { GetMessageExtraInfo() };
let input_struct_kd = encoded.into_iter().map(|c| INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
dwExtraInfo: extra_info.0 as usize,
wVk: VIRTUAL_KEY(0),
wScan: c,
dwFlags: KEYEVENTF_UNICODE,
time: 0,
},
});
let input_struct_kf = input_struct_kd.clone().map(|mut k| {
unsafe {
k.Anonymous.ki.dwFlags |= KEYEVENTF_KEYUP;
}
k
});
let input_struct = input_struct_kd.chain(input_struct_kf).collect::<Vec<_>>();
},
});
let input_struct_kf = input_struct_kd.clone().map(|mut k| {
unsafe {
SendInput(input_struct.as_slice(), std::mem::size_of::<INPUT>() as i32);
k.Anonymous.ki.dwFlags |= KEYEVENTF_KEYUP;
}
})
k
});
let input_struct = input_struct_kd.chain(input_struct_kf).collect::<Vec<_>>();
unsafe {
SendInput(input_struct.as_slice(), std::mem::size_of::<INPUT>() as i32);
}
}

/// Copies the string to the clipboard.
#[cfg(target_os = "windows")]
pub fn clipboard(code: &str) {
use windows::Win32::{
Foundation::{GlobalFree, HANDLE, NO_ERROR},
System::{
DataExchange::{CloseClipboard, EmptyClipboard, OpenClipboard, SetClipboardData},
Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE},
Ole::CF_UNICODETEXT,
},
};

let utf16: Vec<u16> = str::encode_utf16(code)
.chain(std::iter::once(0u16))
.collect();
let len = utf16.len();
unsafe {
if OpenClipboard(None).is_err() {
eprintln!("Failed to open clipboard");
return;
};
if EmptyClipboard().is_err() {
eprint!("Failed to empty clipboard");
let _ = CloseClipboard();
};
let global = GlobalAlloc(GMEM_MOVEABLE, len * std::mem::size_of::<u16>());

if global.is_err() {
eprintln!("Failed to allocate memory");
let _ = CloseClipboard();
return;
}

let global = global.unwrap();

let addr: *mut _ = GlobalLock(global);
if addr.is_null() {
eprintln!("Failed to lock memory");
let _ = GlobalFree(global);
let _ = CloseClipboard();
return;
}

let slice = std::slice::from_raw_parts_mut(addr as *mut u16, len);
slice.copy_from_slice(&utf16);
let unlock_result = GlobalUnlock(global); // This… when… completes… successfully… returns… an… Err…?
if unlock_result.is_ok() || unlock_result.unwrap_err().code() != NO_ERROR.into() {
eprintln!("Failed to unlock memory");
let _ = GlobalFree(global);
let _ = CloseClipboard();
return;
}
if SetClipboardData(CF_UNICODETEXT.0.into(), HANDLE(global.0)).is_err() {
eprint!("Failed to set clipboard data");
let _ = GlobalFree(global);
let _ = CloseClipboard();
return;
};
let _ = CloseClipboard();
};
}
Loading

0 comments on commit f25b378

Please sign in to comment.