Merge remote-tracking branch 'upstream/master' into anonymous-fix

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk 2025-01-22 17:47:34 +01:00
commit 32c54d4375
20 changed files with 1420 additions and 786 deletions

View file

@ -125,6 +125,8 @@ pub enum Action {
Rename,
RestoreFromTrash,
SearchActivate,
SelectFirst,
SelectLast,
SelectAll,
SetSort(HeadingOptions, bool),
Settings,
@ -190,6 +192,8 @@ impl Action {
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
Action::SearchActivate => Message::SearchActivate,
Action::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll),
Action::SelectFirst => Message::TabMessage(entity_opt, tab::Message::SelectFirst),
Action::SelectLast => Message::TabMessage(entity_opt, tab::Message::SelectLast),
Action::SetSort(sort, dir) => {
Message::TabMessage(entity_opt, tab::Message::SetSort(*sort, *dir))
}
@ -352,6 +356,7 @@ pub enum Message {
WindowClose,
WindowCloseRequested(window::Id),
WindowNew,
WindowUnfocus,
ZoomDefault(Option<Entity>),
ZoomIn(Option<Entity>),
ZoomOut(Option<Entity>),
@ -772,7 +777,7 @@ impl App {
Task::batch([
self.update_title(),
self.update_watcher(),
self.rescan_tab(entity, location, selection_paths),
self.update_tab(entity, location, selection_paths),
]),
)
}
@ -793,7 +798,7 @@ impl App {
self.progress_operations.insert(id);
}
self.pending_operations
.insert(id, (operation, Controller::new()));
.insert(id, (operation, Controller::default()));
}
fn remove_window(&mut self, id: &window::Id) {
@ -828,7 +833,20 @@ impl App {
return Task::none();
}
}
self.rescan_tab(entity, tab.location.clone(), Some(op_sel.selected))
self.update_tab(entity, tab.location.clone(), Some(op_sel.selected))
}
fn update_tab(
&mut self,
entity: Entity,
location: Location,
selection_paths: Option<Vec<PathBuf>>,
) -> Task<Message> {
if let Location::Search(_, term, ..) = location {
self.search_set(entity, Some(term), selection_paths)
} else {
self.rescan_tab(entity, location, selection_paths)
}
}
fn rescan_tab(
@ -872,19 +890,11 @@ impl App {
let mut commands = Vec::with_capacity(needs_reload.len());
for (entity, location) in needs_reload {
commands.push(self.rescan_tab(entity, location, None));
commands.push(self.update_tab(entity, location, None));
}
Task::batch(commands)
}
fn search(&mut self) -> Task<Message> {
if let Some(term) = self.search_get() {
self.search_set_active(Some(term.to_string()))
} else {
Task::none()
}
}
fn search_get(&self) -> Option<&str> {
let entity = self.tab_model.active();
let tab = self.tab_model.data::<Tab>(entity)?;
@ -896,10 +906,15 @@ impl App {
fn search_set_active(&mut self, term_opt: Option<String>) -> Task<Message> {
let entity = self.tab_model.active();
self.search_set(entity, term_opt)
self.search_set(entity, term_opt, None)
}
fn search_set(&mut self, tab: Entity, term_opt: Option<String>) -> Task<Message> {
fn search_set(
&mut self,
tab: Entity,
term_opt: Option<String>,
selection_paths: Option<Vec<PathBuf>>,
) -> Task<Message> {
let mut title_location_opt = None;
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab) {
let location_opt = match term_opt {
@ -930,7 +945,7 @@ impl App {
return Task::batch([
self.update_title(),
self.update_watcher(),
self.rescan_tab(tab, location, None),
self.rescan_tab(tab, location, selection_paths),
if focus_search {
widget::text_input::focus(self.search_id.clone())
} else {
@ -989,7 +1004,7 @@ impl App {
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
tab.location = location.clone();
}
commands.push(self.rescan_tab(entity, location, None));
commands.push(self.update_tab(entity, location, None));
}
Task::batch(commands)
}
@ -1589,6 +1604,18 @@ impl Application for App {
let mut commands = vec![app.update_config()];
for location in flags.locations {
if let Some(path) = location.path_opt() {
if path.is_file() {
if let Some(parent) = path.parent() {
commands.push(app.open_tab(
Location::Path(parent.to_path_buf()),
true,
Some(vec![path.to_path_buf()]),
));
continue;
}
}
}
commands.push(app.open_tab(location, true, None));
}
@ -2053,14 +2080,15 @@ impl Application for App {
}
Message::ExtractHere(entity_opt) => {
let paths = self.selected_paths(entity_opt);
if let Some(current_path) = paths.get(0) {
if let Some(destination) = current_path.parent().zip(current_path.file_stem()) {
let destination_path = destination.0.to_path_buf();
self.operation(Operation::Extract {
paths,
to: destination_path,
});
}
if let Some(destination) = paths
.first()
.and_then(|first| first.parent())
.map(|parent| parent.to_path_buf())
{
self.operation(Operation::Extract {
paths,
to: destination,
});
}
}
Message::Key(modifiers, key) => {
@ -2137,7 +2165,7 @@ impl Application for App {
};
if let Some(title) = title_opt {
self.tab_model.text_set(entity, title);
commands.push(self.rescan_tab(entity, home_location.clone(), None));
commands.push(self.update_tab(entity, home_location.clone(), None));
}
}
if !commands.is_empty() {
@ -2302,11 +2330,7 @@ impl Application for App {
let mut commands = Vec::with_capacity(needs_reload.len());
for (entity, location) in needs_reload {
if let Location::Search(_, term, ..) = location {
commands.push(self.search_set(entity, Some(term)));
} else {
commands.push(self.rescan_tab(entity, location, None));
}
commands.push(self.update_tab(entity, location, None));
}
return Task::batch(commands);
}
@ -2545,8 +2569,6 @@ impl Application for App {
commands.push(self.rescan_operation_selection(op_sel));
// Manually rescan any trash tabs after any operation is completed
commands.push(self.rescan_trash());
// if search is active, update "search" tab view
commands.push(self.search());
return Task::batch(commands);
}
Message::PendingDismiss => {
@ -2869,7 +2891,7 @@ impl Application for App {
commands.push(Task::batch([
self.update_title(),
self.update_watcher(),
self.rescan_tab(entity, tab_path, selection_paths),
self.update_tab(entity, tab_path, selection_paths),
]));
}
tab::Command::DropFiles(to, from) => {
@ -3024,6 +3046,12 @@ impl Application for App {
]);
}
}
Message::WindowUnfocus => {
let tab_entity = self.tab_model.active();
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) {
tab.context_menu = None;
}
}
Message::WindowCloseRequested(id) => {
self.remove_window(&id);
}
@ -3156,7 +3184,7 @@ impl Application for App {
return Task::batch([
self.update_title(),
self.update_watcher(),
self.rescan_tab(entity, location, None),
self.update_tab(entity, location, None),
]);
}
}
@ -4377,6 +4405,7 @@ impl Application for App {
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
Some(Message::Modifiers(modifiers))
}
Event::Window(WindowEvent::Unfocused) => Some(Message::WindowUnfocus),
Event::Window(WindowEvent::CloseRequested) => Some(Message::WindowClose),
Event::Window(WindowEvent::Opened { position: _, size }) => {
Some(Message::Size(size))

View file

@ -28,6 +28,8 @@ impl ClipboardCopy {
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: &[P]) -> Self {
let available = vec![
"text/plain".to_string(),
"text/plain;charset=utf-8".to_string(),
"UTF8_STRING".to_string(),
"text/uri-list".to_string(),
"x-special/gnome-copied-files".to_string(),
];
@ -94,7 +96,9 @@ impl AsMimeTypes for ClipboardCopy {
fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
match mime_type {
"text/plain" => Some(self.text_plain.clone()),
"text/plain" | "text/plain;charset=utf-8" | "UTF8_STRING" => {
Some(self.text_plain.clone())
}
"text/uri-list" => Some(self.text_uri_list.clone()),
"x-special/gnome-copied-files" => Some(self.x_special_gnome_copied_files.clone()),
_ => None,

View file

@ -68,7 +68,7 @@ impl Favorite {
Self::Videos,
] {
if let Some(favorite_path) = favorite.path_opt() {
if &favorite_path == &path {
if favorite_path == path {
return favorite.clone();
}
}

View file

@ -157,13 +157,15 @@ impl<M: Send + 'static> Dialog<M> {
let (config_handler, config) = Config::load();
let mut settings = window::Settings::default();
settings.decorations = false;
settings.exit_on_close_request = false;
settings.min_size = Some(Size::new(360.0, 180.0));
settings.resizable = true;
settings.size = Size::new(1024.0, 640.0);
settings.transparent = true;
let mut settings = window::Settings {
decorations: false,
exit_on_close_request: false,
min_size: Some(Size::new(360.0, 180.0)),
resizable: true,
size: Size::new(1024.0, 640.0),
transparent: true,
..Default::default()
};
#[cfg(target_os = "linux")]
{
@ -251,7 +253,8 @@ impl<M: Send + 'static> Dialog<M> {
self.cosmic
.subscription()
.map(DialogMessage)
.map(self.mapper)
.with(self.mapper)
.map(|(mapper, message)| mapper(message))
}
pub fn update(&mut self, message: DialogMessage) -> Task<M> {
@ -295,6 +298,7 @@ struct Flags {
kind: DialogKind,
path_opt: Option<PathBuf>,
window_id: window::Id,
#[expect(dead_code)]
config_handler: Option<cosmic_config::Config>,
config: Config,
}
@ -323,6 +327,7 @@ enum Message {
SearchActivate,
SearchClear,
SearchInput(String),
#[allow(clippy::enum_variant_names)]
TabMessage(tab::Message),
TabRescan(Location, Option<tab::Item>, Vec<tab::Item>),
TabView(tab::View),
@ -344,6 +349,7 @@ impl From<AppMessage> for Message {
AppMessage::ZoomDefault(_entity_opt) => Message::ZoomDefault,
AppMessage::ZoomIn(_entity_opt) => Message::ZoomIn,
AppMessage::ZoomOut(_entity_opt) => Message::ZoomOut,
AppMessage::NewItem(_entity_opt, true) => Message::NewFolder,
unsupported => {
log::warn!("{unsupported:?} not supported in dialog mode");
Message::None
@ -596,7 +602,7 @@ impl App {
.data(Location::Recents)
});
for (_favorite_i, favorite) in self.flags.config.favorites.iter().enumerate() {
for favorite in self.flags.config.favorites.iter() {
if let Some(path) = favorite.path_opt() {
let name = if matches!(favorite, Favorite::Home) {
fl!("home")
@ -972,11 +978,8 @@ impl Application for App {
ContextPage::Preview(..) => self.core.window.show_context,
_ => false,
};
elements.push(
menu::dialog_menu(&self.tab, &self.key_binds, show_details)
.map(Message::from)
.into(),
);
elements
.push(menu::dialog_menu(&self.tab, &self.key_binds, show_details).map(Message::from));
elements
}
@ -1025,7 +1028,7 @@ impl Application for App {
return self.update(message);
}
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() {
if let Some(data) = self.nav_model.data::<MounterData>(entity) {
if let Some(mounter) = MOUNTERS.get(&data.0) {
return mounter.mount(data.1.clone()).map(|_| message::none());
}
@ -1167,11 +1170,9 @@ impl Application for App {
let mut still_mounted = false;
for item in mounter_items.iter() {
if let Some(path) = item.path() {
if path == old_path {
if item.is_mounted() {
still_mounted = true;
break;
}
if path == old_path && item.is_mounted() {
still_mounted = true;
break;
}
}
}
@ -1219,7 +1220,7 @@ impl Application for App {
let mut contains_change = false;
for event in events.iter() {
for event_path in event.paths.iter() {
if event_path.starts_with(&path) {
if event_path.starts_with(path) {
match event.kind {
notify::EventKind::Modify(
notify::event::ModifyKind::Metadata(_),
@ -1233,14 +1234,14 @@ impl Application for App {
for item in items.iter_mut() {
if item.path_opt() == Some(event_path) {
//TODO: reload more, like mime types?
match fs::metadata(&event_path) {
match fs::metadata(event_path) {
Ok(new_metadata) => {
match &mut item.metadata {
ItemMetadata::Path {
metadata,
..
} => *metadata = new_metadata,
_ => {}
if let ItemMetadata::Path {
metadata,
..
} = &mut item.metadata
{
*metadata = new_metadata;
}
}
Err(err) => {
@ -1320,12 +1321,9 @@ impl Application for App {
// If we are in directory mode, return the current directory
if self.flags.kind.is_dir() {
match &self.tab.location {
Location::Path(tab_path) => {
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
return window::close(self.flags.window_id);
}
_ => {}
if let Location::Path(tab_path) = &self.tab.location {
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
return window::close(self.flags.window_id);
}
}
}
@ -1342,7 +1340,7 @@ impl Application for App {
if let DialogKind::SaveFile { filename } = &self.flags.kind {
if !filename.is_empty() {
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() {
// cd to directory
let message = Message::TabMessage(tab::Message::Location(
@ -1590,11 +1588,7 @@ impl Application for App {
}
}
col = col.push(
self.tab
.view(&self.key_binds)
.map(move |message| Message::TabMessage(message)),
);
col = col.push(self.tab.view(&self.key_binds).map(Message::TabMessage));
col.into()
}
@ -1708,16 +1702,17 @@ impl Application for App {
];
for (key, mounter) in MOUNTERS.iter() {
let key = *key;
subscriptions.push(mounter.subscription().map(move |mounter_message| {
match mounter_message {
MounterMessage::Items(items) => Message::MounterItems(key, items),
_ => {
log::warn!("{:?} not supported in dialog mode", mounter_message);
Message::None
}
}
}));
subscriptions.push(
mounter.subscription().with(*key).map(
|(key, mounter_message)| match mounter_message {
MounterMessage::Items(items) => Message::MounterItems(key, items),
_ => {
log::warn!("{:?} not supported in dialog mode", mounter_message);
Message::None
}
},
),
);
}
Subscription::batch(subscriptions)

View file

@ -29,10 +29,14 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {
bind!([], Key::Named(Named::ArrowLeft), ItemLeft);
bind!([], Key::Named(Named::ArrowRight), ItemRight);
bind!([], Key::Named(Named::ArrowUp), ItemUp);
bind!([], Key::Named(Named::Home), SelectFirst);
bind!([], Key::Named(Named::End), SelectLast);
bind!([Shift], Key::Named(Named::ArrowDown), ItemDown);
bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft);
bind!([Shift], Key::Named(Named::ArrowRight), ItemRight);
bind!([Shift], Key::Named(Named::ArrowUp), ItemUp);
bind!([Shift], Key::Named(Named::Home), SelectFirst);
bind!([Shift], Key::Named(Named::End), SelectLast);
bind!([Ctrl, Shift], Key::Character("n".into()), NewFolder);
bind!([], Key::Named(Named::Enter), Open);
bind!([Ctrl], Key::Named(Named::Space), Preview);

View file

@ -89,7 +89,7 @@ pub fn context_menu<'a>(
let mut selected_trash_only = false;
let mut selected_desktop_entry = None;
let mut selected_types: Vec<Mime> = vec![];
tab.items_opt().map(|items| {
if let Some(items) = tab.items_opt() {
for item in items.iter() {
if item.selected {
selected += 1;
@ -110,7 +110,7 @@ pub fn context_menu<'a>(
selected_types.push(item.mime.clone());
}
}
});
};
selected_types.sort_unstable();
selected_types.dedup();
selected_trash_only = selected_trash_only && selected == 1;
@ -163,7 +163,7 @@ pub fn context_menu<'a>(
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
}
}
if matches!(tab.location, Location::Search(..)) {
if matches!(tab.location, Location::Search(..) | Location::Recents) {
children.push(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
);
@ -260,7 +260,7 @@ pub fn context_menu<'a>(
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into());
}
if matches!(tab.location, Location::Search(..)) {
if matches!(tab.location, Location::Search(..) | Location::Recents) {
children.push(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
);
@ -342,7 +342,7 @@ pub fn context_menu<'a>(
.into()
}
pub fn dialog_menu<'a>(
pub fn dialog_menu(
tab: &Tab,
key_binds: &HashMap<KeyBind, Action>,
show_details: bool,
@ -359,15 +359,13 @@ pub fn dialog_menu<'a>(
let in_trash = tab.location == Location::Trash;
let mut selected_gallery = 0;
tab.items_opt().map(|items| {
if let Some(items) = tab.items_opt() {
for item in items.iter() {
if item.selected {
if item.can_gallery() {
selected_gallery += 1;
}
if item.selected && item.can_gallery() {
selected_gallery += 1;
}
}
});
};
MenuBar::new(vec![
menu::Tree::with_children(
@ -504,7 +502,7 @@ pub fn menu_bar<'a>(
let mut selected_dir = 0;
let mut selected = 0;
let mut selected_gallery = 0;
tab_opt.and_then(|tab| tab.items_opt()).map(|items| {
if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) {
for item in items.iter() {
if item.selected {
selected += 1;
@ -516,7 +514,7 @@ pub fn menu_bar<'a>(
}
}
}
});
};
MenuBar::new(vec![
menu::Tree::with_children(

View file

@ -120,7 +120,7 @@ impl MimeAppCache {
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if apps.iter().find(|x| x.id == app.id).is_none() {
if !apps.iter().any(|x| x.id == app.id) {
apps.push(MimeApp::from(app));
}
}
@ -191,11 +191,7 @@ impl MimeAppCache {
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if apps
.iter()
.find(|x| filename_eq(&x.path, filename))
.is_none()
{
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) =
all_apps.iter().find(|x| filename_eq(&x.path, filename))
{
@ -263,9 +259,7 @@ impl MimeAppCache {
}
pub fn get(&self, key: &Mime) -> Vec<MimeApp> {
self.cache
.get(&key)
.map_or_else(|| Vec::new(), |x| x.clone())
self.cache.get(key).map_or_else(Vec::new, |x| x.clone())
}
}
@ -292,5 +286,5 @@ pub fn terminal() -> Option<MimeApp> {
}
// Return whatever was the first terminal found
mime_app_cache.terminals.first().map(|x| x.clone())
mime_app_cache.terminals.first().cloned()
}

View file

@ -97,7 +97,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
info.icon()
.as_ref()
.and_then(|icon| gio_icon_to_path(icon, size))
.map(|path| widget::icon::from_path(path))
.map(widget::icon::from_path)
.unwrap_or(
widget::icon::from_name(if metadata.is_dir() {
"folder"
@ -388,10 +388,7 @@ impl Gvfs {
let file = gio::File::for_uri(&uri);
let needs_mount = match file.find_enclosing_mount(gio::Cancellable::NONE) {
Ok(_) => false,
Err(err) => match err.kind::<gio::IOErrorEnum>() {
Some(gio::IOErrorEnum::NotMounted) => true,
_ => false
}
Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
};
if needs_mount {
let mount_op = mount_op(uri.clone(), event_tx.clone());
@ -468,7 +465,6 @@ impl Mounter for Gvfs {
Task::perform(
async move {
command_tx.send(Cmd::Mount(item)).unwrap();
()
},
|x| x,
)
@ -479,7 +475,6 @@ impl Mounter for Gvfs {
Task::perform(
async move {
command_tx.send(Cmd::NetworkDrive(uri)).unwrap();
()
},
|x| x,
)
@ -498,7 +493,6 @@ impl Mounter for Gvfs {
Task::perform(
async move {
command_tx.send(Cmd::Unmount(item)).unwrap();
()
},
|x| x,
)

View file

@ -117,4 +117,4 @@ pub fn mounters() -> Mounters {
Mounters::new(mounters)
}
pub static MOUNTERS: Lazy<Mounters> = Lazy::new(|| mounters());
pub static MOUNTERS: Lazy<Mounters> = Lazy::new(mounters);

View file

@ -31,156 +31,150 @@ use crate::tab::DOUBLE_CLICK_DURATION;
pub struct MouseArea<'a, Message> {
id: Id,
content: Element<'a, Message>,
on_drag: Option<Box<dyn Fn(Option<Rectangle>) -> Message + 'a>>,
on_double_click: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_drag_end: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_right_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_right_press_no_capture: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_right_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_middle_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_middle_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_back_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_back_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_forward_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_forward_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a>>,
on_enter: Option<Box<dyn Fn() -> Message + 'a>>,
on_exit: Option<Box<dyn Fn() -> Message + 'a>>,
on_drag: Option<Box<dyn OnDrag<'a, Message>>>,
on_double_click: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_drag_end: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_resize: Option<Box<dyn OnResize<'a, Message>>>,
on_right_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_right_press_no_capture: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_right_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_middle_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_middle_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_back_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_back_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_forward_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_forward_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_scroll: Option<Box<dyn OnScroll<'a, Message>>>,
on_enter: Option<Box<dyn OnEnterExit<'a, Message>>>,
on_exit: Option<Box<dyn OnEnterExit<'a, Message>>>,
show_drag_rect: bool,
}
impl<'a, Message> MouseArea<'a, Message> {
/// The message to emit when a drag is initiated.
#[must_use]
pub fn on_drag(mut self, message: impl Fn(Option<Rectangle>) -> Message + 'a) -> Self {
pub fn on_drag(mut self, message: impl OnDrag<'a, Message>) -> Self {
self.on_drag = Some(Box::new(message));
self
}
/// The message to emit when a drag ends.
#[must_use]
pub fn on_drag_end(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_drag_end(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_drag_end = Some(Box::new(message));
self
}
/// The message to emit on a double click.
#[must_use]
pub fn on_double_click(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_double_click(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_double_click = Some(Box::new(message));
self
}
/// The message to emit on a left button press.
#[must_use]
pub fn on_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_press = Some(Box::new(message));
self
}
/// The message to emit on a left button release.
#[must_use]
pub fn on_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_release = Some(Box::new(message));
self
}
/// The message to emit on resizing.
#[must_use]
pub fn on_resize(mut self, message: impl Fn(Size) -> Message + 'a) -> Self {
pub fn on_resize(mut self, message: impl OnResize<'a, Message>) -> Self {
self.on_resize = Some(Box::new(message));
self
}
/// The message to emit on a right button press.
#[must_use]
pub fn on_right_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_right_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_right_press = Some(Box::new(message));
self
}
/// The message to emit on a right button press without capturing.
#[must_use]
pub fn on_right_press_no_capture(
mut self,
message: impl Fn(Option<Point>) -> Message + 'a,
) -> Self {
pub fn on_right_press_no_capture(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_right_press_no_capture = Some(Box::new(message));
self
}
/// The message to emit on a right button release.
#[must_use]
pub fn on_right_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_right_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_right_release = Some(Box::new(message));
self
}
/// The message to emit on a middle button press.
#[must_use]
pub fn on_middle_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_middle_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_middle_press = Some(Box::new(message));
self
}
/// The message to emit on a middle button release.
#[must_use]
pub fn on_middle_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_middle_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_middle_release = Some(Box::new(message));
self
}
/// The message to emit on a back button press.
#[must_use]
pub fn on_back_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_back_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_back_press = Some(Box::new(message));
self
}
/// The message to emit on a back button release.
#[must_use]
pub fn on_back_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_back_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_back_release = Some(Box::new(message));
self
}
/// The message to emit on a forward button press.
#[must_use]
pub fn on_forward_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_forward_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_forward_press = Some(Box::new(message));
self
}
/// The message to emit on a forward button release.
#[must_use]
pub fn on_forward_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
pub fn on_forward_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_forward_release = Some(Box::new(message));
self
}
/// The message to emit on a scroll.
#[must_use]
pub fn on_scroll(
mut self,
message: impl Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a,
) -> Self {
pub fn on_scroll(mut self, message: impl OnScroll<'a, Message>) -> Self {
self.on_scroll = Some(Box::new(message));
self
}
/// The message to emit when a mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: impl Fn() -> Message + 'a) -> Self {
pub fn on_enter(mut self, message: impl OnEnterExit<'a, Message>) -> Self {
self.on_enter = Some(Box::new(message));
self
}
/// The message to emit when a mouse exits the area.
#[must_use]
pub fn on_exit(mut self, message: impl Fn() -> Message + 'a) -> Self {
pub fn on_exit(mut self, message: impl OnEnterExit<'a, Message>) -> Self {
self.on_exit = Some(Box::new(message));
self
}
@ -199,6 +193,24 @@ impl<'a, Message> MouseArea<'a, Message> {
}
}
pub trait OnMouseButton<'a, Message>: Fn(Option<Point>) -> Message + 'a {}
impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option<Point>) -> Message + 'a {}
pub trait OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Message + 'a {}
impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Message + 'a {}
pub trait OnResize<'a, Message>: Fn(Size) -> Message + 'a {}
impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Size) -> Message + 'a {}
pub trait OnScroll<'a, Message>: Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a {}
impl<'a, Message, F> OnScroll<'a, Message> for F where
F: Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a
{
}
pub trait OnEnterExit<'a, Message>: Fn() -> Message + 'a {}
impl<'a, Message, F> OnEnterExit<'a, Message> for F where F: Fn() -> Message + 'a {}
/// Local state of the [`MouseArea`].
#[derive(Default)]
struct State {
@ -250,7 +262,7 @@ impl State {
} else {
mouse::Click::new(pos, mouse::Button::Left, None)
};
self.prev_click = Some((new.clone(), now));
self.prev_click = Some((new, now));
new
}
}
@ -284,7 +296,7 @@ impl<'a, Message> MouseArea<'a, Message> {
}
}
impl<'a, Message> Widget<Message, Theme, Renderer> for MouseArea<'a, Message>
impl<Message> Widget<Message, Theme, Renderer> for MouseArea<'_, Message>
where
Message: Clone,
{
@ -659,7 +671,7 @@ fn update<Message: Clone>(
if let Some(on_scroll) = widget.on_scroll.as_ref() {
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
if let Some(message) = on_scroll(delta.clone(), state.modifiers) {
if let Some(message) = on_scroll(*delta, state.modifiers) {
shell.publish(message);
return event::Status::Captured;
}

View file

@ -22,8 +22,8 @@ pub struct Controller {
inner: Arc<ControllerInner>,
}
impl Controller {
pub fn new() -> Self {
impl Default for Controller {
fn default() -> Self {
Self {
primary: true,
inner: Arc::new(ControllerInner {
@ -33,7 +33,9 @@ impl Controller {
}),
}
}
}
impl Controller {
pub fn check(&self) -> Result<(), String> {
let mut state = self.inner.state.lock().unwrap();
loop {

View file

@ -67,11 +67,20 @@ fn handle_replace(
}
fn get_directory_name(file_name: &str) -> &str {
const SUPPORTED_EXTENSIONS: [&str; 4] = [".tar.gz", ".tgz", ".tar", ".zip"];
// TODO: Chain with COMPOUND_EXTENSIONS once more formats are supported
const SUPPORTED_EXTENSIONS: &[&str] = &[
".tar.bz2",
".tar.gz",
".tar.lzma",
".tar.xz",
".tgz",
".tar",
".zip",
];
for ext in &SUPPORTED_EXTENSIONS {
if file_name.ends_with(ext) {
return &file_name[..file_name.len() - ext.len()];
for ext in SUPPORTED_EXTENSIONS {
if let Some(stripped) = file_name.strip_suffix(ext) {
return stripped;
}
}
file_name
@ -245,7 +254,7 @@ async fn copy_or_move(
if matches!(from.parent(), Some(parent) if parent == to) && !moving {
// `from`'s parent is equal to `to` which means we're copying to the same
// directory (duplicating files)
let to = copy_unique_path(&from, &to);
let to = copy_unique_path(&from, to);
Some((from, to))
} else if let Some(name) = from.file_name() {
let to = to.join(name);
@ -361,12 +370,12 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
to
}
fn file_name<'a>(path: &'a Path) -> Cow<'a, str> {
fn file_name(path: &Path) -> Cow<'_, str> {
path.file_name()
.map_or_else(|| fl!("unknown-folder").into(), |x| x.to_string_lossy())
}
fn parent_name<'a>(path: &'a Path) -> Cow<'a, str> {
fn parent_name(path: &Path) -> Cow<'_, str> {
let Some(parent) = path.parent() else {
return fl!("unknown-folder").into();
};
@ -374,7 +383,7 @@ fn parent_name<'a>(path: &'a Path) -> Cow<'a, str> {
file_name(parent)
}
fn paths_parent_name<'a>(paths: &'a Vec<PathBuf>) -> Cow<'a, str> {
fn paths_parent_name(paths: &[PathBuf]) -> Cow<'_, str> {
let Some(first_path) = paths.first() else {
return fl!("unknown-folder").into();
};
@ -674,7 +683,7 @@ impl Operation {
path.strip_prefix(relative_root).map_err(err_str)?.to_str()
{
if path.is_file() {
let mut file = fs::File::open(&path).map_err(err_str)?;
let mut file = fs::File::open(path).map_err(err_str)?;
let metadata = file.metadata().map_err(err_str)?;
let total = metadata.len();
if total >= 4 * 1024 * 1024 * 1024 {
@ -777,8 +786,6 @@ impl Operation {
controller.set_progress((i as f32) / total_paths as f32);
let to = to.to_owned();
if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
let dir_name = get_directory_name(file_name);
let mut new_dir = to.join(dir_name);
@ -793,7 +800,7 @@ impl Operation {
op_sel.selected.push(new_dir.clone());
let controller = controller.clone();
let mime = mime_for_path(&path);
let mime = mime_for_path(path);
match mime.essence_str() {
"application/gzip" | "application/x-compressed-tar" => {
OpReader::new(path, controller)
@ -953,7 +960,7 @@ mod tests {
};
use cosmic::iced::futures::{channel::mpsc, StreamExt};
use log::{debug, trace};
use log::debug;
use test_log::test;
use tokio::sync;
@ -961,8 +968,8 @@ mod tests {
use crate::{
app::{
test_utils::{
empty_fs, filter_dirs, filter_files, read_dir_sorted, simple_fs, NAME_LEN,
NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED,
empty_fs, filter_dirs, filter_files, simple_fs, NAME_LEN, NUM_DIRS, NUM_FILES,
NUM_HIDDEN, NUM_NESTED,
},
DialogPage, Message,
},
@ -986,7 +993,7 @@ mod tests {
paths: paths_clone,
to: to_clone,
}
.perform(&sync::Mutex::new(tx).into(), Controller::new())
.perform(&sync::Mutex::new(tx).into(), Controller::default())
.await
});

View file

@ -12,12 +12,18 @@ use super::{copy_unique_path, Controller, OperationSelection, ReplaceResult};
pub struct Context {
buf: Vec<u8>,
controller: Controller,
on_progress: Box<dyn Fn(&Op, &Progress) + 'static>,
on_replace: Box<dyn Fn(&Op) -> ReplaceResult + 'static>,
on_progress: Box<dyn OnProgress>,
on_replace: Box<dyn OnReplace>,
pub(crate) op_sel: OperationSelection,
replace_result_opt: Option<ReplaceResult>,
}
pub trait OnProgress: Fn(&Op, &Progress) + 'static {}
impl<F> OnProgress for F where F: Fn(&Op, &Progress) + 'static {}
pub trait OnReplace: Fn(&Op) -> ReplaceResult + 'static {}
impl<F> OnReplace for F where F: Fn(&Op) -> ReplaceResult + 'static {}
impl Context {
pub fn new(controller: Controller) -> Self {
Self {
@ -67,7 +73,7 @@ impl Context {
OpKind::Symlink { target }
} else {
//TODO: present dialog and allow continue
return Err(format!("{} is not a known file type", from.display()).into());
return Err(format!("{} is not a known file type", from.display()));
};
let to = if from == from_parent {
// When copying a file, from matches from_parent, and to_parent must be used
@ -130,12 +136,12 @@ impl Context {
Ok(true)
}
pub fn on_progress<F: Fn(&Op, &Progress) + 'static>(mut self, f: F) -> Self {
pub fn on_progress<F: OnProgress>(mut self, f: F) -> Self {
self.on_progress = Box::new(f);
self
}
pub fn on_replace<F: Fn(&Op) -> ReplaceResult + 'static>(mut self, f: F) -> Self {
pub fn on_replace<F: OnReplace>(mut self, f: F) -> Self {
self.on_replace = Box::new(f);
self
}
@ -153,9 +159,7 @@ impl Context {
Ok(ControlFlow::Continue(op.to.clone()))
}
ReplaceResult::KeepBoth => match op.to.parent() {
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(
&op.from, &to_parent,
))),
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))),
None => Err(format!("failed to get parent of {:?}", op.to).into()),
},
ReplaceResult::Skip(apply_to_all) => {
@ -216,7 +220,7 @@ impl Op {
let metadata = from_file.metadata()?;
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(&self)? {
match ctx.replace(self)? {
ControlFlow::Continue(to) => {
self.to = to;
}
@ -226,7 +230,7 @@ impl Op {
}
}
progress.total_bytes = Some(metadata.len());
(ctx.on_progress)(&self, &progress);
(ctx.on_progress)(self, &progress);
// This is atomic and ensures `to` is not created by any other process
let mut to_file = fs::OpenOptions::new()
.create_new(true)
@ -242,14 +246,14 @@ impl Op {
}
to_file.write_all(&ctx.buf[..count])?;
progress.current_bytes += count as u64;
(ctx.on_progress)(&self, &progress);
(ctx.on_progress)(self, &progress);
}
to_file.sync_all()?;
}
OpKind::Move => {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(&self)? {
match ctx.replace(self)? {
ControlFlow::Continue(to) => {
self.to = to;
}
@ -289,7 +293,7 @@ impl Op {
OpKind::Symlink { ref target } => {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(&self)? {
match ctx.replace(self)? {
ControlFlow::Continue(to) => {
self.to = to;
}
@ -298,8 +302,18 @@ impl Op {
}
}
}
//TODO: use OS-specific function
fs::soft_link(&target, &self.to)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, &self.to)?;
}
#[cfg(windows)]
{
if target.is_dir() {
std::os::windows::fs::symlink_dir(target, &self.to)?;
} else {
std::os::windows::fs::symlink_file(target, &self.to)?;
}
}
}
}
Ok(true)

View file

@ -475,7 +475,7 @@ pub fn item_from_entry(
};
let dir_size = if metadata.is_dir() {
DirSize::Calculating(Controller::new())
DirSize::Calculating(Controller::default())
} else {
DirSize::NotDirectory
};
@ -1071,6 +1071,8 @@ pub enum Message {
SearchContext(Location, SearchContextWrapper),
SearchReady(bool),
SelectAll,
SelectFirst,
SelectLast,
SetSort(HeadingOptions, bool),
Thumbnail(PathBuf, ItemThumbnail),
ToggleShowHidden,
@ -2243,8 +2245,8 @@ impl Tab {
if !item.selected {
self.clicked = click_i_opt;
item.selected = true;
self.select_range = Some((i, i));
}
self.select_range = Some((i, i));
self.select_focus = click_i_opt;
self.selected_clicked = true;
} else if !dont_unset && item.selected {
@ -2752,9 +2754,7 @@ impl Tab {
if let Some(items) = &mut self.items_opt {
if finished || context.ready.swap(false, atomic::Ordering::SeqCst) {
let duration = Instant::now();
while let Some((path, name, metadata)) =
context.results_rx.blocking_recv()
{
while let Ok((path, name, metadata)) = context.results_rx.try_recv() {
//TODO: combine this with column_sort logic, they must match!
let item_modified = metadata.modified().ok();
let index = match items.binary_search_by(|other| {
@ -2801,6 +2801,36 @@ impl Tab {
));
}
}
Message::SelectFirst => {
if self.select_position(0, 0, mod_shift) {
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
}
Message::SelectLast => {
if let Some(ref items) = self.items_opt {
if let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max()
{
if self.select_position(last_pos.0, last_pos.1, mod_shift) {
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset)
.into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
}
}
}
Message::SetSort(heading_option, dir) => {
if !matches!(self.location, Location::Search(..)) {
self.sort_name = heading_option;
@ -2865,7 +2895,7 @@ impl Tab {
Message::Drop(Some((to, mut from))) => {
self.dnd_hovered = None;
match to {
Location::Path(to) => {
Location::Desktop(to, ..) | Location::Path(to) => {
if let Ok(entries) = fs::read_dir(&to) {
for i in entries.into_iter().filter_map(|e| e.ok()) {
let i = i.path();
@ -3581,8 +3611,7 @@ impl Tab {
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
//TODO: left clicking on an empty folder does not clear context menu
widget::column::with_children(vec![widget::container(
mouse_area::MouseArea::new(widget::column::with_children(vec![widget::container(
widget::column::with_children(match self.mode {
Mode::App | Mode::Dialog(_) => vec![
widget::icon::from_name("folder-symbolic")
@ -3604,7 +3633,8 @@ impl Tab {
.spacing(space_xxs),
)
.center(Length::Fill)
.into()])
.into()]))
.on_press(|_| Message::Click(None))
.into()
}
@ -4285,9 +4315,9 @@ impl Tab {
.drag_content(move || {
ClipboardCopy::new(crate::clipboard::ClipboardKind::Copy, &files)
})
.drag_icon(move || {
.drag_icon(move |v| {
let state: tree::State = Widget::<Message, _, _>::state(&drag_list);
(Element::from(drag_list.clone()).map(|_m| ()), state)
(Element::from(drag_list.clone()).map(|_m| ()), state, v)
})
}
_ => item_view,

View file

@ -144,9 +144,7 @@ impl ThumbnailerCache {
}
pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {
self.cache
.get(&key)
.map_or_else(|| Vec::new(), |x| x.clone())
self.cache.get(key).map_or_else(Vec::new, |x| x.clone())
}
}