feat: a11y applet

This commit is contained in:
Ashley Wulber 2024-11-01 08:34:29 -04:00 committed by Ashley Wulber
parent 6b740c59be
commit eb27387fee
15 changed files with 732 additions and 178 deletions

396
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ members = [
"cosmic-applet-workspaces",
"cosmic-panel-button",
"cosmic-applet-input-sources",
"cosmic-applet-a11y",
]
resolver = "2"

View file

@ -0,0 +1,18 @@
[package]
name = "cosmic-applet-a11y"
version = "0.1.0"
edition = "2021"
[dependencies]
cosmic-dbus-a11y = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-time.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
rust-embed.workspace = true
once_cell = "1"
tokio = { version = "1.36.0", features = ["sync", "time", "macros"] }
tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
zbus.workspace = true

View file

@ -0,0 +1,18 @@
[Desktop Entry]
Name=Accessibility
Type=Application
Exec=cosmic-applet-a11y
Terminal=false
Categories=COSMIC;
Keywords=COSMIC;Iced;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=preferences-desktop-accessibility
StartupNotify=true
NoDisplay=true
X-CosmicApplet=true
# Indicates that the auto-hover click should go to the "end" of the hover popup
X-CosmicHoverPopup=Center
X-OverflowPriority=10
X-CosmicApplet=true
X-CosmicHoverPopup=Auto
X-OverflowPriority=10

View file

@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="applet-sound" clip-path="url(#clip0_4614_124622)">
<g id="Group">
<path id="Vector" d="M0 0H16V16H0V0Z" fill="#808080" fill-opacity="0.01"/>
<path id="Vector_2" d="M7.07001 2.04997C6.86376 2.05624 6.66456 2.1265 6.5 2.25098L3 5.00098H1C0.446 5.00098 0 5.44698 0 6.00098V10.001C0 10.555 0.446 11.001 1 11.001H3L6.5 13.751C7 14.144 8 14.001 8 13.001V3.00098C8 2.31398 7.52801 2.03097 7.07001 2.04997Z" fill="#232323"/>
<path id="Vector_3" d="M9 5.62L11 4.995C12 6.995 12 8.995 11 10.995L9 10.37C10 8.37 10 7.62 9 5.62Z" fill="#232323"/>
<path id="Vector_4" opacity="0.35" d="M12 3.94L14 3.00201C16 6.00201 16 10.002 14 13.002L12 12.065C14 9.065 14 6.94 12 3.94Z" fill="#232323"/>
</g>
</g>
<defs>
<clipPath id="clip0_4614_124622">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 903 B

View file

@ -0,0 +1,4 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"

View file

@ -0,0 +1 @@
accessibility = Accessibility

View file

@ -0,0 +1,236 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
backend::{self, A11yRequest},
fl,
};
use cosmic::{
applet::{
menu_button, padded_control,
token::subscription::{activation_token_subscription, TokenRequest, TokenUpdate},
},
cctk::sctk::reexports::calloop,
cosmic_theme::Spacing,
iced::{
alignment::Horizontal,
platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup},
widget::{column, container, row, slider},
window, Alignment, Length, Subscription,
},
iced_core::{alignment::Vertical, Background, Border, Color, Shadow},
iced_runtime::core::layout::Limits,
iced_widget::{Column, Row},
theme,
widget::{divider, horizontal_space, icon, scrollable, text, vertical_space},
Element, Task,
};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
use std::{collections::HashMap, path::PathBuf, time::Duration};
use tokio::sync::mpsc::UnboundedSender;
static ENABLED: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
pub fn run() -> cosmic::iced::Result {
cosmic::applet::run::<CosmicA11yApplet>(())
}
#[derive(Clone, Default)]
struct CosmicA11yApplet {
core: cosmic::app::Core,
icon_name: String,
a11y_enabled: bool,
popup: Option<window::Id>,
a11y_sender: Option<UnboundedSender<backend::A11yRequest>>,
timeline: Timeline,
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
}
#[derive(Debug, Clone)]
enum Message {
TogglePopup,
CloseRequested(window::Id),
Errored(String),
Enabled(chain::Toggler, bool),
Frame(Instant),
Token(TokenUpdate),
OpenSettings,
Update(backend::Update),
}
impl cosmic::Application for CosmicA11yApplet {
type Message = Message;
type Executor = cosmic::SingleThreadExecutor;
type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletA11y";
fn init(
core: cosmic::app::Core,
_flags: Self::Flags,
) -> (
Self,
cosmic::iced::Task<cosmic::app::Message<Self::Message>>,
) {
(
Self {
core,
token_tx: None,
..Default::default()
},
Task::none(),
)
}
fn core(&self) -> &cosmic::app::Core {
&self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
}
fn update(
&mut self,
message: Self::Message,
) -> cosmic::iced::Task<cosmic::app::Message<Self::Message>> {
match message {
Message::Frame(now) => self.timeline.now(now),
Message::Enabled(chain, enabled) => {
self.timeline.set_chain(chain).start();
self.a11y_enabled = enabled;
if let Some(tx) = &self.a11y_sender {
let _ = tx.send(A11yRequest::Status(enabled));
}
}
Message::Errored(why) => {
tracing::error!("{}", why);
}
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
return destroy_popup(p);
} else {
self.timeline = Timeline::new();
let new_id = window::Id::unique();
self.popup.replace(new_id);
let mut popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(),
new_id,
Some((1, 1)),
None,
None,
);
popup_settings.positioner.size_limits = Limits::NONE
.max_width(300.0)
.min_width(200.0)
.min_height(10.0)
.max_height(1080.0);
return get_popup(popup_settings);
}
}
Message::CloseRequested(id) => {
if Some(id) == self.popup {
self.popup = None;
}
}
Message::OpenSettings => {
let exec = "cosmic-settings a11y".to_string();
if let Some(tx) = self.token_tx.as_ref() {
let _ = tx.send(TokenRequest {
app_id: Self::APP_ID.to_string(),
exec,
});
} else {
tracing::error!("Wayland tx is None");
};
}
Message::Token(u) => match u {
TokenUpdate::Init(tx) => {
self.token_tx = Some(tx);
}
TokenUpdate::Finished => {
self.token_tx = None;
}
TokenUpdate::ActivationToken { token, .. } => {
let mut cmd = std::process::Command::new("cosmic-settings");
cmd.arg("a11y");
if let Some(token) = token {
cmd.env("XDG_ACTIVATION_TOKEN", &token);
cmd.env("DESKTOP_STARTUP_ID", &token);
}
tokio::spawn(cosmic::process::spawn(cmd));
}
},
Message::Update(update) => match update {
backend::Update::Error(err) => {
tracing::error!("{err}");
}
backend::Update::Status(enabled) => {
self.a11y_enabled = enabled;
}
backend::Update::Init(enabled, tx) => {
self.a11y_enabled = enabled;
self.a11y_sender = Some(tx);
}
},
}
Task::none()
}
fn view(&self) -> Element<Message> {
self.core
.applet
.icon_button("preferences-desktop-accessibility")
.on_press_down(Message::TogglePopup)
.into()
}
fn view_window(&self, _id: window::Id) -> Element<Message> {
let Spacing {
space_xxs, space_s, ..
} = theme::active().cosmic().spacing;
let toggle = padded_control(
anim!(
//toggler
ENABLED,
&self.timeline,
fl!("accessibility"),
self.a11y_enabled,
Message::Enabled,
)
.text_size(14)
.width(Length::Fill),
);
self.core
.applet
.popup_container(toggle.padding([8, 8]))
.max_width(372.)
.max_height(600.)
.into()
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
backend::subscription().map(Message::Update),
self.timeline
.as_subscription()
.map(|(_, now)| Message::Frame(now)),
activation_token_subscription(0).map(Message::Token),
])
}
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
Some(Message::CloseRequested(id))
}
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
Some(cosmic::applet::style())
}
}

View file

@ -0,0 +1,139 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::iced::futures::FutureExt;
use cosmic::{
iced::{
self,
futures::{self, select, SinkExt, StreamExt},
Subscription,
},
iced_futures::stream,
};
use cosmic_dbus_a11y::*;
use std::{fmt::Debug, hash::Hash};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use zbus::{Connection, Result};
#[derive(Debug, Clone)]
pub enum Update {
Error(String),
Status(bool),
Init(bool, UnboundedSender<A11yRequest>),
}
pub enum A11yRequest {
Status(bool),
}
#[derive(Debug)]
pub enum State {
Ready,
Waiting(Connection, u8, bool, UnboundedReceiver<A11yRequest>),
Finished,
}
pub fn subscription() -> iced::Subscription<Update> {
struct MyId;
Subscription::run_with_id(
std::any::TypeId::of::<MyId>(),
stream::channel(50, move |mut output| async move {
let mut state = State::Ready;
loop {
state = start_listening(state, &mut output).await;
}
}),
)
}
async fn start_listening(
state: State,
output: &mut futures::channel::mpsc::Sender<Update>,
) -> State {
match state {
State::Ready => {
let conn = match Connection::session().await.map_err(|e| e.to_string()) {
Ok(conn) => conn,
Err(e) => {
_ = output.send(Update::Error(e)).await;
return State::Finished;
}
};
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut enabled = false;
if let Ok(proxy) = StatusProxy::new(&conn).await {
if let Ok(status) = proxy.screen_reader_enabled().await {
enabled = status;
}
}
_ = output.send(Update::Init(enabled, tx)).await;
State::Waiting(conn, 20, enabled, rx)
}
State::Waiting(conn, mut retry, mut enabled, mut rx) => {
let Ok(proxy) = StatusProxy::new(&conn).await else {
if retry == 0 {
tracing::error!("Accessibility Status is unavailable.");
return State::Finished;
} else {
_ = tokio::time::sleep(tokio::time::Duration::from_secs(
2_u64.pow(retry as u32),
))
.await;
retry -= 1;
return State::Waiting(conn, retry, enabled, rx);
}
};
retry = 20;
let mut watch_changes = proxy.receive_screen_reader_enabled_changed().await;
if let Ok(status) = proxy.screen_reader_enabled().await {
if enabled != status {
_ = output.send(Update::Status(enabled));
}
enabled = status;
}
loop {
if let Ok(status) = proxy.screen_reader_enabled().await {
if enabled != status {
_ = output.send(Update::Status(enabled));
}
enabled = status;
}
let mut next_change = Box::pin(watch_changes.next()).fuse();
let mut next_request = Box::pin(rx.recv()).fuse();
select! {
v = next_request => {
match v {
Some(A11yRequest::Status(is_enabled)) => {
// Set status
enabled = is_enabled;
_ = proxy.set_is_enabled(is_enabled).await;
_ = proxy.set_screen_reader_enabled(is_enabled).await;
}
None => return State::Finished,
}
}
v = next_change => {
match v {
Some(f) => {
if let Ok(enabled) = f.get().await {
_ = output.send(Update::Status(enabled));
}
}
None => break,
};
}
}
}
State::Waiting(conn, retry, enabled, rx)
}
State::Finished => iced::futures::future::pending().await,
}
}

View file

@ -0,0 +1,13 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
mod app;
mod backend;
mod localize;
use localize::localize;
pub fn run() -> cosmic::iced::Result {
localize();
app::run()
}

View file

@ -0,0 +1,48 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use i18n_embed::{
fluent::{fluent_language_loader, FluentLanguageLoader},
DefaultLocalizer, LanguageLoader, Localizer,
};
use once_cell::sync::Lazy;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "i18n/"]
struct Localizations;
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_fallback_language(&Localizations)
.expect("Error while loading fallback language");
loader
});
#[macro_export]
macro_rules! fl {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
}};
($message_id:literal, $($args:expr),*) => {{
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
}};
}
// Get the `Localizer` to be used for localizing this library.
pub fn localizer() -> Box<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
}
pub fn localize() {
let localizer = localizer();
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
if let Err(error) = localizer.select(&requested_languages) {
eprintln!("Error while loading language for App List {}", error);
}
}

View file

@ -0,0 +1,13 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() -> cosmic::iced::Result {
tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init();
tracing::info!("Starting battery applet with version {VERSION}");
cosmic_applet_a11y::run()
}

View file

@ -7,6 +7,7 @@ license = "GPL-3.0-only"
[dependencies]
cosmic-app-list = { path = "../cosmic-app-list" }
cosmic-applet-audio = { path = "../cosmic-applet-audio" }
cosmic-applet-a11y = { path = "../cosmic-applet-a11y" }
cosmic-applet-battery = { path = "../cosmic-applet-battery" }
cosmic-applet-bluetooth = { path = "../cosmic-applet-bluetooth" }
cosmic-applet-minimize = { path = "../cosmic-applet-minimize" }
@ -17,8 +18,8 @@ cosmic-applet-status-area = { path = "../cosmic-applet-status-area" }
cosmic-applet-tiling = { path = "../cosmic-applet-tiling" }
cosmic-applet-time = { path = "../cosmic-applet-time" }
cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" }
cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources"}
cosmic-panel-button = { path = "../cosmic-panel-button"}
cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" }
cosmic-panel-button = { path = "../cosmic-panel-button" }
libcosmic.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

View file

@ -18,6 +18,7 @@ fn main() -> cosmic::iced::Result {
match cmd {
"cosmic-app-list" => cosmic_app_list::run(),
"cosmic-applet-a11y" => cosmic_applet_a11y::run(),
"cosmic-applet-audio" => cosmic_applet_audio::run(),
"cosmic-applet-battery" => cosmic_applet_battery::run(),
"cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(),

View file

@ -58,7 +58,7 @@ _install_metainfo:
install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
# Installs files into the system
install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo)
install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo)
# Vendor Cargo dependencies locally
vendor: