Browse Source

qr-code-scanner: Make Camera and CameraPaintable subclassable per-OS

fractal-6
Kévin Commaille 2 years ago committed by Kévin Commaille
parent
commit
1f2648f48c
  1. 23
      Cargo.toml
  2. 8
      meson.build
  3. 2
      src/contrib/mod.rs
  4. 123
      src/contrib/qr_code_scanner/camera.rs
  5. 40
      src/contrib/qr_code_scanner/camera/camera_paintable/linux.rs
  6. 101
      src/contrib/qr_code_scanner/camera/camera_paintable/mod.rs
  7. 119
      src/contrib/qr_code_scanner/camera/linux.rs
  8. 155
      src/contrib/qr_code_scanner/camera/mod.rs
  9. 3
      src/contrib/qr_code_scanner/mod.rs
  10. 2
      src/contrib/qr_code_scanner/qr_code_detector.rs
  11. 1
      src/prelude.rs
  12. 2
      src/session/model/verification/mod.rs

23
Cargo.toml

@ -21,11 +21,6 @@ codegen-units = 16
# Please keep dependencies sorted.
[dependencies]
ashpd = { version = "0.6", default-features = false, features = [
"pipewire",
"tracing",
"tokio",
] }
djb_hash = "0.1"
eyeball-im = "0.4"
futures-channel = "0.3"
@ -40,11 +35,6 @@ indexmap = "2"
mime = "0.3"
mime_guess = "2"
once_cell = "1"
oo7 = { version = "0.2", default-features = false, features = [
"native_crypto",
"tokio",
"tracing",
] }
pulldown-cmark = "0.9"
qrcode = "0.12"
rand = "0.8"
@ -106,3 +96,16 @@ features = [
"compat-get-3pids",
"html",
]
# Linux-only dependencies.
[target.'cfg(target_os = "linux")'.dependencies]
ashpd = { version = "0.6", default-features = false, features = [
"pipewire",
"tracing",
"tokio",
] }
oo7 = { version = "0.2", default-features = false, features = [
"native_crypto",
"tokio",
"tracing",
] }

8
meson.build

@ -39,11 +39,15 @@ dependency(
fallback: ['gtksourceview', 'gtksource_dep'],
default_options: ['gtk_doc=false', 'sysprof=false', 'gir=false', 'vapi=false', 'install_tests=false']
)
dependency('libpipewire-0.3', version: '>= 0.3.0')
dependency('openssl', version: '>= 1.0.1')
dependency('shumate-1.0', version: '>= 1.0.0')
dependency('sqlite3', version: '>= 3.24.0')
dependency('xdg-desktop-portal', version: '>= 1.14.1')
# Linux-only dependencies
if build_machine.system() == 'linux'
dependency('libpipewire-0.3', version: '>= 0.3.0')
dependency('xdg-desktop-portal', version: '>= 1.14.1')
endif
glib_compile_resources = find_program('glib-compile-resources', required: true)
glib_compile_schemas = find_program('glib-compile-schemas', required: true)

2
src/contrib/mod.rs

@ -3,5 +3,5 @@ mod qr_code_scanner;
pub use self::{
qr_code::QRCode,
qr_code_scanner::{Camera, QrCodeScanner},
qr_code_scanner::{Camera, CameraExt, QrCodeScanner},
};

123
src/contrib/qr_code_scanner/camera.rs

@ -1,123 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::time::Duration;
use ashpd::desktop::camera;
use gtk::{glib, subclass::prelude::*};
use once_cell::sync::Lazy;
use tracing::error;
use super::camera_paintable::CameraPaintable;
use crate::{spawn_tokio, utils::timeout_future};
mod imp {
use super::*;
#[derive(Debug, Default)]
pub struct Camera {
pub paintable: glib::WeakRef<CameraPaintable>,
}
#[glib::object_subclass]
impl ObjectSubclass for Camera {
const NAME: &'static str = "Camera";
type Type = super::Camera;
}
impl ObjectImpl for Camera {}
}
glib::wrapper! {
pub struct Camera(ObjectSubclass<imp::Camera>);
}
impl Camera {
/// Create a new `Camera`.
///
/// Use `Camera::default()` to get a shared GObject.
fn new() -> Self {
glib::Object::new()
}
/// Ask the system whether cameras are available.
pub async fn has_cameras(&self) -> bool {
let handle = spawn_tokio!(async move {
let camera = match camera::Camera::new().await {
Ok(camera) => camera,
Err(error) => {
error!("Failed to create instance of camera proxy: {error}");
return false;
}
};
match camera.is_present().await {
Ok(is_present) => is_present,
Err(error) => {
error!("Failed to check whether system has cameras: {error}");
false
}
}
});
let abort_handle = handle.abort_handle();
match timeout_future(Duration::from_secs(1), handle).await {
Ok(is_present) => is_present.expect("The task should not have been aborted"),
Err(_) => {
abort_handle.abort();
error!("Failed to check whether system has cameras: the request timed out");
false
}
}
}
/// Get the a `gdk::Paintable` displaying the content of a camera.
///
/// Panics if not called from the `MainContext` where GTK is running.
pub async fn paintable(&self) -> Option<CameraPaintable> {
// We need to make sure that the Paintable is taken only from the MainContext
assert!(glib::MainContext::default().is_owner());
let imp = self.imp();
if let Some(paintable) = imp.paintable.upgrade() {
return Some(paintable);
}
let handle = spawn_tokio!(async move { camera::request().await });
let abort_handle = handle.abort_handle();
match timeout_future(Duration::from_secs(1), handle).await {
Ok(tokio_res) => match tokio_res.expect("The task should not have been aborted") {
Ok(Some((fd, streams))) => {
let paintable = CameraPaintable::new(fd, streams).await;
imp.paintable.set(Some(&paintable));
Some(paintable)
}
Ok(None) => {
error!("Failed to request access to cameras: the response is empty");
None
}
Err(error) => {
error!("Failed to request access to cameras: {error}");
None
}
},
Err(_) => {
// Error because we reached the timeout.
abort_handle.abort();
error!("Failed to request access to cameras: the request timed out");
None
}
}
}
}
impl Default for Camera {
fn default() -> Self {
static CAMERA: Lazy<Camera> = Lazy::new(Camera::new);
CAMERA.to_owned()
}
}
unsafe impl Send for Camera {}
unsafe impl Sync for Camera {}

40
src/contrib/qr_code_scanner/camera_paintable.rs → src/contrib/qr_code_scanner/camera/camera_paintable/linux.rs

@ -24,53 +24,39 @@ use gtk::{
prelude::*,
subclass::prelude::*,
};
use matrix_sdk::encryption::verification::QrVerificationData;
use tracing::{debug, error};
use super::{Action, CameraPaintable, CameraPaintableImpl};
use crate::contrib::qr_code_scanner::{qr_code_detector::QrCodeDetector, QrVerificationDataBoxed};
pub enum Action {
QrCodeDetected(QrVerificationData),
}
mod imp {
use std::cell::RefCell;
use glib::subclass;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
pub struct CameraPaintable {
pub struct LinuxCameraPaintable {
pub pipeline: RefCell<Option<(gst::Pipeline, BusWatchGuard)>>,
pub sink_paintable: RefCell<Option<gdk::Paintable>>,
}
#[glib::object_subclass]
impl ObjectSubclass for CameraPaintable {
const NAME: &'static str = "CameraPaintable";
type Type = super::CameraPaintable;
impl ObjectSubclass for LinuxCameraPaintable {
const NAME: &'static str = "LinuxCameraPaintable";
type Type = super::LinuxCameraPaintable;
type ParentType = CameraPaintable;
type Interfaces = (gdk::Paintable,);
}
impl ObjectImpl for CameraPaintable {
impl ObjectImpl for LinuxCameraPaintable {
fn dispose(&self) {
self.obj().set_pipeline(None);
}
fn signals() -> &'static [subclass::Signal] {
static SIGNALS: Lazy<Vec<subclass::Signal>> = Lazy::new(|| {
vec![subclass::Signal::builder("code-detected")
.param_types([QrVerificationDataBoxed::static_type()])
.run_first()
.build()]
});
SIGNALS.as_ref()
}
}
impl PaintableImpl for CameraPaintable {
impl CameraPaintableImpl for LinuxCameraPaintable {}
impl PaintableImpl for LinuxCameraPaintable {
fn intrinsic_height(&self) -> i32 {
if let Some(paintable) = self.sink_paintable.borrow().as_ref() {
paintable.intrinsic_height()
@ -122,10 +108,12 @@ mod imp {
}
glib::wrapper! {
pub struct CameraPaintable(ObjectSubclass<imp::CameraPaintable>) @implements gdk::Paintable;
/// A paintable to display the output of a camera on Linux.
pub struct LinuxCameraPaintable(ObjectSubclass<imp::LinuxCameraPaintable>)
@extends CameraPaintable, @implements gdk::Paintable;
}
impl CameraPaintable {
impl LinuxCameraPaintable {
pub async fn new<F: AsRawFd>(fd: F, streams: Vec<camera::Stream>) -> Self {
let self_: Self = glib::Object::new();

101
src/contrib/qr_code_scanner/camera/camera_paintable/mod.rs

@ -0,0 +1,101 @@
/// Subclassable camera paintable.
use gtk::{gdk, glib, glib::closure_local, prelude::*, subclass::prelude::*};
use matrix_sdk::encryption::verification::QrVerificationData;
#[cfg(target_os = "linux")]
pub mod linux;
use crate::contrib::qr_code_scanner::QrVerificationDataBoxed;
pub enum Action {
QrCodeDetected(QrVerificationData),
}
mod imp {
use glib::subclass::Signal;
use once_cell::sync::Lazy;
use super::*;
#[repr(C)]
pub struct CameraPaintableClass {
pub parent_class: glib::object::Class<adw::Bin>,
}
unsafe impl ClassStruct for CameraPaintableClass {
type Type = CameraPaintable;
}
#[derive(Debug, Default)]
pub struct CameraPaintable;
#[glib::object_subclass]
impl ObjectSubclass for CameraPaintable {
const NAME: &'static str = "CameraPaintable";
type Type = super::CameraPaintable;
type Class = CameraPaintableClass;
type Interfaces = (gdk::Paintable,);
}
impl ObjectImpl for CameraPaintable {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("code-detected")
.param_types([QrVerificationDataBoxed::static_type()])
.run_first()
.build()]
});
SIGNALS.as_ref()
}
}
impl PaintableImpl for CameraPaintable {
fn snapshot(&self, _snapshot: &gdk::Snapshot, _width: f64, _height: f64) {
// Nothing to do
}
}
}
glib::wrapper! {
/// A subclassable paintable to display the output of a camera.
pub struct CameraPaintable(ObjectSubclass<imp::CameraPaintable>)
@implements gdk::Paintable;
}
pub trait CameraPaintableExt: 'static {
/// Connect to the signal emitted when a code is detected.
fn connect_code_detected<F: Fn(&Self, QrVerificationData) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId;
}
impl<O: IsA<CameraPaintable>> CameraPaintableExt for O {
fn connect_code_detected<F: Fn(&Self, QrVerificationData) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"activated",
true,
closure_local!(move |obj: Self, data: QrVerificationDataBoxed| {
f(&obj, data.0);
}),
)
}
}
/// Public trait that must be implemented for everything that derives from
/// `CameraPaintable`.
///
/// Overriding a method from this Trait overrides also its behavior in
/// `CameraPaintableExt`.
#[allow(async_fn_in_trait)]
pub trait CameraPaintableImpl: ObjectImpl + PaintableImpl {}
unsafe impl<T> IsSubclassable<T> for CameraPaintable
where
T: CameraPaintableImpl,
T::Type: IsA<CameraPaintable>,
{
}

119
src/contrib/qr_code_scanner/camera/linux.rs

@ -0,0 +1,119 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::time::Duration;
use ashpd::desktop::camera;
use gtk::{glib, prelude::*, subclass::prelude::*};
use tracing::error;
use super::{
camera_paintable::{linux::LinuxCameraPaintable, CameraPaintable},
Camera, CameraImpl,
};
use crate::{spawn_tokio, utils::timeout_future};
mod imp {
use super::*;
#[derive(Debug, Default)]
pub struct LinuxCamera {
pub paintable: glib::WeakRef<LinuxCameraPaintable>,
}
#[glib::object_subclass]
impl ObjectSubclass for LinuxCamera {
const NAME: &'static str = "LinuxCamera";
type Type = super::LinuxCamera;
type ParentType = Camera;
}
impl ObjectImpl for LinuxCamera {}
impl CameraImpl for LinuxCamera {
async fn has_cameras(&self) -> bool {
let handle = spawn_tokio!(async move {
let camera = match camera::Camera::new().await {
Ok(camera) => camera,
Err(error) => {
error!("Failed to create instance of camera proxy: {error}");
return false;
}
};
match camera.is_present().await {
Ok(is_present) => is_present,
Err(error) => {
error!("Failed to check whether system has cameras: {error}");
false
}
}
});
let abort_handle = handle.abort_handle();
match timeout_future(Duration::from_secs(1), handle).await {
Ok(is_present) => is_present.expect("The task should not have been aborted"),
Err(_) => {
abort_handle.abort();
error!("Failed to check whether system has cameras: the request timed out");
false
}
}
}
async fn paintable(&self) -> Option<CameraPaintable> {
// We need to make sure that the Paintable is taken only from the MainContext
assert!(glib::MainContext::default().is_owner());
if let Some(paintable) = self.paintable.upgrade() {
return Some(paintable.upcast());
}
let handle = spawn_tokio!(async move { camera::request().await });
let abort_handle = handle.abort_handle();
match timeout_future(Duration::from_secs(1), handle).await {
Ok(tokio_res) => match tokio_res.expect("The task should not have been aborted") {
Ok(Some((fd, streams))) => {
let paintable = LinuxCameraPaintable::new(fd, streams).await;
self.paintable.set(Some(&paintable));
Some(paintable.upcast())
}
Ok(None) => {
error!("Failed to request access to cameras: the response is empty");
None
}
Err(error) => {
error!("Failed to request access to cameras: {error}");
None
}
},
Err(_) => {
// Error because we reached the timeout.
abort_handle.abort();
error!("Failed to request access to cameras: the request timed out");
None
}
}
}
}
}
glib::wrapper! {
pub struct LinuxCamera(ObjectSubclass<imp::LinuxCamera>) @extends Camera;
}
impl LinuxCamera {
/// Create a new `LinuxCamera`.
pub fn new() -> Self {
glib::Object::new()
}
}
impl Default for LinuxCamera {
fn default() -> Self {
Self::new()
}
}
unsafe impl Send for LinuxCamera {}
unsafe impl Sync for LinuxCamera {}

155
src/contrib/qr_code_scanner/camera/mod.rs

@ -0,0 +1,155 @@
//! Camera API.
use futures_util::{future::LocalBoxFuture, FutureExt};
use gtk::{glib, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use tracing::error;
mod camera_paintable;
#[cfg(target_os = "linux")]
mod linux;
pub use self::camera_paintable::Action;
use self::camera_paintable::CameraPaintable;
mod imp {
use super::*;
#[repr(C)]
pub struct CameraClass {
pub parent_class: glib::object::Class<adw::Bin>,
pub has_cameras: fn(&super::Camera) -> LocalBoxFuture<bool>,
pub paintable: fn(&super::Camera) -> LocalBoxFuture<Option<CameraPaintable>>,
}
unsafe impl ClassStruct for CameraClass {
type Type = Camera;
}
pub(super) async fn camera_has_cameras(this: &super::Camera) -> bool {
let klass = this.class();
(klass.as_ref().has_cameras)(this).await
}
pub(super) async fn camera_paintable(this: &super::Camera) -> Option<CameraPaintable> {
let klass = this.class();
(klass.as_ref().paintable)(this).await
}
#[derive(Debug, Default)]
pub struct Camera;
#[glib::object_subclass]
impl ObjectSubclass for Camera {
const NAME: &'static str = "Camera";
type Type = super::Camera;
type Class = CameraClass;
}
impl ObjectImpl for Camera {}
}
glib::wrapper! {
/// Subclassable Camera API.
///
/// The default implementation, for unsupported platforms, makes sure the camera support is disabled.
pub struct Camera(ObjectSubclass<imp::Camera>);
}
impl Camera {
/// Create a new `Camera`.
///
/// Use `Camera::default()` to get a shared GObject.
fn new() -> Self {
#[cfg(target_os = "linux")]
let obj = linux::LinuxCamera::new().upcast();
#[cfg(not(target_os = "linux"))]
let obj = glib::Object::new();
obj
}
}
impl Default for Camera {
fn default() -> Self {
static CAMERA: Lazy<Camera> = Lazy::new(Camera::new);
CAMERA.to_owned()
}
}
unsafe impl Send for Camera {}
unsafe impl Sync for Camera {}
pub trait CameraExt: 'static {
/// Whether any cameras are available.
async fn has_cameras(&self) -> bool;
/// The paintable displaying the camera.
async fn paintable(&self) -> Option<CameraPaintable>;
}
impl<O: IsA<Camera>> CameraExt for O {
async fn has_cameras(&self) -> bool {
imp::camera_has_cameras(self.upcast_ref()).await
}
async fn paintable(&self) -> Option<CameraPaintable> {
imp::camera_paintable(self.upcast_ref()).await
}
}
/// Public trait that must be implemented for everything that derives from
/// `Camera`.
///
/// Overriding a method from this Trait overrides also its behavior in
/// `CameraExt`.
#[allow(async_fn_in_trait)]
pub trait CameraImpl: ObjectImpl {
/// Whether any cameras are available.
async fn has_cameras(&self) -> bool {
false
}
/// The paintable displaying the camera.
async fn paintable(&self) -> Option<CameraPaintable> {
error!("The camera API is not supported on this platform");
None
}
}
unsafe impl<T> IsSubclassable<T> for Camera
where
T: CameraImpl,
T::Type: IsA<Camera>,
{
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class.upcast_ref_mut());
let klass = class.as_mut();
klass.has_cameras = has_cameras_trampoline::<T>;
klass.paintable = paintable_trampoline::<T>;
}
}
// Virtual method implementation trampolines.
fn has_cameras_trampoline<T>(this: &Camera) -> LocalBoxFuture<bool>
where
T: ObjectSubclass + CameraImpl,
T::Type: IsA<Camera>,
{
let this = this.downcast_ref::<T::Type>().unwrap();
this.imp().has_cameras().boxed_local()
}
fn paintable_trampoline<T>(this: &Camera) -> LocalBoxFuture<Option<CameraPaintable>>
where
T: ObjectSubclass + CameraImpl,
T::Type: IsA<Camera>,
{
let this = this.downcast_ref::<T::Type>().unwrap();
this.imp().paintable().boxed_local()
}

3
src/contrib/qr_code_scanner/mod.rs

@ -3,10 +3,9 @@ use gtk::{gdk, glib, glib::subclass, prelude::*, subclass::prelude::*};
use matrix_sdk::encryption::verification::QrVerificationData;
mod camera;
mod camera_paintable;
mod qr_code_detector;
pub use camera::Camera;
pub use camera::{Camera, CameraExt};
mod imp {
use std::cell::RefCell;

2
src/contrib/qr_code_scanner/qr_code_detector.rs

@ -8,7 +8,7 @@ use thiserror::Error;
use tracing::debug;
use super::*;
use crate::contrib::qr_code_scanner::camera_paintable::Action;
use crate::contrib::qr_code_scanner::camera::Action;
const HEADER: &[u8] = b"MATRIX";

1
src/prelude.rs

@ -1,5 +1,6 @@
pub use crate::{
components::{ToastableWindowExt, ToastableWindowImpl},
contrib::CameraExt,
session::model::{TimelineItemExt, UserExt},
session_list::SessionInfoExt,
user_facing_error::UserFacingError,

2
src/session/model/verification/mod.rs

@ -10,7 +10,7 @@ pub use self::{
},
verification_list::VerificationList,
};
use crate::contrib::Camera;
use crate::{contrib::Camera, prelude::*};
/// A unique key to identify an identity verification.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]

Loading…
Cancel
Save