Restructure modules
This commit is contained in:
137
src/ui/entry.rs
Normal file
137
src/ui/entry.rs
Normal 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: >k::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
144
src/ui/hotkey_dialog.rs
Normal 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
8
src/ui/mod.rs
Normal 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
208
src/ui/popup_menu.rs
Normal 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
338
src/ui/popup_window.rs
Normal 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: >k::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: >k::Adjustment, scale: f64) {
|
||||
vol_scale_adj.set_value(scale);
|
||||
}
|
||||
|
||||
|
||||
/// Grab all devices, keyboard and mouse.
|
||||
fn grab_devices(window: >k::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
725
src/ui/prefs_dialog.rs
Normal 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(
|
||||
¬ify_tab,
|
||||
Some(>k::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: >k::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
625
src/ui/tray_icon.rs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user