Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 104 additions & 10 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod debounce;
mod hardware_queue;
mod hotkey_editor;
mod hotkey_views;
mod modal;
mod monitor_views;
mod monitors;
mod profile_handlers;
Expand Down Expand Up @@ -55,7 +56,6 @@ pub enum Page {
Monitor(u32), // 1-indexed monitor ID
Hotkeys,
Profiles,
Settings,
About,
}

Expand Down Expand Up @@ -83,7 +83,10 @@ pub enum Message {
HotkeyTriggered(u32),
ToggleHotkeys(bool),
AddHotkey,
SetHotkeyLabel(String, String),
ToggleHotkeyEditor(String),
RequestDeleteHotkey(String),
CancelDeleteHotkey,
DeleteHotkey(String),
StartRecording(String),
CancelRecording,
Expand All @@ -107,6 +110,8 @@ pub enum Message {
ToggleStartWithWindows(bool),
ToggleStartMinimized(bool),
SaveConfig,
ToggleSettings,
CloseSettings,
RetryConfig,
RecoverConfigBackup,
RequestResetConfig,
Expand All @@ -130,6 +135,9 @@ pub enum Message {
HideWindow,
WindowClosed(window::Id),
OpenUrl(String),
/// Discard edits to the read-only configuration path field.
ConfigPathInput,
CopyConfigPath,
// Errors
Error(String),
}
Expand Down Expand Up @@ -165,9 +173,11 @@ pub struct AppModel {
status_message: String,
recording_state: RecordingState,
expanded_hotkey: Option<String>,
pending_hotkey_delete: Option<String>,
value_drafts: HashMap<(String, usize), String>,
vcp_drafts: HashMap<(String, usize), String>,
config_dirty: bool,
config_path: String,
about: widget::about::About,
slider_debounce: SliderDebounce,
profiles: Vec<String>,
Expand All @@ -188,10 +198,6 @@ const INPUT_SOURCES: &[InputSource] = &[
InputSource::Dp2,
InputSource::UsbC1,
InputSource::UsbC2,
InputSource::Vga1,
InputSource::Vga2,
InputSource::Dvi1,
InputSource::Dvi2,
];

const POWER_MODES: &[PowerMode] = &[
Expand Down Expand Up @@ -287,6 +293,7 @@ impl cosmic::Application for AppModel {
}
};

let config_path = config_store.path().display().to_string();
let mut app = AppModel {
core,
nav,
Expand All @@ -304,9 +311,11 @@ impl cosmic::Application for AppModel {
status_message: "Starting...".into(),
recording_state: RecordingState::NotRecording,
expanded_hotkey: None,
pending_hotkey_delete: None,
value_drafts,
vcp_drafts,
config_dirty: false,
config_path,
about,
slider_debounce: SliderDebounce::default(),
profiles: Vec::new(),
Expand Down Expand Up @@ -334,6 +343,37 @@ impl cosmic::Application for AppModel {
self.update_title()
}

fn dialog(&self) -> Option<Element<'_, Self::Message>> {
if let Some(name) = self.pending_profile_delete.clone() {
return Some(modal::confirm_dialog(
"Delete profile?",
format!("Delete \"{name}\"? This removes the saved layout."),
"Delete",
Message::CancelDeleteProfile,
Message::DeleteProfile(name),
));
}
let id = self.pending_hotkey_delete.clone()?;
let title = self.hotkey_delete_title(&id)?;
Some(modal::confirm_dialog(
"Delete hotkey?",
format!("Delete \"{title}\"? This removes the hotkey and its actions."),
"Delete",
Message::CancelDeleteHotkey,
Message::DeleteHotkey(id),
))
}

fn on_escape(&mut self) -> cosmic::app::Task<Self::Message> {
if self.pending_profile_delete.is_some() {
self.cancel_delete_profile();
}
if self.pending_hotkey_delete.is_some() {
self.cancel_delete_hotkey();
}
cosmic::app::Task::none()
}

// Intercept the header-bar close button → hide to tray instead of exiting
fn on_app_exit(&mut self) -> Option<Self::Message> {
if self.tray.is_some() {
Expand Down Expand Up @@ -401,13 +441,23 @@ impl cosmic::Application for AppModel {
fn update(&mut self, message: Self::Message) -> cosmic::app::Task<Self::Message> {
// The store also guards saves; this gate prevents editing or executing
// placeholder settings while a failed load awaits the user's decision.
if self
.pending_hotkey_delete
.as_ref()
.is_some_and(|id| self.hotkey_delete_title(id).is_none())
{
self.pending_hotkey_delete = None;
}

if self.config_store.recovery_error().is_some()
&& matches!(
message,
Message::HotkeyTriggered(_)
| Message::ToggleHotkeys(_)
| Message::SaveConfig
| Message::AddHotkey
| Message::SetHotkeyLabel(_, _)
| Message::RequestDeleteHotkey(_)
| Message::DeleteHotkey(_)
| Message::StartRecording(_)
| Message::ClearBinding(_)
Expand Down Expand Up @@ -483,7 +533,10 @@ impl cosmic::Application for AppModel {
// -- Hotkey actions ---------------------------------------------
Message::HotkeyTriggered(id) => return self.handle_hotkey_triggered(id),
Message::AddHotkey => self.add_hotkey(),
Message::SetHotkeyLabel(id, label) => self.set_hotkey_label(id, label),
Message::ToggleHotkeyEditor(id) => self.toggle_hotkey_editor(id),
Message::RequestDeleteHotkey(id) => self.request_delete_hotkey(id),
Message::CancelDeleteHotkey => self.cancel_delete_hotkey(),
Message::DeleteHotkey(id) => self.delete_hotkey(id),
Message::StartRecording(hotkey_id) => self.start_recording(hotkey_id),
Message::CancelRecording => self.cancel_recording(),
Expand Down Expand Up @@ -529,6 +582,10 @@ impl cosmic::Application for AppModel {
}

Message::SaveConfig => return self.save_config(),
Message::ToggleSettings => {
self.set_show_context(!self.core.window.show_context);
}
Message::CloseSettings => self.set_show_context(false),
Message::ToggleHotkeys(enabled) => return self.toggle_hotkeys(enabled),
Message::RetryConfig => self.retry_config(),
Message::RecoverConfigBackup => self.recover_config_backup(),
Expand Down Expand Up @@ -605,6 +662,11 @@ impl cosmic::Application for AppModel {
}
}

Message::ConfigPathInput => {}
Message::CopyConfigPath => {
self.status_message = "Copied configuration path".into();
return cosmic::iced::clipboard::write(self.config_path.clone());
}
Message::OpenUrl(url) => {
if let Err(error) = std::process::Command::new("rundll32.exe")
.args(["url.dll,FileProtocolHandler", &url])
Expand Down Expand Up @@ -643,7 +705,6 @@ impl cosmic::Application for AppModel {
Page::Monitor(monitor_id) => self.view_monitor(monitor_id),
Page::Hotkeys => self.view_hotkeys_current(),
Page::Profiles => self.view_profiles(),
Page::Settings => self.view_settings(),
Page::About => self.view_about(),
};

Expand All @@ -655,7 +716,7 @@ impl cosmic::Application for AppModel {
if recovering {
layout = layout.push(self.view_config_recovery());
}
if !recovering || !matches!(page, Page::Hotkeys | Page::Settings) {
if !recovering || !matches!(page, Page::Hotkeys) {
layout = layout.push(content);
}
let layout = layout
Expand All @@ -677,12 +738,45 @@ impl cosmic::Application for AppModel {
}

fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
vec![crate::icons::header_icon_button(
crate::icons::AppIcon::Refresh,
"Refresh",
false,
Message::RefreshMonitors,
)]
}

fn header_end(&self) -> Vec<Element<'_, Self::Message>> {
let save_tip = if self.config_dirty {
"Save configuration (unsaved changes)"
} else {
"Save configuration"
};
vec![
widget::button::text("Refresh")
.on_press(Message::RefreshMonitors)
.into(),
crate::icons::header_icon_button(
crate::icons::AppIcon::Save,
save_tip,
self.config_dirty,
Message::SaveConfig,
),
crate::icons::header_icon_button(
crate::icons::AppIcon::Settings,
"Settings",
self.core.window.show_context,
Message::ToggleSettings,
),
]
}

fn context_drawer(&self) -> Option<cosmic::app::ContextDrawer<'_, Self::Message>> {
if !self.core.window.show_context {
return None;
}
Some(
cosmic::app::context_drawer(self.view_settings(), Message::CloseSettings)
.title("Settings"),
)
}
}

// ---------------------------------------------------------------------------
Expand Down
37 changes: 36 additions & 1 deletion src/app/hotkey_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::AppModel;
use super::{Message, Page, RecordingState};
use crate::config::{
ActionTarget, ActionType, AppConfig, Hotkey, HotkeyActionSpec, HotkeyBinding, MonitorInput,
MonitorTarget,
MonitorTarget, hotkey_headings,
};
use crate::ddc::{InputSource, MonitorKey, PowerMode};
use crate::hotkeys::HotkeyManager;
Expand Down Expand Up @@ -305,6 +305,19 @@ impl AppModel {
cosmic::app::Task::none()
}

pub(super) fn set_hotkey_label(&mut self, id: String, label: String) {
if let Some(hotkey) = self
.config
.hotkeys
.hotkeys
.iter_mut()
.find(|hotkey| hotkey.id == id)
{
hotkey.label = label;
}
self.config_dirty = true;
}

pub(super) fn add_hotkey(&mut self) {
let hotkey = Hotkey::new_empty();
let id = hotkey.id.clone();
Expand All @@ -324,7 +337,29 @@ impl AppModel {
}
}

pub(super) fn request_delete_hotkey(&mut self, id: String) {
self.pending_profile_delete = None;
self.pending_hotkey_delete = Some(id);
}

pub(super) fn cancel_delete_hotkey(&mut self) {
self.pending_hotkey_delete = None;
}

/// Card heading for `id`, matching the hotkey list. `None` when that hotkey is gone.
pub(super) fn hotkey_delete_title(&self, id: &str) -> Option<String> {
let headings = hotkey_headings(&self.config.hotkeys.hotkeys);
self.config
.hotkeys
.hotkeys
.iter()
.zip(headings)
.find(|(hotkey, _)| hotkey.id == id)
.map(|(_, heading)| heading.title)
}

pub(super) fn delete_hotkey(&mut self, id: String) {
self.pending_hotkey_delete = None;
self.config.hotkeys.hotkeys.retain(|hotkey| hotkey.id != id);
self.clear_hotkey_drafts(&id);
if self.expanded_hotkey.as_deref() == Some(&id) {
Expand Down
Loading
Loading