428 lines
20 KiB
Rust
428 lines
20 KiB
Rust
use glib::clone;
|
|
use glib::subclass::prelude::*;
|
|
use gtk::glib::translate::FromGlibPtrBorrow;
|
|
use gtk::prelude::*;
|
|
use gtk::{gdk, glib, CompositeTemplate};
|
|
use log::debug;
|
|
use once_cell::sync::OnceCell;
|
|
use std::cell::{Cell, RefCell};
|
|
|
|
use wayland_client::{Display, GlobalManager};
|
|
use wayland_protocols::unstable::pointer_constraints::v1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1;
|
|
use wayland_protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{
|
|
Lifetime, ZwpPointerConstraintsV1,
|
|
};
|
|
use wayland_protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1;
|
|
use wayland_protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::{
|
|
Event as RelEvent, ZwpRelativePointerV1,
|
|
};
|
|
|
|
use keycodemap::*;
|
|
use qemu_display_listener::{Console, ConsoleEvent as Event, MouseButton};
|
|
|
|
mod imp {
|
|
use super::*;
|
|
use gtk::subclass::prelude::*;
|
|
|
|
#[derive(CompositeTemplate, Default)]
|
|
#[template(resource = "/org/qemu/gtk4/console.ui")]
|
|
pub struct QemuConsole {
|
|
#[template_child]
|
|
pub area: TemplateChild<crate::console_area::QemuConsoleArea>,
|
|
#[template_child]
|
|
pub label: TemplateChild<gtk::Label>,
|
|
pub console: OnceCell<Console>,
|
|
pub wait_rendering: Cell<usize>,
|
|
pub shortcuts_inhibited_id: Cell<Option<glib::SignalHandlerId>>,
|
|
pub ungrab_shortcut: OnceCell<gtk::ShortcutTrigger>,
|
|
pub key_controller: OnceCell<gtk::EventControllerKey>,
|
|
pub event_queue: OnceCell<wayland_client::EventQueue>,
|
|
pub rel_manager: OnceCell<wayland_client::Main<ZwpRelativePointerManagerV1>>,
|
|
pub rel_pointer: RefCell<Option<wayland_client::Main<ZwpRelativePointerV1>>>,
|
|
pub pointer_constraints: OnceCell<wayland_client::Main<ZwpPointerConstraintsV1>>,
|
|
pub lock_pointer: RefCell<Option<wayland_client::Main<ZwpLockedPointerV1>>>,
|
|
pub has_grab: Cell<bool>,
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for QemuConsole {
|
|
const NAME: &'static str = "QemuConsole";
|
|
type Type = super::QemuConsole;
|
|
type ParentType = gtk::Widget;
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
Self::bind_template(klass);
|
|
}
|
|
|
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
|
obj.init_template();
|
|
}
|
|
}
|
|
|
|
impl ObjectImpl for QemuConsole {
|
|
fn constructed(&self, obj: &Self::Type) {
|
|
self.parent_constructed(obj);
|
|
|
|
// TODO: implement a custom trigger with only modifiers, ala spice-gtk?
|
|
let ungrab = gtk::ShortcutTrigger::parse_string("<ctrl><alt>g").unwrap();
|
|
self.ungrab_shortcut.set(ungrab).unwrap();
|
|
|
|
let ec = gtk::EventControllerKey::new();
|
|
ec.set_propagation_phase(gtk::PropagationPhase::Capture);
|
|
self.area.add_controller(&ec);
|
|
ec.connect_key_pressed(
|
|
clone!(@weak obj => @default-panic, move |_, _keyval, keycode, _state| {
|
|
let c = obj.qemu_console();
|
|
if let Some(qnum) = KEYMAP_XORGEVDEV2QNUM.get(keycode as usize) {
|
|
let _ = c.keyboard.press(*qnum as u32);
|
|
}
|
|
glib::signal::Inhibit(true)
|
|
}),
|
|
);
|
|
ec.connect_key_released(clone!(@weak obj => move |_, _keyval, keycode, _state| {
|
|
let c = obj.qemu_console();
|
|
if let Some(qnum) = KEYMAP_XORGEVDEV2QNUM.get(keycode as usize) {
|
|
let _ = c.keyboard.release(*qnum as u32);
|
|
}
|
|
}));
|
|
self.key_controller.set(ec).unwrap();
|
|
|
|
let ec = gtk::EventControllerMotion::new();
|
|
self.area.add_controller(&ec);
|
|
ec.connect_motion(clone!(@weak obj => move |_, x, y| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let c = obj.qemu_console();
|
|
if c.mouse.is_absolute().unwrap_or(true) {
|
|
priv_.motion(x, y);
|
|
};
|
|
}));
|
|
|
|
let ec = gtk::GestureClick::new();
|
|
ec.set_button(0);
|
|
self.area.add_controller(&ec);
|
|
ec.connect_pressed(clone!(@weak obj => @default-panic, move |gesture, _n_press, x, y| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let c = obj.qemu_console();
|
|
let button = from_gdk_button(gesture.get_current_button());
|
|
priv_.motion(x, y);
|
|
let _ = c.mouse.press(button);
|
|
|
|
if !c.mouse.is_absolute().unwrap_or(true) {
|
|
priv_.area.set_cursor_abs(false);
|
|
if let Some(device) = gesture.get_device() {
|
|
if let Ok(device) = device.downcast::<gdk_wl::WaylandDevice>() {
|
|
let pointer = device.get_wl_pointer();
|
|
if priv_.lock_pointer.borrow().is_none() {
|
|
if let Some(constraints) = priv_.pointer_constraints.get() {
|
|
if let Some(surf) = priv_.area.get_native()
|
|
.and_then(|n| n.get_surface())
|
|
.and_then(|s| s.downcast::<gdk_wl::WaylandSurface>().ok())
|
|
.map(|w| w.get_wl_surface()) {
|
|
let lock = constraints.lock_pointer(&surf, &pointer, None, Lifetime::Persistent as _);
|
|
lock.quick_assign(move |_, event, _| {
|
|
debug!("{:?}", event);
|
|
});
|
|
priv_.lock_pointer.replace(Some(lock));
|
|
}
|
|
}
|
|
}
|
|
if priv_.rel_pointer.borrow().is_none() {
|
|
if let Some(rel_manager) = priv_.rel_manager.get() {
|
|
let rel_pointer = rel_manager.get_relative_pointer(&pointer);
|
|
rel_pointer.quick_assign(clone!(@weak obj => @default-panic, move |_, event, _| {
|
|
if let RelEvent::RelativeMotion { dx_unaccel, dy_unaccel, .. } = event {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let c = obj.qemu_console();
|
|
let scale = priv_.area.get_scale_factor();
|
|
let _ = c.mouse.rel_motion(dx_unaccel as i32 / scale, dy_unaccel as i32 / scale);
|
|
}
|
|
}));
|
|
priv_.rel_pointer.replace(Some(rel_pointer));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(toplevel) = priv_.get_toplevel() {
|
|
if !toplevel.get_property_shortcuts_inhibited() {
|
|
toplevel.inhibit_system_shortcuts::<gdk::ButtonEvent>(None);
|
|
|
|
let ec = gtk::EventControllerKey::new();
|
|
ec.set_propagation_phase(gtk::PropagationPhase::Capture);
|
|
ec.connect_key_pressed(clone!(@weak obj, @weak toplevel => @default-panic, move |ec, keyval, keycode, state| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
if let Some(ref e) = ec.get_current_event() {
|
|
if priv_.ungrab_shortcut.get().unwrap().trigger(e, false) == gdk::KeyMatch::Exact {
|
|
//widget.remove_controller(ec); here crashes badly
|
|
glib::idle_add_local(clone!(@weak ec, @weak toplevel, @weak obj => @default-panic, move || {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
if let Some(widget) = ec.get_widget() {
|
|
widget.remove_controller(&ec);
|
|
}
|
|
toplevel.restore_system_shortcuts();
|
|
if let Some(lock) = priv_.lock_pointer.take() {
|
|
lock.destroy();
|
|
}
|
|
if let Some(rel_pointer) = priv_.rel_pointer.take() {
|
|
rel_pointer.destroy();
|
|
}
|
|
priv_.area.set_cursor_abs(true);
|
|
priv_.has_grab.set(false);
|
|
glib::Continue(false)
|
|
}));
|
|
} else {
|
|
priv_.key_controller.get().unwrap().emit_by_name("key-pressed", &[&*keyval, &keycode, &state]).unwrap();
|
|
}
|
|
}
|
|
|
|
glib::signal::Inhibit(true)
|
|
}));
|
|
ec.connect_key_released(clone!(@weak obj => @default-panic, move |_ec, keyval, keycode, state| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
priv_.key_controller.get().unwrap().emit_by_name("key-released", &[&*keyval, &keycode, &state]).unwrap();
|
|
}));
|
|
if let Some(root) = priv_.area.get_root() {
|
|
root.add_controller(&ec);
|
|
}
|
|
|
|
let id = toplevel.connect_property_shortcuts_inhibited_notify(clone!(@weak obj => @default-panic, move |toplevel| {
|
|
let inhibited = toplevel.get_property_shortcuts_inhibited();
|
|
debug!("shortcuts-inhibited: {}", inhibited);
|
|
if !inhibited {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let id = priv_.shortcuts_inhibited_id.take();
|
|
toplevel.disconnect(id.unwrap());
|
|
}
|
|
}));
|
|
priv_.shortcuts_inhibited_id.set(Some(id));
|
|
}
|
|
}
|
|
|
|
priv_.has_grab.set(true);
|
|
priv_.area.grab_focus();
|
|
}));
|
|
ec.connect_released(clone!(@weak obj => move |gesture, _n_press, x, y| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let c = obj.qemu_console();
|
|
let button = from_gdk_button(gesture.get_current_button());
|
|
|
|
priv_.motion(x, y);
|
|
let _ = c.mouse.release(button);
|
|
}));
|
|
|
|
let ec = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::BOTH_AXES);
|
|
self.area.add_controller(&ec);
|
|
ec.connect_scroll(clone!(@weak obj => @default-panic, move |_, _dx, dy| {
|
|
let c = obj.qemu_console();
|
|
|
|
let button = if dy >= 1.0 {
|
|
Some(MouseButton::WheelDown)
|
|
} else if dy <= -1.0 {
|
|
Some(MouseButton::WheelUp)
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(button) = button {
|
|
let _ = c.mouse.press(button);
|
|
let _ = c.mouse.release(button);
|
|
}
|
|
glib::signal::Inhibit(true)
|
|
}));
|
|
|
|
self.area.set_sensitive(true);
|
|
self.area.set_focusable(true);
|
|
self.area.set_focus_on_click(true);
|
|
|
|
self.area
|
|
.connect_create_context(clone!(@weak obj => @default-panic, move |_| {
|
|
// can't connect-after create-context yet, so idle it
|
|
glib::idle_add_local(clone!(@weak ec => @default-panic, move || {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
priv_.attach_qemu_console(&obj);
|
|
glib::Continue(false)
|
|
}));
|
|
None
|
|
}));
|
|
|
|
unsafe {
|
|
self.area.connect_notify_unsafe(
|
|
Some("resize-hack"),
|
|
clone!(@weak obj => move |_, _| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let alloc = priv_.area.get_allocation();
|
|
if let Err(e) = obj.qemu_console().proxy.set_ui_info(0, 0, 0, 0, alloc.width as u32, alloc.height as u32) {
|
|
eprintln!("Failed to SetUIInfo: {}", e);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Needed for direct subclasses of GtkWidget;
|
|
// Here you need to unparent all direct children
|
|
// of your template.
|
|
fn dispose(&self, obj: &Self::Type) {
|
|
while let Some(child) = obj.get_first_child() {
|
|
child.unparent();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WidgetImpl for QemuConsole {
|
|
fn realize(&self, widget: &Self::Type) {
|
|
self.parent_realize(widget);
|
|
|
|
if let Ok(dpy) = widget.get_display().downcast::<gdk_wl::WaylandDisplay>() {
|
|
let display = unsafe {
|
|
Display::from_external_display(dpy.get_wl_display().as_ref().c_ptr() as *mut _)
|
|
};
|
|
let mut event_queue = display.create_event_queue();
|
|
let attached_display = display.attach(event_queue.token());
|
|
let globals = GlobalManager::new(&attached_display);
|
|
event_queue
|
|
.sync_roundtrip(&mut (), |_, _, _| unreachable!())
|
|
.unwrap();
|
|
let rel_manager = globals
|
|
.instantiate_exact::<ZwpRelativePointerManagerV1>(1)
|
|
.unwrap();
|
|
self.rel_manager.set(rel_manager).unwrap();
|
|
let pointer_constraints = globals
|
|
.instantiate_exact::<ZwpPointerConstraintsV1>(1)
|
|
.unwrap();
|
|
self.pointer_constraints.set(pointer_constraints).unwrap();
|
|
let fd = display.get_connection_fd();
|
|
let _ = glib::unix_fd_add_local(fd, glib::IOCondition::IN, move |_, _| {
|
|
event_queue
|
|
.sync_roundtrip(&mut (), |_, _, _| unreachable!())
|
|
.unwrap();
|
|
glib::Continue(true)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl QemuConsole {
|
|
fn get_toplevel(&self) -> Option<gdk::Toplevel> {
|
|
self.area
|
|
.get_root()
|
|
.and_then(|r| r.get_native())
|
|
.and_then(|n| n.get_surface())
|
|
.and_then(|s| s.downcast::<gdk::Toplevel>().ok())
|
|
}
|
|
|
|
fn motion(&self, x: f64, y: f64) {
|
|
if let Some((x, y)) = self.area.transform_input(x, y) {
|
|
let c = self.console.get().unwrap();
|
|
let _ = c.mouse.set_abs_position(x, y);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn attach_qemu_console(&self, obj: &super::QemuConsole) {
|
|
let console = match self.console.get() {
|
|
Some(console) => console,
|
|
None => return,
|
|
};
|
|
if !obj.get_realized() {
|
|
return;
|
|
}
|
|
|
|
let (rx, wait_tx) = console
|
|
.glib_listen()
|
|
.expect("Failed to listen to the console");
|
|
self.area
|
|
.connect_render(clone!(@weak obj => @default-panic, move |_, _| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
let wait_rendering = priv_.wait_rendering.get();
|
|
if wait_rendering > 0 {
|
|
if let Err(e) = wait_tx.send(()) {
|
|
eprintln!("Failed to ack rendering: {}", e);
|
|
}
|
|
priv_.wait_rendering.set(wait_rendering - 1);
|
|
}
|
|
glib::signal::Inhibit(false)
|
|
}));
|
|
rx.attach(
|
|
None,
|
|
clone!(@weak obj => @default-panic, move |t| {
|
|
let priv_ = imp::QemuConsole::from_instance(&obj);
|
|
debug!("Console event: {:?}", t);
|
|
match t {
|
|
Event::Scanout(s) => {
|
|
priv_.area.set_scanout(s);
|
|
priv_.area.queue_render();
|
|
}
|
|
Event::Update(u) => {
|
|
priv_.area.update(u);
|
|
priv_.area.queue_render();
|
|
}
|
|
Event::ScanoutDMABUF(s) => {
|
|
priv_.label.set_label(&format!("{:?}", s));
|
|
priv_.area.set_scanout_dmabuf(s);
|
|
}
|
|
Event::UpdateDMABUF { .. } => {
|
|
priv_.wait_rendering.set(priv_.wait_rendering.get() + 1);
|
|
// we don't simply queue_render, as we want a copy immediately
|
|
priv_.area.make_current();
|
|
priv_.area.attach_buffers();
|
|
let _ = unsafe {
|
|
glib::Object::from_glib_borrow(priv_.area.as_ptr() as *mut glib::gobject_ffi::GObject)
|
|
.emit_by_name("render", &[&priv_.area.get_context().as_ref()])
|
|
.unwrap()
|
|
};
|
|
priv_.area.queue_draw();
|
|
}
|
|
Event::Disconnected => {
|
|
priv_.label.set_label("Console disconnected!");
|
|
}
|
|
Event::CursorDefine { width, height, hot_x, hot_y, data }=> {
|
|
let scale = priv_.area.get_scale_factor();
|
|
let pb = gdk::gdk_pixbuf::Pixbuf::from_mut_slice(data, gdk::gdk_pixbuf::Colorspace::Rgb, true, 8, width, height, width * 4);
|
|
let pb = pb.scale_simple(width * scale, height * scale, gdk::gdk_pixbuf::InterpType::Bilinear).unwrap();
|
|
let tex = gdk::Texture::new_for_pixbuf(&pb);
|
|
let cur = gdk::Cursor::from_texture(&tex, hot_x * scale, hot_y * scale, None);
|
|
priv_.area.cursor_define(cur);
|
|
}
|
|
Event::MouseSet(m) => {
|
|
priv_.area.mouse_set(m);
|
|
let c = obj.qemu_console();
|
|
let abs = c.mouse.is_absolute().unwrap_or(true);
|
|
if priv_.has_grab.get() {
|
|
priv_.area.set_cursor_abs(abs);
|
|
}
|
|
priv_.area.queue_render();
|
|
}
|
|
}
|
|
Continue(true)
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
glib::wrapper! {
|
|
pub struct QemuConsole(ObjectSubclass<imp::QemuConsole>) @extends gtk::Widget;
|
|
}
|
|
|
|
impl QemuConsole {
|
|
pub fn set_qemu_console(&self, console: Console) {
|
|
let priv_ = imp::QemuConsole::from_instance(self);
|
|
priv_.console.set(console).unwrap();
|
|
priv_.attach_qemu_console(self);
|
|
}
|
|
|
|
fn qemu_console(&self) -> &Console {
|
|
let priv_ = imp::QemuConsole::from_instance(self);
|
|
priv_.console.get().expect("Console is not yet set!")
|
|
}
|
|
}
|
|
|
|
fn from_gdk_button(button: u32) -> MouseButton {
|
|
match button {
|
|
1 => MouseButton::Left,
|
|
2 => MouseButton::Middle,
|
|
3 => MouseButton::Right,
|
|
_ => MouseButton::Extra,
|
|
}
|
|
}
|