diff --git a/cosmic-settings/src/pages/desktop/workspaces.rs b/cosmic-settings/src/pages/desktop/workspaces.rs index ecb93a38c..0a0b97d2b 100644 --- a/cosmic-settings/src/pages/desktop/workspaces.rs +++ b/cosmic-settings/src/pages/desktop/workspaces.rs @@ -4,9 +4,11 @@ // TODO make settings work use cosmic::cosmic_config::{self, ConfigGet, ConfigSet}; -use cosmic::widget::{self, settings}; +use cosmic::iced::{Alignment, Length}; +use cosmic::widget::{self, icon, settings, text}; use cosmic::{Apply, Element, surface}; use cosmic_comp_config::workspace::{Action, WorkspaceConfig, WorkspaceLayout, WorkspaceMode}; +use cosmic_comp_config::WorkspaceAssignment; use cosmic_settings_page::{self as page, Section, section}; use slotmap::SlotMap; use tracing::error; @@ -17,9 +19,16 @@ pub enum Message { SetWorkspaceMode(WorkspaceMode), SetWorkspaceLayout(WorkspaceLayout), SetWorkspaceWraparound(bool), + SetGridColumns(u32), + SetGridRows(u32), SetShowName(bool), SetShowNumber(bool), Surface(surface::Action), + SetAssignmentWorkspace(usize, u32), + RemoveAssignment(usize), + NewAssignmentAppId(String), + NewAssignmentWorkspace(u32), + AddAssignment, } pub struct Page { @@ -30,6 +39,9 @@ pub struct Page { action_on_typing_active: Option, show_workspace_name: bool, show_workspace_number: bool, + workspace_assignments: Vec, + new_assignment_app_id: String, + new_assignment_workspace: u32, } impl Default for Page { @@ -59,6 +71,15 @@ impl Default for Page { false }); + let workspace_assignments: Vec = comp_config + .get("workspace_assignments") + .unwrap_or_else(|err| { + if err.is_err() { + error!(?err, "Failed to read config 'workspace_assignments'"); + } + + Vec::new() + }); Self { config, comp_config, @@ -71,6 +92,9 @@ impl Default for Page { action_on_typing_active, show_workspace_name, show_workspace_number, + workspace_assignments, + new_assignment_app_id: String::new(), + new_assignment_workspace: 1, } } } @@ -85,6 +109,7 @@ impl page::Page for Page { sections.insert(multi_behavior()), sections.insert(workspace_orientation()), sections.insert(workspace_navigation()), + sections.insert(workspace_assignments()), ]) } @@ -107,6 +132,15 @@ impl Page { } } + fn save_assignments(&self) { + if let Err(err) = self + .comp_config + .set("workspace_assignments", &self.workspace_assignments) + { + error!(?err, "Failed to set config 'workspace_assignments'"); + } + } + pub fn update(&mut self, message: Message) -> cosmic::iced::Task { match message { Message::SetWorkspaceMode(value) => { @@ -127,6 +161,14 @@ impl Page { self.comp_workspace_config.workspace_wraparound = value; self.save_comp_config(); } + Message::SetGridColumns(value) => { + self.comp_workspace_config.workspace_grid_columns = value.max(1); + self.save_comp_config(); + } + Message::SetGridRows(value) => { + self.comp_workspace_config.workspace_grid_rows = value.max(1); + self.save_comp_config(); + } Message::SetShowName(value) => { self.show_workspace_name = value; if let Err(err) = self.config.set("show_workspace_name", value) { @@ -142,6 +184,36 @@ impl Page { Message::Surface(a) => { return cosmic::task::message(crate::app::Message::Surface(a)); } + Message::SetAssignmentWorkspace(index, value) => { + if let Some(rule) = self.workspace_assignments.get_mut(index) { + rule.workspace = value.max(1); + self.save_assignments(); + } + } + Message::RemoveAssignment(index) => { + if index < self.workspace_assignments.len() { + self.workspace_assignments.remove(index); + self.save_assignments(); + } + } + Message::NewAssignmentAppId(value) => { + self.new_assignment_app_id = value; + } + Message::NewAssignmentWorkspace(value) => { + self.new_assignment_workspace = value.max(1); + } + Message::AddAssignment => { + let app_id = self.new_assignment_app_id.trim().to_string(); + if !app_id.is_empty() { + self.workspace_assignments.push(WorkspaceAssignment { + app_id, + workspace: self.new_assignment_workspace, + }); + self.new_assignment_app_id.clear(); + self.new_assignment_workspace = 1; + self.save_assignments(); + } + } } cosmic::iced::Task::none() } @@ -229,6 +301,9 @@ fn workspace_orientation() -> Section { crate::slab!(descriptions { vertical = fl!("workspaces-orientation", "vertical"); horizontal = fl!("workspaces-orientation", "horizontal"); + grid = fl!("workspaces-orientation", "grid"); + grid_columns = fl!("workspaces-orientation", "grid-columns"); + grid_rows = fl!("workspaces-orientation", "grid-rows"); }); Section::default() @@ -236,7 +311,9 @@ fn workspace_orientation() -> Section { .descriptions(descriptions) .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; - settings::section() + let is_grid = + page.comp_workspace_config.workspace_layout == WorkspaceLayout::Grid; + let mut section = settings::section() .title(§ion.title) .add(settings::item::builder(&descriptions[vertical]).radio( WorkspaceLayout::Vertical, @@ -248,6 +325,41 @@ fn workspace_orientation() -> Section { Some(page.comp_workspace_config.workspace_layout), Message::SetWorkspaceLayout, )) + .add(settings::item::builder(&descriptions[grid]).radio( + WorkspaceLayout::Grid, + Some(page.comp_workspace_config.workspace_layout), + Message::SetWorkspaceLayout, + )); + if is_grid { + section = section + .add( + settings::item::builder(&descriptions[grid_columns]).control( + widget::spin_button( + page.comp_workspace_config.workspace_grid_columns.to_string(), + "grid columns", + page.comp_workspace_config.workspace_grid_columns, + 1, + 1, + 10, + Message::SetGridColumns, + ), + ), + ) + .add( + settings::item::builder(&descriptions[grid_rows]).control( + widget::spin_button( + page.comp_workspace_config.workspace_grid_rows.to_string(), + "grid rows", + page.comp_workspace_config.workspace_grid_rows, + 1, + 1, + 10, + Message::SetGridRows, + ), + ), + ); + } + section .apply(Element::from) .map(crate::pages::Message::DesktopWorkspaces) }) @@ -273,3 +385,80 @@ fn workspace_navigation() -> Section { .map(crate::pages::Message::DesktopWorkspaces) }) } + +fn workspace_assignments() -> Section { + crate::slab!(descriptions { + description = fl!("workspaces-assignments", "description"); + }); + + Section::default() + .title(fl!("workspaces-assignments")) + .descriptions(descriptions) + .view::(move |_binder, page, section| { + let cosmic::cosmic_theme::Spacing { space_xs, .. } = cosmic::theme::spacing(); + let descriptions = §ion.descriptions; + + let mut section = settings::section() + .title(§ion.title) + .add(text::body(&descriptions[description])); + + if page.workspace_assignments.is_empty() { + section = section.add(text::body(fl!("workspaces-assignments", "none"))); + } else { + for (index, rule) in page.workspace_assignments.iter().enumerate() { + let row = widget::row::with_capacity(4) + .spacing(space_xs) + .align_y(Alignment::Center) + .push(icon::from_name("application-x-executable-symbolic").size(24)) + .push(text::body(rule.app_id.clone()).width(Length::Fill)) + .push(widget::spin_button( + rule.workspace.to_string(), + "workspace", + rule.workspace, + 1, + 1, + 99, + move |value| Message::SetAssignmentWorkspace(index, value), + )) + .push( + widget::button::icon(icon::from_name("edit-delete-symbolic")) + .extra_small() + .on_press(Message::RemoveAssignment(index)), + ); + section = section.add(row); + } + } + + let add_row = widget::row::with_capacity(3) + .spacing(space_xs) + .align_y(Alignment::Center) + .push( + widget::text_input( + fl!("workspaces-assignments", "app-id-placeholder"), + &page.new_assignment_app_id, + ) + .on_input(Message::NewAssignmentAppId) + .width(Length::Fill), + ) + .push(widget::spin_button( + page.new_assignment_workspace.to_string(), + "new-assignment-workspace", + page.new_assignment_workspace, + 1, + 1, + 99, + Message::NewAssignmentWorkspace, + )) + .push( + widget::button::standard(fl!("workspaces-assignments", "add")).on_press_maybe( + (!page.new_assignment_app_id.trim().is_empty()) + .then_some(Message::AddAssignment), + ), + ); + + section + .add(add_row) + .apply(Element::from) + .map(crate::pages::Message::DesktopWorkspaces) + }) +} diff --git a/cosmic-settings/src/pages/input/touchpad.rs b/cosmic-settings/src/pages/input/touchpad.rs index 29d167cf3..3622cdecd 100644 --- a/cosmic-settings/src/pages/input/touchpad.rs +++ b/cosmic-settings/src/pages/input/touchpad.rs @@ -312,7 +312,10 @@ fn gestures() -> Section { settings::item::builder( &descriptions[match page.comp_workspace_config.workspace_layout { WorkspaceLayout::Horizontal => switch_workspaces_horizontal, - WorkspaceLayout::Vertical => switch_workspaces_vertical, + // Grid behaves like Vertical for 4-finger swipes. + WorkspaceLayout::Vertical | WorkspaceLayout::Grid => { + switch_workspaces_vertical + } }], ) .flex_control(text::body(&descriptions[switch_workspaces])), diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index b77f075d6..7ae62e6e8 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -538,6 +538,9 @@ workspaces-overview-thumbnails = Workspace overview thumbnails workspaces-orientation = Workspaces orientation .vertical = Vertical .horizontal = Horizontal + .grid = Grid + .grid-columns = Grid columns + .grid-rows = Grid rows hot-corner = Hot Corner .top-left-corner = Enable top-left hot corner for Workspaces @@ -545,6 +548,14 @@ hot-corner = Hot Corner workspaces-navigation = Navigation .wraparound = Move between first and last workspace using keyboard shortcuts and gestures +workspaces-assignments = App workspace assignments + .description = Apps below always open on their assigned workspace, no matter where they're launched from. + .app-id = Application ID + .workspace = Workspace + .add = Add + .none = No apps assigned yet. + .app-id-placeholder = e.g. code, org.mozilla.firefox + ## Displays -requires-restart = Requires restart