2017-06-29 12:55:07 +00:00
|
|
|
use app_state::*;
|
2017-07-04 19:15:11 +00:00
|
|
|
use audio::*;
|
|
|
|
use errors::*;
|
2017-06-30 23:56:32 +00:00
|
|
|
use gdk;
|
2017-07-03 07:21:51 +00:00
|
|
|
use gdk_pixbuf;
|
2017-07-04 14:54:16 +00:00
|
|
|
use gdk_pixbuf_sys::GDK_COLORSPACE_RGB;
|
2017-06-29 12:55:07 +00:00
|
|
|
use gtk::prelude::*;
|
2017-07-04 19:15:11 +00:00
|
|
|
use gtk;
|
2017-07-05 22:14:24 +00:00
|
|
|
use prefs::Prefs;
|
2017-07-04 14:54:16 +00:00
|
|
|
use std::cell::Cell;
|
2017-06-30 23:56:32 +00:00
|
|
|
use std::cell::RefCell;
|
2017-07-04 19:15:11 +00:00
|
|
|
use std::rc::Rc;
|
|
|
|
use support_ui::*;
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: on_apply
|
|
|
|
|
2017-07-04 14:54:16 +00:00
|
|
|
|
2017-07-05 22:14:24 +00:00
|
|
|
const ICON_MIN_SIZE: i32 = 16;
|
2017-06-29 12:55:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
2017-07-04 19:15:11 +00:00
|
|
|
pub struct TrayIcon {
|
2017-07-05 22:14:24 +00:00
|
|
|
pub volmeter: Option<VolMeter>,
|
|
|
|
pub audio_pix: RefCell<AudioPix>,
|
2017-07-04 19:15:11 +00:00
|
|
|
pub status_icon: gtk::StatusIcon,
|
2017-07-05 22:14:24 +00:00
|
|
|
pub icon_size: Cell<i32>,
|
2017-07-04 19:15:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl TrayIcon {
|
2017-07-05 22:14:24 +00:00
|
|
|
pub fn new(prefs: &Prefs) -> Result<TrayIcon> {
|
|
|
|
let draw_vol_meter = prefs.view_prefs.draw_vol_meter;
|
|
|
|
|
|
|
|
let volmeter = {
|
|
|
|
if draw_vol_meter {
|
|
|
|
Some(VolMeter::new(prefs))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let audio_pix = AudioPix::new(ICON_MIN_SIZE, prefs)?;
|
2017-07-04 19:15:11 +00:00
|
|
|
let status_icon = gtk::StatusIcon::new();
|
2017-07-03 21:38:39 +00:00
|
|
|
|
2017-07-05 18:27:16 +00:00
|
|
|
return Ok(TrayIcon {
|
2017-07-06 14:53:19 +00:00
|
|
|
volmeter,
|
|
|
|
audio_pix: RefCell::new(audio_pix),
|
|
|
|
status_icon,
|
|
|
|
icon_size: Cell::new(ICON_MIN_SIZE),
|
|
|
|
});
|
2017-07-04 19:15:11 +00:00
|
|
|
}
|
2017-07-03 21:38:39 +00:00
|
|
|
|
2017-07-06 14:53:19 +00:00
|
|
|
fn update(
|
|
|
|
&self,
|
|
|
|
prefs: &Prefs,
|
|
|
|
audio: &Audio,
|
|
|
|
m_size: Option<i32>,
|
|
|
|
) -> Result<()> {
|
2017-07-04 19:15:11 +00:00
|
|
|
match m_size {
|
|
|
|
Some(s) => {
|
|
|
|
if s < ICON_MIN_SIZE {
|
|
|
|
self.icon_size.set(ICON_MIN_SIZE);
|
2017-07-05 22:14:24 +00:00
|
|
|
|
2017-07-04 19:15:11 +00:00
|
|
|
} else {
|
|
|
|
self.icon_size.set(s);
|
|
|
|
}
|
2017-07-05 22:14:24 +00:00
|
|
|
/* if icon size changed, we have to re-init audio pix */
|
|
|
|
let pixbuf = AudioPix::new(self.icon_size.get(), &prefs)?;
|
|
|
|
*self.audio_pix.borrow_mut() = pixbuf;
|
2017-07-05 18:27:16 +00:00
|
|
|
}
|
2017-07-04 19:15:11 +00:00
|
|
|
None => (),
|
|
|
|
}
|
2017-07-04 14:54:16 +00:00
|
|
|
|
2017-07-05 22:14:24 +00:00
|
|
|
let cur_vol = audio.vol()?;
|
|
|
|
let audio_pix = self.audio_pix.borrow();
|
|
|
|
let pixbuf = audio_pix.select_pix(audio.vol_level());
|
|
|
|
|
|
|
|
let volmeter = &self.volmeter.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)),
|
|
|
|
};
|
2017-07-04 14:54:16 +00:00
|
|
|
|
2017-07-05 22:14:24 +00:00
|
|
|
return Ok(());
|
2017-07-04 19:15:11 +00:00
|
|
|
}
|
2017-07-04 14:54:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-07-04 19:15:11 +00:00
|
|
|
|
|
|
|
pub struct VolMeter {
|
2017-07-04 14:54:16 +00:00
|
|
|
pub red: u8,
|
|
|
|
pub green: u8,
|
|
|
|
pub blue: u8,
|
|
|
|
pub x_offset_pct: i64,
|
|
|
|
pub y_offset_pct: i64,
|
|
|
|
/* dynamic */
|
|
|
|
pub width: Cell<i64>,
|
|
|
|
pub row: RefCell<Vec<u8>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl VolMeter {
|
2017-07-05 22:14:24 +00:00
|
|
|
pub fn new(prefs: &Prefs) -> VolMeter {
|
2017-07-04 14:54:16 +00:00
|
|
|
return VolMeter {
|
2017-07-06 14:53:19 +00:00
|
|
|
red: prefs.view_prefs.vol_meter_color.red,
|
|
|
|
green: prefs.view_prefs.vol_meter_color.green,
|
|
|
|
blue: prefs.view_prefs.vol_meter_color.blue,
|
|
|
|
x_offset_pct: prefs.view_prefs.vol_meter_offset,
|
|
|
|
y_offset_pct: 10,
|
|
|
|
/* dynamic */
|
|
|
|
width: Cell::new(0),
|
|
|
|
row: RefCell::new(vec![]),
|
|
|
|
};
|
2017-07-04 14:54:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: cache input pixbuf?
|
2017-07-06 14:53:19 +00:00
|
|
|
fn meter_draw(
|
|
|
|
&self,
|
|
|
|
volume: i64,
|
|
|
|
pixbuf: &gdk_pixbuf::Pixbuf,
|
|
|
|
) -> Result<gdk_pixbuf::Pixbuf> {
|
|
|
|
|
|
|
|
ensure!(
|
|
|
|
pixbuf.get_colorspace() == GDK_COLORSPACE_RGB,
|
|
|
|
"Invalid colorspace in pixbuf"
|
|
|
|
);
|
|
|
|
ensure!(
|
|
|
|
pixbuf.get_bits_per_sample() == 8,
|
|
|
|
"Invalid bits per sample in pixbuf"
|
|
|
|
);
|
2017-07-04 14:54:16 +00:00
|
|
|
ensure!(pixbuf.get_has_alpha(), "No alpha channel in pixbuf");
|
2017-07-06 14:53:19 +00:00
|
|
|
ensure!(
|
|
|
|
pixbuf.get_n_channels() == 4,
|
|
|
|
"Invalid number of channels in pixbuf"
|
|
|
|
);
|
2017-07-04 14:54:16 +00:00
|
|
|
|
|
|
|
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 *
|
2017-07-06 14:53:19 +00:00
|
|
|
((i_width - vm_width) as f64 / 100.0)) as
|
|
|
|
i64;
|
|
|
|
ensure!(
|
|
|
|
x >= 0 && (x + vm_width) <= i_width,
|
|
|
|
"x coordinate invalid: {}",
|
|
|
|
x
|
|
|
|
);
|
2017-07-04 14:54:16 +00:00
|
|
|
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;
|
2017-07-06 14:53:19 +00:00
|
|
|
ensure!(
|
|
|
|
y >= 0 && (y + vm_height) <= i_height,
|
|
|
|
"y coordinate invalid: {}",
|
|
|
|
y
|
|
|
|
);
|
2017-07-04 14:54:16 +00:00
|
|
|
|
|
|
|
/* 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()
|
2017-07-04 15:14:20 +00:00
|
|
|
.take((vm_width * 4) as usize)
|
2017-07-04 14:54:16 +00:00
|
|
|
.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;
|
2017-07-04 15:14:20 +00:00
|
|
|
let p_index = ((row_offset * rowstride) + col_offset) as usize;
|
|
|
|
|
|
|
|
let row = self.row.borrow();
|
2017-07-06 14:53:19 +00:00
|
|
|
pixels[p_index..p_index + row.len()].copy_from_slice(
|
|
|
|
row.as_ref(),
|
|
|
|
);
|
2017-07-04 15:14:20 +00:00
|
|
|
|
2017-07-04 14:54:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(new_pixbuf);
|
|
|
|
}
|
|
|
|
}
|
2017-07-03 21:38:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
// TODO: connect on icon theme change
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
2017-07-04 19:15:11 +00:00
|
|
|
pub struct AudioPix {
|
2017-07-03 21:38:39 +00:00
|
|
|
muted: gdk_pixbuf::Pixbuf,
|
|
|
|
low: gdk_pixbuf::Pixbuf,
|
|
|
|
medium: gdk_pixbuf::Pixbuf,
|
|
|
|
high: gdk_pixbuf::Pixbuf,
|
|
|
|
off: gdk_pixbuf::Pixbuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AudioPix {
|
2017-07-05 22:14:24 +00:00
|
|
|
pub fn new(size: i32, prefs: &Prefs) -> Result<AudioPix> {
|
|
|
|
let system_theme = prefs.view_prefs.system_theme;
|
|
|
|
|
|
|
|
let pix = {
|
|
|
|
if system_theme {
|
2017-07-06 14:53:19 +00:00
|
|
|
let theme: gtk::IconTheme =
|
|
|
|
gtk::IconTheme::get_default().ok_or(
|
|
|
|
"Couldn't get default icon theme",
|
|
|
|
)?;
|
2017-07-05 22:14:24 +00:00
|
|
|
AudioPix {
|
2017-07-06 14:53:19 +00:00
|
|
|
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,
|
|
|
|
)?,
|
2017-07-05 22:14:24 +00:00
|
|
|
/* '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
|
|
|
|
*/
|
2017-07-06 14:53:19 +00:00
|
|
|
off: pixbuf_new_from_theme(
|
|
|
|
"audio-volume-off",
|
|
|
|
size,
|
|
|
|
&theme,
|
|
|
|
).or(pixbuf_new_from_theme(
|
|
|
|
"audio-volume-low",
|
|
|
|
size,
|
|
|
|
&theme,
|
|
|
|
))?,
|
2017-07-05 22:14:24 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
AudioPix {
|
|
|
|
muted: pixbuf_new_from_file("pnmixer-muted.png")?,
|
|
|
|
low: pixbuf_new_from_file("pnmixer-low.png")?,
|
|
|
|
medium: pixbuf_new_from_file("pnmixer-medium.png")?,
|
|
|
|
high: pixbuf_new_from_file("pnmixer-high.png")?,
|
|
|
|
off: pixbuf_new_from_file("pnmixer-off.png")?,
|
|
|
|
}
|
|
|
|
}
|
2017-07-03 21:38:39 +00:00
|
|
|
};
|
|
|
|
return Ok(pix);
|
|
|
|
}
|
|
|
|
|
2017-07-03 22:23:26 +00:00
|
|
|
|
2017-07-03 21:38:39 +00:00
|
|
|
pub 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-03 07:21:51 +00:00
|
|
|
|
2017-06-30 15:24:26 +00:00
|
|
|
pub fn init_tray_icon(appstate: Rc<AppS>) {
|
2017-07-04 19:15:11 +00:00
|
|
|
let audio = &appstate.audio;
|
|
|
|
let tray_icon = &appstate.gui.tray_icon;
|
2017-07-05 22:14:24 +00:00
|
|
|
try_e!(tray_icon.update(&appstate.prefs.borrow_mut(), &audio, None));
|
2017-07-04 19:15:11 +00:00
|
|
|
|
|
|
|
tray_icon.status_icon.set_visible(true);
|
2017-07-03 21:38:39 +00:00
|
|
|
|
|
|
|
/* connect audio handler */
|
|
|
|
{
|
|
|
|
let apps = appstate.clone();
|
2017-07-04 14:54:16 +00:00
|
|
|
appstate.audio.connect_handler(
|
|
|
|
Box::new(move |s, u| match (s, u) {
|
2017-07-03 21:38:39 +00:00
|
|
|
(AudioSignal::ValuesChanged, _) => {
|
2017-07-06 14:53:19 +00:00
|
|
|
try_w!(apps.gui.tray_icon.update(
|
|
|
|
&apps.prefs.borrow_mut(),
|
|
|
|
&apps.audio,
|
|
|
|
None,
|
|
|
|
));
|
2017-07-04 14:54:16 +00:00
|
|
|
}
|
2017-07-03 21:38:39 +00:00
|
|
|
_ => (),
|
2017-07-04 14:54:16 +00:00
|
|
|
}),
|
|
|
|
);
|
2017-07-03 21:38:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* tray_icon.connect_size_changed */
|
|
|
|
{
|
|
|
|
let apps = appstate.clone();
|
2017-07-04 19:15:11 +00:00
|
|
|
tray_icon.status_icon.connect_size_changed(move |_, size| {
|
2017-07-06 14:53:19 +00:00
|
|
|
try_wr!(
|
|
|
|
apps.gui.tray_icon.update(
|
|
|
|
&apps.prefs.borrow_mut(),
|
|
|
|
&apps.audio,
|
|
|
|
Some(size),
|
|
|
|
),
|
|
|
|
false
|
|
|
|
);
|
2017-07-04 19:15:11 +00:00
|
|
|
return false;
|
2017-07-04 14:54:16 +00:00
|
|
|
});
|
2017-07-03 21:38:39 +00:00
|
|
|
}
|
|
|
|
|
2017-07-01 14:55:35 +00:00
|
|
|
/* tray_icon.connect_activate */
|
2017-06-30 23:56:32 +00:00
|
|
|
{
|
|
|
|
let apps = appstate.clone();
|
2017-07-06 14:53:19 +00:00
|
|
|
tray_icon.status_icon.connect_activate(
|
|
|
|
move |_| on_tray_icon_activate(&apps),
|
|
|
|
);
|
2017-06-30 23:56:32 +00:00
|
|
|
}
|
2017-07-01 14:55:35 +00:00
|
|
|
|
|
|
|
/* tray_icon.connect_scroll_event */
|
2017-06-30 23:56:32 +00:00
|
|
|
{
|
2017-07-01 14:55:35 +00:00
|
|
|
let apps = appstate.clone();
|
2017-07-04 19:15:11 +00:00
|
|
|
tray_icon.status_icon.connect_scroll_event(
|
2017-07-04 14:54:16 +00:00
|
|
|
move |_, e| on_tray_icon_scroll_event(&apps, &e),
|
|
|
|
);
|
2017-06-30 23:56:32 +00:00
|
|
|
}
|
2017-06-29 12:55:07 +00:00
|
|
|
|
2017-07-01 14:55:35 +00:00
|
|
|
/* tray_icon.connect_popup_menu */
|
|
|
|
{
|
|
|
|
let apps = appstate.clone();
|
2017-07-06 14:53:19 +00:00
|
|
|
tray_icon.status_icon.connect_popup_menu(move |_, _, _| {
|
|
|
|
on_tray_icon_popup_menu(&apps)
|
|
|
|
});
|
2017-07-01 14:55:35 +00:00
|
|
|
}
|
2017-07-03 21:52:41 +00:00
|
|
|
|
|
|
|
/* tray_icon.connect_button_release_event */
|
|
|
|
{
|
|
|
|
let apps = appstate.clone();
|
2017-07-06 14:53:19 +00:00
|
|
|
tray_icon.status_icon.connect_button_release_event(move |_, eb| {
|
|
|
|
on_tray_button_release_event(&apps, eb)
|
|
|
|
});
|
2017-07-03 21:52:41 +00:00
|
|
|
}
|
2017-06-30 15:24:26 +00:00
|
|
|
}
|
2017-06-29 12:55:07 +00:00
|
|
|
|
|
|
|
|
2017-06-30 15:24:26 +00:00
|
|
|
fn on_tray_icon_activate(appstate: &AppS) {
|
2017-07-01 14:55:35 +00:00
|
|
|
let popup_window = &appstate.gui.popup_window.popup_window;
|
2017-06-30 15:24:26 +00:00
|
|
|
|
|
|
|
if popup_window.get_visible() {
|
|
|
|
popup_window.hide();
|
|
|
|
} else {
|
|
|
|
popup_window.show_now();
|
|
|
|
}
|
2017-06-29 12:55:07 +00:00
|
|
|
}
|
2017-06-30 23:56:32 +00:00
|
|
|
|
|
|
|
|
2017-07-01 14:55:35 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-07-06 14:53:19 +00:00
|
|
|
fn on_tray_icon_scroll_event(
|
|
|
|
appstate: &AppS,
|
|
|
|
event: &gdk::EventScroll,
|
|
|
|
) -> bool {
|
2017-06-30 23:56:32 +00:00
|
|
|
|
2017-07-01 14:55:35 +00:00
|
|
|
let scroll_dir: gdk::ScrollDirection = event.get_direction();
|
2017-06-30 23:56:32 +00:00
|
|
|
match scroll_dir {
|
2017-07-01 14:55:35 +00:00
|
|
|
gdk::ScrollDirection::Up => {
|
2017-07-03 21:38:39 +00:00
|
|
|
try_wr!(appstate.audio.increase_vol(AudioUser::TrayIcon), false);
|
2017-06-30 23:56:32 +00:00
|
|
|
}
|
2017-07-01 14:55:35 +00:00
|
|
|
gdk::ScrollDirection::Down => {
|
2017-07-03 21:38:39 +00:00
|
|
|
try_wr!(appstate.audio.decrease_vol(AudioUser::TrayIcon), false);
|
2017-06-30 23:56:32 +00:00
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2017-07-03 21:38:39 +00:00
|
|
|
|
|
|
|
|
2017-07-06 14:53:19 +00:00
|
|
|
fn on_tray_button_release_event(
|
|
|
|
appstate: &AppS,
|
|
|
|
event_button: &gdk::EventButton,
|
|
|
|
) -> bool {
|
2017-07-03 21:52:41 +00:00
|
|
|
let button = event_button.get_button();
|
|
|
|
|
|
|
|
if button != 2 {
|
|
|
|
// not middle-click
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let audio = &appstate.audio;
|
|
|
|
try_wr!(audio.toggle_mute(AudioUser::Popup), false);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|