aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/call_window
diff options
context:
space:
mode:
authorfiaxh <git@lightrise.org>2021-03-19 23:09:56 +0100
committerfiaxh <git@lightrise.org>2021-03-21 12:41:39 +0100
commit0f46facecd558786631c2ad4cf66d27331f16a86 (patch)
tree74ba0d120dabbaf55e204cca5355022f3c3ba60e /main/src/ui/call_window
parentcdb4d77259e6c361aaca64a483a43d7441f4803d (diff)
downloaddino-0f46facecd558786631c2ad4cf66d27331f16a86.tar.gz
dino-0f46facecd558786631c2ad4cf66d27331f16a86.zip
Add UI for audio/video calls
Diffstat (limited to 'main/src/ui/call_window')
-rw-r--r--main/src/ui/call_window/audio_settings_popover.vala127
-rw-r--r--main/src/ui/call_window/call_bottom_bar.vala165
-rw-r--r--main/src/ui/call_window/call_window.vala260
-rw-r--r--main/src/ui/call_window/call_window_controller.vala208
-rw-r--r--main/src/ui/call_window/video_settings_popover.vala73
5 files changed, 833 insertions, 0 deletions
diff --git a/main/src/ui/call_window/audio_settings_popover.vala b/main/src/ui/call_window/audio_settings_popover.vala
new file mode 100644
index 00000000..7d1f39b0
--- /dev/null
+++ b/main/src/ui/call_window/audio_settings_popover.vala
@@ -0,0 +1,127 @@
+using Gee;
+using Gtk;
+using Dino.Entities;
+
+public class Dino.Ui.AudioSettingsPopover : Gtk.Popover {
+
+ public signal void microphone_selected(Plugins.MediaDevice device);
+ public signal void speaker_selected(Plugins.MediaDevice device);
+
+ public Plugins.MediaDevice? current_microphone_device { get; set; }
+ public Plugins.MediaDevice? current_speaker_device { get; set; }
+
+ private HashMap<ListBoxRow, Plugins.MediaDevice> row_microphone_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
+ private HashMap<ListBoxRow, Plugins.MediaDevice> row_speaker_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
+
+ public AudioSettingsPopover() {
+ Box box = new Box(Orientation.VERTICAL, 15) { margin=18, visible=true };
+ box.add(create_microphone_box());
+ box.add(create_speaker_box());
+
+ this.add(box);
+ }
+
+ private Widget create_microphone_box() {
+ Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("audio", false);
+
+ Box micro_box = new Box(Orientation.VERTICAL, 10) { visible=true };
+ micro_box.add(new Label("<b>" + _("Microphones") + "</b>") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ });
+
+ if (devices.size == 0) {
+ micro_box.add(new Label("No microphones found."));
+ } else {
+ ListBox micro_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
+ micro_list_box.set_header_func(listbox_header_func);
+ Frame micro_frame = new Frame(null) { visible=true };
+ micro_frame.add(micro_list_box);
+ foreach (Plugins.MediaDevice device in devices) {
+ Label label = new Label(device.display_name) { xalign=0, visible=true };
+ Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
+ if (current_microphone_device == null || current_microphone_device.id != device.id) {
+ image.opacity = 0;
+ }
+ this.notify["current-microphone-device"].connect(() => {
+ if (current_microphone_device == null || current_microphone_device.id != device.id) {
+ image.opacity = 0;
+ } else {
+ image.opacity = 1;
+ }
+ });
+ Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
+ device_box.add(image);
+ device_box.add(label);
+ ListBoxRow list_box_row = new ListBoxRow() { visible=true };
+ list_box_row.add(device_box);
+ micro_list_box.add(list_box_row);
+
+ row_microphone_device[list_box_row] = device;
+ }
+ micro_list_box.row_activated.connect((row) => {
+ if (!row_microphone_device.has_key(row)) return;
+ microphone_selected(row_microphone_device[row]);
+ micro_list_box.unselect_row(row);
+ });
+ micro_box.add(micro_frame);
+ }
+
+ return micro_box;
+ }
+
+ private Widget create_speaker_box() {
+ Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("audio", true);
+
+ Box speaker_box = new Box(Orientation.VERTICAL, 10) { visible=true };
+ speaker_box.add(new Label("<b>" + _("Speakers") +"</b>") { use_markup=true, xalign=0, visible=true });
+
+ if (devices.size == 0) {
+ speaker_box.add(new Label("No speakers found."));
+ } else {
+ ListBox speaker_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
+ speaker_list_box.set_header_func(listbox_header_func);
+ speaker_list_box.row_selected.connect((row) => {
+
+ });
+ Frame speaker_frame = new Frame(null) { visible=true };
+ speaker_frame.add(speaker_list_box);
+ foreach (Plugins.MediaDevice device in devices) {
+ Label label = new Label(device.display_name) { xalign=0, visible=true };
+ Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
+ if (current_speaker_device == null || current_speaker_device.id != device.id) {
+ image.opacity = 0;
+ }
+ this.notify["current-speaker-device"].connect(() => {
+ if (current_speaker_device == null || current_speaker_device.id != device.id) {
+ image.opacity = 0;
+ } else {
+ image.opacity = 1;
+ }
+ });
+ Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
+ device_box.add(image);
+ device_box.add(label);
+ ListBoxRow list_box_row = new ListBoxRow() { visible=true };
+ list_box_row.add(device_box);
+ speaker_list_box.add(list_box_row);
+
+ row_speaker_device[list_box_row] = device;
+ }
+ speaker_list_box.row_activated.connect((row) => {
+ if (!row_speaker_device.has_key(row)) return;
+ speaker_selected(row_speaker_device[row]);
+ speaker_list_box.unselect_row(row);
+ });
+ speaker_box.add(speaker_frame);
+ }
+
+ return speaker_box;
+ }
+
+ private void listbox_header_func(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala
new file mode 100644
index 00000000..bc800485
--- /dev/null
+++ b/main/src/ui/call_window/call_bottom_bar.vala
@@ -0,0 +1,165 @@
+using Dino.Entities;
+using Gtk;
+
+public class Dino.Ui.CallBottomBar : Gtk.Box {
+
+ public signal void hang_up();
+
+ public bool audio_enabled { get; set; }
+ public bool video_enabled { get; set; }
+
+ public static IconSize ICON_SIZE_MEDIADEVICE_BUTTON = Gtk.icon_size_register("im.dino.Dino.CALL_MEDIADEVICE_BUTTON", 10, 10);
+
+ public string counterpart_display_name { get; set; }
+
+ private Button audio_button = new Button() { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
+ private Overlay audio_button_overlay = new Overlay() { visible=true };
+ private Image audio_image = new Image() { visible=true };
+ private MenuButton audio_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
+ public AudioSettingsPopover? audio_settings_popover;
+
+ private Button video_button = new Button() { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
+ private Overlay video_button_overlay = new Overlay() { visible=true };
+ private Image video_image = new Image() { visible=true };
+ private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
+ public VideoSettingsPopover? video_settings_popover;
+
+ private Label label = new Label("") { margin=20, halign=Align.CENTER, valign=Align.CENTER, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, visible=true };
+ private Stack stack = new Stack() { visible=true };
+
+ public CallBottomBar() {
+ Object(orientation:Orientation.HORIZONTAL, spacing:0);
+
+ Overlay default_control = new Overlay() { visible=true };
+ Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END, visible=true };
+ encryption_image.tooltip_text = _("Unencrypted");
+ encryption_image.get_style_context().add_class("unencrypted-box");
+
+ default_control.add_overlay(encryption_image);
+
+ Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true };
+
+ audio_button.add(audio_image);
+ audio_button.get_style_context().add_class("call-button");
+ audio_button.clicked.connect(() => { audio_enabled = !audio_enabled; });
+ audio_button.margin_end = audio_button.margin_bottom = 5; // space for the small settings button
+ audio_button_overlay.add(audio_button);
+ audio_button_overlay.add_overlay(audio_settings_button);
+ audio_settings_button.set_image(new Image.from_icon_name("go-up-symbolic", ICON_SIZE_MEDIADEVICE_BUTTON) { visible=true });
+ audio_settings_button.get_style_context().add_class("call-mediadevice-settings-button");
+ audio_settings_button.use_popover = true;
+ main_buttons.add(audio_button_overlay);
+
+ video_button.add(video_image);
+ video_button.get_style_context().add_class("call-button");
+ video_button.clicked.connect(() => { video_enabled = !video_enabled; });
+ video_button.margin_end = video_button.margin_bottom = 5;
+ video_button_overlay.add(video_button);
+ video_button_overlay.add_overlay(video_settings_button);
+ video_settings_button.set_image(new Image.from_icon_name("go-up-symbolic", ICON_SIZE_MEDIADEVICE_BUTTON) { visible=true });
+ video_settings_button.get_style_context().add_class("call-mediadevice-settings-button");
+ video_settings_button.use_popover = true;
+ main_buttons.add(video_button_overlay);
+
+ Button button_hang = new Button.from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR) { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
+ button_hang.get_style_context().add_class("call-button");
+ button_hang.get_style_context().add_class("destructive-action");
+ button_hang.clicked.connect(() => hang_up());
+ main_buttons.add(button_hang);
+
+ default_control.add(main_buttons);
+
+ label.get_style_context().add_class("text-no-controls");
+
+ stack.add_named(default_control, "control-buttons");
+ stack.add_named(label, "label");
+ this.add(stack);
+
+ this.notify["audio-enabled"].connect(on_audio_enabled_changed);
+ this.notify["video-enabled"].connect(on_video_enabled_changed);
+
+ audio_enabled = true;
+ video_enabled = false;
+
+ on_audio_enabled_changed();
+ on_video_enabled_changed();
+
+ this.get_style_context().add_class("call-bottom-bar");
+ }
+
+ public AudioSettingsPopover? show_audio_device_choices(bool show) {
+ audio_settings_button.visible = show;
+ if (audio_settings_popover != null) audio_settings_popover.visible = false;
+ if (!show) return null;
+
+ audio_settings_popover = new AudioSettingsPopover();
+
+ audio_settings_button.popover = audio_settings_popover;
+
+ audio_settings_popover.set_relative_to(audio_settings_button);
+ audio_settings_popover.microphone_selected.connect(() => { audio_settings_button.active = false; });
+ audio_settings_popover.speaker_selected.connect(() => { audio_settings_button.active = false; });
+
+ return audio_settings_popover;
+ }
+
+ public void show_audio_device_error() {
+ audio_settings_button.set_image(new Image.from_icon_name("dialog-warning-symbolic", IconSize.BUTTON) { visible=true });
+ Util.force_error_color(audio_settings_button);
+ }
+
+ public VideoSettingsPopover? show_video_device_choices(bool show) {
+ video_settings_button.visible = show;
+ if (video_settings_popover != null) video_settings_popover.visible = false;
+ if (!show) return null;
+
+ video_settings_popover = new VideoSettingsPopover();
+
+
+ video_settings_button.popover = video_settings_popover;
+
+ video_settings_popover.set_relative_to(video_settings_button);
+ video_settings_popover.camera_selected.connect(() => { video_settings_button.active = false; });
+
+ return video_settings_popover;
+ }
+
+ public void show_video_device_error() {
+ video_settings_button.set_image(new Image.from_icon_name("dialog-warning-symbolic", IconSize.BUTTON) { visible=true });
+ Util.force_error_color(video_settings_button);
+ }
+
+ public void on_audio_enabled_changed() {
+ if (audio_enabled) {
+ audio_image.set_from_icon_name("dino-microphone-symbolic", IconSize.LARGE_TOOLBAR);
+ audio_button.get_style_context().add_class("white-button");
+ audio_button.get_style_context().remove_class("transparent-white-button");
+ } else {
+ audio_image.set_from_icon_name("dino-microphone-off-symbolic", IconSize.LARGE_TOOLBAR);
+ audio_button.get_style_context().remove_class("white-button");
+ audio_button.get_style_context().add_class("transparent-white-button");
+ }
+ }
+
+ public void on_video_enabled_changed() {
+ if (video_enabled) {
+ video_image.set_from_icon_name("dino-video-symbolic", IconSize.LARGE_TOOLBAR);
+ video_button.get_style_context().add_class("white-button");
+ video_button.get_style_context().remove_class("transparent-white-button");
+
+ } else {
+ video_image.set_from_icon_name("dino-video-off-symbolic", IconSize.LARGE_TOOLBAR);
+ video_button.get_style_context().remove_class("white-button");
+ video_button.get_style_context().add_class("transparent-white-button");
+ }
+ }
+
+ public void show_counterpart_ended(string text) {
+ stack.set_visible_child_name("label");
+ label.label = text;
+ }
+
+ public bool is_menu_active() {
+ return video_settings_button.active || audio_settings_button.active;
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala
new file mode 100644
index 00000000..572f73b6
--- /dev/null
+++ b/main/src/ui/call_window/call_window.vala
@@ -0,0 +1,260 @@
+using Dino.Entities;
+using Gtk;
+
+namespace Dino.Ui {
+
+ public class CallWindow : Gtk.Window {
+ public string counterpart_display_name { get; set; }
+
+ // TODO should find another place for this
+ public CallWindowController controller;
+
+ public Overlay overlay = new Overlay() { visible=true };
+ public EventBox event_box = new EventBox() { visible=true };
+ public CallBottomBar bottom_bar = new CallBottomBar() { visible=true };
+ public Revealer bottom_bar_revealer = new Revealer() { valign=Align.END, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
+ public HeaderBar header_bar = new HeaderBar() { show_close_button=true, visible=true };
+ public Revealer header_bar_revealer = new Revealer() { valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
+ public Stack stack = new Stack() { visible=true };
+ public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { expand=true, visible=true };
+ private Widget? own_video = null;
+ private Box? own_video_border = new Box(Orientation.HORIZONTAL, 0) { expand=true }; // hack to draw a border around our own video, since we apparently can't draw a border around the Gst widget
+
+ private int own_video_width = 150;
+ private int own_video_height = 100;
+
+ private bool hide_controll_elements = false;
+ private uint hide_controll_handler = 0;
+ private Widget? main_widget = null;
+
+ construct {
+ header_bar.get_style_context().add_class("call-header-bar");
+ header_bar_revealer.add(header_bar);
+
+ this.get_style_context().add_class("dino-call-window");
+
+ bottom_bar_revealer.add(bottom_bar);
+
+ overlay.add_overlay(own_video_box);
+ overlay.add_overlay(own_video_border);
+ overlay.add_overlay(bottom_bar_revealer);
+ overlay.add_overlay(header_bar_revealer);
+
+ event_box.add(overlay);
+ add(event_box);
+
+ Util.force_css(own_video_border, "* { border: 1px solid #616161; background-color: transparent; }");
+ }
+
+ public CallWindow() {
+ event_box.events |= Gdk.EventMask.POINTER_MOTION_MASK;
+ event_box.events |= Gdk.EventMask.ENTER_NOTIFY_MASK;
+ event_box.events |= Gdk.EventMask.LEAVE_NOTIFY_MASK;
+
+ this.bind_property("counterpart-display-name", header_bar, "title", BindingFlags.SYNC_CREATE);
+ this.bind_property("counterpart-display-name", bottom_bar, "counterpart-display-name", BindingFlags.SYNC_CREATE);
+
+ event_box.motion_notify_event.connect(reveal_control_elements);
+ event_box.enter_notify_event.connect(reveal_control_elements);
+ event_box.leave_notify_event.connect(reveal_control_elements);
+ this.configure_event.connect(reveal_control_elements); // upon resizing
+ this.configure_event.connect(update_own_video_position);
+
+ this.set_titlebar(new OutsideHeaderBar(this.header_bar) { visible=true });
+
+ reveal_control_elements();
+ }
+
+ public void set_video_fallback(StreamInteractor stream_interactor, Conversation conversation) {
+ hide_controll_elements = false;
+
+ Box box = new Box(Orientation.HORIZONTAL, 0) { visible=true };
+ box.get_style_context().add_class("video-placeholder-box");
+ AvatarImage avatar = new AvatarImage() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100, visible=true };
+ avatar.set_conversation(stream_interactor, conversation);
+ box.add(avatar);
+
+ set_new_main_widget(box);
+ }
+
+ public void set_video(Widget widget) {
+ hide_controll_elements = true;
+
+ widget.visible = true;
+ set_new_main_widget(widget);
+ }
+
+ public void set_own_video(Widget? widget_) {
+ own_video_box.foreach((widget) => { own_video_box.remove(widget); });
+
+ own_video = widget_;
+ if (own_video == null) {
+ own_video = new Box(Orientation.HORIZONTAL, 0) { expand=true };
+ }
+ own_video.visible = true;
+ own_video.width_request = 150;
+ own_video.height_request = 100;
+ own_video_box.add(own_video);
+
+ own_video_border.visible = true;
+
+ update_own_video_position();
+ }
+
+ public void set_own_video_ratio(int width, int height) {
+ if (width / height > 150 / 100) {
+ this.own_video_width = 150;
+ this.own_video_height = height * 150 / width;
+ } else {
+ this.own_video_width = width * 100 / height;
+ this.own_video_height = 100;
+ }
+
+ own_video.width_request = own_video_width;
+ own_video.height_request = own_video_height;
+
+ update_own_video_position();
+ }
+
+ public void unset_own_video() {
+ own_video_box.foreach((widget) => { own_video_box.remove(widget); });
+
+ own_video_border.visible = false;
+ }
+
+ public void set_test_video() {
+ hide_controll_elements = true;
+
+ var pipeline = new Gst.Pipeline(null);
+ var src = Gst.ElementFactory.make("videotestsrc", null);
+ pipeline.add(src);
+ Gst.Video.Sink sink = (Gst.Video.Sink) Gst.ElementFactory.make("gtksink", null);
+ Gtk.Widget widget;
+ sink.get("widget", out widget);
+ widget.unparent();
+ pipeline.add(sink);
+ src.link(sink);
+ widget.visible = true;
+
+ pipeline.set_state(Gst.State.PLAYING);
+
+ sink.get_static_pad("sink").notify["caps"].connect(() => {
+ int width, height;
+ sink.get_static_pad("sink").caps.get_structure(0).get_int("width", out width);
+ sink.get_static_pad("sink").caps.get_structure(0).get_int("height", out height);
+ widget.width_request = width;
+ widget.height_request = height;
+ });
+
+ set_new_main_widget(widget);
+ }
+
+ private void set_new_main_widget(Widget widget) {
+ if (main_widget != null) overlay.remove(main_widget);
+ overlay.add(widget);
+ main_widget = widget;
+ }
+
+ public void set_status(string state) {
+ switch (state) {
+ case "requested":
+ header_bar.subtitle = _("Sending a call request…");
+ break;
+ case "ringing":
+ header_bar.subtitle = _("Ringing…");
+ break;
+ case "establishing":
+ header_bar.subtitle = _("Establishing a (peer-to-peer) connection…");
+ break;
+ default:
+ header_bar.subtitle = null;
+ break;
+ }
+ }
+
+ public void show_counterpart_ended(string? reason_name, string? reason_text) {
+ hide_controll_elements = false;
+ reveal_control_elements();
+
+ string text = "";
+ if (reason_name == Xmpp.Xep.Jingle.ReasonElement.SUCCESS) {
+ text = _("%s ended the call").printf(counterpart_display_name);
+ } else if (reason_name == Xmpp.Xep.Jingle.ReasonElement.DECLINE || reason_name == Xmpp.Xep.Jingle.ReasonElement.BUSY) {
+ text = _("%s declined the call").printf(counterpart_display_name);
+ } else {
+ text = "The call has been terminated: " + (reason_name ?? "") + " " + (reason_text ?? "");
+ }
+
+ bottom_bar.show_counterpart_ended(text);
+ }
+
+ public bool reveal_control_elements() {
+ if (!bottom_bar_revealer.child_revealed) {
+ bottom_bar_revealer.set_reveal_child(true);
+ header_bar_revealer.set_reveal_child(true);
+ }
+
+ if (hide_controll_handler != 0) {
+ Source.remove(hide_controll_handler);
+ hide_controll_handler = 0;
+ }
+
+ if (!hide_controll_elements) {
+ return false;
+ }
+
+ hide_controll_handler = Timeout.add_seconds(3, () => {
+ if (!hide_controll_elements) {
+ return false;
+ }
+
+ if (bottom_bar.is_menu_active()) {
+ return true;
+ }
+
+ header_bar_revealer.set_reveal_child(false);
+ bottom_bar_revealer.set_reveal_child(false);
+ hide_controll_handler = 0;
+ return false;
+ });
+ return false;
+ }
+
+ private bool update_own_video_position() {
+ if (own_video == null) return false;
+
+ int width, height;
+ this.get_size(out width,out height);
+
+ own_video.margin_end = own_video.margin_bottom = own_video_border.margin_end = own_video_border.margin_bottom = 20;
+ own_video.margin_start = own_video_border.margin_start = width - own_video_width - 20;
+ own_video.margin_top = own_video_border.margin_top = height - own_video_height - 20;
+
+ return false;
+ }
+ }
+
+ /* Hack to make the CallHeaderBar feel like a HeaderBar (right click menu, double click, ..) although it isn't set as headerbar.
+ * OutsideHeaderBar is set as a headerbar and it doesn't take any space, but claims to take space (which is actually taken by CallHeaderBar).
+ */
+ public class OutsideHeaderBar : Gtk.Box {
+ HeaderBar header_bar;
+
+ public OutsideHeaderBar(HeaderBar header_bar) {
+ this.header_bar = header_bar;
+
+ size_allocate.connect_after(on_header_bar_size_allocate);
+ header_bar.size_allocate.connect(on_header_bar_size_allocate);
+ }
+
+ public void on_header_bar_size_allocate() {
+ Allocation header_bar_alloc;
+ header_bar.get_allocation(out header_bar_alloc);
+
+ Allocation alloc;
+ get_allocation(out alloc);
+ alloc.height = header_bar_alloc.height;
+ set_allocation(alloc);
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala
new file mode 100644
index 00000000..09c8f88c
--- /dev/null
+++ b/main/src/ui/call_window/call_window_controller.vala
@@ -0,0 +1,208 @@
+using Dino.Entities;
+using Gtk;
+
+public class Dino.Ui.CallWindowController : Object {
+
+ public signal void terminated();
+
+ private CallWindow call_window;
+ private Call call;
+ private Conversation conversation;
+ private StreamInteractor stream_interactor;
+ private Calls calls;
+ private Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+
+ private Plugins.VideoCallWidget? own_video = null;
+ private Plugins.VideoCallWidget? counterpart_video = null;
+
+ public CallWindowController(CallWindow call_window, Call call, StreamInteractor stream_interactor) {
+ this.call_window = call_window;
+ this.call = call;
+ this.stream_interactor = stream_interactor;
+
+ this.calls = stream_interactor.get_module(Calls.IDENTITY);
+ this.conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(call.counterpart.bare_jid, call.account, Conversation.Type.CHAT);
+ this.own_video = call_plugin.create_widget(Plugins.WidgetType.GTK);
+ this.counterpart_video = call_plugin.create_widget(Plugins.WidgetType.GTK);
+
+ call_window.counterpart_display_name = Util.get_conversation_display_name(stream_interactor, conversation);
+ call_window.set_default_size(640, 480);
+ call_window.set_video_fallback(stream_interactor, conversation);
+
+ this.call_window.bottom_bar.video_enabled = calls.should_we_send_video(call);
+
+ if (call.direction == Call.DIRECTION_INCOMING) {
+ call_window.set_status("establishing");
+ } else {
+ call_window.set_status("requested");
+ }
+
+ call_window.bottom_bar.hang_up.connect(end_call);
+ call_window.destroy.connect(end_call);
+
+ call_window.bottom_bar.notify["audio-enabled"].connect(() => {
+ calls.mute_own_audio(call, !call_window.bottom_bar.audio_enabled);
+ });
+ call_window.bottom_bar.notify["video-enabled"].connect(() => {
+ calls.mute_own_video(call, !call_window.bottom_bar.video_enabled);
+ update_own_video();
+ });
+
+ calls.counterpart_sends_video_updated.connect((call, mute) => {
+ if (!this.call.equals(call)) return;
+
+ if (mute) {
+ call_window.set_video_fallback(stream_interactor, conversation);
+ counterpart_video.detach();
+ } else {
+ if (!(counterpart_video is Widget)) return;
+ Widget widget = (Widget) counterpart_video;
+ call_window.set_video(widget);
+ counterpart_video.display_stream(calls.get_video_stream(call));
+ }
+ });
+ calls.info_received.connect((call, session_info) => {
+ if (!this.call.equals(call)) return;
+ if (session_info == Xmpp.Xep.JingleRtp.CallSessionInfo.RINGING) {
+ call_window.set_status("ringing");
+ }
+ });
+
+ own_video.resolution_changed.connect((width, height) => {
+ if (width == 0 || height == 0) return;
+ call_window.set_own_video_ratio((int)width, (int)height);
+ });
+ counterpart_video.resolution_changed.connect((width, height) => {
+ if (width == 0 || height == 0) return;
+ if (width / height > 640 / 480) {
+ call_window.resize(640, (int) (height * 640 / width));
+ } else {
+ call_window.resize((int) (width * 480 / height), 480);
+ }
+ });
+
+ call.notify["state"].connect(on_call_state_changed);
+ calls.call_terminated.connect(on_call_terminated);
+
+ update_own_video();
+ }
+
+ private void end_call() {
+ call.notify["state"].disconnect(on_call_state_changed);
+ calls.call_terminated.disconnect(on_call_terminated);
+
+ calls.end_call(conversation, call);
+ call_window.close();
+ call_window.destroy();
+ terminated();
+ }
+
+ private void on_call_state_changed() {
+ if (call.state == Call.State.IN_PROGRESS) {
+ call_window.set_status("");
+ call_plugin.devices_changed.connect((media, incoming) => {
+ if (media == "audio") update_audio_device_choices();
+ if (media == "video") update_video_device_choices();
+ });
+
+ update_audio_device_choices();
+ update_video_device_choices();
+ }
+ }
+
+ private void on_call_terminated(Call call, string? reason_name, string? reason_text) {
+ call_window.show_counterpart_ended(reason_name, reason_text);
+ Timeout.add_seconds(3, () => {
+ call.notify["state"].disconnect(on_call_state_changed);
+ calls.call_terminated.disconnect(on_call_terminated);
+
+
+ call_window.close();
+ call_window.destroy();
+
+ return false;
+ });
+ }
+
+ private void update_audio_device_choices() {
+ if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) {
+ call_window.bottom_bar.show_audio_device_error();
+ } /*else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) {
+ call_window.bottom_bar.show_audio_device_choices(false);
+ return;
+ }
+
+ AudioSettingsPopover? audio_settings_popover = call_window.bottom_bar.show_audio_device_choices(true);
+ update_current_audio_device(audio_settings_popover);
+
+ audio_settings_popover.microphone_selected.connect((device) => {
+ call_plugin.set_device(calls.get_audio_stream(call), device);
+ update_current_audio_device(audio_settings_popover);
+ });
+ audio_settings_popover.speaker_selected.connect((device) => {
+ call_plugin.set_device(calls.get_audio_stream(call), device);
+ update_current_audio_device(audio_settings_popover);
+ });
+ calls.stream_created.connect((call, media) => {
+ if (media == "audio") {
+ update_current_audio_device(audio_settings_popover);
+ }
+ });*/
+ }
+
+ private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) {
+ Xmpp.Xep.JingleRtp.Stream stream = calls.get_audio_stream(call);
+ if (stream != null) {
+ audio_settings_popover.current_microphone_device = call_plugin.get_device(stream, false);
+ audio_settings_popover.current_speaker_device = call_plugin.get_device(stream, true);
+ }
+ }
+
+ private void update_video_device_choices() {
+ int device_count = call_plugin.get_devices("video", false).size;
+
+ if (device_count == 0) {
+ call_window.bottom_bar.show_video_device_error();
+ } /*else if (device_count == 1 || calls.get_video_stream(call) == null) {
+ call_window.bottom_bar.show_video_device_choices(false);
+ return;
+ }
+
+ VideoSettingsPopover? video_settings_popover = call_window.bottom_bar.show_video_device_choices(true);
+ update_current_video_device(video_settings_popover);
+
+ video_settings_popover.camera_selected.connect((device) => {
+ call_plugin.set_device(calls.get_video_stream(call), device);
+ update_current_video_device(video_settings_popover);
+ own_video.display_device(device);
+ });
+ calls.stream_created.connect((call, media) => {
+ if (media == "video") {
+ update_current_video_device(video_settings_popover);
+ }
+ });*/
+ }
+
+ private void update_current_video_device(VideoSettingsPopover video_settings_popover) {
+ Xmpp.Xep.JingleRtp.Stream stream = calls.get_video_stream(call);
+ if (stream != null) {
+ video_settings_popover.current_device = call_plugin.get_device(stream, false);
+ }
+ }
+
+ private void update_own_video() {
+ if (this.call_window.bottom_bar.video_enabled) {
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("video", false);
+ if (!(own_video is Widget) || devices.is_empty) {
+ call_window.set_own_video(null);
+ } else {
+ Widget widget = (Widget) own_video;
+ call_window.set_own_video(widget);
+ own_video.display_device(devices.first());
+ }
+ } else {
+ own_video.detach();
+ call_window.unset_own_video();
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/video_settings_popover.vala b/main/src/ui/call_window/video_settings_popover.vala
new file mode 100644
index 00000000..396c697c
--- /dev/null
+++ b/main/src/ui/call_window/video_settings_popover.vala
@@ -0,0 +1,73 @@
+using Gee;
+using Gtk;
+using Dino.Entities;
+
+public class Dino.Ui.VideoSettingsPopover : Gtk.Popover {
+
+ public signal void camera_selected(Plugins.MediaDevice device);
+
+ public Plugins.MediaDevice? current_device { get; set; }
+
+ private HashMap<ListBoxRow, Plugins.MediaDevice> row_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
+
+ public VideoSettingsPopover() {
+ Box box = new Box(Orientation.VERTICAL, 15) { margin=18, visible=true };
+ box.add(create_camera_box());
+
+ this.add(box);
+ }
+
+ private Widget create_camera_box() {
+ Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("video", false);
+
+ Box camera_box = new Box(Orientation.VERTICAL, 10) { visible=true };
+ camera_box.add(new Label("<b>" + _("Cameras") + "</b>") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ });
+
+ if (devices.size == 0) {
+ camera_box.add(new Label("No cameras found.") { visible=true });
+ } else {
+ ListBox list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
+ list_box.set_header_func(listbox_header_func);
+ Frame frame = new Frame(null) { visible=true };
+ frame.add(list_box);
+ foreach (Plugins.MediaDevice device in devices) {
+ Label label = new Label(device.display_name) { xalign=0, visible=true };
+ Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
+ if (current_device == null || current_device.id != device.id) {
+ image.opacity = 0;
+ }
+ this.notify["current-device"].connect(() => {
+ if (current_device == null || current_device.id != device.id) {
+ image.opacity = 0;
+ } else {
+ image.opacity = 1;
+ }
+ });
+ Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
+ device_box.add(image);
+ device_box.add(label);
+ ListBoxRow list_box_row = new ListBoxRow() { visible=true };
+ list_box_row.add(device_box);
+ list_box.add(list_box_row);
+
+ row_device[list_box_row] = device;
+ }
+ list_box.row_activated.connect((row) => {
+ if (!row_device.has_key(row)) return;
+ camera_selected(row_device[row]);
+ list_box.unselect_row(row);
+ });
+ camera_box.add(frame);
+ }
+
+ return camera_box;
+ }
+
+ private void listbox_header_func(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+} \ No newline at end of file