gtk_test/
functions.rs

1use enigo::{self, Enigo, KeyboardControllable, MouseButton, MouseControllable};
2use gtk::gdk::keys::constants as key;
3use gtk::gdk::keys::Key;
4use gtk::glib::{Cast, ControlFlow, IsA, Object, Propagation, StaticType};
5use gtk::prelude::{BinExt, ButtonExt, ContainerExt, EditableExt, ToolButtonExt, WidgetExt};
6use gtk::{Bin, Button, Container, Entry, ToolButton, Widget, Window};
7
8use crate::observer_new;
9
10/// Simulate a click on a widget.
11///
12/// ## Warning!
13///
14/// Please note that the click will "fail" if the window isn't on top of all other windows (this
15/// is a common issue on OSX). Don't forget to bring the button's window on top by using:
16///
17/// ```ignore
18/// window.activate_focus();
19/// ```
20///
21/// Example:
22///
23/// ```
24/// extern crate gtk;
25/// #[macro_use]
26/// extern crate gtk_test;
27///
28/// use gtk::{Button, prelude::ButtonExt};
29///
30/// # fn main() {
31/// gtk::init().expect("GTK init failed");
32/// let but = Button::new();
33/// but.connect_clicked(|_| {
34///     println!("clicked");
35/// });
36/// gtk_test::click(&but);
37/// # }
38/// ```
39pub fn click<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt + IsA<W>>(widget: &W) {
40    wait_for_draw(widget, || {
41        let observer = if let Ok(tool_button) = widget.clone().dynamic_cast::<ToolButton>() {
42            observer_new!(tool_button, connect_clicked, |_|)
43        } else if let Ok(tool_button) = widget.clone().dynamic_cast::<Button>() {
44            observer_new!(tool_button, connect_clicked, |_|)
45        } else {
46            observer_new!(widget, connect_button_release_event, |_, _| {
47                Propagation::Stop
48            })
49        };
50        let allocation = widget.allocation();
51        mouse_move(widget, allocation.width() / 2, allocation.height() / 2);
52        let mut enigo = Enigo::new();
53        enigo.mouse_click(MouseButton::Left);
54        observer.wait();
55    });
56}
57
58/// Simulate a double-click on a widget.
59///
60/// ## Warning!
61///
62/// Please note that the double-click will "fail" if the window isn't on top of all other windows
63/// (this is a common issue on OSX). Don't forget to bring the button's window on top by using:
64///
65/// ```ignore
66/// window.activate_focus();
67/// ```
68///
69/// Example:
70///
71/// ```
72/// extern crate gtk;
73/// #[macro_use]
74/// extern crate gtk_test;
75///
76/// use gtk::{FileChooserAction, FileChooserWidget, prelude::FileChooserExt};
77///
78/// # fn main() {
79/// gtk::init().expect("GTK init failed");
80/// let fcw = FileChooserWidget::new(FileChooserAction::Open);
81/// fcw.connect_file_activated(|_| {
82///     println!("double clicked");
83/// });
84/// gtk_test::double_click(&fcw);
85/// # }
86/// ```
87pub fn double_click<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W) {
88    wait_for_draw(widget, || {
89        let observer = observer_new!(widget, connect_button_release_event, |_, _| {
90            Propagation::Stop
91        });
92        let allocation = widget.allocation();
93        mouse_move(widget, allocation.width() / 2, allocation.height() / 2);
94        let mut enigo = Enigo::new();
95        enigo.mouse_click(MouseButton::Left);
96        run_loop();
97        enigo.mouse_click(MouseButton::Left);
98        observer.wait();
99    });
100}
101
102/// Move the mouse relative to the widget position.
103///
104/// Example:
105///
106/// ```
107/// extern crate gtk;
108/// #[macro_use]
109/// extern crate gtk_test;
110///
111/// use gtk::Button;
112///
113/// # fn main() {
114/// gtk::init().expect("GTK init failed");
115/// let but = Button::new();
116/// gtk_test::mouse_move(&but, 0, 0); // the mouse will be on the top-left corner of the button
117/// # }
118/// ```
119pub fn mouse_move<W: IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W, x: i32, y: i32) {
120    wait_for_draw(widget, || {
121        let toplevel_window = widget.toplevel().and_then(|toplevel| toplevel.window());
122        if let (Some(toplevel), Some(toplevel_window)) = (widget.toplevel(), toplevel_window) {
123            let (_, window_x, window_y) = toplevel_window.origin();
124            if let Some((x, y)) = widget.translate_coordinates(&toplevel, x, y) {
125                let x = window_x + x;
126                let y = window_y + y;
127                let mut enigo = Enigo::new();
128                enigo.mouse_move_to(x, y);
129                run_loop();
130            }
131        }
132    });
133}
134
135/// Send a mouse press event to the given widget.
136///
137/// ## Warning!
138///
139/// Please note that the mouse-press event will "fail" if the window isn't on top of all other
140/// windows (this is a common issue on OSX). Don't forget to bring the button's window on top
141/// by using:
142///
143/// ```ignore
144/// window.activate_focus();
145/// ```
146///
147/// Example:
148///
149/// ```
150/// extern crate gtk;
151/// #[macro_use]
152/// extern crate gtk_test;
153///
154/// use gtk::{Entry, prelude::EntryExt};
155///
156/// # fn main() {
157/// gtk::init().expect("GTK init failed");
158/// let entry = Entry::new();
159/// entry.connect_icon_press(|_, _, _| {
160///     println!("pressed");
161/// });
162/// gtk_test::mouse_press(&entry);
163/// # }
164/// ```
165pub fn mouse_press<W: IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W) {
166    wait_for_draw(widget, || {
167        let allocation = widget.allocation();
168        mouse_move(widget, allocation.width() / 2, allocation.height() / 2);
169        let mut enigo = Enigo::new();
170        enigo.mouse_down(MouseButton::Left);
171        run_loop();
172    });
173}
174
175/// Send a mouse release event to the given widget.
176///
177/// ## Warning!
178///
179/// Please note that the mouse-release event will "fail" if the window isn't on top of all other
180/// windows (this is a common issue on OSX). Don't forget to bring the button's window on top
181/// by using:
182///
183/// ```ignore
184/// window.activate_focus();
185/// ```
186///
187/// Example:
188///
189/// ```
190/// extern crate gtk;
191/// #[macro_use]
192/// extern crate gtk_test;
193///
194/// use gtk::{Entry, prelude::EntryExt};
195///
196/// # fn main() {
197/// gtk::init().expect("GTK init failed");
198/// let entry = Entry::new();
199/// entry.connect_icon_release(|_, _, _| {
200///     println!("released");
201/// });
202/// gtk_test::mouse_release(&entry);
203/// # }
204/// ```
205pub fn mouse_release<W: IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W) {
206    wait_for_draw(widget, || {
207        let allocation = widget.allocation();
208        mouse_move(widget, allocation.width() / 2, allocation.height() / 2);
209        let mut enigo = Enigo::new();
210        enigo.mouse_up(MouseButton::Left);
211        run_loop();
212    });
213}
214
215/// Send a key event to the given widget.
216///
217/// ## Warning!
218///
219/// Please note that the enter-key event will "fail" if the window isn't on top of all other
220/// windows (this is a common issue on OSX). Don't forget to bring the button's window on top
221/// by using:
222///
223/// ```ignore
224/// window.activate_focus();
225/// ```
226///
227/// Example:
228///
229/// ```
230/// #[macro_use]
231/// extern crate gtk_test;
232///
233/// use gtk_test::gtk::{Entry, prelude::EntryExt};
234///
235/// # fn main() {
236/// gtk_test::gtk::init().expect("GTK init failed");
237/// let entry = Entry::new();
238/// entry.connect_preedit_changed(|_, _| {
239///     println!("key entered");
240/// });
241/// gtk_test::enter_key(&entry, gtk_test::gdk::keys::constants::Agrave);
242/// # }
243/// ```
244pub fn enter_key<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W, key: Key) {
245    wait_for_draw(widget, || {
246        let observer = observer_new!(widget, connect_key_release_event, |_, _| {
247            Propagation::Stop
248        });
249        focus(widget);
250        let mut enigo = Enigo::new();
251        enigo.key_click(gdk_key_to_enigo_key(key));
252        observer.wait();
253    });
254}
255
256/// Send keys event to the given widget.
257///
258/// ## Warning!
259///
260/// Please note that the enter-key event will "fail" if the window isn't on top of all other
261/// windows (this is a common issue on OSX). Don't forget to bring the button's window on top
262/// by using:
263///
264/// ```ignore
265/// window.activate_focus();
266/// ```
267///
268/// Example:
269///
270/// ```
271/// extern crate gtk;
272/// #[macro_use]
273/// extern crate gtk_test;
274///
275/// use gtk::{Entry, prelude::EntryExt};
276///
277/// # fn main() {
278/// gtk::init().expect("GTK init failed");
279/// let entry = Entry::new();
280/// entry.connect_preedit_changed(|_, _| {
281///     println!("key entered");
282/// });
283/// gtk_test::enter_keys(&entry, "A lot of keys!");
284/// # }
285/// ```
286pub fn enter_keys<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W, text: &str) {
287    wait_for_draw(widget, || {
288        focus(widget);
289        let mut enigo = Enigo::new();
290        for char in text.chars() {
291            let observer = observer_new!(widget, connect_key_release_event, |_, _| {
292                Propagation::Stop
293            });
294            enigo.key_sequence(&char.to_string());
295            observer.wait();
296        }
297    });
298}
299
300/// Returns the child element which has the given name.
301///
302/// Example:
303///
304/// ```
305/// extern crate gtk;
306/// #[macro_use]
307/// extern crate gtk_test;
308///
309/// use gtk::{prelude::BuildableExtManual, Button, prelude::ContainerExt, prelude::WidgetExt, Window, WindowType};
310///
311/// # fn main() {
312/// gtk::init().expect("GTK init failed");
313/// let but = Button::new();
314/// let w = Window::new(WindowType::Toplevel);
315///
316/// but.set_widget_name("Button");
317/// w.add(&but);
318///
319/// gtk_test::find_child_by_name::<Button, Window>(&w, "Button").expect("failed to find child");
320/// // Or even better:
321/// let but: Button = gtk_test::find_child_by_name(&w, "Button").expect("failed to find child");
322/// # }
323/// ```
324pub fn find_child_by_name<C: IsA<Widget>, W: Clone + IsA<Object> + IsA<Widget>>(
325    parent: &W,
326    name: &str,
327) -> Option<C> {
328    find_widget_by_name(parent, name).and_then(|widget| widget.downcast().ok())
329}
330
331/// Returns the child widget which has the given name.
332///
333/// Example:
334///
335/// ```
336/// extern crate gtk;
337/// #[macro_use]
338/// extern crate gtk_test;
339///
340/// use gtk::{Button, prelude::ContainerExt, prelude::WidgetExt, Window, WindowType};
341///
342/// # fn main() {
343/// gtk::init().expect("GTK init failed");
344/// let but = Button::new();
345/// let w = Window::new(WindowType::Toplevel);
346///
347/// but.set_widget_name("Button");
348/// w.add(&but);
349///
350/// gtk_test::find_widget_by_name(&w, "Button").unwrap();
351/// # }
352/// ```
353pub fn find_widget_by_name<W: Clone + IsA<Object> + IsA<Widget>>(
354    parent: &W,
355    name: &str,
356) -> Option<Widget> {
357    if let Ok(container) = parent.clone().dynamic_cast::<Container>() {
358        for child in container.children() {
359            if child.widget_name() == name {
360                return Some(child);
361            }
362            if let Some(widget) = find_widget_by_name(&child, name) {
363                return Some(widget);
364            }
365        }
366    } else if let Ok(bin) = parent.clone().dynamic_cast::<Bin>() {
367        if let Some(child) = bin.child() {
368            if child.widget_name() == name {
369                return Some(child);
370            }
371            if let Some(widget) = find_widget_by_name(&child, name) {
372                return Some(widget);
373            }
374        }
375    }
376    None
377}
378
379/// Focus on the given widget.
380///
381/// Example:
382///
383/// ```
384/// extern crate gtk;
385/// #[macro_use]
386/// extern crate gtk_test;
387///
388/// use gtk::{Button, prelude::WidgetExt};
389/// use gtk::glib::Propagation;
390///
391/// # fn main() {
392/// gtk::init().expect("GTK init failed");
393/// let but = Button::new();
394///
395/// but.connect_focus(|_, _| {
396///     println!("focused!");
397///     Propagation::Stop
398/// });
399/// gtk_test::focus(&but);
400/// # }
401/// ```
402pub fn focus<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W) {
403    wait_for_draw(widget, || {
404        if !widget.has_focus() {
405            widget.grab_focus();
406            if let Ok(entry) = widget.clone().dynamic_cast::<Entry>() {
407                // Hack to make it work on Travis.
408                // Should use grab_focus_without_selecting() instead.
409                entry.set_position(-1);
410            }
411        }
412    });
413}
414
415/// Send a key press event to the given widget.
416///
417/// ## Warning!
418///
419/// Please note that the key-press event will "fail" if the window isn't on top of all other
420/// windows (this is a common issue on OSX). Don't forget to bring the button's window on top
421/// by using:
422///
423/// ```ignore
424/// window.activate_focus();
425/// ```
426///
427/// Example:
428///
429/// ```
430/// #[macro_use]
431/// extern crate gtk_test;
432///
433/// use gtk_test::gtk::{Entry, prelude::WidgetExt};
434/// use gtk_test::gtk::glib::Propagation;
435///
436/// # fn main() {
437/// gtk_test::gtk::init().expect("GTK init failed");
438/// let entry = Entry::new();
439/// entry.connect_key_press_event(|_, _| {
440///     println!("key pressed");
441///     Propagation::Stop
442/// });
443/// gtk_test::key_press(&entry, gtk_test::gdk::keys::constants::Agrave);
444/// # }
445/// ```
446// FIXME: don't wait the observer for modifier keys like shift?
447pub fn key_press<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W, key: Key) {
448    wait_for_draw(widget, || {
449        let observer = observer_new!(widget, connect_key_press_event, |_, _| {
450            Propagation::Stop
451        });
452        focus(widget);
453        let mut enigo = Enigo::new();
454        enigo.key_down(gdk_key_to_enigo_key(key));
455        observer.wait();
456    });
457}
458
459/// Send a key release event to the given widget.
460///
461/// ## Warning!
462///
463/// Please note that the key-release event will "fail" if the window isn't on top of all other
464/// windows (this is a common issue on OSX). Don't forget to bring the button's window on top
465/// by using:
466///
467/// ```ignore
468/// window.activate_focus();
469/// ```
470///
471/// Example:
472///
473/// ```
474/// #[macro_use]
475/// extern crate gtk_test;
476///
477/// use gtk_test::gtk::{Entry, prelude::WidgetExt};
478/// use gtk_test::gtk::glib::Propagation;
479///
480/// # fn main() {
481/// gtk_test::gtk::init().expect("GTK init failed");
482/// let entry = Entry::new();
483/// entry.connect_key_release_event(|_, _| {
484///     println!("key released");
485///     Propagation::Stop
486/// });
487/// gtk_test::key_release(&entry, gtk_test::gdk::keys::constants::Agrave);
488/// # }
489/// ```
490pub fn key_release<W: Clone + IsA<Object> + IsA<Widget> + WidgetExt>(widget: &W, key: Key) {
491    wait_for_draw(widget, || {
492        let observer = observer_new!(widget, connect_key_release_event, |_, _| {
493            Propagation::Stop
494        });
495        focus(widget);
496        let mut enigo = Enigo::new();
497        enigo.key_up(gdk_key_to_enigo_key(key));
498        observer.wait();
499    });
500}
501
502/// Wait for events the specified amount the milliseconds.
503///
504/// Very convenient when you need GTK to update the UI to let it process some events.
505///
506/// Example:
507///
508/// ```
509/// extern crate gtk;
510/// extern crate gtk_test;
511///
512/// # fn main() {
513/// gtk::init().expect("GTK init failed");
514/// gtk_test::wait(1000); // wait for a second
515/// # }
516/// ```
517pub fn wait(ms: u32) {
518    gtk::glib::timeout_add(std::time::Duration::from_millis(ms as u64), || {
519        gtk::main_quit();
520        ControlFlow::Break
521    });
522    gtk::main();
523}
524
525/// Process all pending events and then return.
526///
527/// This function is called in all functions related to events handling (like `key_release` for
528/// example).
529///
530/// Example:
531///
532/// ```
533/// extern crate gtk;
534/// extern crate gtk_test;
535///
536/// # fn main() {
537/// gtk::init().expect("GTK init failed");
538/// gtk_test::run_loop();
539/// # }
540/// ```
541pub fn run_loop() {
542    while gtk::events_pending() {
543        gtk::main_iteration();
544    }
545}
546
547/// Wait until the given condition returns `true`.
548///
549/// Example:
550///
551/// ```
552/// extern crate gtk;
553/// extern crate gtk_test;
554///
555/// # fn main() {
556/// gtk::init().expect("GTK init failed");
557/// let mut x = 0;
558///
559/// gtk_test::wait_until_done(move || {
560///     x += 1;
561///     x > 10
562/// });
563/// # }
564/// ```
565pub fn wait_until_done<F: FnMut() -> bool>(mut f: F) {
566    while !f() {
567        run_loop();
568    }
569}
570
571/// Wait for a widget to be drawn.
572///
573/// Example:
574///
575/// ```
576/// extern crate gtk;
577/// extern crate gtk_test;
578///
579/// use gtk::{prelude::WidgetExt, Window, WindowType};
580///
581/// # fn main() {
582/// gtk::init().expect("GTK init failed");
583/// let mut w = Window::new(WindowType::Toplevel);
584///
585/// w.show_all();
586/// gtk_test::wait_for_draw(&w, || {
587///     println!("drawn!");
588/// });
589/// # }
590/// ```
591pub fn wait_for_draw<W, F: FnOnce()>(widget: &W, callback: F)
592where
593    W: IsA<Object> + IsA<Widget> + WidgetExt,
594{
595    if widget.ancestor(Window::static_type()).is_none() {
596        return;
597    }
598    gtk::test_widget_wait_for_draw(widget);
599    callback();
600}
601
602fn gdk_key_to_enigo_key(key: Key) -> enigo::Key {
603    use enigo::Key::*;
604    match key {
605        key::Return => Return,
606        key::Tab => Tab,
607        key::space => Space,
608        key::BackSpace => Backspace,
609        key::Escape => Escape,
610        key::Super_L | key::Super_R => Meta,
611        key::Control_L | key::Control_R => Control,
612        key::Shift_L | key::Shift_R => Shift,
613        key::Shift_Lock => CapsLock,
614        key::Alt_L | key::Alt_R => Alt,
615        key::End => End,
616        key::Option => Option,
617        key::Home => Home,
618        key::Page_Down => PageDown,
619        key::Page_Up => PageUp,
620        key::leftarrow => LeftArrow,
621        key::rightarrow => RightArrow,
622        key::downarrow => DownArrow,
623        key::uparrow => UpArrow,
624        key::F1 => F1,
625        key::F2 => F2,
626        key::F3 => F3,
627        key::F4 => F4,
628        key::F5 => F5,
629        key::F6 => F6,
630        key::F7 => F7,
631        key::F8 => F8,
632        key::F9 => F9,
633        key::F10 => F10,
634        key::F11 => F11,
635        key::F12 => F12,
636        _ => {
637            if let Some(char) = key.to_unicode() {
638                Layout(char)
639            } else {
640                Raw(*key as u16)
641            }
642        }
643    }
644}