1use cosmic::app::Core;
4use cosmic::iced::advanced::widget::text::Style as TextStyle;
5use cosmic::iced::keyboard::Modifiers;
6use cosmic::iced::{Alignment, Background, Border, Length};
7use cosmic::widget::menu::key_bind::KeyBind;
8use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
9use cosmic::widget::{
10 self, Row, button, column, container, divider, responsive_menu_bar, space, text,
11};
12use cosmic::{Element, theme};
13use mime_guess::Mime;
14use std::collections::HashMap;
15use std::collections::HashSet;
16use std::sync::LazyLock;
17use std::path::PathBuf;
18
19use crate::app::{Action, Message};
20use crate::config::{Config, ContextActionPreset, TBConfig};
21use crate::fl;
22use crate::tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab};
23use crate::trash::{Trash, TrashExt};
24
25static MENU_ID: LazyLock<cosmic::widget::Id> =
26 LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
27
28macro_rules! menu_button {
29 ($($x:expr),+ $(,)?) => (
30 button::custom(
31 Row::with_children(
32 [$(Element::from($x)),+]
33 )
34 .height(Length::Fixed(24.0))
35 .align_y(Alignment::Center)
36 )
37 .padding([theme::spacing().space_xxs, 16])
38 .width(Length::Fill)
39 .class(theme::Button::MenuItem)
40 );
41}
42
43fn is_valid_fastq_selection(paths: &[PathBuf], config: &TBConfig) -> bool {
44 let mut sample_map: HashMap<String, HashSet<u8>> = HashMap::new();
45 for path in paths.iter() {
46 let filename = match path.file_name().and_then(|n| n.to_str()) {
47 Some(f) => f,
48 None => return false,
49 };
50 if let Some(sample) = filename.strip_suffix(config.pair1_suffix.as_str()) {
51 sample_map.entry(sample.to_string()).or_default().insert(1);
52 } else if let Some(sample) = filename.strip_suffix(config.pair2_suffix.as_str()) {
53 sample_map.entry(sample.to_string()).or_default().insert(2);
54 } else {
55 return false;
56 }
57 }
58 sample_map.values().all(|set| set.len() == 2)
59}
60
61const fn menu_button_optional(
62 label: String,
63 action: Action,
64 enabled: bool,
65) -> menu::Item<Action, String> {
66 if enabled {
67 menu::Item::Button(label, None, action)
68 } else {
69 menu::Item::ButtonDisabled(label, None, action)
70 }
71}
72
73pub fn context_menu<'a>(
74 tab: &Tab,
75 key_binds: &HashMap<KeyBind, Action>,
76 modifiers: &Modifiers,
77 clipboard_paste_available: bool,
78 config: &TBConfig,
79 context_actions: &[ContextActionPreset],
80) -> Element<'a, tab::Message> {
81 let find_key = |action: &Action| -> String {
82 for (key_bind, key_action) in key_binds {
83 if action == key_action {
84 return key_bind.to_string();
85 }
86 }
87 String::new()
88 };
89 fn key_style(theme: &cosmic::Theme) -> TextStyle {
90 let mut color = theme.cosmic().background.component.on;
91 color.alpha *= 0.75;
92 TextStyle {
93 color: Some(color.into()),
94 ..Default::default()
95 }
96 }
97 fn disabled_style(theme: &cosmic::Theme) -> TextStyle {
98 let mut color = theme.cosmic().background.component.on;
99 color.alpha *= 0.5;
100 TextStyle {
101 color: Some(color.into()),
102 ..Default::default()
103 }
104 }
105
106 let menu_item = |label, action| {
107 let key = find_key(&action);
108 menu_button!(
109 text::body(label),
110 space::horizontal(),
111 text::body(key).class(theme::Text::Custom(key_style))
112 )
113 .on_press(tab::Message::ContextAction(action))
114 };
115
116 let menu_item_disabled = |label, action: Action| {
117 let key = find_key(&action);
118 menu_button!(
119 text::body(label).class(theme::Text::Custom(disabled_style)),
120 space::horizontal(),
121 text::body(key).class(theme::Text::Custom(disabled_style))
122 )
123 };
124
125 let can_paste = clipboard_paste_available && tab.location.supports_paste();
127
128 let (sort_name, sort_direction, _) = tab.sort_options();
129 let sort_item = |label, variant| {
130 let key = find_key(&Action::ToggleSort(variant));
131 let leading: Element<'a, tab::Message> = if sort_name == variant {
132 let icon_name = if sort_direction {
133 "view-sort-ascending-symbolic"
134 } else {
135 "view-sort-descending-symbolic"
136 };
137 widget::icon::from_name(icon_name).size(14).into()
138 } else {
139 space::horizontal().width(Length::Fixed(14.0)).into()
140 };
141 menu_button!(
142 leading,
143 space::horizontal().width(Length::Fixed(theme::spacing().space_xxs.into())),
144 text::body(label),
145 space::horizontal(),
146 text::body(key).class(theme::Text::Custom(key_style))
147 )
148 .on_press(tab::Message::ContextAction(Action::ToggleSort(variant)))
149 .into()
150 };
151
152 let mut selected_dir = 0;
153 let mut selected = 0;
154 let mut selected_trash_only = false;
155 let mut selected_desktop_entry = None;
156 let mut selected_types: Vec<Mime> = vec![];
157 let mut selected_mount_point = 0;
158 let mut selected_client_point = 0;
159 let mut selected_remote_paths: Vec<PathBuf> = Vec::new();
160 if let Some(items) = tab.items_opt() {
161 for item in items {
162 if item.selected {
163 selected += 1;
164 if item.metadata.is_dir() {
165 selected_mount_point += i32::from(item.is_mount_point);
166 selected_client_point += i32::from(item.is_client_point);
167 selected_dir += 1;
168 }
169 if let Some(Location::Remote(_, _, Some(path))) = &item.location_opt {
170 selected_remote_paths.push(path.clone());
171 }
172 match &item.location_opt {
173 Some(Location::Trash) | Some(Location::Search(SearchLocation::Trash, ..)) => {
174 selected_trash_only = true
175 }
176 Some(Location::Path(path))
177 if selected == 1
178 && path.extension().and_then(|s| s.to_str()) == Some("desktop") =>
179 {
180 selected_desktop_entry = Some(&**path);
181 }
182 _ => (),
183 }
184 selected_types.push(item.mime.clone());
185 }
186 }
187 }
188 selected_types.sort_unstable();
189 selected_types.dedup();
190 selected_trash_only = selected_trash_only && selected == 1;
191 let context_action_items = |selected: usize, selected_dir: usize| {
192 context_actions
193 .iter()
194 .enumerate()
195 .filter(|(_, action)| action.matches_selection(selected, selected_dir))
196 .map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into())
197 .collect::<Vec<Element<'a, tab::Message>>>()
198 };
199 #[cfg(feature = "desktop")]
201 let selected_desktop_entry = selected_desktop_entry.and_then(|path| {
202 if selected == 1 {
203 let lang_id = crate::localize::LANGUAGE_LOADER.current_language();
204 let language = lang_id.language.as_str();
205 cosmic::desktop::load_desktop_file(&[language.into()], path.into())
207 } else {
208 None
209 }
210 });
211
212 let mut children: Vec<Element<_>> = Vec::new();
213 match (&tab.mode, &tab.location) {
214 (
215 tab::Mode::App | tab::Mode::Desktop,
216 Location::Desktop(..)
217 | Location::Path(..)
218 | Location::Search(SearchLocation::Path(..), ..)
219 | Location::Search(SearchLocation::Recents, ..)
220 | Location::Recents
221 | Location::Network(_, _, Some(_)),
222 ) => {
223 if selected_trash_only {
224 children.push(menu_item(fl!("open"), Action::Open).into());
225 if !Trash::is_empty() {
226 children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
227 }
228 } else if let Some(_entry) = selected_desktop_entry {
229 children.push(menu_item(fl!("open"), Action::Open).into());
230 #[cfg(feature = "desktop")]
231 {
232 children.extend(entry.desktop_actions.into_iter().enumerate().map(
233 |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
234 ));
235 }
236 children.push(divider::horizontal::light().into());
237 children.push(menu_item(fl!("rename"), Action::Rename).into());
238 children.push(menu_item(fl!("cut"), Action::Cut).into());
239 if modifiers.shift() && !modifiers.control() {
240 children.push(menu_item(fl!("copy-path"), Action::CopyPath).into());
241 } else {
242 children.push(menu_item(fl!("copy"), Action::Copy).into());
243 }
244 children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
246 let action_items = context_action_items(selected, selected_dir);
247 if !action_items.is_empty() {
248 children.push(divider::horizontal::light().into());
249 children.extend(action_items);
250 }
251 } else if selected > 0 {
252 if selected_dir == 1 && selected == 1 || selected_dir == 0 {
253 children.push(menu_item(fl!("open"), Action::Open).into());
254 }
255 if selected == 1 {
256 children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into());
257 if selected_dir == 1 {
258 children
259 .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
260 }
261 }
262 if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) {
263 children.push(
264 menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
265 );
266 }
267 if selected == selected_dir && matches!(tab.mode, tab::Mode::App) {
269 children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into());
270 children
271 .push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into());
272 }
273 let action_items = context_action_items(selected, selected_dir);
274 if !action_items.is_empty() {
275 children.push(divider::horizontal::light().into());
276 children.extend(action_items);
277 }
278 children.push(divider::horizontal::light().into());
279 if selected_mount_point == 0 && selected_client_point == 0 {
280 children.push(menu_item(fl!("rename"), Action::Rename).into());
281 children.push(menu_item(fl!("cut"), Action::Cut).into());
282 }
283 if modifiers.shift() && !modifiers.control() {
284 children.push(menu_item(fl!("copy-path"), Action::CopyPath).into());
285 } else {
286 children.push(menu_item(fl!("copy"), Action::Copy).into());
287 }
288 if selected_mount_point == 0 {
289 children.push(menu_item(fl!("move-to"), Action::MoveTo).into());
290 }
291 children.push(menu_item(fl!("copy-to"), Action::CopyTo).into());
292
293 children.push(divider::horizontal::light().into());
294 let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;
295 selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));
296 if selected_types.is_empty() {
297 children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
298 children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
299 }
300 children.push(menu_item(fl!("compress"), Action::Compress).into());
301 children.push(divider::horizontal::light().into());
302
303 children.push(menu_item(fl!("show-details"), Action::Preview).into());
305 if matches!(tab.mode, tab::Mode::App) {
306 children.push(divider::horizontal::light().into());
307 children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
308 }
309 children.push(divider::horizontal::light().into());
310 if tab.location.is_recents() {
311 children.push(
312 menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(),
313 );
314 children.push(divider::horizontal::light().into());
315 }
316 if selected_mount_point == 0 && selected_client_point == 0 {
317 if modifiers.shift() && !modifiers.control() {
318 children.push(
319 menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(),
320 );
321 } else {
322 children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
323 }
324 } else if selected == 1 {
325 children.push(menu_item(fl!("eject"), Action::Eject).into());
326 }
327 } else {
328 if tab.location != Location::Recents {
331 children.push(menu_item(fl!("new-folder"), Action::NewFolder).into());
332 children.push(menu_item(fl!("new-file"), Action::NewFile).into());
333 children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
334 children.push(divider::horizontal::light().into());
335 }
336
337 if tab.mode.multiple() {
338 children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
339 }
340 if can_paste {
341 children.push(menu_item(fl!("paste"), Action::Paste).into());
342 } else {
343 children.push(menu_item_disabled(fl!("paste"), Action::Paste).into());
344 }
345
346 if matches!(tab.mode, tab::Mode::Desktop) {
348 children.push(divider::horizontal::light().into());
349 children.push(
350 menu_item(fl!("change-wallpaper"), Action::CosmicSettingsWallpaper).into(),
351 );
352 children.push(
353 menu_item(fl!("desktop-appearance"), Action::CosmicSettingsDesktop).into(),
354 );
355 children.push(
356 menu_item(fl!("display-settings"), Action::CosmicSettingsDisplays).into(),
357 );
358 }
359
360 children.push(divider::horizontal::light().into());
361 children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
363 children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
364 children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
365 if matches!(tab.location, Location::Desktop(..)) {
366 children.push(divider::horizontal::light().into());
367 children.push(
368 menu_item(fl!("desktop-view-options"), Action::DesktopViewOptions).into(),
369 );
370 }
371 }
372 }
373 (
374 tab::Mode::Dialog(dialog_kind),
375 Location::Desktop(..)
376 | Location::Path(..)
377 | Location::Search(SearchLocation::Path(..), ..)
378 | Location::Search(SearchLocation::Recents, ..)
379 | Location::Recents
380 | Location::Network(_, _, Some(_)),
381 ) => {
382 if selected > 0 {
383 if selected_dir == 1 && selected == 1 || selected_dir == 0 {
384 children.push(menu_item(fl!("open"), Action::Open).into());
385 }
386 if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() {
387 children.push(
388 menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
389 );
390 }
391 children.push(divider::horizontal::light().into());
392 children.push(menu_item(fl!("show-details"), Action::Preview).into());
393 } else {
394 if dialog_kind.save() {
395 children.push(menu_item(fl!("new-folder"), Action::NewFolder).into());
396 }
397 if tab.mode.multiple() {
398 children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
399 }
400 if !children.is_empty() {
401 children.push(divider::horizontal::light().into());
402 }
403 children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
404 children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
405 children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
406 }
407 }
408 (_, Location::Network(..)) => {
409 if selected > 0 {
410 if selected_dir == 1 && selected == 1 || selected_dir == 0 {
411 children.push(menu_item(fl!("open"), Action::Open).into());
412 }
413 } else {
414 if tab.mode.multiple() {
415 children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
416 }
417 if !children.is_empty() {
418 children.push(divider::horizontal::light().into());
419 }
420 children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
421 children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
422 children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
423 }
424 }
425 (_, Location::Remote(..)) => {
426 if selected > 0 {
427 if selected == selected_remote_paths.len() && selected_dir == 0 {
428 if config.pair1_suffix.is_empty() || config.pair2_suffix.is_empty() {
429 children.push(
430 menu_item(fl!("run-tb-profiler"), Action::TbProfilerConfigError)
431 .into(),
432 );
433 } else if is_valid_fastq_selection(&selected_remote_paths, config) {
434 children
435 .push(menu_item(fl!("run-tb-profiler"), Action::RunTbProfiler).into());
436 }
437 }
438 if matches!(tab.mode, tab::Mode::App) {
439 children.push(divider::horizontal::light().into());
440 children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
441 }
442 if selected == 1 && selected_dir == 0 {
443 children.push(
444 menu_item(fl!("delete-remote-file"), Action::DeleteRemoteFiles).into(),
445 );
446 }
447 if selected > 1 && selected_dir == 0 {
448 children.push(
449 menu_item(fl!("delete-remote-files"), Action::DeleteRemoteFiles).into(),
450 );
451 }
452 children.push(menu_item(fl!("download-to"), Action::DownloadTo).into());
453 } else {
454 if tab.mode.multiple() {
455 children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
456 }
457 if !children.is_empty() {
458 children.push(divider::horizontal::light().into());
459 }
460 children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
461 children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
462 children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
463 }
464 }
465 (_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {
466 if tab.mode.multiple() {
467 children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
468 }
469 if !children.is_empty() {
470 children.push(divider::horizontal::light().into());
471 }
472 if selected > 0 {
473 children.push(menu_item(fl!("show-details"), Action::Preview).into());
474 children.push(divider::horizontal::light().into());
475 children
476 .push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into());
477 children.push(divider::horizontal::light().into());
478 children.push(menu_item(fl!("delete-permanently"), Action::Delete).into());
479 } else {
480 children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
482 children.push(sort_item(fl!("sort-by-trashed"), HeadingOptions::TrashedOn));
483 children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
484 }
485 }
486 }
487
488 container(column::with_children(children))
489 .padding(1)
490 .style(|theme| {
492 let cosmic = theme.cosmic();
493 let component = &cosmic.background.component;
494 container::Style {
495 icon_color: Some(component.on.into()),
496 text_color: Some(component.on.into()),
497 background: Some(Background::Color(component.base.into())),
498 border: Border {
499 radius: cosmic.radius_s().map(|x| x + 1.0).into(),
500 width: 1.0,
501 color: component.divider.into(),
502 },
503 ..Default::default()
504 }
505 })
506 .width(Length::Fixed(360.0))
507 .into()
508}
509
510pub fn dialog_menu(
511 tab: &Tab,
512 key_binds: &HashMap<KeyBind, Action>,
513 show_details: bool,
514) -> Element<'static, Message> {
515 let (sort_name, sort_direction, _) = tab.sort_options();
516 let sort_item = |label, sort, dir| {
517 menu::Item::CheckBox(
518 label,
519 None,
520 sort_name == sort && sort_direction == dir,
521 Action::SetSort(sort, dir),
522 )
523 };
524 let in_trash = tab.location.is_trash();
525
526 let mut selected_gallery = 0;
527 if let Some(items) = tab.items_opt() {
528 for item in items {
529 if item.selected && item.can_gallery() {
530 selected_gallery += 1;
531 }
532 }
533 }
534
535 MenuBar::new(vec![
536 menu::Tree::with_children(
537 Element::from(
538 widget::button::icon(widget::icon::from_name(match tab.config.view {
539 tab::View::Grid => "view-grid-symbolic",
540 tab::View::List => "view-list-symbolic",
541 }))
542 .on_press(Message::None)
544 .padding(8),
545 ),
546 menu::items(
547 key_binds,
548 vec![
549 menu::Item::CheckBox(
550 fl!("grid-view"),
551 None,
552 matches!(tab.config.view, tab::View::Grid),
553 Action::TabViewGrid,
554 ),
555 menu::Item::CheckBox(
556 fl!("list-view"),
557 None,
558 matches!(tab.config.view, tab::View::List),
559 Action::TabViewList,
560 ),
561 ],
562 ),
563 ),
564 menu::Tree::with_children(
565 Element::from(
566 widget::button::icon(widget::icon::from_name(if sort_direction {
567 "view-sort-ascending-symbolic"
568 } else {
569 "view-sort-descending-symbolic"
570 }))
571 .on_press(Message::None)
573 .padding(8),
574 ),
575 menu::items(
576 key_binds,
577 vec![
578 sort_item(fl!("sort-a-z"), tab::HeadingOptions::Name, true),
579 sort_item(fl!("sort-z-a"), tab::HeadingOptions::Name, false),
580 sort_item(
581 fl!("sort-newest-first"),
582 if in_trash {
583 tab::HeadingOptions::TrashedOn
584 } else {
585 tab::HeadingOptions::Modified
586 },
587 false,
588 ),
589 sort_item(
590 fl!("sort-oldest-first"),
591 if in_trash {
592 tab::HeadingOptions::TrashedOn
593 } else {
594 tab::HeadingOptions::Modified
595 },
596 true,
597 ),
598 sort_item(
599 fl!("sort-smallest-to-largest"),
600 tab::HeadingOptions::Size,
601 true,
602 ),
603 sort_item(
604 fl!("sort-largest-to-smallest"),
605 tab::HeadingOptions::Size,
606 false,
607 ),
608 ],
610 ),
611 ),
612 menu::Tree::with_children(
613 Element::from(
614 widget::button::icon(widget::icon::from_name("view-more-symbolic"))
615 .on_press(Message::None)
617 .padding(8),
618 ),
619 menu::items(
620 key_binds,
621 vec![
622 menu::Item::Button(fl!("zoom-in"), None, Action::ZoomIn),
623 menu::Item::Button(fl!("default-size"), None, Action::ZoomDefault),
624 menu::Item::Button(fl!("zoom-out"), None, Action::ZoomOut),
625 menu::Item::Divider,
626 menu::Item::CheckBox(
627 fl!("show-hidden-files"),
628 None,
629 tab.config.show_hidden,
630 Action::ToggleShowHidden,
631 ),
632 menu::Item::CheckBox(
633 fl!("list-directories-first"),
634 None,
635 tab.config.folders_first,
636 Action::ToggleFoldersFirst,
637 ),
638 menu::Item::CheckBox(fl!("show-details"), None, show_details, Action::Preview),
639 menu::Item::Divider,
640 menu_button_optional(
641 fl!("gallery-preview"),
642 Action::Gallery,
643 selected_gallery > 0,
644 ),
645 ],
646 ),
647 ),
648 ])
649 .item_height(ItemHeight::Dynamic(40))
650 .item_width(ItemWidth::Uniform(360))
651 .spacing(theme::spacing().space_xxxs.into())
652 .into()
653}
654
655pub fn menu_bar<'a>(
656 core: &Core,
657 tab_opt: Option<&Tab>,
658 config: &Config,
659 modifiers: &Modifiers,
660 key_binds: &HashMap<KeyBind, Action>,
661 clipboard_paste_available: bool,
662) -> Element<'a, Message> {
663 let sort_options = tab_opt.map(Tab::sort_options);
664 let sort_item = |label, sort, dir| {
665 menu::Item::CheckBox(
666 label,
667 None,
668 sort_options.is_some_and(|(sort_name, sort_direction, _)| {
669 sort_name == sort && sort_direction == dir
670 }),
671 Action::SetSort(sort, dir),
672 )
673 };
674 let in_trash = tab_opt.is_some_and(|tab| tab.location.is_trash());
675
676 let mut selected_dir = 0;
677 let mut selected = 0;
678 let mut selected_gallery = 0;
679 if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) {
680 for item in items {
681 if item.selected {
682 selected += 1;
683 if item.metadata.is_dir() {
684 selected_dir += 1;
685 }
686 if item.can_gallery() {
687 selected_gallery += 1;
688 }
689 }
690 }
691 }
692
693 let can_paste =
695 clipboard_paste_available && tab_opt.is_some_and(|tab| tab.location.supports_paste());
696
697 let (delete_item, delete_item_action) = if in_trash || modifiers.shift() {
698 (fl!("delete-permanently"), Action::Delete)
699 } else {
700 (fl!("move-to-trash"), Action::Delete)
701 };
702
703 responsive_menu_bar()
704 .item_height(ItemHeight::Dynamic(40))
705 .item_width(ItemWidth::Uniform(360))
706 .spacing(theme::spacing().space_xxxs.into())
707 .into_element(
708 core,
709 key_binds,
710 MENU_ID.clone(),
711 Message::Surface,
712 vec![
713 (
714 fl!("file"),
715 vec![
716 menu::Item::Button(fl!("new-tab"), None, Action::TabNew),
717 menu::Item::Button(fl!("new-window"), None, Action::WindowNew),
718 menu::Item::Button(fl!("new-folder"), None, Action::NewFolder),
719 menu::Item::Button(fl!("new-file"), None, Action::NewFile),
720 menu_button_optional(
721 fl!("open"),
722 Action::Open,
723 (selected > 0 && selected_dir == 0)
724 || (selected_dir == 1 && selected == 1),
725 ),
726 menu_button_optional(
727 fl!("menu-open-with"),
728 Action::OpenWith,
729 selected == 1,
730 ),
731 menu::Item::Divider,
732 menu_button_optional(fl!("rename"), Action::Rename, selected > 0),
733 menu::Item::Divider,
734 menu::Item::Button(fl!("reload-folder"), None, Action::Reload),
735 menu::Item::Divider,
736 menu_button_optional(
737 fl!("add-to-sidebar"),
738 Action::AddToSidebar,
739 selected > 0,
740 ),
741 menu::Item::Divider,
742 menu_button_optional(
743 fl!("restore-from-trash"),
744 Action::RestoreFromTrash,
745 selected > 0 && in_trash,
746 ),
747 menu_button_optional(delete_item, delete_item_action, selected > 0),
748 menu::Item::Divider,
749 menu::Item::Button(fl!("close-tab"), None, Action::TabClose),
750 menu::Item::Button(fl!("quit"), None, Action::WindowClose),
751 ],
752 ),
753 (
754 (fl!("edit")),
755 vec![
756 menu_button_optional(fl!("cut"), Action::Cut, selected > 0),
757 menu_button_optional(fl!("copy"), Action::Copy, selected > 0),
758 menu_button_optional(fl!("move-to"), Action::MoveTo, selected > 0),
759 menu_button_optional(fl!("copy-to"), Action::CopyTo, selected > 0),
760 menu_button_optional(fl!("paste"), Action::Paste, can_paste),
761 menu::Item::Button(fl!("select-all"), None, Action::SelectAll),
762 menu::Item::Divider,
763 menu::Item::Button(fl!("history"), None, Action::EditHistory),
764 ],
765 ),
766 (
767 (fl!("view")),
768 vec![
769 menu::Item::Button(fl!("zoom-in"), None, Action::ZoomIn),
770 menu::Item::Button(fl!("default-size"), None, Action::ZoomDefault),
771 menu::Item::Button(fl!("zoom-out"), None, Action::ZoomOut),
772 menu::Item::Divider,
773 menu::Item::CheckBox(
774 fl!("grid-view"),
775 None,
776 tab_opt.is_some_and(|tab| matches!(tab.config.view, tab::View::Grid)),
777 Action::TabViewGrid,
778 ),
779 menu::Item::CheckBox(
780 fl!("list-view"),
781 None,
782 tab_opt.is_some_and(|tab| matches!(tab.config.view, tab::View::List)),
783 Action::TabViewList,
784 ),
785 menu::Item::Divider,
786 menu::Item::CheckBox(
787 fl!("show-hidden-files"),
788 None,
789 tab_opt.is_some_and(|tab| tab.config.show_hidden),
790 Action::ToggleShowHidden,
791 ),
792 menu::Item::CheckBox(
793 fl!("show-susceptible-samples"),
794 None,
795 tab_opt.is_some_and(|tab| tab.config.show_susceptible),
796 Action::ToggleShowSusceptible,
797 ),
798 menu::Item::CheckBox(
799 fl!("list-directories-first"),
800 None,
801 tab_opt.is_some_and(|tab| tab.config.folders_first),
802 Action::ToggleFoldersFirst,
803 ),
804 menu::Item::CheckBox(
805 fl!("show-details"),
806 None,
807 config.show_details,
808 Action::Preview,
809 ),
810 menu::Item::CheckBox(
811 fl!("show-as-samples"),
812 None,
813 config.tab.show_as_samples,
814 Action::ToggleShowAsSamples,
815 ),
816 menu::Item::Divider,
817 menu_button_optional(
818 fl!("gallery-preview"),
819 Action::Gallery,
820 selected_gallery > 0,
821 ),
822 menu::Item::Divider,
823 menu::Item::Button(fl!("menu-settings"), None, Action::Settings),
824 menu::Item::Button(
825 fl!("menu-tb-profiler-settings"),
826 None,
827 Action::TBSettings,
828 ),
829 menu::Item::Divider,
830 menu::Item::Button(fl!("menu-about"), None, Action::About),
831 ],
832 ),
833 (
834 (fl!("sort")),
835 vec![
836 sort_item(fl!("sort-a-z"), tab::HeadingOptions::Name, true),
837 sort_item(fl!("sort-z-a"), tab::HeadingOptions::Name, false),
838 sort_item(
839 fl!("sort-newest-first"),
840 if in_trash {
841 tab::HeadingOptions::TrashedOn
842 } else {
843 tab::HeadingOptions::Modified
844 },
845 false,
846 ),
847 sort_item(
848 fl!("sort-oldest-first"),
849 if in_trash {
850 tab::HeadingOptions::TrashedOn
851 } else {
852 tab::HeadingOptions::Modified
853 },
854 true,
855 ),
856 sort_item(
857 fl!("sort-smallest-to-largest"),
858 tab::HeadingOptions::Size,
859 true,
860 ),
861 sort_item(
862 fl!("sort-largest-to-smallest"),
863 tab::HeadingOptions::Size,
864 false,
865 ),
866 ],
868 ),
869 ],
870 )
871}
872
873pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
874 let children = [
876 menu_button!(text::body(fl!("open-in-new-tab")))
877 .on_press(tab::Message::LocationMenuAction(
878 LocationMenuAction::OpenInNewTab(ancestor_index),
879 ))
880 .into(),
881 menu_button!(text::body(fl!("open-in-new-window")))
882 .on_press(tab::Message::LocationMenuAction(
883 LocationMenuAction::OpenInNewWindow(ancestor_index),
884 ))
885 .into(),
886 divider::horizontal::light().into(),
887 menu_button!(text::body(fl!("show-details")))
888 .on_press(tab::Message::LocationMenuAction(
889 LocationMenuAction::Preview(ancestor_index),
890 ))
891 .into(),
892 divider::horizontal::light().into(),
893 menu_button!(text::body(fl!("add-to-sidebar")))
894 .on_press(tab::Message::LocationMenuAction(
895 LocationMenuAction::AddToSidebar(ancestor_index),
896 ))
897 .into(),
898 ];
899
900 container(column::with_children(children))
901 .padding(1)
902 .style(|theme| {
903 let cosmic = theme.cosmic();
904 let component = &cosmic.background.component;
905 container::Style {
906 icon_color: Some(component.on.into()),
907 text_color: Some(component.on.into()),
908 background: Some(Background::Color(component.base.into())),
909 border: Border {
910 radius: cosmic.radius_s().map(|x| x + 1.0).into(),
911 width: 1.0,
912 color: component.divider.into(),
913 },
914 ..Default::default()
915 }
916 })
917 .width(Length::Fixed(360.0))
918 .into()
919}