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}