Select result of operation, fixes #500

This commit is contained in:
Jeremy Soller 2024-11-19 20:17:58 -07:00
parent 4ba7d7bbfc
commit 24a7f2bc31
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
5 changed files with 215 additions and 139 deletions

View file

@ -64,7 +64,7 @@ use crate::{
localize::LANGUAGE_SORTER, localize::LANGUAGE_SORTER,
menu, mime_app, mime_icon, menu, mime_app, mime_icon,
mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS}, mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
operation::{Controller, Operation, ReplaceResult}, operation::{Controller, Operation, OperationSelection, ReplaceResult},
spawn_detached::spawn_detached, spawn_detached::spawn_detached,
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
}; };
@ -308,7 +308,7 @@ pub enum Message {
PasteContents(PathBuf, ClipboardPaste), PasteContents(PathBuf, ClipboardPaste),
PendingCancel(u64), PendingCancel(u64),
PendingCancelAll, PendingCancelAll,
PendingComplete(u64), PendingComplete(u64, OperationSelection),
PendingDismiss, PendingDismiss,
PendingError(u64, String), PendingError(u64, String),
PendingPause(u64, bool), PendingPause(u64, bool),
@ -335,7 +335,7 @@ pub enum Message {
Location, Location,
Option<tab::Item>, Option<tab::Item>,
Vec<tab::Item>, Vec<tab::Item>,
Option<PathBuf>, Option<Vec<PathBuf>>,
), ),
TabView(Option<Entity>, tab::View), TabView(Option<Entity>, tab::View),
ToggleContextPage(ContextPage), ToggleContextPage(ContextPage),
@ -655,7 +655,7 @@ impl App {
&mut self, &mut self,
location: Location, location: Location,
activate: bool, activate: bool,
selection_path: Option<PathBuf>, selection_paths: Option<Vec<PathBuf>>,
) -> (Entity, Task<Message>) { ) -> (Entity, Task<Message>) {
let mut tab = Tab::new(location.clone(), self.config.tab); let mut tab = Tab::new(location.clone(), self.config.tab);
tab.mode = match self.mode { tab.mode = match self.mode {
@ -683,7 +683,7 @@ impl App {
Task::batch([ Task::batch([
self.update_title(), self.update_title(),
self.update_watcher(), self.update_watcher(),
self.rescan_tab(entity, location, selection_path), self.rescan_tab(entity, location, selection_paths),
]), ]),
) )
} }
@ -692,9 +692,9 @@ impl App {
&mut self, &mut self,
location: Location, location: Location,
activate: bool, activate: bool,
selection_path: Option<PathBuf>, selection_paths: Option<Vec<PathBuf>>,
) -> Task<Message> { ) -> Task<Message> {
self.open_tab_entity(location, activate, selection_path).1 self.open_tab_entity(location, activate, selection_paths).1
} }
fn operation(&mut self, operation: Operation) { fn operation(&mut self, operation: Operation) {
@ -717,13 +717,38 @@ impl App {
} }
} }
fn rescan_operation_selection(&mut self, op_sel: OperationSelection) -> Task<Message> {
log::info!("rescan_operation_selection {:?}", op_sel);
let entity = self.tab_model.active();
let Some(tab) = self.tab_model.data::<Tab>(entity) else {
return Task::none();
};
let Some(ref items) = tab.items_opt() else {
return Task::none();
};
for item in items.iter() {
if item.selected {
if let Some(path) = item.path_opt() {
if op_sel.selected.contains(path) || op_sel.ignored.contains(path) {
// Ignore if path in selected or ignored paths
continue;
}
}
// Return if there is a previous selection not matching
return Task::none();
}
}
self.rescan_tab(entity, tab.location.clone(), Some(op_sel.selected))
}
fn rescan_tab( fn rescan_tab(
&mut self, &mut self,
entity: Entity, entity: Entity,
location: Location, location: Location,
selection_path: Option<PathBuf>, selection_paths: Option<Vec<PathBuf>>,
) -> Task<Message> { ) -> Task<Message> {
log::info!("rescan_tab {entity:?} {location:?} {selection_path:?}"); log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}");
let icon_sizes = self.config.tab.icon_sizes; let icon_sizes = self.config.tab.icon_sizes;
Task::perform( Task::perform(
async move { async move {
@ -734,7 +759,7 @@ impl App {
location, location,
parent_item_opt, parent_item_opt,
items, items,
selection_path, selection_paths,
)), )),
Err(err) => { Err(err) => {
log::warn!("failed to rescan: {}", err); log::warn!("failed to rescan: {}", err);
@ -2264,7 +2289,7 @@ impl Application for App {
Some(self.open_tab( Some(self.open_tab(
Location::Path(parent.to_path_buf()), Location::Path(parent.to_path_buf()),
true, true,
Some(path), Some(vec![path]),
)) ))
} else { } else {
None None
@ -2381,9 +2406,9 @@ impl Application for App {
self.progress_operations.remove(&id); self.progress_operations.remove(&id);
} }
} }
Message::PendingComplete(id) => { Message::PendingComplete(id, op_sel) => {
let mut commands = Vec::with_capacity(3); let mut commands = Vec::with_capacity(4);
// Show toast for some operations
if let Some((op, _, _)) = self.pending_operations.remove(&id) { if let Some((op, _, _)) = self.pending_operations.remove(&id) {
if let Some(description) = op.toast() { if let Some(description) = op.toast() {
if let Operation::Delete { ref paths } = op { if let Operation::Delete { ref paths } = op {
@ -2412,6 +2437,8 @@ impl Application for App {
} }
// Potentially show a notification // Potentially show a notification
commands.push(self.update_notification()); commands.push(self.update_notification());
// Rescan and select based on operation
commands.push(self.rescan_operation_selection(op_sel));
// Manually rescan any trash tabs after any operation is completed // Manually rescan any trash tabs after any operation is completed
commands.push(self.rescan_trash()); commands.push(self.rescan_trash());
// if search is active, update "search" tab view // if search is active, update "search" tab view
@ -2579,7 +2606,7 @@ impl Application for App {
} }
} }
Message::RestoreFromTrash(entity_opt) => { Message::RestoreFromTrash(entity_opt) => {
let mut paths = Vec::new(); let mut trash_items = 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 Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
@ -2587,7 +2614,7 @@ impl Application for App {
if item.selected { if item.selected {
match &item.metadata { match &item.metadata {
ItemMetadata::Trash { entry, .. } => { ItemMetadata::Trash { entry, .. } => {
paths.push(entry.clone()); trash_items.push(entry.clone());
} }
_ => { _ => {
//TODO: error on trying to restore non-trash file? //TODO: error on trying to restore non-trash file?
@ -2597,8 +2624,8 @@ impl Application for App {
} }
} }
} }
if !paths.is_empty() { if !trash_items.is_empty() {
self.operation(Operation::Restore { paths }); self.operation(Operation::Restore { items: trash_items });
} }
} }
Message::SearchActivate => { Message::SearchActivate => {
@ -2735,14 +2762,14 @@ impl Application for App {
config_set!(favorites, favorites); config_set!(favorites, favorites);
commands.push(self.update_config()); commands.push(self.update_config());
} }
tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => { tab::Command::ChangeLocation(tab_title, tab_path, selection_paths) => {
self.activate_nav_model_location(&tab_path); self.activate_nav_model_location(&tab_path);
self.tab_model.text_set(entity, tab_title); self.tab_model.text_set(entity, tab_title);
commands.push(Task::batch([ commands.push(Task::batch([
self.update_title(), self.update_title(),
self.update_watcher(), self.update_watcher(),
self.rescan_tab(entity, tab_path, selection_path), self.rescan_tab(entity, tab_path, selection_paths),
])); ]));
} }
tab::Command::DropFiles(to, from) => { tab::Command::DropFiles(to, from) => {
@ -2816,14 +2843,14 @@ impl Application for App {
}; };
return self.open_tab(location, true, None); return self.open_tab(location, true, None);
} }
Message::TabRescan(entity, location, parent_item_opt, items, selection_path) => { Message::TabRescan(entity, location, parent_item_opt, items, selection_paths) => {
match self.tab_model.data_mut::<Tab>(entity) { match self.tab_model.data_mut::<Tab>(entity) {
Some(tab) => { Some(tab) => {
if location == tab.location { if location == tab.location {
tab.parent_item_opt = parent_item_opt; tab.parent_item_opt = parent_item_opt;
tab.set_items(items); tab.set_items(items);
if let Some(selection_path) = selection_path { if let Some(selection_paths) = selection_paths {
tab.select_path(selection_path); tab.select_paths(selection_paths);
} }
} }
} }
@ -2882,8 +2909,8 @@ impl Application for App {
Message::UndoTrashStart(paths) Message::UndoTrashStart(paths)
}); });
} }
Message::UndoTrashStart(paths) => { Message::UndoTrashStart(items) => {
self.operation(Operation::Restore { paths }); self.operation(Operation::Restore { items });
} }
Message::WindowClose => { Message::WindowClose => {
if let Some(window_id) = self.window_id_opt.take() { if let Some(window_id) = self.window_id_opt.take() {
@ -4420,8 +4447,12 @@ impl Application for App {
stream::channel(16, move |msg_tx| async move { stream::channel(16, move |msg_tx| async move {
let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx));
match pending_operation.perform(id, &msg_tx, cancelled).await { match pending_operation.perform(id, &msg_tx, cancelled).await {
Ok(()) => { Ok(result_paths) => {
let _ = msg_tx.lock().await.send(Message::PendingComplete(id)).await; let _ = msg_tx
.lock()
.await
.send(Message::PendingComplete(id, result_paths))
.await;
} }
Err(err) => { Err(err) => {
let _ = msg_tx let _ = msg_tx

View file

@ -1381,7 +1381,7 @@ impl Application for App {
tab::Command::Action(action) => { tab::Command::Action(action) => {
commands.push(self.update(Message::from(action.message()))); commands.push(self.update(Message::from(action.message())));
} }
tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_path) => { tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_paths) => {
commands.push(Task::batch([self.update_watcher(), self.rescan_tab()])); commands.push(Task::batch([self.update_watcher(), self.rescan_tab()]));
} }
tab::Command::Iced(iced_command) => { tab::Command::Iced(iced_command) => {

View file

@ -324,55 +324,6 @@ pub enum ReplaceResult {
Cancel, Cancel,
} }
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Operation {
/// Compress files
Compress {
paths: Vec<PathBuf>,
to: PathBuf,
archive_type: ArchiveType,
},
/// Copy items
Copy {
paths: Vec<PathBuf>,
to: PathBuf,
},
/// Move items to the trash
Delete {
paths: Vec<PathBuf>,
},
/// Empty the trash
EmptyTrash,
/// Uncompress files
Extract {
paths: Vec<PathBuf>,
to: PathBuf,
},
/// Move items
Move {
paths: Vec<PathBuf>,
to: PathBuf,
},
NewFile {
path: PathBuf,
},
NewFolder {
path: PathBuf,
},
Rename {
from: PathBuf,
to: PathBuf,
},
/// Restore a path from the trash
Restore {
paths: Vec<trash::TrashItem>,
},
/// Set executable and launch
SetExecutableAndLaunch {
path: PathBuf,
},
}
async fn copy_or_move( async fn copy_or_move(
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
to: PathBuf, to: PathBuf,
@ -380,9 +331,9 @@ async fn copy_or_move(
id: u64, id: u64,
msg_tx: &Arc<TokioMutex<Sender<Message>>>, msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller, controller: Controller,
) -> Result<(), String> { ) -> Result<OperationSelection, String> {
let msg_tx = msg_tx.clone(); let msg_tx = msg_tx.clone();
tokio::task::spawn_blocking(move || -> Result<(), String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> {
log::info!( log::info!(
"{} {:?} to {:?}", "{} {:?} to {:?}",
if moving { "Move" } else { "Copy" }, if moving { "Move" } else { "Copy" },
@ -446,7 +397,7 @@ async fn copy_or_move(
context.recursive_copy_or_move(from_to_pairs, moving)?; context.recursive_copy_or_move(from_to_pairs, moving)?;
Ok(()) Ok(context.op_sel)
}) })
.await .await
.map_err(err_str)? .map_err(err_str)?
@ -553,6 +504,63 @@ fn paths_parent_name<'a>(paths: &'a Vec<PathBuf>) -> Cow<'a, str> {
file_name(parent) file_name(parent)
} }
#[derive(Clone, Debug, Default)]
pub struct OperationSelection {
// Paths to ignore if they are already selected
pub ignored: Vec<PathBuf>,
// Paths to select
pub selected: Vec<PathBuf>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Operation {
/// Compress files
Compress {
paths: Vec<PathBuf>,
to: PathBuf,
archive_type: ArchiveType,
},
/// Copy items
Copy {
paths: Vec<PathBuf>,
to: PathBuf,
},
/// Move items to the trash
Delete {
paths: Vec<PathBuf>,
},
/// Empty the trash
EmptyTrash,
/// Uncompress files
Extract {
paths: Vec<PathBuf>,
to: PathBuf,
},
/// Move items
Move {
paths: Vec<PathBuf>,
to: PathBuf,
},
NewFile {
path: PathBuf,
},
NewFolder {
path: PathBuf,
},
Rename {
from: PathBuf,
to: PathBuf,
},
/// Restore a path from the trash
Restore {
items: Vec<trash::TrashItem>,
},
/// Set executable and launch
SetExecutableAndLaunch {
path: PathBuf,
},
}
impl Operation { impl Operation {
pub fn pending_text(&self, percent: i32, state: ControllerState) -> String { pub fn pending_text(&self, percent: i32, state: ControllerState) -> String {
let progress = || match state { let progress = || match state {
@ -610,7 +618,7 @@ impl Operation {
Self::Rename { from, to } => { Self::Rename { from, to } => {
fl!("renaming", from = file_name(from), to = file_name(to)) fl!("renaming", from = file_name(from), to = file_name(to))
} }
Self::Restore { paths } => fl!("restoring", items = paths.len(), progress = progress()), Self::Restore { items } => fl!("restoring", items = items.len(), progress = progress()),
Self::SetExecutableAndLaunch { path } => { Self::SetExecutableAndLaunch { path } => {
fl!("setting-executable-and-launching", name = file_name(path)) fl!("setting-executable-and-launching", name = file_name(path))
} }
@ -661,7 +669,7 @@ impl Operation {
parent = parent_name(path) parent = parent_name(path)
), ),
Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)), Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)),
Self::Restore { paths } => fl!("restored", items = paths.len()), Self::Restore { items } => fl!("restored", items = items.len()),
Self::SetExecutableAndLaunch { path } => { Self::SetExecutableAndLaunch { path } => {
fl!("set-executable-and-launched", name = file_name(path)) fl!("set-executable-and-launched", name = file_name(path))
} }
@ -701,7 +709,7 @@ impl Operation {
id: u64, id: u64,
msg_tx: &Arc<TokioMutex<Sender<Message>>>, msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller, controller: Controller,
) -> Result<(), String> { ) -> Result<OperationSelection, String> {
let _ = msg_tx let _ = msg_tx
.lock() .lock()
.await .await
@ -709,18 +717,23 @@ impl Operation {
.await; .await;
//TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE //TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE
match self { let paths = match self {
Self::Compress { Self::Compress {
paths, paths,
to, to,
archive_type, archive_type,
} => { } => {
let msg_tx = msg_tx.clone(); let msg_tx = msg_tx.clone();
tokio::task::spawn_blocking(move || -> Result<(), String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> {
let Some(relative_root) = to.parent() else { let Some(relative_root) = to.parent() else {
return Err(format!("path {:?} has no parent directory", to)); return Err(format!("path {:?} has no parent directory", to));
}; };
let op_sel = OperationSelection {
ignored: paths.clone(),
selected: vec![to.clone()],
};
let mut paths = paths; let mut paths = paths;
for path in paths.clone().iter() { for path in paths.clone().iter() {
if path.is_dir() { if path.is_dir() {
@ -844,14 +857,14 @@ impl Operation {
} }
} }
Ok(()) Ok(op_sel)
}) })
.await .await
.map_err(err_str)? .map_err(err_str)?
.map_err(err_str)?; .map_err(err_str)?
} }
Self::Copy { paths, to } => { Self::Copy { paths, to } => {
copy_or_move(paths, to, false, id, msg_tx, controller).await?; copy_or_move(paths, to, false, id, msg_tx, controller).await?
} }
Self::Delete { paths } => { Self::Delete { paths } => {
let total = paths.len(); let total = paths.len();
@ -873,6 +886,7 @@ impl Operation {
.map_err(err_str)?; .map_err(err_str)?;
//TODO: items_opt allows for easy restore //TODO: items_opt allows for easy restore
} }
OperationSelection::default()
} }
Self::EmptyTrash => { Self::EmptyTrash => {
#[cfg(any( #[cfg(any(
@ -908,11 +922,13 @@ impl Operation {
.await .await
.map_err(err_str)??; .map_err(err_str)??;
} }
OperationSelection::default()
} }
Self::Extract { paths, to } => { Self::Extract { paths, to } => {
let msg_tx = msg_tx.clone(); let msg_tx = msg_tx.clone();
tokio::task::spawn_blocking(move || -> Result<(), String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> {
let total_paths = paths.len(); let total_paths = paths.len();
let mut op_sel = OperationSelection::default();
for (i, path) in paths.iter().enumerate() { for (i, path) in paths.iter().enumerate() {
controller.check()?; controller.check()?;
@ -937,6 +953,9 @@ impl Operation {
} }
} }
op_sel.ignored.push(path.clone());
op_sel.selected.push(new_dir.clone());
let msg_tx = msg_tx.clone(); let msg_tx = msg_tx.clone();
let controller = controller.clone(); let controller = controller.clone();
let mime = mime_for_path(&path); let mime = mime_for_path(&path);
@ -968,7 +987,7 @@ impl Operation {
.map(io::BufReader::new) .map(io::BufReader::new)
.map(bzip2::read::BzDecoder::new) .map(bzip2::read::BzDecoder::new)
.map(tar::Archive::new) .map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir)) .and_then(|mut archive| archive.unpack(&new_dir))
.map_err(err_str)? .map_err(err_str)?
} }
#[cfg(feature = "liblzma")] #[cfg(feature = "liblzma")]
@ -977,7 +996,7 @@ impl Operation {
.map(io::BufReader::new) .map(io::BufReader::new)
.map(liblzma::read::XzDecoder::new) .map(liblzma::read::XzDecoder::new)
.map(tar::Archive::new) .map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir)) .and_then(|mut archive| archive.unpack(&new_dir))
.map_err(err_str)? .map_err(err_str)?
} }
_ => Err(format!("unsupported mime type {:?}", mime))?, _ => Err(format!("unsupported mime type {:?}", mime))?,
@ -985,38 +1004,50 @@ impl Operation {
} }
} }
Ok(()) Ok(op_sel)
}) })
.await .await
.map_err(err_str)? .map_err(err_str)?
.map_err(err_str)?; .map_err(err_str)?
} }
Self::Move { paths, to } => { Self::Move { paths, to } => {
copy_or_move(paths, to, true, id, msg_tx, controller).await?; copy_or_move(paths, to, true, id, msg_tx, controller).await?
} }
Self::NewFolder { path } => { Self::NewFolder { path } => {
controller.check()?; tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> {
controller.check()?;
tokio::task::spawn_blocking(|| fs::create_dir(path)) fs::create_dir(&path).map_err(err_str)?;
.await Ok(OperationSelection {
.map_err(err_str)? ignored: Vec::new(),
.map_err(err_str)?; selected: vec![path],
})
})
.await
.map_err(err_str)??
} }
Self::NewFile { path } => { Self::NewFile { path } => {
controller.check()?; tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> {
controller.check()?;
tokio::task::spawn_blocking(|| fs::File::create(path)) fs::File::create(&path).map_err(err_str)?;
.await Ok(OperationSelection {
.map_err(err_str)? ignored: Vec::new(),
.map_err(err_str)?; selected: vec![path],
})
})
.await
.map_err(err_str)??
} }
Self::Rename { from, to } => { Self::Rename { from, to } => {
controller.check()?; tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> {
controller.check()?;
tokio::task::spawn_blocking(|| fs::rename(from, to)) fs::rename(&from, &to).map_err(err_str)?;
.await Ok(OperationSelection {
.map_err(err_str)? ignored: vec![from],
.map_err(err_str)?; selected: vec![to],
})
})
.await
.map_err(err_str)??
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Self::Restore { .. } => { Self::Restore { .. } => {
@ -1024,9 +1055,10 @@ impl Operation {
return Err("Restoring from trash is not supported on macos".to_string()); return Err("Restoring from trash is not supported on macos".to_string());
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
Self::Restore { paths } => { Self::Restore { items } => {
let total = paths.len(); let total = items.len();
for (i, path) in paths.into_iter().enumerate() { let mut paths = Vec::with_capacity(total);
for (i, item) in items.into_iter().enumerate() {
controller.check()?; controller.check()?;
let _ = msg_tx let _ = msg_tx
@ -1038,11 +1070,17 @@ impl Operation {
)) ))
.await; .await;
tokio::task::spawn_blocking(|| trash::os_limited::restore_all([path])) paths.push(item.original_path());
tokio::task::spawn_blocking(|| trash::os_limited::restore_all([item]))
.await .await
.map_err(err_str)? .map_err(err_str)?
.map_err(err_str)?; .map_err(err_str)?;
} }
OperationSelection {
ignored: Vec::new(),
selected: paths,
}
} }
Self::SetExecutableAndLaunch { path } => { Self::SetExecutableAndLaunch { path } => {
tokio::task::spawn_blocking(move || -> Result<(), String> { tokio::task::spawn_blocking(move || -> Result<(), String> {
@ -1070,8 +1108,9 @@ impl Operation {
.await .await
.map_err(err_str)? .map_err(err_str)?
.map_err(err_str)?; .map_err(err_str)?;
OperationSelection::default()
} }
} };
let _ = msg_tx let _ = msg_tx
.lock() .lock()
@ -1079,7 +1118,7 @@ impl Operation {
.send(Message::PendingProgress(id, 100.0)) .send(Message::PendingProgress(id, 100.0))
.await; .await;
Ok(()) Ok(paths)
} }
} }
@ -1112,7 +1151,10 @@ mod tests {
const BUF_SIZE: usize = 8; const BUF_SIZE: usize = 8;
/// Simple wrapper around `[Operation::Copy]` /// Simple wrapper around `[Operation::Copy]`
pub async fn operation_copy(paths: Vec<PathBuf>, to: PathBuf) -> Result<(), String> { pub async fn operation_copy(
paths: Vec<PathBuf>,
to: PathBuf,
) -> Result<OperationSelection, String> {
let id = fastrand::u64(0..u64::MAX); let id = fastrand::u64(0..u64::MAX);
let (tx, mut rx) = mpsc::channel(BUF_SIZE); let (tx, mut rx) = mpsc::channel(BUF_SIZE);
let paths_clone = paths.clone(); let paths_clone = paths.clone();

View file

@ -7,13 +7,14 @@ use std::{
}; };
use walkdir::WalkDir; use walkdir::WalkDir;
use super::{copy_unique_path, Controller, ReplaceResult}; use super::{copy_unique_path, Controller, OperationSelection, ReplaceResult};
pub struct Context { pub struct Context {
buf: Vec<u8>, buf: Vec<u8>,
controller: Controller, controller: Controller,
on_progress: Box<dyn Fn(&Op, &Progress) + 'static>, on_progress: Box<dyn Fn(&Op, &Progress) + 'static>,
on_replace: Box<dyn Fn(&Op) -> ReplaceResult + 'static>, on_replace: Box<dyn Fn(&Op) -> ReplaceResult + 'static>,
pub(crate) op_sel: OperationSelection,
replace_result_opt: Option<ReplaceResult>, replace_result_opt: Option<ReplaceResult>,
} }
@ -24,6 +25,7 @@ impl Context {
controller, controller,
on_progress: Box::new(|_op, _progress| {}), on_progress: Box::new(|_op, _progress| {}),
on_replace: Box::new(|_op| ReplaceResult::Cancel), on_replace: Box::new(|_op| ReplaceResult::Cancel),
op_sel: OperationSelection::default(),
replace_result_opt: None, replace_result_opt: None,
} }
} }
@ -88,6 +90,8 @@ impl Context {
} }
ops.push(op); ops.push(op);
} }
self.op_sel.ignored.push(from_parent);
} }
// Add cleanup ops after standard ops, in reverse // Add cleanup ops after standard ops, in reverse
@ -106,12 +110,19 @@ impl Context {
total_bytes: None, total_bytes: None,
}; };
(self.on_progress)(&op, &progress); (self.on_progress)(&op, &progress);
if !op.run(self, progress).map_err(|err| { if op.run(self, progress).map_err(|err| {
format!( format!(
"failed to {:?} {:?} to {:?}: {}", "failed to {:?} {:?} to {:?}: {}",
op.kind, op.from, op.to, err op.kind, op.from, op.to, err
) )
})? { })? {
// The from path is ignored in the operation selection if it is a top level item
if self.op_sel.ignored.contains(&op.from) {
// So add the to path to the selection
self.op_sel.selected.push(op.to.clone());
}
} else {
// Cancelled
return Ok(false); return Ok(false);
} }
} }

View file

@ -43,7 +43,7 @@ use mime_guess::{mime, Mime};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
cell::{Cell, RefCell}, cell::Cell,
cmp::Ordering, cmp::Ordering,
collections::HashMap, collections::HashMap,
fmt::{self, Display}, fmt::{self, Display},
@ -1015,7 +1015,7 @@ pub enum Command {
Action(Action), Action(Action),
AddNetworkDrive, AddNetworkDrive,
AddToSidebar(PathBuf), AddToSidebar(PathBuf),
ChangeLocation(String, Location, Option<PathBuf>), ChangeLocation(String, Location, Option<Vec<PathBuf>>),
DropFiles(PathBuf, ClipboardPaste), DropFiles(PathBuf, ClipboardPaste),
EmptyTrash, EmptyTrash,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
@ -1663,7 +1663,6 @@ pub struct Tab {
scrollable_id: widget::Id, scrollable_id: widget::Id,
select_focus: Option<usize>, select_focus: Option<usize>,
select_range: Option<(usize, usize)>, select_range: Option<(usize, usize)>,
cached_selected: RefCell<Option<bool>>,
clicked: Option<usize>, clicked: Option<usize>,
selected_clicked: bool, selected_clicked: bool,
last_right_click: Option<usize>, last_right_click: Option<usize>,
@ -1751,7 +1750,6 @@ impl Tab {
scrollable_id: widget::Id::unique(), scrollable_id: widget::Id::unique(),
select_focus: None, select_focus: None,
select_range: None, select_range: None,
cached_selected: RefCell::new(None),
clicked: None, clicked: None,
dnd_hovered: None, dnd_hovered: None,
selected_clicked: false, selected_clicked: false,
@ -1821,7 +1819,6 @@ impl Tab {
} }
pub fn select_all(&mut self) { pub fn select_all(&mut self) {
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() { for item in items.iter_mut() {
if !self.config.show_hidden && item.hidden { if !self.config.show_hidden && item.hidden {
@ -1834,7 +1831,6 @@ impl Tab {
} }
pub fn select_none(&mut self) -> bool { pub fn select_none(&mut self) -> bool {
*self.cached_selected.borrow_mut() = None;
self.select_focus = None; self.select_focus = None;
let mut had_selection = false; let mut had_selection = false;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
@ -1849,7 +1845,6 @@ impl Tab {
} }
pub fn select_name(&mut self, name: &str) { pub fn select_name(&mut self, name: &str) {
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() { for item in items.iter_mut() {
item.selected = item.name == name; item.selected = item.name == name;
@ -1857,18 +1852,20 @@ impl Tab {
} }
} }
pub fn select_path(&mut self, path: PathBuf) { pub fn select_paths(&mut self, paths: Vec<PathBuf>) {
let location = Location::Path(path);
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() { for item in items.iter_mut() {
item.selected = item.location_opt.as_ref() == Some(&location); item.selected = false;
if let Some(path) = item.path_opt() {
if paths.contains(path) {
item.selected = true;
}
}
} }
} }
} }
fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool { fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool {
*self.cached_selected.borrow_mut() = None;
let mut start = (row, col); let mut start = (row, col);
let mut end = (row, col); let mut end = (row, col);
if mod_shift { if mod_shift {
@ -1919,7 +1916,6 @@ impl Tab {
} }
pub fn select_rect(&mut self, rect: Rectangle, mod_ctrl: bool, mod_shift: bool) { pub fn select_rect(&mut self, rect: Rectangle, mod_ctrl: bool, mod_shift: bool) {
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() { for item in items.iter_mut() {
let was_overlapped = item.overlaps_drag_rect; let was_overlapped = item.overlaps_drag_rect;
@ -1996,7 +1992,6 @@ impl Tab {
} }
fn select_first_pos_opt(&self) -> Option<(usize, usize)> { fn select_first_pos_opt(&self) -> Option<(usize, usize)> {
*self.cached_selected.borrow_mut() = None;
let items = self.items_opt.as_ref()?; let items = self.items_opt.as_ref()?;
let mut first = None; let mut first = None;
for item in items.iter() { for item in items.iter() {
@ -2026,7 +2021,6 @@ impl Tab {
} }
fn select_last_pos_opt(&self) -> Option<(usize, usize)> { fn select_last_pos_opt(&self) -> Option<(usize, usize)> {
*self.cached_selected.borrow_mut() = None;
let items = self.items_opt.as_ref()?; let items = self.items_opt.as_ref()?;
let mut last = None; let mut last = None;
for item in items.iter() { for item in items.iter() {
@ -2231,7 +2225,6 @@ impl Tab {
l.iter() l.iter()
.any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected) .any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected)
}); });
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for (i, item) in items.iter_mut().enumerate() { for (i, item) in items.iter_mut().enumerate() {
if Some(i) == click_i_opt { if Some(i) == click_i_opt {
@ -2680,7 +2673,6 @@ impl Tab {
} }
Message::RightClick(click_i_opt) => { Message::RightClick(click_i_opt) => {
self.update(Message::Click(click_i_opt), modifiers); self.update(Message::Click(click_i_opt), modifiers);
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
if !click_i_opt.map_or(false, |click_i| { if !click_i_opt.map_or(false, |click_i| {
items.get(click_i).map_or(false, |x| x.selected) items.get(click_i).map_or(false, |x| x.selected)
@ -2978,7 +2970,7 @@ impl Tab {
} else if location != self.location { } else if location != self.location {
if location.path_opt().map_or(true, |path| path.is_dir()) { if location.path_opt().map_or(true, |path| path.is_dir()) {
let prev_path = if let Some(path) = self.location.path_opt() { let prev_path = if let Some(path) = self.location.path_opt() {
Some(path.to_path_buf()) Some(vec![path.to_path_buf()])
} else { } else {
None None
}; };