| 1 | use std::io; |
| 2 | use std::os::raw::*; |
| 3 | use std::path::{Path, PathBuf}; |
| 4 | use std::str::Utf8Error; |
| 5 | use std::sync::Arc; |
| 6 | |
| 7 | use percent_encoding::percent_decode; |
| 8 | use x11rb::protocol::xproto::{self, ConnectionExt}; |
| 9 | |
| 10 | use super::atoms::AtomName::None as DndNone; |
| 11 | use super::atoms::*; |
| 12 | use super::{util, CookieResultExt, X11Error, XConnection}; |
| 13 | |
| 14 | #[derive (Debug, Clone, Copy)] |
| 15 | pub enum DndState { |
| 16 | Accepted, |
| 17 | Rejected, |
| 18 | } |
| 19 | |
| 20 | #[derive (Debug)] |
| 21 | pub enum DndDataParseError { |
| 22 | EmptyData, |
| 23 | InvalidUtf8(#[allow (dead_code)] Utf8Error), |
| 24 | HostnameSpecified(#[allow (dead_code)] String), |
| 25 | UnexpectedProtocol(#[allow (dead_code)] String), |
| 26 | UnresolvablePath(#[allow (dead_code)] io::Error), |
| 27 | } |
| 28 | |
| 29 | impl From<Utf8Error> for DndDataParseError { |
| 30 | fn from(e: Utf8Error) -> Self { |
| 31 | DndDataParseError::InvalidUtf8(e) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | impl From<io::Error> for DndDataParseError { |
| 36 | fn from(e: io::Error) -> Self { |
| 37 | DndDataParseError::UnresolvablePath(e) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | pub struct Dnd { |
| 42 | xconn: Arc<XConnection>, |
| 43 | // Populated by XdndEnter event handler |
| 44 | pub version: Option<c_long>, |
| 45 | pub type_list: Option<Vec<xproto::Atom>>, |
| 46 | // Populated by XdndPosition event handler |
| 47 | pub source_window: Option<xproto::Window>, |
| 48 | // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) |
| 49 | pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>, |
| 50 | } |
| 51 | |
| 52 | impl Dnd { |
| 53 | pub fn new(xconn: Arc<XConnection>) -> Result<Self, X11Error> { |
| 54 | Ok(Dnd { xconn, version: None, type_list: None, source_window: None, result: None }) |
| 55 | } |
| 56 | |
| 57 | pub fn reset(&mut self) { |
| 58 | self.version = None; |
| 59 | self.type_list = None; |
| 60 | self.source_window = None; |
| 61 | self.result = None; |
| 62 | } |
| 63 | |
| 64 | pub unsafe fn send_status( |
| 65 | &self, |
| 66 | this_window: xproto::Window, |
| 67 | target_window: xproto::Window, |
| 68 | state: DndState, |
| 69 | ) -> Result<(), X11Error> { |
| 70 | let atoms = self.xconn.atoms(); |
| 71 | let (accepted, action) = match state { |
| 72 | DndState::Accepted => (1, atoms[XdndActionPrivate]), |
| 73 | DndState::Rejected => (0, atoms[DndNone]), |
| 74 | }; |
| 75 | self.xconn |
| 76 | .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ |
| 77 | this_window, |
| 78 | accepted, |
| 79 | 0, |
| 80 | 0, |
| 81 | action as _, |
| 82 | ])? |
| 83 | .ignore_error(); |
| 84 | |
| 85 | Ok(()) |
| 86 | } |
| 87 | |
| 88 | pub unsafe fn send_finished( |
| 89 | &self, |
| 90 | this_window: xproto::Window, |
| 91 | target_window: xproto::Window, |
| 92 | state: DndState, |
| 93 | ) -> Result<(), X11Error> { |
| 94 | let atoms = self.xconn.atoms(); |
| 95 | let (accepted, action) = match state { |
| 96 | DndState::Accepted => (1, atoms[XdndActionPrivate]), |
| 97 | DndState::Rejected => (0, atoms[DndNone]), |
| 98 | }; |
| 99 | self.xconn |
| 100 | .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ |
| 101 | this_window, |
| 102 | accepted, |
| 103 | action as _, |
| 104 | 0, |
| 105 | 0, |
| 106 | ])? |
| 107 | .ignore_error(); |
| 108 | |
| 109 | Ok(()) |
| 110 | } |
| 111 | |
| 112 | pub unsafe fn get_type_list( |
| 113 | &self, |
| 114 | source_window: xproto::Window, |
| 115 | ) -> Result<Vec<xproto::Atom>, util::GetPropertyError> { |
| 116 | let atoms = self.xconn.atoms(); |
| 117 | self.xconn.get_property( |
| 118 | source_window, |
| 119 | atoms[XdndTypeList], |
| 120 | xproto::Atom::from(xproto::AtomEnum::ATOM), |
| 121 | ) |
| 122 | } |
| 123 | |
| 124 | pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { |
| 125 | let atoms = self.xconn.atoms(); |
| 126 | self.xconn |
| 127 | .xcb_connection() |
| 128 | .convert_selection( |
| 129 | window, |
| 130 | atoms[XdndSelection], |
| 131 | atoms[TextUriList], |
| 132 | atoms[XdndSelection], |
| 133 | time, |
| 134 | ) |
| 135 | .expect_then_ignore_error("Failed to send XdndSelection event" ) |
| 136 | } |
| 137 | |
| 138 | pub unsafe fn read_data( |
| 139 | &self, |
| 140 | window: xproto::Window, |
| 141 | ) -> Result<Vec<c_uchar>, util::GetPropertyError> { |
| 142 | let atoms = self.xconn.atoms(); |
| 143 | self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) |
| 144 | } |
| 145 | |
| 146 | pub fn parse_data(&self, data: &mut [c_uchar]) -> Result<Vec<PathBuf>, DndDataParseError> { |
| 147 | if !data.is_empty() { |
| 148 | let mut path_list = Vec::new(); |
| 149 | let decoded = percent_decode(data).decode_utf8()?.into_owned(); |
| 150 | for uri in decoded.split(" \r\n" ).filter(|u| !u.is_empty()) { |
| 151 | // The format is specified as protocol://host/path |
| 152 | // However, it's typically simply protocol:///path |
| 153 | let path_str = if uri.starts_with("file://" ) { |
| 154 | let path_str = uri.replace("file://" , "" ); |
| 155 | if !path_str.starts_with('/' ) { |
| 156 | // A hostname is specified |
| 157 | // Supporting this case is beyond the scope of my mental health |
| 158 | return Err(DndDataParseError::HostnameSpecified(path_str)); |
| 159 | } |
| 160 | path_str |
| 161 | } else { |
| 162 | // Only the file protocol is supported |
| 163 | return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); |
| 164 | }; |
| 165 | |
| 166 | let path = Path::new(&path_str).canonicalize()?; |
| 167 | path_list.push(path); |
| 168 | } |
| 169 | Ok(path_list) |
| 170 | } else { |
| 171 | Err(DndDataParseError::EmptyData) |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | |