Implement desktop view toggles, part of #547
This commit is contained in:
parent
cc2a62a14c
commit
c511242dd4
8 changed files with 459 additions and 125 deletions
|
|
@ -11,6 +11,15 @@ recents = Recents
|
||||||
undo = Undo
|
undo = Undo
|
||||||
today = Today
|
today = Today
|
||||||
|
|
||||||
|
# Desktop view options
|
||||||
|
desktop-view-options = Desktop view options...
|
||||||
|
show-on-desktop = Show on Desktop
|
||||||
|
desktop-folder-content = Desktop folder content
|
||||||
|
mounted-drives = Mounted drives
|
||||||
|
trash-folder-icon = Trash folder icon
|
||||||
|
icon-size-and-spacing = Icon size and spacing
|
||||||
|
icon-size = Icon size
|
||||||
|
|
||||||
# List view
|
# List view
|
||||||
name = Name
|
name = Name
|
||||||
modified = Modified
|
modified = Modified
|
||||||
|
|
|
||||||
255
src/app.rs
255
src/app.rs
|
|
@ -58,7 +58,7 @@ use wayland_client::{protocol::wl_output::WlOutput, Proxy};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
||||||
config::{AppTheme, Config, Favorite, IconSizes, TabConfig},
|
config::{AppTheme, Config, DesktopConfig, Favorite, IconSizes, TabConfig},
|
||||||
desktop_dir, fl, home_dir,
|
desktop_dir, fl, home_dir,
|
||||||
key_bind::key_binds,
|
key_bind::key_binds,
|
||||||
localize::LANGUAGE_SORTER,
|
localize::LANGUAGE_SORTER,
|
||||||
|
|
@ -95,6 +95,7 @@ pub enum Action {
|
||||||
CosmicSettingsAppearance,
|
CosmicSettingsAppearance,
|
||||||
CosmicSettingsDisplays,
|
CosmicSettingsDisplays,
|
||||||
CosmicSettingsWallpaper,
|
CosmicSettingsWallpaper,
|
||||||
|
DesktopViewOptions,
|
||||||
EditHistory,
|
EditHistory,
|
||||||
EditLocation,
|
EditLocation,
|
||||||
ExtractHere,
|
ExtractHere,
|
||||||
|
|
@ -151,6 +152,7 @@ impl Action {
|
||||||
Action::CosmicSettingsAppearance => Message::CosmicSettings("appearance"),
|
Action::CosmicSettingsAppearance => Message::CosmicSettings("appearance"),
|
||||||
Action::CosmicSettingsDisplays => Message::CosmicSettings("displays"),
|
Action::CosmicSettingsDisplays => Message::CosmicSettings("displays"),
|
||||||
Action::CosmicSettingsWallpaper => Message::CosmicSettings("wallpaper"),
|
Action::CosmicSettingsWallpaper => Message::CosmicSettings("wallpaper"),
|
||||||
|
Action::DesktopViewOptions => Message::DesktopViewOptions,
|
||||||
Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),
|
Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),
|
||||||
Action::EditLocation => {
|
Action::EditLocation => {
|
||||||
Message::TabMessage(entity_opt, tab::Message::EditLocationToggle)
|
Message::TabMessage(entity_opt, tab::Message::EditLocationToggle)
|
||||||
|
|
@ -260,6 +262,8 @@ pub enum Message {
|
||||||
Copy(Option<Entity>),
|
Copy(Option<Entity>),
|
||||||
CosmicSettings(&'static str),
|
CosmicSettings(&'static str),
|
||||||
Cut(Option<Entity>),
|
Cut(Option<Entity>),
|
||||||
|
DesktopConfig(DesktopConfig),
|
||||||
|
DesktopViewOptions,
|
||||||
DialogCancel,
|
DialogCancel,
|
||||||
DialogComplete,
|
DialogComplete,
|
||||||
DialogPush(DialogPage),
|
DialogPush(DialogPage),
|
||||||
|
|
@ -446,6 +450,7 @@ pub struct MounterData(MounterKey, MounterItem);
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum WindowKind {
|
pub enum WindowKind {
|
||||||
Desktop(Entity),
|
Desktop(Entity),
|
||||||
|
DesktopViewOptions,
|
||||||
Preview(Option<Entity>, PreviewKind),
|
Preview(Option<Entity>, PreviewKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,13 +588,16 @@ impl App {
|
||||||
selection_path: Option<PathBuf>,
|
selection_path: Option<PathBuf>,
|
||||||
) -> Command<Message> {
|
) -> Command<Message> {
|
||||||
log::info!("rescan_tab {entity:?} {location:?} {selection_path:?}");
|
log::info!("rescan_tab {entity:?} {location:?} {selection_path:?}");
|
||||||
|
let desktop_config = self.config.desktop;
|
||||||
let mounters = self.mounters.clone();
|
let mounters = self.mounters.clone();
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(mounters, icon_sizes))
|
match tokio::task::spawn_blocking(move || {
|
||||||
.await
|
location2.scan(desktop_config, mounters, icon_sizes)
|
||||||
|
})
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
message::app(Message::TabRescan(entity, location, items, selection_path))
|
message::app(Message::TabRescan(entity, location, items, selection_path))
|
||||||
|
|
@ -625,17 +633,14 @@ impl App {
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
let mut title_location_opt = None;
|
let mut title_location_opt = None;
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
match &tab.location {
|
if let Some(path) = tab.location.path_opt() {
|
||||||
Location::Path(path) | Location::Search(path, ..) => {
|
let location = if !self.search_input.is_empty() {
|
||||||
let location = if !self.search_input.is_empty() {
|
Location::Search(path.to_path_buf(), self.search_input.clone())
|
||||||
Location::Search(path.clone(), self.search_input.clone())
|
} else {
|
||||||
} else {
|
Location::Path(path.to_path_buf())
|
||||||
Location::Path(path.clone())
|
};
|
||||||
};
|
tab.change_location(&location, None);
|
||||||
tab.change_location(&location, None);
|
title_location_opt = Some((tab.title(), tab.location.clone()));
|
||||||
title_location_opt = Some((tab.title(), tab.location.clone()));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((title, location)) = title_location_opt {
|
if let Some((title, location)) = title_location_opt {
|
||||||
|
|
@ -680,6 +685,22 @@ impl App {
|
||||||
Command::batch(commands)
|
Command::batch(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_desktop(&mut self) -> Command<Message> {
|
||||||
|
let mut needs_reload = Vec::new();
|
||||||
|
for entity in self.tab_model.iter() {
|
||||||
|
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||||
|
if let Location::Desktop(..) = &tab.location {
|
||||||
|
needs_reload.push((entity, tab.location.clone()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut commands = Vec::with_capacity(needs_reload.len());
|
||||||
|
for (entity, location) in needs_reload {
|
||||||
|
commands.push(self.rescan_tab(entity, location, None));
|
||||||
|
}
|
||||||
|
Command::batch(commands)
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_nav_model_location(&mut self, location: &Location) {
|
fn activate_nav_model_location(&mut self, location: &Location) {
|
||||||
let nav_bar_id = self.nav_model.iter().find(|&id| {
|
let nav_bar_id = self.nav_model.iter().find(|&id| {
|
||||||
self.nav_model
|
self.nav_model
|
||||||
|
|
@ -771,7 +792,7 @@ impl App {
|
||||||
if let Some(path) = item.path() {
|
if let Some(path) = item.path() {
|
||||||
b = b.data(Location::Path(path.clone()));
|
b = b.data(Location::Path(path.clone()));
|
||||||
}
|
}
|
||||||
if let Some(icon) = item.icon() {
|
if let Some(icon) = item.icon(true) {
|
||||||
b = b.icon(widget::icon::icon(icon).size(16));
|
b = b.icon(widget::icon::icon(icon).size(16));
|
||||||
}
|
}
|
||||||
if item.is_mounted() {
|
if item.is_mounted() {
|
||||||
|
|
@ -830,8 +851,8 @@ impl App {
|
||||||
let mut new_paths = HashSet::new();
|
let mut new_paths = HashSet::new();
|
||||||
for entity in self.tab_model.iter() {
|
for entity in self.tab_model.iter() {
|
||||||
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||||
if let Location::Path(path) = &tab.location {
|
if let Some(path) = tab.location.path_opt() {
|
||||||
new_paths.insert(path.clone());
|
new_paths.insert(path.to_path_buf());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -954,6 +975,72 @@ impl App {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn desktop_view_options(&self) -> Element<Message> {
|
||||||
|
let config = self.config.desktop;
|
||||||
|
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
let mut section = widget::settings::section().title(fl!("show-on-desktop"));
|
||||||
|
section = section.add(
|
||||||
|
widget::settings::item::builder(fl!("desktop-folder-content")).toggler(
|
||||||
|
config.show_content,
|
||||||
|
move |show_content| {
|
||||||
|
Message::DesktopConfig(DesktopConfig {
|
||||||
|
show_content,
|
||||||
|
..config
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
section = section.add(
|
||||||
|
widget::settings::item::builder(fl!("mounted-drives")).toggler(
|
||||||
|
config.show_mounted_drives,
|
||||||
|
move |show_mounted_drives| {
|
||||||
|
Message::DesktopConfig(DesktopConfig {
|
||||||
|
show_mounted_drives,
|
||||||
|
..config
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
section = section.add(
|
||||||
|
widget::settings::item::builder(fl!("trash-folder-icon")).toggler(
|
||||||
|
config.show_trash,
|
||||||
|
move |show_trash| {
|
||||||
|
Message::DesktopConfig(DesktopConfig {
|
||||||
|
show_trash,
|
||||||
|
..config
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
children.push(section.into());
|
||||||
|
|
||||||
|
/*TODO: Desktop icon size and spacing
|
||||||
|
let mut section = widget::settings::section().title(fl!("icon-size-and-spacing"));
|
||||||
|
let grid: u16 = config.icon_sizes.grid.into();
|
||||||
|
section = section.add(
|
||||||
|
widget::settings::item::builder(fl!("icon-size"))
|
||||||
|
.description(format!("{}%", grid))
|
||||||
|
.control(
|
||||||
|
widget::slider(50..=500, grid, move |grid| {
|
||||||
|
Message::DesktopConfig(DesktopConfig {
|
||||||
|
icon_sizes: IconSizes {
|
||||||
|
grid: NonZeroU16::new(grid).unwrap(),
|
||||||
|
..config.icon_sizes
|
||||||
|
},
|
||||||
|
..config
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.step(25u16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
children.push(section.into());
|
||||||
|
*/
|
||||||
|
|
||||||
|
widget::settings::view_column(children).into()
|
||||||
|
}
|
||||||
|
|
||||||
fn edit_history(&self) -> Element<Message> {
|
fn edit_history(&self) -> Element<Message> {
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
|
@ -1459,6 +1546,31 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::DesktopConfig(config) => {
|
||||||
|
if config != self.config.desktop {
|
||||||
|
config_set!(desktop, config);
|
||||||
|
return self.update_desktop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::DesktopViewOptions => {
|
||||||
|
let mut settings = window::Settings::default();
|
||||||
|
settings.decorations = true;
|
||||||
|
settings.min_size = Some(Size::new(360.0, 180.0));
|
||||||
|
settings.resizable = true;
|
||||||
|
settings.size = Size::new(480.0, 444.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::DesktopViewOptions);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
Message::DialogCancel => {
|
Message::DialogCancel => {
|
||||||
self.dialog_pages.pop_front();
|
self.dialog_pages.pop_front();
|
||||||
}
|
}
|
||||||
|
|
@ -1685,6 +1797,9 @@ impl Application for App {
|
||||||
//TODO: this could change favorites IDs while they are in use
|
//TODO: this could change favorites IDs while they are in use
|
||||||
self.update_nav_model();
|
self.update_nav_model();
|
||||||
|
|
||||||
|
// Update desktop tabs
|
||||||
|
commands.push(self.update_desktop());
|
||||||
|
|
||||||
return Command::batch(commands);
|
return Command::batch(commands);
|
||||||
}
|
}
|
||||||
Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => {
|
Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => {
|
||||||
|
|
@ -1739,9 +1854,9 @@ impl Application for App {
|
||||||
Message::NewItem(entity_opt, dir) => {
|
Message::NewItem(entity_opt, dir) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if let Location::Path(path) = &tab.location {
|
if let Some(path) = &tab.location.path_opt() {
|
||||||
self.dialog_pages.push_back(DialogPage::NewItem {
|
self.dialog_pages.push_back(DialogPage::NewItem {
|
||||||
parent: path.clone(),
|
parent: path.to_path_buf(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
dir,
|
dir,
|
||||||
});
|
});
|
||||||
|
|
@ -1760,7 +1875,7 @@ impl Application for App {
|
||||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if let Location::Path(path) = &tab.location {
|
if let Some(path) = &tab.location.path_opt() {
|
||||||
let mut contains_change = false;
|
let mut contains_change = false;
|
||||||
for event in events.iter() {
|
for event in events.iter() {
|
||||||
for event_path in event.paths.iter() {
|
for event_path in event.paths.iter() {
|
||||||
|
|
@ -1834,18 +1949,18 @@ impl Application for App {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if let Location::Path(path) = &tab.location {
|
if let Some(path) = &tab.location.path_opt() {
|
||||||
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 {
|
||||||
if let Some(Location::Path(path)) = &item.location_opt {
|
if let Some(path) = item.path_opt() {
|
||||||
paths.push(path.clone());
|
paths.push(path.to_path_buf());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
paths.push(path.clone());
|
paths.push(path.to_path_buf());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1975,7 +2090,7 @@ impl Application for App {
|
||||||
Message::Paste(entity_opt) => {
|
Message::Paste(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if let Location::Path(path) = &tab.location {
|
if let Some(path) = tab.location.path_opt() {
|
||||||
let to = path.clone();
|
let to = path.clone();
|
||||||
return clipboard::read_data::<ClipboardPaste, _>(move |contents_opt| {
|
return clipboard::read_data::<ClipboardPaste, _>(move |contents_opt| {
|
||||||
match contents_opt {
|
match contents_opt {
|
||||||
|
|
@ -2124,7 +2239,7 @@ impl Application for App {
|
||||||
let maybe_entity = self.nav_model.iter().find(|&entity| {
|
let maybe_entity = self.nav_model.iter().find(|&entity| {
|
||||||
self.nav_model
|
self.nav_model
|
||||||
.data::<Location>(entity)
|
.data::<Location>(entity)
|
||||||
.map(|loc| *loc == Location::Trash)
|
.map(|loc| matches!(loc, Location::Trash))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
});
|
});
|
||||||
if let Some(entity) = maybe_entity {
|
if let Some(entity) = maybe_entity {
|
||||||
|
|
@ -2132,19 +2247,19 @@ impl Application for App {
|
||||||
.icon_set(entity, widget::icon::icon(tab::trash_icon_symbolic(16)));
|
.icon_set(entity, widget::icon::icon(tab::trash_icon_symbolic(16)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.rescan_trash();
|
return Command::batch([self.rescan_trash(), self.update_desktop()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Rename(entity_opt) => {
|
Message::Rename(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if let Location::Path(parent) = &tab.location {
|
if let Some(parent) = tab.location.path_opt() {
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
let mut selected = Vec::new();
|
let mut selected = Vec::new();
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(Location::Path(path)) = &item.location_opt {
|
if let Some(path) = item.path_opt() {
|
||||||
selected.push(path.clone());
|
selected.push(path.to_path_buf());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2470,6 +2585,17 @@ impl Application for App {
|
||||||
log::error!("failed to get current executable path: {}", err);
|
log::error!("failed to get current executable path: {}", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
tab::Command::OpenTrash => {
|
||||||
|
//TODO: use handler for x-scheme-handler/trash and open trash:///
|
||||||
|
let mut command = process::Command::new("cosmic-files");
|
||||||
|
command.arg("--trash");
|
||||||
|
match spawn_detached(&mut command) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to run cosmic-files --trash: {}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
tab::Command::Preview(kind) => {
|
tab::Command::Preview(kind) => {
|
||||||
self.context_page = ContextPage::Preview(Some(entity), kind);
|
self.context_page = ContextPage::Preview(Some(entity), kind);
|
||||||
self.set_show_context(true);
|
self.set_show_context(true);
|
||||||
|
|
@ -2532,12 +2658,13 @@ impl Application for App {
|
||||||
self.toasts.remove(id);
|
self.toasts.remove(id);
|
||||||
|
|
||||||
let mut paths = Vec::with_capacity(recently_trashed.len());
|
let mut paths = Vec::with_capacity(recently_trashed.len());
|
||||||
|
let desktop_config = self.config.desktop;
|
||||||
let mounters = self.mounters.clone();
|
let mounters = self.mounters.clone();
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
|
|
||||||
return cosmic::command::future(async move {
|
return cosmic::command::future(async move {
|
||||||
match tokio::task::spawn_blocking(move || {
|
match tokio::task::spawn_blocking(move || {
|
||||||
Location::Trash.scan(mounters, icon_sizes)
|
Location::Trash.scan(desktop_config, mounters, icon_sizes)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -2802,7 +2929,11 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
NavMenuAction::Preview(entity) => {
|
NavMenuAction::Preview(entity) => {
|
||||||
if let Some(Location::Path(path)) = self.nav_model.data::<Location>(entity) {
|
if let Some(path) = self
|
||||||
|
.nav_model
|
||||||
|
.data::<Location>(entity)
|
||||||
|
.and_then(|location| location.path_opt())
|
||||||
|
{
|
||||||
match tab::item_from_path(path, IconSizes::default()) {
|
match tab::item_from_path(path, IconSizes::default()) {
|
||||||
Ok(item) => {
|
Ok(item) => {
|
||||||
self.context_page = ContextPage::Preview(
|
self.context_page = ContextPage::Preview(
|
||||||
|
|
@ -2856,22 +2987,28 @@ impl Application for App {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match output_info_opt {
|
let display = match output_info_opt {
|
||||||
Some(output_info) => match output_info.name {
|
Some(output_info) => match output_info.name {
|
||||||
Some(output_name) => {
|
Some(output_name) => {
|
||||||
self.surface_names.insert(surface_id, output_name.clone());
|
self.surface_names.insert(surface_id, output_name.clone());
|
||||||
|
output_name
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::warn!("output {}: no output name", output.id());
|
log::warn!("output {}: no output name", output.id());
|
||||||
|
String::new()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
log::warn!("output {}: no output info", output.id());
|
log::warn!("output {}: no output info", output.id());
|
||||||
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let (entity, command) =
|
let (entity, command) = self.open_tab_entity(
|
||||||
self.open_tab_entity(Location::Path(desktop_dir()), false, None);
|
Location::Desktop(desktop_dir(), display),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
self.windows.insert(surface_id, WindowKind::Desktop(entity));
|
self.windows.insert(surface_id, WindowKind::Desktop(entity));
|
||||||
return Command::batch([
|
return Command::batch([
|
||||||
command,
|
command,
|
||||||
|
|
@ -3557,19 +3694,20 @@ impl Application for App {
|
||||||
fn view_window(&self, id: WindowId) -> Element<Self::Message> {
|
fn view_window(&self, id: WindowId) -> Element<Self::Message> {
|
||||||
let content = match self.windows.get(&id) {
|
let content = match self.windows.get(&id) {
|
||||||
Some(WindowKind::Desktop(entity)) => {
|
Some(WindowKind::Desktop(entity)) => {
|
||||||
let mut tab_column = widget::column::with_capacity(2);
|
let mut tab_column = widget::column::with_capacity(3);
|
||||||
|
|
||||||
match self.tab_model.data::<Tab>(*entity) {
|
let tab_view = match self.tab_model.data::<Tab>(*entity) {
|
||||||
Some(tab) => {
|
Some(tab) => tab
|
||||||
let tab_view = tab
|
.view(&self.key_binds)
|
||||||
.view(&self.key_binds)
|
.map(move |message| Message::TabMessage(Some(*entity), message)),
|
||||||
.map(move |message| Message::TabMessage(Some(*entity), message));
|
None => widget::vertical_space(Length::Fill).into(),
|
||||||
tab_column = tab_column.push(tab_view);
|
};
|
||||||
}
|
|
||||||
None => {
|
let mut popover = widget::popover(tab_view);
|
||||||
//TODO
|
if let Some(dialog) = self.dialog() {
|
||||||
}
|
popover = popover.popup(dialog);
|
||||||
}
|
}
|
||||||
|
tab_column = tab_column.push(popover);
|
||||||
|
|
||||||
// The toaster is added on top of an empty element to ensure that it does not override context menus
|
// The toaster is added on top of an empty element to ensure that it does not override context menus
|
||||||
tab_column = tab_column.push(widget::toaster(
|
tab_column = tab_column.push(widget::toaster(
|
||||||
|
|
@ -3579,6 +3717,7 @@ impl Application for App {
|
||||||
|
|
||||||
return tab_column.into();
|
return tab_column.into();
|
||||||
}
|
}
|
||||||
|
Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(),
|
||||||
Some(WindowKind::Preview(entity_opt, kind)) => self.preview(entity_opt, kind, false),
|
Some(WindowKind::Preview(entity_opt, kind)) => self.preview(entity_opt, kind, false),
|
||||||
None => {
|
None => {
|
||||||
//TODO: distinct views per monitor in desktop mode
|
//TODO: distinct views per monitor in desktop mode
|
||||||
|
|
@ -3597,7 +3736,10 @@ impl Application for App {
|
||||||
widget::container(
|
widget::container(
|
||||||
widget::scrollable(widget::row::with_children(vec![
|
widget::scrollable(widget::row::with_children(vec![
|
||||||
content,
|
content,
|
||||||
widget::horizontal_space(Length::Fixed((scrollbar_width + scrollbar_margin).into())).into(),
|
widget::horizontal_space(Length::Fixed(
|
||||||
|
(scrollbar_width + scrollbar_margin).into(),
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
]))
|
]))
|
||||||
.direction(scrollable::Direction::Vertical(
|
.direction(scrollable::Direction::Vertical(
|
||||||
scrollable::Properties::new()
|
scrollable::Properties::new()
|
||||||
|
|
@ -3607,7 +3749,12 @@ impl Application for App {
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.padding([0, space_l - (scrollbar_width + scrollbar_margin), space_l, space_l])
|
.padding([
|
||||||
|
0,
|
||||||
|
space_l - (scrollbar_width + scrollbar_margin),
|
||||||
|
space_l,
|
||||||
|
space_l,
|
||||||
|
])
|
||||||
.style(theme::Container::WindowBackground)
|
.style(theme::Container::WindowBackground)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
@ -4085,7 +4232,11 @@ pub(crate) mod test_utils {
|
||||||
|
|
||||||
// New tab with items
|
// New tab with items
|
||||||
let location = Location::Path(path.to_owned());
|
let location = Location::Path(path.to_owned());
|
||||||
let items = location.scan(Mounters::new(MounterMap::new()), IconSizes::default());
|
let items = location.scan(
|
||||||
|
DesktopConfig::default(),
|
||||||
|
Mounters::new(MounterMap::new()),
|
||||||
|
IconSizes::default(),
|
||||||
|
);
|
||||||
let mut tab = Tab::new(location, TabConfig::default());
|
let mut tab = Tab::new(location, TabConfig::default());
|
||||||
tab.set_items(items);
|
tab.set_items(items);
|
||||||
|
|
||||||
|
|
@ -4127,7 +4278,7 @@ pub(crate) mod test_utils {
|
||||||
/// Asserts `tab`'s location changed to `path`
|
/// Asserts `tab`'s location changed to `path`
|
||||||
pub fn assert_eq_tab_path(tab: &Tab, path: &Path) {
|
pub fn assert_eq_tab_path(tab: &Tab, path: &Path) {
|
||||||
// Paths should be the same
|
// Paths should be the same
|
||||||
let Location::Path(ref tab_path) = tab.location else {
|
let Some(tab_path) = tab.location.path_opt() else {
|
||||||
panic!("Expected tab's location to be a path");
|
panic!("Expected tab's location to be a path");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -4142,7 +4293,7 @@ pub(crate) mod test_utils {
|
||||||
|
|
||||||
/// Assert that tab's items are equal to a path's entries.
|
/// Assert that tab's items are equal to a path's entries.
|
||||||
pub fn assert_eq_tab_path_contents(tab: &Tab, path: &Path) {
|
pub fn assert_eq_tab_path_contents(tab: &Tab, path: &Path) {
|
||||||
let Location::Path(ref tab_path) = tab.location else {
|
let Some(tab_path) = tab.location.path_opt() else {
|
||||||
panic!("Expected tab's location to be a path");
|
panic!("Expected tab's location to be a path");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,10 @@ impl Favorite {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub app_theme: AppTheme,
|
pub app_theme: AppTheme,
|
||||||
|
pub desktop: DesktopConfig,
|
||||||
pub favorites: Vec<Favorite>,
|
pub favorites: Vec<Favorite>,
|
||||||
pub show_details: bool,
|
pub show_details: bool,
|
||||||
pub tab: TabConfig,
|
pub tab: TabConfig,
|
||||||
|
|
@ -131,6 +133,7 @@ impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
app_theme: AppTheme::System,
|
app_theme: AppTheme::System,
|
||||||
|
desktop: DesktopConfig::default(),
|
||||||
favorites: vec![
|
favorites: vec![
|
||||||
Favorite::Home,
|
Favorite::Home,
|
||||||
Favorite::Documents,
|
Favorite::Documents,
|
||||||
|
|
@ -145,12 +148,31 @@ impl Default for Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DesktopConfig {
|
||||||
|
pub show_content: bool,
|
||||||
|
pub show_mounted_drives: bool,
|
||||||
|
pub show_trash: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DesktopConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
show_content: true,
|
||||||
|
show_mounted_drives: false,
|
||||||
|
show_trash: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Global and local [`crate::tab::Tab`] config.
|
/// Global and local [`crate::tab::Tab`] config.
|
||||||
///
|
///
|
||||||
/// [`TabConfig`] contains options that are passed to each instance of [`crate::tab::Tab`].
|
/// [`TabConfig`] contains options that are passed to each instance of [`crate::tab::Tab`].
|
||||||
/// These options are set globally through the main config, but each tab may change options
|
/// These options are set globally through the main config, but each tab may change options
|
||||||
/// locally. Local changes aren't saved to the main config.
|
/// locally. Local changes aren't saved to the main config.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct TabConfig {
|
pub struct TabConfig {
|
||||||
pub view: View,
|
pub view: View,
|
||||||
/// Show folders before files
|
/// Show folders before files
|
||||||
|
|
@ -179,6 +201,7 @@ macro_rules! percent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct IconSizes {
|
pub struct IconSizes {
|
||||||
pub list: NonZeroU16,
|
pub list: NonZeroU16,
|
||||||
pub grid: NonZeroU16,
|
pub grid: NonZeroU16,
|
||||||
|
|
|
||||||
|
|
@ -485,11 +485,15 @@ impl App {
|
||||||
|
|
||||||
fn rescan_tab(&self) -> Command<Message> {
|
fn rescan_tab(&self) -> Command<Message> {
|
||||||
let location = self.tab.location.clone();
|
let location = self.tab.location.clone();
|
||||||
|
let desktop_config = self.flags.config.desktop;
|
||||||
let mounters = self.mounters.clone();
|
let mounters = self.mounters.clone();
|
||||||
let icon_sizes = self.tab.config.icon_sizes;
|
let icon_sizes = self.tab.config.icon_sizes;
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
match tokio::task::spawn_blocking(move || location.scan(mounters, icon_sizes)).await
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
location.scan(desktop_config, mounters, icon_sizes)
|
||||||
|
})
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(items) => message::app(Message::TabRescan(items)),
|
Ok(items) => message::app(Message::TabRescan(items)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -589,7 +593,7 @@ impl App {
|
||||||
if let Some(path) = item.path() {
|
if let Some(path) = item.path() {
|
||||||
b = b.data(Location::Path(path.clone()));
|
b = b.data(Location::Path(path.clone()));
|
||||||
}
|
}
|
||||||
if let Some(icon) = item.icon() {
|
if let Some(icon) = item.icon(true) {
|
||||||
b = b.icon(widget::icon::icon(icon).size(16));
|
b = b.icon(widget::icon::icon(icon).size(16));
|
||||||
}
|
}
|
||||||
if item.is_mounted() {
|
if item.is_mounted() {
|
||||||
|
|
@ -615,8 +619,8 @@ impl App {
|
||||||
fn update_watcher(&mut self) -> Command<Message> {
|
fn update_watcher(&mut self) -> Command<Message> {
|
||||||
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
|
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
|
||||||
let mut new_paths = HashSet::new();
|
let mut new_paths = HashSet::new();
|
||||||
if let Location::Path(path) = &self.tab.location {
|
if let Some(path) = &self.tab.location.path_opt() {
|
||||||
new_paths.insert(path.clone());
|
new_paths.insert(path.to_path_buf());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwatch paths no longer used
|
// Unwatch paths no longer used
|
||||||
|
|
@ -1121,9 +1125,9 @@ impl Application for App {
|
||||||
return Command::batch(commands);
|
return Command::batch(commands);
|
||||||
}
|
}
|
||||||
Message::NewFolder => {
|
Message::NewFolder => {
|
||||||
if let Location::Path(path) = &self.tab.location {
|
if let Some(path) = self.tab.location.path_opt() {
|
||||||
self.dialog_pages.push_back(DialogPage::NewFolder {
|
self.dialog_pages.push_back(DialogPage::NewFolder {
|
||||||
parent: path.clone(),
|
parent: path.to_path_buf(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
});
|
});
|
||||||
return widget::text_input::focus(self.dialog_text_input.clone());
|
return widget::text_input::focus(self.dialog_text_input.clone());
|
||||||
|
|
@ -1132,7 +1136,7 @@ impl Application for App {
|
||||||
Message::NotifyEvents(events) => {
|
Message::NotifyEvents(events) => {
|
||||||
log::debug!("{:?}", events);
|
log::debug!("{:?}", events);
|
||||||
|
|
||||||
if let Location::Path(path) = &self.tab.location {
|
if let Some(path) = self.tab.location.path_opt() {
|
||||||
let mut contains_change = false;
|
let mut contains_change = false;
|
||||||
for event in events.iter() {
|
for event in events.iter() {
|
||||||
for event_path in event.paths.iter() {
|
for event_path in event.paths.iter() {
|
||||||
|
|
@ -1198,7 +1202,7 @@ impl Application for 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 {
|
||||||
if let Some(Location::Path(path)) = &item.location_opt {
|
if let Some(path) = item.path_opt() {
|
||||||
paths.push(path.clone());
|
paths.push(path.clone());
|
||||||
let _ = update_recently_used(
|
let _ = update_recently_used(
|
||||||
&path.clone(),
|
&path.clone(),
|
||||||
|
|
@ -1258,7 +1262,7 @@ impl Application for App {
|
||||||
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() {
|
||||||
if let Location::Path(tab_path) = &self.tab.location {
|
if let Some(tab_path) = self.tab.location.path_opt() {
|
||||||
let path = tab_path.join(&filename);
|
let path = tab_path.join(&filename);
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
// cd to directory
|
// cd to directory
|
||||||
|
|
|
||||||
14
src/menu.rs
14
src/menu.rs
|
|
@ -95,7 +95,10 @@ pub fn context_menu<'a>(
|
||||||
match (&tab.mode, &tab.location) {
|
match (&tab.mode, &tab.location) {
|
||||||
(
|
(
|
||||||
tab::Mode::App | tab::Mode::Desktop,
|
tab::Mode::App | tab::Mode::Desktop,
|
||||||
Location::Path(_) | Location::Search(_, _) | Location::Recents,
|
Location::Desktop(_, _)
|
||||||
|
| Location::Path(_)
|
||||||
|
| Location::Search(_, _)
|
||||||
|
| Location::Recents,
|
||||||
) => {
|
) => {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||||
|
|
@ -189,11 +192,18 @@ pub fn context_menu<'a>(
|
||||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||||
|
children.push(divider::horizontal::light().into());
|
||||||
|
children.push(
|
||||||
|
menu_item(fl!("desktop-view-options"), Action::DesktopViewOptions).into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
tab::Mode::Dialog(dialog_kind),
|
tab::Mode::Dialog(dialog_kind),
|
||||||
Location::Path(_) | Location::Search(_, _) | Location::Recents,
|
Location::Desktop(_, _)
|
||||||
|
| Location::Path(_)
|
||||||
|
| Location::Search(_, _)
|
||||||
|
| Location::Recents,
|
||||||
) => {
|
) => {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,37 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
|
||||||
|
let mut items = MounterItems::new();
|
||||||
|
for (i, mount) in monitor.mounts().into_iter().enumerate() {
|
||||||
|
items.push(MounterItem::Gvfs(Item {
|
||||||
|
kind: ItemKind::Mount,
|
||||||
|
index: i,
|
||||||
|
name: MountExt::name(&mount).to_string(),
|
||||||
|
is_mounted: true,
|
||||||
|
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
|
||||||
|
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
|
||||||
|
path_opt: MountExt::root(&mount).path(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (i, volume) in monitor.volumes().into_iter().enumerate() {
|
||||||
|
if volume.get_mount().is_some() {
|
||||||
|
// Volumes with mounts are already listed by mount
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items.push(MounterItem::Gvfs(Item {
|
||||||
|
kind: ItemKind::Volume,
|
||||||
|
index: i,
|
||||||
|
name: VolumeExt::name(&volume).to_string(),
|
||||||
|
is_mounted: false,
|
||||||
|
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
|
||||||
|
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
|
||||||
|
path_opt: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
let file = gio::File::for_uri(uri);
|
let file = gio::File::for_uri(uri);
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
@ -166,6 +197,7 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOp
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
|
Items(IconSizes, mpsc::Sender<MounterItems>),
|
||||||
Rescan,
|
Rescan,
|
||||||
Mount(MounterItem),
|
Mount(MounterItem),
|
||||||
NetworkDrive(String),
|
NetworkDrive(String),
|
||||||
|
|
@ -198,6 +230,7 @@ pub struct Item {
|
||||||
name: String,
|
name: String,
|
||||||
is_mounted: bool,
|
is_mounted: bool,
|
||||||
icon_opt: Option<PathBuf>,
|
icon_opt: Option<PathBuf>,
|
||||||
|
icon_symbolic_opt: Option<PathBuf>,
|
||||||
path_opt: Option<PathBuf>,
|
path_opt: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,10 +243,13 @@ impl Item {
|
||||||
self.is_mounted
|
self.is_mounted
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(&self) -> Option<widget::icon::Handle> {
|
pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {
|
||||||
self.icon_opt
|
if symbolic {
|
||||||
.as_ref()
|
self.icon_symbolic_opt.as_ref()
|
||||||
.map(|icon| widget::icon::from_path(icon.clone()))
|
} else {
|
||||||
|
self.icon_opt.as_ref()
|
||||||
|
}
|
||||||
|
.map(|icon| widget::icon::from_path(icon.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> Option<PathBuf> {
|
pub fn path(&self) -> Option<PathBuf> {
|
||||||
|
|
@ -281,39 +317,11 @@ impl Gvfs {
|
||||||
|
|
||||||
while let Some(command) = command_rx.recv().await {
|
while let Some(command) = command_rx.recv().await {
|
||||||
match command {
|
match command {
|
||||||
|
Cmd::Items(sizes, items_tx) => {
|
||||||
|
items_tx.send(items(&monitor, sizes)).await.unwrap();
|
||||||
|
}
|
||||||
Cmd::Rescan => {
|
Cmd::Rescan => {
|
||||||
let mut items = MounterItems::new();
|
event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap();
|
||||||
for (i, mount) in monitor.mounts().into_iter().enumerate() {
|
|
||||||
items.push(MounterItem::Gvfs(Item {
|
|
||||||
kind: ItemKind::Mount,
|
|
||||||
index: i,
|
|
||||||
name: MountExt::name(&mount).to_string(),
|
|
||||||
is_mounted: true,
|
|
||||||
icon_opt: gio_icon_to_path(
|
|
||||||
&MountExt::symbolic_icon(&mount),
|
|
||||||
16,
|
|
||||||
),
|
|
||||||
path_opt: MountExt::root(&mount).path(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
for (i, volume) in monitor.volumes().into_iter().enumerate() {
|
|
||||||
if volume.get_mount().is_some() {
|
|
||||||
// Volumes with mounts are already listed by mount
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
items.push(MounterItem::Gvfs(Item {
|
|
||||||
kind: ItemKind::Volume,
|
|
||||||
index: i,
|
|
||||||
name: VolumeExt::name(&volume).to_string(),
|
|
||||||
is_mounted: false,
|
|
||||||
icon_opt: gio_icon_to_path(
|
|
||||||
&VolumeExt::symbolic_icon(&volume),
|
|
||||||
16,
|
|
||||||
),
|
|
||||||
path_opt: None,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
event_tx.send(Event::Items(items)).unwrap();
|
|
||||||
}
|
}
|
||||||
Cmd::Mount(mounter_item) => {
|
Cmd::Mount(mounter_item) => {
|
||||||
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
||||||
|
|
@ -437,6 +445,12 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mounter for Gvfs {
|
impl Mounter for Gvfs {
|
||||||
|
fn items(&self, sizes: IconSizes) -> Option<MounterItems> {
|
||||||
|
let (items_tx, mut items_rx) = mpsc::channel(1);
|
||||||
|
self.command_tx.send(Cmd::Items(sizes, items_tx)).unwrap();
|
||||||
|
items_rx.blocking_recv()
|
||||||
|
}
|
||||||
|
|
||||||
fn mount(&self, item: MounterItem) -> Command<()> {
|
fn mount(&self, item: MounterItem) -> Command<()> {
|
||||||
let command_tx = self.command_tx.clone();
|
let command_tx = self.command_tx.clone();
|
||||||
Command::perform(
|
Command::perform(
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,10 @@ impl MounterItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(&self) -> Option<widget::icon::Handle> {
|
pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
Self::Gvfs(item) => item.icon(),
|
Self::Gvfs(item) => item.icon(symbolic),
|
||||||
Self::None => unreachable!(),
|
Self::None => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +89,7 @@ pub enum MounterMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Mounter: Send + Sync {
|
pub trait Mounter: Send + Sync {
|
||||||
|
fn items(&self, sizes: IconSizes) -> Option<MounterItems>;
|
||||||
//TODO: send result
|
//TODO: send result
|
||||||
fn mount(&self, item: MounterItem) -> Command<()>;
|
fn mount(&self, item: MounterItem) -> Command<()>;
|
||||||
fn network_drive(&self, uri: String) -> Command<()>;
|
fn network_drive(&self, uri: String) -> Command<()>;
|
||||||
|
|
|
||||||
170
src/tab.rs
170
src/tab.rs
|
|
@ -57,7 +57,7 @@ use std::{
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{self, Action, PreviewItem, PreviewKind},
|
app::{self, Action, PreviewItem, PreviewKind},
|
||||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
||||||
config::{IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID},
|
config::{DesktopConfig, IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID},
|
||||||
dialog::DialogKind,
|
dialog::DialogKind,
|
||||||
fl,
|
fl,
|
||||||
localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER},
|
localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER},
|
||||||
|
|
@ -184,15 +184,31 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han
|
||||||
.handle()
|
.handle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn trash_entries() -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub fn trash_entries() -> usize {
|
||||||
|
match trash::os_limited::list() {
|
||||||
|
Ok(entries) => entries.len(),
|
||||||
|
Err(_err) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
|
||||||
|
widget::icon::from_name(if trash_entries() > 0 {
|
||||||
|
"user-trash-full"
|
||||||
|
} else {
|
||||||
|
"user-trash"
|
||||||
|
})
|
||||||
|
.size(icon_size)
|
||||||
|
.handle()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
||||||
#[cfg(target_os = "macos")]
|
widget::icon::from_name(if trash_entries() > 0 {
|
||||||
let full = false; // TODO: add support for macos
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let full = match trash::os_limited::list() {
|
|
||||||
Ok(entries) => !entries.is_empty(),
|
|
||||||
Err(_err) => false,
|
|
||||||
};
|
|
||||||
widget::icon::from_name(if full {
|
|
||||||
"user-trash-full-symbolic"
|
"user-trash-full-symbolic"
|
||||||
} else {
|
} else {
|
||||||
"user-trash-symbolic"
|
"user-trash-symbolic"
|
||||||
|
|
@ -783,23 +799,111 @@ pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec<Item
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: organize desktop items based on display
|
||||||
|
pub fn scan_desktop(
|
||||||
|
tab_path: &PathBuf,
|
||||||
|
display: &str,
|
||||||
|
desktop_config: DesktopConfig,
|
||||||
|
mounters: Mounters,
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> Vec<Item> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
if desktop_config.show_content {
|
||||||
|
items.extend(scan_path(tab_path, sizes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if desktop_config.show_mounted_drives {
|
||||||
|
for (_mounter_key, mounter) in mounters.iter() {
|
||||||
|
for mounter_item in mounter.items(sizes).unwrap_or_default() {
|
||||||
|
let Some(path) = mounter_item.path() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get most item data from path
|
||||||
|
let mut item = match item_from_path(&path, sizes) {
|
||||||
|
Ok(item) => item,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to get item from mounter item {:?}: {}", path, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Override some data with mounter information
|
||||||
|
item.name = mounter_item.name();
|
||||||
|
item.display_name = Item::display_name(&item.name);
|
||||||
|
|
||||||
|
//TODO: use icon size for mounter item icon
|
||||||
|
if let Some(icon) = mounter_item.icon(false) {
|
||||||
|
item.icon_handle_grid = icon.clone();
|
||||||
|
item.icon_handle_list = icon.clone();
|
||||||
|
item.icon_handle_list_condensed = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if desktop_config.show_trash {
|
||||||
|
let name = fl!("trash");
|
||||||
|
let display_name = Item::display_name(&name);
|
||||||
|
|
||||||
|
let metadata = ItemMetadata::SimpleDir {
|
||||||
|
entries: trash_entries() as u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
||||||
|
(
|
||||||
|
"inode/directory".parse().unwrap(),
|
||||||
|
trash_icon(sizes.grid()),
|
||||||
|
trash_icon(sizes.list()),
|
||||||
|
trash_icon(sizes.list_condensed()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
items.push(Item {
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
metadata,
|
||||||
|
hidden: false,
|
||||||
|
location_opt: Some(Location::Trash),
|
||||||
|
mime,
|
||||||
|
icon_handle_grid,
|
||||||
|
icon_handle_list,
|
||||||
|
icon_handle_list_condensed,
|
||||||
|
open_with: Vec::new(),
|
||||||
|
thumbnail_opt: Some(ItemThumbnail::NotImage),
|
||||||
|
button_id: widget::Id::unique(),
|
||||||
|
pos_opt: Cell::new(None),
|
||||||
|
rect_opt: Cell::new(None),
|
||||||
|
selected: false,
|
||||||
|
overlaps_drag_rect: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Location {
|
pub enum Location {
|
||||||
|
Desktop(PathBuf, String),
|
||||||
|
Network(String, String),
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
|
Recents,
|
||||||
Search(PathBuf, String),
|
Search(PathBuf, String),
|
||||||
Trash,
|
Trash,
|
||||||
Recents,
|
|
||||||
Network(String, String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Location {
|
impl std::fmt::Display for Location {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Desktop(path, display) => write!(f, "{} on display {display}", path.display()),
|
||||||
|
Self::Network(uri, _) => write!(f, "{}", uri),
|
||||||
Self::Path(path) => write!(f, "{}", path.display()),
|
Self::Path(path) => write!(f, "{}", path.display()),
|
||||||
|
Self::Recents => write!(f, "recents"),
|
||||||
Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term),
|
Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term),
|
||||||
Self::Trash => write!(f, "trash"),
|
Self::Trash => write!(f, "trash"),
|
||||||
Self::Recents => write!(f, "recents"),
|
|
||||||
Self::Network(uri, _) => write!(f, "{}", uri),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -807,14 +911,23 @@ impl std::fmt::Display for Location {
|
||||||
impl Location {
|
impl Location {
|
||||||
pub fn path_opt(&self) -> Option<&PathBuf> {
|
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Desktop(path, _display) => Some(&path),
|
||||||
Self::Path(path) => Some(&path),
|
Self::Path(path) => Some(&path),
|
||||||
Self::Search(path, _) => Some(&path),
|
Self::Search(path, _) => Some(&path),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan(&self, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan(
|
||||||
|
&self,
|
||||||
|
desktop_config: DesktopConfig,
|
||||||
|
mounters: Mounters,
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> Vec<Item> {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Desktop(path, display) => {
|
||||||
|
scan_desktop(path, display, desktop_config, mounters, sizes)
|
||||||
|
}
|
||||||
Self::Path(path) => scan_path(path, sizes),
|
Self::Path(path) => scan_path(path, sizes),
|
||||||
Self::Search(path, term) => scan_search(path, term, sizes),
|
Self::Search(path, term) => scan_search(path, term, sizes),
|
||||||
Self::Trash => scan_trash(sizes),
|
Self::Trash => scan_trash(sizes),
|
||||||
|
|
@ -836,6 +949,7 @@ pub enum Command {
|
||||||
OpenFile(PathBuf),
|
OpenFile(PathBuf),
|
||||||
OpenInNewTab(PathBuf),
|
OpenInNewTab(PathBuf),
|
||||||
OpenInNewWindow(PathBuf),
|
OpenInNewWindow(PathBuf),
|
||||||
|
OpenTrash,
|
||||||
Preview(PreviewKind),
|
Preview(PreviewKind),
|
||||||
WindowDrag,
|
WindowDrag,
|
||||||
WindowToggleMaximize,
|
WindowToggleMaximize,
|
||||||
|
|
@ -1079,7 +1193,7 @@ impl Item {
|
||||||
{
|
{
|
||||||
ItemThumbnail::NotImage => icon,
|
ItemThumbnail::NotImage => icon,
|
||||||
ItemThumbnail::Rgba(rgba, _) => {
|
ItemThumbnail::Rgba(rgba, _) => {
|
||||||
if let Some(Location::Path(path)) = &self.location_opt {
|
if let Some(path) = self.path_opt() {
|
||||||
if self.mime.type_() == mime::IMAGE {
|
if self.mime.type_() == mime::IMAGE {
|
||||||
return widget::image(widget::image::Handle::from_path(path)).into();
|
return widget::image(widget::image::Handle::from_path(path)).into();
|
||||||
}
|
}
|
||||||
|
|
@ -1430,6 +1544,10 @@ impl Tab {
|
||||||
|
|
||||||
pub fn title(&self) -> String {
|
pub fn title(&self) -> String {
|
||||||
match &self.location {
|
match &self.location {
|
||||||
|
Location::Desktop(path, _display) => {
|
||||||
|
let (name, _) = folder_name(path);
|
||||||
|
name
|
||||||
|
}
|
||||||
Location::Path(path) => {
|
Location::Path(path) => {
|
||||||
let (name, _) = folder_name(path);
|
let (name, _) = folder_name(path);
|
||||||
name
|
name
|
||||||
|
|
@ -1787,8 +1905,8 @@ impl Tab {
|
||||||
if clicked_item.metadata.is_dir() {
|
if clicked_item.metadata.is_dir() {
|
||||||
cd = Some(location.clone());
|
cd = Some(location.clone());
|
||||||
} else {
|
} else {
|
||||||
if let Location::Path(path) = location {
|
if let Some(path) = location.path_opt() {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
commands.push(Command::OpenFile(path.to_path_buf()));
|
||||||
} else {
|
} else {
|
||||||
log::warn!("no path for item {:?}", clicked_item);
|
log::warn!("no path for item {:?}", clicked_item);
|
||||||
}
|
}
|
||||||
|
|
@ -2275,8 +2393,9 @@ impl Tab {
|
||||||
//TODO: allow opening multiple tabs?
|
//TODO: allow opening multiple tabs?
|
||||||
cd = Some(location.clone());
|
cd = Some(location.clone());
|
||||||
} else {
|
} else {
|
||||||
if let Location::Path(path) = location {
|
if let Some(path) = location.path_opt() {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
commands
|
||||||
|
.push(Command::OpenFile(path.to_path_buf()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2316,7 +2435,7 @@ impl Tab {
|
||||||
if let Some(clicked_item) =
|
if let Some(clicked_item) =
|
||||||
self.items_opt.as_ref().and_then(|items| items.get(click_i))
|
self.items_opt.as_ref().and_then(|items| items.get(click_i))
|
||||||
{
|
{
|
||||||
if let Some(Location::Path(path)) = &clicked_item.location_opt {
|
if let Some(path) = clicked_item.path_opt() {
|
||||||
if clicked_item.metadata.is_dir() {
|
if clicked_item.metadata.is_dir() {
|
||||||
//cd = Some(Location::Path(path.clone()));
|
//cd = Some(Location::Path(path.clone()));
|
||||||
commands.push(Command::OpenInNewTab(path.clone()))
|
commands.push(Command::OpenInNewTab(path.clone()))
|
||||||
|
|
@ -2483,6 +2602,9 @@ impl Tab {
|
||||||
Location::Path(path) => {
|
Location::Path(path) => {
|
||||||
commands.push(Command::OpenFile(path));
|
commands.push(Command::OpenFile(path));
|
||||||
}
|
}
|
||||||
|
Location::Trash => {
|
||||||
|
commands.push(Command::OpenTrash);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if location != self.location {
|
} else if location != self.location {
|
||||||
|
|
@ -2491,8 +2613,8 @@ impl Tab {
|
||||||
Location::Search(path, _term) => path.is_dir(),
|
Location::Search(path, _term) => path.is_dir(),
|
||||||
_ => true,
|
_ => true,
|
||||||
} {
|
} {
|
||||||
let prev_path = if let Location::Path(path) = &self.location {
|
let prev_path = if let Some(path) = self.location.path_opt() {
|
||||||
Some(path.clone())
|
Some(path.to_path_buf())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -2968,7 +3090,7 @@ impl Tab {
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut children: Vec<Element<_>> = Vec::new();
|
||||||
match &self.location {
|
match &self.location {
|
||||||
Location::Path(path) | Location::Search(path, ..) => {
|
Location::Desktop(path, _) | Location::Path(path) | Location::Search(path, _) => {
|
||||||
let excess_str = "...";
|
let excess_str = "...";
|
||||||
let excess_width = text_width_body(excess_str);
|
let excess_width = text_width_body(excess_str);
|
||||||
for (index, ancestor) in path.ancestors().enumerate() {
|
for (index, ancestor) in path.ancestors().enumerate() {
|
||||||
|
|
@ -3974,7 +4096,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Location::Path(path)) = item.location_opt.clone() {
|
if let Some(path) = item.path_opt().map(|path| path.to_path_buf()) {
|
||||||
let mime = item.mime.clone();
|
let mime = item.mime.clone();
|
||||||
subscriptions.push(subscription::channel(
|
subscriptions.push(subscription::channel(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue