Initial commit

This commit is contained in:
Marc-André Lureau 2021-01-23 20:03:56 +04:00
commit edaffdb868
43 changed files with 1656 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
Cargo.lock
.DS_Store
.idea
*.log

6
Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[workspace]
members = ["qemu-display-listener", "qemu-gtk4"]
#[patch.crates-io]
#zbus = { path = '/home/elmarco/src/zbus/zbus' }

2
qemu-display-listener/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

View File

@ -0,0 +1,14 @@
[package]
name = "qemu-display-listener"
version = "0.1.0"
authors = ["Marc-André Lureau <marcandre.lureau@redhat.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
zbus = "2.0.0-beta"
derivative = "2.1.3"
zvariant = "2.4.0"
libc = "0.2.86"
glib = { git = "https://github.com/gtk-rs/gtk-rs", optional = true }

View File

@ -0,0 +1,121 @@
use std::cell::RefCell;
use std::os::unix::net::UnixStream;
use std::rc::Rc;
use std::sync::mpsc::{self, Receiver};
use std::{os::unix::io::AsRawFd, thread};
use zbus::{dbus_proxy, export::zvariant::Fd};
use crate::Result;
use crate::{Event, Listener};
#[dbus_proxy(default_service = "org.qemu", interface = "org.qemu.Display1.Console")]
pub trait Console {
/// RegisterListener method
fn register_listener(&self, listener: Fd) -> zbus::Result<()>;
#[dbus_proxy(property)]
fn label(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
fn head(&self) -> zbus::Result<u32>;
#[dbus_proxy(property)]
fn type_(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
fn width(&self) -> zbus::Result<u32>;
#[dbus_proxy(property)]
fn height(&self) -> zbus::Result<u32>;
}
#[derive(derivative::Derivative)]
#[derivative(Debug)]
pub struct Console<'c> {
#[derivative(Debug = "ignore")]
proxy: ConsoleProxy<'c>,
}
impl<'c> Console<'c> {
pub fn new(conn: &zbus::Connection, idx: u32) -> Result<Self> {
let proxy = ConsoleProxy::new_for_owned_path(
conn.clone(),
format!("/org/qemu/Display1/Console_{}", idx),
)?;
Ok(Self { proxy })
}
pub fn label(&self) -> Result<String> {
Ok(self.proxy.label()?)
}
pub fn width(&self) -> Result<u32> {
Ok(self.proxy.width()?)
}
pub fn height(&self) -> Result<u32> {
Ok(self.proxy.height()?)
}
pub fn listen(&self) -> Result<Receiver<Event>> {
let (p0, p1) = UnixStream::pair()?;
let (tx, rx) = mpsc::channel();
self.proxy.register_listener(p0.as_raw_fd().into())?;
let _thread = thread::spawn(move || {
let c = zbus::Connection::new_unix_client(p1, false).unwrap();
let mut s = zbus::ObjectServer::new(&c);
let err = Rc::new(RefCell::new(None));
s.at(
&zvariant::ObjectPath::from_str_unchecked("/org/qemu/Display1/Listener"),
Listener::new(tx, err.clone()),
)
.unwrap();
loop {
if let Err(e) = s.try_handle_next() {
eprintln!("Listener DBus error: {}", e);
return;
}
if let Some(e) = &*err.borrow() {
eprintln!("Listener channel error: {}", e);
return;
}
}
});
Ok(rx)
}
}
#[cfg(feature = "glib")]
impl<'c> Console<'c> {
pub fn glib_listen(&self) -> Result<glib::Receiver<Event>> {
let (p0, p1) = UnixStream::pair()?;
let (tx, rx) = glib::MainContext::channel(glib::source::Priority::default());
self.proxy.register_listener(p0.as_raw_fd().into())?;
let _thread = thread::spawn(move || {
let c = zbus::Connection::new_unix_client(p1, false).unwrap();
let mut s = zbus::ObjectServer::new(&c);
let err = Rc::new(RefCell::new(None));
s.at(
&zvariant::ObjectPath::from_str_unchecked("/org/qemu/Display1/Listener"),
Listener::new(tx, err.clone()),
)
.unwrap();
loop {
if let Err(e) = s.try_handle_next() {
eprintln!("Listener DBus error: {}", e);
return;
}
if let Some(e) = &*err.borrow() {
eprintln!("Listener channel error: {}", e);
return;
}
}
});
Ok(rx)
}
}

View File

@ -0,0 +1,50 @@
use std::error;
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Zbus(zbus::Error),
Zvariant(zvariant::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(e) => write!(f, "{}", e),
Error::Zbus(e) => write!(f, "{}", e),
Error::Zvariant(e) => write!(f, "{}", e),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
Error::Zbus(e) => Some(e),
Error::Zvariant(e) => Some(e),
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl From<zbus::Error> for Error {
fn from(e: zbus::Error) -> Self {
Error::Zbus(e)
}
}
impl From<zvariant::Error> for Error {
fn from(e: zvariant::Error) -> Self {
Error::Zvariant(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@ -0,0 +1,21 @@
#![allow(clippy::too_many_arguments)]
mod error;
pub use error::*;
mod vm;
pub use vm::*;
mod console;
pub use console::*;
mod listener;
pub use listener::*;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -0,0 +1,124 @@
use std::cell::RefCell;
use std::os::unix::io::{AsRawFd, RawFd};
use std::rc::Rc;
use std::sync::mpsc::{SendError, Sender};
use zbus::{dbus_interface, export::zvariant::Fd};
// TODO: replace events mpsc with async traits
#[derive(Debug)]
pub enum Event {
Switch {
width: i32,
height: i32,
},
Update {
x: i32,
y: i32,
w: i32,
h: i32,
},
Scanout {
fd: RawFd,
width: u32,
height: u32,
stride: u32,
fourcc: u32,
modifier: u64,
y0_top: bool,
},
MouseSet {
x: i32,
y: i32,
on: i32,
},
CursorDefine {
width: i32,
height: i32,
hot_x: i32,
hot_y: i32,
data: Vec<u8>,
},
}
pub(crate) trait EventSender {
fn send_event(&self, t: Event) -> Result<(), SendError<Event>>;
}
impl EventSender for Sender<Event> {
fn send_event(&self, t: Event) -> Result<(), SendError<Event>> {
self.send(t)
}
}
#[cfg(feature = "glib")]
impl EventSender for glib::Sender<Event> {
fn send_event(&self, t: Event) -> Result<(), SendError<Event>> {
self.send(t)
}
}
#[derive(Debug)]
pub(crate) struct Listener<E: EventSender> {
tx: E,
err: Rc<RefCell<Option<SendError<Event>>>>,
}
#[dbus_interface(name = "org.qemu.Display1.Listener")]
impl<E: 'static + EventSender> Listener<E> {
fn switch(&mut self, width: i32, height: i32) {
self.send(Event::Switch { width, height })
}
fn update(&mut self, x: i32, y: i32, w: i32, h: i32) {
self.send(Event::Update { x, y, w, h })
}
fn scanout(
&mut self,
fd: Fd,
width: u32,
height: u32,
stride: u32,
fourcc: u32,
modifier: u64,
y0_top: bool,
) {
let fd = unsafe { libc::dup(fd.as_raw_fd()) };
self.send(Event::Scanout {
fd,
width,
height,
stride,
fourcc,
modifier,
y0_top,
})
}
fn mouse_set(&mut self, x: i32, y: i32, on: i32) {
self.send(Event::MouseSet { x, y, on })
}
fn cursor_define(&mut self, width: i32, height: i32, hot_x: i32, hot_y: i32, data: Vec<u8>) {
self.send(Event::CursorDefine {
width,
height,
hot_x,
hot_y,
data,
})
}
}
impl<E: EventSender> Listener<E> {
pub(crate) fn new(tx: E, err: Rc<RefCell<Option<SendError<Event>>>>) -> Self {
Listener { tx, err }
}
fn send(&mut self, event: Event) {
if let Err(e) = self.tx.send_event(event) {
*self.err.borrow_mut() = Some(e);
}
}
}

View File

@ -0,0 +1,16 @@
use zbus::dbus_proxy;
#[dbus_proxy(
default_service = "org.qemu",
interface = "org.qemu.Display1.VM",
default_path = "/org/qemu/Display1/VM"
)]
pub trait VM {
/// Name property
#[dbus_proxy(property)]
fn name(&self) -> zbus::Result<String>;
/// UUID property
#[dbus_proxy(property)]
fn uuid(&self) -> zbus::Result<String>;
}

13
qemu-gtk4/.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
[*.{build,yml,ui,yaml}]
indent_size = 2
[*.{json,py}]
indent_size = 4

19
qemu-gtk4/.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,19 @@
on:
push:
branches: [master]
pull_request:
name: CI
jobs:
flatpak:
name: "Flatpak"
runs-on: ubuntu-20.04
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-3.38
options: --privileged
steps:
- uses: actions/checkout@v2
- uses: bilelmoussaoui/flatpak-github-actions@v2
with:
bundle: "qemu-gtk4.flatpak"
manifest-path: "build-aux/org.qemu.gtk4.Devel.json"
run-tests: "true"

10
qemu-gtk4/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
target/
build/
_build/
builddir/
build-aux/app
build-aux/.flatpak-builder/
src/config.rs
*.ui.in~
*.ui~
.flatpak/

39
qemu-gtk4/.gitlab-ci.yml Normal file
View File

@ -0,0 +1,39 @@
stages:
- check
- test
flatpak:
image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.38'
stage: test
tags:
- flatpak
variables:
BUNDLE: "qemu-gtk4-nightly.flatpak"
MANIFEST_PATH: "build-aux/org.qemu.gtk4.Devel.json"
FLATPAK_MODULE: "qemu-gtk4"
APP_ID: "org.qemu.gtk4.Devel"
RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo"
script:
- >
xvfb-run -a -s "-screen 0 1024x768x24"
flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH}
- flatpak build-bundle repo ${BUNDLE} --runtime-repo=${RUNTIME_REPO} ${APP_ID} ${BRANCH}
artifacts:
name: 'Flatpak artifacts'
expose_as: 'Get Flatpak bundle here'
when: 'always'
paths:
- "${BUNDLE}"
- '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/meson-log.txt'
- '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/testlog.txt'
expire_in: 14 days
# Configure and run rustfmt
# Exits and builds fails if on bad format
rustfmt:
image: "rust:slim"
script:
- rustup component add rustfmt
- rustc -Vv && cargo -Vv
- cargo fmt --version
- cargo fmt --all -- --color=always --check

19
qemu-gtk4/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "qemu-gtk4"
version = "0.1.0"
authors = ["QEMU developpers <qemu-devel@nongnu.org>"]
edition = "2018"
[dependencies]
qemu-display-listener = { path = "../qemu-display-listener", features = ["glib"] }
zbus = { version = "2.0.0-beta" }
log = "0.4"
pretty_env_logger = "0.4"
gettext-rs = { version = "0.5", features = ["gettext-system"] }
gtk-macros = "0.2"
once_cell = "1.5"
[dependencies.gtk]
package = "gtk4"
git = "https://github.com/gtk-rs/gtk4-rs"
rev = "abea0c9980bc083494eceb30dfab5eeb99a73118"

7
qemu-gtk4/LICENSE.md Normal file
View File

@ -0,0 +1,7 @@
Copyright © 2019, QEMU developpers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders X be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.
Except as contained in this notice, the name of the QEMU developpers shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the QEMU developpers.

5
qemu-gtk4/README.md Normal file
View File

@ -0,0 +1,5 @@
# QEMU Gtk4
## Credits
Based on [GTK Rust template](https://gitlab.gnome.org/bilelmoussaoui/gtk-rust-template)

View File

@ -0,0 +1,20 @@
#!/bin/sh
export MESON_BUILD_ROOT="$1"
export MESON_SOURCE_ROOT="$2"
export CARGO_TARGET_DIR="$MESON_BUILD_ROOT"/target
export CARGO_HOME="$CARGO_TARGET_DIR"/cargo-home
if [[ $4 = "Devel" ]]
then
echo "DEBUG MODE"
cargo build --manifest-path \
"$MESON_SOURCE_ROOT"/Cargo.toml && \
cp "$CARGO_TARGET_DIR"/debug/$5 $3
else
echo "RELEASE MODE"
cargo build --manifest-path \
"$MESON_SOURCE_ROOT"/Cargo.toml --release && \
cp "$CARGO_TARGET_DIR"/release/$5 $3
fi

View File

@ -0,0 +1,10 @@
#!/bin/bash
export DIST="$1"
export SOURCE_ROOT="$2"
cd "$SOURCE_ROOT"
mkdir "$DIST"/.cargo
cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config
# Move vendor into dist tarball directory
mv vendor "$DIST"

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
from os import environ, path
from subprocess import call
if not environ.get('DESTDIR', ''):
PREFIX = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
DATA_DIR = path.join(PREFIX, 'share')
print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(DATA_DIR, 'icons/hicolor')])
print("Compiling new schemas...")
call(["glib-compile-schemas", path.join(DATA_DIR, 'glib-2.0/schemas')])
print("Updating desktop database...")
call(["update-desktop-database", path.join(DATA_DIR, 'applications')])

View File

@ -0,0 +1,104 @@
{
"app-id": "org.qemu.gtk4.Devel",
"runtime": "org.gnome.Platform",
"runtime-version": "3.38",
"sdk": "org.gnome.Sdk",
"sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"],
"command": "qemu-gtk4",
"finish-args" : [
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
"--talk-name=org.a11y.Bus",
"--env=RUST_LOG=qemu-gtk4=debug",
"--env=G_MESSAGES_DEBUG=none"
],
"build-options" : {
"append-path" : "/usr/lib/sdk/rust-stable/bin",
"build-args" : [
"--share=network"
],
"test-args": [
"--socket=x11",
"--share=network"
],
"env" : {
"CARGO_HOME" : "/run/build/qemu-gtk4/cargo",
"RUST_BACKTRACE": "1",
"RUSTFLAGS": "-L=/app/lib"
}
},
"modules": [
{
"name": "gtk4",
"buildsystem": "meson",
"config-opts": [
"-Ddemos=false",
"-Dbuild-examples=false",
"-Dbuild-tests=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/gtk/4.0/gtk-4.0.3.tar.xz",
"sha256": "d7c9893725790b50bd9a3bb278856d9d543b44b6b9b951d7b60e7bdecc131890"
}
],
"modules": [
{
"name": "pango",
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/pango/1.48/pango-1.48.1.tar.xz",
"sha256": "08c2d550a96559f15fb317d7167b96df57ef743fef946f4e274bd8b6f2918058"
}
]
},
{
"name": "libsass",
"sources": [
{
"type": "archive",
"url": "https://github.com/sass/libsass/archive/3.6.4.tar.gz",
"sha256": "f9484d9a6df60576e791566eab2f757a97fd414fce01dd41fc0a693ea5db2889"
},
{
"type": "script",
"dest-filename": "autogen.sh",
"commands": ["autoreconf -si"]
}
]
},
{
"name": "sassc",
"sources": [
{
"type": "archive",
"url": "https://github.com/sass/sassc/archive/3.6.1.tar.gz",
"sha256": "8cee391c49a102b4464f86fc40c4ceac3a2ada52a89c4c933d8348e3e4542a60"
},
{
"type": "script",
"dest-filename": "autogen.sh",
"commands": ["autoreconf -si"]
}
]
}
]
},
{
"name": "qemu-gtk4",
"buildsystem": "meson",
"run-tests": true,
"config-opts": ["-Dprofile=development"],
"sources": [
{
"type": "dir",
"path": "../"
}
]
}
]
}

View File

@ -0,0 +1,10 @@
install_data(
'@0@.svg'.format(application_id),
install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps'
)
install_data(
'@0@-symbolic.svg'.format(base_id),
install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps',
rename: '@0@-symbolic.svg'.format(application_id)
)

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<defs>
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect x="0" y="0" width="16" height="16" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip1">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10818" clip-path="url(#clip1)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask1">
<g filter="url(#alpha)">
<rect x="0" y="0" width="16" height="16" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip2">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10821" clip-path="url(#clip2)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask2">
<g filter="url(#alpha)">
<rect x="0" y="0" width="16" height="16" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip3">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10824" clip-path="url(#clip3)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask3">
<g filter="url(#alpha)">
<rect x="0" y="0" width="16" height="16" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip4">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10827" clip-path="url(#clip4)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
</defs>
<g id="surface10764">
<rect x="0" y="0" width="16" height="16" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
<use xlink:href="#surface10818" transform="matrix(1,0,0,1,-168,-16)" mask="url(#mask0)"/>
<use xlink:href="#surface10821" transform="matrix(1,0,0,1,-168,-16)" mask="url(#mask1)"/>
<use xlink:href="#surface10824" transform="matrix(1,0,0,1,-168,-16)" mask="url(#mask2)"/>
<use xlink:href="#surface10827" transform="matrix(1,0,0,1,-168,-16)" mask="url(#mask3)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
<defs>
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip1">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10726" clip-path="url(#clip1)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask1">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip2">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10729" clip-path="url(#clip2)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask2">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip3">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10732" clip-path="url(#clip3)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask3">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip4">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10735" clip-path="url(#clip4)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask5">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip7">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10726" clip-path="url(#clip7)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask6">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip8">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10729" clip-path="url(#clip8)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask7">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip9">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10732" clip-path="url(#clip9)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask8">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip10">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10735" clip-path="url(#clip10)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<clipPath id="clip6">
<rect x="0" y="0" width="128" height="128"/>
</clipPath>
<g id="surface10750" clip-path="url(#clip6)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
<use xlink:href="#surface10726" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask5)"/>
<use xlink:href="#surface10729" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask6)"/>
<use xlink:href="#surface10732" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask7)"/>
<use xlink:href="#surface10735" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask8)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(38.431373%,62.7451%,91.764706%);stroke-opacity:1;stroke-miterlimit:4;" d="M 0 289 L 128 289 " transform="matrix(1,0,0,1,0,-172)"/>
</g>
<clipPath id="clip5">
<rect x="0" y="0" width="128" height="128"/>
</clipPath>
<g id="surface10753" clip-path="url(#clip5)" filter="url(#alpha)">
<use xlink:href="#surface10750"/>
</g>
<mask id="mask4">
<use xlink:href="#surface10753"/>
</mask>
<mask id="mask9">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.8;stroke:none;"/>
</g>
</mask>
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="300" y1="235" x2="428" y2="235" gradientTransform="matrix(0.000000000000000023,0.37,-0.98462,0.00000000000000006,295.38501,-30.360001)">
<stop offset="0" style="stop-color:rgb(97.647059%,94.117647%,41.960785%);stop-opacity:1;"/>
<stop offset="1" style="stop-color:rgb(96.078432%,76.078433%,6.666667%);stop-opacity:1;"/>
</linearGradient>
<clipPath id="clip12">
<rect x="0" y="0" width="128" height="128"/>
</clipPath>
<g id="surface10747" clip-path="url(#clip12)">
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 128 80.640625 L 128 128 L 0 128 L 0 80.640625 Z M 128 80.640625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 13.308594 80.640625 L 60.664062 128 L 81.878906 128 L 34.519531 80.640625 Z M 55.730469 80.640625 L 103.09375 128 L 124.308594 128 L 76.945312 80.640625 Z M 98.160156 80.640625 L 128 110.480469 L 128 89.269531 L 119.371094 80.640625 Z M 0 88.546875 L 0 109.761719 L 18.238281 128 L 39.453125 128 Z M 0 88.546875 "/>
</g>
<clipPath id="clip11">
<rect x="0" y="0" width="128" height="128"/>
</clipPath>
<g id="surface10752" clip-path="url(#clip11)">
<use xlink:href="#surface10747" mask="url(#mask9)"/>
</g>
</defs>
<g id="surface10672">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
<use xlink:href="#surface10726" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask0)"/>
<use xlink:href="#surface10729" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask1)"/>
<use xlink:href="#surface10732" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask2)"/>
<use xlink:href="#surface10735" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask3)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(38.431373%,62.7451%,91.764706%);stroke-opacity:1;stroke-miterlimit:4;" d="M 0 289 L 128 289 " transform="matrix(1,0,0,1,0,-172)"/>
<use xlink:href="#surface10752" mask="url(#mask4)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
<defs>
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip1">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10632" clip-path="url(#clip1)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask1">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip2">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10635" clip-path="url(#clip2)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask2">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip3">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10638" clip-path="url(#clip3)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask3">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip4">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10641" clip-path="url(#clip4)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
</defs>
<g id="surface10578">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
<use xlink:href="#surface10632" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask0)"/>
<use xlink:href="#surface10635" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask1)"/>
<use xlink:href="#surface10638" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask2)"/>
<use xlink:href="#surface10641" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask3)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(38.431373%,62.7451%,91.764706%);stroke-opacity:1;stroke-miterlimit:4;" d="M 0 289 L 128 289 " transform="matrix(1,0,0,1,0,-172)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,83 @@
subdir('icons')
# Desktop file
desktop_conf = configuration_data()
desktop_conf.set('icon', application_id)
desktop_file = i18n.merge_file(
type: 'desktop',
input: configure_file(
input: '@0@.desktop.in.in'.format(base_id),
output: '@BASENAME@',
configuration: desktop_conf
),
output: '@0@.desktop'.format(application_id),
po_dir: podir,
install: true,
install_dir: datadir / 'applications'
)
# Validate Desktop file
if desktop_file_validate.found()
test(
'validate-desktop',
desktop_file_validate,
args: [
desktop_file.full_path()
]
)
endif
# Appdata
appdata_conf = configuration_data()
appdata_conf.set('app-id', application_id)
appdata_conf.set('gettext-package', gettext_package)
appdata_file = i18n.merge_file(
input: configure_file(
input: '@0@.metainfo.xml.in.in'.format(base_id),
output: '@BASENAME@',
configuration: appdata_conf
),
output: '@0@.metainfo.xml'.format(application_id),
po_dir: podir,
install: true,
install_dir: datadir / 'metainfo'
)
# Validate Appdata
if appstream_util.found()
test(
'validate-appdata', appstream_util,
args: [
'validate', '--nonet', appdata_file.full_path()
]
)
endif
# GSchema
gschema_conf = configuration_data()
gschema_conf.set('app-id', application_id)
gschema_conf.set('gettext-package', gettext_package)
configure_file(
input: '@0@.gschema.xml.in'.format(base_id),
output: '@0@.gschema.xml'.format(application_id),
configuration: gschema_conf,
install: true,
install_dir: datadir / 'glib-2.0' / 'schemas'
)
# Validata GSchema
if glib_compile_schemas.found()
test(
'validate-gschema', glib_compile_schemas,
args: [
'--strict', '--dry-run', meson.current_source_dir()
]
)
endif
# Resources
resources = gnome.compile_resources(
'resources',
'resources.gresource.xml',
gresource_bundle: true,
source_dir: meson.current_build_dir(),
install: true,
install_dir: pkgdatadir,
)

View File

@ -0,0 +1,11 @@
[Desktop Entry]
Name=QEMU Gtk
Comment=A GTK + Rust application boilerplate template
Type=Application
Exec=qemu-gtk4
Terminal=false
Categories=GNOME;GTK;
Keywords=Gnome;GTK;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=@icon@
StartupNotify=true

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema path="/org/qemu/gtk4/" id="@app-id@" gettext-domain="@gettext-package@">
<key name="window-width" type="i">
<default>-1</default>
<summary>Default window width</summary>
<description>Default window width</description>
</key>
<key name="window-height" type="i">
<default>-1</default>
<summary>Default window height</summary>
<description>Default window height</description>
</key>
<key name="is-maximized" type="b">
<default>false</default>
<summary>Default window maximized behaviour</summary>
<description></description>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- QEMU developpers 2021 <qemu-devel@nongnu.org> -->
<component type="desktop-application">
<id>@app-id@</id>
<metadata_license>CC0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>QEMU Gtk</name>
<summary>A GTK QEMU display.</summary>
<description>
<p>GTK application to interact with QEMU display.</p>
</description>
<url type="homepage">https://gitlab.com/qemu-project/qemu</url>
<url type="bugtracker">https://gitlab.com/qemu-project/qemu/issues</url>
<content_rating type="oars-1.0" />
<releases>
<release version="0.0.1" date="2021-01-01" />
</releases>
<kudos>
<!--
GNOME Software kudos:
https://gitlab.gnome.org/GNOME/gnome-software/blob/master/doc/kudos.md
-->
<kudo>ModernToolkit</kudo>
<kudo>HiDpiIcon</kudo>
</kudos>
<developer_name>QEMU developpers</developer_name>
<update_contact>qemu-devel@nongnu.org</update_contact>
<translation type="gettext">@gettext-package@</translation>
<launchable type="desktop-id">@app-id@.desktop</launchable>
</component>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/qemu/gtk4/">
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">resources/ui/shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="window.ui">resources/ui/window.ui</file>
<file compressed="true" alias="style.css">resources/style.css</file>
</gresource>
</gresources>

View File

@ -0,0 +1,4 @@
.title-header{
font-size: 36px;
font-weight: bold;
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="shortcuts">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">10</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes" context="shortcut window">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Show Shortcuts</property>
<property name="accelerator">&lt;Primary&gt;question</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Quit</property>
<property name="accelerator">&lt;Primary&gt;Q</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1,40 @@
<interface>
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">app.preferences</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About GTK QEMU</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
<template class="QemuApplicationWindow" parent="GtkApplicationWindow">
<property name="default-width">600</property>
<property name="default-height">400</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="headerbar">
<child type="end">
<object class="GtkMenuButton" id="appmenu_button">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="label">
<property name="label" translatable="yes">Hello world!</property>
<style>
<class name="title-header"/>
</style>
</object>
</child>
</template>
</interface>

54
qemu-gtk4/hooks/pre-commit.hook Executable file
View File

@ -0,0 +1,54 @@
#!/bin/sh
# Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook
install_rustfmt() {
if ! which rustup &> /dev/null; then
curl https://sh.rustup.rs -sSf | sh -s -- -y
export PATH=$PATH:$HOME/.cargo/bin
if ! which rustup &> /dev/null; then
echo "Failed to install rustup. Performing the commit without style checking."
exit 0
fi
fi
if ! rustup component list|grep rustfmt &> /dev/null; then
echo "Installing rustfmt…"
rustup component add rustfmt
fi
}
if ! which cargo &> /dev/null || ! cargo fmt --help &> /dev/null; then
echo "Unable to check Fractals code style, because rustfmt could not be run."
if [ ! -t 1 ]; then
# No input is possible
echo "Performing commit."
exit 0
fi
echo ""
echo "y: Install rustfmt via rustup"
echo "n: Don't install rustfmt and perform the commit"
echo "Q: Don't install rustfmt and abort the commit"
while true; do
read -p "Install rustfmt via rustup? [y/n/Q]: " yn
case $yn in
[Yy]* ) install_rustfmt; break;;
[Nn]* ) echo "Performing commit."; exit 0;;
[Qq]* | "" ) echo "Aborting commit."; exit -1;;
* ) echo "Invalid input";;
esac
done
fi
echo "--Checking style--"
cargo fmt --all -- --check
if test $? != 0; then
echo "--Checking style fail--"
echo "Please fix the above issues, either manually or by running: cargo fmt --all"
exit -1
else
echo "--Checking style pass--"
fi

70
qemu-gtk4/meson.build Normal file
View File

@ -0,0 +1,70 @@
project('qemu-gtk4',
'rust',
version: '0.0.1',
license: 'MIT',
meson_version: '>= 0.50')
i18n = import('i18n')
gnome = import('gnome')
base_id = 'org.qemu.gtk4'
dependency('glib-2.0', version: '>= 2.66')
dependency('gio-2.0', version: '>= 2.66')
dependency('gtk4', version: '>= 4.0.0')
glib_compile_resources = find_program('glib-compile-resources', required: true)
glib_compile_schemas = find_program('glib-compile-schemas', required: true)
desktop_file_validate = find_program('desktop-file-validate', required: false)
appstream_util = find_program('appstream-util', required: false)
cargo = find_program('cargo', required: true)
cargo_script = find_program('build-aux/cargo.sh')
version = meson.project_version()
version_array = version.split('.')
major_version = version_array[0].to_int()
minor_version = version_array[1].to_int()
version_micro = version_array[2].to_int()
prefix = get_option('prefix')
bindir = prefix / get_option('bindir')
localedir = prefix / get_option('localedir')
datadir = prefix / get_option('datadir')
pkgdatadir = datadir / meson.project_name()
iconsdir = datadir / 'icons'
podir = meson.source_root() / 'po'
gettext_package = meson.project_name()
if get_option('profile') == 'development'
profile = 'Devel'
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip()
if vcs_tag == ''
version_suffix = '-devel'
else
version_suffix = '-@0@'.format(vcs_tag)
endif
application_id = '@0@.@1@'.format(base_id, profile)
else
profile = ''
version_suffix = ''
application_id = base_id
endif
meson.add_dist_script(
'build-aux/dist-vendor.sh',
meson.build_root() / 'meson-dist' / meson.project_name() + '-' + version,
meson.source_root()
)
if get_option('profile') == 'development'
# Setup pre-commit hook for ensuring coding style is always consistent
message('Setting up git pre-commit hook..')
run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit')
endif
subdir('data')
subdir('po')
subdir('src')
meson.add_install_script('build-aux/meson_post_install.py')

View File

@ -0,0 +1,10 @@
option(
'profile',
type: 'combo',
choices: [
'default',
'development'
],
value: 'default',
description: 'The build profile for QEMU Gtk. One of "default" or "development".'
)

0
qemu-gtk4/po/LINGUAS Normal file
View File

5
qemu-gtk4/po/POTFILES.in Normal file
View File

@ -0,0 +1,5 @@
data/resources/ui/shortcuts.ui
data/resources/ui/window.ui.in
data/org.qemu.gtk4.desktop.in.in
data/org.qemu.gtk4.gschema.xml.in
data/org.qemu.gtk4.metainfo.xml.in.in

1
qemu-gtk4/po/meson.build Normal file
View File

@ -0,0 +1 @@
i18n.gettext(gettext_package, preset: 'glib')

View File

@ -0,0 +1,170 @@
use crate::config;
use crate::window::QemuApplicationWindow;
use gio::ApplicationFlags;
use glib::clone;
use glib::WeakRef;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gdk, gio, glib};
use gtk_macros::action;
use log::{debug, info};
use once_cell::sync::OnceCell;
use std::env;
use qemu_display_listener::Console;
use zbus::Connection;
mod imp {
use super::*;
use glib::subclass;
#[derive(Debug)]
pub struct QemuApplication {
pub window: OnceCell<WeakRef<QemuApplicationWindow>>,
pub conn: OnceCell<Connection>,
}
impl ObjectSubclass for QemuApplication {
const NAME: &'static str = "QemuApplication";
type Type = super::QemuApplication;
type ParentType = gtk::Application;
type Interfaces = ();
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib::object_subclass!();
fn new() -> Self {
Self {
window: OnceCell::new(),
conn: OnceCell::new(),
}
}
}
impl ObjectImpl for QemuApplication {}
impl gio::subclass::prelude::ApplicationImpl for QemuApplication {
fn activate(&self, app: &Self::Type) {
debug!("GtkApplication<QemuApplication>::activate");
let priv_ = QemuApplication::from_instance(app);
if let Some(window) = priv_.window.get() {
let window = window.upgrade().unwrap();
window.show();
window.present();
return;
}
app.set_resource_base_path(Some("/org/qemu/gtk4/"));
app.setup_css();
let conn = Connection::new_session().expect("Failed to connect");
let console = Console::new(&conn, 0).expect("Failed to get the console");
self.conn.set(conn).expect("Connection already set.");
let window = QemuApplicationWindow::new(app, console);
self.window
.set(window.downgrade())
.expect("Window already set.");
app.setup_gactions();
app.setup_accels();
app.get_main_window().present();
}
fn startup(&self, app: &Self::Type) {
debug!("GtkApplication<QemuApplication>::startup");
self.parent_startup(app);
}
}
impl GtkApplicationImpl for QemuApplication {}
}
glib::wrapper! {
pub struct QemuApplication(ObjectSubclass<imp::QemuApplication>)
@extends gio::Application, gtk::Application, @implements gio::ActionMap, gio::ActionGroup;
}
impl QemuApplication {
pub fn new() -> Self {
glib::Object::new(&[
("application-id", &Some(config::APP_ID)),
("flags", &ApplicationFlags::empty()),
])
.expect("Application initialization failed...")
}
fn get_main_window(&self) -> QemuApplicationWindow {
let priv_ = imp::QemuApplication::from_instance(self);
priv_.window.get().unwrap().upgrade().unwrap()
}
fn setup_gactions(&self) {
// Quit
action!(
self,
"quit",
clone!(@weak self as app => move |_, _| {
// This is needed to trigger the delete event
// and saving the window state
app.get_main_window().close();
app.quit();
})
);
// About
action!(
self,
"about",
clone!(@weak self as app => move |_, _| {
app.show_about_dialog();
})
);
}
// Sets up keyboard shortcuts
fn setup_accels(&self) {
self.set_accels_for_action("app.quit", &["<primary>q"]);
self.set_accels_for_action("win.show-help-overlay", &["<primary>question"]);
}
fn setup_css(&self) {
let provider = gtk::CssProvider::new();
provider.load_from_resource("/org/qemu/gtk4/style.css");
if let Some(display) = gdk::Display::get_default() {
gtk::StyleContext::add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
fn show_about_dialog(&self) {
let dialog = gtk::AboutDialogBuilder::new()
.program_name("QEMU Gtk")
.logo_icon_name(config::APP_ID)
.license_type(gtk::License::MitX11)
.website("https://gitlab.com/qemu-project/qemu/")
.version(config::VERSION)
.transient_for(&self.get_main_window())
.modal(true)
.authors(vec!["QEMU developpers".into()])
.artists(vec!["QEMU developpers".into()])
.build();
dialog.show();
}
pub fn run(&self) {
info!("QEMU Gtk ({})", config::APP_ID);
info!("Version: {} ({})", config::VERSION, config::PROFILE);
info!("Datadir: {}", config::PKGDATADIR);
let args: Vec<String> = env::args().collect();
ApplicationExtManual::run(self, &args);
}
}

View File

@ -0,0 +1,7 @@
pub const APP_ID: &str = @APP_ID@;
pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@;
pub const LOCALEDIR: &str = @LOCALEDIR@;
pub const PKGDATADIR: &str = @PKGDATADIR@;
pub const PROFILE: &str = @PROFILE@;
pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource");
pub const VERSION: &str = @VERSION@;

32
qemu-gtk4/src/main.rs Normal file
View File

@ -0,0 +1,32 @@
#[allow(clippy::new_without_default)]
mod application;
#[rustfmt::skip]
mod config;
mod window;
use application::QemuApplication;
use config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE};
use gettextrs::*;
use gtk::gio;
fn main() {
// Initialize logger, debug is carried out via debug!, info!, and warn!.
pretty_env_logger::init();
// Prepare i18n
setlocale(LocaleCategory::LcAll, "");
bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
textdomain(GETTEXT_PACKAGE);
gtk::glib::set_application_name("QEMU Gtk");
gtk::glib::set_prgname(Some("qemu-gtk4"));
gtk::init().expect("Unable to start GTK4");
let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file");
gio::resources_register(&res);
let app = QemuApplication::new();
app.run();
}

45
qemu-gtk4/src/meson.build Normal file
View File

@ -0,0 +1,45 @@
global_conf = configuration_data()
global_conf.set_quoted('APP_ID', application_id)
global_conf.set_quoted('PKGDATADIR', pkgdatadir)
global_conf.set_quoted('PROFILE', profile)
global_conf.set_quoted('VERSION', version + version_suffix)
global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package)
global_conf.set_quoted('LOCALEDIR', localedir)
config = configure_file(
input: 'config.rs.in',
output: 'config.rs',
configuration: global_conf
)
# Copy the config.rs output to the source directory.
run_command(
'cp',
meson.build_root() / 'src' / 'config.rs',
meson.source_root() / 'src' / 'config.rs',
check: true
)
sources = files(
'application.rs',
'config.rs',
'main.rs',
'window.rs',
)
custom_target(
'cargo-build',
build_by_default: true,
input: sources,
output: meson.project_name(),
console: true,
install: true,
install_dir: bindir,
depends: resources,
command: [
cargo_script,
meson.build_root(),
meson.source_root(),
'@OUTPUT@',
profile,
meson.project_name(),
]
)

140
qemu-gtk4/src/window.rs Normal file
View File

@ -0,0 +1,140 @@
use crate::application::QemuApplication;
use crate::config::{APP_ID, PROFILE};
use glib::clone;
use glib::signal::Inhibit;
use gtk::subclass::prelude::*;
use gtk::{self, prelude::*};
use gtk::{gio, glib, CompositeTemplate};
use log::warn;
use qemu_display_listener::Console;
mod imp {
use super::*;
use glib::subclass;
#[derive(Debug, CompositeTemplate)]
#[template(resource = "/org/qemu/gtk4/window.ui")]
pub struct QemuApplicationWindow {
#[template_child]
pub headerbar: TemplateChild<gtk::HeaderBar>,
#[template_child]
pub label: TemplateChild<gtk::Label>,
pub settings: gio::Settings,
}
impl ObjectSubclass for QemuApplicationWindow {
const NAME: &'static str = "QemuApplicationWindow";
type Type = super::QemuApplicationWindow;
type ParentType = gtk::ApplicationWindow;
type Interfaces = ();
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib::object_subclass!();
fn new() -> Self {
Self {
headerbar: TemplateChild::default(),
label: TemplateChild::default(),
settings: gio::Settings::new(APP_ID),
}
}
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
// You must call `Widget`'s `init_template()` within `instance_init()`.
fn instance_init(obj: &glib::subclass::InitializingObject<Self::Type>) {
obj.init_template();
}
}
impl ObjectImpl for QemuApplicationWindow {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
let builder = gtk::Builder::from_resource("/org/qemu/gtk4/shortcuts.ui");
let shortcuts = builder.get_object("shortcuts").unwrap();
obj.set_help_overlay(Some(&shortcuts));
// Devel Profile
if PROFILE == "Devel" {
obj.get_style_context().add_class("devel");
}
// load latest window state
obj.load_window_size();
}
}
impl WindowImpl for QemuApplicationWindow {
// save window state on delete event
fn close_request(&self, obj: &Self::Type) -> Inhibit {
if let Err(err) = obj.save_window_size() {
warn!("Failed to save window state, {}", &err);
}
Inhibit(false)
}
}
impl WidgetImpl for QemuApplicationWindow {}
impl ApplicationWindowImpl for QemuApplicationWindow {}
}
glib::wrapper! {
pub struct QemuApplicationWindow(ObjectSubclass<imp::QemuApplicationWindow>)
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, @implements gio::ActionMap, gio::ActionGroup;
}
impl QemuApplicationWindow {
pub fn new(app: &QemuApplication, console: Console) -> Self {
let window: Self = glib::Object::new(&[]).expect("Failed to create QemuApplicationWindow");
window.set_application(Some(app));
// Set icons for shell
gtk::Window::set_default_icon_name(APP_ID);
let rx = console
.glib_listen()
.expect("Failed to listen to the console");
rx.attach(
None,
clone!(@weak window as win => move |t| {
let label = &imp::QemuApplicationWindow::from_instance(&win).label;
label.set_text(&format!("{:?}", t));
Continue(true)
}),
);
window
}
pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
let settings = &imp::QemuApplicationWindow::from_instance(self).settings;
let size = self.get_default_size();
settings.set_int("window-width", size.0)?;
settings.set_int("window-height", size.1)?;
settings.set_boolean("is-maximized", self.is_maximized())?;
Ok(())
}
fn load_window_size(&self) {
let settings = &imp::QemuApplicationWindow::from_instance(self).settings;
let width = settings.get_int("window-width");
let height = settings.get_int("window-height");
let is_maximized = settings.get_boolean("is-maximized");
self.set_default_size(width, height);
if is_maximized {
self.maximize();
}
}
}