feat: add editable dock launchers
This commit is contained in:
parent
9677159db6
commit
d58fe9d74c
5 changed files with 735 additions and 36 deletions
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use crate::{
|
||||
fl,
|
||||
launcher_edit::{self, LauncherEditRequest},
|
||||
wayland_subscription::{
|
||||
OutputUpdate, ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
|
||||
wayland_subscription,
|
||||
|
|
@ -47,7 +48,7 @@ use cosmic::{
|
|||
icon::{self, from_name},
|
||||
image::Handle,
|
||||
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
|
||||
svg, text,
|
||||
svg, text, text_input,
|
||||
},
|
||||
};
|
||||
use cosmic::{
|
||||
|
|
@ -387,10 +388,25 @@ pub struct Popup {
|
|||
popup_type: PopupType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LauncherEditState {
|
||||
original_app_id: String,
|
||||
source_path: PathBuf,
|
||||
original_name: String,
|
||||
original_exec: String,
|
||||
name: String,
|
||||
exec: String,
|
||||
icon: String,
|
||||
terminal: bool,
|
||||
saving: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct CosmicAppList {
|
||||
core: cosmic::app::Core,
|
||||
popup: Option<Popup>,
|
||||
launcher_edit: Option<LauncherEditState>,
|
||||
subscription_ctr: u32,
|
||||
item_ctr: u32,
|
||||
desktop_entries: Vec<DesktopEntry>,
|
||||
|
|
@ -432,6 +448,7 @@ struct CosmicAppList {
|
|||
pub enum PopupType {
|
||||
RightClickMenu,
|
||||
ToplevelList,
|
||||
LauncherEditor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -440,6 +457,13 @@ enum Message {
|
|||
Wayland(WaylandUpdate),
|
||||
PinApp(u32),
|
||||
UnpinApp(u32),
|
||||
EditLauncher(u32),
|
||||
LauncherNameChanged(String),
|
||||
LauncherExecChanged(String),
|
||||
LauncherIconChanged(String),
|
||||
SaveLauncherEdit,
|
||||
CancelLauncherEdit,
|
||||
LauncherEditSaved(Result<launcher_edit::LauncherEditResult, String>),
|
||||
/// Yoda: pointer entered (Some) or left (None) a dock icon — drives
|
||||
/// the macOS Tahoe-style hover magnification effect.
|
||||
DockItemHover(Option<DockItemId>),
|
||||
|
|
@ -822,10 +846,45 @@ impl CosmicAppList {
|
|||
.collect();
|
||||
}
|
||||
|
||||
fn sync_pinned_list_from_config(&mut self) {
|
||||
for item in self.pinned_list.drain(..) {
|
||||
if !item.toplevels.is_empty() {
|
||||
self.active_list.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
self.pinned_list = find_desktop_entries(&self.desktop_entries, &self.config.favorites)
|
||||
.zip(&self.config.favorites)
|
||||
.map(|(de, original_id)| {
|
||||
if let Some(p) = self
|
||||
.active_list
|
||||
.iter()
|
||||
.position(|dock_item| dock_item.desktop_info.id() == de.id())
|
||||
{
|
||||
let mut d = self.active_list.remove(p);
|
||||
d.desktop_info = de.clone();
|
||||
d.original_app_id.clone_from(original_id);
|
||||
d
|
||||
} else {
|
||||
self.item_ctr += 1;
|
||||
DockItem {
|
||||
id: self.item_ctr,
|
||||
toplevels: Vec::new(),
|
||||
desktop_info: de.clone(),
|
||||
original_app_id: original_id.clone(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Close any open popups.
|
||||
fn close_popups(&mut self) -> Task<cosmic::Action<Message>> {
|
||||
let mut commands = Vec::new();
|
||||
if let Some(popup) = self.popup.take() {
|
||||
if popup.popup_type == PopupType::LauncherEditor {
|
||||
self.launcher_edit = None;
|
||||
}
|
||||
commands.push(destroy_popup(popup.id));
|
||||
}
|
||||
if let Some(popup) = self.overflow_active_popup.take() {
|
||||
|
|
@ -1199,6 +1258,182 @@ impl cosmic::Application for CosmicAppList {
|
|||
return destroy_popup(popup_id);
|
||||
}
|
||||
}
|
||||
Message::EditLauncher(id) => {
|
||||
let Some(dock_item) = self.pinned_list.iter().find(|t| t.id == id).cloned() else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(exec) = dock_item.desktop_info.exec() else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(existing_popup) = self.popup.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(rectangle) = self.rectangles.get(&dock_item.id.into()) else {
|
||||
self.popup = Some(existing_popup);
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let original_name = dock_item
|
||||
.desktop_info
|
||||
.desktop_entry("Name")
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| {
|
||||
dock_item
|
||||
.desktop_info
|
||||
.name(&self.locales)
|
||||
.map(Cow::into_owned)
|
||||
})
|
||||
.unwrap_or_else(|| dock_item.original_app_id.clone());
|
||||
let original_exec = exec.to_string();
|
||||
|
||||
self.launcher_edit = Some(LauncherEditState {
|
||||
original_app_id: dock_item.original_app_id.clone(),
|
||||
source_path: dock_item.desktop_info.path.clone(),
|
||||
original_name: original_name.clone(),
|
||||
original_exec: original_exec.clone(),
|
||||
name: original_name,
|
||||
exec: original_exec,
|
||||
icon: dock_item
|
||||
.desktop_info
|
||||
.icon()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
terminal: dock_item.desktop_info.terminal(),
|
||||
saving: false,
|
||||
error: None,
|
||||
});
|
||||
|
||||
let new_id = window::Id::unique();
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
existing_popup.parent,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let iced::Rectangle {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
} = *rectangle;
|
||||
popup_settings.positioner.anchor_rect = iced::Rectangle::<i32> {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
};
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_width(480.)
|
||||
.min_height(1.)
|
||||
.max_width(520.)
|
||||
.max_height(1000.);
|
||||
|
||||
self.popup = Some(Popup {
|
||||
parent: existing_popup.parent,
|
||||
id: new_id,
|
||||
dock_item,
|
||||
popup_type: PopupType::LauncherEditor,
|
||||
});
|
||||
|
||||
return Task::batch([destroy_popup(existing_popup.id), get_popup(popup_settings)]);
|
||||
}
|
||||
Message::LauncherNameChanged(name) => {
|
||||
if let Some(edit) = self.launcher_edit.as_mut()
|
||||
&& !edit.saving
|
||||
{
|
||||
edit.name = name;
|
||||
edit.error = None;
|
||||
}
|
||||
}
|
||||
Message::LauncherExecChanged(exec) => {
|
||||
if let Some(edit) = self.launcher_edit.as_mut()
|
||||
&& !edit.saving
|
||||
{
|
||||
edit.exec = exec;
|
||||
edit.error = None;
|
||||
}
|
||||
}
|
||||
Message::LauncherIconChanged(icon) => {
|
||||
if let Some(edit) = self.launcher_edit.as_mut()
|
||||
&& !edit.saving
|
||||
{
|
||||
edit.icon = icon;
|
||||
edit.error = None;
|
||||
}
|
||||
}
|
||||
Message::SaveLauncherEdit => {
|
||||
let Some(edit) = self.launcher_edit.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
if edit.saving {
|
||||
return Task::none();
|
||||
}
|
||||
if let Err(error) =
|
||||
launcher_edit::validate_launcher_fields(&edit.name, &edit.exec, &edit.icon)
|
||||
{
|
||||
edit.error = Some(error);
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let request = LauncherEditRequest {
|
||||
current_app_id: edit.original_app_id.clone(),
|
||||
source_path: edit.source_path.clone(),
|
||||
name: edit.name.clone(),
|
||||
exec: edit.exec.clone(),
|
||||
icon: edit.icon.clone(),
|
||||
terminal: edit.terminal,
|
||||
replace_localized_name: edit.name.trim() != edit.original_name.trim(),
|
||||
disable_dbus_activation: edit.exec.trim() != edit.original_exec.trim(),
|
||||
};
|
||||
|
||||
edit.saving = true;
|
||||
edit.error = None;
|
||||
|
||||
return Task::perform(launcher_edit::save_launcher_edit(request), |result| {
|
||||
cosmic::Action::App(Message::LauncherEditSaved(result))
|
||||
});
|
||||
}
|
||||
Message::CancelLauncherEdit => {
|
||||
return self.close_popups();
|
||||
}
|
||||
Message::LauncherEditSaved(result) => match result {
|
||||
Ok(result) => {
|
||||
tracing::info!(
|
||||
app_id = result.new_app_id,
|
||||
path = ?result.path,
|
||||
"saved editable launcher"
|
||||
);
|
||||
|
||||
let mut favorites = self.config.favorites.clone();
|
||||
let mut favorites_changed = false;
|
||||
for favorite in &mut favorites {
|
||||
if *favorite == result.old_app_id && *favorite != result.new_app_id {
|
||||
*favorite = result.new_app_id.clone();
|
||||
favorites_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if favorites_changed {
|
||||
self.config.update_pinned(
|
||||
favorites.clone(),
|
||||
&Config::new(APP_ID, AppListConfig::VERSION).unwrap(),
|
||||
);
|
||||
self.config.favorites = favorites;
|
||||
}
|
||||
|
||||
self.update_desktop_entries();
|
||||
self.sync_pinned_list_from_config();
|
||||
self.launcher_edit = None;
|
||||
return self.close_popups();
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(edit) = self.launcher_edit.as_mut() {
|
||||
edit.saving = false;
|
||||
edit.error = Some(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::Activate(handle) => {
|
||||
if let Some(tx) = self.wayland_sender.as_ref() {
|
||||
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
|
||||
|
|
@ -1651,6 +1886,9 @@ impl cosmic::Application for CosmicAppList {
|
|||
},
|
||||
Message::ClosePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
if p.popup_type == PopupType::LauncherEditor {
|
||||
self.launcher_edit = None;
|
||||
}
|
||||
return destroy_popup(p.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1665,42 +1903,16 @@ impl cosmic::Application for CosmicAppList {
|
|||
}
|
||||
Message::ConfigUpdated(config) => {
|
||||
self.config = config;
|
||||
// drain to active list
|
||||
for item in self.pinned_list.drain(..) {
|
||||
if !item.toplevels.is_empty() {
|
||||
self.active_list.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// pull back configured items into the favorites list
|
||||
self.pinned_list =
|
||||
find_desktop_entries(&self.desktop_entries, &self.config.favorites)
|
||||
.zip(&self.config.favorites)
|
||||
.map(|(de, original_id)| {
|
||||
if let Some(p) = self
|
||||
.active_list
|
||||
.iter()
|
||||
// match using heuristic id
|
||||
.position(|dock_item| dock_item.desktop_info.id() == de.id())
|
||||
{
|
||||
let mut d = self.active_list.remove(p);
|
||||
// but use the id from the config
|
||||
d.original_app_id.clone_from(original_id);
|
||||
d
|
||||
} else {
|
||||
self.item_ctr += 1;
|
||||
DockItem {
|
||||
id: self.item_ctr,
|
||||
toplevels: Vec::new(),
|
||||
desktop_info: de.clone(),
|
||||
original_app_id: original_id.clone(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.update_desktop_entries();
|
||||
self.sync_pinned_list_from_config();
|
||||
}
|
||||
Message::CloseRequested(id) => {
|
||||
if Some(id) == self.popup.as_ref().map(|p| p.id) {
|
||||
if let Some(popup) = &self.popup
|
||||
&& popup.id == id
|
||||
{
|
||||
if popup.popup_type == PopupType::LauncherEditor {
|
||||
self.launcher_edit = None;
|
||||
}
|
||||
self.popup = None;
|
||||
}
|
||||
if self.overflow_active_popup.is_some_and(|p| p == id) {
|
||||
|
|
@ -2372,6 +2584,19 @@ impl cosmic::Application for CosmicAppList {
|
|||
}),
|
||||
);
|
||||
|
||||
if is_pinned && desktop_info.exec().is_some() {
|
||||
content = content.push(
|
||||
menu_button(
|
||||
row![
|
||||
icon::icon(from_name("edit-symbolic").into()).size(16),
|
||||
text::body(fl!("edit-launcher"))
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
.on_press(Message::EditLauncher(*id)),
|
||||
);
|
||||
}
|
||||
|
||||
if !toplevels.is_empty() {
|
||||
content = content.push(divider::horizontal::light());
|
||||
content = match toplevels.len() {
|
||||
|
|
@ -2418,6 +2643,89 @@ impl cosmic::Application for CosmicAppList {
|
|||
)
|
||||
.into()
|
||||
}
|
||||
PopupType::LauncherEditor => {
|
||||
let Some(edit) = self.launcher_edit.as_ref() else {
|
||||
return text::body("").into();
|
||||
};
|
||||
|
||||
let spacing = theme::spacing();
|
||||
let can_save = !edit.saving
|
||||
&& launcher_edit::validate_launcher_fields(
|
||||
&edit.name, &edit.exec, &edit.icon,
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
let mut form = column![
|
||||
text::title4(fl!("edit-launcher")),
|
||||
text_input("", edit.name.as_str())
|
||||
.label(fl!("launcher-name"))
|
||||
.on_input(Message::LauncherNameChanged)
|
||||
.on_submit(|_| Message::SaveLauncherEdit)
|
||||
.width(Length::Fill)
|
||||
.size(14),
|
||||
text_input("", edit.exec.as_str())
|
||||
.label(fl!("launcher-command"))
|
||||
.on_input(Message::LauncherExecChanged)
|
||||
.on_submit(|_| Message::SaveLauncherEdit)
|
||||
.width(Length::Fill)
|
||||
.size(14),
|
||||
text_input("", edit.icon.as_str())
|
||||
.label(fl!("launcher-icon"))
|
||||
.on_input(Message::LauncherIconChanged)
|
||||
.on_submit(|_| Message::SaveLauncherEdit)
|
||||
.width(Length::Fill)
|
||||
.size(14),
|
||||
]
|
||||
.spacing(spacing.space_s)
|
||||
.width(Length::Fill);
|
||||
|
||||
if let Some(error) = edit.error.as_ref() {
|
||||
form = form.push(text::caption(error.as_str()).class(
|
||||
cosmic::theme::Text::Color(theme.cosmic().destructive_color().into()),
|
||||
));
|
||||
}
|
||||
|
||||
let cancel =
|
||||
button::custom(text::body(fl!("cancel")).center().width(Length::Fill))
|
||||
.on_press_maybe(if edit.saving {
|
||||
None
|
||||
} else {
|
||||
Some(Message::CancelLauncherEdit)
|
||||
})
|
||||
.padding([spacing.space_xxs, spacing.space_s])
|
||||
.width(142);
|
||||
|
||||
let save = button::custom(text::body(fl!("save")).center().width(Length::Fill))
|
||||
.class(Button::Suggested)
|
||||
.on_press_maybe(if can_save {
|
||||
Some(Message::SaveLauncherEdit)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.padding([spacing.space_xxs, spacing.space_s])
|
||||
.width(142);
|
||||
|
||||
let actions = row![horizontal_space(), cancel, save]
|
||||
.spacing(spacing.space_xxs)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let content = column![form, actions]
|
||||
.spacing(spacing.space_m)
|
||||
.padding(spacing.space_m)
|
||||
.width(Length::Fill);
|
||||
|
||||
self.core
|
||||
.applet
|
||||
.popup_container(container(content).width(Length::Fill))
|
||||
.limits(
|
||||
Limits::NONE
|
||||
.min_width(480.)
|
||||
.min_height(1.)
|
||||
.max_width(520.)
|
||||
.max_height(1000.),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
PopupType::ToplevelList => match self.core.applet.anchor {
|
||||
PanelAnchor::Left | PanelAnchor::Right => {
|
||||
let mut content =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue