ui(launcher): mirror upstream preview and stop disabled media tests
This commit is contained in:
parent
a305746a0e
commit
6d0387ace7
Binary file not shown.
|
Before Width: | Height: | Size: 505 KiB After Width: | Height: | Size: 505 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 496 KiB |
BIN
client/assets/placeholders/webcam_disabled.png
Normal file
BIN
client/assets/placeholders/webcam_disabled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 KiB |
@ -74,10 +74,15 @@ impl DeviceTestController {
|
|||||||
camera_picture: >k::Picture,
|
camera_picture: >k::Picture,
|
||||||
camera_status: >k::Label,
|
camera_status: >k::Label,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let mirrored = self
|
||||||
|
.camera
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(LocalCameraPreview::mirrored);
|
||||||
if let Some(camera) = self.camera.as_mut() {
|
if let Some(camera) = self.camera.as_mut() {
|
||||||
camera.stop();
|
camera.stop();
|
||||||
}
|
}
|
||||||
let mut preview = LocalCameraPreview::new(camera_picture, camera_status);
|
let mut preview = LocalCameraPreview::new(camera_picture, camera_status);
|
||||||
|
preview.set_mirrored(mirrored);
|
||||||
preview.set_selected(self.selected_camera.as_deref())?;
|
preview.set_selected(self.selected_camera.as_deref())?;
|
||||||
preview.set_selected_mode(self.selected_camera_mode)?;
|
preview.set_selected_mode(self.selected_camera_mode)?;
|
||||||
self.camera = Some(preview);
|
self.camera = Some(preview);
|
||||||
@ -129,6 +134,24 @@ impl DeviceTestController {
|
|||||||
preview.toggle()
|
preview.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stop_camera_preview(&mut self) {
|
||||||
|
if let Some(camera) = self.camera.as_mut() {
|
||||||
|
camera.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_camera_preview_mirrored(&mut self, mirrored: bool) {
|
||||||
|
if let Some(camera) = self.camera.as_mut() {
|
||||||
|
camera.set_mirrored(mirrored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn camera_preview_mirrored(&self) -> bool {
|
||||||
|
self.camera
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(LocalCameraPreview::mirrored)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_microphone(&mut self, source: Option<&str>, sink: Option<&str>) -> Result<bool> {
|
pub fn toggle_microphone(&mut self, source: Option<&str>, sink: Option<&str>) -> Result<bool> {
|
||||||
self.cleanup_finished();
|
self.cleanup_finished();
|
||||||
if self.microphone.is_some() {
|
if self.microphone.is_some() {
|
||||||
@ -146,6 +169,10 @@ impl DeviceTestController {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stop_microphone_monitor(&mut self) {
|
||||||
|
self.stop(DeviceTestKind::Microphone);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stop_local_capture_for_relay(&mut self) {
|
pub fn stop_local_capture_for_relay(&mut self) {
|
||||||
if self
|
if self
|
||||||
.camera
|
.camera
|
||||||
@ -215,6 +242,12 @@ impl DeviceTestController {
|
|||||||
self.toggle_child(DeviceTestKind::Speaker, build_speaker_test(sink))
|
self.toggle_child(DeviceTestKind::Speaker, build_speaker_test(sink))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stop_speaker_test(&mut self) {
|
||||||
|
if self.speaker.is_some() {
|
||||||
|
self.stop(DeviceTestKind::Speaker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_microphone_replay(&mut self, sink: Option<&str>) -> Result<bool> {
|
pub fn toggle_microphone_replay(&mut self, sink: Option<&str>) -> Result<bool> {
|
||||||
self.cleanup_finished();
|
self.cleanup_finished();
|
||||||
if self.microphone_replay.is_some() {
|
if self.microphone_replay.is_some() {
|
||||||
@ -231,6 +264,12 @@ impl DeviceTestController {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stop_microphone_replay(&mut self) {
|
||||||
|
if self.microphone_replay.is_some() {
|
||||||
|
self.stop(DeviceTestKind::MicrophoneReplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn microphone_level_fraction(&mut self) -> f64 {
|
pub fn microphone_level_fraction(&mut self) -> f64 {
|
||||||
self.cleanup_finished();
|
self.cleanup_finished();
|
||||||
self.microphone_level
|
self.microphone_level
|
||||||
@ -370,10 +409,12 @@ impl DeviceTestController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct LocalCameraPreview {
|
struct LocalCameraPreview {
|
||||||
|
picture: gtk::Picture,
|
||||||
latest: Arc<Mutex<Option<PreviewFrame>>>,
|
latest: Arc<Mutex<Option<PreviewFrame>>>,
|
||||||
status_text: Arc<Mutex<String>>,
|
status_text: Arc<Mutex<String>>,
|
||||||
generation: Arc<AtomicU64>,
|
generation: Arc<AtomicU64>,
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
|
mirrored: Arc<AtomicBool>,
|
||||||
selected_device: Option<String>,
|
selected_device: Option<String>,
|
||||||
selected_mode: Option<CameraMode>,
|
selected_mode: Option<CameraMode>,
|
||||||
relay_preview_path: Option<PathBuf>,
|
relay_preview_path: Option<PathBuf>,
|
||||||
|
|||||||
@ -4,17 +4,22 @@ impl LocalCameraPreview {
|
|||||||
let status_text = Arc::new(Mutex::new(CAMERA_PREVIEW_IDLE.to_string()));
|
let status_text = Arc::new(Mutex::new(CAMERA_PREVIEW_IDLE.to_string()));
|
||||||
let generation = Arc::new(AtomicU64::new(0));
|
let generation = Arc::new(AtomicU64::new(0));
|
||||||
let running = Arc::new(AtomicBool::new(false));
|
let running = Arc::new(AtomicBool::new(false));
|
||||||
|
let mirrored = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
picture.set_paintable(Some(&blank_camera_preview_texture()));
|
picture.set_paintable(Some(&camera_preview_placeholder_texture()));
|
||||||
|
|
||||||
{
|
{
|
||||||
let picture = picture.clone();
|
let picture = picture.clone();
|
||||||
let status_label = status_label.clone();
|
let status_label = status_label.clone();
|
||||||
let latest = Arc::clone(&latest);
|
let latest = Arc::clone(&latest);
|
||||||
let status_text = Arc::clone(&status_text);
|
let status_text = Arc::clone(&status_text);
|
||||||
|
let mirrored = Arc::clone(&mirrored);
|
||||||
glib::timeout_add_local(Duration::from_millis(120), move || {
|
glib::timeout_add_local(Duration::from_millis(120), move || {
|
||||||
let next = latest.lock().ok().and_then(|mut slot| slot.take());
|
let next = latest.lock().ok().and_then(|mut slot| slot.take());
|
||||||
if let Some(frame) = next {
|
if let Some(mut frame) = next {
|
||||||
|
if mirrored.load(Ordering::Acquire) {
|
||||||
|
mirror_preview_frame(&mut frame);
|
||||||
|
}
|
||||||
let bytes = glib::Bytes::from_owned(frame.rgba);
|
let bytes = glib::Bytes::from_owned(frame.rgba);
|
||||||
let texture = gdk::MemoryTexture::new(
|
let texture = gdk::MemoryTexture::new(
|
||||||
frame.width,
|
frame.width,
|
||||||
@ -33,10 +38,12 @@ impl LocalCameraPreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
picture: picture.clone(),
|
||||||
latest,
|
latest,
|
||||||
status_text,
|
status_text,
|
||||||
generation,
|
generation,
|
||||||
running,
|
running,
|
||||||
|
mirrored,
|
||||||
selected_device: None,
|
selected_device: None,
|
||||||
selected_mode: None,
|
selected_mode: None,
|
||||||
relay_preview_path: None,
|
relay_preview_path: None,
|
||||||
@ -55,6 +62,14 @@ impl LocalCameraPreview {
|
|||||||
self.is_running() && self.relay_preview_path.is_some()
|
self.is_running() && self.relay_preview_path.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_mirrored(&mut self, mirrored: bool) {
|
||||||
|
self.mirrored.store(mirrored, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirrored(&self) -> bool {
|
||||||
|
self.mirrored.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
|
||||||
fn set_selected(&mut self, camera: Option<&str>) -> Result<()> {
|
fn set_selected(&mut self, camera: Option<&str>) -> Result<()> {
|
||||||
self.selected_device = normalize_camera_selection(camera);
|
self.selected_device = normalize_camera_selection(camera);
|
||||||
|
|
||||||
@ -186,6 +201,8 @@ impl LocalCameraPreview {
|
|||||||
if let Ok(mut latest) = self.latest.lock() {
|
if let Ok(mut latest) = self.latest.lock() {
|
||||||
*latest = None;
|
*latest = None;
|
||||||
}
|
}
|
||||||
|
self.picture
|
||||||
|
.set_paintable(Some(&camera_preview_placeholder_texture()));
|
||||||
let message = if was_relay_file {
|
let message = if was_relay_file {
|
||||||
"Relay webcam preview stopped.".to_string()
|
"Relay webcam preview stopped.".to_string()
|
||||||
} else {
|
} else {
|
||||||
@ -212,17 +229,41 @@ impl LocalCameraPreview {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blank_camera_preview_texture() -> gdk::MemoryTexture {
|
fn camera_preview_placeholder_texture() -> gdk::Texture {
|
||||||
let rgba =
|
let path = format!(
|
||||||
vec![12_u8; (CAMERA_PREVIEW_DEFAULT_WIDTH * CAMERA_PREVIEW_DEFAULT_HEIGHT * 4) as usize];
|
"{}/assets/placeholders/webcam_disabled.png",
|
||||||
let bytes = glib::Bytes::from_owned(rgba);
|
env!("CARGO_MANIFEST_DIR")
|
||||||
gdk::MemoryTexture::new(
|
);
|
||||||
CAMERA_PREVIEW_DEFAULT_WIDTH,
|
gdk::Texture::from_filename(path).unwrap_or_else(|_| {
|
||||||
CAMERA_PREVIEW_DEFAULT_HEIGHT,
|
let rgba = vec![
|
||||||
gdk::MemoryFormat::R8g8b8a8,
|
12_u8;
|
||||||
&bytes,
|
(CAMERA_PREVIEW_DEFAULT_WIDTH * CAMERA_PREVIEW_DEFAULT_HEIGHT * 4) as usize
|
||||||
(CAMERA_PREVIEW_DEFAULT_WIDTH * 4) as usize,
|
];
|
||||||
)
|
let bytes = glib::Bytes::from_owned(rgba);
|
||||||
|
glib::object::Cast::upcast(gdk::MemoryTexture::new(
|
||||||
|
CAMERA_PREVIEW_DEFAULT_WIDTH,
|
||||||
|
CAMERA_PREVIEW_DEFAULT_HEIGHT,
|
||||||
|
gdk::MemoryFormat::R8g8b8a8,
|
||||||
|
&bytes,
|
||||||
|
(CAMERA_PREVIEW_DEFAULT_WIDTH * 4) as usize,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirror_preview_frame(frame: &mut PreviewFrame) {
|
||||||
|
let width = frame.width.max(0) as usize;
|
||||||
|
if width == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for row in frame.rgba.chunks_exact_mut(frame.stride) {
|
||||||
|
let row = &mut row[..width * 4];
|
||||||
|
for left_pixel in 0..(width / 2) {
|
||||||
|
let right_pixel = width - 1 - left_pixel;
|
||||||
|
for channel in 0..4 {
|
||||||
|
row.swap(left_pixel * 4 + channel, right_pixel * 4 + channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalMicrophoneMonitor {
|
impl LocalMicrophoneMonitor {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use super::{
|
use super::{
|
||||||
MIC_REPLAY_MAX_BYTES, build_wav_bytes, camera_preview_mode, camera_preview_pipeline_desc,
|
MIC_REPLAY_MAX_BYTES, PreviewFrame, build_wav_bytes, camera_preview_mode,
|
||||||
microphone_monitor_pipeline_desc, normalize_camera_selection, push_recent_audio,
|
camera_preview_pipeline_desc, microphone_monitor_pipeline_desc, mirror_preview_frame,
|
||||||
read_camera_preview_tap, read_microphone_level_tap, resolve_camera_device,
|
normalize_camera_selection, push_recent_audio, read_camera_preview_tap,
|
||||||
|
read_microphone_level_tap, resolve_camera_device,
|
||||||
};
|
};
|
||||||
use crate::launcher::devices::CameraMode;
|
use crate::launcher::devices::CameraMode;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@ -114,3 +115,15 @@ fn relay_microphone_level_tap_clamps_values() {
|
|||||||
assert_eq!(read_microphone_level_tap(&path), None);
|
assert_eq!(read_microphone_level_tap(&path), None);
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mirror_preview_frame_flips_each_row_without_changing_channels() {
|
||||||
|
let mut frame = PreviewFrame {
|
||||||
|
width: 2,
|
||||||
|
height: 1,
|
||||||
|
stride: 8,
|
||||||
|
rgba: vec![1, 2, 3, 4, 9, 10, 11, 12],
|
||||||
|
};
|
||||||
|
mirror_preview_frame(&mut frame);
|
||||||
|
assert_eq!(frame.rgba, vec![9, 10, 11, 12, 1, 2, 3, 4]);
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let widgets = widgets.clone();
|
||||||
|
let tests = Rc::clone(&tests);
|
||||||
|
let camera_mirror_button = widgets.camera_mirror_button.clone();
|
||||||
|
camera_mirror_button.connect_toggled(move |button| {
|
||||||
|
tests
|
||||||
|
.borrow_mut()
|
||||||
|
.set_camera_preview_mirrored(button.is_active());
|
||||||
|
button.set_tooltip_text(Some(if button.is_active() {
|
||||||
|
"Launcher preview mirrored."
|
||||||
|
} else {
|
||||||
|
"Mirror launcher preview only."
|
||||||
|
}));
|
||||||
|
if tests.borrow_mut().is_running(DeviceTestKind::Camera) {
|
||||||
|
widgets.status_label.set_text(if button.is_active() {
|
||||||
|
"Launcher webcam preview mirrored. The actual uplink stays untouched."
|
||||||
|
} else {
|
||||||
|
"Launcher webcam preview returned to the real uplink orientation."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let tests = Rc::clone(&tests);
|
let tests = Rc::clone(&tests);
|
||||||
|
|||||||
@ -84,11 +84,18 @@
|
|||||||
let state = Rc::clone(&state);
|
let state = Rc::clone(&state);
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
|
let tests = Rc::clone(&tests);
|
||||||
let toggle = widgets.camera_channel_toggle.clone();
|
let toggle = widgets.camera_channel_toggle.clone();
|
||||||
toggle.connect_toggled(move |toggle| {
|
toggle.connect_toggled(move |toggle| {
|
||||||
if let Ok(mut state) = state.try_borrow_mut() {
|
if let Ok(mut state) = state.try_borrow_mut() {
|
||||||
state.set_camera_channel_enabled(toggle.is_active());
|
state.set_camera_channel_enabled(toggle.is_active());
|
||||||
}
|
}
|
||||||
|
if !toggle.is_active() {
|
||||||
|
tests.borrow_mut().stop_camera_preview();
|
||||||
|
widgets
|
||||||
|
.status_label
|
||||||
|
.set_text("Camera stream disabled. Webcam preview stopped.");
|
||||||
|
}
|
||||||
if let Ok(state_snapshot) = state.try_borrow().map(|state| state.clone()) {
|
if let Ok(state_snapshot) = state.try_borrow().map(|state| state.clone()) {
|
||||||
refresh_launcher_ui(
|
refresh_launcher_ui(
|
||||||
&widgets,
|
&widgets,
|
||||||
@ -96,6 +103,7 @@
|
|||||||
child_proc.borrow().is_some(),
|
child_proc.borrow().is_some(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,11 +111,20 @@
|
|||||||
let state = Rc::clone(&state);
|
let state = Rc::clone(&state);
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
|
let tests = Rc::clone(&tests);
|
||||||
let toggle = widgets.microphone_channel_toggle.clone();
|
let toggle = widgets.microphone_channel_toggle.clone();
|
||||||
toggle.connect_toggled(move |toggle| {
|
toggle.connect_toggled(move |toggle| {
|
||||||
if let Ok(mut state) = state.try_borrow_mut() {
|
if let Ok(mut state) = state.try_borrow_mut() {
|
||||||
state.set_microphone_channel_enabled(toggle.is_active());
|
state.set_microphone_channel_enabled(toggle.is_active());
|
||||||
}
|
}
|
||||||
|
if !toggle.is_active() {
|
||||||
|
let mut tests = tests.borrow_mut();
|
||||||
|
tests.stop_microphone_monitor();
|
||||||
|
tests.stop_microphone_replay();
|
||||||
|
widgets
|
||||||
|
.status_label
|
||||||
|
.set_text("Mic stream disabled. Mic monitor and replay stopped.");
|
||||||
|
}
|
||||||
if let Ok(state_snapshot) = state.try_borrow().map(|state| state.clone()) {
|
if let Ok(state_snapshot) = state.try_borrow().map(|state| state.clone()) {
|
||||||
refresh_launcher_ui(
|
refresh_launcher_ui(
|
||||||
&widgets,
|
&widgets,
|
||||||
@ -115,6 +132,7 @@
|
|||||||
child_proc.borrow().is_some(),
|
child_proc.borrow().is_some(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,11 +140,20 @@
|
|||||||
let state = Rc::clone(&state);
|
let state = Rc::clone(&state);
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
|
let tests = Rc::clone(&tests);
|
||||||
let toggle = widgets.audio_channel_toggle.clone();
|
let toggle = widgets.audio_channel_toggle.clone();
|
||||||
toggle.connect_toggled(move |toggle| {
|
toggle.connect_toggled(move |toggle| {
|
||||||
if let Ok(mut state) = state.try_borrow_mut() {
|
if let Ok(mut state) = state.try_borrow_mut() {
|
||||||
state.set_audio_channel_enabled(toggle.is_active());
|
state.set_audio_channel_enabled(toggle.is_active());
|
||||||
}
|
}
|
||||||
|
if !toggle.is_active() {
|
||||||
|
let mut tests = tests.borrow_mut();
|
||||||
|
tests.stop_speaker_test();
|
||||||
|
tests.stop_microphone_replay();
|
||||||
|
widgets
|
||||||
|
.status_label
|
||||||
|
.set_text("Speaker stream disabled. Local audio playback stopped.");
|
||||||
|
}
|
||||||
if let Ok(state_snapshot) = state.try_borrow().map(|state| state.clone()) {
|
if let Ok(state_snapshot) = state.try_borrow().map(|state| state.clone()) {
|
||||||
refresh_launcher_ui(
|
refresh_launcher_ui(
|
||||||
&widgets,
|
&widgets,
|
||||||
@ -134,6 +161,7 @@
|
|||||||
child_proc.borrow().is_some(),
|
child_proc.borrow().is_some(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,8 @@ pub fn build_launcher_view(
|
|||||||
preview_panel,
|
preview_panel,
|
||||||
camera_preview_frame,
|
camera_preview_frame,
|
||||||
camera_preview,
|
camera_preview,
|
||||||
|
camera_mirror_button,
|
||||||
|
camera_mirror_revealer,
|
||||||
camera_status,
|
camera_status,
|
||||||
camera_test_button,
|
camera_test_button,
|
||||||
microphone_test_button,
|
microphone_test_button,
|
||||||
|
|||||||
@ -141,6 +141,8 @@
|
|||||||
device_refresh_button: device_refresh_button.clone(),
|
device_refresh_button: device_refresh_button.clone(),
|
||||||
swap_key_button: swap_key_button.clone(),
|
swap_key_button: swap_key_button.clone(),
|
||||||
camera_test_button: camera_test_button.clone(),
|
camera_test_button: camera_test_button.clone(),
|
||||||
|
camera_mirror_button: camera_mirror_button.clone(),
|
||||||
|
camera_mirror_revealer: camera_mirror_revealer.clone(),
|
||||||
microphone_test_button: microphone_test_button.clone(),
|
microphone_test_button: microphone_test_button.clone(),
|
||||||
microphone_replay_button: microphone_replay_button.clone(),
|
microphone_replay_button: microphone_replay_button.clone(),
|
||||||
speaker_test_button: speaker_test_button.clone(),
|
speaker_test_button: speaker_test_button.clone(),
|
||||||
@ -173,6 +175,7 @@
|
|||||||
preview_panel,
|
preview_panel,
|
||||||
camera_preview_frame,
|
camera_preview_frame,
|
||||||
camera_preview,
|
camera_preview,
|
||||||
|
camera_mirror_button,
|
||||||
camera_status,
|
camera_status,
|
||||||
},
|
},
|
||||||
widgets,
|
widgets,
|
||||||
|
|||||||
@ -35,6 +35,8 @@ struct DeviceControlsContext {
|
|||||||
preview_panel: gtk::Box,
|
preview_panel: gtk::Box,
|
||||||
camera_preview_frame: gtk::AspectFrame,
|
camera_preview_frame: gtk::AspectFrame,
|
||||||
camera_preview: gtk::Picture,
|
camera_preview: gtk::Picture,
|
||||||
|
camera_mirror_button: gtk::ToggleButton,
|
||||||
|
camera_mirror_revealer: gtk::Revealer,
|
||||||
camera_status: gtk::Label,
|
camera_status: gtk::Label,
|
||||||
camera_test_button: gtk::Button,
|
camera_test_button: gtk::Button,
|
||||||
microphone_test_button: gtk::Button,
|
microphone_test_button: gtk::Button,
|
||||||
|
|||||||
@ -219,6 +219,25 @@
|
|||||||
);
|
);
|
||||||
camera_preview.set_keep_aspect_ratio(true);
|
camera_preview.set_keep_aspect_ratio(true);
|
||||||
camera_preview.add_css_class("camera-preview-frame");
|
camera_preview.add_css_class("camera-preview-frame");
|
||||||
|
let camera_mirror_button = gtk::ToggleButton::new();
|
||||||
|
camera_mirror_button.add_css_class("camera-preview-mirror-toggle");
|
||||||
|
camera_mirror_button.add_css_class("flat");
|
||||||
|
camera_mirror_button.set_focus_on_click(false);
|
||||||
|
camera_mirror_button.set_halign(gtk::Align::End);
|
||||||
|
camera_mirror_button.set_valign(gtk::Align::Start);
|
||||||
|
camera_mirror_button.set_margin_top(10);
|
||||||
|
camera_mirror_button.set_margin_end(10);
|
||||||
|
camera_mirror_button.set_tooltip_text(Some("Mirror launcher preview only."));
|
||||||
|
camera_mirror_button.set_visible(false);
|
||||||
|
let camera_mirror_icon = gtk::Image::from_icon_name("object-flip-horizontal-symbolic");
|
||||||
|
camera_mirror_button.set_child(Some(&camera_mirror_icon));
|
||||||
|
let camera_mirror_revealer = gtk::Revealer::new();
|
||||||
|
camera_mirror_revealer.set_transition_type(gtk::RevealerTransitionType::Crossfade);
|
||||||
|
camera_mirror_revealer.set_transition_duration(120);
|
||||||
|
camera_mirror_revealer.set_halign(gtk::Align::End);
|
||||||
|
camera_mirror_revealer.set_valign(gtk::Align::Start);
|
||||||
|
camera_mirror_revealer.set_reveal_child(false);
|
||||||
|
camera_mirror_revealer.set_child(Some(&camera_mirror_button));
|
||||||
let camera_status = gtk::Label::new(Some("Select a webcam and click Start Preview."));
|
let camera_status = gtk::Label::new(Some("Select a webcam and click Start Preview."));
|
||||||
camera_status.add_css_class("dim-label");
|
camera_status.add_css_class("dim-label");
|
||||||
camera_status.set_wrap(false);
|
camera_status.set_wrap(false);
|
||||||
@ -244,7 +263,34 @@
|
|||||||
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
|
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
|
||||||
);
|
);
|
||||||
camera_preview_frame.set_child(Some(&camera_preview));
|
camera_preview_frame.set_child(Some(&camera_preview));
|
||||||
camera_preview_shell.append(&camera_preview_frame);
|
let camera_preview_overlay = gtk::Overlay::new();
|
||||||
|
camera_preview_overlay.set_hexpand(true);
|
||||||
|
camera_preview_overlay.set_vexpand(true);
|
||||||
|
camera_preview_overlay.set_halign(gtk::Align::Fill);
|
||||||
|
camera_preview_overlay.set_valign(gtk::Align::Fill);
|
||||||
|
camera_preview_overlay.set_size_request(
|
||||||
|
CAMERA_PREVIEW_VIEWPORT_WIDTH,
|
||||||
|
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
|
||||||
|
);
|
||||||
|
camera_preview_overlay.set_child(Some(&camera_preview_frame));
|
||||||
|
camera_preview_overlay.add_overlay(&camera_mirror_revealer);
|
||||||
|
let hover_revealer = camera_mirror_revealer.clone();
|
||||||
|
let hover_button = camera_mirror_button.clone();
|
||||||
|
let hover_controller = gtk::EventControllerMotion::new();
|
||||||
|
hover_controller.connect_enter(move |_, _, _| {
|
||||||
|
if hover_button.is_visible() {
|
||||||
|
hover_revealer.set_reveal_child(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let leave_revealer = camera_mirror_revealer.clone();
|
||||||
|
let leave_button = camera_mirror_button.clone();
|
||||||
|
hover_controller.connect_leave(move |_| {
|
||||||
|
if leave_button.is_visible() {
|
||||||
|
leave_revealer.set_reveal_child(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
camera_preview_overlay.add_controller(hover_controller);
|
||||||
|
camera_preview_shell.append(&camera_preview_overlay);
|
||||||
let webcam_group = build_subgroup("Webcam Preview");
|
let webcam_group = build_subgroup("Webcam Preview");
|
||||||
webcam_group.set_hexpand(true);
|
webcam_group.set_hexpand(true);
|
||||||
webcam_group.set_vexpand(true);
|
webcam_group.set_vexpand(true);
|
||||||
@ -299,6 +345,8 @@
|
|||||||
preview_panel,
|
preview_panel,
|
||||||
camera_preview_frame,
|
camera_preview_frame,
|
||||||
camera_preview,
|
camera_preview,
|
||||||
|
camera_mirror_button,
|
||||||
|
camera_mirror_revealer,
|
||||||
camera_status,
|
camera_status,
|
||||||
camera_test_button,
|
camera_test_button,
|
||||||
microphone_test_button,
|
microphone_test_button,
|
||||||
|
|||||||
@ -100,6 +100,23 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
}
|
}
|
||||||
|
button.camera-preview-mirror-toggle {
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(16, 19, 25, 0.28);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
color: rgba(238, 242, 247, 0.92);
|
||||||
|
}
|
||||||
|
button.camera-preview-mirror-toggle:checked {
|
||||||
|
background: rgba(76, 154, 255, 0.26);
|
||||||
|
border-color: rgba(76, 154, 255, 0.58);
|
||||||
|
color: #eef6ff;
|
||||||
|
}
|
||||||
|
button.camera-preview-mirror-toggle image {
|
||||||
|
-gtk-icon-size: 18px;
|
||||||
|
}
|
||||||
label.status-line {
|
label.status-line {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,6 +145,8 @@ pub struct LauncherWidgets {
|
|||||||
pub device_refresh_button: gtk::Button,
|
pub device_refresh_button: gtk::Button,
|
||||||
pub swap_key_button: gtk::Button,
|
pub swap_key_button: gtk::Button,
|
||||||
pub camera_test_button: gtk::Button,
|
pub camera_test_button: gtk::Button,
|
||||||
|
pub camera_mirror_button: gtk::ToggleButton,
|
||||||
|
pub camera_mirror_revealer: gtk::Revealer,
|
||||||
pub microphone_test_button: gtk::Button,
|
pub microphone_test_button: gtk::Button,
|
||||||
pub microphone_replay_button: gtk::Button,
|
pub microphone_replay_button: gtk::Button,
|
||||||
pub speaker_test_button: gtk::Button,
|
pub speaker_test_button: gtk::Button,
|
||||||
@ -164,6 +166,7 @@ pub struct DeviceStageWidgets {
|
|||||||
pub preview_panel: gtk::Box,
|
pub preview_panel: gtk::Box,
|
||||||
pub camera_preview_frame: gtk::AspectFrame,
|
pub camera_preview_frame: gtk::AspectFrame,
|
||||||
pub camera_preview: gtk::Picture,
|
pub camera_preview: gtk::Picture,
|
||||||
|
pub camera_mirror_button: gtk::ToggleButton,
|
||||||
pub camera_status: gtk::Label,
|
pub camera_status: gtk::Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -211,6 +211,15 @@ pub fn refresh_test_buttons(widgets: &LauncherWidgets, tests: &mut DeviceTestCon
|
|||||||
} else {
|
} else {
|
||||||
"Start Preview"
|
"Start Preview"
|
||||||
});
|
});
|
||||||
|
let camera_mirrored = tests.camera_preview_mirrored();
|
||||||
|
if widgets.camera_mirror_button.is_active() != camera_mirrored {
|
||||||
|
widgets.camera_mirror_button.set_active(camera_mirrored);
|
||||||
|
}
|
||||||
|
widgets.camera_mirror_button.set_visible(camera_running);
|
||||||
|
widgets.camera_mirror_button.set_sensitive(camera_running);
|
||||||
|
if !camera_running {
|
||||||
|
widgets.camera_mirror_revealer.set_reveal_child(false);
|
||||||
|
}
|
||||||
widgets
|
widgets
|
||||||
.microphone_test_button
|
.microphone_test_button
|
||||||
.set_label(if microphone_running {
|
.set_label(if microphone_running {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user