diff --git a/CMakeLists.txt b/CMakeLists.txt index 3770d46..3386539 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,8 @@ if (BRICKMAN_TEST) test/controller/FakeFileBrowserController.vala test/controller/FakeNetworkController.vala test/controller/FakeOpenRobertaController.vala + test/controller/FakeSoundController.vala + test/view_model/FakeMixerElement.vala ) else (BRICKMAN_TEST) set (BRICKMAN_SOURCE_FILES @@ -58,6 +60,8 @@ else (BRICKMAN_TEST) src/controller/FileBrowserController.vala src/controller/NetworkController.vala src/controller/OpenRobertaController.vala + src/controller/SoundController.vala + src/view_model/AlsaBackedMixerElement.vala src/GlobalManager.vala src/main.vala ) @@ -86,6 +90,8 @@ set (BRICKMAN_COMMON_SOURCE_FILES src/view/DeviceBrowserWindow.vala src/view/FileBrowserWindow.vala src/view/HomeWindow.vala + src/view/MixerElementSelectorWindow.vala + src/view/MixerElementVolumeWindow.vala src/view/MotorBrowserWindow.vala src/view/MotorInfoWindow.vala src/view/MotorValueDialog.vala @@ -114,10 +120,12 @@ set (BRICKMAN_COMMON_SOURCE_FILES src/view/WifiNetworkWindow.vala src/view/WifiStatusBarItem.vala src/view/WifiWindow.vala + src/view_model/IMixerElementViewModel.vala ) find_package(PkgConfig REQUIRED) pkg_check_modules(DEPS REQUIRED + alsa ev3devkit-0.4 glib-2.0 gobject-2.0 @@ -174,6 +182,7 @@ PACKAGES curses posix linux + alsa ${BRICKMAN_TEST_PACKAGES} CUSTOM_VAPIS bindings/*.vapi diff --git a/README.md b/README.md index 65cbe84..34df713 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ not in brickstrap shell: mkdir -p cd - cmake -DCMAKE_BUILD_TYPE=string:Debug -DBRICKMAN_TEST=bool:Yes + cmake -DCMAKE_BUILD_TYPE=string:Debug -DBRICKMAN_TEST=bool:Yes make make run diff --git a/debian/control b/debian/control index 6e4aa15..1fd2aa2 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,8 @@ Priority: standard Maintainer: David Lechner Build-Depends: debhelper (>= 9), dh-systemd, cmake, valac (>= 0.24), libgirepository1.0-dev, libgudev-1.0-dev, - libncurses5-dev, libev3devkit-dev, netpbm + libncurses5-dev, libev3devkit-dev, netpbm, + libasound2-dev Standards-Version: 3.9.5 Homepage: https://www.ev3dev.org Vcs-Git: git://github.com/ev3dev/brickman.git diff --git a/src/controller/SoundController.vala b/src/controller/SoundController.vala new file mode 100644 index 0000000..5d32b68 --- /dev/null +++ b/src/controller/SoundController.vala @@ -0,0 +1,121 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright 2016 Kaelin Laundry + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +/* SoundController.vala - Controller for sound volume control */ + +using Ev3devKit.Devices; +using Ev3devKit.Ui; +using Alsa; + +namespace BrickManager { + public class SoundController : Object, IBrickManagerModule { + const int VOLUME_STEP = 10; + + Mixer mixer; + MixerElementSelectorWindow mixer_select_window; + MixerElementVolumeWindow volume_window; + + public string display_name { get { return "Sound"; } } + + private void initialize_mixer () { + mixer = null; + + int err = Mixer.open (out mixer); + if (err != 0) { + critical ("Failed to open mixer: %s", Alsa.strerror (err)); + return; + } + + err = mixer.attach (); + if (err != 0) { + critical ("Failed to attach mixer: %s", Alsa.strerror (err)); + return; + } + + err = mixer.register (); + if (err != 0) { + critical ("Failed to register mixer: %s", Alsa.strerror (err)); + return; + } + + err = mixer.load (); + if (err != 0) { + critical ("Failed to load mixer: %s", Alsa.strerror (err)); + return; + } + } + + void create_main_window () { + mixer_select_window = new MixerElementSelectorWindow (); + + mixer_select_window.mixer_element_selected.connect ((selected_element) => { + if (volume_window == null) { + create_volume_window (); + } + + volume_window.current_element = selected_element; + volume_window.show_element_details = true; + volume_window.show (); + }); + } + + void create_volume_window () { + volume_window = new MixerElementVolumeWindow (); + + weak MixerElementVolumeWindow weak_volume_window = volume_window; + // Wire up handlers for volume window signals + volume_window.volume_up.connect (() => + weak_volume_window.current_element.volume += VOLUME_STEP); + + volume_window.volume_down.connect (() => + weak_volume_window.current_element.volume -= VOLUME_STEP); + + volume_window.mute.connect (() => + weak_volume_window.current_element.volume = IMixerElementViewModel.MIN_VOLUME); + } + + public void show_main_window () { + if (mixer_select_window == null) { + create_main_window (); + } + + // Whenever the sound item is launched from the main menu, + // repopulate the mixer list + mixer_select_window.clear_elements (); + // Re-initializing will return updated data, including volume + initialize_mixer (); + for (MixerElement element = mixer.first_elem (); element != null; element = element.next ()) { + mixer_select_window.add_element (new AlsaBackedMixerElement (element)); + } + + if (mixer_select_window.has_single_element) { + if (volume_window == null) + create_volume_window (); + + volume_window.current_element = mixer_select_window.first_element; + volume_window.show_element_details = false; + volume_window.show (); + } else { + mixer_select_window.show (); + } + } + } +} diff --git a/src/main.vala b/src/main.vala index cb1f923..54c74a3 100644 --- a/src/main.vala +++ b/src/main.vala @@ -67,6 +67,8 @@ namespace BrickManager { var bluetooth_controller = new BluetoothController (); network_controller.add_controller (bluetooth_controller); network_controller.add_controller (network_controller.wifi_controller); + var sound_controller = new SoundController (); + home_window.add_controller (sound_controller); var battery_controller = new BatteryController (); home_window.add_controller (battery_controller); var open_roberta_controller = new OpenRobertaController (); diff --git a/src/view/MixerElementSelectorWindow.vala b/src/view/MixerElementSelectorWindow.vala new file mode 100644 index 0000000..83a37dd --- /dev/null +++ b/src/view/MixerElementSelectorWindow.vala @@ -0,0 +1,130 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright (C) 2016 Kaelin Laundry + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +/* + * MixerElementSelectorWindow.vala - Lists ALSA mixer elements + */ + +using Ev3devKit; +using Ev3devKit.Ui; + +namespace BrickManager { + + public class MixerElementSelectorWindow : BrickManagerWindow { + Ui.Menu element_menu; + + public signal void mixer_element_selected (IMixerElementViewModel selected_element); + + public MixerElementSelectorWindow () { + title = "Volume Controls"; + element_menu = new Ui.Menu (); + content_vbox.add (element_menu); + } + + protected string get_element_label_text (IMixerElementViewModel element) { + var builder = new StringBuilder (); + builder.append (element.name); + + if (element.index != 0) { + builder.append_printf ("[%u]", element.index); + } + + if (element.can_mute && element.is_muted) { + builder.append (" (muted)"); + } else { + builder.append_printf (" (%ld%%)", element.volume); + } + + return builder.str; + } + + protected void sort_element_menu () { + // TODO: we would get much better performance if we just inserted + // the item in the correct place instead of sorting the entire list + // each time an item is inserted. + element_menu.sort_menu_items ((a, b) => { + var element_a = a.represented_object as IMixerElementViewModel; + var element_b = b.represented_object as IMixerElementViewModel; + + // Group by name, and sort by index within the same name + if (element_a.name == element_b.name) { + return (int)element_a.index - (int)element_b.index; + } else { + return element_a.name.ascii_casecmp (element_b.name); + } + }); + } + + public void add_element (IMixerElementViewModel element) { + var menu_item = new Ui.MenuItem (get_element_label_text (element)) { + represented_object = (Object)element + }; + + weak IMixerElementViewModel weak_element = element; + // Update the menu item whenever the represented element changes + weak_element.notify.connect ((sender, property) => { + menu_item.label.text = get_element_label_text (weak_element); + sort_element_menu (); + }); + + // Emit a selection signal for this element when its menu item is selected + menu_item.button.pressed.connect (() => + mixer_element_selected (weak_element)); + + element_menu.add_menu_item (menu_item); + } + + protected void remove_menu_item (Ui.MenuItem menu_item) { + if (menu_item != null) { + element_menu.remove_menu_item (menu_item); + } + } + + public void remove_element (IMixerElementViewModel element) { + var item = element_menu.find_menu_item (element, (menu_item, target_element) => { + return target_element == (menu_item.represented_object as IMixerElementViewModel); + }); + + remove_menu_item (item); + } + + public void clear_elements () { + var iter = element_menu.menu_item_iter (); + while (iter.size > 0) { + remove_menu_item (iter[0]); + } + } + + public bool has_single_element { + get { + return element_menu.menu_item_iter ().size == 1; + } + } + + public IMixerElementViewModel? first_element { + get { + if (element_menu.menu_item_iter ().size <= 0) { + return null; + } + + return element_menu.menu_item_iter ().get (0).represented_object as IMixerElementViewModel; + } + } + } +} diff --git a/src/view/MixerElementVolumeWindow.vala b/src/view/MixerElementVolumeWindow.vala new file mode 100644 index 0000000..934cf92 --- /dev/null +++ b/src/view/MixerElementVolumeWindow.vala @@ -0,0 +1,126 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright (C) 2016 Kaelin Laundry + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +/* + * MixerElementVolumeWindow.vala - Allows control of mixer element volume + */ + +using Ev3devKit; +using Ev3devKit.Ui; + +namespace BrickManager { + public class MixerElementVolumeWindow : BrickManagerWindow { + public signal void volume_up (); + public signal void volume_down (); + public signal void mute (); + + IMixerElementViewModel _current_element; + bool _show_element_details = true; + + Ui.Label element_label; + + public MixerElementVolumeWindow () { + title = "Volume control"; + + element_label = new Ui.Label (); + content_vbox.add (element_label); + + var controls_menu = new Ui.Menu (); + + var volume_up_item = new Ui.MenuItem ("+ Volume up"); + volume_up_item.button.pressed.connect (() => { + if (current_element != null) { + volume_up (); + } + }); + controls_menu.add_menu_item (volume_up_item); + + var volume_down_item = new Ui.MenuItem ("- Volume down"); + volume_down_item.button.pressed.connect (() => { + if (current_element != null) { + volume_down (); + } + }); + controls_menu.add_menu_item (volume_down_item); + + var mute_item = new Ui.MenuItem ("Mute"); + mute_item.button.pressed.connect (() => { + if (current_element != null) { + mute (); + } + }); + controls_menu.add_menu_item (mute_item); + + content_vbox.add (controls_menu); + + update_from_element (); + } + + public IMixerElementViewModel current_element { + get { + return _current_element; + } + + set { + if (_current_element != null) { + _current_element.notify.disconnect (update_from_element); + } + + _current_element = value; + _current_element.notify.connect (update_from_element); + update_from_element (); + } + } + + public bool show_element_details { + get { + return _show_element_details; + } + set { + _show_element_details = value; + update_from_element (); + } + } + + private void update_from_element () { + if (_current_element == null) { + element_label.text = "???"; + } else { + var builder = new StringBuilder(); + if(_current_element.can_mute && _current_element.is_muted) { + builder.append("muted"); + } else { + builder.append_printf("%ld%%", _current_element.volume); + } + + if(show_element_details) { + builder.append_printf(" (%s", _current_element.name); + + if(_current_element.index != 0) { + builder.append_printf(" [%u]", _current_element.index); + } + + builder.append(")"); + } + + element_label.text = builder.str; + } + } + } +} diff --git a/src/view_model/AlsaBackedMixerElement.vala b/src/view_model/AlsaBackedMixerElement.vala new file mode 100644 index 0000000..a54fa31 --- /dev/null +++ b/src/view_model/AlsaBackedMixerElement.vala @@ -0,0 +1,110 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright (C) 2016 Kaelin Laundry + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +/* AlsaBackedMixerElement.vala - Implementation of IMixerElementViewModel using ALSA API */ + +using Alsa; + +namespace BrickManager { + public class AlsaBackedMixerElement: IMixerElementViewModel, Object { + const SimpleChannelId primary_channel_id = SimpleChannelId.MONO; + + unowned MixerElement alsa_element; + SimpleElementId alsa_id; + + public AlsaBackedMixerElement (MixerElement element) { + this.alsa_element = element; + SimpleElementId.alloc (out alsa_id); + element.get_id (alsa_id); + } + + public string name { + get { + return alsa_id.get_name (); + } + } + + public uint index { + get { + return alsa_id.get_index (); + } + } + + public int volume { + get { + long volume = 0; + alsa_element.get_playback_volume (primary_channel_id, out volume); + + long min_volume = 0, max_volume = 0; + alsa_element.get_playback_volume_range (out min_volume, out max_volume); + + // Prevent division by zero + if (max_volume == min_volume) + return 0; + + // Do calculations as floats so avoid odd-looking increments from truncation + return (int)Math.round ((volume - min_volume) * 100 / (float)(max_volume - min_volume)); + } + set { + long min_volume, max_volume; + alsa_element.get_playback_volume_range (out min_volume, out max_volume); + + var constrained_volume = int.min (100, int.max (0, value)); + float scaled_volume = constrained_volume * (max_volume - min_volume) / 100f + min_volume; + long rounded_volume = (long)Math.round (scaled_volume); + + alsa_element.set_playback_volume_all (rounded_volume); + + bool should_mute = rounded_volume <= min_volume; + if (is_muted != should_mute) { + set_is_muted (should_mute); + } + } + } + + public bool can_mute { + get { + return alsa_element.has_playback_switch (); + } + } + + public bool is_muted { + get { + if (!can_mute) { + return false; + } + + int mute_switch = 1; + if (alsa_element.get_playback_switch (primary_channel_id, out mute_switch) != 0) { + critical ("Error while getting mute switch state"); + } + + return mute_switch == 0; + } + } + + protected void set_is_muted (bool is_muted) { + if (can_mute) { + alsa_element.set_playback_switch_all (is_muted ? 0 : 1); + } + } + } +} diff --git a/src/view_model/IMixerElementViewModel.vala b/src/view_model/IMixerElementViewModel.vala new file mode 100644 index 0000000..abe7539 --- /dev/null +++ b/src/view_model/IMixerElementViewModel.vala @@ -0,0 +1,38 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright (C) 2016 Kaelin Laundry + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +/* IMixerElementViewModel.vala - Interface for object controlling a mixer element */ + +using Alsa; + +namespace BrickManager { + public interface IMixerElementViewModel : Object { + public const int MIN_VOLUME = 0; + public const int MAX_VOLUME = 100; + public const int HALF_VOLUME = (MAX_VOLUME + MIN_VOLUME) / 2; + + public abstract string name { get; } + public abstract uint index { get; } + public abstract int volume { get; set; } + public abstract bool can_mute { get; } + public abstract bool is_muted { get; } + } +} diff --git a/test/ControlPanel.vala b/test/ControlPanel.vala index b306855..2191941 100644 --- a/test/ControlPanel.vala +++ b/test/ControlPanel.vala @@ -33,6 +33,7 @@ namespace BrickManager { public FakeFileBrowserController file_browser_controller; public FakeDeviceBrowserController device_browser_controller; public FakeNetworkController network_controller; + public FakeSoundController sound_controller; public FakeBluetoothController bluetooth_controller; public FakeBatteryController battery_controller; public FakeAboutController about_controller; @@ -41,6 +42,7 @@ namespace BrickManager { enum Tab { DEVICE_BROWSER, NETWORK, + SOUND, BLUETOOTH, BATTERY, OPEN_ROBERTA, @@ -129,6 +131,15 @@ namespace BrickManager { COLUMN_COUNT; } + enum SoundMixerElementsColumn { + NAME, + INDEX, + VOLUME, + CAN_MUTE, + MUTE, + USER_DATA + } + enum BluetoothDeviceColumn { PRESENT, NAME, @@ -147,6 +158,7 @@ namespace BrickManager { file_browser_controller = new FakeFileBrowserController (builder); device_browser_controller = new FakeDeviceBrowserController (builder); network_controller = new FakeNetworkController (builder); + sound_controller = new FakeSoundController (builder); bluetooth_controller = new FakeBluetoothController (builder); battery_controller = new FakeBatteryController (builder); about_controller = new FakeAboutController (builder); diff --git a/test/controller/FakeSoundController.vala b/test/controller/FakeSoundController.vala new file mode 100644 index 0000000..c9797cd --- /dev/null +++ b/test/controller/FakeSoundController.vala @@ -0,0 +1,264 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright (C) 2016 Kaelin Laundry + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +/* FakeSoundController.vala - Fake sound controller for testing */ + +using Ev3devKit; +using Ev3devKit.Ui; + +namespace BrickManager { + public class FakeSoundController : Object, IBrickManagerModule { + private const int VOLUME_STEP = 10; + + MixerElementSelectorWindow mixer_select_window; + MixerElementVolumeWindow volume_window; + + public string display_name { get { return "Sound"; } } + + public FakeSoundController (Gtk.Builder builder) { + // Initialize windows that the controller needs + mixer_select_window = new MixerElementSelectorWindow (); + volume_window = new MixerElementVolumeWindow (); + + // Register for callback so that we can focus on the correct control panel tab + // the first time either window is invoked + var control_panel_notebook = builder.get_object ("control-panel-notebook") as Gtk.Notebook; + mixer_select_window.shown.connect (() => + control_panel_notebook.page = (int)ControlPanel.Tab.SOUND); + volume_window.shown.connect (() => + control_panel_notebook.page = (int)ControlPanel.Tab.SOUND); + + // Initialize items in brickman for all current elements + var mixer_elems_liststore = builder.get_object ("mixer-elements-liststore") as Gtk.ListStore; + mixer_elems_liststore.foreach ((model, path, iter) => { + update_fake_element_from_liststore (iter, mixer_elems_liststore); + return false; + }); + + // Propagate changes from liststore to views + mixer_elems_liststore.row_changed.connect ((path, iter) => { + update_fake_element_from_liststore (iter, mixer_elems_liststore); + }); + + // Link liststore and control panel GUI + (builder.get_object ("mixer-element-name-cellrenderertext") as Gtk.CellRendererText) + .edited.connect ((path, new_text) => ControlPanel.update_listview_text_item ( + mixer_elems_liststore, path, new_text, ControlPanel.SoundMixerElementsColumn.NAME)); + (builder.get_object ("mixer-element-index-cellrenderertext") as Gtk.CellRendererText) + .edited.connect ((path, new_text) => ControlPanel.update_listview_text_item ( + mixer_elems_liststore, path, new_text, ControlPanel.SoundMixerElementsColumn.INDEX)); + (builder.get_object ("mixer-element-volume-cellrenderertext") as Gtk.CellRendererText) + .edited.connect ((path, new_text) => ControlPanel.update_listview_text_item ( + mixer_elems_liststore, path, new_text, ControlPanel.SoundMixerElementsColumn.VOLUME)); + (builder.get_object ("mixer-element-can-mute-cellrenderertoggle") as Gtk.CellRendererToggle) + .toggled.connect ((toggle, path) => ControlPanel.update_listview_toggle_item ( + mixer_elems_liststore, toggle, path, ControlPanel.SoundMixerElementsColumn.CAN_MUTE)); + (builder.get_object ("mixer-element-mute-cellrenderertoggle") as Gtk.CellRendererToggle) + .toggled.connect ((toggle, path) => ControlPanel.update_listview_toggle_item ( + mixer_elems_liststore, toggle, path, ControlPanel.SoundMixerElementsColumn.MUTE)); + + // Configure the add button + (builder.get_object ("mixer-element-add-button") as Gtk.Button).clicked.connect (() => { + Gtk.TreeIter iter; + mixer_elems_liststore.append (out iter); + + // Doing this all at once ensures that the row_changed handler is only called once, + // and never called with partial data. + mixer_elems_liststore.set_valuesv (iter, new int[] { + ControlPanel.SoundMixerElementsColumn.NAME, + ControlPanel.SoundMixerElementsColumn.INDEX, + ControlPanel.SoundMixerElementsColumn.VOLUME, + ControlPanel.SoundMixerElementsColumn.CAN_MUTE, + ControlPanel.SoundMixerElementsColumn.MUTE + }, new Value[] { "New Element", 0, IMixerElementViewModel.HALF_VOLUME, true, false }); + }); + + // Store references to the remove button + var mixer_element_remove_button = builder.get_object ("mixer-element-remove-button") as Gtk.Button; + var mixer_element_treeview_selection = (builder.get_object ("mixer-elements-treeview") as Gtk.TreeView).get_selection (); + + // Configure the remove button action + mixer_element_remove_button.clicked.connect (() => { + Gtk.TreeModel model; + Gtk.TreeIter iter; + + if (mixer_element_treeview_selection.get_selected (out model, out iter)) { + Value user_data; + model.get_value (iter, ControlPanel.SoundMixerElementsColumn.USER_DATA, out user_data); + + var mixer_element = (FakeMixerElement)user_data.get_pointer (); + if (mixer_element != null) { + mixer_select_window.remove_element (mixer_element); + } + + mixer_elems_liststore.remove (iter); + } + }); + + // Desensitize the remove button if nothing is selected + mixer_element_treeview_selection.changed.connect (() => { + mixer_element_remove_button.sensitive = mixer_element_treeview_selection.count_selected_rows () > 0; + }); + + // Invoke the button logic once to initialize it + mixer_element_treeview_selection.changed (); + + // Configure the direct window link buttons + (builder.get_object ("sound-mixer-select-window-button") as Gtk.Button).clicked.connect (() => + mixer_select_window.show ()); + + (builder.get_object ("sound-volume-window-button") as Gtk.Button).clicked.connect (() => { + if (mixer_select_window.first_element == null) { + return; + } + + volume_window.current_element = mixer_select_window.first_element; + volume_window.show_element_details = !mixer_select_window.has_single_element; + volume_window.show (); + }); + + // Wire up handlers for volume window signals + weak MixerElementVolumeWindow weak_volume_window = volume_window; + volume_window.volume_up.connect (() => { + weak_volume_window.current_element.volume += VOLUME_STEP; + update_liststore_for_element (mixer_elems_liststore, weak_volume_window.current_element); + }); + + volume_window.volume_down.connect (() => { + weak_volume_window.current_element.volume -= VOLUME_STEP; + update_liststore_for_element (mixer_elems_liststore, weak_volume_window.current_element); + }); + + volume_window.mute.connect (() => { + weak_volume_window.current_element.volume = IMixerElementViewModel.MIN_VOLUME; + update_liststore_for_element (mixer_elems_liststore, weak_volume_window.current_element); + }); + + // Show volume window when mixer element is selected + mixer_select_window.mixer_element_selected.connect ((selected_element) => { + volume_window.current_element = selected_element; + volume_window.show_element_details = true; + volume_window.show (); + }); + } + + /** + * Updates the mixer element associated with the specified TreeIter object in the Control Panel + * GUI with data from the backing ListStore. Will create the mixer element if one does not + * already exist. + */ + private void update_fake_element_from_liststore (Gtk.TreeIter iter, Gtk.ListStore mixer_elems_liststore) { + var name = get_liststore_value (mixer_elems_liststore, iter, ControlPanel.SoundMixerElementsColumn.NAME); + var index = get_liststore_value (mixer_elems_liststore, iter, ControlPanel.SoundMixerElementsColumn.INDEX); + var volume = get_liststore_value (mixer_elems_liststore, iter, ControlPanel.SoundMixerElementsColumn.VOLUME); + var can_mute = get_liststore_value (mixer_elems_liststore, iter, ControlPanel.SoundMixerElementsColumn.CAN_MUTE); + var user_data = get_liststore_value (mixer_elems_liststore, iter, ControlPanel.SoundMixerElementsColumn.USER_DATA); + + // The mixer elements will make sure that these numbers are within proper bounds later + var parsed_index = (int)parse_double_with_default (index.get_string (), 0); + var parsed_volume = (int)parse_double_with_default (volume.get_string (), IMixerElementViewModel.HALF_VOLUME); + + // This is guaranteed to be a fake mixer element; as such, it is referenced by the concrete implementation name + var mixer_element = (FakeMixerElement?)user_data.get_pointer (); + + if (mixer_element == null) { + mixer_element = new FakeMixerElement (name.get_string (), parsed_index, parsed_volume, can_mute.get_boolean ()); + mixer_select_window.add_element (mixer_element); + + mixer_elems_liststore.set (iter, ControlPanel.SoundMixerElementsColumn.USER_DATA, mixer_element.ref ()); + } else { + mixer_element.freeze_notify (); + mixer_element.set_name (name.get_string ()); + mixer_element.set_index (parsed_index); + mixer_element.volume = parsed_volume; + mixer_element.set_can_mute (can_mute.get_boolean ()); + mixer_element.thaw_notify (); + + // If the original value was invalid and the mixer object modified it, replace the entered text with the valid version + // This will invoke any active signal handlers again; while not optimal, running them twice shouldn't be a problem. + if (index.get_string () != mixer_element.index.to_string () || volume.get_string () != mixer_element.volume.to_string ()) { + update_liststore_for_element (mixer_elems_liststore, mixer_element, iter); + } + } + } + + /** + * Updates the ListStore entry associated with the specified mixer element with data from + * the mixer element. + */ + private void update_liststore_for_element (Gtk.ListStore mixer_elems_liststore, IMixerElementViewModel element, Gtk.TreeIter? iter = null) { + // Find the iter pointing to this element if one was not supplied + if (iter == null) { + mixer_elems_liststore.foreach ((model, path, current_iter) => { + var user_data = get_liststore_value (mixer_elems_liststore, current_iter, ControlPanel.SoundMixerElementsColumn.USER_DATA); + var other_element = (FakeMixerElement)user_data.get_pointer (); + + if (other_element == element) { + iter = current_iter; + return true; + } + + return false; + }); + } + + mixer_elems_liststore.set_valuesv (iter, + new int[] { + ControlPanel.SoundMixerElementsColumn.NAME, + ControlPanel.SoundMixerElementsColumn.INDEX, + ControlPanel.SoundMixerElementsColumn.VOLUME, + ControlPanel.SoundMixerElementsColumn.CAN_MUTE, + ControlPanel.SoundMixerElementsColumn.MUTE + }, new Value[] { + element.name, + element.index.to_string (), + element.volume.to_string (), + element.can_mute, + element.is_muted + }); + } + + private Value get_liststore_value (Gtk.ListStore list_store, Gtk.TreeIter iter, int column) { + Value ret_value; + list_store.get_value (iter, column, out ret_value); + return ret_value; + } + + private double parse_double_with_default (string str, int default_value) { + double parsed_result; + if (double.try_parse (str, out parsed_result)) { + return parsed_result; + } + + return default_value; + } + + public void show_main_window () { + if (mixer_select_window.has_single_element) { + volume_window.current_element = mixer_select_window.first_element; + volume_window.show_element_details = false; + volume_window.show (); + } else { + mixer_select_window.show (); + } + } + } +} diff --git a/test/glade/ControlPanel.glade b/test/glade/ControlPanel.glade index 36ed074..ee09653 100644 --- a/test/glade/ControlPanel.glade +++ b/test/glade/ControlPanel.glade @@ -106,6 +106,31 @@ + + + + + + + + + + + + + + + + + + Playback + 0 + 75 + True + False + + + @@ -2854,6 +2879,239 @@ False + + + True + False + vertical + + + True + False + Mixer elements + + + False + True + 0 + + + + + True + False + + + True + True + never + never + in + + + True + True + natural + mixer-elements-liststore + + + + + + Name + + + True + + + 0 + + + + + + + Index + + + True + + + 1 + + + + + + + Volume + + + True + + + 2 + + + + + + + Can mute + + + + 3 + + + + + + + Is muted + + + 0 + False + + + 4 + + + + + + + + + True + True + 0 + + + + + True + False + vertical + + + Add + True + True + True + + + False + True + 0 + + + + + Remove + True + True + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 6 + 6 + 12 + 12 + + + True + False + + + Mixer select + True + True + True + + + False + True + 0 + + + + + Volume control + True + True + True + + + False + True + 1 + + + + + + + + + True + False + Audio windows + + + + + False + True + 2 + + + + + 2 + + + + + True + False + Sound + + + 2 + False + + True @@ -3547,7 +3805,7 @@ - 2 + 3 @@ -3557,7 +3815,7 @@ Bluetooth - 2 + 3 False @@ -3786,7 +4044,7 @@ - 3 + 4 @@ -3796,7 +4054,7 @@ Battery - 3 + 4 False @@ -4129,7 +4387,7 @@ - 4 + 5 @@ -4140,7 +4398,7 @@ right - 4 + 5 False @@ -4308,7 +4566,7 @@ - 5 + 6 @@ -4318,7 +4576,7 @@ About - 5 + 6 False diff --git a/test/main.vala b/test/main.vala index e25cb2c..972be72 100644 --- a/test/main.vala +++ b/test/main.vala @@ -51,6 +51,7 @@ namespace BrickManager { control_panel.network_controller.add_controller (control_panel.network_controller.wifi_controller); control_panel.bluetooth_controller.show_network_connection_requested.connect ((name) => control_panel.network_controller.show_connection (name)); + home_window.add_controller (control_panel.sound_controller); home_window.add_controller (control_panel.battery_controller); home_window.add_controller (control_panel.open_roberta_controller); home_window.add_controller (control_panel.about_controller); diff --git a/test/view_model/FakeMixerElement.vala b/test/view_model/FakeMixerElement.vala new file mode 100644 index 0000000..3174bae --- /dev/null +++ b/test/view_model/FakeMixerElement.vala @@ -0,0 +1,100 @@ +/* + * brickman -- Brick Manager for LEGO MINDSTORMS EV3/ev3dev + * + * Copyright (C) 2016 Kaelin Laundry + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +/* FakeMixerElement.vala - Fake implementation of mixer element */ + +namespace BrickManager { + public class FakeMixerElement: IMixerElementViewModel, Object { + private string _name; + private uint _index; + private int _volume; + private bool _can_mute; + private bool _is_muted; + + public string name { + get { + return _name; + } + } + + public uint index { + get { + return _index; + } + } + + public FakeMixerElement (string name, uint index, int volume, bool can_mute) { + set_name (name); + set_index (index); + this.volume = volume; + + set_can_mute (can_mute); + } + + public int volume { + get { + return _volume; + } + set { + _volume = int.min (100, int.max (0, value)); + + if(_can_mute) { + bool should_mute = _volume <= 0; + if (_is_muted != should_mute) { + set_is_muted (should_mute); + } + } + } + } + + public bool can_mute { + get { + return _can_mute; + } + } + + public bool is_muted { + get { + return _is_muted; + } + } + + public void set_name (string new_name) { + this._name = new_name; + notify_property ("name"); + } + + public void set_index (uint new_index) { + this._index = new_index; + notify_property ("index"); + } + + public void set_can_mute (bool can_mute) { + this._can_mute = can_mute; + notify_property ("can_mute"); + } + + public void set_is_muted (bool is_muted) { + this._is_muted = is_muted; + notify_property ("is_muted"); + } + } +}