feat(shortcuts): runtime configurable keyboard shortcuts

This commit is contained in:
Michael Aaron Murphy 2024-06-06 16:57:08 +02:00
parent 6f051b2456
commit cf322fdb5e
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
47 changed files with 3305 additions and 416 deletions

939
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,9 @@
members = ["cosmic-settings", "page", "pages/*"]
default-members = ["cosmic-settings"]
resolver = "2"
rust-version = "1.71.0"
sunrise_sunset = "1.0.1"
[workspace.package]
rust-version = "1.75.0"
[workspace.dependencies]
cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" }
@ -11,7 +12,7 @@ tokio = { version = "1.37.0", features = ["macros"] }
[workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
features = ["dbus-config", "single-instance", "tokio", "wayland", "wgpu", "xdg-portal"]
features = ["dbus-config", "single-instance", "multi-window", "tokio", "wayland", "wgpu", "xdg-portal"]
[workspace.dependencies.cosmic-config]
git = "https://github.com/pop-os/libcosmic"
@ -34,8 +35,13 @@ git = "https://github.com/smithay/client-toolkit/"
package = "smithay-client-toolkit"
rev = "3bed072"
[profile.dev]
opt-level = 3
lto = false
[profile.release]
opt-level = 3
lto = "thin"
[patch.'https://github.com/smithay/client-toolkit/']
smithay-client-toolkit = { git = "https://github.com/smithay/client-toolkit//", rev = "3bed072" }

View file

@ -3,7 +3,6 @@ name = "cosmic-settings"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0"
rust-version = "1.65.0"
[dependencies]
anyhow = "1.0"
@ -14,9 +13,11 @@ clap = { version = "4.4.18", features = ["derive"] }
color-eyre = "0.6.2"
cosmic-bg-config.workspace = true
cosmic-comp-config.workspace = true
cosmic-config.workspace = true
cosmic-panel-config.workspace = true
cosmic-randr-shell.workspace = true
cosmic-randr.workspace = true
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon" }
cosmic-settings-page = { path = "../page" }
cosmic-settings-system = { path = "../pages/system" }
cosmic-settings-time = { path = "../pages/time" }

View file

@ -312,6 +312,10 @@ impl cosmic::Application for SettingsApp {
page::update!(self.pages, message, desktop::Page);
}
crate::pages::Message::DesktopOptions(message) => {
page::update!(self.pages, message, desktop::options::Page);
}
crate::pages::Message::DesktopWallpaper(message) => {
if let Some(page) = self.pages.page_mut::<desktop::wallpaper::Page>() {
return page.update(message).map(Into::into);
@ -334,6 +338,66 @@ impl cosmic::Application for SettingsApp {
}
}
crate::pages::Message::KeyboardShortcuts(message) => {
if let Some(page) = self.pages.page_mut::<input::keyboard::shortcuts::Page>() {
return page.update(message).map(Into::into);
}
}
crate::pages::Message::CustomShortcuts(message) => {
if let Some(page) = self
.pages
.page_mut::<input::keyboard::shortcuts::custom::Page>()
{
return page.update(message).map(Into::into);
}
}
crate::pages::Message::ManageWindowShortcuts(message) => {
if let Some(page) = self
.pages
.page_mut::<input::keyboard::shortcuts::manage_windows::Page>()
{
return page.update(message).map(Into::into);
}
}
crate::pages::Message::MoveWindowShortcuts(message) => {
if let Some(page) = self
.pages
.page_mut::<input::keyboard::shortcuts::move_window::Page>()
{
return page.update(message).map(Into::into);
}
}
crate::pages::Message::NavShortcuts(message) => {
if let Some(page) = self
.pages
.page_mut::<input::keyboard::shortcuts::nav::Page>()
{
return page.update(message).map(Into::into);
}
}
crate::pages::Message::SystemShortcuts(message) => {
if let Some(page) = self
.pages
.page_mut::<input::keyboard::shortcuts::system::Page>()
{
return page.update(message).map(Into::into);
}
}
crate::pages::Message::TilingShortcuts(message) => {
if let Some(page) = self
.pages
.page_mut::<input::keyboard::shortcuts::tiling::Page>()
{
return page.update(message).map(Into::into);
}
}
crate::pages::Message::Input(message) => {
if let Some(page) = self.pages.page_mut::<input::Page>() {
return page.update(message).map(Into::into);
@ -641,7 +705,9 @@ impl SettingsApp {
let page_info = &self.pages.info[self.active_page];
let mut column_widgets = Vec::with_capacity(1 + content.len());
column_widgets.push(if let Some(parent) = page_info.parent {
column_widgets.push(if let Some(custom_header) = page.header() {
custom_header.map(Message::from)
} else if let Some(parent) = page_info.parent {
let page_header = crate::widget::sub_page_header(
page_info.title.as_str(),
self.pages.info[parent].title.as_str(),

View file

@ -5,6 +5,7 @@
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::too_many_lines)]
pub mod app;
use std::str::FromStr;

View file

@ -64,7 +64,7 @@ enum ContextView {
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct IconTheme {
pub struct IconTheme {
// COSMIC uses the file name of the folder containing the theme
id: String,
// GTK uses the name of the theme as specified in its index file
@ -1073,7 +1073,7 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(
&mut self,
_: page::Entity,
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
command::future(fetch_icon_themes()).map(crate::pages::Message::Appearance)
}

View file

@ -1,21 +1,60 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use super::Message;
use cosmic::{
iced::Length,
theme,
widget::{button, container, horizontal_space, icon, row, settings, toggler},
widget::{self, button, container, horizontal_space, icon, row, settings, toggler},
Apply, Element,
};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::{shortcuts, Action, Binding, Shortcuts};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slab::Slab;
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
#[derive(Copy, Clone, Debug)]
pub enum Message {
SuperKey(usize),
}
pub struct Page {
pub super_key_selections: Vec<String>,
pub super_key_active: Option<usize>,
}
impl Default for Page {
fn default() -> Self {
Page {
super_key_selections: vec![
fl!("super-key", "launcher"),
fl!("super-key", "workspaces"),
fl!("super-key", "applications"),
],
super_key_active: super_key_active_config(),
}
}
}
impl Page {
pub fn update(&mut self, message: Message) {
match message {
Message::SuperKey(id) => {
let action = match id {
0 => shortcuts::action::System::Launcher,
1 => shortcuts::action::System::WorkspaceOverview,
2 => shortcuts::action::System::AppLibrary,
_ => return,
};
self.super_key_active = Some(id);
super_key_set(action);
}
}
}
}
impl page::Page<crate::pages::Message> for Page {
#[allow(clippy::too_many_lines)]
@ -47,30 +86,26 @@ impl page::AutoBind<crate::pages::Message> for Page {
pub fn super_key_action() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
let launcher = descriptions.insert(fl!("super-key-action", "launcher"));
let workspaces = descriptions.insert(fl!("super-key-action", "workspaces"));
let applications = descriptions.insert(fl!("super-key-action", "applications"));
let super_key = descriptions.insert(fl!("super-key"));
let _launcher = descriptions.insert(fl!("super-key", "launcher"));
let _workspaces = descriptions.insert(fl!("super-key", "workspaces"));
let _applications = descriptions.insert(fl!("super-key", "applications"));
Section::default()
.title(fl!("super-key-action"))
.descriptions(descriptions)
.view::<Page>(move |_binder, _page, section| {
.view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
settings::view_section(&section.title)
.add(settings::item(
&descriptions[launcher],
horizontal_space(Length::Fill),
))
.add(settings::item(
&descriptions[workspaces],
horizontal_space(Length::Fill),
))
.add(settings::item(
&descriptions[applications],
horizontal_space(Length::Fill),
))
.into()
.add(
settings::item::builder(&descriptions[super_key]).control(widget::dropdown(
&page.super_key_selections,
page.super_key_active,
Message::SuperKey,
)),
)
.apply(Element::from)
.map(crate::pages::Message::DesktopOptions)
})
}
@ -95,7 +130,7 @@ pub fn window_controls() -> Section<crate::pages::Message> {
toggler(
None,
desktop.cosmic_tk.show_minimize,
Message::ShowMinimizeButton,
super::Message::ShowMinimizeButton,
),
))
.add(settings::flex_item(
@ -103,7 +138,7 @@ pub fn window_controls() -> Section<crate::pages::Message> {
toggler(
None,
desktop.cosmic_tk.show_maximize,
Message::ShowMaximizeButton,
super::Message::ShowMaximizeButton,
),
))
.apply(Element::from)
@ -117,7 +152,8 @@ pub fn panel_dock_links() -> Section<crate::pages::Message> {
.view::<Page>(move |binder, _page, section| {
// TODO probably a way of getting the entity and its info
let mut settings = settings::view_section(&section.title);
settings = if let Some((panel_entity, panel_info)) =
if let Some((panel_entity, panel_info)) =
binder.info.iter().find(|(_, v)| v.id == "panel")
{
let control = row::with_children(vec![
@ -125,7 +161,7 @@ pub fn panel_dock_links() -> Section<crate::pages::Message> {
icon::from_name("go-next-symbolic").size(16).into(),
]);
settings.add(
settings = settings.add(
settings::item::builder(panel_info.title.clone())
.description(panel_info.description.clone())
.control(control)
@ -135,10 +171,8 @@ pub fn panel_dock_links() -> Section<crate::pages::Message> {
.apply(button)
.style(theme::Button::Transparent)
.on_press(crate::pages::Message::Page(panel_entity)),
)
} else {
settings
};
);
}
settings = if let Some((dock_entity, dock_info)) =
binder.info.iter().find(|(_, v)| v.id == "dock")
@ -166,3 +200,39 @@ pub fn panel_dock_links() -> Section<crate::pages::Message> {
Element::from(settings)
})
}
fn super_key_active_config() -> Option<usize> {
let super_binding = Binding::new(shortcuts::Modifiers::new().logo(), None);
let config = shortcuts::context().ok()?;
let shortcuts = shortcuts::shortcuts(&config);
let new_id = shortcuts
.iter()
.find(|(binding, _action)| binding == &&super_binding)
.and_then(|(_, action)| match action {
Action::System(shortcuts::action::System::Launcher) => Some(0),
Action::System(shortcuts::action::System::WorkspaceOverview) => Some(1),
Action::System(shortcuts::action::System::AppLibrary) => Some(2),
_ => None,
});
new_id
}
fn super_key_set(action: shortcuts::action::System) {
let Ok(config) = shortcuts::context() else {
return;
};
let Ok(mut shortcuts) = config.get::<Shortcuts>("custom") else {
return;
};
shortcuts.0.insert(
Binding::new(shortcuts::Modifiers::new().logo(), None),
Action::System(action),
);
_ = config.set("custom", &shortcuts);
}

View file

@ -211,7 +211,7 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(
&mut self,
_page: page::Entity,
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
let current_folder = self.config.current_folder().to_owned();

View file

@ -311,8 +311,8 @@ impl Page {
Message::Mirroring(mirroring) => match mirroring {
Mirroring::Disable => (),
Mirroring::Mirror(target_display) => (),
Mirroring::Project(target_display) => (),
Mirroring::Mirror(_target_display) => (),
Mirroring::Project(_target_display) => (),
Mirroring::ProjectToAll => (),
},
@ -411,12 +411,12 @@ impl Page {
}
/// Changes the color depth of the active display.
pub fn set_color_depth(&mut self, depth: ColorDepth) -> Command<app::Message> {
pub fn set_color_depth(&mut self, _depth: ColorDepth) -> Command<app::Message> {
unimplemented!()
}
/// Changes the color profile of the active display.
pub fn set_color_profile(&mut self, profile: usize) -> Command<app::Message> {
pub fn set_color_profile(&mut self, _profile: usize) -> Command<app::Message> {
unimplemented!()
}

View file

@ -1,3 +1,5 @@
pub mod shortcuts;
use std::cmp;
use cosmic::{
@ -404,11 +406,11 @@ impl Page {
}
}
SourceContext::Settings(id) => {
SourceContext::Settings(_id) => {
eprintln!("settings not implemented");
}
SourceContext::ViewLayout(id) => {
SourceContext::ViewLayout(_id) => {
eprintln!("view layout not implemented");
}
}

View file

@ -1,47 +0,0 @@
use cosmic::widget::{column, settings};
use cosmic::{Apply, Element};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slab::Slab;
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
//crate::app::Message::Page
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn info(&self) -> page::Info {
page::Info::new("keyboard-shortcuts", "input-keyboard-symbolic")
.title(fl!("keyboard-shortcuts"))
.description(fl!("keyboard-shortcuts", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
fn shortcuts() -> Section<crate::pages::Message> {
let descriptions = Slab::new();
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, _page, section| {
// TODO need something more custom
/*
settings::view_section(&section.title)
.apply(Element::from)
.map(crate::pages::Message::Input)
*/
column()
.push(settings::view_section(&section.title))
.apply(Element::from)
.map(crate::pages::Message::Input)
})
}

View file

@ -0,0 +1,588 @@
use cosmic::iced::alignment::Horizontal;
use cosmic::iced::{Alignment, Length};
use cosmic::prelude::CollectionWidget;
use cosmic::widget::{self, button, icon, settings, text};
use cosmic::{command, theme, Apply, Command, Element};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::shortcuts::{self, Action, Binding, Shortcuts};
use slab::Slab;
use std::borrow::Cow;
use std::io;
use std::str::FromStr;
#[derive(Clone, Debug)]
pub enum ShortcutMessage {
AddKeybinding,
ApplyReplace,
CancelReplace,
DeleteBinding(usize),
DeleteShortcut(usize),
EditBinding(usize, bool),
InputBinding(usize, String),
ResetBindings,
ShowShortcut(usize, String),
SubmitBinding(usize),
}
#[derive(Debug)]
pub struct ShortcutBinding {
pub id: widget::Id,
pub binding: Binding,
pub input: String,
pub editing: bool,
pub is_default: bool,
}
#[must_use]
#[derive(Debug)]
pub struct ShortcutModel {
pub action: Action,
pub bindings: Slab<ShortcutBinding>,
pub description: String,
pub modified: u16,
}
impl ShortcutModel {
pub fn new(defaults: &Shortcuts, shortcuts: &Shortcuts, action: Action) -> Self {
let (bindings, modified) =
shortcuts
.shortcuts(&action)
.fold((Slab::new(), 0), |(mut slab, modified), binding| {
let is_default = defaults.0.get(binding) == Some(&action);
slab.insert(ShortcutBinding {
id: widget::Id::unique(),
binding: binding.clone(),
input: String::new(),
editing: false,
is_default,
});
(slab, if is_default { modified } else { modified + 1 })
});
Self {
description: super::localize_action(&action),
modified: defaults.0.iter().filter(|(_, a)| **a == action).fold(
modified,
|modified, (binding, _)| {
if bindings.iter().any(|(_, model)| model.binding == *binding) {
modified
} else {
modified + 1
}
},
),
action,
bindings,
}
}
}
#[must_use]
pub struct Model {
pub defaults: Shortcuts,
pub replace_dialog: Option<(usize, Binding, Action, String)>,
pub shortcut_models: Slab<ShortcutModel>,
pub shortcut_context: Option<usize>,
pub config: cosmic_config::Config,
pub custom: bool,
pub actions: fn(&Shortcuts, &Shortcuts) -> Slab<ShortcutModel>,
}
impl Default for Model {
fn default() -> Self {
Self {
defaults: Shortcuts::default(),
replace_dialog: None,
shortcut_models: Slab::new(),
shortcut_context: None,
config: shortcuts::context().unwrap(),
custom: false,
actions: |_, _| Slab::new(),
}
}
}
impl Model {
pub fn actions(mut self, actions: fn(&Shortcuts, &Shortcuts) -> Slab<ShortcutModel>) -> Self {
self.actions = actions;
self
}
pub fn custom(mut self) -> Self {
self.custom = true;
self
}
/// Adds a new binding to the shortcuts config
pub(super) fn config_add(&self, action: Action, binding: Binding) {
let mut shortcuts = self.shortcuts_config();
shortcuts.0.insert(binding, action);
self.shortcuts_config_set(shortcuts);
}
/// Check if a binding is already set
pub(super) fn config_contains(&self, binding: &Binding) -> Option<Action> {
self.shortcuts_system_config()
.0
.get(binding)
.cloned()
.filter(|action| *action != Action::Disable)
}
/// Removes a binding from the shortcuts config
pub(super) fn config_remove(&self, binding: &Binding) {
let mut shortcuts = self.shortcuts_config();
shortcuts.0.retain(|b, _| b != binding);
self.shortcuts_config_set(shortcuts);
}
pub(super) fn context_drawer(&self) -> Option<Element<'_, ShortcutMessage>> {
self.shortcut_context
.as_ref()
.map(|id| context_drawer(&self.shortcut_models, *id, self.custom))
}
pub(super) fn dialog(&self) -> Option<Element<'_, ShortcutMessage>> {
if let Some(&(id, _, _, ref action)) = self.replace_dialog.as_ref() {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get(short_id) {
if let Some(shortcut) = model.bindings.get(id) {
let primary_action = button::suggested(fl!("replace"))
.on_press(ShortcutMessage::ApplyReplace);
let secondary_action = button::standard(fl!("cancel"))
.on_press(ShortcutMessage::CancelReplace);
let dialog = widget::dialog(fl!("replace-shortcut-dialog"))
.icon(icon::from_name("dialog-warning").size(64))
.body(fl!(
"replace-shortcut-dialog",
"desc",
shortcut = shortcut.input.clone(),
name = shortcut
.binding
.description
.as_ref()
.unwrap_or(action)
.to_owned()
))
.primary_action(primary_action)
.secondary_action(secondary_action);
return Some(dialog.into());
}
}
}
}
None
}
pub(super) fn on_enter(&mut self) {
let mut shortcuts = self.config.get::<Shortcuts>("defaults").unwrap_or_default();
self.defaults = shortcuts.clone();
if let Ok(custom) = self.config.get::<Shortcuts>("custom") {
for (binding, action) in custom.0 {
shortcuts.0.remove(&binding);
shortcuts.0.insert(binding, action);
}
}
self.shortcut_models = (self.actions)(&self.defaults, &shortcuts);
}
pub(super) fn on_clear(&mut self) {
self.shortcut_models.clear();
}
/// Gets the custom configuration for keyboard shortcuts.
pub(super) fn shortcuts_config(&self) -> Shortcuts {
match self.config.get::<Shortcuts>("custom") {
Ok(shortcuts) => shortcuts,
Err(cosmic_config::Error::GetKey(_, why)) if why.kind() == io::ErrorKind::NotFound => {
Shortcuts::default()
}
Err(why) => {
tracing::error!(?why, "unable to get the current shortcuts config");
Shortcuts::default()
}
}
}
/// Gets the system configuration for keyboard shortcuts.
pub(super) fn shortcuts_system_config(&self) -> Shortcuts {
let mut shortcuts = self.config.get::<Shortcuts>("defaults").unwrap_or_default();
if let Ok(custom) = self.config.get::<Shortcuts>("custom") {
shortcuts.0.extend(custom.0);
}
shortcuts
}
/// Writes a new configuration to the keyboard shortcuts config file.
pub(super) fn shortcuts_config_set(&self, shortcuts: Shortcuts) {
if let Err(why) = self.config.set("custom", shortcuts) {
tracing::error!(?why, "failed to write shortcuts config");
}
}
#[allow(clippy::too_many_lines)]
pub(super) fn update(&mut self, message: ShortcutMessage) -> Command<crate::app::Message> {
match message {
ShortcutMessage::AddKeybinding => {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
// If an empty entry exists, focus it instead of creating a new input.
for (_, shortcut) in &mut model.bindings {
if shortcut.binding.is_set()
|| Binding::from_str(&shortcut.input).is_ok()
{
continue;
}
shortcut.input.clear();
return widget::text_input::focus(shortcut.id.clone());
}
// Create a new input and focus it.
let id = widget::Id::unique();
model.bindings.insert(ShortcutBinding {
id: id.clone(),
binding: Binding::default(),
input: String::new(),
editing: true,
is_default: false,
});
return widget::text_input::focus(id);
}
}
}
ShortcutMessage::ApplyReplace => {
if let Some((id, new_binding, ..)) = self.replace_dialog.take() {
if let Some(short_id) = self.shortcut_context {
// Remove conflicting bindings that are saved on disk.
self.config_remove(&new_binding);
// Clear any binding that matches this in the current model
for (_, model) in &mut self.shortcut_models {
if let Some(id) = model
.bindings
.iter()
.find(|(_, shortcut)| shortcut.binding == new_binding)
.map(|(id, _)| id)
{
model.bindings.remove(id);
break;
}
}
// Update the current model and save the binding to disk.
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
let prev_binding = shortcut.binding.clone();
shortcut.binding = new_binding.clone();
shortcut.input.clear();
shortcut.editing = false;
let action = model.action.clone();
self.config_remove(&prev_binding);
self.config_add(action, new_binding);
}
}
self.on_enter();
}
}
}
ShortcutMessage::CancelReplace => self.replace_dialog = None,
ShortcutMessage::DeleteBinding(id) => {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
let shortcut = model.bindings.remove(id);
if shortcut.is_default {
self.config_add(Action::Disable, shortcut.binding.clone());
} else {
self.config_remove(&shortcut.binding);
}
self.on_enter();
}
}
}
ShortcutMessage::DeleteShortcut(id) => {
let model = self.shortcut_models.remove(id);
for (_, shortcut) in model.bindings {
self.config_remove(&shortcut.binding);
self.on_enter();
}
}
ShortcutMessage::EditBinding(id, enable) => {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
shortcut.editing = enable;
if enable {
shortcut.input = shortcut.binding.to_string();
return widget::text_input::select_all(shortcut.id.clone());
}
}
}
}
}
ShortcutMessage::InputBinding(id, text) => {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
shortcut.input = text;
}
}
}
}
// Removes all bindings from the active shortcut context, and reloads the shortcuts model.
ShortcutMessage::ResetBindings => {
if let Some(short_id) = self.shortcut_context {
if let Some(model) = self.shortcut_models.get(short_id) {
for (_, shortcut) in &model.bindings {
self.config_remove(&shortcut.binding);
}
if let Ok(defaults) = self.config.get::<Shortcuts>("defaults") {
for (binding, action) in defaults.0 {
if action == model.action {
self.config_remove(&binding);
}
}
}
}
self.on_enter();
}
}
ShortcutMessage::ShowShortcut(id, description) => {
self.shortcut_context = Some(id);
self.replace_dialog = None;
let mut commands = vec![command::message(crate::app::Message::OpenContextDrawer(
description.into(),
))];
if let Some(model) = self.shortcut_models.get(0) {
if let Some(shortcut) = model.bindings.get(0) {
commands.push(widget::text_input::focus(shortcut.id.clone()));
commands.push(widget::text_input::select_all(shortcut.id.clone()));
}
}
return Command::batch(commands);
}
ShortcutMessage::SubmitBinding(id) => {
if let Some(short_id) = self.shortcut_context {
let mut apply_binding = None;
// Check for conflicts with the new binding.
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
match Binding::from_str(&shortcut.input) {
Ok(new_binding) => {
if !new_binding.is_set() {
shortcut.input.clear();
return Command::none();
}
if let Some(action) = self.config_contains(&new_binding) {
let action_str = super::localize_action(&action);
self.replace_dialog =
Some((id, new_binding, action, action_str));
return Command::none();
}
apply_binding = Some(new_binding);
}
Err(why) => {
tracing::error!(why, "keybinding input invalid");
}
}
}
}
// Apply if no conflict was found.
if let Some(new_binding) = apply_binding {
if let Some(model) = self.shortcut_models.get_mut(short_id) {
if let Some(shortcut) = model.bindings.get_mut(id) {
let prev_binding = shortcut.binding.clone();
shortcut.binding = new_binding.clone();
shortcut.input.clear();
shortcut.editing = false;
let action = model.action.clone();
self.config_remove(&prev_binding);
self.config_add(action, new_binding);
self.on_enter();
}
}
}
}
}
}
Command::none()
}
pub(super) fn view(&self) -> Element<ShortcutMessage> {
self.shortcut_models
.iter()
.map(|(id, shortcut)| shortcut_item(self.custom, id, shortcut))
.fold(widget::list_column(), widget::ListColumn::add)
.into()
}
}
fn context_drawer(
shortcuts: &Slab<ShortcutModel>,
id: usize,
show_action: bool,
) -> Element<ShortcutMessage> {
let model = &shortcuts[id];
let action = show_action.then(|| {
let description = if let Action::Spawn(command) = &model.action {
Cow::Borrowed(command.as_str())
} else {
Cow::Owned(super::localize_action(&model.action))
};
text::body(description)
});
let bindings = model.bindings.iter().enumerate().fold(
widget::list_column().spacing(8),
|section, (_, (bind_id, shortcut))| {
let text: Cow<'_, str> = if !shortcut.editing && shortcut.binding.is_set() {
Cow::Owned(shortcut.binding.to_string())
} else {
Cow::Borrowed(&shortcut.input)
};
let input = widget::editable_input("", text, shortcut.editing, move |enable| {
ShortcutMessage::EditBinding(bind_id, enable)
})
.select_on_focus(true)
.on_input(move |text| ShortcutMessage::InputBinding(bind_id, text))
.on_submit(ShortcutMessage::SubmitBinding(bind_id))
.padding([0, 12])
.id(shortcut.id.clone())
.into();
let delete_button = widget::button::icon(icon::from_name("edit-delete-symbolic"))
.on_press(ShortcutMessage::DeleteBinding(bind_id))
.into();
let flex_control =
settings::flex_item_row(vec![input, delete_button]).align_items(Alignment::Center);
section.add(flex_control)
},
);
// TODO: Detect when it is necessary
let reset_keybinding_button = if show_action {
None
} else {
let button = widget::button::standard(fl!("reset-to-default"))
.on_press(ShortcutMessage::ResetBindings);
Some(button)
};
let add_keybinding_button =
widget::button::standard(fl!("add-keybinding")).on_press(ShortcutMessage::AddKeybinding);
let button_container = widget::row::with_capacity(2)
.push_maybe(reset_keybinding_button)
.push(add_keybinding_button)
.spacing(12)
.apply(widget::container)
.width(Length::Fill)
.align_x(Horizontal::Right);
widget::column::with_capacity(if show_action { 3 } else { 2 })
.spacing(32)
.push_maybe(action)
.push(bindings)
.push(button_container)
.into()
}
/// Display a shortcut as a list item
fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<ShortcutMessage> {
#[derive(Copy, Clone, Debug)]
enum LocalMessage {
Remove,
Show,
}
let bindings = data
.bindings
.iter()
.take(3)
.filter(|(_, shortcut)| shortcut.binding.is_set())
.map(|(_, shortcut)| widget::text::body(shortcut.binding.to_string()).into())
.collect::<Vec<_>>();
let shortcuts: Element<LocalMessage> = if bindings.is_empty() {
widget::text::body(fl!("disabled")).into()
} else {
widget::column::with_children(bindings)
.align_items(Alignment::End)
.into()
};
let modified = if data.modified == 0 {
None
} else {
Some(widget::text::body(fl!("modified", count = data.modified)))
};
let control = widget::row::with_capacity(4)
.push_maybe(modified)
.push(shortcuts)
.push(icon::from_name("go-next-symbolic").size(16))
.push_maybe(custom.then(|| {
widget::button::icon(icon::from_name("edit-delete-symbolic"))
.on_press(LocalMessage::Remove)
}))
.align_items(Alignment::Center)
.spacing(8);
settings::item::builder(&data.description)
.flex_control(control)
.spacing(16)
.apply(widget::container)
.style(theme::Container::List)
.apply(widget::button)
.style(theme::Button::Transparent)
.on_press(LocalMessage::Show)
.apply(Element::from)
.map(move |message| match message {
LocalMessage::Show => ShortcutMessage::ShowShortcut(id, data.description.clone()),
LocalMessage::Remove => ShortcutMessage::DeleteShortcut(id),
})
}

View file

@ -0,0 +1,440 @@
use std::str::FromStr;
use super::{ShortcutBinding, ShortcutMessage, ShortcutModel};
use cosmic::iced::alignment::Horizontal;
use cosmic::iced::Length;
use cosmic::widget::{self, button, icon};
use cosmic::{Apply, Command, Element};
use cosmic_settings_config::shortcuts::{Action, Shortcuts};
use cosmic_settings_config::Binding;
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
use slotmap::SlotMap;
pub struct Page {
model: super::Model,
add_shortcut: AddShortcut,
replace_dialog: Vec<(Binding, Action, String)>,
command_id: widget::Id,
name_id: widget::Id,
}
impl Default for Page {
fn default() -> Self {
Self {
model: super::Model::default().custom().actions(bindings),
add_shortcut: AddShortcut::default(),
replace_dialog: Vec::new(),
command_id: widget::Id::unique(),
name_id: widget::Id::unique(),
}
}
}
#[derive(Clone, Debug)]
pub enum Message {
/// Adds a new key binding input
AddKeybinding,
/// Add a new custom shortcut to the config
AddShortcut,
/// Update the command text input
CommandInput(String),
/// Toggle editing of the key text input
EditCombination,
/// Toggle editability of the key text input
KeyEditing(usize, bool),
/// Update the key text input
KeyInput(usize, String),
/// Update the name text input
NameInput(String),
/// Enter key pressed in the name text input
NameSubmit,
/// Apply a requested shortcut replace operation
ReplaceApply,
/// Cancel a requested shortcut replace operation
ReplaceCancel,
/// Emit a generic shortcut message
Shortcut(ShortcutMessage),
/// Open the add shortcut context drawer
ShortcutContext,
}
#[derive(Default)]
struct AddShortcut {
pub active: bool,
pub name: String,
pub command: String,
pub keys: Slab<(String, widget::Id, bool)>,
}
impl AddShortcut {
pub fn enable(&mut self) {
self.active = true;
self.name.clear();
self.command.clear();
if self.keys.is_empty() {
self.keys
.insert((String::new(), widget::Id::unique(), false));
} else {
while self.keys.len() > 1 {
self.keys.remove(self.keys.len() - 1);
}
self.keys[0].0.clear();
}
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
match message {
Message::CommandInput(text) => {
self.add_shortcut.command = text;
}
Message::KeyInput(id, text) => {
self.add_shortcut.keys[id].0 = text;
}
Message::KeyEditing(id, enable) => {
self.add_shortcut.keys[id].2 = enable;
}
Message::NameInput(text) => {
self.add_shortcut.name = text;
}
Message::AddKeybinding => {
// If an empty entry exists, focus it instead of creating a new input.
for (_, (binding, id, _)) in &mut self.add_shortcut.keys {
if Binding::from_str(binding).is_ok() {
continue;
}
binding.clear();
return widget::text_input::focus(id.clone());
}
let new_id = widget::Id::unique();
self.add_shortcut
.keys
.insert((String::new(), new_id.clone(), true));
return Command::batch(vec![
widget::text_input::focus(new_id.clone()),
widget::text_input::select_all(new_id),
]);
}
Message::AddShortcut => {
let name = self.add_shortcut.name.trim();
let command = self.add_shortcut.command.trim();
if name.is_empty() || command.is_empty() {
return Command::none();
}
let mut addable_bindings = Vec::new();
for (_, (keys, ..)) in &self.add_shortcut.keys {
if keys.is_empty() {
continue;
}
let Ok(binding) = Binding::from_str(keys) else {
return Command::none();
};
if !binding.is_set() {
return Command::none();
}
if let Some(action) = self.model.config_contains(&binding) {
let action_str = super::localize_action(&action);
self.replace_dialog.push((binding, action, action_str));
continue;
}
addable_bindings.push(binding);
}
for binding in addable_bindings {
self.add_shortcut(binding);
}
self.model.on_enter();
}
Message::EditCombination => {
let (_, id, editing) = &mut self.add_shortcut.keys[0];
*editing = true;
return Command::batch(vec![
widget::text_input::focus(id.clone()),
widget::text_input::select_all(id.clone()),
]);
}
Message::NameSubmit => {
if !self.add_shortcut.name.trim().is_empty() {
return widget::text_input::focus(self.command_id.clone());
}
}
Message::ReplaceApply => {
if let Some((binding, ..)) = self.replace_dialog.pop() {
self.model.config_remove(&binding);
self.add_shortcut(binding);
if self.replace_dialog.is_empty() {
self.model.on_enter();
}
}
}
Message::ReplaceCancel => {
_ = self.replace_dialog.pop();
if self.replace_dialog.is_empty() {
self.model.on_enter();
}
}
Message::Shortcut(message) => {
if let ShortcutMessage::ShowShortcut(..) = message {
self.add_shortcut.active = false;
}
return self.model.update(message);
}
Message::ShortcutContext => {
self.add_shortcut.enable();
return Command::batch(vec![
cosmic::command::message(crate::app::Message::OpenContextDrawer(
fl!("custom-shortcuts", "context").into(),
)),
widget::text_input::focus(self.name_id.clone()),
]);
}
}
Command::none()
}
fn add_keybinding_context(&self) -> Element<'_, Message> {
let name_input = widget::text_input("", &self.add_shortcut.name)
.padding([6, 12])
.on_input(Message::NameInput)
.on_submit(Message::NameSubmit)
.id(self.name_id.clone());
let command_input = widget::text_input("", &self.add_shortcut.command)
.padding([6, 12])
.on_input(Message::CommandInput)
.on_submit(Message::EditCombination)
.id(self.command_id.clone());
let name_control = widget::column()
.spacing(4)
.push(widget::text::body(fl!("shortcut-name")))
.push(name_input);
let command_control = widget::column()
.spacing(4)
.push(widget::text::body(fl!("command")))
.push(command_input);
let input_fields = widget::column()
.spacing(12)
.push(name_control)
.push(command_control)
.padding([16, 24]);
let keys = self.add_shortcut.keys.iter().fold(
widget::list_column().spacing(0),
|column, (id, (text, widget_id, editing))| {
let key_combination = widget::editable_input(
fl!("type-key-combination"),
text,
*editing,
move |enable| Message::KeyEditing(id, enable),
)
.padding([0, 12])
.on_input(move |input| Message::KeyInput(id, input))
.on_submit(Message::AddKeybinding)
.id(widget_id.clone())
.apply(widget::container)
.padding([8, 24]);
column.add(key_combination)
},
);
let controls = widget::list_column().add(input_fields).add(keys).spacing(0);
let add_keybinding_button = widget::button::standard(fl!("add-keybinding"))
.on_press(Message::AddShortcut)
.apply(widget::container)
.width(Length::Fill)
.align_x(Horizontal::Right);
widget::column()
.spacing(32)
.push(controls)
.push(add_keybinding_button)
.into()
}
fn add_shortcut(&mut self, mut binding: Binding) {
self.add_shortcut.active = !self.replace_dialog.is_empty();
binding.description = Some(self.add_shortcut.name.clone());
let new_action = Action::Spawn(self.add_shortcut.command.clone());
self.model.config_add(new_action, binding);
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("custom-shortcuts", "input-keyboard-symbolic")
.title(fl!("custom-shortcuts"))
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
// Check if a new shortcut is being added that requires a replace dialog.
if let Some((binding, _action, action_str)) = self.replace_dialog.last() {
let primary_action = button::suggested(fl!("replace")).on_press(Message::ReplaceApply);
let secondary_action = button::standard(fl!("cancel")).on_press(Message::ReplaceCancel);
let dialog = widget::dialog(fl!("replace-shortcut-dialog"))
.icon(icon::from_name("dialog-warning").size(64))
.body(fl!(
"replace-shortcut-dialog",
"desc",
shortcut = binding.to_string(),
name = action_str.clone()
))
.primary_action(primary_action)
.secondary_action(secondary_action)
.apply(Element::from)
.map(crate::pages::Message::CustomShortcuts);
return Some(dialog);
}
// Check if a keybinding is being added that requires a replace dialog.
self.model
.dialog()
.map(|el| el.map(|m| crate::pages::Message::CustomShortcuts(Message::Shortcut(m))))
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
if self.add_shortcut.active {
Some(self.add_keybinding_context())
} else {
self.model
.context_drawer()
.map(|el| el.map(Message::Shortcut))
}
.map(|el| el.map(crate::pages::Message::CustomShortcuts))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
self.model.on_enter();
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.model.on_clear();
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
fn bindings(_defaults: &Shortcuts, keybindings: &Shortcuts) -> Slab<ShortcutModel> {
keybindings
.iter()
.fold(Slab::new(), |mut slab, (binding, action)| {
if let Action::Spawn(command) = action {
let description = binding
.description
.clone()
.unwrap_or_else(|| command.to_owned());
let new_binding = ShortcutBinding {
id: widget::Id::unique(),
binding: binding.clone(),
input: String::new(),
editing: false,
is_default: false,
};
if let Some((_, existing_model)) =
slab.iter_mut().find(|(_, m)| &m.action == action)
{
existing_model.description = description;
existing_model.bindings.insert(new_binding);
} else {
slab.insert(ShortcutModel {
action: action.clone(),
bindings: {
let mut slab = Slab::new();
slab.insert(new_binding);
slab
},
description,
modified: 0,
});
}
}
slab
})
}
fn shortcuts() -> Section<crate::pages::Message> {
let descriptions = Slab::new();
// TODO: Add shortcuts to descriptions
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, _section| {
let content = if page.model.shortcut_models.is_empty() {
widget::settings::view_section("")
.add(widget::settings::item_row(vec![widget::text::body(fl!(
"custom-shortcuts",
"none"
))
.into()]))
.into()
} else {
page.model.view().map(Message::Shortcut)
};
let add_shortcut = widget::button::standard(fl!("custom-shortcuts", "add"))
.on_press(Message::ShortcutContext)
.apply(widget::container)
.width(Length::Fill)
.align_x(Horizontal::Right);
widget::column()
.push(content)
.push(add_shortcut)
.spacing(24)
.apply(Element::from)
.map(crate::pages::Message::CustomShortcuts)
})
}

View file

@ -0,0 +1,99 @@
use super::{ShortcutMessage, ShortcutModel};
use cosmic::{Command, Element};
use cosmic_settings_config::shortcuts::action::ResizeDirection;
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
pub struct Page {
model: super::Model,
}
impl Default for Page {
fn default() -> Self {
Self {
model: super::Model::default().actions(|defaults, keybindings| {
actions().iter().fold(Slab::new(), |mut slab, action| {
slab.insert(ShortcutModel::new(defaults, keybindings, action.clone()));
slab
})
}),
}
}
}
impl Page {
pub fn update(&mut self, message: ShortcutMessage) -> Command<crate::app::Message> {
self.model.update(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("manage-windows", "input-keyboard-symbolic").title(fl!("manage-windows"))
}
fn content(
&self,
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.context_drawer()
.map(|el| el.map(crate::pages::Message::ManageWindowShortcuts))
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.dialog()
.map(|el| el.map(crate::pages::Message::ManageWindowShortcuts))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
self.model.on_enter();
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.model.on_clear();
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
#[must_use]
pub const fn actions() -> &'static [Action] {
&[
Action::Close,
Action::Maximize,
Action::Minimize,
Action::Resizing(ResizeDirection::Inwards),
Action::Resizing(ResizeDirection::Outwards),
Action::ToggleSticky,
]
}
fn shortcuts() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
// Make these searchable in the global settings search.
for action in actions() {
descriptions.insert(super::localize_action(action));
}
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, _section| {
page.model
.view()
.map(crate::pages::Message::ManageWindowShortcuts)
})
}

View file

@ -0,0 +1,620 @@
mod common;
pub use common::{Model, ShortcutBinding, ShortcutMessage, ShortcutModel};
pub mod custom;
pub mod manage_windows;
pub mod move_window;
pub mod nav;
pub mod system;
pub mod tiling;
use cosmic::iced::Length;
use cosmic::widget::{self, icon, settings, text};
use cosmic::{command, theme, Apply, Command, Element};
use cosmic_config::ConfigGet;
use cosmic_settings_config::shortcuts::action::{
Direction, FocusDirection, Orientation, ResizeDirection,
};
use cosmic_settings_config::shortcuts::{self, Action, Shortcuts};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use shortcuts::action::System as SystemAction;
use slab::Slab;
use slotmap::{DefaultKey, Key, SecondaryMap, SlotMap};
pub struct Page {
modified: Modified,
search: Search,
search_model: Model,
shortcuts_context: Option<cosmic_config::Config>,
sub_pages: SubPages,
}
#[derive(Default)]
struct Modified {
manage_windows: u16,
move_windows: u16,
nav: u16,
system: u16,
window_tiling: u16,
custom: u16,
}
struct SubPages {
custom: page::Entity,
manage_window: page::Entity,
move_window: page::Entity,
nav: page::Entity,
system: page::Entity,
window_tiling: page::Entity,
}
#[derive(Default)]
struct Search {
input: String,
actions: SlotMap<DefaultKey, Action>,
localized: SecondaryMap<DefaultKey, String>,
shortcuts: Shortcuts,
defaults: Shortcuts,
}
#[derive(Clone, Debug)]
pub enum Message {
Category(Category),
Search(String),
SearchShortcut(ShortcutMessage),
}
#[derive(Clone, Copy, Debug)]
pub enum Category {
Custom,
ManageWindow,
MoveWindow,
Nav,
System,
WindowTiling,
}
impl Default for Page {
fn default() -> Self {
Self {
modified: Modified::default(),
search: Search::default(),
search_model: Model::default(),
shortcuts_context: None,
sub_pages: SubPages {
custom: page::Entity::null(),
manage_window: page::Entity::null(),
move_window: page::Entity::null(),
nav: page::Entity::null(),
system: page::Entity::null(),
window_tiling: page::Entity::null(),
},
}
}
}
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn info(&self) -> page::Info {
page::Info::new("keyboard-shortcuts", "input-keyboard-symbolic")
.title(fl!("keyboard-shortcuts"))
.description(fl!("keyboard-shortcuts", "desc"))
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
if self.search_model.shortcut_models.is_empty() {
None
} else {
self.search_model.context_drawer().map(|el| {
el.map(|msg| crate::pages::Message::KeyboardShortcuts(Message::SearchShortcut(msg)))
})
}
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
if self.search_model.shortcut_models.is_empty() {
None
} else {
self.search_model.dialog().map(|el| {
el.map(|msg| crate::pages::Message::KeyboardShortcuts(Message::SearchShortcut(msg)))
})
}
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
if self.shortcuts_context.is_none() {
self.shortcuts_context = cosmic_settings_config::shortcuts::context().ok();
}
if let Some(context) = self.shortcuts_context.as_ref() {
let mut defaults = context.get::<Shortcuts>("defaults").unwrap_or_default();
let custom = context.get::<Shortcuts>("custom").unwrap_or_default();
for (custom_binding, custom_action) in &custom.0 {
// Skip bindings for the super key
if custom_binding.is_super() {
continue;
}
// Check if a custom binding overrides a default binding, or is in addition to it.
match defaults.0.get(custom_binding) {
Some(default_action) if default_action == custom_action => continue,
_ => (),
}
match action_category(custom_action) {
Some(Category::ManageWindow) => self.modified.manage_windows += 1,
Some(Category::MoveWindow) => self.modified.move_windows += 1,
Some(Category::Nav) => self.modified.nav += 1,
Some(Category::System) => self.modified.system += 1,
Some(Category::WindowTiling) => self.modified.window_tiling += 1,
None | Some(Category::Custom) => (),
}
}
// Check if default bindings are missing
for (binding, action) in &defaults.0 {
if binding.is_super() {
continue;
}
match custom.0.get(binding) {
Some(custom_action) if action != custom_action => (),
_ => continue,
};
match action_category(action) {
Some(Category::ManageWindow) => self.modified.manage_windows += 1,
Some(Category::MoveWindow) => self.modified.move_windows += 1,
Some(Category::Nav) => self.modified.nav += 1,
Some(Category::System) => self.modified.system += 1,
Some(Category::WindowTiling) => self.modified.window_tiling += 1,
None | Some(Category::Custom) => (),
}
}
self.search.defaults = defaults.clone();
defaults.0.extend(custom.0);
self.search.shortcuts = defaults;
}
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.search.actions.clear();
self.search.localized.clear();
self.search.input.clear();
self.search_model.on_clear();
self.modified.custom = 0;
self.modified.manage_windows = 0;
self.modified.move_windows = 0;
self.modified.nav = 0;
self.modified.system = 0;
Command::none()
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
match message {
Message::Category(category) => match category {
Category::Custom => {
command::message(crate::app::Message::Page(self.sub_pages.custom))
}
Category::ManageWindow => {
command::message(crate::app::Message::Page(self.sub_pages.manage_window))
}
Category::MoveWindow => {
command::message(crate::app::Message::Page(self.sub_pages.move_window))
}
Category::Nav => command::message(crate::app::Message::Page(self.sub_pages.nav)),
Category::System => {
command::message(crate::app::Message::Page(self.sub_pages.system))
}
Category::WindowTiling => {
command::message(crate::app::Message::Page(self.sub_pages.window_tiling))
}
},
Message::Search(input) => {
self.search(input);
Command::none()
}
Message::SearchShortcut(message) => self.search_model.update(message),
}
}
fn search(&mut self, input: String) {
self.search.input = input;
if self.search.input.is_empty() {
self.search_model.on_clear();
return;
}
if self.search.actions.is_empty() {
self.search.cache_localized_actions();
}
self.search_model.shortcut_models = self.search.shortcut_models();
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(
mut page: cosmic_settings_page::Insert<crate::pages::Message>,
) -> cosmic_settings_page::Insert<crate::pages::Message> {
let custom = page.sub_page_with_id::<custom::Page>();
let manage_window = page.sub_page_with_id::<manage_windows::Page>();
let move_window = page.sub_page_with_id::<move_window::Page>();
let nav = page.sub_page_with_id::<nav::Page>();
let system = page.sub_page_with_id::<system::Page>();
let window_tiling = page.sub_page_with_id::<tiling::Page>();
let model = page.model.page_mut::<Page>().unwrap();
model.sub_pages.custom = custom;
model.sub_pages.manage_window = manage_window;
model.sub_pages.move_window = move_window;
model.sub_pages.nav = nav;
model.sub_pages.system = system;
model.sub_pages.window_tiling = window_tiling;
page
}
}
impl Search {
fn cache_localized_actions(&mut self) {
self.actions.clear();
self.localized.clear();
for action in all_actions() {
let localized = localize_action(action);
let id = self.actions.insert(action.clone());
self.localized.insert(id, localized);
}
}
fn shortcut_models(&mut self) -> Slab<ShortcutModel> {
let input = self.input.to_lowercase();
self.actions
.iter()
.filter(|(id, _)| self.localized[*id].to_lowercase().contains(&input))
.fold(Slab::new(), |mut slab, (_, action)| {
slab.insert(ShortcutModel::new(
&self.defaults,
&self.shortcuts,
action.clone(),
));
slab
})
}
}
fn shortcuts() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
let custom_label = descriptions.insert(fl!("custom"));
let manage_window_label = descriptions.insert(fl!("manage-windows"));
let move_window_label = descriptions.insert(fl!("move-windows"));
let nav_label = descriptions.insert(fl!("nav-shortcuts"));
let system_label = descriptions.insert(fl!("system-shortcut"));
let window_tiling_label = descriptions.insert(fl!("window-tiling"));
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
let search = widget::search_input(fl!("type-to-search"), &page.search.input)
.width(314)
.on_clear(Message::Search(String::new()))
.on_input(Message::Search)
.apply(widget::container)
.center_x()
.width(Length::Fill);
// If the search input is not empty, show the category view, else the search results.
let content = if page.search.input.is_empty() {
settings::view_section("")
.add(category_item(
Category::ManageWindow,
&descriptions[manage_window_label],
page.modified.manage_windows,
))
.add(category_item(
Category::MoveWindow,
&descriptions[move_window_label],
page.modified.move_windows,
))
.add(category_item(
Category::Nav,
&descriptions[nav_label],
page.modified.nav,
))
.add(category_item(
Category::System,
&descriptions[system_label],
page.modified.system,
))
.add(category_item(
Category::WindowTiling,
&descriptions[window_tiling_label],
page.modified.window_tiling,
))
.add(category_item(
Category::Custom,
&descriptions[custom_label],
page.modified.custom,
))
.apply(Element::from)
} else {
page.search_model.view().map(Message::SearchShortcut)
};
widget::column::with_capacity(2)
.spacing(32)
.push(search)
.push(content)
.apply(Element::from)
.map(crate::pages::Message::KeyboardShortcuts)
})
}
/// Display a category as a list item
fn category_item(category: Category, name: &str, modified: u16) -> Element<Message> {
let icon = icon::from_name("go-next-symbolic").size(16);
let control = if modified == 0 {
Element::from(icon)
} else {
widget::row()
.push(text::body(fl!("modified", count = modified)))
.push(icon)
.into()
};
settings::item::builder(name)
.control(control)
.spacing(16)
.apply(widget::container)
.style(theme::Container::List)
.apply(widget::button)
.style(theme::Button::Transparent)
.on_press(Message::Category(category))
.into()
}
fn action_category(action: &Action) -> Option<Category> {
Some(if manage_windows::actions().contains(action) {
Category::ManageWindow
} else if move_window::actions().contains(action) {
Category::MoveWindow
} else if nav::actions().contains(action) {
Category::Nav
} else if system::actions().contains(action) {
Category::System
} else {
return None;
})
}
fn all_actions() -> &'static [Action] {
&[
Action::Close,
Action::Debug,
Action::Focus(FocusDirection::Down),
Action::Focus(FocusDirection::In),
Action::Focus(FocusDirection::Left),
Action::Focus(FocusDirection::Out),
Action::Focus(FocusDirection::Right),
Action::Focus(FocusDirection::Up),
Action::LastWorkspace,
Action::Maximize,
Action::MigrateWorkspaceToNextOutput,
Action::MigrateWorkspaceToOutput(Direction::Down),
Action::MigrateWorkspaceToOutput(Direction::Left),
Action::MigrateWorkspaceToOutput(Direction::Right),
Action::MigrateWorkspaceToOutput(Direction::Up),
Action::MigrateWorkspaceToPreviousOutput,
Action::Minimize,
Action::Move(Direction::Down),
Action::Move(Direction::Left),
Action::Move(Direction::Right),
Action::Move(Direction::Up),
Action::MoveToLastWorkspace,
Action::MoveToNextOutput,
Action::MoveToNextWorkspace,
Action::MoveToOutput(Direction::Down),
Action::MoveToOutput(Direction::Left),
Action::MoveToOutput(Direction::Right),
Action::MoveToOutput(Direction::Up),
Action::MoveToPreviousOutput,
Action::MoveToPreviousWorkspace,
Action::MoveToWorkspace(1),
Action::MoveToWorkspace(2),
Action::MoveToWorkspace(3),
Action::MoveToWorkspace(4),
Action::MoveToWorkspace(5),
Action::MoveToWorkspace(6),
Action::MoveToWorkspace(7),
Action::MoveToWorkspace(8),
Action::MoveToWorkspace(9),
Action::NextOutput,
Action::NextWorkspace,
Action::Orientation(Orientation::Horizontal),
Action::Orientation(Orientation::Vertical),
Action::PreviousOutput,
Action::PreviousWorkspace,
Action::Resizing(ResizeDirection::Inwards),
Action::Resizing(ResizeDirection::Outwards),
Action::SwapWindow,
Action::SwitchOutput(Direction::Down),
Action::SwitchOutput(Direction::Left),
Action::SwitchOutput(Direction::Right),
Action::SwitchOutput(Direction::Up),
Action::System(SystemAction::AppLibrary),
Action::System(SystemAction::BrightnessDown),
Action::System(SystemAction::BrightnessUp),
Action::System(SystemAction::HomeFolder),
Action::System(SystemAction::KeyboardBrightnessDown),
Action::System(SystemAction::KeyboardBrightnessUp),
Action::System(SystemAction::Launcher),
Action::System(SystemAction::LockScreen),
Action::System(SystemAction::Mute),
Action::System(SystemAction::MuteMic),
Action::System(SystemAction::Screenshot),
Action::System(SystemAction::Terminal),
Action::System(SystemAction::VolumeLower),
Action::System(SystemAction::VolumeRaise),
Action::System(SystemAction::WebBrowser),
Action::System(SystemAction::WindowSwitcher),
Action::System(SystemAction::WorkspaceOverview),
Action::Terminate,
Action::ToggleOrientation,
Action::ToggleStacking,
Action::ToggleSticky,
Action::ToggleTiling,
Action::ToggleWindowFloating,
Action::Workspace(1),
Action::Workspace(2),
Action::Workspace(3),
Action::Workspace(4),
Action::Workspace(5),
Action::Workspace(6),
Action::Workspace(7),
Action::Workspace(8),
Action::Workspace(9),
]
}
fn localize_action(action: &Action) -> String {
match action {
Action::Close => fl!("manage-windows", "close"),
Action::Disable => fl!("disabled"),
Action::Focus(FocusDirection::Down) => fl!("nav-shortcuts", "focus", direction = "down"),
Action::Focus(FocusDirection::In) => fl!("nav-shortcuts", "focus", direction = "in"),
Action::Focus(FocusDirection::Left) => fl!("nav-shortcuts", "focus", direction = "left"),
Action::Focus(FocusDirection::Out) => fl!("nav-shortcuts", "focus", direction = "out"),
Action::Focus(FocusDirection::Right) => fl!("nav-shortcuts", "focus", direction = "right"),
Action::Focus(FocusDirection::Up) => fl!("nav-shortcuts", "focus", direction = "up"),
Action::Workspace(i) => fl!("nav-shortcuts", "workspace", num = (*i as usize)),
Action::LastWorkspace => fl!("nav-shortcuts", "last-workspace"),
Action::Maximize => fl!("manage-windows", "maximize"),
Action::Minimize => fl!("manage-windows", "minimize"),
Action::Move(Direction::Down) => fl!("move-windows", "direction", direction = "down"),
Action::Move(Direction::Right) => fl!("move-windows", "direction", direction = "right"),
Action::Move(Direction::Left) => fl!("move-windows", "direction", direction = "left"),
Action::Move(Direction::Up) => fl!("move-windows", "direction", direction = "up"),
Action::MoveToLastWorkspace | Action::SendToLastWorkspace => {
fl!("move-windows", "last-workspace")
}
Action::MoveToNextOutput | Action::SendToNextOutput => fl!("move-windows", "next-display"),
Action::MoveToNextWorkspace | Action::SendToNextWorkspace => {
fl!("move-windows", "next-workspace")
}
Action::MoveToPreviousWorkspace | Action::SendToPreviousWorkspace => {
fl!("move-windows", "prev-workspace")
}
Action::MoveToOutput(Direction::Down) | Action::SendToOutput(Direction::Down) => {
fl!("move-windows", "display", direction = "down")
}
Action::MoveToOutput(Direction::Left) | Action::SendToOutput(Direction::Left) => {
fl!("move-windows", "display", direction = "left")
}
Action::MoveToOutput(Direction::Right) | Action::SendToOutput(Direction::Right) => {
fl!("move-windows", "display", direction = "right")
}
Action::MoveToOutput(Direction::Up) | Action::SendToOutput(Direction::Up) => {
fl!("move-windows", "display", direction = "up")
}
Action::MoveToPreviousOutput | Action::SendToPreviousOutput => {
fl!("move-windows", "prev-display")
}
Action::MoveToWorkspace(i) | Action::SendToWorkspace(i) => {
fl!("move-windows", "workspace-num", num = (*i as usize))
}
Action::NextOutput => fl!("nav-shortcuts", "next-output"),
Action::NextWorkspace => fl!("nav-shortcuts", "next-workspace"),
Action::Orientation(Orientation::Horizontal) => fl!("window-tiling", "horizontal"),
Action::Orientation(Orientation::Vertical) => fl!("window-tiling", "vertical"),
Action::PreviousOutput => fl!("nav-shortcuts", "prev-output"),
Action::PreviousWorkspace => fl!("nav-shortcuts", "prev-workspace"),
Action::Resizing(ResizeDirection::Inwards) => fl!("manage-windows", "resize-inwards"),
Action::Resizing(ResizeDirection::Outwards) => fl!("manage-windows", "resize-outwards"),
Action::SwapWindow => fl!("window-tiling", "swap-window"),
Action::SwitchOutput(Direction::Down) => fl!("nav-shortcuts", "output", direction = "down"),
Action::SwitchOutput(Direction::Left) => fl!("nav-shortcuts", "output", direction = "left"),
Action::SwitchOutput(Direction::Right) => {
fl!("nav-shortcuts", "output", direction = "right")
}
Action::SwitchOutput(Direction::Up) => fl!("nav-shortcuts", "output", direction = "up"),
Action::ToggleOrientation => fl!("window-tiling", "toggle-orientation"),
Action::ToggleStacking => fl!("window-tiling", "toggle-stacking"),
Action::ToggleSticky => fl!("manage-windows", "toggle-sticky"),
Action::ToggleTiling => fl!("window-tiling", "toggle-tiling"),
Action::ToggleWindowFloating => fl!("window-tiling", "toggle-floating"),
// Currently unused by any settings pages
Action::Debug => fl!("debug"),
Action::MigrateWorkspaceToNextOutput => fl!("migrate-workspace-next"),
Action::MigrateWorkspaceToOutput(Direction::Down) => {
fl!("migrate-workspace", direction = "down")
}
Action::MigrateWorkspaceToOutput(Direction::Left) => {
fl!("migrate-workspace", direction = "left")
}
Action::MigrateWorkspaceToOutput(Direction::Right) => {
fl!("migrate-workspace", direction = "right")
}
Action::MigrateWorkspaceToOutput(Direction::Up) => {
fl!("migrate-workspace", direction = "up")
}
Action::MigrateWorkspaceToPreviousOutput => fl!("migrate-workspace-prev"),
Action::Terminate => fl!("terminate"),
Action::System(system) => match system {
SystemAction::AppLibrary => fl!("system-shortcut", "app-library"),
SystemAction::BrightnessDown => fl!("system-shortcut", "brightness-down"),
SystemAction::BrightnessUp => fl!("system-shortcut", "brightness-up"),
SystemAction::HomeFolder => fl!("system-shortcut", "home-folder"),
SystemAction::KeyboardBrightnessDown => {
fl!("system-shortcut", "keyboard-brightness-down")
}
SystemAction::KeyboardBrightnessUp => fl!("system-shortcut", "keyboard-brightness-up"),
SystemAction::Launcher => fl!("system-shortcut", "launcher"),
SystemAction::LockScreen => fl!("system-shortcut", "lock-screen"),
SystemAction::Mute => fl!("system-shortcut", "mute"),
SystemAction::MuteMic => fl!("system-shortcut", "mute-mic"),
SystemAction::Screenshot => fl!("system-shortcut", "screenshot"),
SystemAction::Terminal => fl!("system-shortcut", "terminal"),
SystemAction::VolumeLower => fl!("system-shortcut", "volume-lower"),
SystemAction::VolumeRaise => fl!("system-shortcut", "volume-raise"),
SystemAction::WebBrowser => fl!("system-shortcut", "web-browser"),
SystemAction::WindowSwitcher => fl!("system-shortcut", "window-switcher"),
SystemAction::WorkspaceOverview => fl!("system-shortcut", "workspace-overview"),
},
Action::Spawn(command) => command.clone(),
}
}

View file

@ -0,0 +1,116 @@
use super::{ShortcutMessage, ShortcutModel};
use cosmic::{Command, Element};
use cosmic_settings_config::shortcuts::action::Direction;
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
pub struct Page {
model: super::Model,
}
impl Default for Page {
fn default() -> Self {
Self {
model: super::Model::default().actions(|defaults, keybindings| {
actions().iter().fold(Slab::new(), |mut slab, action| {
slab.insert(ShortcutModel::new(defaults, keybindings, action.clone()));
slab
})
}),
}
}
}
impl Page {
pub fn update(&mut self, message: ShortcutMessage) -> Command<crate::app::Message> {
self.model.update(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("move-windows", "input-keyboard-symbolic").title(fl!("move-windows"))
}
fn content(
&self,
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.context_drawer()
.map(|el| el.map(crate::pages::Message::MoveWindowShortcuts))
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.dialog()
.map(|el| el.map(crate::pages::Message::MoveWindowShortcuts))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
self.model.on_enter();
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.model.on_clear();
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
#[must_use]
pub const fn actions() -> &'static [Action] {
&[
Action::Move(Direction::Down),
Action::Move(Direction::Left),
Action::Move(Direction::Right),
Action::Move(Direction::Up),
Action::MoveToPreviousWorkspace,
Action::MoveToNextWorkspace,
Action::MoveToLastWorkspace,
Action::MoveToWorkspace(1),
Action::MoveToWorkspace(2),
Action::MoveToWorkspace(3),
Action::MoveToWorkspace(4),
Action::MoveToWorkspace(5),
Action::MoveToWorkspace(6),
Action::MoveToWorkspace(7),
Action::MoveToWorkspace(8),
Action::MoveToWorkspace(9),
Action::MoveToPreviousOutput,
Action::MoveToNextOutput,
Action::MoveToOutput(Direction::Down),
Action::MoveToOutput(Direction::Left),
Action::MoveToOutput(Direction::Right),
Action::MoveToOutput(Direction::Up),
]
}
fn shortcuts() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
// Make these searchable in the global settings search.
for action in actions() {
descriptions.insert(super::localize_action(action));
}
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, _section| {
page.model
.view()
.map(crate::pages::Message::MoveWindowShortcuts)
})
}

View file

@ -0,0 +1,107 @@
use super::{ShortcutMessage, ShortcutModel};
use cosmic::{Command, Element};
use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection};
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
pub struct Page {
model: super::Model,
}
impl Default for Page {
fn default() -> Self {
Self {
model: super::Model::default().actions(|defaults, keybindings| {
actions().iter().fold(Slab::new(), |mut slab, action| {
slab.insert(ShortcutModel::new(defaults, keybindings, action.clone()));
slab
})
}),
}
}
}
impl Page {
pub fn update(&mut self, message: ShortcutMessage) -> Command<crate::app::Message> {
self.model.update(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("nav-shortcuts", "input-keyboard-symbolic").title(fl!("nav-shortcuts"))
}
fn content(
&self,
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.context_drawer()
.map(|el| el.map(crate::pages::Message::NavShortcuts))
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.dialog()
.map(|el| el.map(crate::pages::Message::NavShortcuts))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
self.model.on_enter();
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.model.on_clear();
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
#[must_use]
pub const fn actions() -> &'static [Action] {
&[
Action::Focus(FocusDirection::Left),
Action::Focus(FocusDirection::Right),
Action::Focus(FocusDirection::Up),
Action::Focus(FocusDirection::Down),
Action::Focus(FocusDirection::In),
Action::Focus(FocusDirection::Out),
Action::PreviousWorkspace,
Action::NextWorkspace,
Action::LastWorkspace,
Action::PreviousOutput,
Action::NextOutput,
Action::SwitchOutput(Direction::Left),
Action::SwitchOutput(Direction::Right),
Action::SwitchOutput(Direction::Up),
Action::SwitchOutput(Direction::Down),
]
}
fn shortcuts() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
// Make these searchable in the global settings search.
for action in actions() {
descriptions.insert(super::localize_action(action));
}
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, _section| {
page.model.view().map(crate::pages::Message::NavShortcuts)
})
}

View file

@ -0,0 +1,111 @@
use super::{ShortcutMessage, ShortcutModel};
use cosmic::{Command, Element};
use cosmic_settings_config::shortcuts::action::System as SystemAction;
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
pub struct Page {
model: super::Model,
}
impl Default for Page {
fn default() -> Self {
Self {
model: super::Model::default().actions(|defaults, keybindings| {
actions().iter().fold(Slab::new(), |mut slab, action| {
slab.insert(ShortcutModel::new(defaults, keybindings, action.clone()));
slab
})
}),
}
}
}
impl Page {
pub fn update(&mut self, message: ShortcutMessage) -> Command<crate::app::Message> {
self.model.update(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("system-shortcut", "input-keyboard-symbolic").title(fl!("system-shortcut"))
}
fn content(
&self,
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.context_drawer()
.map(|el| el.map(crate::pages::Message::SystemShortcuts))
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.dialog()
.map(|el| el.map(crate::pages::Message::SystemShortcuts))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
self.model.on_enter();
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.model.on_clear();
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
#[must_use]
pub const fn actions() -> &'static [Action] {
&[
Action::System(SystemAction::AppLibrary),
Action::System(SystemAction::Launcher),
Action::System(SystemAction::WorkspaceOverview),
Action::System(SystemAction::WindowSwitcher),
Action::System(SystemAction::LockScreen),
Action::System(SystemAction::VolumeLower),
Action::System(SystemAction::VolumeRaise),
Action::System(SystemAction::Mute),
Action::System(SystemAction::MuteMic),
Action::System(SystemAction::BrightnessDown),
Action::System(SystemAction::BrightnessUp),
Action::System(SystemAction::KeyboardBrightnessDown),
Action::System(SystemAction::KeyboardBrightnessUp),
Action::System(SystemAction::Screenshot),
Action::System(SystemAction::Terminal),
Action::System(SystemAction::HomeFolder),
Action::System(SystemAction::WebBrowser),
]
}
fn shortcuts() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
// Make these searchable in the global settings search.
for action in actions() {
descriptions.insert(super::localize_action(action));
}
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, _section| {
page.model
.view()
.map(crate::pages::Message::SystemShortcuts)
})
}

View file

@ -0,0 +1,101 @@
use super::{ShortcutMessage, ShortcutModel};
use cosmic::{Command, Element};
use cosmic_settings_config::shortcuts::action::Orientation;
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
pub struct Page {
model: super::Model,
}
impl Default for Page {
fn default() -> Self {
Self {
model: super::Model::default().actions(|defaults, keybindings| {
actions().iter().fold(Slab::new(), |mut slab, action| {
slab.insert(ShortcutModel::new(defaults, keybindings, action.clone()));
slab
})
}),
}
}
}
impl Page {
pub fn update(&mut self, message: ShortcutMessage) -> Command<crate::app::Message> {
self.model.update(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("window-tiling", "input-keyboard-symbolic").title(fl!("window-tiling"))
}
fn content(
&self,
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(shortcuts())])
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.context_drawer()
.map(|el| el.map(crate::pages::Message::TilingShortcuts))
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
self.model
.dialog()
.map(|el| el.map(crate::pages::Message::TilingShortcuts))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
self.model.on_enter();
Command::none()
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
self.model.on_clear();
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
#[must_use]
pub fn actions() -> &'static [Action] {
&[
Action::ToggleTiling,
Action::ToggleStacking,
Action::ToggleWindowFloating,
Action::ToggleOrientation,
Action::Orientation(Orientation::Horizontal),
Action::Orientation(Orientation::Horizontal),
Action::SwapWindow,
]
}
fn shortcuts() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
// Make these searchable in the global settings search.
for action in actions() {
descriptions.insert(super::localize_action(action));
}
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, _section| {
page.model
.view()
.map(crate::pages::Message::TilingShortcuts)
})
}

View file

@ -16,21 +16,28 @@ pub mod time;
pub enum Message {
About(system::about::Message),
Appearance(desktop::appearance::Message),
CustomShortcuts(input::keyboard::shortcuts::custom::Message),
DateAndTime(time::date::Message),
Power(power::Message),
Desktop(desktop::Message),
DesktopOptions(desktop::options::Message),
DesktopWallpaper(desktop::wallpaper::Message),
DesktopWorkspaces(desktop::workspaces::Message),
Displays(display::Message),
Dock(desktop::dock::Message),
DockApplet(desktop::dock::applets::Message),
External { id: String, message: Vec<u8> },
Input(input::Message),
Keyboard(input::keyboard::Message),
KeyboardShortcuts(input::keyboard::shortcuts::Message),
Input(input::Message),
ManageWindowShortcuts(input::keyboard::shortcuts::ShortcutMessage),
MoveWindowShortcuts(input::keyboard::shortcuts::ShortcutMessage),
NavShortcuts(input::keyboard::shortcuts::ShortcutMessage),
Page(Entity),
Panel(desktop::panel::Message),
PanelApplet(desktop::panel::applets_inner::Message),
Power(power::Message),
SystemShortcuts(input::keyboard::shortcuts::ShortcutMessage),
TilingShortcuts(input::keyboard::shortcuts::ShortcutMessage),
}
impl From<Message> for crate::Message {

View file

@ -99,12 +99,9 @@ impl PowerBackend for S76Backend {}
impl SetPowerProfile for S76Backend {
async fn set_power_profile(&self, profile: PowerProfile) {
let daemon = match get_s76power_daemon_proxy().await {
Ok(c) => c,
Err(e) => {
tracing::error!("Problem while setting power profile.");
return;
}
let Ok(daemon) = get_s76power_daemon_proxy().await else {
tracing::error!("Problem while setting power profile.");
return;
};
match profile {
@ -126,19 +123,15 @@ impl SetPowerProfile for S76Backend {
impl GetCurrentPowerProfile for S76Backend {
async fn get_current_power_profile(&self) -> PowerProfile {
let daemon = match get_s76power_daemon_proxy().await {
Ok(c) => c,
Err(e) => {
tracing::error!("Problem while getting power profile.");
//Default
return PowerProfile::Balanced;
}
let Ok(daemon) = get_s76power_daemon_proxy().await else {
tracing::error!("Problem while getting power profile.");
return PowerProfile::Balanced;
};
match daemon.get_profile().await {
Ok(p) => PowerProfile::from_string(p.as_str()),
//Default
Err(_) => {
Err(_why) => {
tracing::error!("Problem while getting power profile.");
//Default
PowerProfile::Balanced
@ -173,7 +166,7 @@ impl SetPowerProfile for PPBackend {
async fn set_power_profile(&self, profile: PowerProfile) {
let daemon = match get_power_profiles_proxy().await {
Ok(c) => c,
Err(e) => {
Err(()) => {
tracing::error!("Problem while setting power profile.");
return;
}
@ -198,18 +191,14 @@ impl SetPowerProfile for PPBackend {
impl GetCurrentPowerProfile for PPBackend {
async fn get_current_power_profile(&self) -> PowerProfile {
let daemon = match get_power_profiles_proxy().await {
Ok(c) => c,
Err(e) => {
tracing::error!("Problem while getting power profile.");
//Default
return PowerProfile::Balanced;
}
let Ok(daemon) = get_power_profiles_proxy().await else {
tracing::error!("Problem while getting power profile.");
return PowerProfile::Balanced;
};
let profile = match daemon.active_profile().await {
Ok(p) => p,
Err(e) => {
Err(_e) => {
tracing::error!("Problem while getting power profile.");
//Default
return PowerProfile::Balanced;

View file

@ -1,4 +1,4 @@
use zbus::{proxy, Connection};
use zbus::proxy;
#[proxy(
interface = "org.freedesktop.UPower.PowerProfiles",

View file

@ -1,4 +1,5 @@
use zbus::{proxy, Connection};
use zbus::proxy;
#[proxy(
interface = "com.system76.PowerDaemon",
default_path = "/com/system76/PowerDaemon",

View file

@ -46,7 +46,7 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(
&mut self,
_page: page::Entity,
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
command::future(async move {
crate::pages::Message::About(Message::Info(Box::new(Info::load())))

View file

@ -135,11 +135,6 @@ dock = Док
hot-corner = Гарачы вугал
.top-left-corner = Уключыць верхні левы гарачы вугал для працоўных прастораў
super-key-action = Дзеянне клавішы Super
.launcher = Запускальнік
.workspaces = Працоўныя прасторы
.applications = Праграмы
top-panel = Верхняя панэль
.workspaces = Паказаць кнопку Працоўныя прасторы
.applications = Паказаць кнопку Праграмы

View file

@ -23,11 +23,6 @@ notifications = Oznámení
desktop-panel-options = Plocha a Panel
.desc = Činnost super klávesy, roh obrazovky, nastavení ovládání oken.
super-key-action = Činnost super klávesy
.launcher = Spouštěč
.workspaces = Pracovní plochy
.applications = Applikace
hot-corner = Rohy
.top-left-corner = Povolit použití levého horního rohu pro otevření pracovních ploch

View file

@ -26,11 +26,6 @@ notifications = Benachrichtigungen
desktop-options = Desktopoptionen
.desc = Super Key Aktion, hot corners, Fenstersteuerung.
super-key-action = Super Key Aktion
.launcher = Launcher
.workspaces = Arbeitsbereiche
.applications = Apps
hot-corner = Hot Corner
.top-left-corner = Enable top-left hot corner for Workspaces

View file

@ -136,10 +136,10 @@ dock = Dock
hot-corner = Hot Corner
.top-left-corner = Enable top-left hot corner for Workspaces
super-key-action = Super Key Action
.launcher = Launcher
.workspaces = Workspaces
.applications = Applications
super-key = Super key
.launcher = Open Launcher
.workspaces = Open Workspaces
.applications = Open Applications
top-panel = Top Panel
.workspaces = Show Workspaces Button
@ -401,6 +401,125 @@ type-to-search = Type to search...
keyboard-shortcuts = Keyboard Shortcuts
.desc = View and customize shortcuts
add-keybinding = Add keybinding
cancel = Cancel
command = Command
custom = Custom
debug = Debug
disabled = Disabled
migrate-workspace-prev = Migrate workspace to previous output
migrate-workspace-next = Migrate workspace to next output
migrate-workspace = Migrate workspace to output { $direction ->
*[down] down
[left] left
[right] right
[up] up
}
navigate = Navigate
replace = Replace
shortcut-name = Shortcut name
system-controls = System controls
terminate = Terminate
toggle-stacking = Toggle window stacking
type-key-combination = Type key combination
unknown = Unknown
custom-shortcuts = Custom Shortcuts
.add = Add shortcut
.context = Add Custom Shortcut
.none = No custom shortcuts
modified = { $count } modified
nav-shortcuts = Navigation
.prev-output = Focus previous output
.next-output = Focus next output
.last-workspace = Focus last workspace
.prev-workspace = Focus previous workspace
.next-workspace = Focus next workspace
.focus = Focus window { $direction ->
*[down] down
[in] in
[left] left
[out] out
[right] right
[up] up
}
.output = Switch to output { $direction ->
*[down] down
[left] left
[right] right
[up] up
}
.workspace = Switch to workspace { $num }
manage-windows = Manage windows
.close = Close window
.maximize = Maximize window
.minimize = Minimize window
.resize-inwards = Resize window inwards
.resize-outwards = Resize window outwards
.toggle-sticky = Toggle sticky window
move-windows = Move Windows
.direction = Move window { $direction ->
*[down] down
[left] left
[right] right
[up] up
}
.display = Move window one monitor { $direction ->
*[down] down
[left] left
[right] right
[up] up
}
.workspace = Move window one workspace { $direction ->
*[below] below
[left] left
[right] right
[above] above
}
.workspace-num = Move window to workspace { $num }
.prev-workspace = Move window to prev workspace
.next-workspace = Move window to next workspace
.last-workspace = Move window to last workspace
.next-display = Move window to next display
.prev-display = Move window to prev display
.send-to-prev-workspace = Move window to previous workspace
.send-to-next-workspace = Move window to next workspace
system-shortcut = System
.app-library = Open the app library
.brightness-down = Decrease display brightness
.brightness-up = Increase display brightness
.home-folder = Open home folder
.keyboard-brightness-down = Decrease keyboard brightness
.keyboard-brightness-up = Increase keyboard brightness
.launcher = Open the launcher
.lock-screen = Lock the screen
.mute = Mute audio output
.mute-mic = Mutes microphone input
.screenshot = Take a screenshot
.terminal = Open a terminal
.volume-lower = Decrease audio output volume
.volume-raise = Increase audio output volume
.web-browser = Opens a web browser
.window-switcher = Switch between open windows
.workspace-overview = Open the workspace overview
window-tiling = Window tiling
.horizontal = Set horizontal orientation
.vertical = Set vertical orientation
.swap-window = Swap window
.toggle-tiling = Toggle window tiling
.toggle-stacking = Toggle window stacking
.toggle-floating = Toggle window floating
.toggle-orientation = Toggle orientation
replace-shortcut-dialog = Replace Shortcut?
.desc = { $shortcut } is used by { $name }. If you replace it, { $name } will be disabled.
## Input: Mouse
mouse = Mouse
@ -443,13 +562,13 @@ open-workspaces-view = Open Workspaces Overview
## Power
power = Power
.desc = Manage power settings
.desc = Manage power settings
power-mode = Power Mode
.performance = High performance
.balanced = Balanced
.battery = Extended battery life
.performance-desc = Peak performance and power usage.
.balanced-desc = Quiet performance and moderate power usage.
.battery-desc = Reduced power usage and silent performance.
.nobackend = Backend not found. Install system76-power or power-profiles-daemon.
.performance = High performance
.balanced = Balanced
.battery = Extended battery life
.performance-desc = Peak performance and power usage.
.balanced-desc = Quiet performance and moderate power usage.
.battery-desc = Reduced power usage and silent performance.
.nobackend = Backend not found. Install system76-power or power-profiles-daemon.

View file

@ -136,11 +136,6 @@ dock = Dock
hot-corner = Esquina Activa
.top-left-corner = Habilitar esquina superior izquierda para Espacios de Trabajo
super-key-action = Acción de la tecla Super
.launcher = Lanzador
.workspaces = Espacios de Trabajo
.applications = Aplicaciones
top-panel = Panel Superior
.workspaces = Mostrar botón de Espacios de Trabajo
.applications = Mostrar botón de Aplicaciones

View file

@ -23,11 +23,6 @@ notifications = اعلانات
desktop-panel-options = تنظیمات پنل میزکار
.desc = عملکرد کلید سوپر، گوشه‌های داغ، گزینه‌های کنترل پنجره.
super-key-action = عملکرد کلید سوپر
.launcher = راه انداز
.workspaces = فضاهای کاری
.applications = برنامه‌ها
hot-corner = گوشه‌داغ
.top-left-corner = فعال سازی گوشه‌داغ بالا چپ برای فضاهای کاری

View file

@ -136,11 +136,6 @@ dock = Dock
hot-corner = Coin Actif
.top-left-corner = Activer le coin actif supérieur gauche pour les Espaces de Travail
super-key-action = Action de la touche Super
.launcher = Lanceur
.workspaces = Espaces de Travail
.applications = Applications
top-panel = Panneau supérieur
.workspaces = Afficher le bouton Espace de Travail
.applications = Afficher le bouton Applications

View file

@ -27,11 +27,6 @@ notifications = सूचनाएं
desktop-options = डेस्कटॉप विकल्प
.desc = सुपर की एक्शन, हॉट कॉर्नर, विंडो कंट्रोल विकल्प।
super-key-action = सुपर की एक्शन
.launcher = लांचर
.workspaces = कार्यस्थानों
.applications = अनुप्रयोग
hot-corner = गर्म कोना
.top-left-corner = कार्यस्थानों के लिए शीर्ष-बाएँ हॉट कॉर्नर को सक्षम करें

View file

@ -125,11 +125,6 @@ dock = Barra delle applicazioni
hot-corner = Bordi reattivi
.top-left-corner = Abilita bordo reattivo in alto a sinistra per gli spazi di lavoro
super-key-action = Azione tasto Super
.launcher = Launcher
.workspaces = Spazi di lavoro
.applications = Applicazioni
top-panel = Pannello superiore
.workspaces = Pulsante "mostra spazi di lavoro"
.applications = Pulsante "mostra applicazioni"

View file

@ -134,11 +134,6 @@ dock = ドック
hot-corner = ホットコーナー
.top-left-corner = ワークスペースための左上のホットコーナーを有効にする
super-key-action = スーパーキーの行動
.launcher = ランチャー
.workspaces = ワークスペース
.applications = アプリケーション
top-panel = トップパネル
.workspaces = ワークスペースボタンを表示
.applications = アプリケーションボタンを表示

View file

@ -141,11 +141,6 @@ dock = Dok
hot-corner = Narożniki Funkcyjne
.top-left-corner = Włącz Obszary Robocze w lewym górnym narożniku funkcyjnym.
super-key-action = Akcje Klawisza Super
.launcher = Program Startowy
.workspaces = Obszary Robocze
.applications = Aplikacje
top-panel = Górny Panel
.workspaces = Pokaż Przycisk Obszarów Roboczych
.applications = Pokaż Przycisk Aplikacji

View file

@ -136,11 +136,6 @@ dock = Dock
hot-corner = Canto ativo
.top-left-corner = Ativar o canto superior esquerdo para áreas de trabalho
super-key-action = Ações da tecla Super
.launcher = Inicializador
.workspaces = Áreas de Trabalho
.applications = Aplicações
top-panel = Painel superior
.workspaces = Mostrar Botão de Áreas de Trabalho
.applications = Mostrar Botão de Aplicações

View file

@ -136,11 +136,6 @@ dock = Doca
hot-corner = Canto ativo
.top-left-corner = Ativar o canto superior esquerdo para as áreas de trabalho
super-key-action = Ação da tecla Super
.launcher = Lançador
.workspaces = Áreas de trabalho
.applications = Aplicações
top-panel = Painel superior
.workspaces = Mostrar o botão das áreas de trabalho
.applications = Mostrar o botão das aplicações

View file

@ -136,11 +136,6 @@ dock = Dock
hot-corner = Marginea ecranului
.top-left-corner = Activați marginea din stânga sus pentru Spații de lucru
super-key-action = Acțiunea butonului Super
.launcher = Lansator
.workspaces = Spații de lucru
.applications = Aplicații
top-panel = Panoul de sus
.workspaces = Afișați butonul pentru Spații de lucru
.applications = Afișați butonul pentru aplicații

View file

@ -136,11 +136,6 @@ dock = Док
hot-corner = Активные углы
.top-left-corner = Открывать рабочие места при наведении в левый верхний угол
super-key-action = Действие кнопки Super
.launcher = Панель запуска
.workspaces = Рабочие места
.applications = Приложения
top-panel = Верхняя панель
.workspaces = Отображать кнопку «Рабочие места»
.applications = Отображать кнопку «Приложения»

View file

@ -121,11 +121,6 @@ dock = Док
hot-corner = Лепљиви углови
.top-left-corner = Укључити горњи леви лепљиви угао за радне просторе
super-key-action = Улога Super тастера
.launcher = Покретач апликација
.workspaces = Радни простори
.applications = Апликације
top-panel = Горњи панел
.workspaces = Дугме за приказивање радних простора
.applications = Дугме за приказивање апликација

View file

@ -121,11 +121,6 @@ dock = Dok
hot-corner = Lepljivi uglovi
.top-left-corner = Uključiti gornji levi lepljivi ugao za radne prostore
super-key-action = Uloga Super tastera
.launcher = Pokretač aplikacija
.workspaces = Radni prostori
.applications = Aplikacije
top-panel = Gornji panel
.workspaces = Dugme za prikazivanje radnih prostora
.applications = Dugme za prikazivanje aplikacija

View file

@ -118,11 +118,6 @@ dock = Docka
hot-corner = Het hörn
.top-left-corner = Aktivera det övre vänstra hörnet för arbetsytor
super-key-action = Supertangent åtgärd
.launcher = Programstartare
.workspaces = Arbetsytor
.applications = Applikationer
top-panel = Övre Panel
.workspaces = Visa knappen arbetsytor
.applications = Visa knappen applikationer

View file

@ -24,11 +24,6 @@ notifications = Bildirimler
desktop-panel-options = Masaüstü ve Panel
.desc = Logo Tuşu eylemi, hızlı köşeler, pencere kontrol seçenekleri.
super-key-action = Logo Tuşu Eylemi
.launcher = Başlatıcı
.workspaces = Çalışma Alanları
.applications = Uygulamalar
hot-corner = Hızlı Köşe
.top-left-corner = Çalışma Alanları için sol-üst hızlı köşeyi etkinleştir

View file

@ -136,11 +136,6 @@ dock = 程序坞
hot-corner = 热区
.top-left-corner = 启用左上角热区来查看工作区概览
super-key-action = Super 键行为
.launcher = 启动器
.workspaces = 工作区概览
.applications = 应用程序库
top-panel = 顶部面板
.workspaces = 显示工作区概览按钮
.applications = 显示应用程序库按钮

View file

@ -136,11 +136,6 @@ dock = Dock
hot-corner = 螢幕角落熱點
.top-left-corner = 為工作區啟用位於左上方的螢幕角落熱點
super-key-action = Super 按鍵行為
.launcher = 啟動器
.workspaces = 工作區
.applications = 應用程式
top-panel = 頂部面板
.workspaces = 顯示工作區按鈕
.applications = 顯示應用程式按鈕

View file

@ -38,4 +38,21 @@ impl<'a, Message: 'static> Insert<'a, Message> {
self
}
#[allow(clippy::return_self_not_must_use)]
#[allow(clippy::must_use_candidate)]
pub fn sub_page_with_id<P: AutoBind<Message>>(&mut self) -> Entity {
let sub_page = self.model.register::<P>().id();
self.model.info[sub_page].parent = Some(self.id);
self.model
.sub_pages
.entry(self.id)
.expect("parent page missing")
.and_modify(|v| v.push(sub_page))
.or_insert_with(|| vec![sub_page]);
sub_page
}
}

View file

@ -43,6 +43,11 @@ pub trait Page<Message: 'static>: Downcast {
None
}
/// Set a custom page header
fn header(&self) -> Option<Element<'_, Message>> {
None
}
/// Display an inner app dialog for the page.
fn dialog(&self) -> Option<Element<'_, Message>> {
None
@ -116,6 +121,7 @@ impl Info {
#[macro_export]
macro_rules! update {
($binder:expr, $message:expr, $page:ty) => {{
#[allow(unused_must_use)]
if let Some(page) = $binder.page_mut::<$page>() {
page.update($message);
}