Initial commit

This commit is contained in:
2017-06-26 09:08:37 +02:00
commit 62b052ff13
45 changed files with 24014 additions and 0 deletions

273
src/alsa_card.rs Normal file
View File

@@ -0,0 +1,273 @@
use alsa::card::Card;
use alsa::mixer::SelemChannelId::*;
use alsa::mixer::{Mixer, Selem, SelemId};
use alsa::poll::PollDescriptors;
use alsa_sys;
use errors::*;
use glib_sys;
use libc::c_uint;
use libc::pollfd;
use libc::size_t;
use std::cell::Cell;
use std::mem;
use std::ptr;
use std::rc::Rc;
use std::u8;
use support_alsa::*;
#[derive(Clone, Copy, Debug)]
pub enum AlsaEvent {
AlsaCardError,
AlsaCardDiconnected,
AlsaCardValuesChanged,
}
pub struct AlsaCard {
_cannot_construct: (),
pub card: Card,
pub mixer: Mixer,
pub selem_id: SelemId,
pub watch_ids: Cell<Vec<u32>>,
pub cb: Rc<Fn(AlsaEvent)>,
}
impl AlsaCard {
pub fn new(card_name: Option<String>,
elem_name: Option<String>,
cb: Rc<Fn(AlsaEvent)>)
-> Result<Box<AlsaCard>> {
let card = {
match card_name {
Some(name) => {
if name == "(default)" {
let default = get_default_alsa_card();
if alsa_card_has_playable_selem(&default) {
default
} else {
warn!("Default alsa card not playabla, trying others");
get_first_playable_alsa_card()?
}
} else {
let mycard = get_alsa_card_by_name(name.clone());
match mycard {
Ok(card) => card,
Err(_) => {
warn!("Card {} not playable, trying others",
name);
get_first_playable_alsa_card()?
}
}
}
}
None => get_first_playable_alsa_card()?,
}
};
let mixer = get_mixer(&card)?;
let selem_id = {
let requested_selem =
get_playable_selem_by_name(&mixer,
elem_name.unwrap_or(String::from("Master")));
match requested_selem {
Ok(s) => s.get_id(),
Err(_) => {
warn!("No playable Selem found, trying others");
get_first_playable_selem(&mixer)?.get_id()
}
}
};
let vec_pollfd = PollDescriptors::get(&mixer)?;
let acard = Box::new(AlsaCard {
_cannot_construct: (),
card: card,
mixer: mixer,
selem_id: selem_id,
watch_ids: Cell::new(vec![]),
cb: cb,
});
let watch_ids = AlsaCard::watch_poll_descriptors(vec_pollfd,
acard.as_ref());
acard.watch_ids.set(watch_ids);
return Ok(acard);
}
pub fn card_name(&self) -> Result<String> {
return self.card.get_name().from_err();
}
pub fn chan_name(&self) -> Result<String> {
let n = self.selem_id
.get_name()
.map(|y| String::from(y))?;
return Ok(n);
}
pub fn selem(&self) -> Selem {
return self.mixer.find_selem(&self.selem_id).unwrap();
}
pub fn get_vol(&self) -> Result<i64> {
let selem = self.selem();
let volume = selem.get_playback_volume(FrontRight);
return volume.from_err();
}
pub fn set_vol(&self, new_vol: i64) -> Result<()> {
let selem = self.selem();
return selem.set_playback_volume_all(new_vol).from_err();
}
pub fn get_volume_range(&self) -> (i64, i64) {
let selem = self.selem();
return selem.get_playback_volume_range();
}
pub fn has_mute(&self) -> bool {
let selem = self.selem();
return selem.has_playback_switch();
}
pub fn get_mute(&self) -> Result<bool> {
let selem = self.selem();
let val = selem.get_playback_switch(FrontRight)?;
return Ok(val == 0);
}
pub fn set_mute(&self, mute: bool) -> Result<()> {
let selem = self.selem();
/* true -> mute, false -> unmute */
let _ = selem.set_playback_switch_all(!mute as i32)?;
return Ok(());
}
fn watch_poll_descriptors(polls: Vec<pollfd>,
acard: &AlsaCard)
-> Vec<c_uint> {
let mut watch_ids: Vec<c_uint> = vec![];
let acard_ptr =
unsafe { mem::transmute::<&AlsaCard, glib_sys::gpointer>(acard) };
for poll in polls {
let gioc: *mut glib_sys::GIOChannel =
unsafe { glib_sys::g_io_channel_unix_new(poll.fd) };
let id = unsafe {
glib_sys::g_io_add_watch(
gioc,
glib_sys::GIOCondition::from_bits(
glib_sys::G_IO_IN.bits() | glib_sys::G_IO_ERR.bits(),
).unwrap(),
Some(watch_cb),
acard_ptr,
)
};
watch_ids.push(id);
unsafe { glib_sys::g_io_channel_unref(gioc) }
}
return watch_ids;
}
fn unwatch_poll_descriptors(watch_ids: &Vec<u32>) {
for watch_id in watch_ids {
unsafe {
glib_sys::g_source_remove(*watch_id);
}
}
}
}
impl Drop for AlsaCard {
// call Box::new(x), transmute the Box into a raw pointer, and then
// std::mem::forget
//
// if you unregister the callback, you should keep a raw pointer to the
// box
//
// For instance, `register` could return a raw pointer to the
// Box + a std::marker::PhantomData with the appropriate
// lifetime (if applicable)
//
// The struct could implement Drop, which unregisters the
// callback and frees the Box, by simply transmuting the
// raw pointer to a Box<T>
fn drop(&mut self) {
debug!("Destructing watch_ids: {:?}", self.watch_ids.get_mut());
AlsaCard::unwatch_poll_descriptors(&self.watch_ids.get_mut());
}
}
extern "C" fn watch_cb(chan: *mut glib_sys::GIOChannel,
cond: glib_sys::GIOCondition,
data: glib_sys::gpointer)
-> glib_sys::gboolean {
let acard =
unsafe { mem::transmute::<glib_sys::gpointer, &AlsaCard>(data) };
let cb = &acard.cb;
unsafe {
let mixer_ptr =
mem::transmute::<&Mixer, &*mut alsa_sys::snd_mixer_t>(&acard.mixer);
alsa_sys::snd_mixer_handle_events(*mixer_ptr);
};
if cond == glib_sys::G_IO_ERR {
return false as glib_sys::gboolean;
}
let mut sread: size_t = 1;
let mut buf: Vec<u8> = vec![0; 256];
while sread > 0 {
let stat: glib_sys::GIOStatus =
unsafe {
glib_sys::g_io_channel_read_chars(chan,
buf.as_mut_ptr() as *mut u8,
256,
&mut sread as *mut size_t,
ptr::null_mut())
};
match stat {
glib_sys::G_IO_STATUS_AGAIN => {
debug!("G_IO_STATUS_AGAIN");
continue;
}
glib_sys::G_IO_STATUS_NORMAL => {
error!("Alsa failed to clear the channel");
cb(AlsaEvent::AlsaCardError);
}
glib_sys::G_IO_STATUS_ERROR => (),
glib_sys::G_IO_STATUS_EOF => {
error!("GIO error has occurred");
cb(AlsaEvent::AlsaCardError);
}
_ => warn!("Unknown status from g_io_channel_read_chars()"),
}
return true as glib_sys::gboolean;
}
cb(AlsaEvent::AlsaCardValuesChanged);
return true as glib_sys::gboolean;
}

92
src/app_state.rs Normal file
View File

@@ -0,0 +1,92 @@
use audio::{Audio, AudioUser};
use errors::*;
use gtk;
use prefs::*;
use std::cell::RefCell;
use support_audio::*;
use ui_entry::Gui;
#[cfg(feature = "notify")]
use notif::*;
// TODO: destructors
pub struct AppS {
_cant_construct: (),
pub gui: Gui,
pub audio: Audio,
pub prefs: RefCell<Prefs>,
#[cfg(feature = "notify")]
pub notif: Notif,
}
impl AppS {
pub fn new() -> AppS {
let builder_popup_window =
gtk::Builder::new_from_string(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),
"/data/ui/popup-window.glade")));
let builder_popup_menu =
gtk::Builder::new_from_string(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),
"/data/ui/popup-menu.glade")));
let prefs = RefCell::new(Prefs::new().unwrap());
let gui =
Gui::new(builder_popup_window, builder_popup_menu, &prefs.borrow());
let card_name = prefs.borrow()
.device_prefs
.card
.clone();
let chan_name = prefs.borrow()
.device_prefs
.channel
.clone();
// TODO: better error handling
#[cfg(feature = "notify")]
let notif = Notif::new(&prefs.borrow()).unwrap();
return AppS {
_cant_construct: (),
gui,
audio: Audio::new(Some(card_name), Some(chan_name)).unwrap(),
prefs,
#[cfg(feature = "notify")]
notif,
};
}
/* some functions that need to be easily accessible */
pub fn update_tray_icon(&self) -> Result<()> {
debug!("Update tray icon!");
return self.gui.tray_icon.update_all(&self.prefs.borrow(),
&self.audio,
None);
}
pub fn update_popup_window(&self) -> Result<()> {
debug!("Update PopupWindow!");
return self.gui.popup_window.update(&self.audio);
}
#[cfg(feature = "notify")]
pub fn update_notify(&self) -> Result<()> {
return self.notif.reload(&self.prefs.borrow());
}
#[cfg(not(feature = "notify"))]
pub fn update_notify(&self) -> Result<()> {
return Ok(());
}
pub fn update_audio(&self, user: AudioUser) -> Result<()> {
return audio_reload(&self.audio, &self.prefs.borrow(), user);
}
pub fn update_config(&self) -> Result<()> {
let prefs = self.prefs.borrow_mut();
return prefs.store_config();
}
}

358
src/audio.rs Normal file
View File

@@ -0,0 +1,358 @@
use alsa_card::*;
use errors::*;
use glib;
use std::cell::Cell;
use std::cell::Ref;
use std::cell::RefCell;
use std::f64;
use std::rc::Rc;
use support_audio::*;
#[derive(Clone, Copy, Debug)]
pub enum VolLevel {
Muted,
Low,
Medium,
High,
Off,
}
#[derive(Clone, Copy, Debug)]
pub enum AudioUser {
Unknown,
Popup,
TrayIcon,
Hotkeys,
PrefsWindow,
}
#[derive(Clone, Copy, Debug)]
pub enum AudioSignal {
NoCard,
CardInitialized,
CardCleanedUp,
CardDisconnected,
CardError,
ValuesChanged,
}
#[derive(Clone)]
pub struct Handlers {
inner: Rc<RefCell<Vec<Box<Fn(AudioSignal, AudioUser)>>>>,
}
impl Handlers {
fn new() -> Handlers {
return Handlers { inner: Rc::new(RefCell::new(vec![])) };
}
fn borrow(&self) -> Ref<Vec<Box<Fn(AudioSignal, AudioUser)>>> {
return self.inner.borrow();
}
fn add_handler(&self, cb: Box<Fn(AudioSignal, AudioUser)>) {
self.inner.borrow_mut().push(cb);
}
}
pub struct Audio {
_cannot_construct: (),
pub acard: RefCell<Box<AlsaCard>>,
pub last_action_timestamp: Rc<RefCell<i64>>,
pub handlers: Handlers,
pub scroll_step: Cell<u32>,
}
impl Audio {
pub fn new(card_name: Option<String>,
elem_name: Option<String>)
-> Result<Audio> {
let handlers = Handlers::new();
let last_action_timestamp = Rc::new(RefCell::new(0));
let cb = {
let myhandler = handlers.clone();
let ts = last_action_timestamp.clone();
Rc::new(move |event| {
on_alsa_event(&mut *ts.borrow_mut(),
&myhandler.borrow(),
event)
})
};
let acard = AlsaCard::new(card_name, elem_name, cb);
/* additionally dispatch signals */
if acard.is_err() {
invoke_handlers(&handlers.borrow(),
AudioSignal::NoCard,
AudioUser::Unknown);
} else {
invoke_handlers(&handlers.borrow(),
AudioSignal::CardInitialized,
AudioUser::Unknown);
}
let audio = Audio {
_cannot_construct: (),
acard: RefCell::new(acard?),
last_action_timestamp: last_action_timestamp.clone(),
handlers: handlers.clone(),
scroll_step: Cell::new(5),
};
return Ok(audio);
}
pub fn switch_acard(&self,
card_name: Option<String>,
elem_name: Option<String>,
user: AudioUser)
-> Result<()> {
debug!("Switching cards");
debug!("Old card name: {}",
self.acard
.borrow()
.card_name()
.unwrap());
debug!("Old chan name: {}",
self.acard
.borrow()
.chan_name()
.unwrap());
let cb = self.acard
.borrow()
.cb
.clone();
{
let mut ac = self.acard.borrow_mut();
*ac = AlsaCard::new(card_name, elem_name, cb)?;
}
// invoke_handlers(&self.handlers.borrow(),
// AudioSignal::CardCleanedUp,
// user);
invoke_handlers(&self.handlers.borrow(),
AudioSignal::CardInitialized,
user);
return Ok(());
}
pub fn vol(&self) -> Result<f64> {
let alsa_vol = self.acard
.borrow()
.get_vol()?;
return vol_to_percent(alsa_vol, self.acard.borrow().get_volume_range());
}
pub fn vol_level(&self) -> VolLevel {
let muted = self.get_mute().unwrap_or(false);
if muted {
return VolLevel::Muted;
}
let cur_vol = try_r!(self.vol(), VolLevel::Muted);
match cur_vol {
0. => return VolLevel::Off,
0.0...33.0 => return VolLevel::Low,
0.0...66.0 => return VolLevel::Medium,
0.0...100.0 => return VolLevel::High,
_ => return VolLevel::Off,
}
}
pub fn set_vol(&self,
new_vol: f64,
user: AudioUser,
dir: VolDir,
auto_unmute: bool)
-> Result<()> {
{
let mut rc = self.last_action_timestamp.borrow_mut();
*rc = glib::get_monotonic_time();
}
let alsa_vol = percent_to_vol(new_vol,
self.acard.borrow().get_volume_range(),
dir)?;
/* only invoke handlers etc. if volume did actually change */
{
let old_alsa_vol =
percent_to_vol(self.vol()?,
self.acard.borrow().get_volume_range(),
dir)?;
if old_alsa_vol == alsa_vol {
return Ok(());
}
}
/* auto-unmute */
if auto_unmute && self.has_mute() && self.get_mute()? {
self.set_mute(false, user)?;
}
debug!("Setting vol on card {:?} and chan {:?} to {:?} by user {:?}",
self.acard
.borrow()
.card_name()
.unwrap(),
self.acard
.borrow()
.chan_name()
.unwrap(),
new_vol,
user);
self.acard
.borrow()
.set_vol(alsa_vol)?;
invoke_handlers(&self.handlers.borrow(),
AudioSignal::ValuesChanged,
user);
return Ok(());
}
pub fn increase_vol(&self,
user: AudioUser,
auto_unmute: bool)
-> Result<()> {
let old_vol = self.vol()?;
let new_vol = old_vol + (self.scroll_step.get() as f64);
return self.set_vol(new_vol, user, VolDir::Up, auto_unmute);
}
pub fn decrease_vol(&self,
user: AudioUser,
auto_unmute: bool)
-> Result<()> {
let old_vol = self.vol()?;
let new_vol = old_vol - (self.scroll_step.get() as f64);
return self.set_vol(new_vol, user, VolDir::Down, auto_unmute);
}
pub fn has_mute(&self) -> bool {
return self.acard.borrow().has_mute();
}
pub fn get_mute(&self) -> Result<bool> {
return self.acard.borrow().get_mute();
}
pub fn set_mute(&self, mute: bool, user: AudioUser) -> Result<()> {
let mut rc = self.last_action_timestamp.borrow_mut();
*rc = glib::get_monotonic_time();
debug!("Setting mute to {} on card {:?} and chan {:?} by user {:?}",
mute,
self.acard
.borrow()
.card_name()
.unwrap(),
self.acard
.borrow()
.chan_name()
.unwrap(),
user);
self.acard
.borrow()
.set_mute(mute)?;
invoke_handlers(&self.handlers.borrow(),
AudioSignal::ValuesChanged,
user);
return Ok(());
}
pub fn toggle_mute(&self, user: AudioUser) -> Result<()> {
let muted = self.get_mute()?;
return self.set_mute(!muted, user);
}
pub fn connect_handler(&self, cb: Box<Fn(AudioSignal, AudioUser)>) {
self.handlers.add_handler(cb);
}
}
fn invoke_handlers(handlers: &Vec<Box<Fn(AudioSignal, AudioUser)>>,
signal: AudioSignal,
user: AudioUser) {
debug!("Invoking handlers for signal {:?} by user {:?}",
signal,
user);
if handlers.is_empty() {
debug!("No handler found");
} else {
debug!("Executing handlers")
}
for handler in handlers {
let unboxed = handler.as_ref();
unboxed(signal, user);
}
}
fn on_alsa_event(last_action_timestamp: &mut i64,
handlers: &Vec<Box<Fn(AudioSignal, AudioUser)>>,
alsa_event: AlsaEvent) {
let last: i64 = *last_action_timestamp;
if last != 0 {
let now: i64 = glib::get_monotonic_time();
let delay: i64 = now - last;
if delay < 1000000 {
return;
}
debug!("Discarding last time stamp, too old");
*last_action_timestamp = 0;
}
/* external change */
match alsa_event {
AlsaEvent::AlsaCardError => {
invoke_handlers(handlers,
self::AudioSignal::CardError,
self::AudioUser::Unknown);
}
AlsaEvent::AlsaCardDiconnected => {
invoke_handlers(handlers,
self::AudioSignal::CardDisconnected,
self::AudioUser::Unknown);
}
AlsaEvent::AlsaCardValuesChanged => {
invoke_handlers(handlers,
self::AudioSignal::ValuesChanged,
self::AudioUser::Unknown);
}
e => warn!("Unhandled alsa event: {:?}", e),
}
}

113
src/errors.rs Normal file
View File

@@ -0,0 +1,113 @@
use alsa;
use glib;
use std::convert::From;
use std;
use toml;
error_chain! {
foreign_links {
Alsa(alsa::Error);
IO(std::io::Error);
Toml(toml::de::Error);
}
}
#[macro_export]
macro_rules! try_w {
($expr:expr) => {
try_wr!($expr, ())
};
($expr:expr, $fmt:expr, $($arg:tt)+) => {
try_wr!($expr, (), $fmt, $(arg)+)
};
($expr:expr, $fmt:expr) => {
try_wr!($expr, (), $fmt)
}
}
#[macro_export]
macro_rules! try_wr {
($expr:expr, $ret:expr) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => {
warn!("{:?}", err);
return $ret;
},
});
($expr:expr, $ret:expr, $fmt:expr) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => {
warn!("Original error: {:?}", err);
warn!($fmt);
return $ret;
},
});
($expr:expr, $ret:expr, $fmt:expr, $($arg:tt)+) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => {
warn!("Original error: {:?}", err);
warn!(format!($fmt, $(arg)+));
return $ret;
},
})
}
#[macro_export]
macro_rules! try_r {
($expr:expr, $ret:expr) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(_) => {
return $ret;
},
});
}
#[macro_export]
macro_rules! try_e {
($expr:expr) => {
try_er!($expr, ())
};
($expr:expr, $fmt:expr, $($arg:tt)+) => {
try_er!($expr, (), $fmt, $(arg)+)
};
($expr:expr, $fmt:expr) => {
try_er!($expr, (), $fmt)
}
}
#[macro_export]
macro_rules! try_er {
($expr:expr, $ret:expr) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => {
error!("{:?}", err);
::std::process::exit(1);
},
});
($expr:expr, $ret:expr, $fmt:expr) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => {
error!("Original error: {:?}", err);
error!($fmt);
std::process::exit(1);
},
});
($expr:expr, $ret:expr, $fmt:expr, $($arg:tt)+) => (match $expr {
::std::result::Result::Ok(val) => val,
::std::result::Result::Err(err) => {
error!("Original error: {:?}", err);
error!(format!($fmt, $(arg)+));
std::process::exit(1);
},
})
}

20
src/glade_helpers.rs Normal file
View File

@@ -0,0 +1,20 @@
#[macro_export]
macro_rules! create_builder_item {
($sname:ident, $($element: ident: $ty: ty),+) => {
pub struct $sname {
$(
pub $element: $ty
),+
}
impl $sname {
pub fn new(builder: gtk::Builder) -> $sname {
return $sname {
$(
$element: builder.get_object(stringify!($element)).unwrap()
),+
};
}
}
}
}

91
src/main.rs Normal file
View File

@@ -0,0 +1,91 @@
#![feature(alloc_system)]
extern crate alloc_system;
extern crate flexi_logger;
#[macro_use]
extern crate log;
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate serde_derive;
extern crate toml;
extern crate serde;
extern crate alsa;
extern crate alsa_sys;
extern crate ffi;
extern crate gdk;
extern crate gdk_pixbuf;
extern crate gdk_pixbuf_sys;
extern crate gdk_sys;
extern crate gio;
extern crate glib;
extern crate glib_sys;
extern crate gobject_sys;
extern crate gtk;
extern crate gtk_sys;
extern crate libc;
extern crate which;
extern crate xdg;
#[cfg(feature = "notify")]
extern crate libnotify;
use std::rc::Rc;
#[macro_use]
mod errors;
#[macro_use]
mod glade_helpers;
mod alsa_card;
mod app_state;
mod audio;
mod prefs;
mod support_alsa;
mod support_audio;
mod support_cmd;
#[macro_use]
mod support_ui;
mod ui_entry;
mod ui_popup_menu;
mod ui_popup_window;
mod ui_prefs_dialog;
mod ui_tray_icon;
#[cfg(feature = "notify")]
mod notif;
use app_state::*;
#[cfg(feature = "notify")]
use libnotify::*;
fn main() {
gtk::init().unwrap();
// TODO: error handling
#[cfg(feature = "notify")]
init("PNMixer-rs").unwrap();
flexi_logger::LogOptions::new()
.log_to_file(false)
// ... your configuration options go here ...
.init(Some("pnmixer=debug".to_string()))
.unwrap_or_else(|e| panic!("Logger initialization failed with {}", e));
let apps = Rc::new(AppS::new());
ui_entry::init(apps);
gtk::main();
#[cfg(feature = "notify")]
uninit();
}

163
src/notif.rs Normal file
View File

@@ -0,0 +1,163 @@
use app_state::*;
use audio::*;
use errors::*;
use glib::Variant;
use glib::prelude::*;
use gtk::DialogExt;
use gtk::MessageDialogExt;
use gtk::WidgetExt;
use gtk::WindowExt;
use gtk;
use gtk_sys::{GTK_DIALOG_DESTROY_WITH_PARENT, GTK_RESPONSE_YES};
use libnotify;
use prefs::*;
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
use std::thread;
use std::time::Duration;
use support_audio::*;
use support_ui::*;
use ui_popup_menu::*;
use ui_popup_window::*;
use ui_prefs_dialog::*;
use ui_tray_icon::*;
pub struct Notif {
enabled: Cell<bool>,
from_popup: Cell<bool>,
from_tray: Cell<bool>,
// TODO: from hotkey
from_external: Cell<bool>,
volume_notif: libnotify::Notification,
text_notif: libnotify::Notification,
}
impl Notif {
pub fn new(prefs: &Prefs) -> Result<Self> {
let notif = Notif {
enabled: Cell::new(false),
from_popup: Cell::new(false),
from_tray: Cell::new(false),
from_external: Cell::new(false),
volume_notif: libnotify::Notification::new("", None, None),
text_notif: libnotify::Notification::new("", None, None),
};
notif.reload(prefs)?;
return Ok(notif);
}
pub fn reload(&self, prefs: &Prefs) -> Result<()> {
let timeout = prefs.notify_prefs.notifcation_timeout;
self.enabled.set(prefs.notify_prefs.enable_notifications);
self.from_popup.set(prefs.notify_prefs.notify_popup);
self.from_tray.set(prefs.notify_prefs.notify_mouse_scroll);
self.from_external.set(prefs.notify_prefs.notify_external);
self.volume_notif.set_timeout(timeout as i32);
self.volume_notif.set_hint("x-canonical-private-synchronous",
Some("".to_variant()));
self.text_notif.set_timeout(timeout as i32);
self.text_notif.set_hint("x-canonical-private-synchronous",
Some("".to_variant()));
return Ok(());
}
pub fn show_volume_notif(&self, audio: &Audio) -> Result<()> {
let vol = audio.vol()?;
let vol_level = audio.vol_level();
let icon = {
match vol_level {
VolLevel::Muted => "audio-volume-muted",
VolLevel::Off => "audio-volume-off",
VolLevel::Low => "audio-volume-low",
VolLevel::Medium => "audio-volume-medium",
VolLevel::High => "audio-volume-high",
}
};
let summary = {
match vol_level {
VolLevel::Muted => String::from("Volume muted"),
_ => {
format!("{} ({})\nVolume: {}",
audio.acard
.borrow()
.card_name()?,
audio.acard
.borrow()
.chan_name()?,
vol)
}
}
};
// TODO: error handling
self.volume_notif.update(summary.as_str(), None, Some(icon)).unwrap();
self.volume_notif.set_hint("value", Some((vol as i32).to_variant()));
// TODO: error handling
self.volume_notif.show().unwrap();
return Ok(());
}
pub fn show_text_notif(&self, summary: &str, body: &str) -> Result<()> {
// TODO: error handling
self.text_notif.update(summary, Some(body), None).unwrap();
// TODO: error handling
self.text_notif.show().unwrap();
return Ok(());
}
}
pub fn init_notify(appstate: Rc<AppS>) {
debug!("Blah");
{
/* connect handler */
let apps = appstate.clone();
appstate.audio.connect_handler(Box::new(move |s, u| {
let notif = &apps.notif;
if !notif.enabled.get() {
return;
}
match (s,
u,
(notif.from_popup.get(),
notif.from_tray.get(),
notif.from_external.get())) {
(AudioSignal::NoCard, _, _) => try_w!(notif.show_text_notif("No sound card", "No playable soundcard found")),
(AudioSignal::CardDisconnected, _, _) => try_w!(notif.show_text_notif("Soundcard disconnected", "Soundcard has been disconnected, reloading sound system...")),
(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)),
// TODO hotkeys
_ => (),
}
}));
}
}

266
src/prefs.rs Normal file
View File

@@ -0,0 +1,266 @@
use errors::*;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fs::File;
use std::io::prelude::*;
use std;
use toml;
use which;
use xdg;
const VOL_CONTROL_COMMANDS: [&str; 3] =
["gnome-alsamixer", "xfce4-mixer", "alsamixergui"];
#[derive(Deserialize, Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum MiddleClickAction {
ToggleMute,
ShowPreferences,
VolumeControl,
CustomCommand,
}
impl Default for MiddleClickAction {
fn default() -> MiddleClickAction {
return MiddleClickAction::ToggleMute;
}
}
impl From<i32> for MiddleClickAction {
fn from(i: i32) -> Self {
match i {
0 => MiddleClickAction::ToggleMute,
1 => MiddleClickAction::ShowPreferences,
2 => MiddleClickAction::VolumeControl,
3 => MiddleClickAction::CustomCommand,
_ => MiddleClickAction::ToggleMute,
}
}
}
impl From<MiddleClickAction> for i32 {
fn from(action: MiddleClickAction) -> Self {
match action {
MiddleClickAction::ToggleMute => 0,
MiddleClickAction::ShowPreferences => 1,
MiddleClickAction::VolumeControl => 2,
MiddleClickAction::CustomCommand => 3,
}
}
}
#[derive(Deserialize, Debug, Serialize)]
#[serde(default)]
pub struct DevicePrefs {
pub card: String,
pub channel: String,
// TODO: normalize volume?
}
impl Default for DevicePrefs {
fn default() -> DevicePrefs {
return DevicePrefs {
card: String::from("(default)"),
channel: String::from("Master"),
};
}
}
#[derive(Deserialize, Debug, Serialize)]
#[serde(default)]
pub struct ViewPrefs {
pub draw_vol_meter: bool,
pub vol_meter_offset: i32,
pub system_theme: bool,
pub vol_meter_color: VolColor,
// TODO: Display text folume/text volume pos?
}
impl Default for ViewPrefs {
fn default() -> ViewPrefs {
return ViewPrefs {
draw_vol_meter: true,
vol_meter_offset: 10,
system_theme: true,
vol_meter_color: VolColor::default(),
};
}
}
#[derive(Deserialize, Debug, Serialize)]
#[serde(default)]
pub struct VolColor {
pub red: f64,
pub green: f64,
pub blue: f64,
}
impl Default for VolColor {
fn default() -> VolColor {
return VolColor {
red: 0.960784313725,
green: 0.705882352941,
blue: 0.0,
};
}
}
#[derive(Deserialize, Debug, Serialize)]
#[serde(default)]
pub struct BehaviorPrefs {
pub unmute_on_vol_change: bool,
pub vol_control_cmd: Option<String>,
pub vol_scroll_step: f64,
pub vol_fine_scroll_step: f64,
pub middle_click_action: MiddleClickAction,
pub custom_command: Option<String>, // TODO: fine scroll step?
}
impl Default for BehaviorPrefs {
fn default() -> BehaviorPrefs {
return BehaviorPrefs {
unmute_on_vol_change: true,
vol_control_cmd: None,
vol_scroll_step: 5.0,
vol_fine_scroll_step: 1.0,
middle_click_action: MiddleClickAction::default(),
custom_command: None,
};
}
}
#[cfg(feature = "notify")]
#[derive(Deserialize, Debug, Serialize)]
#[serde(default)]
pub struct NotifyPrefs {
pub enable_notifications: bool,
pub notifcation_timeout: i64,
pub notify_mouse_scroll: bool,
pub notify_popup: bool,
pub notify_external: bool,
// TODO: notify_hotkeys?
}
#[cfg(feature = "notify")]
impl Default for NotifyPrefs {
fn default() -> NotifyPrefs {
return NotifyPrefs {
enable_notifications: true,
notifcation_timeout: 1500,
notify_mouse_scroll: true,
notify_popup: true,
notify_external: true,
};
}
}
#[derive(Deserialize, Debug, Serialize, Default)]
#[serde(default)]
pub struct Prefs {
pub device_prefs: DevicePrefs,
pub view_prefs: ViewPrefs,
pub behavior_prefs: BehaviorPrefs,
#[cfg(feature = "notify")]
pub notify_prefs: NotifyPrefs,
// TODO: HotKeys?
}
impl Prefs {
pub fn new() -> Result<Prefs> {
let m_config_file = get_xdg_dirs().find_config_file("pnmixer.toml");
match m_config_file {
Some(c) => {
debug!("Config file present at {:?}, using it.", c);
let mut f = File::open(c)?;
let mut buffer = vec![];
f.read_to_end(&mut buffer)?;
let prefs = toml::from_slice(buffer.as_slice())?;
return Ok(prefs);
}
None => {
debug!("No config file present, creating one with defaults.");
let prefs = Prefs::default();
prefs.store_config()?;
return Ok(prefs);
}
}
}
pub fn reload_config(&mut self) -> Result<()> {
debug!("Reloading config...");
let new_prefs = Prefs::new()?;
*self = new_prefs;
return Ok(());
}
pub fn store_config(&self) -> Result<()> {
let config_path = get_xdg_dirs().place_config_file("pnmixer.toml")
.from_err()?;
debug!("Storing config in {:?}", config_path);
let mut f = File::create(config_path)?;
f.write_all(self.to_str().as_bytes())?;
return Ok(());
}
pub fn to_str(&self) -> String {
return toml::to_string(self).unwrap();
}
pub fn get_avail_vol_control_cmd(&self) -> Option<String> {
match self.behavior_prefs.vol_control_cmd {
Some(ref c) => return Some(c.clone()),
None => {
for command in VOL_CONTROL_COMMANDS.iter() {
if which::which(command).is_ok() {
return Some(String::from(*command));
}
}
}
}
return None;
}
}
impl Display for Prefs {
fn fmt(&self,
f: &mut Formatter)
-> std::result::Result<(), std::fmt::Error> {
let s = self.to_str();
return write!(f, "{}", s);
}
}
fn get_xdg_dirs() -> xdg::BaseDirectories {
return xdg::BaseDirectories::with_prefix("pnmixer-rs").unwrap();
}

148
src/support_alsa.rs Normal file
View File

@@ -0,0 +1,148 @@
use alsa::card::Card;
use alsa::mixer::{Mixer, Selem, SelemId, Elem};
use alsa;
use errors::*;
use libc::c_int;
use std::iter::Map;
use std::iter::Filter;
pub fn get_default_alsa_card() -> Card {
return get_alsa_card_by_id(0);
}
pub fn get_alsa_card_by_id(index: c_int) -> Card {
return Card::new(index);
}
pub fn get_alsa_cards() -> alsa::card::Iter {
return alsa::card::Iter::new();
}
pub fn get_first_playable_alsa_card() -> Result<Card> {
for m_card in get_alsa_cards() {
match m_card {
Ok(card) => {
if alsa_card_has_playable_selem(&card) {
return Ok(card);
}
}
_ => (),
}
}
bail!("No playable alsa card found!")
}
pub fn get_playable_alsa_card_names() -> Vec<String> {
let mut vec = vec![];
for m_card in get_alsa_cards() {
match m_card {
Ok(card) => {
if alsa_card_has_playable_selem(&card) {
let m_name = card.get_name();
if m_name.is_ok() {
vec.push(m_name.unwrap())
}
}
}
_ => (),
}
}
return vec;
}
pub fn get_alsa_card_by_name(name: String) -> Result<Card> {
for r_card in get_alsa_cards() {
let card = r_card?;
let card_name = card.get_name()?;
if name == card_name {
return Ok(card);
}
}
bail!("Not found a matching card named {}", name);
}
pub fn alsa_card_has_playable_selem(card: &Card) -> bool {
let mixer = try_wr!(get_mixer(&card), false);
for selem in get_playable_selems(&mixer) {
if selem_is_playable(&selem) {
return true;
}
}
return false;
}
pub fn get_mixer(card: &Card) -> Result<Mixer> {
return Mixer::new(&format!("hw:{}", card.get_index()), false).from_err();
}
pub fn get_selem(elem: Elem) -> Selem {
/* in the ALSA API, there are currently only simple elements,
* so this unwrap() should be safe.
*http://www.alsa-project.org/alsa-doc/alsa-lib/group___mixer.html#enum-members */
return Selem::new(elem).unwrap();
}
pub fn get_playable_selems(mixer: &Mixer) -> Vec<Selem> {
let mut v = vec![];
for s in mixer.iter().map(get_selem).filter(selem_is_playable) {
v.push(s);
}
return v;
}
pub fn get_first_playable_selem(mixer: &Mixer) -> Result<Selem> {
for s in mixer.iter().map(get_selem).filter(selem_is_playable) {
return Ok(s);
}
bail!("No playable Selem found!")
}
pub fn get_playable_selem_names(mixer: &Mixer) -> Vec<String> {
let mut vec = vec![];
for selem in get_playable_selems(mixer) {
let n = selem.get_id().get_name().map(|y| String::from(y));
match n {
Ok(name) => vec.push(name),
_ => (),
}
}
return vec;
}
pub fn get_playable_selem_by_name(mixer: &Mixer,
name: String)
-> Result<Selem> {
for selem in get_playable_selems(mixer) {
let n = selem.get_id()
.get_name()
.map(|y| String::from(y))?;
if n == name {
return Ok(selem);
}
}
bail!("Not found a matching playable selem named {}", name);
}
pub fn selem_is_playable(selem: &Selem) -> bool {
return selem.has_playback_volume();
}

64
src/support_audio.rs Normal file
View File

@@ -0,0 +1,64 @@
use audio::{Audio, AudioUser};
use errors::*;
use prefs::*;
#[derive(Clone, Copy, Debug)]
pub enum VolDir {
Up,
Down,
Unknown,
}
pub fn vol_change_to_voldir(old: f64, new: f64) -> VolDir {
if old < new {
return VolDir::Up;
} else if old > new {
return VolDir::Down;
} else {
return VolDir::Unknown;
}
}
pub fn lrint(v: f64, dir: VolDir) -> f64 {
match dir {
VolDir::Up => v.ceil(),
VolDir::Down => v.floor(),
_ => v,
}
}
pub fn audio_reload(audio: &Audio,
prefs: &Prefs,
user: AudioUser)
-> Result<()> {
let card = &prefs.device_prefs.card;
let channel = &prefs.device_prefs.channel;
// TODO: is this clone safe?
return audio.switch_acard(Some(card.clone()), Some(channel.clone()), user);
}
pub fn vol_to_percent(vol: i64, range: (i64, i64)) -> Result<f64> {
let (min, max) = range;
ensure!(min < max,
"Invalid playback volume range [{} - {}]",
min,
max);
let perc = ((vol - min) as f64) / ((max - min) as f64) * 100.0;
return Ok(perc);
}
pub fn percent_to_vol(vol: f64, range: (i64, i64), dir: VolDir) -> Result<i64> {
let (min, max) = range;
ensure!(min < max,
"Invalid playback volume range [{} - {}]",
min,
max);
let _v = lrint(vol / 100.0 * ((max - min) as f64), dir) + (min as f64);
return Ok(_v as i64);
}

26
src/support_cmd.rs Normal file
View File

@@ -0,0 +1,26 @@
use errors::*;
use glib;
use prefs::Prefs;
use std::error::Error;
use std;
pub fn execute_vol_control_command(prefs: &Prefs) -> Result<()> {
let m_cmd = prefs.get_avail_vol_control_cmd();
match m_cmd {
Some(ref cmd) => execute_command(cmd.as_str()),
None => bail!("No command found"),
}
}
pub fn execute_command(cmd: &str) -> Result<()> {
return glib::spawn_command_line_async(cmd)
.map_err(|e| {
std::io::Error::new(std::io::ErrorKind::Other,
e.description())
})
.from_err();
}

90
src/support_ui.rs Normal file
View File

@@ -0,0 +1,90 @@
use errors::*;
use gdk_pixbuf;
use gdk_pixbuf_sys;
use glib::translate::FromGlibPtrFull;
use glib::translate::ToGlibPtr;
use gtk::prelude::*;
use gtk;
use std::path::*;
pub fn copy_pixbuf(pixbuf: &gdk_pixbuf::Pixbuf) -> gdk_pixbuf::Pixbuf {
let new_pixbuf = unsafe {
let gdk_pixbuf = pixbuf.to_glib_full();
let copy = gdk_pixbuf_sys::gdk_pixbuf_copy(gdk_pixbuf);
FromGlibPtrFull::from_glib_full(copy)
};
return new_pixbuf;
}
pub fn pixbuf_new_from_theme(icon_name: &str,
size: i32,
theme: &gtk::IconTheme)
-> Result<gdk_pixbuf::Pixbuf> {
let icon_info =
theme.lookup_icon(icon_name, size, gtk::IconLookupFlags::empty())
.ok_or(format!("Couldn't find icon {}", icon_name))?;
debug!("Loading stock icon {} from {:?}",
icon_name,
icon_info.get_filename().unwrap_or(PathBuf::new()));
// TODO: propagate error
let pixbuf = icon_info.load_icon().unwrap();
return Ok(pixbuf);
}
pub fn pixbuf_new_from_file(filename: &str) -> Result<gdk_pixbuf::Pixbuf> {
ensure!(!filename.is_empty(), "Filename is empty");
let mut syspath = String::new();
let sysdir = option_env!("PIXMAPSDIR").map(|s| {
syspath = format!("{}/{}",
s,
filename);
Path::new(syspath.as_str())
});
let cargopath = format!("./data/pixmaps/{}", filename);
let cargodir = Path::new(cargopath.as_str());
// prefer local dir
let final_dir = {
if cargodir.exists() {
cargodir
} else if sysdir.is_some() && sysdir.unwrap().exists() {
sysdir.unwrap()
} else {
bail!("No valid path found")
}
};
let str_path = final_dir.to_str().ok_or("Path is not valid unicode")?;
debug!("Loading icon from {}", str_path);
// TODO: propagate error
return Ok(gdk_pixbuf::Pixbuf::new_from_file(str_path).unwrap());
}
#[macro_export]
macro_rules! pixbuf_new_from_xpm {
($name:ident) => {
{
use glib::translate::from_glib_full;
use libc::c_char;
extern "C" { fn $name() -> *mut *mut c_char; };
unsafe {
from_glib_full(
gdk_pixbuf_sys::gdk_pixbuf_new_from_xpm_data($name()))
}
}
}
}

102
src/ui_entry.rs Normal file
View File

@@ -0,0 +1,102 @@
use app_state::*;
use audio::{AudioUser, AudioSignal};
use gtk::DialogExt;
use gtk::MessageDialogExt;
use gtk::WidgetExt;
use gtk::WindowExt;
use gtk;
use gtk_sys::GTK_RESPONSE_YES;
use prefs::*;
use std::cell::RefCell;
use std::rc::Rc;
use support_audio::*;
use ui_popup_menu::*;
use ui_popup_window::*;
use ui_prefs_dialog::*;
use ui_tray_icon::*;
#[cfg(feature = "notify")]
use notif::*;
pub struct Gui {
_cant_construct: (),
pub tray_icon: TrayIcon,
pub popup_window: PopupWindow,
pub popup_menu: PopupMenu,
/* prefs_dialog is dynamically created and destroyed */
pub prefs_dialog: RefCell<Option<PrefsDialog>>,
}
impl Gui {
pub fn new(builder_popup_window: gtk::Builder,
builder_popup_menu: gtk::Builder,
prefs: &Prefs)
-> Gui {
return Gui {
_cant_construct: (),
tray_icon: TrayIcon::new(prefs).unwrap(),
popup_window: PopupWindow::new(builder_popup_window),
popup_menu: PopupMenu::new(builder_popup_menu),
prefs_dialog: RefCell::new(None),
};
}
}
pub fn init(appstate: Rc<AppS>) {
{
/* "global" audio signal handler */
let apps = appstate.clone();
appstate.audio.connect_handler(
Box::new(move |s, u| match (s, u) {
(AudioSignal::CardDisconnected, _) => {
try_w!(audio_reload(&apps.audio,
&apps.prefs.borrow(),
AudioUser::Unknown));
},
(AudioSignal::CardError, _) => {
if run_audio_error_dialog(&apps.gui.popup_menu.menu_window) == (GTK_RESPONSE_YES as i32) {
try_w!(audio_reload(&apps.audio,
&apps.prefs.borrow(),
AudioUser::Unknown));
}
},
_ => (),
}
));
}
init_tray_icon(appstate.clone());
init_popup_window(appstate.clone());
init_popup_menu(appstate.clone());
init_prefs_callback(appstate.clone());
#[cfg(feature = "notify")]
init_notify(appstate.clone());
}
fn run_audio_error_dialog(parent: &gtk::Window) -> i32 {
error!("Connection with audio failed, you probably need to restart pnmixer.");
let dialog = gtk::MessageDialog::new(Some(parent),
gtk::DIALOG_DESTROY_WITH_PARENT,
gtk::MessageType::Error,
gtk::ButtonsType::YesNo,
"Warning: Connection to sound system failed.");
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
or select the 'Reload Audio' option in the right-click
menu in order for PNMixer to function."));
dialog.set_title("PNMixer-rs Error");
let resp = dialog.run();
dialog.destroy();
return resp;
}

177
src/ui_popup_menu.rs Normal file
View File

@@ -0,0 +1,177 @@
use app_state::*;
use audio::{AudioUser, AudioSignal};
use gtk::prelude::*;
use gtk;
use std::rc::Rc;
use support_audio::*;
use support_cmd::*;
use ui_prefs_dialog::*;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
create_builder_item!(PopupMenu,
menu_window: gtk::Window,
menubar: gtk::MenuBar,
menu: gtk::Menu,
about_item: gtk::MenuItem,
mixer_item: gtk::MenuItem,
mute_item: gtk::MenuItem,
mute_check: gtk::CheckButton,
prefs_item: gtk::MenuItem,
quit_item: gtk::MenuItem,
reload_item: gtk::MenuItem);
pub fn init_popup_menu(appstate: Rc<AppS>) {
/* audio.connect_handler */
{
let apps = appstate.clone();
appstate.audio.connect_handler(Box::new(move |s, u| {
/* skip if window is hidden */
if !apps.gui
.popup_menu
.menu
.get_visible() {
return;
}
match (s, u) {
(_, _) => set_mute_check(&apps),
}
}));
}
/* popup_menu.menu.connect_show */
{
let apps = appstate.clone();
appstate.gui
.popup_menu
.menu
.connect_show(move |_| set_mute_check(&apps));
}
/* mixer_item.connect_activate_link */
{
let apps = appstate.clone();
let mixer_item = &appstate.gui.popup_menu.mixer_item;
mixer_item.connect_activate(move |_| {
try_w!(execute_vol_control_command(&apps.prefs.borrow()));
});
}
/* mute_item.connect_activate_link */
{
let apps = appstate.clone();
let mute_item = &appstate.gui.popup_menu.mute_item;
mute_item.connect_activate(move |_| {
if apps.audio.has_mute() {
try_w!(apps.audio.toggle_mute(AudioUser::Popup));
}
});
}
/* about_item.connect_activate_link */
{
let apps = appstate.clone();
let about_item = &appstate.gui.popup_menu.about_item;
about_item.connect_activate(move |_| {
on_about_item_activate(&apps);
});
}
/* prefs_item.connect_activate_link */
{
let apps = appstate.clone();
let prefs_item = &appstate.gui.popup_menu.prefs_item;
prefs_item.connect_activate(move |_| {
on_prefs_item_activate(&apps);
});
}
/* reload_item.connect_activate_link */
{
let apps = appstate.clone();
let reload_item = &appstate.gui.popup_menu.reload_item;
reload_item.connect_activate(move |_| {
try_w!(audio_reload(&apps.audio,
&apps.prefs.borrow(),
AudioUser::Popup))
});
}
/* quit_item.connect_activate_link */
{
let quit_item = &appstate.gui.popup_menu.quit_item;
quit_item.connect_activate(|_| { gtk::main_quit(); });
}
}
fn on_about_item_activate(appstate: &AppS) {
let popup_menu = &appstate.gui.popup_menu.menu_window;
let about_dialog = create_about_dialog();
about_dialog.set_skip_taskbar_hint(true);
about_dialog.set_transient_for(popup_menu);
about_dialog.run();
about_dialog.destroy();
}
fn create_about_dialog() -> gtk::AboutDialog {
let about_dialog: gtk::AboutDialog = gtk::AboutDialog::new();
about_dialog.set_license(Some(
"PNMixer-rs is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License v3 as published
by the Free Software Foundation.
PNMixer is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PNMixer; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.",
));
about_dialog.set_copyright(Some("Copyright © 2017 Julian Ospald"));
about_dialog.set_authors(&["Julian Ospald"]);
about_dialog.set_artists(&["Paul Davey"]);
about_dialog.set_program_name("PNMixer-rs");
about_dialog.set_logo_icon_name("pnmixer");
about_dialog.set_version(VERSION);
about_dialog.set_website("https://github.com/hasufell/pnmixer-rust");
about_dialog.set_comments("A mixer for the system tray");
return about_dialog;
}
fn on_prefs_item_activate(appstate: &Rc<AppS>) {
/* TODO: only create if needed */
show_prefs_dialog(appstate);
}
fn set_mute_check(apps: &Rc<AppS>) {
let mute_check = &apps.gui.popup_menu.mute_check;
let m_muted = apps.audio.get_mute();
match m_muted {
Ok(muted) => {
mute_check.set_sensitive(false);
mute_check.set_active(muted);
mute_check.set_tooltip_text("");
}
Err(_) => {
mute_check.set_active(true);
mute_check.set_sensitive(false);
mute_check.set_tooltip_text("Soundcard has no mute switch");
}
}
}

300
src/ui_popup_window.rs Normal file
View File

@@ -0,0 +1,300 @@
use app_state::*;
use audio::*;
use errors::*;
use gdk::DeviceExt;
use gdk::{GrabOwnership, GrabStatus, BUTTON_PRESS_MASK, KEY_PRESS_MASK};
use gdk;
use gdk_sys::{GDK_KEY_Escape, GDK_CURRENT_TIME};
use glib;
use gtk::ToggleButtonExt;
use gtk::prelude::*;
use gtk;
use prefs::*;
use std::cell::Cell;
use std::rc::Rc;
use support_audio::*;
use support_cmd::*;
pub struct PopupWindow {
_cant_construct: (),
pub popup_window: gtk::Window,
pub vol_scale_adj: gtk::Adjustment,
pub vol_scale: gtk::Scale,
pub mute_check: gtk::CheckButton,
pub mixer_button: gtk::Button,
pub toggle_signal: Cell<u64>,
pub changed_signal: Cell<u64>,
}
impl PopupWindow {
pub fn new(builder: gtk::Builder) -> PopupWindow {
return PopupWindow {
_cant_construct: (),
popup_window: builder.get_object("popup_window").unwrap(),
vol_scale_adj: builder.get_object("vol_scale_adj").unwrap(),
vol_scale: builder.get_object("vol_scale").unwrap(),
mute_check: builder.get_object("mute_check").unwrap(),
mixer_button: builder.get_object("mixer_button").unwrap(),
toggle_signal: Cell::new(0),
changed_signal: Cell::new(0),
};
}
pub fn update(&self, audio: &Audio) -> Result<()> {
let cur_vol = audio.vol()?;
set_slider(&self.vol_scale_adj, cur_vol);
self.update_mute_check(&audio);
return Ok(());
}
pub fn update_mute_check(&self, audio: &Audio) {
let m_muted = audio.get_mute();
glib::signal_handler_block(&self.mute_check, self.toggle_signal.get());
match m_muted {
Ok(val) => {
self.mute_check.set_sensitive(true);
self.mute_check.set_active(val);
self.mute_check.set_tooltip_text("");
}
Err(_) => {
/* can't figure out whether channel is muted, grey out */
self.mute_check.set_active(true);
self.mute_check.set_sensitive(false);
self.mute_check.set_tooltip_text(
"Soundcard has no mute switch",
);
}
}
glib::signal_handler_unblock(&self.mute_check,
self.toggle_signal.get());
}
fn set_vol_increment(&self, prefs: &Prefs) {
self.vol_scale_adj
.set_page_increment(prefs.behavior_prefs.vol_scroll_step);
self.vol_scale_adj
.set_step_increment(prefs.behavior_prefs.vol_fine_scroll_step);
}
}
pub fn init_popup_window(appstate: Rc<AppS>) {
/* audio.connect_handler */
{
let apps = appstate.clone();
appstate.audio.connect_handler(Box::new(move |s, u| {
/* skip if window is hidden */
if !apps.gui
.popup_window
.popup_window
.get_visible() {
return;
}
match (s, u) {
/* Update only mute check here
* If the user changes the volume through the popup window,
* we MUST NOT update the slider value, it's been done already.
* It means that, as long as the popup window is visible,
* the slider value reflects the value set by user,
* and not the real value reported by the audio system.
*/
(_, AudioUser::Popup) => {
apps.gui.popup_window.update_mute_check(&apps.audio);
}
/* external change, safe to update slider too */
(_, _) => {
try_w!(apps.gui.popup_window.update(&apps.audio));
}
}
}));
}
/* mute_check.connect_toggled */
{
let _appstate = appstate.clone();
let mute_check = &appstate.clone()
.gui
.popup_window
.mute_check;
let toggle_signal =
mute_check.connect_toggled(move |_| {
on_mute_check_toggled(&_appstate)
});
appstate.gui
.popup_window
.toggle_signal
.set(toggle_signal);
}
/* popup_window.connect_show */
{
let _appstate = appstate.clone();
let popup_window = &appstate.clone()
.gui
.popup_window
.popup_window;
popup_window.connect_show(move |_| on_popup_window_show(&_appstate));
}
/* vol_scale_adj.connect_value_changed */
{
let _appstate = appstate.clone();
let vol_scale_adj = &appstate.clone()
.gui
.popup_window
.vol_scale_adj;
let changed_signal = vol_scale_adj.connect_value_changed(
move |_| on_vol_scale_value_changed(&_appstate),
);
appstate.gui
.popup_window
.changed_signal
.set(changed_signal);
}
/* popup_window.connect_event */
{
let popup_window = &appstate.clone()
.gui
.popup_window
.popup_window;
popup_window.connect_event(move |w, e| on_popup_window_event(w, e));
}
/* mixer_button.connect_clicked */
{
let apps = appstate.clone();
let mixer_button = &appstate.clone()
.gui
.popup_window
.mixer_button;
mixer_button.connect_clicked(move |_| {
apps.gui
.popup_window
.popup_window
.hide();
try_w!(execute_vol_control_command(&apps.prefs.borrow()));
});
}
}
fn on_popup_window_show(appstate: &AppS) {
let popup_window = &appstate.gui.popup_window;
appstate.gui.popup_window.set_vol_increment(&appstate.prefs.borrow());
glib::signal_handler_block(&popup_window.vol_scale_adj,
popup_window.changed_signal.get());
try_w!(appstate.gui.popup_window.update(&appstate.audio));
glib::signal_handler_unblock(&popup_window.vol_scale_adj,
popup_window.changed_signal.get());
popup_window.vol_scale.grab_focus();
try_w!(grab_devices(&appstate.gui.popup_window.popup_window));
}
fn on_popup_window_event(w: &gtk::Window, e: &gdk::Event) -> gtk::Inhibit {
match gdk::Event::get_event_type(e) {
gdk::EventType::GrabBroken => w.hide(),
gdk::EventType::KeyPress => {
let key: gdk::EventKey = e.clone().downcast().unwrap();
if key.get_keyval() == (GDK_KEY_Escape as u32) {
w.hide();
}
}
gdk::EventType::ButtonPress => {
let device = try_wr!(
gtk::get_current_event_device().ok_or(
"No current event device!",
),
Inhibit(false)
);
let (window, _, _) =
gdk::DeviceExt::get_window_at_position(&device);
if window.is_none() {
w.hide();
}
}
_ => (),
}
return Inhibit(false);
}
fn on_vol_scale_value_changed(appstate: &AppS) {
let audio = &appstate.audio;
let old_vol = try_w!(audio.vol());
let val = appstate.gui
.popup_window
.vol_scale
.get_value();
let dir = vol_change_to_voldir(old_vol, val);
try_w!(audio.set_vol(val,
AudioUser::Popup,
dir,
appstate.prefs
.borrow()
.behavior_prefs
.unmute_on_vol_change));
}
fn on_mute_check_toggled(appstate: &AppS) {
let audio = &appstate.audio;
try_w!(audio.toggle_mute(AudioUser::Popup))
}
pub fn set_slider(vol_scale_adj: &gtk::Adjustment, scale: f64) {
vol_scale_adj.set_value(scale);
}
fn grab_devices(window: &gtk::Window) -> Result<()> {
let device = gtk::get_current_event_device().ok_or("No current device")?;
let gdk_window = window.get_window().ok_or("No window?!")?;
/* Grab the mouse */
let m_grab_status =
device.grab(&gdk_window,
GrabOwnership::None,
true,
BUTTON_PRESS_MASK,
None,
GDK_CURRENT_TIME as u32);
if m_grab_status != GrabStatus::Success {
warn!("Could not grab {}",
device.get_name().unwrap_or(String::from("UNKNOWN DEVICE")));
}
/* Grab the keyboard */
let k_dev = device.get_associated_device()
.ok_or("Couldn't get associated device")?;
let k_grab_status = k_dev.grab(&gdk_window,
GrabOwnership::None,
true,
KEY_PRESS_MASK,
None,
GDK_CURRENT_TIME as u32);
if k_grab_status != GrabStatus::Success {
warn!("Could not grab {}",
k_dev.get_name().unwrap_or(String::from("UNKNOWN DEVICE")));
}
return Ok(());
}

413
src/ui_prefs_dialog.rs Normal file
View File

@@ -0,0 +1,413 @@
use app_state::*;
use audio::{AudioUser, AudioSignal};
use errors::*;
use gdk;
use gtk::ResponseType;
use gtk::prelude::*;
use gtk;
use prefs::*;
use std::rc::Rc;
use support_alsa::*;
pub struct PrefsDialog {
_cant_construct: (),
prefs_dialog: gtk::Dialog,
notebook: gtk::Notebook,
/* DevicePrefs */
card_combo: gtk::ComboBoxText,
chan_combo: gtk::ComboBoxText,
/* ViewPrefs */
vol_meter_draw_check: gtk::CheckButton,
vol_meter_pos_spin: gtk::SpinButton,
vol_meter_color_button: gtk::ColorButton,
system_theme: gtk::CheckButton,
/* BehaviorPrefs */
unmute_on_vol_change: gtk::CheckButton,
vol_control_entry: gtk::Entry,
scroll_step_spin: gtk::SpinButton,
fine_scroll_step_spin: gtk::SpinButton,
middle_click_combo: gtk::ComboBoxText,
custom_entry: gtk::Entry,
/* NotifyPrefs */
#[cfg(feature = "notify")]
noti_enable_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_timeout_spin: gtk::SpinButton,
// pub noti_hotkey_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_mouse_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_popup_check: gtk::CheckButton,
#[cfg(feature = "notify")]
noti_ext_check: gtk::CheckButton,
}
impl PrefsDialog {
fn new() -> PrefsDialog {
let builder =
gtk::Builder::new_from_string(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),
"/data/ui/prefs-dialog.glade")));
let prefs_dialog = PrefsDialog {
_cant_construct: (),
prefs_dialog: builder.get_object("prefs_dialog").unwrap(),
notebook: builder.get_object("notebook").unwrap(),
/* DevicePrefs */
card_combo: builder.get_object("card_combo").unwrap(),
chan_combo: builder.get_object("chan_combo").unwrap(),
/* ViewPrefs */
vol_meter_draw_check: builder.get_object("vol_meter_draw_check")
.unwrap(),
vol_meter_pos_spin: builder.get_object("vol_meter_pos_spin")
.unwrap(),
vol_meter_color_button: builder.get_object("vol_meter_color_button")
.unwrap(),
system_theme: builder.get_object("system_theme").unwrap(),
/* BehaviorPrefs */
unmute_on_vol_change: builder.get_object("unmute_on_vol_change")
.unwrap(),
vol_control_entry: builder.get_object("vol_control_entry").unwrap(),
scroll_step_spin: builder.get_object("scroll_step_spin").unwrap(),
fine_scroll_step_spin: builder.get_object("fine_scroll_step_spin")
.unwrap(),
middle_click_combo: builder.get_object("middle_click_combo")
.unwrap(),
custom_entry: builder.get_object("custom_entry").unwrap(),
/* NotifyPrefs */
#[cfg(feature = "notify")]
noti_enable_check: builder.get_object("noti_enable_check").unwrap(),
#[cfg(feature = "notify")]
noti_timeout_spin: builder.get_object("noti_timeout_spin").unwrap(),
// noti_hotkey_check: builder.get_object("noti_hotkey_check").unwrap(),
#[cfg(feature = "notify")]
noti_mouse_check: builder.get_object("noti_mouse_check").unwrap(),
#[cfg(feature = "notify")]
noti_popup_check: builder.get_object("noti_popup_check").unwrap(),
#[cfg(feature = "notify")]
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;
}
fn from_prefs(&self, prefs: &Prefs) {
/* DevicePrefs */
/* filled on show signal with audio info */
self.card_combo.remove_all();
self.chan_combo.remove_all();
/* ViewPrefs */
self.vol_meter_draw_check.set_active(prefs.view_prefs.draw_vol_meter);
self.vol_meter_pos_spin.set_value(prefs.view_prefs.vol_meter_offset as
f64);
let rgba = gdk::RGBA {
red: prefs.view_prefs.vol_meter_color.red,
green: prefs.view_prefs.vol_meter_color.green,
blue: prefs.view_prefs.vol_meter_color.blue,
alpha: 1.0,
};
self.vol_meter_color_button.set_rgba(&rgba);
self.system_theme.set_active(prefs.view_prefs.system_theme);
/* BehaviorPrefs */
self.unmute_on_vol_change
.set_active(prefs.behavior_prefs.unmute_on_vol_change);
self.vol_control_entry.set_text(prefs.behavior_prefs
.vol_control_cmd
.as_ref()
.unwrap_or(&String::from(""))
.as_str());
self.scroll_step_spin.set_value(prefs.behavior_prefs.vol_scroll_step);
self.fine_scroll_step_spin
.set_value(prefs.behavior_prefs.vol_fine_scroll_step);
// TODO: make sure these values always match, must be a better way
// also check to_prefs()
self.middle_click_combo.append_text("Toggle Mute");
self.middle_click_combo.append_text("Show Preferences");
self.middle_click_combo.append_text("Volume Control");
self.middle_click_combo.append_text("Custom Command (set below)");
self.middle_click_combo.set_active(prefs.behavior_prefs
.middle_click_action
.into());
self.custom_entry.set_text(prefs.behavior_prefs
.custom_command
.as_ref()
.unwrap_or(&String::from(""))
.as_str());
/* NotifyPrefs */
#[cfg(feature = "notify")]
{
self.noti_enable_check
.set_active(prefs.notify_prefs.enable_notifications);
self.noti_timeout_spin
.set_value(prefs.notify_prefs.notifcation_timeout as f64);
self.noti_mouse_check
.set_active(prefs.notify_prefs.notify_mouse_scroll);
self.noti_popup_check.set_active(prefs.notify_prefs.notify_popup);
self.noti_ext_check.set_active(prefs.notify_prefs.notify_external);
}
}
fn to_prefs(&self) -> Prefs {
let card = self.card_combo.get_active_text();
let channel = self.chan_combo.get_active_text();
if card.is_none() || channel.is_none() {
return Prefs::default();
}
let device_prefs = DevicePrefs {
card: self.card_combo.get_active_text().unwrap(),
channel: self.chan_combo.get_active_text().unwrap(),
};
let vol_meter_color = VolColor {
red: (self.vol_meter_color_button.get_rgba().red),
green: (self.vol_meter_color_button.get_rgba().green),
blue: (self.vol_meter_color_button.get_rgba().blue),
};
let view_prefs = ViewPrefs {
draw_vol_meter: self.vol_meter_draw_check.get_active(),
vol_meter_offset: self.vol_meter_pos_spin.get_value_as_int(),
system_theme: self.system_theme.get_active(),
vol_meter_color,
};
let vol_control_cmd =
self.vol_control_entry.get_text().and_then(|x| if x.is_empty() {
None
} else {
Some(x)
});
let custom_command =
self.custom_entry.get_text().and_then(|x| if x.is_empty() {
None
} else {
Some(x)
});
let behavior_prefs = BehaviorPrefs {
unmute_on_vol_change: self.unmute_on_vol_change.get_active(),
vol_control_cmd,
vol_scroll_step: self.scroll_step_spin.get_value(),
vol_fine_scroll_step: self.fine_scroll_step_spin.get_value(),
middle_click_action: self.middle_click_combo.get_active().into(),
custom_command,
};
#[cfg(feature = "notify")]
let notify_prefs = NotifyPrefs {
enable_notifications: self.noti_enable_check.get_active(),
notifcation_timeout: self.noti_timeout_spin.get_value_as_int() as
i64,
notify_mouse_scroll: self.noti_mouse_check.get_active(),
notify_popup: self.noti_popup_check.get_active(),
notify_external: self.noti_ext_check.get_active(),
};
return Prefs {
device_prefs,
view_prefs,
behavior_prefs,
#[cfg(feature = "notify")]
notify_prefs,
};
}
}
pub fn show_prefs_dialog(appstate: &Rc<AppS>) {
if appstate.gui
.prefs_dialog
.borrow()
.is_some() {
return;
}
*appstate.gui.prefs_dialog.borrow_mut() = Some(PrefsDialog::new());
init_prefs_dialog(&appstate);
{
let m_pd = appstate.gui.prefs_dialog.borrow();
let prefs_dialog = &m_pd.as_ref().unwrap();
let prefs_dialog_w = &prefs_dialog.prefs_dialog;
prefs_dialog.from_prefs(&appstate.prefs.borrow());
prefs_dialog_w.set_transient_for(&appstate.gui.popup_menu.menu_window);
prefs_dialog_w.present();
}
}
pub fn init_prefs_callback(appstate: Rc<AppS>) {
let apps = appstate.clone();
appstate.audio.connect_handler(Box::new(move |s, u| {
/* skip if prefs window is not present */
if apps.gui
.prefs_dialog
.borrow()
.is_none() {
return;
}
match (s, u) {
(AudioSignal::CardInitialized, _) => (),
(AudioSignal::CardCleanedUp, _) => {
fill_card_combo(&apps);
fill_chan_combo(&apps, None);
}
_ => (),
}
}));
}
fn init_prefs_dialog(appstate: &Rc<AppS>) {
/* prefs_dialog.connect_show */
{
let apps = appstate.clone();
let m_pd = appstate.gui.prefs_dialog.borrow();
let pd = m_pd.as_ref().unwrap();
pd.prefs_dialog.connect_show(move |_| {
fill_card_combo(&apps);
fill_chan_combo(&apps, None);
});
}
/* card_combo.connect_changed */
{
let apps = appstate.clone();
let m_cc = appstate.gui.prefs_dialog.borrow();
let card_combo = &m_cc.as_ref().unwrap().card_combo;
card_combo.connect_changed(move |_| {
let m_cc = apps.gui.prefs_dialog.borrow();
let card_combo = &m_cc.as_ref().unwrap().card_combo;
let card_name = card_combo.get_active_text().unwrap();
fill_chan_combo(&apps, Some(card_name));
return;
});
}
/* prefs_dialog.connect_response */
{
let apps = appstate.clone();
let m_pd = appstate.gui.prefs_dialog.borrow();
let pd = m_pd.as_ref().unwrap();
pd.prefs_dialog.connect_response(move |_, response_id| {
if response_id == ResponseType::Ok.into() ||
response_id == ResponseType::Apply.into() {
let mut prefs = apps.prefs.borrow_mut();
let prefs_dialog = apps.gui.prefs_dialog.borrow();
*prefs = prefs_dialog.as_ref().unwrap().to_prefs();
}
if response_id != ResponseType::Apply.into() {
let mut prefs_dialog = apps.gui.prefs_dialog.borrow_mut();
prefs_dialog.as_ref()
.unwrap()
.prefs_dialog
.destroy();
*prefs_dialog = None;
}
if response_id == ResponseType::Ok.into() ||
response_id == ResponseType::Apply.into() {
// TODO: update hotkeys
try_w!(apps.update_notify());
try_w!(apps.update_tray_icon());
try_w!(apps.update_popup_window());
try_w!(apps.update_audio(AudioUser::PrefsWindow));
try_w!(apps.update_config());
}
});
}
}
fn fill_card_combo(appstate: &AppS) {
let m_cc = appstate.gui.prefs_dialog.borrow();
let card_combo = &m_cc.as_ref().unwrap().card_combo;
card_combo.remove_all();
let acard = appstate.audio.acard.borrow();
/* set card combo */
let cur_card_name = try_w!(acard.card_name(),
"Can't get current card name!");
let available_card_names = get_playable_alsa_card_names();
/* set_active_id doesn't work, so save the index */
let mut c_index: i32 = -1;
for i in 0..available_card_names.len() {
let name = available_card_names.get(i).unwrap();
if *name == cur_card_name {
c_index = i as i32;
}
card_combo.append_text(&name);
}
// TODO, block signal?
card_combo.set_active(c_index);
}
fn fill_chan_combo(appstate: &AppS, cardname: Option<String>) {
let m_cc = appstate.gui.prefs_dialog.borrow();
let chan_combo = &m_cc.as_ref().unwrap().chan_combo;
chan_combo.remove_all();
let cur_acard = appstate.audio.acard.borrow();
let card = match cardname {
Some(name) => try_w!(get_alsa_card_by_name(name).from_err()),
None => cur_acard.as_ref().card,
};
/* set chan combo */
let cur_chan_name = try_w!(cur_acard.chan_name());
let mixer = try_w!(get_mixer(&card));
let available_chan_names = get_playable_selem_names(&mixer);
/* set_active_id doesn't work, so save the index */
let mut c_index: i32 = -1;
for i in 0..available_chan_names.len() {
let name = available_chan_names.get(i).unwrap();
if *name == cur_chan_name {
c_index = i as i32;
}
chan_combo.append_text(&name);
}
/* TODO, block signal?`*/
chan_combo.set_active(c_index);
}

489
src/ui_tray_icon.rs Normal file
View File

@@ -0,0 +1,489 @@
use app_state::*;
use audio::*;
use errors::*;
use gdk;
use gdk_pixbuf;
use gdk_pixbuf_sys;
use gtk::prelude::*;
use gtk;
use prefs::{Prefs, MiddleClickAction};
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
use support_cmd::*;
use support_ui::*;
use ui_prefs_dialog::show_prefs_dialog;
const ICON_MIN_SIZE: i32 = 16;
pub struct TrayIcon {
_cant_construct: (),
pub volmeter: RefCell<Option<VolMeter>>,
pub audio_pix: RefCell<AudioPix>,
pub status_icon: gtk::StatusIcon,
pub icon_size: Cell<i32>,
}
impl TrayIcon {
pub fn new(prefs: &Prefs) -> Result<TrayIcon> {
let draw_vol_meter = prefs.view_prefs.draw_vol_meter;
let volmeter = {
if draw_vol_meter {
RefCell::new(Some(VolMeter::new(prefs)))
} else {
RefCell::new(None)
}
};
let audio_pix = AudioPix::new(ICON_MIN_SIZE, prefs)?;
let status_icon = gtk::StatusIcon::new();
return Ok(TrayIcon {
_cant_construct: (),
volmeter,
audio_pix: RefCell::new(audio_pix),
status_icon,
icon_size: Cell::new(ICON_MIN_SIZE),
});
}
fn update_vol_meter(&self, cur_vol: f64, vol_level: VolLevel) -> Result<()> {
let audio_pix = self.audio_pix.borrow();
let pixbuf = audio_pix.select_pix(vol_level);
let vol_borrow = self.volmeter.borrow();
let volmeter = &vol_borrow.as_ref();
match volmeter {
&Some(v) => {
let vol_pix = v.meter_draw(cur_vol as i64, &pixbuf)?;
self.status_icon.set_from_pixbuf(Some(&vol_pix));
}
&None => self.status_icon.set_from_pixbuf(Some(pixbuf)),
};
return Ok(());
}
fn update_tooltip(&self, audio: &Audio) {
let cardname = audio.acard
.borrow()
.card_name()
.unwrap_or(String::from("Unknown card"));
let channame = audio.acard
.borrow()
.chan_name()
.unwrap_or(String::from("unknown channel"));
let vol = audio.vol()
.map(|s| format!("{}", s.round()))
.unwrap_or(String::from("unknown volume"));
let mute_info = {
if !audio.has_mute() {
"\nNo mute switch"
} else if audio.get_mute().unwrap_or(false) {
"\nMuted"
} else {
""
}
};
self.status_icon.set_tooltip_text(format!("{} ({})\nVolume: {}{}",
cardname,
channame,
vol,
mute_info)
.as_str());
}
pub fn update_all(&self,
prefs: &Prefs,
audio: &Audio,
m_size: Option<i32>)
-> Result<()> {
match m_size {
Some(s) => {
if s < ICON_MIN_SIZE {
self.icon_size.set(ICON_MIN_SIZE);
} else {
self.icon_size.set(s);
}
}
None => (),
}
let audio_pix = AudioPix::new(self.icon_size.get(), &prefs)?;
*self.audio_pix.borrow_mut() = audio_pix;
let draw_vol_meter = prefs.view_prefs.draw_vol_meter;
if draw_vol_meter {
let volmeter = VolMeter::new(&prefs);
*self.volmeter.borrow_mut() = Some(volmeter);
}
self.update_tooltip(&audio);
return self.update_vol_meter(audio.vol()?, audio.vol_level());
}
}
pub struct VolMeter {
red: u8,
green: u8,
blue: u8,
x_offset_pct: i64,
y_offset_pct: i64,
/* dynamic */
width: Cell<i64>,
row: RefCell<Vec<u8>>,
}
impl VolMeter {
fn new(prefs: &Prefs) -> VolMeter {
return VolMeter {
red: (prefs.view_prefs.vol_meter_color.red * 255.0) as u8,
green: (prefs.view_prefs.vol_meter_color.green * 255.0) as
u8,
blue: (prefs.view_prefs.vol_meter_color.blue * 255.0) as u8,
x_offset_pct: prefs.view_prefs.vol_meter_offset as i64,
y_offset_pct: 10,
/* dynamic */
width: Cell::new(0),
row: RefCell::new(vec![]),
};
}
// TODO: cache input pixbuf?
fn meter_draw(&self,
volume: i64,
pixbuf: &gdk_pixbuf::Pixbuf)
-> Result<gdk_pixbuf::Pixbuf> {
ensure!(pixbuf.get_colorspace() == gdk_pixbuf_sys::GDK_COLORSPACE_RGB,
"Invalid colorspace in pixbuf");
ensure!(pixbuf.get_bits_per_sample() == 8,
"Invalid bits per sample in pixbuf");
ensure!(pixbuf.get_has_alpha(), "No alpha channel in pixbuf");
ensure!(pixbuf.get_n_channels() == 4,
"Invalid number of channels in pixbuf");
let i_width = pixbuf.get_width() as i64;
let i_height = pixbuf.get_height() as i64;
let new_pixbuf = copy_pixbuf(pixbuf);
let vm_width = i_width / 6;
let x = (self.x_offset_pct as f64 *
((i_width - vm_width) as f64 / 100.0)) as i64;
ensure!(x >= 0 && (x + vm_width) <= i_width,
"x coordinate invalid: {}",
x);
let y = (self.y_offset_pct as f64 * (i_height as f64 / 100.0)) as i64;
let vm_height =
((i_height - (y * 2)) as f64 * (volume as f64 / 100.0)) as i64;
ensure!(y >= 0 && (y + vm_height) <= i_height,
"y coordinate invalid: {}",
y);
/* Let's check if the icon width changed, in which case we
* must reinit our internal row of pixels.
*/
if vm_width != self.width.get() {
self.width.set(vm_width);
let mut row = self.row.borrow_mut();
*row = vec![];
}
if self.row.borrow().len() == 0 {
debug!("Allocating vol meter row (width {})", vm_width);
let mut row = self.row.borrow_mut();
*row = [self.red, self.green, self.blue, 255]
.iter()
.cloned()
.cycle()
.take((vm_width * 4) as usize)
.collect();
}
/* Draw the volume meter.
* Rows in the image are stored top to bottom.
*/
{
let y = i_height - y;
let rowstride: i64 = new_pixbuf.get_rowstride() as i64;
let pixels: &mut [u8] = unsafe { new_pixbuf.get_pixels() };
for i in 0..(vm_height - 1) {
let row_offset: i64 = y - i;
let col_offset: i64 = x * 4;
let p_index = ((row_offset * rowstride) + col_offset) as usize;
let row = self.row.borrow();
pixels[p_index..p_index + row.len()]
.copy_from_slice(row.as_ref());
}
}
return Ok(new_pixbuf);
}
}
// TODO: connect on icon theme change
#[derive(Clone, Debug)]
pub struct AudioPix {
muted: gdk_pixbuf::Pixbuf,
low: gdk_pixbuf::Pixbuf,
medium: gdk_pixbuf::Pixbuf,
high: gdk_pixbuf::Pixbuf,
off: gdk_pixbuf::Pixbuf,
}
impl AudioPix {
fn new(size: i32, prefs: &Prefs) -> Result<AudioPix> {
let system_theme = prefs.view_prefs.system_theme;
let pix = {
if system_theme {
let theme: gtk::IconTheme =
gtk::IconTheme::get_default().ok_or(
"Couldn't get default icon theme",
)?;
AudioPix {
muted: pixbuf_new_from_theme(
"audio-volume-muted",
size,
&theme,
)?,
low: pixbuf_new_from_theme(
"audio-volume-low",
size,
&theme,
)?,
medium: pixbuf_new_from_theme(
"audio-volume-medium",
size,
&theme,
)?,
high: pixbuf_new_from_theme(
"audio-volume-high",
size,
&theme,
)?,
/* 'audio-volume-off' is not available in every icon set.
* Check freedesktop standard for more info:
* http://standards.freedesktop.org/icon-naming-spec/
* icon-naming-spec-latest.html
*/
off: pixbuf_new_from_theme(
"audio-volume-off",
size,
&theme,
).or(pixbuf_new_from_theme(
"audio-volume-low",
size,
&theme,
))?,
}
} else {
AudioPix {
muted: pixbuf_new_from_xpm!(pnmixer_muted),
low: pixbuf_new_from_xpm!(pnmixer_low),
medium: pixbuf_new_from_xpm!(pnmixer_medium),
high: pixbuf_new_from_xpm!(pnmixer_high),
off: pixbuf_new_from_xpm!(pnmixer_off),
}
}
};
return Ok(pix);
}
fn select_pix(&self, vol_level: VolLevel) -> &gdk_pixbuf::Pixbuf {
match vol_level {
VolLevel::Muted => &self.muted,
VolLevel::Low => &self.low,
VolLevel::Medium => &self.medium,
VolLevel::High => &self.high,
VolLevel::Off => &self.off,
}
}
}
pub fn init_tray_icon(appstate: Rc<AppS>) {
let audio = &appstate.audio;
let tray_icon = &appstate.gui.tray_icon;
try_e!(tray_icon.update_all(&appstate.prefs.borrow_mut(), &audio, None));
tray_icon.status_icon.set_visible(true);
/* connect audio handler */
{
let apps = appstate.clone();
appstate.audio.connect_handler(Box::new(move |s, u| match (s, u) {
(_, _) => {
apps.gui.tray_icon.update_tooltip(&apps.audio);
try_w!(apps.gui.tray_icon.update_vol_meter(try_w!(apps.audio.vol()),
apps.audio.vol_level()));
}
}));
}
/* tray_icon.connect_size_changed */
{
let apps = appstate.clone();
tray_icon.status_icon.connect_size_changed(move |_, size| {
try_wr!(apps.gui.tray_icon.update_all(&apps.prefs.borrow_mut(),
&apps.audio,
Some(size)),
false);
return false;
});
}
/* tray_icon.connect_activate */
{
let apps = appstate.clone();
tray_icon.status_icon.connect_activate(move |_| {
on_tray_icon_activate(&apps)
});
}
/* tray_icon.connect_scroll_event */
{
let apps = appstate.clone();
tray_icon.status_icon.connect_scroll_event(
move |_, e| on_tray_icon_scroll_event(&apps, &e),
);
}
/* tray_icon.connect_popup_menu */
{
let apps = appstate.clone();
tray_icon.status_icon.connect_popup_menu(move |_, _, _| {
on_tray_icon_popup_menu(&apps)
});
}
/* tray_icon.connect_button_release_event */
{
let apps = appstate.clone();
tray_icon.status_icon.connect_button_release_event(move |_, eb| {
on_tray_button_release_event(&apps, eb)
});
}
/* default_theme.connect_changed */
{
let apps = appstate.clone();
let default_theme = try_w!(gtk::IconTheme::get_default().ok_or(
"Couldn't get default icon theme",
));
default_theme.connect_changed(move |_| {
let tray_icon = &apps.gui.tray_icon;
let audio = &apps.audio;
try_e!(tray_icon.update_all(&apps.prefs.borrow_mut(), &audio, None));
});
}
}
fn on_tray_icon_activate(appstate: &AppS) {
let popup_window = &appstate.gui.popup_window.popup_window;
if popup_window.get_visible() {
popup_window.hide();
} else {
popup_window.show_now();
}
}
fn on_tray_icon_popup_menu(appstate: &AppS) {
let popup_window = &appstate.gui.popup_window.popup_window;
let popup_menu = &appstate.gui.popup_menu.menu;
popup_window.hide();
popup_menu.popup_at_pointer(None);
}
fn on_tray_icon_scroll_event(appstate: &AppS,
event: &gdk::EventScroll)
-> bool {
let scroll_dir: gdk::ScrollDirection = event.get_direction();
match scroll_dir {
gdk::ScrollDirection::Up => {
try_wr!(appstate.audio.increase_vol(AudioUser::TrayIcon,
appstate.prefs
.borrow()
.behavior_prefs
.unmute_on_vol_change),
false);
}
gdk::ScrollDirection::Down => {
try_wr!(appstate.audio.decrease_vol(AudioUser::TrayIcon,
appstate.prefs
.borrow()
.behavior_prefs
.unmute_on_vol_change),
false);
}
_ => (),
}
return false;
}
fn on_tray_button_release_event(appstate: &Rc<AppS>,
event_button: &gdk::EventButton)
-> bool {
let button = event_button.get_button();
if button != 2 {
// not middle-click
return false;
}
let audio = &appstate.audio;
let prefs = &appstate.prefs.borrow();
let middle_click_action = &prefs.behavior_prefs.middle_click_action;
let custom_command = &prefs.behavior_prefs.custom_command;
match middle_click_action {
&MiddleClickAction::ToggleMute => {
if audio.has_mute() {
try_wr!(audio.toggle_mute(AudioUser::Popup), false);
}
}
// TODO
&MiddleClickAction::ShowPreferences => show_prefs_dialog(&appstate),
&MiddleClickAction::VolumeControl => {
try_wr!(execute_vol_control_command(&appstate.prefs.borrow()),
false);
}
&MiddleClickAction::CustomCommand => {
match custom_command {
&Some(ref cmd) => try_wr!(execute_command(cmd.as_str()), false),
&None => warn!("No custom command found"),
}
}
}
return false;
}

32
src/xpm.c Normal file
View File

@@ -0,0 +1,32 @@
#include "../data/pixmaps/pnmixer-about.xpm"
#include "../data/pixmaps/pnmixer-high.xpm"
#include "../data/pixmaps/pnmixer-low.xpm"
#include "../data/pixmaps/pnmixer-medium.xpm"
#include "../data/pixmaps/pnmixer-muted.xpm"
#include "../data/pixmaps/pnmixer-off.xpm"
char** pnmixer_about() {
return pnmixer_about_xpm;
}
char** pnmixer_high() {
return pnmixer_high_xpm;
}
char** pnmixer_low() {
return pnmixer_low_xpm;
}
char** pnmixer_medium() {
return pnmixer_medium_xpm;
}
char** pnmixer_muted() {
return pnmixer_muted_xpm;
}
char** pnmixer_off() {
return pnmixer_off_xpm;
}