Merge branch 'pop-os:master' into master
This commit is contained in:
commit
e10e73be69
15 changed files with 785 additions and 253 deletions
31
examples/gio-list.rs
Normal file
31
examples/gio-list.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
use gio::prelude::*;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let uri = env::args().nth(1).expect("no uri provided");
|
||||||
|
let file = gio::File::for_uri(&uri);
|
||||||
|
for entry_res in file
|
||||||
|
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
let entry = entry_res.unwrap();
|
||||||
|
println!("{:?}", entry.display_name());
|
||||||
|
for attribute in entry.list_attributes(None) {
|
||||||
|
println!(
|
||||||
|
" {:?}: {:?}",
|
||||||
|
attribute,
|
||||||
|
entry.attribute_as_string(&attribute)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: what is the best way to resolve shortcuts?
|
||||||
|
let child = if let Some(target_uri) =
|
||||||
|
entry.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)
|
||||||
|
{
|
||||||
|
gio::File::for_uri(&target_uri)
|
||||||
|
} else {
|
||||||
|
file.child(entry.name())
|
||||||
|
};
|
||||||
|
println!("{:?}", child.uri());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ notification-in-progress = File operations are in progress.
|
||||||
trash = Trash
|
trash = Trash
|
||||||
recents = Recents
|
recents = Recents
|
||||||
undo = Undo
|
undo = Undo
|
||||||
|
today = Today
|
||||||
|
|
||||||
# List view
|
# List view
|
||||||
name = Name
|
name = Name
|
||||||
|
|
@ -235,4 +236,4 @@ sort-z-a = Z-A
|
||||||
sort-newest-first = Newest first
|
sort-newest-first = Newest first
|
||||||
sort-oldest-first = Oldest first
|
sort-oldest-first = Oldest first
|
||||||
sort-smallest-to-largest = Smallest to largest
|
sort-smallest-to-largest = Smallest to largest
|
||||||
sort-largest-to-smallest = Largest to smallest
|
sort-largest-to-smallest = Largest to smallest
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ empty-folder-hidden = Dossier vide (contient des éléments cachés)
|
||||||
no-results = Aucun résultat trouvé
|
no-results = Aucun résultat trouvé
|
||||||
filesystem = Système de fichiers
|
filesystem = Système de fichiers
|
||||||
home = Dossier personnel
|
home = Dossier personnel
|
||||||
notification-in-progress = Les opérations sur les fichiers sont en cours.
|
notification-in-progress = Des opérations sur des fichiers sont en cours.
|
||||||
trash = Corbeille
|
trash = Corbeille
|
||||||
recents = Récente
|
recents = Récents
|
||||||
undo = Annuler
|
undo = Annuler
|
||||||
|
|
||||||
# List view
|
# List view
|
||||||
|
|
@ -77,6 +77,31 @@ execute = Exécution
|
||||||
## About
|
## About
|
||||||
git-description = Git commit {$hash} le {$date}
|
git-description = Git commit {$hash} le {$date}
|
||||||
|
|
||||||
|
## Add Network Drive
|
||||||
|
add-network-drive = Ajouter un lecteur réseau
|
||||||
|
connect = Connecter
|
||||||
|
connect-anonymously = Connecter anonymement
|
||||||
|
connecting = Connection en cours...
|
||||||
|
domain = Domaine
|
||||||
|
enter-server-address = Entrez l'adresse du serveur
|
||||||
|
network-drive-description =
|
||||||
|
Les adresses de serveur incluent un préfixe de protocole et une adresse.
|
||||||
|
Exemples: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||||
|
### Make sure to keep the comma which separates the columns
|
||||||
|
network-drive-schemes =
|
||||||
|
Protocoles disponibles,Préfixe
|
||||||
|
AppleTalk,afp://
|
||||||
|
File Transfer Protocol,ftp:// or ftps://
|
||||||
|
Network File System,nfs://
|
||||||
|
Server Message Block,smb://
|
||||||
|
SSH File Transfer Protocol,sftp:// or ssh://
|
||||||
|
WebDav,dav:// or davs://
|
||||||
|
network-drive-error = Impossible d'accéder au lecteur réseau
|
||||||
|
password = Mot de passe
|
||||||
|
remember-password = Se souvenir du mot de passe
|
||||||
|
try-again = Essayer à nouveau
|
||||||
|
username = Nom d'utilisateur
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
edit-history = Modifier l'historique
|
edit-history = Modifier l'historique
|
||||||
history = Historique
|
history = Historique
|
||||||
|
|
@ -203,3 +228,12 @@ show-hidden-files = Afficher les fichiers cachés
|
||||||
list-directories-first = Lister les répertoires en premier
|
list-directories-first = Lister les répertoires en premier
|
||||||
menu-settings = Paramètres...
|
menu-settings = Paramètres...
|
||||||
menu-about = À propos de Fichiers COSMIC...
|
menu-about = À propos de Fichiers COSMIC...
|
||||||
|
|
||||||
|
## Sort
|
||||||
|
sort = Trier
|
||||||
|
sort-a-z = A-Z
|
||||||
|
sort-z-a = Z-A
|
||||||
|
sort-newest-first = Le plus récent en premier
|
||||||
|
sort-oldest-first = Le plus ancien en premier
|
||||||
|
sort-smallest-to-largest = Du plus petit au plus grand
|
||||||
|
sort-largest-to-smallest = Du plus grand au plus petit
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ empty-folder-hidden = Pusty katalog (z ukrytymi plikami)
|
||||||
no-results = Brak wyników
|
no-results = Brak wyników
|
||||||
filesystem = System plików
|
filesystem = System plików
|
||||||
home = Katalog Domowy
|
home = Katalog Domowy
|
||||||
|
networks = sieci
|
||||||
notification-in-progress = Operacje na plikach w toku.
|
notification-in-progress = Operacje na plikach w toku.
|
||||||
trash = Kosz
|
trash = Kosz
|
||||||
|
recents = Ubiegłe
|
||||||
undo = Cofnij
|
undo = Cofnij
|
||||||
|
|
||||||
# List view
|
# List view
|
||||||
|
|
@ -62,11 +64,44 @@ apply-to-all = Zastosuj do wszystkich
|
||||||
keep-both = Zachowaj oba
|
keep-both = Zachowaj oba
|
||||||
skip = Pomiń
|
skip = Pomiń
|
||||||
|
|
||||||
|
## Metadata Dialog
|
||||||
|
owner = Właściciel
|
||||||
|
group = Grupa
|
||||||
|
other = Inni
|
||||||
|
read = Odczyt
|
||||||
|
write = Zapis
|
||||||
|
execute = Wykonywanie
|
||||||
|
|
||||||
# Context Pages
|
# Context Pages
|
||||||
|
|
||||||
## About
|
## About
|
||||||
git-description = Git commit {$hash} z {$date}
|
git-description = Git commit {$hash} z {$date}
|
||||||
|
|
||||||
|
## Add Network Drive
|
||||||
|
add-network-drive = Dodaj dysk sieciowy
|
||||||
|
connect = Połącz
|
||||||
|
connect-anonymously = Połącz anonimowo
|
||||||
|
connecting = Łączenie...
|
||||||
|
domain = Domena
|
||||||
|
enter-server-address = Wprowadź adres serwera
|
||||||
|
network-drive-description =
|
||||||
|
Adres serwera zawiera prefiks protokołu i adres.
|
||||||
|
Przykładowo: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||||
|
### Make sure to keep the comma which separates the columns
|
||||||
|
network-drive-schemes =
|
||||||
|
Dostępne protokoły,Prefiks
|
||||||
|
AppleTalk,afp://
|
||||||
|
File Transfer Protocol,ftp:// or ftps://
|
||||||
|
Network File System,nfs://
|
||||||
|
Server Message Block,smb://
|
||||||
|
SSH File Transfer Protocol,sftp:// or ssh://
|
||||||
|
WebDav,dav:// or davs://
|
||||||
|
network-drive-error = Brak dostępu do dysku sieciowego
|
||||||
|
password = Hasło
|
||||||
|
remember-password = Zapamiętaj hasło
|
||||||
|
try-again = Spróbuj ponownie
|
||||||
|
username = Nazwa użytkownika
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
edit-history = Edytuj historię
|
edit-history = Edytuj historię
|
||||||
history = Historia
|
history = Historia
|
||||||
|
|
@ -200,11 +235,6 @@ show-hidden-files = Pokaż ukryte pliki
|
||||||
list-directories-first = Najpierw wyświetlaj katalogi
|
list-directories-first = Najpierw wyświetlaj katalogi
|
||||||
menu-settings = Ustawienia...
|
menu-settings = Ustawienia...
|
||||||
menu-about = O Plikach COSMIC...
|
menu-about = O Plikach COSMIC...
|
||||||
list-view = Widok listy
|
|
||||||
show-hidden-files = Pokaż ukryte pliki
|
|
||||||
list-directories-first = Najpierw wyświetlaj katalogi
|
|
||||||
menu-settings = Ustawienia...
|
|
||||||
menu-about = O Plikach COSMIC...
|
|
||||||
|
|
||||||
## Sort
|
## Sort
|
||||||
sort = Uszereguj
|
sort = Uszereguj
|
||||||
|
|
@ -212,5 +242,5 @@ sort-a-z = A-Z
|
||||||
sort-z-a = Z-A
|
sort-z-a = Z-A
|
||||||
sort-newest-first = Najpierw najnowsze
|
sort-newest-first = Najpierw najnowsze
|
||||||
sort-oldest-first = Najpierw najstarsze
|
sort-oldest-first = Najpierw najstarsze
|
||||||
sort-smallest-to-largest = Od najmniejszego do największego
|
sort-smallest-to-largest = Najpierw najmniejsze
|
||||||
sort-largest-to-smallest = Od największego do najmniejszego
|
sort-largest-to-smallest = Najpierw największe
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ empty-folder = Tom katalog
|
||||||
empty-folder-hidden = Tom katalog (har dolda objekt)
|
empty-folder-hidden = Tom katalog (har dolda objekt)
|
||||||
filesystem = Filsystem
|
filesystem = Filsystem
|
||||||
home = Hem
|
home = Hem
|
||||||
|
networks = Nätverk
|
||||||
trash = Papperskorg
|
trash = Papperskorg
|
||||||
|
recents = Senaste
|
||||||
|
undo = Ångra
|
||||||
|
today = Idag
|
||||||
|
|
||||||
# Dialog
|
# Dialog
|
||||||
cancel = Avbryt
|
cancel = Avbryt
|
||||||
|
|
@ -27,6 +31,15 @@ properties = Egenskaper
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
settings = Inställningar
|
settings = Inställningar
|
||||||
|
settings-tab = Flik
|
||||||
|
settings-show-hidden = Visa dolda filer
|
||||||
|
default-view = Standardvy
|
||||||
|
icon-size-list = Ikonstorlek (lista)
|
||||||
|
icon-size-grid = Ikonstorlek (rutnät)
|
||||||
|
sorting-name = Sortera efter
|
||||||
|
direction = Riktning
|
||||||
|
ascending = Stigande
|
||||||
|
descending = Fallande
|
||||||
|
|
||||||
### Appearance
|
### Appearance
|
||||||
appearance = Utseende
|
appearance = Utseende
|
||||||
|
|
@ -78,3 +91,39 @@ default-app = {$name} (standard)
|
||||||
|
|
||||||
## Show details
|
## Show details
|
||||||
show-details = Visa detaljer
|
show-details = Visa detaljer
|
||||||
|
|
||||||
|
|
||||||
|
network-drive-description =
|
||||||
|
Serveradresser inkluderar ett protokollprefix och en adress.
|
||||||
|
Exempel: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||||
|
### Se till att behålla kommatecken som skiljer kolumnerna åt
|
||||||
|
network-drive-schemes =
|
||||||
|
Tillgängliga protokoll, Prefix
|
||||||
|
AppleTalk,afp://
|
||||||
|
Filöverföringsprotokoll,ftp:// eller ftps://
|
||||||
|
Nätverksfilsystem,nfs://
|
||||||
|
Servermeddelandeblock,smb://
|
||||||
|
SSH-filöverföringsprotokoll,sftp:// eller ssh://
|
||||||
|
WebDav,dav:// eller davs://
|
||||||
|
network-drive-error = Kan inte komma åt nätverksenheten
|
||||||
|
password = Lösenord
|
||||||
|
remember-password = Kom ihåg lösenord
|
||||||
|
|
||||||
|
## Lägg till en Nätverksenhet
|
||||||
|
add-network-drive = Lägg till en Nätverksenhet
|
||||||
|
connect = Anslut
|
||||||
|
connect-anonymously = Anslut anonymt
|
||||||
|
connecting = Ansluter...
|
||||||
|
domain = Domän
|
||||||
|
enter-server-address = Ange server address
|
||||||
|
try-again = Försök igen
|
||||||
|
username = Användarnamn
|
||||||
|
|
||||||
|
## Sort
|
||||||
|
sort = Sortera
|
||||||
|
sort-a-z = A-Z
|
||||||
|
sort-z-a = Z-A
|
||||||
|
sort-newest-first = Nyaste först
|
||||||
|
sort-oldest-first = Äldst först
|
||||||
|
sort-smallest-to-largest = Minsta till största
|
||||||
|
sort-largest-to-smallest = Största till minsta
|
||||||
|
|
|
||||||
89
src/app.rs
89
src/app.rs
|
|
@ -521,11 +521,14 @@ impl App {
|
||||||
location: Location,
|
location: Location,
|
||||||
selection_path: Option<PathBuf>,
|
selection_path: Option<PathBuf>,
|
||||||
) -> Command<Message> {
|
) -> Command<Message> {
|
||||||
|
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(icon_sizes)).await {
|
match tokio::task::spawn_blocking(move || location2.scan(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))
|
||||||
}
|
}
|
||||||
|
|
@ -591,7 +594,7 @@ impl App {
|
||||||
if let Some(ref items) = tab.items_opt() {
|
if let Some(ref items) = tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(path) = &item.path_opt {
|
if let Some(Location::Path(path)) = &item.location_opt {
|
||||||
paths.push(path.clone());
|
paths.push(path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -672,7 +675,10 @@ impl App {
|
||||||
.size(16)
|
.size(16)
|
||||||
.handle(),
|
.handle(),
|
||||||
))
|
))
|
||||||
.data(Location::Networks)
|
.data(Location::Network(
|
||||||
|
"network:///".to_string(),
|
||||||
|
fl!("networks"),
|
||||||
|
))
|
||||||
.divider_above()
|
.divider_above()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -941,49 +947,35 @@ impl App {
|
||||||
fn properties(&self, entity: Option<ContextItem>) -> Element<Message> {
|
fn properties(&self, entity: Option<ContextItem>) -> Element<Message> {
|
||||||
match entity {
|
match entity {
|
||||||
None => self.tab_properties(self.tab_model.active()),
|
None => self.tab_properties(self.tab_model.active()),
|
||||||
|
|
||||||
Some(ContextItem::TabBar(entity)) => self.tab_properties(entity),
|
Some(ContextItem::TabBar(entity)) => self.tab_properties(entity),
|
||||||
|
|
||||||
Some(ContextItem::NavBar(item)) => {
|
Some(ContextItem::NavBar(item)) => {
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::with_capacity(1);
|
||||||
|
|
||||||
if let Some(location) = self.nav_model.data::<Location>(item) {
|
if let Some(location) = self.nav_model.data::<Location>(item) {
|
||||||
if let Location::Path(path) = location {
|
if let Location::Path(path) = location {
|
||||||
let parent = path.parent().unwrap_or(path);
|
//TODO: this should be done once, not when generating the view!
|
||||||
|
if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) {
|
||||||
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
|
children.push(item.property_view(IconSizes::default()));
|
||||||
if item.path_opt.as_deref() == Some(path) {
|
|
||||||
children.push(item.property_view(IconSizes::default()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
widget::settings::view_column(children).into()
|
widget::settings::view_column(children).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(ContextItem::BreadCrumbs(index)) => {
|
Some(ContextItem::BreadCrumbs(index)) => {
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::with_capacity(1);
|
||||||
|
|
||||||
if let Some(tab) = self.tab_model.active_data::<Tab>() {
|
if let Some(tab) = self.tab_model.active_data::<Tab>() {
|
||||||
let path = match tab.location {
|
let path_opt = tab
|
||||||
Location::Path(ref path) => Some(path),
|
.location
|
||||||
Location::Search(ref path, _) => Some(path),
|
.path_opt()
|
||||||
_ => None,
|
.and_then(|path| path.ancestors().nth(index))
|
||||||
}
|
.map(|path| path.to_path_buf());
|
||||||
.and_then(|path| path.ancestors().nth(index))
|
if let Some(ref path) = path_opt {
|
||||||
.map(|path| path.to_path_buf());
|
//TODO: this should be done once, not when generating the view!
|
||||||
if let Some(ref path) = path {
|
if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) {
|
||||||
let parent = path.parent().unwrap_or(path);
|
children.push(item.property_view(IconSizes::default()));
|
||||||
|
|
||||||
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
|
|
||||||
if item.path_opt.as_deref() == Some(path) {
|
|
||||||
children.push(item.property_view(IconSizes::default()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
widget::settings::view_column(children).into()
|
widget::settings::view_column(children).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1772,9 +1764,7 @@ impl Application for App {
|
||||||
//TODO: this could be further optimized by looking at what exactly changed
|
//TODO: this could be further optimized by looking at what exactly changed
|
||||||
if let Some(items) = &mut tab.items_opt {
|
if let Some(items) = &mut tab.items_opt {
|
||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
if item.path_opt.as_ref()
|
if item.path_opt() == Some(event_path) {
|
||||||
== Some(event_path)
|
|
||||||
{
|
|
||||||
//TODO: reload more, like mime types?
|
//TODO: reload more, like mime types?
|
||||||
match fs::metadata(&event_path) {
|
match fs::metadata(&event_path) {
|
||||||
Ok(new_metadata) => match &mut item
|
Ok(new_metadata) => match &mut item
|
||||||
|
|
@ -1836,7 +1826,7 @@ impl Application for App {
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(path) = &item.path_opt {
|
if let Some(Location::Path(path)) = &item.location_opt {
|
||||||
paths.push(path.clone());
|
paths.push(path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2036,7 +2026,7 @@ impl Application for App {
|
||||||
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(path) = &item.path_opt {
|
if let Some(Location::Path(path)) = &item.location_opt {
|
||||||
selected.push(path.clone());
|
selected.push(path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2398,11 +2388,14 @@ 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 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 || Location::Trash.scan(icon_sizes))
|
match tokio::task::spawn_blocking(move || {
|
||||||
.await
|
Location::Trash.scan(mounters, icon_sizes)
|
||||||
|
})
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
for path in &*recently_trashed {
|
for path in &*recently_trashed {
|
||||||
|
|
@ -3551,6 +3544,7 @@ pub(crate) mod test_utils {
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{IconSizes, TabConfig},
|
config::{IconSizes, TabConfig},
|
||||||
|
mounter::MounterMap,
|
||||||
tab::Item,
|
tab::Item,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -3713,7 +3707,7 @@ 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(IconSizes::default());
|
let items = location.scan(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);
|
||||||
|
|
||||||
|
|
@ -3748,7 +3742,7 @@ pub(crate) mod test_utils {
|
||||||
|
|
||||||
name == item.name
|
name == item.name
|
||||||
&& is_dir == item.metadata.is_dir()
|
&& is_dir == item.metadata.is_dir()
|
||||||
&& path == item.path_opt.as_ref().expect("item should have path")
|
&& path == item.path_opt().expect("item should have path")
|
||||||
&& is_hidden == item.hidden
|
&& is_hidden == item.hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3768,6 +3762,19 @@ pub(crate) mod test_utils {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assert_zoom_affects_item_size(tab: &mut Tab, message: tab::Message, should_zoom: bool) {
|
||||||
|
let grid_icon_size = tab.config.icon_sizes.grid;
|
||||||
|
let list_icon_size = tab.config.icon_sizes.list;
|
||||||
|
|
||||||
|
debug!("Emitting {:?}", message);
|
||||||
|
tab.update(message, Modifiers::empty());
|
||||||
|
|
||||||
|
let grid_size_changed = grid_icon_size != tab.config.icon_sizes.grid;
|
||||||
|
let list_size_changed = list_icon_size != tab.config.icon_sizes.list;
|
||||||
|
|
||||||
|
assert_eq!(grid_size_changed || list_size_changed, should_zoom);
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 Location::Path(ref tab_path) = tab.location else {
|
||||||
|
|
|
||||||
|
|
@ -376,10 +376,12 @@ struct App {
|
||||||
impl App {
|
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 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(icon_sizes)).await {
|
match tokio::task::spawn_blocking(move || location.scan(mounters, icon_sizes)).await
|
||||||
|
{
|
||||||
Ok(items) => message::app(Message::TabRescan(items)),
|
Ok(items) => message::app(Message::TabRescan(items)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to rescan: {}", err);
|
log::warn!("failed to rescan: {}", err);
|
||||||
|
|
@ -895,7 +897,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DialogPage::Replace { filename } => {
|
DialogPage::Replace { .. } => {
|
||||||
return self.update(Message::Save(true));
|
return self.update(Message::Save(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -999,7 +1001,7 @@ impl Application for App {
|
||||||
//TODO: this could be further optimized by looking at what exactly changed
|
//TODO: this could be further optimized by looking at what exactly changed
|
||||||
if let Some(items) = &mut self.tab.items_opt {
|
if let Some(items) = &mut self.tab.items_opt {
|
||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
if item.path_opt.as_ref() == Some(event_path) {
|
if item.path_opt() == Some(event_path) {
|
||||||
//TODO: reload more, like mime types?
|
//TODO: reload more, like mime types?
|
||||||
match fs::metadata(&event_path) {
|
match fs::metadata(&event_path) {
|
||||||
Ok(new_metadata) => {
|
Ok(new_metadata) => {
|
||||||
|
|
@ -1049,7 +1051,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(path) = &item.path_opt {
|
if let Some(Location::Path(path)) = &item.location_opt {
|
||||||
paths.push(path.clone());
|
paths.push(path.clone());
|
||||||
let _ = update_recently_used(
|
let _ = update_recently_used(
|
||||||
&path.clone(),
|
&path.clone(),
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ mod spawn_detached;
|
||||||
use tab::Location;
|
use tab::Location;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
|
||||||
|
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
||||||
|
err.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn home_dir() -> PathBuf {
|
pub fn home_dir() -> PathBuf {
|
||||||
match dirs::home_dir() {
|
match dirs::home_dir() {
|
||||||
Some(home) => home,
|
Some(home) => home,
|
||||||
|
|
|
||||||
18
src/menu.rs
18
src/menu.rs
|
|
@ -197,8 +197,22 @@ pub fn context_menu<'a>(
|
||||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(_, Location::Networks) => {
|
(_, Location::Network(_, _)) => {
|
||||||
//TODO: networks context menu?
|
if selected > 0 {
|
||||||
|
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||||
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if tab.mode.multiple() {
|
||||||
|
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||||
|
}
|
||||||
|
if !children.is_empty() {
|
||||||
|
children.push(divider::horizontal::light().into());
|
||||||
|
}
|
||||||
|
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-size"), HeadingOptions::Size));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(_, Location::Trash) => {
|
(_, Location::Trash) => {
|
||||||
if tab.mode.multiple() {
|
if tab.mode.multiple() {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,15 @@ use cosmic::{
|
||||||
widget, Command,
|
widget, Command,
|
||||||
};
|
};
|
||||||
use gio::{glib, prelude::*};
|
use gio::{glib, prelude::*};
|
||||||
use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc};
|
use std::{any::TypeId, cell::Cell, future::pending, path::PathBuf, sync::Arc};
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||||
|
use crate::{
|
||||||
|
config::IconSizes,
|
||||||
|
err_str,
|
||||||
|
tab::{self, ItemMetadata, ItemThumbnail, Location},
|
||||||
|
};
|
||||||
|
|
||||||
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||||
|
|
@ -21,10 +26,154 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
|
let file = gio::File::for_uri(uri);
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for info_res in file
|
||||||
|
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
|
||||||
|
.map_err(err_str)?
|
||||||
|
{
|
||||||
|
let info = info_res.map_err(err_str)?;
|
||||||
|
println!("{:?}", info.display_name());
|
||||||
|
for attribute in info.list_attributes(None) {
|
||||||
|
println!(
|
||||||
|
" {:?}: {:?}: {:?}",
|
||||||
|
attribute,
|
||||||
|
info.attribute_type(&attribute),
|
||||||
|
info.attribute_as_string(&attribute)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = info.name().to_string_lossy().to_string();
|
||||||
|
let display_name = info.display_name().to_string();
|
||||||
|
|
||||||
|
//TODO: what is the best way to resolve shortcuts?
|
||||||
|
let location = Location::Network(
|
||||||
|
if let Some(target_uri) = info.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)
|
||||||
|
{
|
||||||
|
target_uri.to_string()
|
||||||
|
} else {
|
||||||
|
file.child(info.name()).uri().to_string()
|
||||||
|
},
|
||||||
|
display_name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
//TODO: support dir or file
|
||||||
|
let metadata = ItemMetadata::SimpleDir { entries: 0 };
|
||||||
|
|
||||||
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
||||||
|
let file_icon = |size| {
|
||||||
|
info.icon()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|icon| gio_icon_to_path(icon, size))
|
||||||
|
.map(|path| widget::icon::from_path(path))
|
||||||
|
.unwrap_or(
|
||||||
|
widget::icon::from_name(if metadata.is_dir() {
|
||||||
|
"folder"
|
||||||
|
} else {
|
||||||
|
"text-x-generic"
|
||||||
|
})
|
||||||
|
.size(size)
|
||||||
|
.handle(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(
|
||||||
|
//TODO: get mime from content_type?
|
||||||
|
"inode/directory".parse().unwrap(),
|
||||||
|
file_icon(sizes.grid()),
|
||||||
|
file_icon(sizes.list()),
|
||||||
|
file_icon(sizes.list_condensed()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
items.push(tab::Item {
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
metadata,
|
||||||
|
hidden: false,
|
||||||
|
location_opt: Some(location),
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
|
||||||
|
let mount_op = gio::MountOperation::new();
|
||||||
|
mount_op.connect_ask_password(
|
||||||
|
move |mount_op, message, default_user, default_domain, flags| {
|
||||||
|
let auth = MounterAuth {
|
||||||
|
message: message.to_string(),
|
||||||
|
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
||||||
|
Some(default_user.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
||||||
|
Some(default_domain.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
||||||
|
Some(String::new())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||||
|
event_tx
|
||||||
|
.send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
|
||||||
|
.unwrap();
|
||||||
|
//TODO: async recv?
|
||||||
|
if let Some(auth) = auth_rx.blocking_recv() {
|
||||||
|
if auth.anonymous_opt == Some(true) {
|
||||||
|
mount_op.set_anonymous(true);
|
||||||
|
} else {
|
||||||
|
mount_op.set_username(auth.username_opt.as_deref());
|
||||||
|
mount_op.set_domain(auth.domain_opt.as_deref());
|
||||||
|
mount_op.set_password(auth.password_opt.as_deref());
|
||||||
|
if auth.remember_opt == Some(true) {
|
||||||
|
mount_op.set_password_save(gio::PasswordSave::Permanently);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mount_op.reply(gio::MountOperationResult::Handled);
|
||||||
|
} else {
|
||||||
|
mount_op.reply(gio::MountOperationResult::Aborted);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
mount_op
|
||||||
|
}
|
||||||
|
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
Rescan,
|
Rescan,
|
||||||
Mount(MounterItem),
|
Mount(MounterItem),
|
||||||
NetworkDrive(String),
|
NetworkDrive(String),
|
||||||
|
NetworkScan(
|
||||||
|
String,
|
||||||
|
IconSizes,
|
||||||
|
mpsc::Sender<Result<Vec<tab::Item>, String>>,
|
||||||
|
),
|
||||||
Unmount(MounterItem),
|
Unmount(MounterItem),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,62 +343,8 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Cmd::NetworkDrive(uri) => {
|
Cmd::NetworkDrive(uri) => {
|
||||||
let mount_op = gio::MountOperation::new();
|
|
||||||
|
|
||||||
{
|
|
||||||
let event_tx = event_tx.clone();
|
|
||||||
let uri = uri.clone();
|
|
||||||
mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| {
|
|
||||||
let auth = MounterAuth {
|
|
||||||
message: message.to_string(),
|
|
||||||
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
|
||||||
Some(default_user.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
|
||||||
Some(default_domain.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
|
||||||
Some(String::new())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
|
||||||
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap();
|
|
||||||
//TODO: async recv?
|
|
||||||
if let Some(auth) = auth_rx.blocking_recv() {
|
|
||||||
if auth.anonymous_opt == Some(true) {
|
|
||||||
mount_op.set_anonymous(true);
|
|
||||||
} else {
|
|
||||||
mount_op.set_username(auth.username_opt.as_deref());
|
|
||||||
mount_op.set_domain(auth.domain_opt.as_deref());
|
|
||||||
mount_op.set_password(auth.password_opt.as_deref());
|
|
||||||
if auth.remember_opt == Some(true) {
|
|
||||||
mount_op.set_password_save(gio::PasswordSave::Permanently);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mount_op.reply(gio::MountOperationResult::Handled);
|
|
||||||
} else {
|
|
||||||
mount_op.reply(gio::MountOperationResult::Aborted);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = gio::File::for_uri(&uri);
|
let file = gio::File::for_uri(&uri);
|
||||||
|
let mount_op = mount_op(uri.clone(), event_tx.clone());
|
||||||
let event_tx = event_tx.clone();
|
let event_tx = event_tx.clone();
|
||||||
file.mount_enclosing_volume(
|
file.mount_enclosing_volume(
|
||||||
gio::MountMountFlags::empty(),
|
gio::MountMountFlags::empty(),
|
||||||
|
|
@ -267,6 +362,40 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Cmd::NetworkScan(uri, sizes, items_tx) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if needs_mount {
|
||||||
|
let mount_op = mount_op(uri.clone(), event_tx.clone());
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
file.mount_enclosing_volume(
|
||||||
|
gio::MountMountFlags::empty(),
|
||||||
|
Some(&mount_op),
|
||||||
|
gio::Cancellable::NONE,
|
||||||
|
move |res| {
|
||||||
|
log::info!("network scan mounted {}: result {:?}", uri, res);
|
||||||
|
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();
|
||||||
|
event_tx.send(Event::NetworkResult(uri, match res {
|
||||||
|
Ok(()) => {
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
||||||
|
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||||
|
_ => Err(format!("{}", err))
|
||||||
|
}
|
||||||
|
})).unwrap();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items_tx.send(network_scan(&uri, sizes)).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
Cmd::Unmount(mounter_item) => {
|
Cmd::Unmount(mounter_item) => {
|
||||||
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
||||||
let ItemKind::Mount = item.kind else { continue };
|
let ItemKind::Mount = item.kind else { continue };
|
||||||
|
|
@ -330,6 +459,14 @@ impl Mounter for Gvfs {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>> {
|
||||||
|
let (items_tx, mut items_rx) = mpsc::channel(1);
|
||||||
|
self.command_tx
|
||||||
|
.send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx))
|
||||||
|
.unwrap();
|
||||||
|
items_rx.blocking_recv()
|
||||||
|
}
|
||||||
|
|
||||||
fn unmount(&self, item: MounterItem) -> Command<()> {
|
fn unmount(&self, item: MounterItem) -> Command<()> {
|
||||||
let command_tx = self.command_tx.clone();
|
let command_tx = self.command_tx.clone();
|
||||||
Command::perform(
|
Command::perform(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ use cosmic::{iced::subscription, widget, Command};
|
||||||
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
|
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::{config::IconSizes, tab};
|
||||||
|
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
mod gvfs;
|
mod gvfs;
|
||||||
|
|
||||||
|
|
@ -86,10 +88,11 @@ pub enum MounterMessage {
|
||||||
NetworkResult(String, Result<bool, String>),
|
NetworkResult(String, Result<bool, String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Mounter {
|
pub trait Mounter: Send + Sync {
|
||||||
//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<()>;
|
||||||
|
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;
|
||||||
fn unmount(&self, item: MounterItem) -> Command<()>;
|
fn unmount(&self, item: MounterItem) -> Command<()>;
|
||||||
fn subscription(&self) -> subscription::Subscription<MounterMessage>;
|
fn subscription(&self) -> subscription::Subscription<MounterMessage>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,14 @@ use cosmic::{
|
||||||
iced_core::{
|
iced_core::{
|
||||||
border::Border,
|
border::Border,
|
||||||
event::{self, Event},
|
event::{self, Event},
|
||||||
|
keyboard::{
|
||||||
|
self,
|
||||||
|
key::{self, Key},
|
||||||
|
Event::{KeyPressed, KeyReleased},
|
||||||
|
Modifiers,
|
||||||
|
},
|
||||||
layout,
|
layout,
|
||||||
mouse::{self, click},
|
mouse::{self, click, Event as MouseEvent},
|
||||||
overlay,
|
overlay,
|
||||||
renderer::{self, Quad, Renderer as _},
|
renderer::{self, Quad, Renderer as _},
|
||||||
touch,
|
touch,
|
||||||
|
|
@ -39,6 +45,7 @@ pub struct MouseArea<'a, Message> {
|
||||||
on_back_release: 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_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||||
on_forward_release: 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>>,
|
||||||
show_drag_rect: bool,
|
show_drag_rect: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,6 +151,16 @@ impl<'a, Message> MouseArea<'a, Message> {
|
||||||
self
|
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 {
|
||||||
|
self.on_scroll = Some(Box::new(message));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
|
pub fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
|
||||||
self.show_drag_rect = show_drag_rect;
|
self.show_drag_rect = show_drag_rect;
|
||||||
|
|
@ -163,7 +180,7 @@ impl<'a, Message> MouseArea<'a, Message> {
|
||||||
struct State {
|
struct State {
|
||||||
// TODO: Support on_mouse_enter and on_mouse_exit
|
// TODO: Support on_mouse_enter and on_mouse_exit
|
||||||
drag_initiated: Option<Point>,
|
drag_initiated: Option<Point>,
|
||||||
|
modifiers: Modifiers,
|
||||||
prev_click: Option<(mouse::Click, Instant)>,
|
prev_click: Option<(mouse::Click, Instant)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,6 +244,7 @@ impl<'a, Message> MouseArea<'a, Message> {
|
||||||
on_back_release: None,
|
on_back_release: None,
|
||||||
on_forward_press: None,
|
on_forward_press: None,
|
||||||
on_forward_release: None,
|
on_forward_release: None,
|
||||||
|
on_scroll: None,
|
||||||
show_drag_rect: false,
|
show_drag_rect: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -578,6 +596,21 @@ fn update<Message: Clone>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(message) = widget.on_scroll.as_ref() {
|
||||||
|
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
|
||||||
|
if let Some(on_scroll) = widget.on_scroll.as_ref() {
|
||||||
|
if let Some(message) = on_scroll(delta.clone(), state.modifiers) {
|
||||||
|
shell.publish(message);
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Event::Keyboard(key_event) = event {
|
||||||
|
handle_key_event(key_event, state)
|
||||||
|
};
|
||||||
|
|
||||||
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
|
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
|
||||||
shell.publish(message(drag_rect.intersection(&layout_bounds).map(
|
shell.publish(message(drag_rect.intersection(&layout_bounds).map(
|
||||||
|mut rect| {
|
|mut rect| {
|
||||||
|
|
@ -590,3 +623,53 @@ fn update<Message: Clone>(
|
||||||
|
|
||||||
event::Status::Ignored
|
event::Status::Ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(key_event: &keyboard::Event, state: &mut State) {
|
||||||
|
if let KeyPressed {
|
||||||
|
key: Key::Named(key::Named::Control),
|
||||||
|
..
|
||||||
|
} = key_event
|
||||||
|
{
|
||||||
|
state.modifiers.insert(Modifiers::CTRL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyReleased {
|
||||||
|
key: Key::Named(key::Named::Control),
|
||||||
|
..
|
||||||
|
} = key_event
|
||||||
|
{
|
||||||
|
state.modifiers.remove(Modifiers::CTRL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyPressed {
|
||||||
|
key: Key::Named(key::Named::Shift),
|
||||||
|
..
|
||||||
|
} = key_event
|
||||||
|
{
|
||||||
|
state.modifiers.insert(Modifiers::SHIFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyReleased {
|
||||||
|
key: Key::Named(key::Named::Shift),
|
||||||
|
..
|
||||||
|
} = key_event
|
||||||
|
{
|
||||||
|
state.modifiers.remove(Modifiers::SHIFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyPressed {
|
||||||
|
key: Key::Named(key::Named::Alt),
|
||||||
|
..
|
||||||
|
} = key_event
|
||||||
|
{
|
||||||
|
state.modifiers.insert(Modifiers::ALT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyReleased {
|
||||||
|
key: Key::Named(key::Named::Alt),
|
||||||
|
..
|
||||||
|
} = key_event
|
||||||
|
{
|
||||||
|
state.modifiers.remove(Modifiers::ALT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,11 @@ use walkdir::WalkDir;
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{ArchiveType, DialogPage, Message},
|
app::{ArchiveType, DialogPage, Message},
|
||||||
config::IconSizes,
|
config::IconSizes,
|
||||||
fl,
|
err_str, fl,
|
||||||
mime_icon::mime_for_path,
|
mime_icon::mime_for_path,
|
||||||
tab,
|
tab,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn err_str<T: ToString>(err: T) -> String {
|
|
||||||
err.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_replace(
|
fn handle_replace(
|
||||||
msg_tx: &Arc<Mutex<Sender<Message>>>,
|
msg_tx: &Arc<Mutex<Sender<Message>>>,
|
||||||
file_from: PathBuf,
|
file_from: PathBuf,
|
||||||
|
|
|
||||||
405
src/tab.rs
405
src/tab.rs
|
|
@ -7,8 +7,9 @@ use cosmic::{
|
||||||
},
|
},
|
||||||
alignment::{Horizontal, Vertical},
|
alignment::{Horizontal, Vertical},
|
||||||
clipboard::dnd::DndAction,
|
clipboard::dnd::DndAction,
|
||||||
|
event,
|
||||||
futures::SinkExt,
|
futures::SinkExt,
|
||||||
keyboard::Modifiers,
|
keyboard::{self, Modifiers},
|
||||||
subscription::{self, Subscription},
|
subscription::{self, Subscription},
|
||||||
//TODO: export in cosmic::widget
|
//TODO: export in cosmic::widget
|
||||||
widget::{
|
widget::{
|
||||||
|
|
@ -24,7 +25,7 @@ use cosmic::{
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Size,
|
Size,
|
||||||
},
|
},
|
||||||
iced_core::widget::tree,
|
iced_core::{mouse::ScrollDelta, widget::tree},
|
||||||
iced_style::rule,
|
iced_style::rule,
|
||||||
theme,
|
theme,
|
||||||
widget::{
|
widget::{
|
||||||
|
|
@ -44,7 +45,7 @@ use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt,
|
fmt::{self, Display},
|
||||||
fs::{self, Metadata},
|
fs::{self, Metadata},
|
||||||
num::NonZeroU16,
|
num::NonZeroU16,
|
||||||
os::unix::fs::MetadataExt,
|
os::unix::fs::MetadataExt,
|
||||||
|
|
@ -63,6 +64,7 @@ use crate::{
|
||||||
menu,
|
menu,
|
||||||
mime_app::{mime_apps, MimeApp},
|
mime_app::{mime_apps, MimeApp},
|
||||||
mime_icon::{mime_for_path, mime_icon},
|
mime_icon::{mime_for_path, mime_icon},
|
||||||
|
mounter::Mounters,
|
||||||
mouse_area,
|
mouse_area,
|
||||||
};
|
};
|
||||||
use unix_permissions_ext::UNIXPermissionsExt;
|
use unix_permissions_ext::UNIXPermissionsExt;
|
||||||
|
|
@ -72,7 +74,8 @@ pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
|
||||||
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
||||||
|
|
||||||
//TODO: adjust for locales?
|
//TODO: adjust for locales?
|
||||||
const TIME_FORMAT: &'static str = "%a %-d %b %-Y %r";
|
const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p";
|
||||||
|
const TIME_FORMAT: &'static str = "%-I:%M %p";
|
||||||
static SPECIAL_DIRS: Lazy<HashMap<PathBuf, &'static str>> = Lazy::new(|| {
|
static SPECIAL_DIRS: Lazy<HashMap<PathBuf, &'static str>> = Lazy::new(|| {
|
||||||
let mut special_dirs = HashMap::new();
|
let mut special_dirs = HashMap::new();
|
||||||
if let Some(dir) = dirs::document_dir() {
|
if let Some(dir) = dirs::document_dir() {
|
||||||
|
|
@ -260,6 +263,31 @@ fn format_permissions(metadata: &Metadata, owner: PermissionOwner) -> String {
|
||||||
perms.join(" ")
|
perms.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FormatTime(std::time::SystemTime);
|
||||||
|
|
||||||
|
impl Display for FormatTime {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let date_time = chrono::DateTime::<chrono::Local>::from(self.0);
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
if date_time.date() == now.date() {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}, {}",
|
||||||
|
fl!("today"),
|
||||||
|
date_time.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
date_time
|
||||||
|
.format_localized(DATE_TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time(time: std::time::SystemTime) -> FormatTime {
|
||||||
|
FormatTime(time)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
fn hidden_attribute(_metadata: &Metadata) -> bool {
|
fn hidden_attribute(_metadata: &Metadata) -> bool {
|
||||||
false
|
false
|
||||||
|
|
@ -377,7 +405,7 @@ pub fn item_from_entry(
|
||||||
display_name,
|
display_name,
|
||||||
metadata: ItemMetadata::Path { metadata, children },
|
metadata: ItemMetadata::Path { metadata, children },
|
||||||
hidden,
|
hidden,
|
||||||
path_opt: Some(path),
|
location_opt: Some(Location::Path(path)),
|
||||||
mime,
|
mime,
|
||||||
icon_handle_grid,
|
icon_handle_grid,
|
||||||
icon_handle_list,
|
icon_handle_list,
|
||||||
|
|
@ -535,7 +563,7 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item
|
||||||
items.par_sort_unstable_by(|a, b| {
|
items.par_sort_unstable_by(|a, b| {
|
||||||
let get_modified = |x: &Item| match &x.metadata {
|
let get_modified = |x: &Item| match &x.metadata {
|
||||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
||||||
ItemMetadata::Trash { .. } => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort with latest modified first
|
// Sort with latest modified first
|
||||||
|
|
@ -621,7 +649,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||||
display_name,
|
display_name,
|
||||||
metadata: ItemMetadata::Trash { metadata, entry },
|
metadata: ItemMetadata::Trash { metadata, entry },
|
||||||
hidden: false,
|
hidden: false,
|
||||||
path_opt: None,
|
location_opt: None,
|
||||||
mime,
|
mime,
|
||||||
icon_handle_grid,
|
icon_handle_grid,
|
||||||
icon_handle_list,
|
icon_handle_list,
|
||||||
|
|
@ -724,9 +752,17 @@ pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
||||||
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_networks(sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
|
||||||
//TODO: network folder items
|
for (key, mounter) in mounters.iter() {
|
||||||
vec![]
|
match mounter.network_scan(uri, sizes) {
|
||||||
|
Some(Ok(items)) => return items,
|
||||||
|
Some(Err(err)) => {
|
||||||
|
log::warn!("failed to scan {:?}: {}", uri, err);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -735,7 +771,7 @@ pub enum Location {
|
||||||
Search(PathBuf, String),
|
Search(PathBuf, String),
|
||||||
Trash,
|
Trash,
|
||||||
Recents,
|
Recents,
|
||||||
Networks,
|
Network(String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Location {
|
impl std::fmt::Display for Location {
|
||||||
|
|
@ -745,19 +781,27 @@ impl std::fmt::Display for Location {
|
||||||
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::Recents => write!(f, "recents"),
|
||||||
Self::Networks => write!(f, "networks"),
|
Self::Network(uri, _) => write!(f, "{}", uri),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Location {
|
impl Location {
|
||||||
pub fn scan(&self, sizes: IconSizes) -> Vec<Item> {
|
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||||
|
match self {
|
||||||
|
Self::Path(path) => Some(&path),
|
||||||
|
Self::Search(path, _) => Some(&path),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(&self, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
|
||||||
match self {
|
match self {
|
||||||
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),
|
||||||
Self::Recents => scan_recents(sizes),
|
Self::Recents => scan_recents(sizes),
|
||||||
Self::Networks => scan_networks(sizes),
|
Self::Network(uri, _) => scan_network(uri, mounters, sizes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -847,6 +891,12 @@ pub enum ItemMetadata {
|
||||||
metadata: trash::TrashItemMetadata,
|
metadata: trash::TrashItemMetadata,
|
||||||
entry: trash::TrashItem,
|
entry: trash::TrashItem,
|
||||||
},
|
},
|
||||||
|
SimpleDir {
|
||||||
|
entries: u64,
|
||||||
|
},
|
||||||
|
SimpleFile {
|
||||||
|
size: u64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemMetadata {
|
impl ItemMetadata {
|
||||||
|
|
@ -857,6 +907,8 @@ impl ItemMetadata {
|
||||||
trash::TrashItemSize::Entries(_) => true,
|
trash::TrashItemSize::Entries(_) => true,
|
||||||
trash::TrashItemSize::Bytes(_) => false,
|
trash::TrashItemSize::Bytes(_) => false,
|
||||||
},
|
},
|
||||||
|
Self::SimpleDir { .. } => true,
|
||||||
|
Self::SimpleFile { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -874,7 +926,7 @@ pub struct Item {
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub metadata: ItemMetadata,
|
pub metadata: ItemMetadata,
|
||||||
pub hidden: bool,
|
pub hidden: bool,
|
||||||
pub path_opt: Option<PathBuf>,
|
pub location_opt: Option<Location>,
|
||||||
pub mime: Mime,
|
pub mime: Mime,
|
||||||
pub icon_handle_grid: widget::icon::Handle,
|
pub icon_handle_grid: widget::icon::Handle,
|
||||||
pub icon_handle_list: widget::icon::Handle,
|
pub icon_handle_list: widget::icon::Handle,
|
||||||
|
|
@ -894,6 +946,10 @@ impl Item {
|
||||||
name.replace(".", ".\u{200B}").replace("_", "_\u{200B}")
|
name.replace(".", ".\u{200B}").replace("_", "_\u{200B}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||||
|
self.location_opt.as_ref()?.path_opt()
|
||||||
|
}
|
||||||
|
|
||||||
fn preview(&self, sizes: IconSizes) -> Element<'static, app::Message> {
|
fn preview(&self, sizes: IconSizes) -> Element<'static, app::Message> {
|
||||||
// This loads the image only if thumbnailing worked
|
// This loads the image only if thumbnailing worked
|
||||||
let icon = widget::icon::icon(self.icon_handle_grid.clone())
|
let icon = widget::icon::icon(self.icon_handle_grid.clone())
|
||||||
|
|
@ -907,7 +963,7 @@ impl Item {
|
||||||
{
|
{
|
||||||
ItemThumbnail::NotImage => icon,
|
ItemThumbnail::NotImage => icon,
|
||||||
ItemThumbnail::Rgba(_) => {
|
ItemThumbnail::Rgba(_) => {
|
||||||
if let Some(path) = &self.path_opt {
|
if let Some(Location::Path(path)) = &self.location_opt {
|
||||||
widget::image::viewer(widget::image::Handle::from_path(path))
|
widget::image::viewer(widget::image::Handle::from_path(path))
|
||||||
.min_scale(1.0)
|
.min_scale(1.0)
|
||||||
.into()
|
.into()
|
||||||
|
|
@ -916,7 +972,7 @@ impl Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemThumbnail::Svg => {
|
ItemThumbnail::Svg => {
|
||||||
if let Some(path) = &self.path_opt {
|
if let Some(Location::Path(path)) = &self.location_opt {
|
||||||
widget::Svg::from_path(path).into()
|
widget::Svg::from_path(path).into()
|
||||||
} else {
|
} else {
|
||||||
icon
|
icon
|
||||||
|
|
@ -944,7 +1000,7 @@ impl Item {
|
||||||
|
|
||||||
column = column.push(widget::text(format!("Type: {}", self.mime)));
|
column = column.push(widget::text(format!("Type: {}", self.mime)));
|
||||||
|
|
||||||
if let Some(path) = &self.path_opt {
|
if let Some(Location::Path(path)) = &self.location_opt {
|
||||||
for app in self.open_with.iter() {
|
for app in self.open_with.iter() {
|
||||||
column = column.push(
|
column = column.push(
|
||||||
widget::button(
|
widget::button(
|
||||||
|
|
@ -998,27 +1054,15 @@ impl Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(time) = metadata.created() {
|
if let Ok(time) = metadata.created() {
|
||||||
column = column.push(widget::text(format!(
|
column = column.push(widget::text(format!("Created: {}", format_time(time))));
|
||||||
"Created: {}",
|
|
||||||
chrono::DateTime::<chrono::Local>::from(time)
|
|
||||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(time) = metadata.modified() {
|
if let Ok(time) = metadata.modified() {
|
||||||
column = column.push(widget::text(format!(
|
column = column.push(widget::text(format!("Modified: {}", format_time(time))));
|
||||||
"Modified: {}",
|
|
||||||
chrono::DateTime::<chrono::Local>::from(time)
|
|
||||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(time) = metadata.accessed() {
|
if let Ok(time) = metadata.accessed() {
|
||||||
column = column.push(widget::text(format!(
|
column = column.push(widget::text(format!("Accessed: {}", format_time(time))));
|
||||||
"Accessed: {}",
|
|
||||||
chrono::DateTime::<chrono::Local>::from(time)
|
|
||||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
|
|
@ -1061,8 +1105,8 @@ impl Item {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemMetadata::Trash { .. } => {
|
_ => {
|
||||||
//TODO: trash metadata
|
//TODO: other metadata types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1097,13 +1141,12 @@ impl Item {
|
||||||
if let Ok(time) = metadata.modified() {
|
if let Ok(time) = metadata.modified() {
|
||||||
column = column.push(widget::text(format!(
|
column = column.push(widget::text(format!(
|
||||||
"Last modified: {}",
|
"Last modified: {}",
|
||||||
chrono::DateTime::<chrono::Local>::from(time)
|
format_time(time)
|
||||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemMetadata::Trash { .. } => {
|
_ => {
|
||||||
//TODO: trash metadata
|
//TODO: other metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1253,9 +1296,7 @@ impl Tab {
|
||||||
Location::Recents => {
|
Location::Recents => {
|
||||||
fl!("recents")
|
fl!("recents")
|
||||||
}
|
}
|
||||||
Location::Networks => {
|
Location::Network(_uri, display_name) => display_name.clone(),
|
||||||
fl!("networks")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1309,10 +1350,11 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_path(&mut self, path: PathBuf) {
|
pub fn select_path(&mut self, path: PathBuf) {
|
||||||
|
let location = Location::Path(path);
|
||||||
*self.cached_selected.borrow_mut() = None;
|
*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.path_opt.as_ref() == Some(&path);
|
item.selected = item.location_opt.as_ref() == Some(&location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1566,14 +1608,18 @@ impl Tab {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i)))
|
.and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i)))
|
||||||
{
|
{
|
||||||
if let Some(path) = &clicked_item.path_opt {
|
if let Some(location) = &clicked_item.location_opt {
|
||||||
if clicked_item.metadata.is_dir() {
|
if clicked_item.metadata.is_dir() {
|
||||||
cd = Some(Location::Path(path.clone()));
|
cd = Some(location.clone());
|
||||||
} else {
|
} else {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
if let Location::Path(path) = location {
|
||||||
|
commands.push(Command::OpenFile(path.clone()));
|
||||||
|
} else {
|
||||||
|
log::warn!("no path for item {:?}", clicked_item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("no path for item {:?}", clicked_item);
|
log::warn!("no location for item {:?}", clicked_item);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("no item for click index {:?}", click_i_opt);
|
log::warn!("no item for click index {:?}", click_i_opt);
|
||||||
|
|
@ -1964,12 +2010,14 @@ impl Tab {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(path) = &item.path_opt {
|
if let Some(location) = &item.location_opt {
|
||||||
if path.is_dir() {
|
if item.metadata.is_dir() {
|
||||||
//TODO: allow opening multiple tabs?
|
//TODO: allow opening multiple tabs?
|
||||||
cd = Some(Location::Path(path.clone()));
|
cd = Some(location.clone());
|
||||||
} else {
|
} else {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
if let Location::Path(path) = location {
|
||||||
|
commands.push(Command::OpenFile(path.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//TODO: open properties?
|
//TODO: open properties?
|
||||||
|
|
@ -2004,7 +2052,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(path) = &clicked_item.path_opt {
|
if let Some(Location::Path(path)) = &clicked_item.location_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()))
|
||||||
|
|
@ -2035,8 +2083,9 @@ impl Tab {
|
||||||
}
|
}
|
||||||
Message::Thumbnail(path, thumbnail) => {
|
Message::Thumbnail(path, thumbnail) => {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
let location = Location::Path(path);
|
||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
if item.path_opt.as_ref() == Some(&path) {
|
if item.location_opt.as_ref() == Some(&location) {
|
||||||
if let ItemThumbnail::Rgba(rgba) = &thumbnail {
|
if let ItemThumbnail::Rgba(rgba) = &thumbnail {
|
||||||
//TODO: pass handles already generated to avoid blocking main thread
|
//TODO: pass handles already generated to avoid blocking main thread
|
||||||
let handle = widget::icon::from_raster_pixels(
|
let handle = widget::icon::from_raster_pixels(
|
||||||
|
|
@ -2085,20 +2134,11 @@ impl Tab {
|
||||||
}
|
}
|
||||||
commands.push(Command::DropFiles(to, from))
|
commands.push(Command::DropFiles(to, from))
|
||||||
}
|
}
|
||||||
Location::Search(_, _) => {
|
|
||||||
log::warn!(" Copy/cut to search not supported.");
|
|
||||||
}
|
|
||||||
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
|
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
|
||||||
commands.push(Command::MoveToTrash(from.paths))
|
commands.push(Command::MoveToTrash(from.paths))
|
||||||
}
|
}
|
||||||
Location::Trash => {
|
_ => {
|
||||||
log::warn!("Copy to trash is not supported.");
|
log::warn!("{:?} to {:?} is not supported.", from.kind, to);
|
||||||
}
|
|
||||||
Location::Recents => {
|
|
||||||
log::warn!("Copy to recents is not supported.");
|
|
||||||
}
|
|
||||||
Location::Networks => {
|
|
||||||
log::warn!("Copy to networks is not supported.");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -2227,6 +2267,8 @@ impl Tab {
|
||||||
trash::TrashItemSize::Entries(entries) => (true, entries as u64),
|
trash::TrashItemSize::Entries(entries) => (true, entries as u64),
|
||||||
trash::TrashItemSize::Bytes(bytes) => (false, bytes),
|
trash::TrashItemSize::Bytes(bytes) => (false, bytes),
|
||||||
},
|
},
|
||||||
|
ItemMetadata::SimpleDir { entries } => (true, *entries),
|
||||||
|
ItemMetadata::SimpleFile { size } => (false, *size),
|
||||||
};
|
};
|
||||||
let (a_is_entry, a_size) = get_size(a.1);
|
let (a_is_entry, a_size) = get_size(a.1);
|
||||||
let (b_is_entry, b_size) = get_size(b.1);
|
let (b_is_entry, b_size) = get_size(b.1);
|
||||||
|
|
@ -2260,7 +2302,7 @@ impl Tab {
|
||||||
items.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
let get_modified = |x: &Item| match &x.metadata {
|
let get_modified = |x: &Item| match &x.metadata {
|
||||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
||||||
ItemMetadata::Trash { .. } => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let a_modified = get_modified(a.1);
|
let a_modified = get_modified(a.1);
|
||||||
|
|
@ -2561,11 +2603,14 @@ impl Tab {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Location::Networks => {
|
Location::Network(uri, display_name) => {
|
||||||
children.push(
|
children.push(
|
||||||
widget::button(widget::text::heading(fl!("networks")))
|
widget::button(widget::text::heading(display_name))
|
||||||
.padding(space_xxxs)
|
.padding(space_xxxs)
|
||||||
.on_press(Message::Location(Location::Networks))
|
.on_press(Message::Location(Location::Network(
|
||||||
|
uri.clone(),
|
||||||
|
display_name.clone(),
|
||||||
|
)))
|
||||||
.style(theme::Button::Text)
|
.style(theme::Button::Text)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
@ -2765,9 +2810,10 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let column: Element<Message> = if item.metadata.is_dir() && item.path_opt.is_some()
|
let column: Element<Message> = if item.metadata.is_dir()
|
||||||
|
&& item.location_opt.is_some()
|
||||||
{
|
{
|
||||||
let tab_location = Location::Path(item.path_opt.clone().unwrap());
|
let tab_location = item.location_opt.clone().unwrap();
|
||||||
let tab_location_enter = tab_location.clone();
|
let tab_location_enter = tab_location.clone();
|
||||||
let tab_location_leave = tab_location.clone();
|
let tab_location_leave = tab_location.clone();
|
||||||
let is_dnd_hovered =
|
let is_dnd_hovered =
|
||||||
|
|
@ -3036,18 +3082,21 @@ impl Tab {
|
||||||
|
|
||||||
let modified_text = match &item.metadata {
|
let modified_text = match &item.metadata {
|
||||||
ItemMetadata::Path { metadata, .. } => match metadata.modified() {
|
ItemMetadata::Path { metadata, .. } => match metadata.modified() {
|
||||||
Ok(time) => chrono::DateTime::<chrono::Local>::from(time)
|
Ok(time) => format_time(time).to_string(),
|
||||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
|
||||||
.to_string(),
|
|
||||||
Err(_) => String::new(),
|
Err(_) => String::new(),
|
||||||
},
|
},
|
||||||
ItemMetadata::Trash { .. } => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let size_text = match &item.metadata {
|
let size_text = match &item.metadata {
|
||||||
ItemMetadata::Path { metadata, children } => {
|
ItemMetadata::Path { metadata, children } => {
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
format!("{} items", children)
|
//TODO: translate
|
||||||
|
if *children == 1 {
|
||||||
|
format!("{} item", children)
|
||||||
|
} else {
|
||||||
|
format!("{} items", children)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
format_size(metadata.len())
|
format_size(metadata.len())
|
||||||
}
|
}
|
||||||
|
|
@ -3063,6 +3112,15 @@ impl Tab {
|
||||||
}
|
}
|
||||||
trash::TrashItemSize::Bytes(bytes) => format_size(bytes),
|
trash::TrashItemSize::Bytes(bytes) => format_size(bytes),
|
||||||
},
|
},
|
||||||
|
ItemMetadata::SimpleDir { entries } => {
|
||||||
|
//TODO: translate
|
||||||
|
if *entries == 1 {
|
||||||
|
format!("{} item", entries)
|
||||||
|
} else {
|
||||||
|
format!("{} items", entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ItemMetadata::SimpleFile { size } => format_size(*size),
|
||||||
};
|
};
|
||||||
|
|
||||||
let row = if condensed {
|
let row = if condensed {
|
||||||
|
|
@ -3126,58 +3184,59 @@ impl Tab {
|
||||||
};
|
};
|
||||||
|
|
||||||
let button_row = button(row.into());
|
let button_row = button(row.into());
|
||||||
let button_row: Element<_> = if item.metadata.is_dir() && item.path_opt.is_some() {
|
let button_row: Element<_> =
|
||||||
let tab_location = Location::Path(item.path_opt.clone().unwrap());
|
if item.metadata.is_dir() && item.location_opt.is_some() {
|
||||||
let tab_location_enter = tab_location.clone();
|
let tab_location = item.location_opt.clone().unwrap();
|
||||||
let tab_location_leave = tab_location.clone();
|
let tab_location_enter = tab_location.clone();
|
||||||
let is_dnd_hovered =
|
let tab_location_leave = tab_location.clone();
|
||||||
self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location);
|
let is_dnd_hovered =
|
||||||
cosmic::widget::container(
|
self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location);
|
||||||
DndDestination::for_data(button_row, move |data, action| {
|
cosmic::widget::container(
|
||||||
if let Some(mut data) = data {
|
DndDestination::for_data(button_row, move |data, action| {
|
||||||
if action == DndAction::Copy {
|
if let Some(mut data) = data {
|
||||||
Message::Drop(Some((tab_location.clone(), data)))
|
if action == DndAction::Copy {
|
||||||
} else if action == DndAction::Move {
|
Message::Drop(Some((tab_location.clone(), data)))
|
||||||
data.kind = ClipboardKind::Cut;
|
} else if action == DndAction::Move {
|
||||||
Message::Drop(Some((tab_location.clone(), data)))
|
data.kind = ClipboardKind::Cut;
|
||||||
|
Message::Drop(Some((tab_location.clone(), data)))
|
||||||
|
} else {
|
||||||
|
log::warn!("unsupported action: {:?}", action);
|
||||||
|
Message::Drop(None)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("unsupported action: {:?}", action);
|
log::warn!("No data for drop.");
|
||||||
Message::Drop(None)
|
Message::Drop(None)
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
log::warn!("No data for drop.");
|
.on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone()))
|
||||||
Message::Drop(None)
|
.on_leave(move || Message::DndLeave(tab_location_leave.clone())),
|
||||||
}
|
)
|
||||||
})
|
// todo refactor into the dnd destination wrapper
|
||||||
.on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone()))
|
.style(if is_dnd_hovered {
|
||||||
.on_leave(move || Message::DndLeave(tab_location_leave.clone())),
|
theme::Container::custom(|t| {
|
||||||
)
|
let mut a = cosmic::iced_style::container::StyleSheet::appearance(
|
||||||
// todo refactor into the dnd destination wrapper
|
t,
|
||||||
.style(if is_dnd_hovered {
|
&theme::Container::default(),
|
||||||
theme::Container::custom(|t| {
|
);
|
||||||
let mut a = cosmic::iced_style::container::StyleSheet::appearance(
|
let t = t.cosmic();
|
||||||
t,
|
// todo use theme drop target color
|
||||||
&theme::Container::default(),
|
let mut bg = t.accent_color();
|
||||||
);
|
bg.alpha = 0.2;
|
||||||
let t = t.cosmic();
|
a.background = Some(Color::from(bg).into());
|
||||||
// todo use theme drop target color
|
a.border = Border {
|
||||||
let mut bg = t.accent_color();
|
color: t.accent_color().into(),
|
||||||
bg.alpha = 0.2;
|
width: 1.0,
|
||||||
a.background = Some(Color::from(bg).into());
|
radius: t.radius_s().into(),
|
||||||
a.border = Border {
|
};
|
||||||
color: t.accent_color().into(),
|
a
|
||||||
width: 1.0,
|
})
|
||||||
radius: t.radius_s().into(),
|
} else {
|
||||||
};
|
theme::Container::default()
|
||||||
a
|
|
||||||
})
|
})
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
theme::Container::default()
|
button_row.into()
|
||||||
})
|
};
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
button_row.into()
|
|
||||||
};
|
|
||||||
|
|
||||||
if item.selected || !drag_items.is_empty() {
|
if item.selected || !drag_items.is_empty() {
|
||||||
let dnd_row = if !item.selected {
|
let dnd_row = if !item.selected {
|
||||||
|
|
@ -3306,8 +3365,8 @@ impl Tab {
|
||||||
items
|
items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|item| item.selected)
|
.filter(|item| item.selected)
|
||||||
.filter_map(|item| item.path_opt.clone())
|
.filter_map(|item| item.path_opt().map(|x| x.clone()))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<PathBuf>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let item_view = DndSource::<_, cosmic::app::Message<app::Message>, ClipboardCopy>::with_id(
|
let item_view = DndSource::<_, cosmic::app::Message<app::Message>, ClipboardCopy>::with_id(
|
||||||
|
|
@ -3336,7 +3395,8 @@ impl Tab {
|
||||||
.on_press(move |_point_opt| Message::Click(None))
|
.on_press(move |_point_opt| Message::Click(None))
|
||||||
.on_release(|_| Message::ClickRelease(None))
|
.on_release(|_| Message::ClickRelease(None))
|
||||||
.on_back_press(move |_point_opt| Message::GoPrevious)
|
.on_back_press(move |_point_opt| Message::GoPrevious)
|
||||||
.on_forward_press(move |_point_opt| Message::GoNext);
|
.on_forward_press(move |_point_opt| Message::GoNext)
|
||||||
|
.on_scroll(respond_to_scroll_direction);
|
||||||
|
|
||||||
if self.context_menu.is_some() {
|
if self.context_menu.is_some() {
|
||||||
mouse_area = mouse_area.on_right_press(move |_point_opt| Message::ContextMenu(None));
|
mouse_area = mouse_area.on_right_press(move |_point_opt| Message::ContextMenu(None));
|
||||||
|
|
@ -3344,6 +3404,7 @@ impl Tab {
|
||||||
mouse_area = mouse_area.on_right_press(Message::ContextMenu);
|
mouse_area = mouse_area.on_right_press(Message::ContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let should_propogate_events = true;
|
||||||
let mut popover = widget::popover(mouse_area);
|
let mut popover = widget::popover(mouse_area);
|
||||||
|
|
||||||
if let Some(point) = self.context_menu {
|
if let Some(point) = self.context_menu {
|
||||||
|
|
@ -3383,7 +3444,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Location::Networks => {
|
Location::Network(uri, display_name) if uri == "network:///" => {
|
||||||
tab_column = tab_column.push(
|
tab_column = tab_column.push(
|
||||||
widget::layer_container(widget::row::with_children(vec![
|
widget::layer_container(widget::row::with_children(vec![
|
||||||
widget::horizontal_space(Length::Fill).into(),
|
widget::horizontal_space(Length::Fill).into(),
|
||||||
|
|
@ -3485,7 +3546,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(path) = item.path_opt.clone() {
|
if let Some(Location::Path(path)) = item.location_opt.clone() {
|
||||||
subscriptions.push(subscription::channel(
|
subscriptions.push(subscription::channel(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
1,
|
1,
|
||||||
|
|
@ -3549,20 +3610,42 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: Modifiers) -> Option<Message> {
|
||||||
|
if !modifiers.control() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta_y = match delta {
|
||||||
|
ScrollDelta::Lines { y, .. } => y,
|
||||||
|
ScrollDelta::Pixels { y, .. } => y,
|
||||||
|
};
|
||||||
|
|
||||||
|
if delta_y > 0.0 {
|
||||||
|
return Some(Message::ZoomIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if delta_y < 0.0 {
|
||||||
|
return Some(Message::ZoomOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs, io, path::PathBuf};
|
use std::{fs, io, path::PathBuf};
|
||||||
|
|
||||||
use cosmic::iced_runtime::keyboard::Modifiers;
|
use cosmic::{iced::mouse::ScrollDelta, iced_runtime::keyboard::Modifiers};
|
||||||
use log::{debug, trace};
|
use log::{debug, trace};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use test_log::test;
|
use test_log::test;
|
||||||
|
|
||||||
use super::{scan_path, Location, Message, Tab};
|
use super::{respond_to_scroll_direction, scan_path, Location, Message, Tab};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::test_utils::{
|
app::test_utils::{
|
||||||
assert_eq_tab_path, empty_fs, eq_path_item, filter_dirs, read_dir_sorted, simple_fs,
|
assert_eq_tab_path, assert_zoom_affects_item_size, empty_fs, eq_path_item, filter_dirs,
|
||||||
tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED,
|
read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN,
|
||||||
|
NUM_NESTED,
|
||||||
},
|
},
|
||||||
config::{IconSizes, TabConfig},
|
config::{IconSizes, TabConfig},
|
||||||
};
|
};
|
||||||
|
|
@ -3795,6 +3878,64 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_zoom_in_increases_item_view_size() -> io::Result<()> {
|
||||||
|
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
||||||
|
let path = fs.path();
|
||||||
|
|
||||||
|
let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default());
|
||||||
|
|
||||||
|
let should_affect_size = true;
|
||||||
|
assert_zoom_affects_item_size(&mut tab, Message::ZoomIn, should_affect_size);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_zoom_out_decreases_item_view_size() -> io::Result<()> {
|
||||||
|
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
||||||
|
let path = fs.path();
|
||||||
|
|
||||||
|
let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default());
|
||||||
|
|
||||||
|
let should_affect_size = true;
|
||||||
|
assert_zoom_affects_item_size(&mut tab, Message::ZoomOut, should_affect_size);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> {
|
||||||
|
let message_maybe =
|
||||||
|
respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::CTRL);
|
||||||
|
assert!(!message_maybe.is_none());
|
||||||
|
assert!(matches!(message_maybe.unwrap(), Message::ZoomIn));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> {
|
||||||
|
let message_maybe =
|
||||||
|
respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::empty());
|
||||||
|
assert!(message_maybe.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> {
|
||||||
|
let message_maybe =
|
||||||
|
respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::CTRL);
|
||||||
|
assert!(!message_maybe.is_none());
|
||||||
|
assert!(matches!(message_maybe.unwrap(), Message::ZoomOut));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> {
|
||||||
|
let message_maybe = respond_to_scroll_direction(
|
||||||
|
ScrollDelta::Pixels { x: 0.0, y: -1.0 },
|
||||||
|
Modifiers::empty(),
|
||||||
|
);
|
||||||
|
assert!(message_maybe.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> {
|
fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> {
|
||||||
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
||||||
|
|
@ -3942,7 +4083,7 @@ impl<M> Widget<M, cosmic::Theme, cosmic::Renderer> for ArcElementWrapper<M> {
|
||||||
_clipboard: &mut dyn cosmic::iced_core::Clipboard,
|
_clipboard: &mut dyn cosmic::iced_core::Clipboard,
|
||||||
_shell: &mut cosmic::iced_core::Shell<'_, M>,
|
_shell: &mut cosmic::iced_core::Shell<'_, M>,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) -> cosmic::iced_core::event::Status {
|
) -> event::Status {
|
||||||
self.0.lock().unwrap().as_widget_mut().on_event(
|
self.0.lock().unwrap().as_widget_mut().on_event(
|
||||||
_state, _event, _layout, _cursor, _renderer, _clipboard, _shell, _viewport,
|
_state, _event, _layout, _cursor, _renderer, _clipboard, _shell, _viewport,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue