Skip to main content

native_windows_gui2/controls/
tray_notification.rs

1use super::{ControlBase, ControlHandle};
2use crate::win32::base_helper::to_utf16;
3use crate::win32::window_helper as wh;
4use crate::{Icon, NwgError};
5use std::{mem, ptr};
6use winapi::um::shellapi::{
7    NIIF_ERROR, NIIF_INFO, NIIF_LARGE_ICON, NIIF_NONE, NIIF_NOSOUND, NIIF_RESPECT_QUIET_TIME,
8    NIIF_USER, NIIF_WARNING,
9};
10use winapi::um::shellapi::{NOTIFYICONDATAW, Shell_NotifyIconW};
11
12const NOT_BOUND: &'static str = "TrayNotification is not yet bound to a winapi object";
13const BAD_HANDLE: &'static str = "INTERNAL ERROR: TrayNotification handle is not HWND!";
14
15bitflags! {
16    pub struct TrayNotificationFlags: u32 {
17        const NO_ICON = NIIF_NONE;
18        const INFO_ICON = NIIF_INFO;
19        const WARNING_ICON = NIIF_WARNING;
20        const ERROR_ICON = NIIF_ERROR;
21        const USER_ICON = NIIF_USER;
22        const SILENT = NIIF_NOSOUND;
23        const LARGE_ICON = NIIF_LARGE_ICON;
24        const QUIET = NIIF_RESPECT_QUIET_TIME;
25    }
26}
27
28///
29/// A control that handle system tray notification.
30/// A TrayNotification wraps a single icon in the Windows system tray.
31///
32/// An application can have many TrayNotification, but each window (aka parent) can only have a single traynotification.
33/// It is possible to create system tray only application with the `MessageOnlyWindow` control.
34///
35/// A system tray will receive events if `callback` is set to true in the builder (the default behaviour).
36/// The control will generate mouse events such as `OnMouseMove` when the user interact with the tray icon or the message popup.
37/// A system tray will also receive a `OnContextMenu` when the user right click the icon. It is highly recommended handle this message and display a popup menu
38///
39/// You can't get information on the state of a tray notification (such as visibility) because Windows don't want you to.
40///
41/// **Builder parameters:**
42///
43/// * `parent`:       **Required.** The tray notification parent container.
44/// * `icon`:         **Required.** The icon to display in the system tray
45/// * `tips`:         Display a simple tooltip when hovering the icon in the system tray
46/// * `flags`:        A combination of the TrayNotificationFlags values.
47/// * `visible`:      If the icon should be visible in the system tray
48/// * `realtime`:     If the balloon notification cannot be displayed immediately, discard it.
49/// * `info`:         Display a fancy tooltip when the system tray icon is hovered (replaces tip)
50/// * `balloon_icon`: The icon to display in the fancy tooltip
51/// * `info_title`:   The title of the fancy tooltip
52///
53/// **Control events:**
54///
55/// * `OnContextMenu`: When the user right clicks on the system tray icon
56/// * `MousePressLeftUp`: When the user left click the system tray icon
57/// * `OnTrayNotificationShow`: When a TrayNotification info popup (not the tooltip) is shown
58/// * `OnTrayNotificationHide`: When a TrayNotification info popup (not the tooltip) is hidden
59/// * `OnTrayNotificationTimeout`: When a TrayNotification is closed due to a timeout
60/// * `OnTrayNotificationUserClose`: When a TrayNotification is closed due to a user click
61///
62/// ## Example
63///
64/// ```rust
65/// use native_windows_gui2 as nwg;
66///
67/// fn notice_user(tray: &nwg::TrayNotification, image: &nwg::Icon) {
68///     let flags = nwg::TrayNotificationFlags::USER_ICON | nwg::TrayNotificationFlags::LARGE_ICON;
69///     tray.show("Hello World", Some("Welcome to my application"), Some(flags), Some(image));
70/// }
71/// ```
72///
73/// ```rust
74/// use native_windows_gui2 as nwg;
75/// fn build_tray(tray: &mut nwg::TrayNotification, window: &nwg::Window, icon: &nwg::Icon) {
76///     nwg::TrayNotification::builder()
77///         .parent(window)
78///         .icon(Some(icon))
79///         .tip(Some("Hello"))
80///         .build(tray);
81/// }
82/// ```
83///
84/// Winapi docs: <https://docs.microsoft.com/en-us/windows/win32/shell/notification-area>
85
86#[derive(Default, PartialEq, Eq)]
87pub struct TrayNotification {
88    pub handle: ControlHandle,
89}
90
91impl TrayNotification {
92    pub fn builder<'a>() -> TrayNotificationBuilder<'a> {
93        TrayNotificationBuilder {
94            parent: None,
95            icon: None,
96            balloon_icon: None,
97            tip: None,
98            info: None,
99            info_title: None,
100            flags: TrayNotificationFlags::NO_ICON,
101            realtime: false,
102            callback: true,
103            visible: true,
104        }
105    }
106
107    /// Set the visibility of the icon in the system tray
108    pub fn set_visibility(&self, v: bool) {
109        use winapi::um::shellapi::{NIF_STATE, NIM_MODIFY, NIS_HIDDEN};
110
111        if self.handle.blank() {
112            panic!("{}", NOT_BOUND);
113        }
114        self.handle.tray().expect(BAD_HANDLE);
115
116        unsafe {
117            let mut data = self.notify_default();
118            data.uFlags |= NIF_STATE;
119            data.dwState = if v { 0 } else { NIS_HIDDEN };
120            data.dwStateMask = NIS_HIDDEN;
121            Shell_NotifyIconW(NIM_MODIFY, &mut data);
122        }
123    }
124
125    /// Set the tooltip for the tray notification.
126    /// Note: tip will be truncated to 127 characters
127    pub fn set_tip<'a>(&self, tip: &'a str) {
128        use winapi::um::shellapi::{NIF_SHOWTIP, NIF_TIP, NIM_MODIFY};
129
130        if self.handle.blank() {
131            panic!("{}", NOT_BOUND);
132        }
133        self.handle.tray().expect(BAD_HANDLE);
134
135        unsafe {
136            let mut data = self.notify_default();
137
138            data.uFlags = NIF_TIP | NIF_SHOWTIP;
139
140            let tip_v = to_utf16(tip);
141            let length = if tip_v.len() >= 128 { 127 } else { tip_v.len() };
142            for i in 0..length {
143                data.szTip[i] = tip_v[i];
144            }
145
146            Shell_NotifyIconW(NIM_MODIFY, &mut data);
147        }
148    }
149
150    /// Set the focus to the tray icon
151    pub fn set_focus(&self) {
152        use winapi::um::shellapi::NIM_SETFOCUS;
153
154        if self.handle.blank() {
155            panic!("{}", NOT_BOUND);
156        }
157        self.handle.tray().expect(BAD_HANDLE);
158
159        unsafe {
160            let mut data = self.notify_default();
161            Shell_NotifyIconW(NIM_SETFOCUS, &mut data);
162        }
163    }
164
165    /// Update the icon in the system tray
166    pub fn set_icon(&self, icon: &Icon) {
167        use winapi::shared::windef::HICON;
168        use winapi::um::shellapi::{NIF_ICON, NIM_MODIFY};
169
170        if self.handle.blank() {
171            panic!("{}", NOT_BOUND);
172        }
173        self.handle.tray().expect(BAD_HANDLE);
174
175        unsafe {
176            let mut data = self.notify_default();
177
178            data.uFlags = NIF_ICON;
179            data.hIcon = icon.handle as HICON;
180            Shell_NotifyIconW(NIM_MODIFY, &mut data);
181        }
182    }
183
184    /// Shows a popup message on top of the system tray
185    ///
186    /// Parameters:
187    ///   - text: The text in the popup
188    ///   - title: The title of the popup
189    ///   - flags: Flags that specify how the popup is shown. Default is NO_ICON | QUIET.
190    ///   - icon: Icon to display in the popup. Only used if `USER_ICON` is set in flags.
191    ///
192    /// Note 1: text will be truncated to 255 characters
193    /// Note 2: title will be truncated to 63 characters
194    pub fn show<'a>(
195        &self,
196        text: &'a str,
197        title: Option<&'a str>,
198        flags: Option<TrayNotificationFlags>,
199        icon: Option<&'a Icon>,
200    ) {
201        use winapi::shared::windef::HICON;
202        use winapi::um::shellapi::{NIF_INFO, NIM_MODIFY};
203
204        if self.handle.blank() {
205            panic!("{}", NOT_BOUND);
206        }
207        self.handle.tray().expect(BAD_HANDLE);
208
209        let default_flags = TrayNotificationFlags::NO_ICON | TrayNotificationFlags::SILENT;
210
211        unsafe {
212            let mut data = self.notify_default();
213            data.uFlags = NIF_INFO;
214            data.dwInfoFlags = flags.unwrap_or(default_flags).bits();
215            data.hBalloonIcon = icon.map(|i| i.handle as HICON).unwrap_or(ptr::null_mut());
216
217            let info_v = to_utf16(text);
218            let length = if info_v.len() >= 256 {
219                255
220            } else {
221                info_v.len()
222            };
223            for i in 0..length {
224                data.szInfo[i] = info_v[i];
225            }
226
227            let info_title_v = match title {
228                Some(t) => to_utf16(t),
229                None => vec![],
230            };
231
232            let length = if info_title_v.len() >= 256 {
233                255
234            } else {
235                info_title_v.len()
236            };
237            for i in 0..length {
238                data.szInfoTitle[i] = info_title_v[i];
239            }
240
241            Shell_NotifyIconW(NIM_MODIFY, &mut data);
242        }
243    }
244
245    fn notify_default(&self) -> NOTIFYICONDATAW {
246        unsafe {
247            let parent = self.handle.tray().unwrap();
248            NOTIFYICONDATAW {
249                cbSize: mem::size_of::<NOTIFYICONDATAW>() as u32,
250                hWnd: parent,
251                uID: 0,
252                uFlags: 0,
253                uCallbackMessage: 0,
254                hIcon: ptr::null_mut(),
255                szTip: mem::zeroed(),
256                dwState: 0,
257                dwStateMask: 0,
258                szInfo: mem::zeroed(),
259                u: mem::zeroed(),
260                szInfoTitle: mem::zeroed(),
261                dwInfoFlags: 0,
262                guidItem: mem::zeroed(),
263                hBalloonIcon: ptr::null_mut(),
264            }
265        }
266    }
267}
268
269impl Drop for TrayNotification {
270    fn drop(&mut self) {
271        use winapi::um::shellapi::NIM_DELETE;
272
273        if self.handle.tray().is_some() {
274            let mut data = self.notify_default();
275            unsafe {
276                Shell_NotifyIconW(NIM_DELETE, &mut data);
277            }
278        }
279
280        self.handle.destroy();
281    }
282}
283
284pub struct TrayNotificationBuilder<'a> {
285    parent: Option<ControlHandle>,
286    icon: Option<&'a Icon>,
287
288    tip: Option<&'a str>,
289
290    info: Option<&'a str>,
291    info_title: Option<&'a str>,
292    flags: TrayNotificationFlags,
293    balloon_icon: Option<&'a Icon>,
294
295    realtime: bool,
296    callback: bool,
297    visible: bool,
298}
299
300impl<'a> TrayNotificationBuilder<'a> {
301    pub fn parent<C: Into<ControlHandle>>(mut self, p: C) -> TrayNotificationBuilder<'a> {
302        self.parent = Some(p.into());
303        self
304    }
305
306    pub fn icon(mut self, ico: Option<&'a Icon>) -> TrayNotificationBuilder<'a> {
307        self.icon = ico;
308        self
309    }
310
311    pub fn realtime(mut self, r: bool) -> TrayNotificationBuilder<'a> {
312        self.realtime = r;
313        self
314    }
315
316    pub fn callback(mut self, cb: bool) -> TrayNotificationBuilder<'a> {
317        self.callback = cb;
318        self
319    }
320
321    pub fn visible(mut self, v: bool) -> TrayNotificationBuilder<'a> {
322        self.visible = v;
323        self
324    }
325
326    /// Note: balloon_icon is only used if `info` is set AND flags uses `USER_ICON`
327    pub fn balloon_icon(mut self, ico: Option<&'a Icon>) -> TrayNotificationBuilder<'a> {
328        self.balloon_icon = ico;
329        self
330    }
331
332    /// Note: flags are only used if `info` is set
333    pub fn flags(mut self, flags: TrayNotificationFlags) -> TrayNotificationBuilder<'a> {
334        self.flags = flags;
335        self
336    }
337
338    /// Note: tip will be truncated to 127 characters
339    pub fn tip(mut self, tip: Option<&'a str>) -> TrayNotificationBuilder<'a> {
340        self.tip = tip;
341        self
342    }
343
344    /// Note: info will be truncated to 255 characters
345    pub fn info(mut self, info: Option<&'a str>) -> TrayNotificationBuilder<'a> {
346        self.info = info;
347        self
348    }
349
350    /// Note: info will be truncated to 63 characters
351    /// Note 2: This value is only used if info is also specified
352    pub fn info_title(mut self, title: Option<&'a str>) -> TrayNotificationBuilder<'a> {
353        self.info_title = title;
354        self
355    }
356
357    pub fn build(self, out: &mut TrayNotification) -> Result<(), NwgError> {
358        use winapi::shared::windef::HICON;
359        use winapi::um::shellapi::{
360            NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_REALTIME, NIF_SHOWTIP, NIF_STATE, NIF_TIP,
361            NIM_ADD, NIS_HIDDEN, NOTIFYICON_VERSION_4, NOTIFYICONDATAW_u,
362        };
363        use winapi::um::winnt::WCHAR;
364
365        // Flags
366        let version = NOTIFYICON_VERSION_4;
367        let mut flags = NIF_ICON;
368        let mut info_flags = 0;
369        let mut state = 0;
370
371        if self.info.is_some() {
372            flags |= NIF_INFO;
373            info_flags |= self.flags.bits();
374        }
375
376        if self.tip.is_some() {
377            flags |= NIF_TIP | NIF_SHOWTIP;
378        }
379
380        if self.realtime {
381            flags |= NIF_REALTIME;
382        }
383        if self.callback {
384            flags |= NIF_MESSAGE;
385        }
386        if !self.visible {
387            state |= NIS_HIDDEN;
388            flags |= NIF_STATE;
389        }
390
391        // Resource handles
392
393        let parent = match self.parent {
394            Some(p) => match p.hwnd() {
395                Some(handle) => Ok(handle),
396                None => Err(NwgError::control_create(
397                    "TrayNotification must be window-like control.",
398                )),
399            },
400            None => Err(NwgError::no_parent("Button")),
401        }?;
402
403        let icon = match self.icon {
404            Some(i) => i.handle as HICON,
405            None => panic!("Tray notification requires an Icon at creation"),
406        };
407
408        let balloon_icon = match (self.info.is_some(), self.balloon_icon) {
409            (false, _) | (true, None) => ptr::null_mut(),
410            (true, Some(i)) => i.handle as HICON,
411        };
412
413        // UID
414        let handle = ControlBase::build_tray_notification()
415            .parent(parent)
416            .build()?;
417
418        // Tips or infos
419        let mut tip: [WCHAR; 128] = [0; 128];
420        if self.tip.is_some() {
421            let tip_v = to_utf16(self.tip.unwrap());
422            let length = if tip_v.len() >= 128 { 127 } else { tip_v.len() };
423            for i in 0..length {
424                tip[i] = tip_v[i];
425            }
426        }
427
428        let mut info: [WCHAR; 256] = [0; 256];
429        if self.info.is_some() {
430            let info_v = to_utf16(self.info.unwrap());
431            let length = if info_v.len() >= 256 {
432                255
433            } else {
434                info_v.len()
435            };
436            for i in 0..length {
437                info[i] = info_v[i];
438            }
439        }
440
441        let mut title: [WCHAR; 64] = [0; 64];
442        if self.info.is_some() && self.info_title.is_some() {
443            let info_title_v = to_utf16(self.info_title.unwrap());
444            let length = if info_title_v.len() >= 256 {
445                255
446            } else {
447                info_title_v.len()
448            };
449            for i in 0..length {
450                title[i] = info_title_v[i];
451            }
452        }
453
454        // Creation
455        unsafe {
456            let mut u: NOTIFYICONDATAW_u = mem::zeroed();
457            *u.uVersion_mut() = version;
458
459            let mut data = NOTIFYICONDATAW {
460                cbSize: mem::size_of::<NOTIFYICONDATAW>() as u32,
461                hWnd: parent,
462                uID: 0,
463                uFlags: flags,
464                uCallbackMessage: wh::NWG_TRAY,
465                hIcon: icon,
466                szTip: tip,
467                dwState: state,
468                dwStateMask: state,
469                szInfo: info,
470                u,
471                szInfoTitle: title,
472                dwInfoFlags: info_flags,
473                guidItem: mem::zeroed(),
474                hBalloonIcon: balloon_icon,
475            };
476
477            Shell_NotifyIconW(NIM_ADD, &mut data);
478        }
479
480        // Finish
481        *out = Default::default();
482        out.handle = handle;
483
484        Ok(())
485    }
486}