Browse Source

Merge branch 'wip/appindicator-support' into 'main'

Appindicator support / background mode

See merge request World/fractal!2138
merge-requests/2138/merge
Pavel Shirshov 7 days ago
parent
commit
7e46495e74
  1. 14
      Cargo.lock
  2. 1
      Cargo.toml
  3. 48
      src/application.rs
  4. 22
      src/main.rs
  5. 10
      src/session/notifications/mod.rs
  6. 7
      src/session_view/sidebar/mod.blp
  7. 198
      src/tray.rs
  8. 4
      src/window.rs

14
Cargo.lock generated

@ -1143,6 +1143,7 @@ dependencies = [
"gstreamer-video",
"gtk4",
"indexmap",
"ksni",
"libadwaita",
"libglycin-gtk4-rebind",
"libglycin-rebind",
@ -2469,6 +2470,19 @@ dependencies = [
"typewit",
]
[[package]]
name = "ksni"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b29c089f14ce24c5b25d9bdcb265413b5e0c3df0871823e0d96bd83bc52a24"
dependencies = [
"futures-util",
"pastey",
"serde",
"tokio",
"zbus",
]
[[package]]
name = "kstring"
version = "2.0.2"

1
Cargo.toml

@ -74,6 +74,7 @@ gst_video = { version = "0.24", package = "gstreamer-video" }
gtk = { version = "0.10", features = ["gnome_49"], package = "gtk4" }
shumate = { version = "0.7", features = ["v1_1"], package = "libshumate" }
sourceview = { version = "0.10", package = "sourceview5" }
ksni = "0.3"
[dependencies.matrix-sdk]
# version = "0.14"

48
src/application.rs

@ -25,9 +25,10 @@ pub(crate) const APP_NAME: &str = "Fractal";
pub(crate) const APP_HOMEPAGE_URL: &str = "https://gitlab.gnome.org/World/fractal/";
mod imp {
use std::cell::Cell;
use std::cell::{Cell, OnceCell};
use super::*;
use crate::tray::{spawn_tray, TrayCommand, TrayHandle};
#[derive(Debug)]
pub struct Application {
@ -39,6 +40,10 @@ mod imp {
pub(super) session_list: SessionList,
intent_handler: BoundObjectWeakRef<glib::Object>,
last_network_state: Cell<NetworkState>,
/// Whether the app was started with --minimized flag.
pub start_minimized: Cell<bool>,
/// System tray handle.
pub tray_handle: OnceCell<TrayHandle>,
}
impl Default for Application {
@ -49,6 +54,8 @@ mod imp {
session_list: Default::default(),
intent_handler: Default::default(),
last_network_state: Default::default(),
start_minimized: Cell::new(false),
tray_handle: OnceCell::new(),
}
}
}
@ -89,6 +96,30 @@ mod imp {
}
));
// Initialize system tray.
let mut tray_handle = spawn_tray();
let tray_rx = tray_handle.take_receiver();
let _ = self.tray_handle.set(tray_handle);
if let Some(mut rx) = tray_rx {
spawn!(clone!(
#[weak(rename_to = obj)]
self.obj(),
async move {
while let Some(cmd) = rx.recv().await {
match cmd {
TrayCommand::Show => {
obj.imp().present_main_window();
}
TrayCommand::Quit => {
obj.quit();
}
}
}
}
));
}
// Watch the network to log its state.
let network_monitor = gio::NetworkMonitor::default();
network_monitor.connect_network_changed(clone!(
@ -149,6 +180,21 @@ mod imp {
///
/// Returns the main window.
fn present_main_window(&self) -> Window {
// Clear tray unread indicator when presenting window.
if let Some(tray) = self.tray_handle.get() {
tray.set_has_unread(false);
}
// If started with --minimized, skip presentation on first call only.
if self.start_minimized.replace(false) {
let window = if let Some(window) = self.obj().active_window().and_downcast() {
window
} else {
Window::new(&self.obj())
};
return window;
}
let window = if let Some(window) = self.obj().active_window().and_downcast() {
window
} else {

22
src/main.rs

@ -22,14 +22,18 @@ mod session;
mod session_list;
mod session_view;
mod system_settings;
mod tray;
mod user_facing_error;
mod utils;
mod window;
use std::sync::LazyLock;
use std::{ops::ControlFlow, sync::LazyLock};
use gettextrs::*;
use gio::prelude::*;
use gtk::{IconTheme, gdk::Display, gio};
use gtk::glib::{Char, OptionArg, OptionFlags};
use gtk::subclass::prelude::ObjectSubclassIsExt;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use self::{application::*, config::*, i18n::*, utils::OneshotNotifier, window::Window};
@ -77,5 +81,21 @@ fn main() {
.add_resource_path("/org/gnome/Fractal/icons");
let app = Application::new();
// Register --minimized/-m option.
app.add_main_option(
"minimized",
Char::from(b'm'),
OptionFlags::NONE,
OptionArg::None,
"Start minimized to system tray",
None,
);
app.connect_handle_local_options(|app, options| {
app.imp().start_minimized.set(options.contains("minimized"));
ControlFlow::Continue(())
});
app.run();
}

10
src/session/notifications/mod.rs

@ -13,7 +13,7 @@ use ruma::{
},
html::{HtmlSanitizerMode, RemoveReplyFallback},
};
use tracing::{debug, warn};
use tracing::{debug, info, warn};
mod notifications_settings;
@ -144,6 +144,14 @@ impl Notifications {
}
Application::default().send_notification(Some(id), &notification);
// Update tray unread indicator.
info!("Notification sent, updating tray unread indicator");
if let Some(tray) = Application::default().imp().tray_handle.get() {
tray.set_has_unread(true);
} else {
warn!("Tray handle not available");
}
}
/// Ask the system to show the given push notification, if applicable.

7
src/session_view/sidebar/mod.blp

@ -30,6 +30,13 @@ menu primary_menu {
action: "app.about";
}
}
section {
item {
label: _("_Quit");
action: "app.quit";
}
}
}
menu room_row_menu {

198
src/tray.rs

@ -0,0 +1,198 @@
use gettextrs::gettext;
use ksni::{menu::StandardItem, Icon, MenuItem, Tray, TrayMethods};
use tokio::sync::mpsc;
use tracing::{info, warn};
use crate::{RUNTIME, gettext_f};
#[derive(Debug)]
pub enum TrayCommand {
Show,
Quit,
}
pub struct FractalTray {
tx: mpsc::UnboundedSender<TrayCommand>,
pub has_unread: bool,
}
impl FractalTray {
pub fn new(tx: mpsc::UnboundedSender<TrayCommand>) -> Self {
Self {
tx,
has_unread: false,
}
}
}
impl Tray for FractalTray {
fn icon_name(&self) -> String {
if self.has_unread {
"mail-message-new".to_string()
} else {
"org.gnome.Fractal".to_string()
}
}
fn overlay_icon_pixmap(&self) -> Vec<Icon> {
if !self.has_unread {
return vec![];
}
// Red notification dot overlay.
let size = 24;
let mut data = Vec::with_capacity((size * size * 4) as usize);
let dot_radius = 5i32;
let dot_center_x = size - dot_radius - 1;
let dot_center_y = dot_radius + 1;
for y in 0..size {
for x in 0..size {
let dx = x - dot_center_x;
let dy = y - dot_center_y;
let in_dot = dx * dx + dy * dy <= dot_radius * dot_radius;
if in_dot {
// Red dot.
data.push(255); // A
data.push(220); // R
data.push(50); // G
data.push(50); // B
} else {
// Transparent.
data.push(0); // A
data.push(0); // R
data.push(0); // G
data.push(0); // B
}
}
}
vec![Icon {
width: size,
height: size,
data,
}]
}
fn title(&self) -> String {
gettext("Fractal")
}
fn id(&self) -> String {
"org.gnome.Fractal".to_string()
}
fn category(&self) -> ksni::Category {
ksni::Category::Communications
}
fn status(&self) -> ksni::Status {
if self.has_unread {
ksni::Status::NeedsAttention
} else {
ksni::Status::Active
}
}
fn attention_icon_name(&self) -> String {
self.icon_name()
}
fn tool_tip(&self) -> ksni::ToolTip {
let app_name = gettext("Fractal");
let description = if self.has_unread {
gettext("Unread messages")
} else {
app_name.clone()
};
ksni::ToolTip {
icon_name: self.icon_name(),
title: app_name,
description,
..Default::default()
}
}
fn activate(&mut self, _x: i32, _y: i32) {
info!("Tray icon activated");
let _ = self.tx.send(TrayCommand::Show);
}
fn menu(&self) -> Vec<MenuItem<Self>> {
let app_name = gettext("Fractal");
let show_label = gettext_f("Show {app_name}", &[("app_name", app_name.as_str())]);
let quit_label = gettext("Quit");
let tx_show = self.tx.clone();
let tx_quit = self.tx.clone();
vec![
StandardItem {
label: show_label.into(),
activate: Box::new(move |_| {
info!("Tray menu: Show clicked");
let _ = tx_show.send(TrayCommand::Show);
}),
..Default::default()
}
.into(),
MenuItem::Separator,
StandardItem {
label: quit_label.into(),
activate: Box::new(move |_| {
info!("Tray menu: Quit clicked");
let _ = tx_quit.send(TrayCommand::Quit);
}),
..Default::default()
}
.into(),
]
}
}
#[derive(Debug)]
pub struct TrayHandle {
rx: Option<mpsc::UnboundedReceiver<TrayCommand>>,
update_tx: mpsc::UnboundedSender<bool>,
}
impl TrayHandle {
pub fn take_receiver(&mut self) -> Option<mpsc::UnboundedReceiver<TrayCommand>> {
self.rx.take()
}
pub fn set_has_unread(&self, value: bool) {
info!("Tray set_has_unread: {}", value);
let _ = self.update_tx.send(value);
}
}
pub fn spawn_tray() -> TrayHandle {
let (tx, rx) = mpsc::unbounded_channel();
let (update_tx, mut update_rx) = mpsc::unbounded_channel::<bool>();
let tray = FractalTray::new(tx);
info!("Spawning system tray icon...");
RUNTIME.spawn(async move {
match tray.spawn().await {
Ok(handle) => {
info!("System tray icon created successfully");
while let Some(has_unread) = update_rx.recv().await {
info!("Updating tray icon, has_unread: {}", has_unread);
handle
.update(|tray| {
tray.has_unread = has_unread;
})
.await;
info!("Tray icon update complete");
}
}
Err(e) => {
warn!("Failed to create tray icon: {e}");
}
}
});
TrayHandle {
rx: Some(rx),
update_tx,
}
}

4
src/window.rs

@ -227,7 +227,9 @@ mod imp {
warn!("Could not save current session: {error}");
}
glib::Propagation::Proceed
// Hide to tray instead of closing.
self.obj().set_visible(false);
glib::Propagation::Stop
}
}

Loading…
Cancel
Save