Add a basic context menu to the project panel

Max Brunsfeld created

Change summary

crates/project_panel2/src/project_panel.rs | 175 +++++++++++++----------
crates/ui2/src/components/context_menu.rs  |   8 
2 files changed, 103 insertions(+), 80 deletions(-)

Detailed changes

crates/project_panel2/src/project_panel.rs 🔗

@@ -8,10 +8,11 @@ use file_associations::FileAssociations;
 
 use anyhow::{anyhow, Result};
 use gpui::{
-    actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+    actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
     ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
-    Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
-    Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
+    Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
+    Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
+    VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -29,7 +30,7 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme as _;
-use ui::{v_stack, IconElement, Label, ListItem};
+use ui::{v_stack, ContextMenu, IconElement, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -49,6 +50,7 @@ pub struct ProjectPanel {
     last_worktree_root_id: Option<ProjectEntryId>,
     expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
     selection: Option<Selection>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
     edit_state: Option<EditState>,
     filename_editor: View<Editor>,
     clipboard_entry: Option<ClipboardEntry>,
@@ -231,6 +233,7 @@ impl ProjectPanel {
                 expanded_dir_ids: Default::default(),
                 selection: None,
                 edit_state: None,
+                context_menu: None,
                 filename_editor,
                 clipboard_entry: None,
                 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@@ -366,80 +369,92 @@ impl ProjectPanel {
 
     fn deploy_context_menu(
         &mut self,
-        _position: Point<Pixels>,
-        _entry_id: ProjectEntryId,
-        _cx: &mut ViewContext<Self>,
+        position: Point<Pixels>,
+        entry_id: ProjectEntryId,
+        cx: &mut ViewContext<Self>,
     ) {
-        // todo!()
-        //     let project = self.project.read(cx);
+        let this = cx.view().clone();
+        let project = self.project.read(cx);
 
-        //     let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
-        //         id
-        //     } else {
-        //         return;
-        //     };
+        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
+            id
+        } else {
+            return;
+        };
 
-        //     self.selection = Some(Selection {
-        //         worktree_id,
-        //         entry_id,
-        //     });
-
-        //     let mut menu_entries = Vec::new();
-        //     if let Some((worktree, entry)) = self.selected_entry(cx) {
-        //         let is_root = Some(entry) == worktree.root_entry();
-        //         if !project.is_remote() {
-        //             menu_entries.push(ContextMenuItem::action(
-        //                 "Add Folder to Project",
-        //                 workspace::AddFolderToProject,
-        //             ));
-        //             if is_root {
-        //                 let project = self.project.clone();
-        //                 menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
-        //                     project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-        //                 }));
-        //             }
-        //         }
-        //         menu_entries.push(ContextMenuItem::action("New File", NewFile));
-        //         menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
-        //         menu_entries.push(ContextMenuItem::Separator);
-        //         menu_entries.push(ContextMenuItem::action("Cut", Cut));
-        //         menu_entries.push(ContextMenuItem::action("Copy", Copy));
-        //         if let Some(clipboard_entry) = self.clipboard_entry {
-        //             if clipboard_entry.worktree_id() == worktree.id() {
-        //                 menu_entries.push(ContextMenuItem::action("Paste", Paste));
-        //             }
-        //         }
-        //         menu_entries.push(ContextMenuItem::Separator);
-        //         menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
-        //         menu_entries.push(ContextMenuItem::action(
-        //             "Copy Relative Path",
-        //             CopyRelativePath,
-        //         ));
-
-        //         if entry.is_dir() {
-        //             menu_entries.push(ContextMenuItem::Separator);
-        //         }
-        //         menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
-        //         if entry.is_dir() {
-        //             menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
-        //             menu_entries.push(ContextMenuItem::action(
-        //                 "Search Inside",
-        //                 NewSearchInDirectory,
-        //             ));
-        //         }
-
-        //         menu_entries.push(ContextMenuItem::Separator);
-        //         menu_entries.push(ContextMenuItem::action("Rename", Rename));
-        //         if !is_root {
-        //             menu_entries.push(ContextMenuItem::action("Delete", Delete));
-        //         }
-        //     }
-
-        //     // self.context_menu.update(cx, |menu, cx| {
-        //     //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
-        //     // });
-
-        //     cx.notify();
+        self.selection = Some(Selection {
+            worktree_id,
+            entry_id,
+        });
+
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            let is_root = Some(entry) == worktree.root_entry();
+            let is_dir = entry.is_dir();
+            let worktree_id = worktree.id();
+            let is_local = project.is_local();
+
+            let context_menu = ContextMenu::build(cx, |mut menu, cx| {
+                if is_local {
+                    menu = menu.action(
+                        "Add Folder to Project",
+                        Box::new(workspace::AddFolderToProject),
+                    );
+                    if is_root {
+                        menu = menu.entry(
+                            "Remove from Project",
+                            cx.listener_for(&this, move |this, _, cx| {
+                                this.project.update(cx, |project, cx| {
+                                    project.remove_worktree(worktree_id, cx)
+                                });
+                            }),
+                        );
+                    }
+                }
+
+                menu = menu
+                    .action("New File", Box::new(NewFile))
+                    .action("New Folder", Box::new(NewDirectory))
+                    .separator()
+                    .action("Cut", Box::new(Cut))
+                    .action("Copy", Box::new(Copy));
+
+                if let Some(clipboard_entry) = self.clipboard_entry {
+                    if clipboard_entry.worktree_id() == worktree_id {
+                        menu = menu.action("Paste", Box::new(Paste));
+                    }
+                }
+
+                menu = menu
+                    .separator()
+                    .action("Copy Path", Box::new(CopyPath))
+                    .action("Copy Relative Path", Box::new(CopyRelativePath))
+                    .separator()
+                    .action("Reveal in Finder", Box::new(RevealInFinder));
+
+                if is_dir {
+                    menu = menu
+                        .action("Open in Terminal", Box::new(OpenInTerminal))
+                        .action("Search Inside", Box::new(NewSearchInDirectory))
+                }
+
+                menu = menu.separator().action("Rename", Box::new(Rename));
+
+                if !is_root {
+                    menu = menu.action("Delete", Box::new(Delete));
+                }
+
+                menu
+            });
+
+            cx.focus_view(&context_menu);
+            let subscription = cx.on_blur(&context_menu.focus_handle(cx), |this, cx| {
+                this.context_menu.take();
+                cx.notify();
+            });
+            self.context_menu = Some((context_menu, position, subscription));
+        }
+
+        cx.notify();
     }
 
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
@@ -1379,6 +1394,9 @@ impl ProjectPanel {
                 .ml_1(),
             )
             .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
+                if event.down.button == MouseButton::Right {
+                    return;
+                }
                 if !show_editor {
                     if kind.is_dir() {
                         this.toggle_expanded(entry_id, cx);
@@ -1415,6 +1433,7 @@ impl Render for ProjectPanel {
             div()
                 .id("project-panel")
                 .size_full()
+                .relative()
                 .key_context("ProjectPanel")
                 .on_action(cx.listener(Self::select_next))
                 .on_action(cx.listener(Self::select_prev))
@@ -1458,6 +1477,12 @@ impl Render for ProjectPanel {
                     .size_full()
                     .track_scroll(self.list.clone()),
                 )
+                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
+                    overlay()
+                        .position(*position)
+                        .anchor(gpui::AnchorCorner::BottomLeft)
+                        .child(menu.clone())
+                }))
         } else {
             v_stack()
                 .id("empty-project_panel")

crates/ui2/src/components/context_menu.rs 🔗

@@ -1,13 +1,10 @@
-use std::cell::RefCell;
-use std::rc::Rc;
-
-use crate::{prelude::*, v_stack, Label, List};
-use crate::{ListItem, ListSeparator, ListSubHeader};
+use crate::{prelude::*, v_stack, Label, List, ListItem, ListSeparator, ListSubHeader};
 use gpui::{
     overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
     DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
     ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
 };
+use std::{cell::RefCell, rc::Rc};
 
 pub enum ContextMenuItem {
     Separator,
@@ -177,6 +174,7 @@ pub struct MenuHandleState<M> {
     child_element: Option<AnyElement>,
     menu_element: Option<AnyElement>,
 }
+
 impl<M: ManagedView> Element for MenuHandle<M> {
     type State = MenuHandleState<M>;