Adds a way to dismiss workspace notifications (#30015)
Closes https://github.com/zed-industries/zed/issues/10140 * On `menu::Cancel` action (`ESC`), close notifications, one by one, if `Workspace` gets to handle this action. More specific, focused items contexts (e.g. `Editor`) take priority. * Allows to temporarily suppress notifications of this kind either by clicking a corresponding button in the UI, or using `workspace::SuppressNotification` action. This might not work well out of the box for all notifications and might require further improvement. https://github.com/user-attachments/assets/0ea49ee6-cd21-464f-ba74-fc40f7a8dedf Release Notes: - Added a way to dismiss workspace notifications
This commit is contained in:
parent
7d361ec97e
commit
007fd0586a
5 changed files with 96 additions and 13 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -18158,6 +18158,7 @@ dependencies = [
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
|
"menu",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
|
|
|
@ -22,7 +22,9 @@ use ui::{
|
||||||
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
|
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
|
||||||
};
|
};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
|
use workspace::notifications::{
|
||||||
|
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
|
||||||
|
};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
Workspace,
|
Workspace,
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
|
@ -823,6 +825,11 @@ impl Render for NotificationToast {
|
||||||
IconButton::new("close", IconName::Close)
|
IconButton::new("close", IconName::Close)
|
||||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("suppress", IconName::XCircle)
|
||||||
|
.tooltip(Tooltip::text("Do not show until restart"))
|
||||||
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
|
||||||
|
)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.focus_notification_panel(window, cx);
|
this.focus_notification_panel(window, cx);
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
|
@ -831,3 +838,4 @@ impl Render for NotificationToast {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for NotificationToast {}
|
impl EventEmitter<DismissEvent> for NotificationToast {}
|
||||||
|
impl EventEmitter<SuppressEvent> for NotificationToast {}
|
||||||
|
|
|
@ -43,6 +43,7 @@ http_client.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
menu.workspace = true
|
||||||
node_runtime.workspace = true
|
node_runtime.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl std::ops::DerefMut for Notifications {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum NotificationId {
|
pub enum NotificationId {
|
||||||
Unique(TypeId),
|
Unique(TypeId),
|
||||||
Composite(TypeId, ElementId),
|
Composite(TypeId, ElementId),
|
||||||
|
@ -54,7 +54,12 @@ impl NotificationId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Notification: EventEmitter<DismissEvent> + Focusable + Render {}
|
pub trait Notification:
|
||||||
|
EventEmitter<DismissEvent> + EventEmitter<SuppressEvent> + Focusable + Render
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SuppressEvent;
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -81,6 +86,13 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.subscribe(¬ification, {
|
||||||
|
let id = id.clone();
|
||||||
|
move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
|
||||||
|
workspace.suppress_notification(&id, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
notification.into()
|
notification.into()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -96,6 +108,9 @@ impl Workspace {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,
|
build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,
|
||||||
) {
|
) {
|
||||||
|
if self.suppressed_notifications.contains(id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.dismiss_notification(id, cx);
|
self.dismiss_notification(id, cx);
|
||||||
self.notifications
|
self.notifications
|
||||||
.push((id.clone(), build_notification(cx)));
|
.push((id.clone(), build_notification(cx)));
|
||||||
|
@ -172,6 +187,11 @@ impl Workspace {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
|
||||||
|
self.dismiss_notification(id, cx);
|
||||||
|
self.suppressed_notifications.insert(id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
|
pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
|
||||||
// Allow absence of the global so that tests don't need to initialize it.
|
// Allow absence of the global so that tests don't need to initialize it.
|
||||||
let app_notifications = GLOBAL_APP_NOTIFICATIONS
|
let app_notifications = GLOBAL_APP_NOTIFICATIONS
|
||||||
|
@ -268,6 +288,14 @@ impl Render for LanguageServerPrompt {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
IconButton::new("suppress", IconName::XCircle)
|
||||||
|
.tooltip(Tooltip::text("Do not show until restart"))
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|_, _, _, cx| cx.emit(SuppressEvent)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("copy", IconName::Copy)
|
IconButton::new("copy", IconName::Copy)
|
||||||
.on_click({
|
.on_click({
|
||||||
|
@ -305,6 +333,7 @@ impl Render for LanguageServerPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
|
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
|
||||||
|
impl EventEmitter<SuppressEvent> for LanguageServerPrompt {}
|
||||||
|
|
||||||
fn workspace_error_notification_id() -> NotificationId {
|
fn workspace_error_notification_id() -> NotificationId {
|
||||||
struct WorkspaceErrorNotification;
|
struct WorkspaceErrorNotification;
|
||||||
|
@ -401,6 +430,7 @@ impl Focusable for ErrorMessagePrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
|
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
|
||||||
|
impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
|
||||||
|
|
||||||
impl Notification for ErrorMessagePrompt {}
|
impl Notification for ErrorMessagePrompt {}
|
||||||
|
|
||||||
|
@ -411,9 +441,9 @@ pub mod simple_message_notification {
|
||||||
AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
|
AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
|
||||||
SharedString, Styled, div,
|
SharedString, Styled, div,
|
||||||
};
|
};
|
||||||
use ui::prelude::*;
|
use ui::{Tooltip, prelude::*};
|
||||||
|
|
||||||
use super::Notification;
|
use super::{Notification, SuppressEvent};
|
||||||
|
|
||||||
pub struct MessageNotification {
|
pub struct MessageNotification {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
@ -429,6 +459,7 @@ pub mod simple_message_notification {
|
||||||
more_info_message: Option<SharedString>,
|
more_info_message: Option<SharedString>,
|
||||||
more_info_url: Option<Arc<str>>,
|
more_info_url: Option<Arc<str>>,
|
||||||
show_close_button: bool,
|
show_close_button: bool,
|
||||||
|
show_suppress_button: bool,
|
||||||
title: Option<SharedString>,
|
title: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +470,7 @@ pub mod simple_message_notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for MessageNotification {}
|
impl EventEmitter<DismissEvent> for MessageNotification {}
|
||||||
|
impl EventEmitter<SuppressEvent> for MessageNotification {}
|
||||||
|
|
||||||
impl Notification for MessageNotification {}
|
impl Notification for MessageNotification {}
|
||||||
|
|
||||||
|
@ -470,6 +502,7 @@ pub mod simple_message_notification {
|
||||||
more_info_message: None,
|
more_info_message: None,
|
||||||
more_info_url: None,
|
more_info_url: None,
|
||||||
show_close_button: true,
|
show_close_button: true,
|
||||||
|
show_suppress_button: true,
|
||||||
title: None,
|
title: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
|
@ -568,6 +601,11 @@ pub mod simple_message_notification {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_suppress_button(mut self, show: bool) -> Self {
|
||||||
|
self.show_suppress_button = show;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_title<S>(mut self, title: S) -> Self
|
pub fn with_title<S>(mut self, title: S) -> Self
|
||||||
where
|
where
|
||||||
S: Into<SharedString>,
|
S: Into<SharedString>,
|
||||||
|
@ -597,12 +635,26 @@ pub mod simple_message_notification {
|
||||||
})
|
})
|
||||||
.child(div().max_w_96().child((self.build_content)(window, cx))),
|
.child(div().max_w_96().child((self.build_content)(window, cx))),
|
||||||
)
|
)
|
||||||
.when(self.show_close_button, |this| {
|
.child(
|
||||||
this.child(
|
h_flex()
|
||||||
IconButton::new("close", IconName::Close)
|
.gap_2()
|
||||||
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
|
.when(self.show_suppress_button, |this| {
|
||||||
)
|
this.child(
|
||||||
}),
|
IconButton::new("suppress", IconName::XCircle)
|
||||||
|
.tooltip(Tooltip::text("Do not show until restart"))
|
||||||
|
.on_click(cx.listener(|_, _, _, cx| {
|
||||||
|
cx.emit(SuppressEvent);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(self.show_close_button, |this| {
|
||||||
|
this.child(
|
||||||
|
IconButton::new("close", IconName::Close).on_click(
|
||||||
|
cx.listener(|this, _, _, cx| this.dismiss(cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
|
@ -52,7 +52,8 @@ use language::{Buffer, LanguageRegistry, Rope};
|
||||||
pub use modal_layer::*;
|
pub use modal_layer::*;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use notifications::{
|
use notifications::{
|
||||||
DetachAndPromptErr, Notifications, simple_message_notification::MessageNotification,
|
DetachAndPromptErr, Notifications, dismiss_app_notification,
|
||||||
|
simple_message_notification::MessageNotification,
|
||||||
};
|
};
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
|
@ -179,6 +180,7 @@ actions!(
|
||||||
SaveAs,
|
SaveAs,
|
||||||
SaveWithoutFormat,
|
SaveWithoutFormat,
|
||||||
ShutdownDebugAdapters,
|
ShutdownDebugAdapters,
|
||||||
|
SuppressNotification,
|
||||||
ToggleBottomDock,
|
ToggleBottomDock,
|
||||||
ToggleCenteredLayout,
|
ToggleCenteredLayout,
|
||||||
ToggleLeftDock,
|
ToggleLeftDock,
|
||||||
|
@ -921,6 +923,7 @@ pub struct Workspace {
|
||||||
toast_layer: Entity<ToastLayer>,
|
toast_layer: Entity<ToastLayer>,
|
||||||
titlebar_item: Option<AnyView>,
|
titlebar_item: Option<AnyView>,
|
||||||
notifications: Notifications,
|
notifications: Notifications,
|
||||||
|
suppressed_notifications: HashSet<NotificationId>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
follower_states: HashMap<CollaboratorId, FollowerState>,
|
follower_states: HashMap<CollaboratorId, FollowerState>,
|
||||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
||||||
|
@ -1245,7 +1248,8 @@ impl Workspace {
|
||||||
modal_layer,
|
modal_layer,
|
||||||
toast_layer,
|
toast_layer,
|
||||||
titlebar_item: None,
|
titlebar_item: None,
|
||||||
notifications: Default::default(),
|
notifications: Notifications::default(),
|
||||||
|
suppressed_notifications: HashSet::default(),
|
||||||
left_dock,
|
left_dock,
|
||||||
bottom_dock,
|
bottom_dock,
|
||||||
bottom_dock_layout,
|
bottom_dock_layout,
|
||||||
|
@ -5301,12 +5305,20 @@ impl Workspace {
|
||||||
workspace.clear_all_notifications(cx);
|
workspace.clear_all_notifications(cx);
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
.on_action(cx.listener(
|
||||||
|
|workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
|
||||||
|
if let Some((notification_id, _)) = workspace.notifications.pop() {
|
||||||
|
workspace.suppress_notification(¬ification_id, cx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
.on_action(cx.listener(
|
.on_action(cx.listener(
|
||||||
|workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
|
|workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
|
||||||
workspace.reopen_closed_item(window, cx).detach();
|
workspace.reopen_closed_item(window, cx).detach();
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.on_action(cx.listener(Workspace::toggle_centered_layout))
|
.on_action(cx.listener(Workspace::toggle_centered_layout))
|
||||||
|
.on_action(cx.listener(Workspace::cancel))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -5477,6 +5489,15 @@ impl Workspace {
|
||||||
.update(cx, |_, window, _| window.activate_window())
|
.update(cx, |_, window, _| window.activate_window())
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some((notification_id, _)) = self.notifications.pop() {
|
||||||
|
dismiss_app_notification(¬ification_id, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.propagate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leader_border_for_pane(
|
fn leader_border_for_pane(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue