Show details in popup window in desktop mode, part of #547

This commit is contained in:
Jeremy Soller 2024-10-04 11:07:27 -06:00
parent 88948aea0c
commit 191af673b1
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
4 changed files with 148 additions and 59 deletions

View file

@ -1,4 +1,4 @@
// This launches the desktop mode as a regular window for easier testing. // This launches the desktop mode as a regular window for easier testing.
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
cosmic_files::desktop() cosmic_files::desktop()
} }

View file

@ -21,8 +21,9 @@ use cosmic::{
futures::{self, SinkExt}, futures::{self, SinkExt},
keyboard::{Event as KeyEvent, Key, Modifiers}, keyboard::{Event as KeyEvent, Key, Modifiers},
subscription::{self, Subscription}, subscription::{self, Subscription},
widget::scrollable,
window::{self, Event as WindowEvent, Id as WindowId}, window::{self, Event as WindowEvent, Id as WindowId},
Alignment, Event, Length, Alignment, Event, Length, Size,
}, },
iced_runtime::clipboard, iced_runtime::clipboard,
style, theme, style, theme,
@ -175,7 +176,7 @@ impl Action {
Action::OpenTerminal => Message::OpenTerminal(entity_opt), Action::OpenTerminal => Message::OpenTerminal(entity_opt),
Action::OpenWith => Message::OpenWithDialog(entity_opt), Action::OpenWith => Message::OpenWithDialog(entity_opt),
Action::Paste => Message::Paste(entity_opt), Action::Paste => Message::Paste(entity_opt),
Action::Preview => Message::ToggleShowDetails, Action::Preview => Message::Preview,
Action::Rename => Message::Rename(entity_opt), Action::Rename => Message::Rename(entity_opt),
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
Action::SearchActivate => Message::SearchActivate, Action::SearchActivate => Message::SearchActivate,
@ -297,6 +298,7 @@ pub enum Message {
PendingComplete(u64), PendingComplete(u64),
PendingError(u64, String), PendingError(u64, String),
PendingProgress(u64, f32), PendingProgress(u64, f32),
Preview,
RescanTrash, RescanTrash,
Rename(Option<Entity>), Rename(Option<Entity>),
ReplaceResult(ReplaceResult), ReplaceResult(ReplaceResult),
@ -316,12 +318,12 @@ pub enum Message {
TabRescan(Entity, Location, Vec<tab::Item>, Option<PathBuf>), TabRescan(Entity, Location, Vec<tab::Item>, Option<PathBuf>),
TabView(Option<Entity>, tab::View), TabView(Option<Entity>, tab::View),
ToggleContextPage(ContextPage), ToggleContextPage(ContextPage),
ToggleShowDetails,
ToggleFoldersFirst, ToggleFoldersFirst,
Undo(usize), Undo(usize),
UndoTrash(widget::ToastId, Arc<[PathBuf]>), UndoTrash(widget::ToastId, Arc<[PathBuf]>),
UndoTrashStart(Vec<TrashItem>), UndoTrashStart(Vec<TrashItem>),
WindowClose, WindowClose,
WindowCloseRequested(window::Id),
WindowNew, WindowNew,
ZoomDefault(Option<Entity>), ZoomDefault(Option<Entity>),
ZoomIn(Option<Entity>), ZoomIn(Option<Entity>),
@ -443,6 +445,12 @@ pub struct FavoriteIndex(usize);
pub struct MounterData(MounterKey, MounterItem); pub struct MounterData(MounterKey, MounterItem);
#[derive(Clone, Debug)]
pub enum WindowKind {
Main,
Preview(Option<Entity>, PreviewKind),
}
pub struct WatcherWrapper { pub struct WatcherWrapper {
watcher_opt: Option<Debouncer<RecommendedWatcher, FileIdMap>>, watcher_opt: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
} }
@ -500,6 +508,7 @@ pub struct App {
toasts: widget::toaster::Toasts<Message>, toasts: widget::toaster::Toasts<Message>,
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>, watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
window_id_opt: Option<window::Id>, window_id_opt: Option<window::Id>,
windows: HashMap<window::Id, WindowKind>,
nav_dnd_hover: Option<(Location, Instant)>, nav_dnd_hover: Option<(Location, Instant)>,
tab_dnd_hover: Option<(Entity, Instant)>, tab_dnd_hover: Option<(Entity, Instant)>,
nav_drag_id: DragId, nav_drag_id: DragId,
@ -969,19 +978,25 @@ impl App {
widget::settings::view_column(children).into() widget::settings::view_column(children).into()
} }
fn preview(&self, entity_opt: &Option<Entity>, kind: &PreviewKind) -> Element<Message> { fn preview(
&self,
entity_opt: &Option<Entity>,
kind: &PreviewKind,
context_drawer: bool,
) -> Element<Message> {
let mut children = Vec::with_capacity(1); let mut children = Vec::with_capacity(1);
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
match kind { match kind {
PreviewKind::Custom(PreviewItem(item)) => { PreviewKind::Custom(PreviewItem(item)) => {
children.push(item.preview_view(IconSizes::default())); children.push(item.preview_view(IconSizes::default(), context_drawer));
} }
PreviewKind::Location(location) => { PreviewKind::Location(location) => {
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity) {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.location_opt.as_ref() == Some(location) { if item.location_opt.as_ref() == Some(location) {
children.push(item.preview_view(tab.config.icon_sizes)); children
.push(item.preview_view(tab.config.icon_sizes, context_drawer));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -995,7 +1010,8 @@ impl App {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
children.push(item.preview_view(tab.config.icon_sizes)); children
.push(item.preview_view(tab.config.icon_sizes, context_drawer));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1005,7 +1021,13 @@ impl App {
} }
} }
} }
widget::settings::view_column(children).into()
// View column has extra padding not wanted when not showing in context drawer
if context_drawer {
widget::settings::view_column(children).into()
} else {
widget::column::with_children(children).into()
}
} }
fn settings(&self) -> Element<Message> { fn settings(&self) -> Element<Message> {
@ -1116,6 +1138,7 @@ impl Application for App {
toasts: widget::toaster::Toasts::new(Message::CloseToast), toasts: widget::toaster::Toasts::new(Message::CloseToast),
watcher_opt: None, watcher_opt: None,
window_id_opt: Some(window::Id::MAIN), window_id_opt: Some(window::Id::MAIN),
windows: HashMap::new(),
nav_dnd_hover: None, nav_dnd_hover: None,
tab_dnd_hover: None, tab_dnd_hover: None,
nav_drag_id: DragId::new(), nav_drag_id: DragId::new(),
@ -1254,12 +1277,16 @@ impl Application for App {
Some(Message::WindowClose) Some(Message::WindowClose)
} }
fn on_close_requested(&self, id: window::Id) -> Option<Self::Message> {
Some(Message::WindowCloseRequested(id))
}
fn on_context_drawer(&mut self) -> Command<Self::Message> { fn on_context_drawer(&mut self) -> Command<Self::Message> {
match self.context_page { match self.context_page {
ContextPage::Preview(_, _) => { ContextPage::Preview(_, _) => {
// Persist state of preview page // Persist state of preview page
if self.core.window.show_context != self.config.show_details { if self.core.window.show_context != self.config.show_details {
return self.update(Message::ToggleShowDetails); return self.update(Message::Preview);
} }
} }
_ => {} _ => {}
@ -2029,6 +2056,47 @@ impl Application for App {
} }
return self.update_notification(); return self.update_notification();
} }
Message::Preview => {
match self.mode {
Mode::App => {
let show_details = !self.config.show_details;
//TODO: move to update_config?
self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
self.core.window.show_context = show_details;
config_set!(show_details, show_details);
return self.update_config();
}
Mode::Desktop => {
let selected_paths = self.selected_paths(None);
let mut commands = Vec::with_capacity(selected_paths.len());
for path in selected_paths {
let mut settings = window::Settings::default();
settings.decorations = true;
settings.resizable = true;
settings.size = Size::new(480.0, 600.0);
settings.transparent = true;
#[cfg(target_os = "linux")]
{
// Use the dialog ID to make it float
settings.platform_specific.application_id =
"com.system76.CosmicFilesDialog".to_string();
}
let (id, command) = window::spawn(settings);
self.windows.insert(
id,
WindowKind::Preview(
None,
PreviewKind::Location(Location::Path(path)),
),
);
commands.push(command);
}
return Command::batch(commands);
}
}
}
Message::RescanTrash => { Message::RescanTrash => {
// Update trash icon if empty/full // Update trash icon if empty/full
let maybe_entity = self.nav_model.iter().find(|&entity| { let maybe_entity = self.nav_model.iter().find(|&entity| {
@ -2237,14 +2305,6 @@ impl Application for App {
return self.update_config(); return self.update_config();
} }
} }
Message::ToggleShowDetails => {
let show_details = !self.config.show_details;
//TODO: move to update_config?
self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
self.core.window.show_context = show_details;
config_set!(show_details, show_details);
return self.update_config();
}
Message::ToggleFoldersFirst => { Message::ToggleFoldersFirst => {
let mut config = self.config.tab; let mut config = self.config.tab;
config.folders_first = !config.folders_first; config.folders_first = !config.folders_first;
@ -2490,6 +2550,9 @@ impl Application for App {
]); ]);
} }
} }
Message::WindowCloseRequested(id) => {
self.windows.remove(&id);
}
Message::WindowNew => match env::current_exe() { Message::WindowNew => match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() { Ok(exe) => match process::Command::new(&exe).spawn() {
Ok(_child) => {} Ok(_child) => {}
@ -2840,7 +2903,7 @@ impl Application for App {
ContextPage::About => self.about(), ContextPage::About => self.about(),
ContextPage::EditHistory => self.edit_history(), ContextPage::EditHistory => self.edit_history(),
ContextPage::NetworkDrive => self.network_drive(), ContextPage::NetworkDrive => self.network_drive(),
ContextPage::Preview(entity_opt, kind) => self.preview(entity_opt, kind), ContextPage::Preview(entity_opt, kind) => self.preview(entity_opt, kind, true),
ContextPage::Settings => self.settings(), ContextPage::Settings => self.settings(),
}) })
} }
@ -3462,13 +3525,37 @@ impl Application for App {
content content
} }
fn view_window(&self, _id: WindowId) -> Element<Self::Message> { fn view_window(&self, id: WindowId) -> Element<Self::Message> {
//TODO: distinct views per window? let content = match self.windows.get(&id) {
self.view_main().map(|message| match message { Some(WindowKind::Main) | None => {
app::Message::App(app) => app, //TODO: distinct views per monitor in desktop mode
app::Message::Cosmic(cosmic) => Message::Cosmic(cosmic), return self.view_main().map(|message| match message {
app::Message::None => Message::None, app::Message::App(app) => app,
}) app::Message::Cosmic(cosmic) => Message::Cosmic(cosmic),
app::Message::None => Message::None,
});
}
Some(WindowKind::Preview(entity_opt, kind)) => self.preview(entity_opt, kind, false),
};
//TODO: these are hacks to have a sane scroll bar
let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing;
let scrollbar_width = 8;
widget::container(
widget::scrollable(widget::row::with_children(vec![
content,
widget::horizontal_space(Length::Fixed(scrollbar_width.into())).into(),
]))
.direction(scrollable::Direction::Vertical(
scrollable::Properties::new()
.width(scrollbar_width)
.scroller_width(scrollbar_width),
)),
)
.width(Length::Fill)
.height(Length::Fill)
.padding([0, space_l - scrollbar_width, space_l, space_l])
.style(theme::Container::WindowBackground)
.into()
} }
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {

View file

@ -164,9 +164,9 @@ impl<M: Send + 'static> Dialog<M> {
let mut settings = window::Settings::default(); let mut settings = window::Settings::default();
settings.decorations = false; settings.decorations = false;
settings.exit_on_close_request = false; settings.exit_on_close_request = false;
settings.transparent = true;
settings.size = Size::new(1024.0, 640.0);
settings.resizable = true; settings.resizable = true;
settings.size = Size::new(1024.0, 640.0);
settings.transparent = true;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -317,6 +317,7 @@ enum Message {
NotifyEvents(Vec<DebouncedEvent>), NotifyEvents(Vec<DebouncedEvent>),
NotifyWatcher(WatcherWrapper), NotifyWatcher(WatcherWrapper),
Open, Open,
Preview,
Save(bool), Save(bool),
SearchActivate, SearchActivate,
SearchClear, SearchClear,
@ -324,15 +325,14 @@ enum Message {
SearchSubmit, SearchSubmit,
TabMessage(tab::Message), TabMessage(tab::Message),
TabRescan(Vec<tab::Item>), TabRescan(Vec<tab::Item>),
ToggleShowDetails,
} }
impl From<AppMessage> for Message { impl From<AppMessage> for Message {
fn from(app_message: AppMessage) -> Message { fn from(app_message: AppMessage) -> Message {
match app_message { match app_message {
AppMessage::Preview => Message::Preview,
AppMessage::SearchActivate => Message::SearchActivate, AppMessage::SearchActivate => Message::SearchActivate,
AppMessage::TabMessage(_entity_opt, tab_message) => Message::TabMessage(tab_message), AppMessage::TabMessage(_entity_opt, tab_message) => Message::TabMessage(tab_message),
AppMessage::ToggleShowDetails => Message::ToggleShowDetails,
unsupported => { unsupported => {
log::warn!("{unsupported:?} not supported in dialog mode"); log::warn!("{unsupported:?} not supported in dialog mode");
Message::None Message::None
@ -452,13 +452,13 @@ impl App {
let mut children = Vec::with_capacity(1); let mut children = Vec::with_capacity(1);
match kind { match kind {
PreviewKind::Custom(PreviewItem(item)) => { PreviewKind::Custom(PreviewItem(item)) => {
children.push(item.preview_view(IconSizes::default())); children.push(item.preview_view(IconSizes::default(), true));
} }
PreviewKind::Location(location) => { PreviewKind::Location(location) => {
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.location_opt.as_ref() == Some(location) { if item.location_opt.as_ref() == Some(location) {
children.push(item.preview_view(self.tab.config.icon_sizes)); children.push(item.preview_view(self.tab.config.icon_sizes, true));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -470,7 +470,7 @@ impl App {
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
children.push(item.preview_view(self.tab.config.icon_sizes)); children.push(item.preview_view(self.tab.config.icon_sizes, true));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1245,6 +1245,15 @@ impl Application for App {
} }
} }
} }
Message::Preview => match self.context_page {
ContextPage::Preview(_, _) => {
self.core.window.show_context = !self.core.window.show_context;
}
_ => {
self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
self.core.window.show_context = true;
}
},
Message::Save(replace) => { Message::Save(replace) => {
if let DialogKind::SaveFile { filename } = &self.flags.kind { if let DialogKind::SaveFile { filename } = &self.flags.kind {
if !filename.is_empty() { if !filename.is_empty() {
@ -1426,15 +1435,6 @@ impl Application for App {
// Reset focus on location change // Reset focus on location change
return widget::text_input::focus(self.filename_id.clone()); return widget::text_input::focus(self.filename_id.clone());
} }
Message::ToggleShowDetails => match self.context_page {
ContextPage::Preview(_, _) => {
self.core.window.show_context = !self.core.window.show_context;
}
_ => {
self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
self.core.window.show_context = true;
}
},
} }
Command::none() Command::none()

View file

@ -1097,7 +1097,7 @@ impl Item {
} }
} }
pub fn preview_view(&self, sizes: IconSizes) -> Element<'static, app::Message> { pub fn preview_view(&self, sizes: IconSizes, nav_row: bool) -> Element<'static, app::Message> {
let cosmic_theme::Spacing { let cosmic_theme::Spacing {
space_xxxs, space_xxxs,
space_xxs, space_xxs,
@ -1107,25 +1107,27 @@ impl Item {
let mut column = widget::column().spacing(space_m); let mut column = widget::column().spacing(space_m);
let mut row = widget::row::with_capacity(3).spacing(space_xxs); if nav_row {
row = row.push( let mut row = widget::row::with_capacity(3).spacing(space_xxs);
widget::button::icon(widget::icon::from_name("go-previous-symbolic")) row = row.push(
.on_press(app::Message::TabMessage(None, Message::ItemLeft)), widget::button::icon(widget::icon::from_name("go-previous-symbolic"))
); .on_press(app::Message::TabMessage(None, Message::ItemLeft)),
row = row.push( );
widget::button::icon(widget::icon::from_name("go-next-symbolic")) row = row.push(
.on_press(app::Message::TabMessage(None, Message::ItemRight)), widget::button::icon(widget::icon::from_name("go-next-symbolic"))
); .on_press(app::Message::TabMessage(None, Message::ItemRight)),
);
if self.mime.type_() == mime::IMAGE { if self.mime.type_() == mime::IMAGE {
if let Some(_path) = self.path_opt() { if let Some(_path) = self.path_opt() {
row = row.push( row = row.push(
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
.on_press(app::Message::TabMessage(None, Message::Gallery(true))), .on_press(app::Message::TabMessage(None, Message::Gallery(true))),
); );
}
} }
column = column.push(row);
} }
column = column.push(row);
column = column.push(widget::row::with_children(vec![ column = column.push(widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(), widget::horizontal_space(Length::Fill).into(),