This commit is contained in:
Julian Ospald 2017-07-10 21:07:59 +02:00
parent 3b943131df
commit 20e5b97842
No known key found for this signature in database
GPG Key ID: 511B62C09D50CD28
11 changed files with 127 additions and 76 deletions

View File

@ -24,7 +24,7 @@ serde_derive = "^1.0.9"
toml = "^0.4.2" toml = "^0.4.2"
which = "*" which = "*"
xdg = "*" xdg = "*"
libnotify = { path = "3rdparty/rust-libnotify" } libnotify = { path = "3rdparty/rust-libnotify", optional = true }
[dependencies.gtk] [dependencies.gtk]
git = "https://github.com/gtk-rs/gtk.git" git = "https://github.com/gtk-rs/gtk.git"
@ -45,3 +45,6 @@ codegen-units = 1 # controls whether the compiler passes `-C codegen-units`
# `codegen-units` is ignored when `lto = true` # `codegen-units` is ignored when `lto = true`
panic = 'unwind' # panic strategy (`-C panic=...`), can also be 'abort' panic = 'unwind' # panic strategy (`-C panic=...`), can also be 'abort'
[features]
notify = ["libnotify"]

View File

@ -262,17 +262,17 @@ extern "C" fn watch_cb(chan: *mut glib_sys::GIOChannel,
glib_sys::G_IO_STATUS_AGAIN => { glib_sys::G_IO_STATUS_AGAIN => {
debug!("G_IO_STATUS_AGAIN"); debug!("G_IO_STATUS_AGAIN");
continue; continue;
}, }
// TODO: handle these failure cases // TODO: handle these failure cases
glib_sys::G_IO_STATUS_NORMAL => { glib_sys::G_IO_STATUS_NORMAL => {
error!("Alsa failed to clear the channel"); error!("Alsa failed to clear the channel");
cb(AlsaEvent::AlsaCardError); cb(AlsaEvent::AlsaCardError);
}, }
glib_sys::G_IO_STATUS_ERROR => (), glib_sys::G_IO_STATUS_ERROR => (),
glib_sys::G_IO_STATUS_EOF => { glib_sys::G_IO_STATUS_EOF => {
error!("GIO error has occurred"); error!("GIO error has occurred");
cb(AlsaEvent::AlsaCardError); cb(AlsaEvent::AlsaCardError);
}, }
_ => warn!("Unknown status from g_io_channel_read_chars()"), _ => warn!("Unknown status from g_io_channel_read_chars()"),
} }
return true as glib_sys::gboolean; return true as glib_sys::gboolean;

View File

@ -1,12 +1,12 @@
use audio::Audio; use audio::Audio;
use errors::*; use errors::*;
use gtk; use gtk;
use notif;
use prefs::*; use prefs::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc;
use ui_entry::Gui; use ui_entry::Gui;
#[cfg(feature = "notify")]
use notif::*;
// TODO: notify popups // TODO: notify popups
@ -19,6 +19,8 @@ pub struct AppS {
pub gui: Gui, pub gui: Gui,
pub audio: Audio, pub audio: Audio,
pub prefs: RefCell<Prefs>, pub prefs: RefCell<Prefs>,
#[cfg(feature = "notify")]
pub notif: Notif,
} }
@ -42,14 +44,17 @@ impl AppS {
.device_prefs .device_prefs
.channel .channel
.clone(); .clone();
// TODO: better error handling
// let notif = Notif::new(&prefs.borrow()); #[cfg(feature = "notify")]
let notif = Notif::new(&prefs.borrow()).unwrap();
return AppS { return AppS {
_cant_construct: (), _cant_construct: (),
gui, gui,
audio: Audio::new(Some(card_name), Some(chan_name)).unwrap(), audio: Audio::new(Some(card_name), Some(chan_name)).unwrap(),
prefs, prefs,
#[cfg(feature = "notify")]
notif,
}; };
} }

View File

@ -141,8 +141,8 @@ impl Audio {
} }
// invoke_handlers(&self.handlers.borrow(), // invoke_handlers(&self.handlers.borrow(),
// AudioSignal::CardCleanedUp, // AudioSignal::CardCleanedUp,
// user); // user);
invoke_handlers(&self.handlers.borrow(), invoke_handlers(&self.handlers.borrow(),
AudioSignal::CardInitialized, AudioSignal::CardInitialized,
user); user);
@ -341,12 +341,12 @@ fn on_alsa_event(last_action_timestamp: &mut i64,
invoke_handlers(handlers, invoke_handlers(handlers,
self::AudioSignal::CardError, self::AudioSignal::CardError,
self::AudioUser::Unknown); self::AudioUser::Unknown);
}, }
AlsaEvent::AlsaCardDiconnected => { AlsaEvent::AlsaCardDiconnected => {
invoke_handlers(handlers, invoke_handlers(handlers,
self::AudioSignal::CardDisconnected, self::AudioSignal::CardDisconnected,
self::AudioUser::Unknown); self::AudioUser::Unknown);
}, }
AlsaEvent::AlsaCardValuesChanged => { AlsaEvent::AlsaCardValuesChanged => {
invoke_handlers(handlers, invoke_handlers(handlers,
self::AudioSignal::ValuesChanged, self::AudioSignal::ValuesChanged,

View File

@ -1,10 +1,11 @@
use alsa; use alsa;
use glib;
use libnotify;
use std::convert::From; use std::convert::From;
use std; use std;
use toml; use toml;
#[cfg(feature = "notify")]
use libnotify;
error_chain! { error_chain! {
@ -12,12 +13,13 @@ error_chain! {
Alsa(alsa::Error); Alsa(alsa::Error);
IO(std::io::Error); IO(std::io::Error);
Toml(toml::de::Error); Toml(toml::de::Error);
Libnotify(libnotify::errors::Error); Libnotify(libnotify::errors::Error) #[cfg(feature = "notify")];
} }
} }
#[macro_export] #[macro_export]
macro_rules! try_w { macro_rules! try_w {
($expr:expr) => { ($expr:expr) => {
@ -64,7 +66,7 @@ macro_rules! try_wr {
macro_rules! try_r { macro_rules! try_r {
($expr:expr, $ret:expr) => (match $expr { ($expr:expr, $ret:expr) => (match $expr {
::std::result::Result::Ok(val) => val, ::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => { ::std::result::Result::Err(_) => {
return $ret; return $ret;
}, },
}); });

View File

@ -29,6 +29,8 @@ extern crate gtk_sys;
extern crate libc; extern crate libc;
extern crate which; extern crate which;
extern crate xdg; extern crate xdg;
#[cfg(feature = "notify")]
extern crate libnotify; extern crate libnotify;
use std::rc::Rc; use std::rc::Rc;
@ -52,6 +54,8 @@ mod ui_popup_menu;
mod ui_popup_window; mod ui_popup_window;
mod ui_prefs_dialog; mod ui_prefs_dialog;
mod ui_tray_icon; mod ui_tray_icon;
#[cfg(feature = "notify")]
mod notif; mod notif;
@ -63,19 +67,10 @@ use app_state::*;
fn main() { fn main() {
gtk::init().unwrap(); gtk::init().unwrap();
// TODO: error handling
#[cfg(feature = "notify")]
libnotify::init("PNMixer-rs").unwrap(); libnotify::init("PNMixer-rs").unwrap();
{
// Create a new notification and show it
let n = libnotify::Notification::new("Summary", Some("Optional Body"), None)
.unwrap();
n.show().unwrap();
// You can also use the .show() convenience method on the context
n.update("I am another notification", None, None).unwrap();
n.show().unwrap();
// we are done, deinit
}
flexi_logger::LogOptions::new() flexi_logger::LogOptions::new()
.log_to_file(false) .log_to_file(false)
// ... your configuration options go here ... // ... your configuration options go here ...
@ -87,5 +82,7 @@ fn main() {
ui_entry::init(apps); ui_entry::init(apps);
gtk::main(); gtk::main();
#[cfg(feature = "notify")]
libnotify::uninit(); libnotify::uninit();
} }

View File

@ -30,7 +30,6 @@ pub struct Notif {
from_popup: Cell<bool>, from_popup: Cell<bool>,
from_tray: Cell<bool>, from_tray: Cell<bool>,
// TODO: from hotkey // TODO: from hotkey
from_external: Cell<bool>, from_external: Cell<bool>,
volume_notif: libnotify::Notification, volume_notif: libnotify::Notification,
@ -58,18 +57,16 @@ impl Notif {
let timeout = prefs.notify_prefs.notifcation_timeout; let timeout = prefs.notify_prefs.notifcation_timeout;
self.enabled.set(prefs.notify_prefs.enable_notifications); self.enabled.set(prefs.notify_prefs.enable_notifications);
self.enabled.set(prefs.notify_prefs.notify_popup); self.from_popup.set(prefs.notify_prefs.notify_popup);
self.enabled.set(prefs.notify_prefs.notify_mouse_scroll); self.from_tray.set(prefs.notify_prefs.notify_mouse_scroll);
self.enabled.set(prefs.notify_prefs.notify_external); self.from_external.set(prefs.notify_prefs.notify_external);
self.volume_notif self.volume_notif.set_notification_timeout(timeout as i32);
.set_notification_timeout(timeout as i32);
self.volume_notif self.volume_notif
.set_hint("x-canonical-private-synchronous", Some("".to_variant()))?; .set_hint("x-canonical-private-synchronous", Some("".to_variant()))?;
self.text_notif self.text_notif.set_notification_timeout(timeout as i32);
.set_notification_timeout(timeout as i32);
self.text_notif self.text_notif
.set_hint("x-canonical-private-synchronous", Some("".to_variant()))?; .set_hint("x-canonical-private-synchronous", Some("".to_variant()))?;
@ -95,17 +92,19 @@ impl Notif {
VolLevel::Muted => String::from("Volume muted"), VolLevel::Muted => String::from("Volume muted"),
_ => { _ => {
format!("{} ({})\nVolume: {}", format!("{} ({})\nVolume: {}",
audio.acard.borrow().card_name()?, audio.acard
audio.acard.borrow().chan_name()?, .borrow()
.card_name()?,
audio.acard
.borrow()
.chan_name()?,
vol) vol)
} }
} }
}; };
self.volume_notif self.volume_notif.update(summary.as_str(), None, Some(icon))?;
.update(summary.as_str(), None, Some(icon))?; self.volume_notif.set_hint("value", Some((vol as i32).to_variant()))?;
self.volume_notif
.set_hint("value", Some(vol.to_variant()))?;
self.volume_notif.show()?; self.volume_notif.show()?;
return Ok(()); return Ok(());
@ -113,8 +112,7 @@ impl Notif {
pub fn show_text_notif(&self, summary: &str, body: &str) -> Result<()> { pub fn show_text_notif(&self, summary: &str, body: &str) -> Result<()> {
self.text_notif self.text_notif.update(summary, Some(body), None)?;
.update(summary, Some(body), None)?;
self.text_notif.show()?; self.text_notif.show()?;
return Ok(()); return Ok(());
@ -124,25 +122,35 @@ impl Notif {
pub fn init_notify(appstate: Rc<AppS>) { pub fn init_notify(appstate: Rc<AppS>) {
debug!("Blah");
{ {
/* connect handler */ /* connect handler */
let apps = appstate.clone(); let apps = appstate.clone();
let notif = try_e!(Notif::new(&apps.prefs.borrow())); appstate.audio.connect_handler(Box::new(move |s, u| {
appstate.audio.connect_handler( let notif = &apps.notif;
Box::new(move |s, u| match (s, u) { if !notif.enabled.get() {
(AudioSignal::CardDisconnected, _) => (), return;
(AudioSignal::CardError, _) => (), }
(AudioSignal::ValuesChanged, AudioUser::TrayIcon) => { match (s,
try_w!(notif.show_volume_notif(&apps.audio)) u,
}, (notif.from_popup.get(),
notif.from_tray.get(),
notif.from_external.get())) {
(AudioSignal::CardDisconnected, _, _) => (),
(AudioSignal::CardError, _, _) => (),
(AudioSignal::ValuesChanged,
AudioUser::TrayIcon,
(_, true, _)) => try_w!(notif.show_volume_notif(&apps.audio)),
(AudioSignal::ValuesChanged,
AudioUser::Popup,
(true, _, _)) => try_w!(notif.show_volume_notif(&apps.audio)),
(AudioSignal::ValuesChanged,
AudioUser::Unknown,
(_, _, true)) => try_w!(notif.show_volume_notif(&apps.audio)),
_ => (), _ => (),
} }
) }));
);
} }
} }

View File

@ -140,6 +140,7 @@ impl Default for BehaviorPrefs {
} }
#[cfg(feature = "notify")]
#[derive(Deserialize, Debug, Serialize)] #[derive(Deserialize, Debug, Serialize)]
#[serde(default)] #[serde(default)]
pub struct NotifyPrefs { pub struct NotifyPrefs {
@ -151,6 +152,7 @@ pub struct NotifyPrefs {
// TODO: notify_hotkeys? // TODO: notify_hotkeys?
} }
#[cfg(feature = "notify")]
impl Default for NotifyPrefs { impl Default for NotifyPrefs {
fn default() -> NotifyPrefs { fn default() -> NotifyPrefs {
return NotifyPrefs { return NotifyPrefs {
@ -170,6 +172,7 @@ pub struct Prefs {
pub device_prefs: DevicePrefs, pub device_prefs: DevicePrefs,
pub view_prefs: ViewPrefs, pub view_prefs: ViewPrefs,
pub behavior_prefs: BehaviorPrefs, pub behavior_prefs: BehaviorPrefs,
#[cfg(feature = "notify")]
pub notify_prefs: NotifyPrefs, pub notify_prefs: NotifyPrefs,
// TODO: HotKeys? // TODO: HotKeys?
} }

View File

@ -44,12 +44,13 @@ pub fn pixbuf_new_from_theme(icon_name: &str,
pub fn pixbuf_new_from_file(filename: &str) -> Result<gdk_pixbuf::Pixbuf> { pub fn pixbuf_new_from_file(filename: &str) -> Result<gdk_pixbuf::Pixbuf> {
ensure!(!filename.is_empty(), "Filename is empty"); ensure!(!filename.is_empty(), "Filename is empty");
let mut syspath = String::new(); let mut syspath = String::new();
let sysdir = option_env!("PIXMAPSDIR").map(|s|{ let sysdir = option_env!("PIXMAPSDIR").map(|s| {
syspath = format!("{}/{}", s, filename); syspath = format!("{}/{}",
Path::new(syspath.as_str()) s,
}); filename);
let cargopath = format!("./data/pixmaps/{}", Path::new(syspath.as_str())
filename); });
let cargopath = format!("./data/pixmaps/{}", filename);
let cargodir = Path::new(cargopath.as_str()); let cargodir = Path::new(cargopath.as_str());
// prefer local dir // prefer local dir

View File

@ -5,7 +5,7 @@ use gtk::MessageDialogExt;
use gtk::WidgetExt; use gtk::WidgetExt;
use gtk::WindowExt; use gtk::WindowExt;
use gtk; use gtk;
use gtk_sys::{GTK_DIALOG_DESTROY_WITH_PARENT, GTK_RESPONSE_YES}; use gtk_sys::{GTK_RESPONSE_YES};
use prefs::*; use prefs::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@ -14,8 +14,9 @@ use ui_popup_menu::*;
use ui_popup_window::*; use ui_popup_window::*;
use ui_prefs_dialog::*; use ui_prefs_dialog::*;
use ui_tray_icon::*; use ui_tray_icon::*;
use notif::*;
#[cfg(feature = "notify")]
use notif::*;
@ -26,6 +27,7 @@ pub struct Gui {
pub popup_menu: PopupMenu, pub popup_menu: PopupMenu,
/* prefs_dialog is dynamically created and destroyed */ /* prefs_dialog is dynamically created and destroyed */
pub prefs_dialog: RefCell<Option<PrefsDialog>>, pub prefs_dialog: RefCell<Option<PrefsDialog>>,
} }
impl Gui { impl Gui {
@ -72,6 +74,8 @@ pub fn init(appstate: Rc<AppS>) {
init_popup_window(appstate.clone()); init_popup_window(appstate.clone());
init_popup_menu(appstate.clone()); init_popup_menu(appstate.clone());
init_prefs_callback(appstate.clone()); init_prefs_callback(appstate.clone());
#[cfg(feature = "notify")]
init_notify(appstate.clone()); init_notify(appstate.clone());
} }
@ -79,13 +83,11 @@ pub fn init(appstate: Rc<AppS>) {
fn run_audio_error_dialog(parent: &gtk::Window) -> i32 { fn run_audio_error_dialog(parent: &gtk::Window) -> i32 {
error!("Connection with audio failed, you probably need to restart pnmixer."); error!("Connection with audio failed, you probably need to restart pnmixer.");
let dialog = gtk::MessageDialog::new( let dialog = gtk::MessageDialog::new(Some(parent),
Some(parent), gtk::DIALOG_DESTROY_WITH_PARENT,
gtk::DIALOG_DESTROY_WITH_PARENT, gtk::MessageType::Error,
gtk::MessageType::Error, gtk::ButtonsType::YesNo,
gtk::ButtonsType::YesNo, "Warning: Connection to sound system failed.");
"Warning: Connection to sound system failed."
);
dialog.set_property_secondary_text(Some("Do you want to re-initialize the audio connection ? dialog.set_property_secondary_text(Some("Do you want to re-initialize the audio connection ?
If you do not, you will either need to restart PNMixer If you do not, you will either need to restart PNMixer

View File

@ -19,6 +19,7 @@ use support_audio::*;
pub struct PrefsDialog { pub struct PrefsDialog {
_cant_construct: (), _cant_construct: (),
prefs_dialog: gtk::Dialog, prefs_dialog: gtk::Dialog,
notebook: gtk::Notebook,
/* DevicePrefs */ /* DevicePrefs */
card_combo: gtk::ComboBoxText, card_combo: gtk::ComboBoxText,
@ -38,11 +39,16 @@ pub struct PrefsDialog {
custom_entry: gtk::Entry, custom_entry: gtk::Entry,
/* NotifyPrefs */ /* NotifyPrefs */
#[cfg(feature = "notify")]
noti_enable_check: gtk::CheckButton, noti_enable_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_timeout_spin: gtk::SpinButton, noti_timeout_spin: gtk::SpinButton,
// pub noti_hotkey_check: gtk::CheckButton, // pub noti_hotkey_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_mouse_check: gtk::CheckButton, noti_mouse_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_popup_check: gtk::CheckButton, noti_popup_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_ext_check: gtk::CheckButton, noti_ext_check: gtk::CheckButton,
} }
@ -54,6 +60,7 @@ impl PrefsDialog {
let prefs_dialog = PrefsDialog { let prefs_dialog = PrefsDialog {
_cant_construct: (), _cant_construct: (),
prefs_dialog: builder.get_object("prefs_dialog").unwrap(), prefs_dialog: builder.get_object("prefs_dialog").unwrap(),
notebook: builder.get_object("notebook").unwrap(),
card_combo: builder.get_object("card_combo").unwrap(), card_combo: builder.get_object("card_combo").unwrap(),
chan_combo: builder.get_object("chan_combo").unwrap(), chan_combo: builder.get_object("chan_combo").unwrap(),
@ -74,15 +81,26 @@ impl PrefsDialog {
.unwrap(), .unwrap(),
custom_entry: builder.get_object("custom_entry").unwrap(), custom_entry: builder.get_object("custom_entry").unwrap(),
#[cfg(feature = "notify")]
noti_enable_check: builder.get_object("noti_enable_check").unwrap(), noti_enable_check: builder.get_object("noti_enable_check").unwrap(),
#[cfg(feature = "notify")]
noti_timeout_spin: builder.get_object("noti_timeout_spin").unwrap(), noti_timeout_spin: builder.get_object("noti_timeout_spin").unwrap(),
// noti_hotkey_check: builder.get_object("noti_hotkey_check").unwrap(), // noti_hotkey_check: builder.get_object("noti_hotkey_check").unwrap(),
#[cfg(feature = "notify")]
noti_mouse_check: builder.get_object("noti_mouse_check").unwrap(), noti_mouse_check: builder.get_object("noti_mouse_check").unwrap(),
#[cfg(feature = "notify")]
noti_popup_check: builder.get_object("noti_popup_check").unwrap(), noti_popup_check: builder.get_object("noti_popup_check").unwrap(),
#[cfg(feature = "notify")]
noti_ext_check: builder.get_object("noti_ext_check").unwrap(), noti_ext_check: builder.get_object("noti_ext_check").unwrap(),
}; };
#[cfg(feature = "notify")]
let notify_tab: gtk::Box = builder.get_object("noti_vbox_enabled").unwrap();
#[cfg(not(feature = "notify"))]
let notify_tab: gtk::Box = builder.get_object("noti_vbox_disabled").unwrap();
prefs_dialog.notebook.append_page(&notify_tab,
Some(&gtk::Label::new(Some("Notifications"))));
return prefs_dialog; return prefs_dialog;
} }
@ -134,13 +152,18 @@ impl PrefsDialog {
.as_str()); .as_str());
/* NotifyPrefs */ /* NotifyPrefs */
#[cfg(feature = "notify")]
self.noti_enable_check self.noti_enable_check
.set_active(prefs.notify_prefs.enable_notifications); .set_active(prefs.notify_prefs.enable_notifications);
#[cfg(feature = "notify")]
self.noti_timeout_spin self.noti_timeout_spin
.set_value(prefs.notify_prefs.notifcation_timeout as f64); .set_value(prefs.notify_prefs.notifcation_timeout as f64);
#[cfg(feature = "notify")]
self.noti_mouse_check self.noti_mouse_check
.set_active(prefs.notify_prefs.notify_mouse_scroll); .set_active(prefs.notify_prefs.notify_mouse_scroll);
#[cfg(feature = "notify")]
self.noti_popup_check.set_active(prefs.notify_prefs.notify_popup); self.noti_popup_check.set_active(prefs.notify_prefs.notify_popup);
#[cfg(feature = "notify")]
self.noti_ext_check.set_active(prefs.notify_prefs.notify_external); self.noti_ext_check.set_active(prefs.notify_prefs.notify_external);
} }
@ -193,6 +216,7 @@ impl PrefsDialog {
custom_command, custom_command,
}; };
#[cfg(feature = "notify")]
let notify_prefs = NotifyPrefs { let notify_prefs = NotifyPrefs {
enable_notifications: self.noti_enable_check.get_active(), enable_notifications: self.noti_enable_check.get_active(),
notifcation_timeout: self.noti_timeout_spin.get_value_as_int() as notifcation_timeout: self.noti_timeout_spin.get_value_as_int() as
@ -206,6 +230,7 @@ impl PrefsDialog {
device_prefs, device_prefs,
view_prefs, view_prefs,
behavior_prefs, behavior_prefs,
#[cfg(feature = "notify")]
notify_prefs, notify_prefs,
}; };
@ -240,7 +265,10 @@ pub fn init_prefs_callback(appstate: Rc<AppS>) {
let apps = appstate.clone(); let apps = appstate.clone();
appstate.audio.connect_handler(Box::new(move |s, u| { appstate.audio.connect_handler(Box::new(move |s, u| {
/* skip if prefs window is not present */ /* skip if prefs window is not present */
if apps.gui.prefs_dialog.borrow().is_none() { if apps.gui
.prefs_dialog
.borrow()
.is_none() {
return; return;
} }
@ -249,7 +277,7 @@ pub fn init_prefs_callback(appstate: Rc<AppS>) {
(AudioSignal::CardCleanedUp, _) => { (AudioSignal::CardCleanedUp, _) => {
fill_card_combo(&apps); fill_card_combo(&apps);
fill_chan_combo(&apps, None); fill_chan_combo(&apps, None);
}, }
_ => (), _ => (),
} }
})); }));
@ -308,6 +336,8 @@ fn init_prefs_dialog(appstate: &Rc<AppS>) {
if response_id == ResponseType::Ok.into() || if response_id == ResponseType::Ok.into() ||
response_id == ResponseType::Apply.into() { response_id == ResponseType::Apply.into() {
// TODO: update popup, tray_icon, hotkeys, notification and audio // TODO: update popup, tray_icon, hotkeys, notification and audio
#[cfg(feature = "notify")]
try_w!(apps.notif.reload(&apps.prefs.borrow()));
try_w!(apps.update_tray_icon()); try_w!(apps.update_tray_icon());
try_w!(apps.update_popup_window()); try_w!(apps.update_popup_window());
{ {