Restructure modules

This commit is contained in:
2017-07-19 12:12:08 +02:00
parent d8f1a794ac
commit 72cc33ac7d
29 changed files with 1706 additions and 1439 deletions

137
src/ui/entry.rs Normal file
View File

@@ -0,0 +1,137 @@
//! Global GUI state.
use app_state::*;
use audio::frontend::*;
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::*;
/// The GUI struct mostly describing the main widgets (mostly wrapped)
/// the user interacts with.
pub struct Gui {
_cant_construct: (),
/// The tray icon.
pub tray_icon: TrayIcon,
/// The popup window.
pub popup_window: PopupWindow,
/// The popup menu.
pub popup_menu: PopupMenu,
/* prefs_dialog is dynamically created and destroyed */
/// The preferences dialog.
pub prefs_dialog: RefCell<Option<PrefsDialog>>,
}
impl Gui {
/// Constructor. The prefs dialog is initialized as `None`.
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),
};
}
}
/// Initialize the GUI system.
pub fn init<T>(appstate: Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
{
/* "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.as_ref(),
&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.as_ref(),
&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());
}
/// Used to run a dialog when an audio error occured, suggesting the user
/// may reload the audio system either manually or by confirming the dialog
/// via the confirmation button.
///
/// # Returns
///
/// `GTK_RESPONSE_YES` if the user wants to reload the audio system,
/// `GTK_RESPONSE_NO` otherwise.
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;
}

144
src/ui/hotkey_dialog.rs Normal file
View File

@@ -0,0 +1,144 @@
//! The ui hotkey preferences dialog.
//!
//! Usually run from the preferences window.
use errors::*;
use gdk::DeviceExt;
use gdk;
use gdk_sys;
use glib::translate::*;
use gtk::prelude::*;
use gtk;
use gtk_sys;
use libc::c_uint;
use std;
/// Hotkey dialog struct holding the relevant gtk widgets.
pub struct HotkeyDialog {
hotkey_dialog: gtk::Dialog,
// instruction_label: gtk::Label, // not needed
key_pressed_label: gtk::Label,
}
impl HotkeyDialog {
/// Creates a new hotkey dialog.
pub fn new<P>(parent: &P, hotkey: String) -> HotkeyDialog
where
P: IsA<gtk::Window>,
{
let builder = gtk::Builder::new_from_string(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/data/ui/hotkey-dialog.glade"
)));
let hotkey_dialog: gtk::Dialog =
builder.get_object("hotkey_dialog").unwrap();
let instruction_label: gtk::Label =
builder.get_object("instruction_label").unwrap();
let key_pressed_label: gtk::Label =
builder.get_object("key_pressed_label").unwrap();
hotkey_dialog.set_title(format!("Set {} HotKey", hotkey).as_str());
instruction_label.set_markup(
format!("Press new HotKey for <b>{}</b>", hotkey)
.as_str(),
);
hotkey_dialog.set_transient_for(parent);
{
let key_pressed_label = key_pressed_label.clone();
hotkey_dialog.connect_key_press_event(move |_, e| {
let mut state = e.get_state();
unsafe {
let mut keyval: c_uint = 0;
let mut consumed: gdk_sys::GdkModifierType =
gdk_sys::GdkModifierType::empty();
gdk_sys::gdk_keymap_translate_keyboard_state(
gdk_sys::gdk_keymap_get_default(),
e.get_hardware_keycode() as u32,
state.to_glib(),
e.get_group() as i32,
&mut keyval as *mut c_uint,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut consumed as *mut gdk_sys::GdkModifierType,
);
let consumed: gdk::ModifierType = from_glib(!consumed);
state = state & consumed;
state = state & gtk::accelerator_get_default_mod_mask();
let key_text = gtk::accelerator_name(keyval, state);
key_pressed_label.set_text(
key_text
.unwrap_or(String::from("(None)"))
.as_str(),
);
};
return Inhibit(false);
});
}
hotkey_dialog.connect_key_release_event(move |w, _| {
w.response(gtk_sys::GtkResponseType::Ok as i32);
return Inhibit(false);
});
return HotkeyDialog {
hotkey_dialog,
key_pressed_label,
};
}
/// Runs the hotkey dialog and returns a String representing the hotkey
/// that has been pressed.
pub fn run(&self) -> Result<String> {
self.hotkey_dialog.show_now();
let device = gtk::get_current_event_device().ok_or(
"Could not get current device",
)?;
let window = self.hotkey_dialog.get_window().ok_or(
"Could not get window",
)?;
let m_grab_status = device.grab(
&window,
gdk::GrabOwnership::Application,
true,
gdk::KEY_PRESS_MASK,
None,
gdk_sys::GDK_CURRENT_TIME as u32,
);
if m_grab_status != gdk::GrabStatus::Success {
bail!("Could not grab the keyboard");
}
let resp = self.hotkey_dialog.run();
device.ungrab(gdk_sys::GDK_CURRENT_TIME as u32);
if resp != gtk::ResponseType::Ok.into() {
bail!(ErrorKind::GtkResponseCancel(
String::from("not assigning hotkey"),
));
}
return Ok(self.key_pressed_label.get_text().ok_or(
"Could not get text",
)?);
}
}
impl Drop for HotkeyDialog {
fn drop(&mut self) {
self.hotkey_dialog.destroy();
}
}

8
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
//! The UI subsystem.
pub mod entry;
pub mod hotkey_dialog;
pub mod popup_menu;
pub mod popup_window;
pub mod prefs_dialog;
pub mod tray_icon;

208
src/ui/popup_menu.rs Normal file
View File

@@ -0,0 +1,208 @@
#![allow(missing_docs)] // glade_helpers
//! The popup menu subsystem when the user right-clicks on the tray icon.
//!
//! Shows the menu with the following entries:
//!
//! * Mute
//! * Volume Control
//! * Preferences
//! * Reload Sound
//! * About
//! * Quit
use app_state::*;
use audio::frontend::*;
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
);
/// Initialize the popup menu subsystem, registering all callbacks.
pub fn init_popup_menu<T>(appstate: Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
/* 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 |_| {
let _ = result_warn!(
execute_vol_control_command(&apps.prefs.borrow()),
Some(&apps.gui.popup_menu.menu_window)
);
});
}
/* 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.as_ref(),
&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(); });
}
}
/// When the about menu item is activated.
fn on_about_item_activate<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
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();
}
/// Create the About dialog from scratch.
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;
}
/// When the Preferences item is activated.
fn on_prefs_item_activate<T>(appstate: &Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
/* TODO: only create if needed */
show_prefs_dialog(appstate);
}
/// When the Mute item is checked.
fn set_mute_check<T>(apps: &Rc<AppS<T>>)
where
T: AudioFrontend,
{
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");
}
}
}

338
src/ui/popup_window.rs Normal file
View File

@@ -0,0 +1,338 @@
//! The popup window subsystem when the user left-clicks on the tray icon.
//!
//! This shows the manipulatable volume slider with the current volume and
//! the mute checkbox.
use app_state::*;
use audio::frontend::*;
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::*;
/// The main struct for the popup window, holding all relevant sub-widgets
/// and some mutable state.
pub struct PopupWindow {
_cant_construct: (),
/// The main window for the popup window widget.
pub popup_window: gtk::Window,
/// The volume scale adjustment.
pub vol_scale_adj: gtk::Adjustment,
/// The volume scale.
pub vol_scale: gtk::Scale,
/// The mute check button.
pub mute_check: gtk::CheckButton,
/// The button to start the external mixer.
pub mixer_button: gtk::Button,
/// Signal for mute_check.connect_toggled callback,
/// so we can block it temporarily.
toggle_signal: Cell<u64>,
/// Signal for vol_scale_adj.connect_value_changed callback,
/// so we can block it temporarily.
changed_signal: Cell<u64>,
}
impl PopupWindow {
/// Constructor.
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),
};
}
/// Update the popup window state, including the slider
/// and the mute checkbutton.
pub fn update<T>(&self, audio: &T) -> Result<()>
where
T: AudioFrontend,
{
let cur_vol = audio.get_vol()?;
set_slider(&self.vol_scale_adj, cur_vol);
self.update_mute_check(audio);
return Ok(());
}
/// Update the mute checkbutton.
pub fn update_mute_check<T>(&self, audio: &T)
where
T: AudioFrontend,
{
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(),
);
}
/// Set the page increment fro the volume scale adjustment based on the
/// preferences.
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,
);
}
}
/// Initialize the popup window subsystem.
pub fn init_popup_window<T>(appstate: Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
/* 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.as_ref(),
);
}
/* external change, safe to update slider too */
(_, _) => {
try_w!(apps.gui.popup_window.update(apps.audio.as_ref()));
}
}
}));
}
/* 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();
let _ = result_warn!(
execute_vol_control_command(&apps.prefs.borrow()),
Some(&apps.gui.popup_menu.menu_window)
);
});
}
}
/// When the popup window is shown.
fn on_popup_window_show<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
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.as_ref()));
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));
}
/// On key or button press event on the 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);
}
/// When the volume scale slider is moved.
fn on_vol_scale_value_changed<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
let audio = &appstate.audio;
let old_vol = try_w!(audio.get_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,
));
}
/// When the mute checkbutton is toggled.
fn on_mute_check_toggled<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
let audio = &appstate.audio;
try_w!(audio.toggle_mute(AudioUser::Popup))
}
/// Set the volume slider to the given value.
pub fn set_slider(vol_scale_adj: &gtk::Adjustment, scale: f64) {
vol_scale_adj.set_value(scale);
}
/// Grab all devices, keyboard and mouse.
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(());
}

725
src/ui/prefs_dialog.rs Normal file
View File

@@ -0,0 +1,725 @@
//! The preferences window subsystem, when the user clicks the "Preferences"
//! menu item on the popup menu.
use app_state::*;
use audio::frontend::*;
use errors::*;
use gdk;
use gtk::ResponseType;
use gtk::prelude::*;
use gtk;
use prefs::*;
use std::ascii::AsciiExt;
use std::cell::RefCell;
use std::rc::Rc;
use support::audio::*;
use ui::hotkey_dialog::HotkeyDialog;
/// The main preferences dialog, holding all the relevant subwidgets we
/// need to convert its state to preferences and back.
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,
#[cfg(feature = "notify")]
noti_hotkey_check: gtk::CheckButton,
/* HotkeyPrefs */
hotkeys_enable_check: gtk::CheckButton,
hotkeys_mute_label: gtk::Label,
hotkeys_up_label: gtk::Label,
hotkeys_down_label: gtk::Label,
/* Hotkey stuff (not prefs) */
hotkeys_mute_eventbox: gtk::EventBox,
hotkeys_up_eventbox: gtk::EventBox,
hotkeys_down_eventbox: gtk::EventBox,
hotkey_dialog: RefCell<Option<HotkeyDialog>>,
}
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")]
#[cfg(feature = "notify")]
#[cfg(feature = "notify")]
#[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")]
#[cfg(feature = "notify")]
#[cfg(feature = "notify")]
#[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")]
noti_hotkey_check: builder.get_object("noti_hotkey_check").unwrap(),
/* HotkeyPrefs */
hotkeys_enable_check: builder
.get_object("hotkeys_enable_check")
.unwrap(),
hotkeys_mute_label: builder
.get_object("hotkeys_mute_label")
.unwrap(),
hotkeys_up_label: builder.get_object("hotkeys_up_label").unwrap(),
hotkeys_down_label: builder
.get_object("hotkeys_down_label")
.unwrap(),
/* Hotkey stuff (not prefs) */
hotkeys_mute_eventbox: builder
.get_object("hotkeys_mute_eventbox")
.unwrap(),
hotkeys_up_eventbox: builder
.get_object("hotkeys_up_eventbox")
.unwrap(),
hotkeys_down_eventbox: builder
.get_object("hotkeys_down_eventbox")
.unwrap(),
hotkey_dialog: RefCell::new(None),
};
#[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;
}
/// Import the given preferences into the preferences dialog state.
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,
);
self.noti_hotkey_check.set_active(
prefs.notify_prefs.notify_hotkeys,
);
}
/* hotkey prefs */
self.hotkeys_enable_check.set_active(
prefs.hotkey_prefs.enable_hotkeys,
);
self.hotkeys_mute_label.set_text(
prefs
.hotkey_prefs
.mute_unmute_key
.clone()
.unwrap_or(String::from("(None)"))
.as_str(),
);
self.hotkeys_up_label.set_text(
prefs
.hotkey_prefs
.vol_up_key
.clone()
.unwrap_or(String::from("(None)"))
.as_str(),
);
self.hotkeys_down_label.set_text(
prefs
.hotkey_prefs
.vol_down_key
.clone()
.unwrap_or(String::from("(None)"))
.as_str(),
);
}
/// Export the dialog state to the `Prefs` struct, which can be used
/// to write them to the config file.
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(),
notify_hotkeys: self.noti_hotkey_check.get_active(),
};
let hotkey_prefs =
HotkeyPrefs {
enable_hotkeys: self.hotkeys_enable_check.get_active(),
mute_unmute_key: self.hotkeys_mute_label.get_text().and_then(
|s| {
if s == "(None)" { None } else { Some(s) }
},
),
vol_up_key: self.hotkeys_up_label.get_text().and_then(
|s| if s ==
"(None)"
{
None
} else {
Some(s)
},
),
vol_down_key: self.hotkeys_down_label.get_text().and_then(
|s| if s ==
"(None)"
{
None
} else {
Some(s)
},
),
};
return Prefs {
device_prefs,
view_prefs,
behavior_prefs,
notify_prefs,
hotkey_prefs,
};
}
}
/// Show the preferences dialog. This is created and destroyed dynamically
/// and not persistent across the application lifetime.
pub fn show_prefs_dialog<T>(appstate: &Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
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();
}
}
/// Initialize the internal prefs dialog handler that connects to the audio
/// system.
pub fn init_prefs_callback<T>(appstate: Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
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);
}
_ => (),
}
}));
}
/// Initialize the preferences dialog gtk callbacks.
fn init_prefs_dialog<T>(appstate: &Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
/* 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()
{
try_w!(apps.update_popup_window());
try_w!(apps.update_tray_icon());
let _ = result_warn!(
apps.update_hotkeys(),
Some(&apps.gui.popup_menu.menu_window)
);
apps.update_notify();
try_w!(apps.update_audio(AudioUser::PrefsWindow));
let _ = result_warn!(
apps.update_config(),
Some(&apps.gui.popup_menu.menu_window)
);
}
});
}
/* prefs_dialog.hotkeys_mute_eventbox */
{
let apps = appstate.clone();
let m_pd = appstate.gui.prefs_dialog.borrow();
let pd = m_pd.as_ref().unwrap();
pd.hotkeys_mute_eventbox.connect_button_press_event(
move |w, e| {
return Inhibit(
on_hotkey_event_box_button_press_event(&apps, &w, e),
);
},
);
}
/* prefs_dialog.hotkeys_up_eventbox */
{
let apps = appstate.clone();
let m_pd = appstate.gui.prefs_dialog.borrow();
let pd = m_pd.as_ref().unwrap();
pd.hotkeys_up_eventbox.connect_button_press_event(
move |w, e| {
return Inhibit(
on_hotkey_event_box_button_press_event(&apps, &w, e),
);
},
);
}
/* prefs_dialog.hotkeys_down_eventbox */
{
let apps = appstate.clone();
let m_pd = appstate.gui.prefs_dialog.borrow();
let pd = m_pd.as_ref().unwrap();
pd.hotkeys_down_eventbox.connect_button_press_event(
move |w, e| {
return Inhibit(
on_hotkey_event_box_button_press_event(&apps, &w, e),
);
},
);
}
}
/// Fill the card combo box in the Devices tab.
fn fill_card_combo<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
let m_cc = appstate.gui.prefs_dialog.borrow();
let card_combo = &m_cc.as_ref().unwrap().card_combo;
card_combo.remove_all();
let audio = &appstate.audio;
/* set card combo */
let cur_card_name =
try_w!(audio.card_name(), "Can't get current card name!");
let available_card_names = get_playable_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);
}
/// Fill the channel combo box in the Devices tab.
fn fill_chan_combo<T>(appstate: &AppS<T>, cardname: Option<String>)
where
T: AudioFrontend,
{
let m_cc = appstate.gui.prefs_dialog.borrow();
let chan_combo = &m_cc.as_ref().unwrap().chan_combo;
chan_combo.remove_all();
let audio = &appstate.audio;
let available_chan_names = match cardname {
Some(name) => get_playable_chan_names(name),
None => audio.playable_chan_names(),
};
/* set chan combo */
let cur_chan_name = try_w!(audio.chan_name());
/* 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);
}
fn on_hotkey_event_box_button_press_event<T>(
appstate: &AppS<T>,
widget: &gtk::EventBox,
event: &gdk::EventButton,
) -> bool
where
T: AudioFrontend,
{
let borrow = appstate.gui.prefs_dialog.borrow();
let prefs_dialog = &borrow.as_ref().unwrap();
/* we want a left-click */
if event.get_button() != 1 {
return false;
}
/* we want it to be double-click */
if event.get_event_type() != gdk::EventType::DoubleButtonPress {
return false;
}
let (hotkey_label, hotkey) = {
if *widget == prefs_dialog.hotkeys_mute_eventbox {
(
prefs_dialog.hotkeys_mute_label.clone(),
String::from("Mute/Unmute"),
)
} else if *widget == prefs_dialog.hotkeys_up_eventbox {
(
prefs_dialog.hotkeys_up_label.clone(),
String::from("Volume Up"),
)
} else if *widget == prefs_dialog.hotkeys_down_eventbox {
(
prefs_dialog.hotkeys_down_label.clone(),
String::from("Volume Down"),
)
} else {
warn!("Unknown hotkey eventbox");
return false;
}
};
/* Ensure there's no dialog already running */
if prefs_dialog.hotkey_dialog.borrow().is_some() {
return false;
}
/* Unbind hotkeys */
appstate.hotkeys.borrow().unbind();
/* Run the hotkey dialog */
let hotkey_dialog = &prefs_dialog.hotkey_dialog;
*hotkey_dialog.borrow_mut() =
Some(HotkeyDialog::new(&prefs_dialog.prefs_dialog, hotkey));
let key_pressed = hotkey_dialog.borrow().as_ref().unwrap().run();
*hotkey_dialog.borrow_mut() = None;
/* Bind hotkeys */
appstate.hotkeys.borrow().bind();
/* Check the response */
match key_pressed {
Ok(k) => {
println!("k: {}", k);
if k.eq_ignore_ascii_case("<Primary>c") {
hotkey_label.set_text("(None)");
} else {
hotkey_label.set_text(k.as_str());
}
}
Err(Error(ErrorKind::GtkResponseCancel(msg), _)) => {
info!("{}", ErrorKind::GtkResponseCancel(msg));
return false;
}
Err(e) => {
// Could not grab hotkey, most likely
error_dialog!(
e.description(),
Some(&appstate.gui.popup_menu.menu_window)
);
warn!("{}", e);
return false;
}
}
return false;
}

625
src/ui/tray_icon.rs Normal file
View File

@@ -0,0 +1,625 @@
//! The tray icon subsystem.
//!
//! This manages the tray icon Pixbuf as well as the callbacks on left and
//! right-click.
use app_state::*;
use audio::frontend::*;
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;
/// The tray icon struct, describing the complete visual state.
pub struct TrayIcon {
_cant_construct: (),
/// The volume meter to draw on the actual Pixbuf, if requested.
pub volmeter: RefCell<Option<VolMeter>>,
/// The actual Pixbuf tray icon.
pub audio_pix: RefCell<AudioPix>,
/// The gtk `StatusIcon` widget, used to register callbacks.
pub status_icon: gtk::StatusIcon,
/// The current icon size.
pub icon_size: Cell<i32>,
}
impl TrayIcon {
/// Constructor. `audio_pix` is initialized as empty GdkPixbuf, to save
/// one iteration of png decoding (`update_all()` is triggered immediately
/// on startup through `tray_icon.connect_size_changed` usually,
/// otherwise we have to trigger it manually).
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)
}
};
// audio_pix is initialized as empty GdkPixbuf, to save
// one iteration of png decoding (update_all is triggered immediately
// on startup through tray_icon.connect_size_changed.
let audio_pix = AudioPix::default();
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),
});
}
/// Update the volume meter Pixbuf, which is drawn on top of the
/// actual Pixbuf.
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(());
}
/// Update the tooltip of the tray icon.
fn update_tooltip<T>(&self, audio: &T)
where
T: AudioFrontend,
{
let cardname =
audio.card_name().unwrap_or(String::from("Unknown card"));
let channame =
audio.chan_name().unwrap_or(String::from("unknown channel"));
let vol = audio
.get_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(),
);
}
/// Update the whole tray icon state.
pub fn update_all<T>(
&self,
prefs: &Prefs,
audio: &T,
m_size: Option<i32>,
) -> Result<()>
where
T: AudioFrontend,
{
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.get_vol()?, audio.vol_level());
}
}
/// The volume meter, describes by its colors, offset and width/row
/// properties.
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 {
/// Constructor. `width` and `row` are initialized with default values.
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?
/// Draw the volume meter on top of the actual tray icon 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)]
/// The actual tray icon Pixbuf, which depends on the current volume level.
pub struct AudioPix {
muted: gdk_pixbuf::Pixbuf,
low: gdk_pixbuf::Pixbuf,
medium: gdk_pixbuf::Pixbuf,
high: gdk_pixbuf::Pixbuf,
off: gdk_pixbuf::Pixbuf,
}
impl Default for AudioPix {
fn default() -> AudioPix {
let dummy_pixbuf = unsafe {
gdk_pixbuf::Pixbuf::new(
gdk_pixbuf_sys::GDK_COLORSPACE_RGB,
false,
8,
1,
1,
).unwrap()
};
return AudioPix {
muted: dummy_pixbuf.clone(),
low: dummy_pixbuf.clone(),
medium: dummy_pixbuf.clone(),
high: dummy_pixbuf.clone(),
off: dummy_pixbuf.clone(),
};
}
}
impl AudioPix {
/// Constructor.
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_png!(
"../../data/pixmaps/pnmixer-muted.png"
)?,
low: pixbuf_new_from_png!(
"../../data/pixmaps/pnmixer-low.png"
)?,
medium: pixbuf_new_from_png!(
"../../data/pixmaps/pnmixer-medium.png"
)?,
high: pixbuf_new_from_png!(
"../../data/pixmaps/pnmixer-high.png"
)?,
off: pixbuf_new_from_png!(
"../../data/pixmaps/pnmixer-off.png"
)?,
}
}
};
return Ok(pix);
}
/// Select the try icon Pixbuf depending on the `VolLevel`.
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,
}
}
}
/// Initialize the tray icon subsystem.
pub fn init_tray_icon<T>(appstate: Rc<AppS<T>>)
where
T: AudioFrontend + 'static,
{
let tray_icon = &appstate.gui.tray_icon;
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.as_ref());
try_w!(apps.gui.tray_icon.update_vol_meter(
try_w!(apps.audio.get_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.as_ref(),
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;
try_e!(tray_icon.update_all(
&apps.prefs.borrow_mut(),
apps.audio.as_ref(),
None,
));
});
}
}
/// When the tray icon is activated.
fn on_tray_icon_activate<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
let popup_window = &appstate.gui.popup_window.popup_window;
if popup_window.get_visible() {
popup_window.hide();
} else {
popup_window.show_now();
}
}
/// When the popup menu is shown, hide the popup window, if any.
fn on_tray_icon_popup_menu<T>(appstate: &AppS<T>)
where
T: AudioFrontend,
{
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);
}
/// When the mouse scroll event happens while the mouse pointer is
/// on the tray icon.
fn on_tray_icon_scroll_event<T>(
appstate: &AppS<T>,
event: &gdk::EventScroll,
) -> bool
where
T: AudioFrontend,
{
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;
}
/// Basically when the tray icon is clicked (although we connect to the `release`
/// event). This decides whether it was a left, right or middle-click and
/// takes appropriate actions.
fn on_tray_button_release_event<T>(
appstate: &Rc<AppS<T>>,
event_button: &gdk::EventButton,
) -> bool
where
T: AudioFrontend + 'static,
{
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::TrayIcon), false);
}
}
// TODO
&MiddleClickAction::ShowPreferences => show_prefs_dialog(&appstate),
&MiddleClickAction::VolumeControl => {
let _ = result_warn!(
execute_vol_control_command(&appstate.prefs.borrow()),
Some(&appstate.gui.popup_menu.menu_window)
);
}
&MiddleClickAction::CustomCommand => {
match custom_command {
&Some(ref cmd) => {
let _ = result_warn!(
execute_command(cmd.as_str()),
Some(&appstate.gui.popup_menu.menu_window)
);
}
&None => warn!("No custom command found"),
}
}
}
return false;
}