Compare commits
23 commits
pre-1.8-me
...
local/libc
| Author | SHA1 | Date | |
|---|---|---|---|
| 20842026d7 | |||
| 4b5fcfe5d7 | |||
|
|
d1f0af78e6 | ||
|
|
87f407e3ed | ||
|
|
e2cad979ce | ||
|
|
b326e48cf6 | ||
|
|
c0b8a8c60a | ||
|
|
fe71aca0e5 | ||
|
|
d6e7618b76 | ||
|
|
c576bbcd60 | ||
|
|
762fc8fb48 | ||
|
|
7ba55f66cc | ||
|
|
b7005660f1 | ||
|
|
77f062647c | ||
|
|
00233428b3 | ||
| 98159d317c | |||
| e8eb42b507 | |||
|
|
089ad2b417 | ||
|
|
1761b9b84b | ||
|
|
06278ee7f1 | ||
|
|
a412787d69 | ||
|
|
f3b8f6f585 | ||
|
|
de739425cb |
36 changed files with 1146 additions and 1122 deletions
668
Cargo.lock
generated
668
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-files"
|
||||
version = "1.7.0"
|
||||
version = "1.9.0"
|
||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
|
|
@ -11,16 +11,16 @@ anyhow = "1"
|
|||
jiff = "0.2"
|
||||
jiff-icu = "0.2"
|
||||
icu = { version = "2.2.0", features = ["compiled_data"] }
|
||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "32283d7", optional = true }
|
||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c0cff4d", optional = true }
|
||||
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
|
||||
dirs = "6.0.0"
|
||||
gio = { version = "0.21", optional = true }
|
||||
glib = { version = "0.21", optional = true }
|
||||
glob = "0.3"
|
||||
ignore = "0.4"
|
||||
image = "0.25"
|
||||
image.workspace = true
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
log.workspace = true
|
||||
mime_guess = "2"
|
||||
notify-debouncer-full = "0.7"
|
||||
notify-rust = { version = "4", optional = true }
|
||||
|
|
@ -166,4 +166,8 @@ mime = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch =
|
|||
cosmic-text = { path = "../cosmic-text" }
|
||||
|
||||
[workspace]
|
||||
members = ["cosmic-files-applet"]
|
||||
members = ["cosmic-files-applet", "cosmic-files-thumbnailer"]
|
||||
|
||||
[workspace.dependencies]
|
||||
image = "0.25"
|
||||
log = "0.4"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "cosmic-files-applet"
|
||||
version = "1.7.0"
|
||||
version = "1.9.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
log.workspace = true
|
||||
zbus = "4" # Blocking connection in zbus 5 hangs
|
||||
|
||||
[dependencies.cosmic-files]
|
||||
|
|
|
|||
8
cosmic-files-thumbnailer/Cargo.toml
Normal file
8
cosmic-files-thumbnailer/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "cosmic-files-thumbnailer"
|
||||
version = "1.9.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
image.workspace = true
|
||||
pelite = "0.10"
|
||||
254
cosmic-files-thumbnailer/src/main.rs
Normal file
254
cosmic-files-thumbnailer/src/main.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||
use std::error::Error;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
thumbnail_from_args(std::env::args_os().skip(1))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn thumbnail_from_args(mut args: impl Iterator<Item = OsString>) -> Result<(), Box<dyn Error>> {
|
||||
let usage = || {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"usage: cosmic-files-thumbnailer OUTPUT --size SIZE INPUT",
|
||||
)
|
||||
};
|
||||
let output = args.next().ok_or_else(usage)?;
|
||||
if args.next().as_deref() != Some(OsStr::new("--size")) {
|
||||
return Err(usage().into());
|
||||
}
|
||||
let size = args
|
||||
.next()
|
||||
.and_then(|arg| arg.into_string().ok())
|
||||
.and_then(|arg| arg.parse::<u32>().ok())
|
||||
.filter(|size| *size > 0)
|
||||
.ok_or_else(usage)?;
|
||||
let input = args.next().ok_or_else(usage)?;
|
||||
if args.next().is_some() {
|
||||
return Err(usage().into());
|
||||
}
|
||||
|
||||
thumbnail(Path::new(&input), Path::new(&output), size)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn thumbnail(input: &Path, output: &Path, size: u32) -> Result<(), Box<dyn Error>> {
|
||||
let icon = thumbnail_image(input, size)?;
|
||||
DynamicImage::ImageRgba8(icon).save_with_format(output, ImageFormat::Png)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn thumbnail_image(input: &Path, size: u32) -> Result<RgbaImage, Box<dyn Error>> {
|
||||
if size == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"thumbnail size must be positive",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Map the PE so only the headers and resource pages touched by pelite are read.
|
||||
let data = pelite::FileMap::open(input)?;
|
||||
let bytes = data.as_ref();
|
||||
if bytes.len() < 64 || !bytes.starts_with(b"MZ") {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "input is not a PE file").into());
|
||||
}
|
||||
|
||||
let file = pelite::PeFile::from_bytes(bytes).map_err(invalid_data)?;
|
||||
let resources = file.resources().map_err(invalid_data)?;
|
||||
let (_name, group) = resources
|
||||
.icons()
|
||||
.next()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "PE file has no icon group"))?
|
||||
.map_err(invalid_data)?;
|
||||
|
||||
let mut ico = Vec::new();
|
||||
group.write(&mut ico).map_err(invalid_data)?;
|
||||
|
||||
Ok(
|
||||
DynamicImage::ImageRgba8(largest_icon(&ico).map_err(invalid_data)?)
|
||||
.thumbnail(size, size)
|
||||
.into_rgba8(),
|
||||
)
|
||||
}
|
||||
|
||||
fn invalid_data(error: impl std::fmt::Display) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, error.to_string())
|
||||
}
|
||||
|
||||
fn largest_icon(ico: &[u8]) -> Result<RgbaImage, String> {
|
||||
if ico.len() < 6 {
|
||||
return Err("ICO header is truncated".to_string());
|
||||
}
|
||||
let count = usize::from(u16::from_le_bytes([ico[4], ico[5]]));
|
||||
let mut best: Option<(u64, RgbaImage)> = None;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for i in 0..count.min(32) {
|
||||
let entry = 6 + i * 16;
|
||||
if ico.len() < entry + 16 {
|
||||
break;
|
||||
}
|
||||
let size = u32::from_le_bytes([
|
||||
ico[entry + 8],
|
||||
ico[entry + 9],
|
||||
ico[entry + 10],
|
||||
ico[entry + 11],
|
||||
]) as usize;
|
||||
let offset = u32::from_le_bytes([
|
||||
ico[entry + 12],
|
||||
ico[entry + 13],
|
||||
ico[entry + 14],
|
||||
ico[entry + 15],
|
||||
]) as usize;
|
||||
let Some(end) = offset.checked_add(size) else {
|
||||
continue;
|
||||
};
|
||||
let Some(data) = ico.get(offset..end) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match decode_icon(&ico[entry..entry + 16], data) {
|
||||
Ok(image) => {
|
||||
let pixels = u64::from(image.width()) * u64::from(image.height());
|
||||
if best
|
||||
.as_ref()
|
||||
.is_none_or(|(best_pixels, _)| pixels > *best_pixels)
|
||||
{
|
||||
best = Some((pixels, image));
|
||||
}
|
||||
}
|
||||
Err(err) => errors.push(format!("frame {i}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
best.map(|(_, image)| image).ok_or_else(|| {
|
||||
if errors.is_empty() {
|
||||
"PE icon contains no complete frames".to_string()
|
||||
} else {
|
||||
format!("PE icon could not be decoded ({})", errors.join("; "))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_icon(entry: &[u8], data: &[u8]) -> Result<RgbaImage, String> {
|
||||
if data.starts_with(b"\x89PNG\r\n\x1a\n") {
|
||||
return image::ImageReader::with_format(io::Cursor::new(data), ImageFormat::Png)
|
||||
.decode()
|
||||
.map(DynamicImage::into_rgba8)
|
||||
.map_err(|err| err.to_string());
|
||||
}
|
||||
|
||||
let capacity = 22usize
|
||||
.checked_add(data.len())
|
||||
.ok_or_else(|| "frame is too large".to_string())?;
|
||||
let data_len = u32::try_from(data.len()).map_err(|_| "frame is too large".to_string())?;
|
||||
let mut ico = Vec::with_capacity(capacity);
|
||||
ico.extend_from_slice(&[0, 0, 1, 0, 1, 0]);
|
||||
ico.extend_from_slice(entry);
|
||||
ico[14..18].copy_from_slice(&data_len.to_le_bytes());
|
||||
ico[18..22].copy_from_slice(&22u32.to_le_bytes());
|
||||
ico.extend_from_slice(data);
|
||||
|
||||
image::ImageReader::with_format(io::Cursor::new(ico), ImageFormat::Ico)
|
||||
.decode()
|
||||
.map(DynamicImage::into_rgba8)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{largest_icon, thumbnail_from_args};
|
||||
use image::{DynamicImage, ImageFormat, Rgb, RgbImage, Rgba, RgbaImage};
|
||||
use std::ffi::OsString;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn malformed_thumbnail_request_returns_an_error() {
|
||||
let args = [OsString::from("output.png")];
|
||||
assert!(thumbnail_from_args(args.into_iter()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn largest_icon_skips_invalid_entries() {
|
||||
let image = RgbaImage::from_pixel(2, 2, Rgba([1, 2, 3, 255]));
|
||||
let mut png = Cursor::new(Vec::new());
|
||||
DynamicImage::ImageRgba8(image)
|
||||
.write_to(&mut png, ImageFormat::Png)
|
||||
.unwrap();
|
||||
let png = png.into_inner();
|
||||
|
||||
let mut ico = vec![0, 0, 1, 0, 2, 0];
|
||||
let mut invalid_entry = [0; 16];
|
||||
invalid_entry[8..12].copy_from_slice(&4u32.to_le_bytes());
|
||||
invalid_entry[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
ico.extend_from_slice(&invalid_entry);
|
||||
|
||||
let mut valid_entry = [0; 16];
|
||||
valid_entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 32, 0]);
|
||||
valid_entry[8..12].copy_from_slice(&u32::try_from(png.len()).unwrap().to_le_bytes());
|
||||
valid_entry[12..16].copy_from_slice(&38u32.to_le_bytes());
|
||||
ico.extend_from_slice(&valid_entry);
|
||||
ico.extend_from_slice(&png);
|
||||
|
||||
let decoded = largest_icon(&ico).expect("valid icon after malformed entry");
|
||||
assert_eq!(decoded.dimensions(), (2, 2));
|
||||
assert_eq!(decoded.get_pixel(0, 0), &Rgba([1, 2, 3, 255]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dib_and_mask_is_applied() {
|
||||
let mut dib = vec![0; 40];
|
||||
dib[0..4].copy_from_slice(&40u32.to_le_bytes());
|
||||
dib[4..8].copy_from_slice(&2i32.to_le_bytes());
|
||||
dib[8..12].copy_from_slice(&4i32.to_le_bytes());
|
||||
dib[12..14].copy_from_slice(&1u16.to_le_bytes());
|
||||
dib[14..16].copy_from_slice(&24u16.to_le_bytes());
|
||||
|
||||
// Two bottom-up BGR rows, padded to four-byte boundaries.
|
||||
dib.extend_from_slice(&[0, 0, 255, 0, 0, 255, 0, 0]);
|
||||
dib.extend_from_slice(&[0, 0, 255, 0, 0, 255, 0, 0]);
|
||||
// The first mask row is the bottom image row; its first pixel is transparent.
|
||||
dib.extend_from_slice(&[0b1000_0000, 0, 0, 0]);
|
||||
dib.extend_from_slice(&[0, 0, 0, 0]);
|
||||
|
||||
let mut ico = vec![0, 0, 1, 0, 1, 0];
|
||||
let mut entry = [0; 16];
|
||||
entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 24, 0]);
|
||||
entry[8..12].copy_from_slice(&u32::try_from(dib.len()).unwrap().to_le_bytes());
|
||||
entry[12..16].copy_from_slice(&22u32.to_le_bytes());
|
||||
ico.extend_from_slice(&entry);
|
||||
ico.extend_from_slice(&dib);
|
||||
|
||||
let decoded = largest_icon(&ico).expect("valid DIB icon");
|
||||
assert_eq!(decoded.get_pixel(0, 0), &Rgba([255, 0, 0, 255]));
|
||||
assert_eq!(decoded.get_pixel(0, 1), &Rgba([255, 0, 0, 0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_png_icon_is_decoded() {
|
||||
let image = RgbImage::from_pixel(2, 2, Rgb([1, 2, 3]));
|
||||
let mut png = Cursor::new(Vec::new());
|
||||
DynamicImage::ImageRgb8(image)
|
||||
.write_to(&mut png, ImageFormat::Png)
|
||||
.unwrap();
|
||||
let png = png.into_inner();
|
||||
|
||||
let mut ico = vec![0, 0, 1, 0, 1, 0];
|
||||
let mut entry = [0; 16];
|
||||
entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 24, 0]);
|
||||
entry[8..12].copy_from_slice(&u32::try_from(png.len()).unwrap().to_le_bytes());
|
||||
entry[12..16].copy_from_slice(&22u32.to_le_bytes());
|
||||
ico.extend_from_slice(&entry);
|
||||
ico.extend_from_slice(&png);
|
||||
|
||||
let decoded = largest_icon(&ico).expect("valid RGB PNG icon");
|
||||
assert_eq!(decoded.get_pixel(0, 0), &Rgba([1, 2, 3, 255]));
|
||||
}
|
||||
}
|
||||
12
debian/changelog
vendored
12
debian/changelog
vendored
|
|
@ -1,3 +1,15 @@
|
|||
cosmic-files (1.9.0) noble; urgency=medium
|
||||
|
||||
* Epoch 1.9.0 version update
|
||||
|
||||
-- Michael Murphy <michael@mmurphy.dev> Mon, 14 Sep 2026 20:35:19 +0200
|
||||
|
||||
cosmic-files (1.8.0) noble; urgency=medium
|
||||
|
||||
* Epoch 1.8.0 version update
|
||||
|
||||
-- Jeremy Soller <jeremy@system76.com> Wed, 02 Sep 2026 08:14:53 -0600
|
||||
|
||||
cosmic-files (1.7.0) noble; urgency=medium
|
||||
|
||||
* Epoch 1.7.0 version update
|
||||
|
|
|
|||
6
debian/source/options
vendored
6
debian/source/options
vendored
|
|
@ -1,4 +1,6 @@
|
|||
tar-ignore=.github
|
||||
tar-ignore=.vscode
|
||||
tar-ignore=target/.rustc_info.json
|
||||
tar-ignore=target/CACHEDIR.TAG
|
||||
tar-ignore=target/debug
|
||||
tar-ignore=target/release
|
||||
tar-ignore=vendor
|
||||
tar-ignore=target
|
||||
|
|
@ -431,3 +431,6 @@ rename-confirm = Přejmenovat
|
|||
checksum = Kontrolní součet { $kind }
|
||||
calculate = Vypočítat
|
||||
error = Chyba
|
||||
search-application = Hledat podle názvu aplikace
|
||||
sidebar-label = Popisek v postranním panelu
|
||||
change-sidebar-label = Změnit popisek
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ new-folder = Neuer Ordner ...
|
|||
open-in-terminal = Im Terminal öffnen
|
||||
move-to-trash = In den Papierkorb verschieben
|
||||
restore-from-trash = Aus dem Papierkorb wiederherstellen
|
||||
remove-from-sidebar = Von der Seitenleiste entfernen
|
||||
remove-from-sidebar = Aus der Seitenleiste entfernen
|
||||
sort-by-name = Nach Name sortieren
|
||||
sort-by-modified = Nach Änderung sortieren
|
||||
sort-by-size = Nach Größe sortieren
|
||||
|
|
@ -441,3 +441,5 @@ rename-confirm = Umbenennen
|
|||
checksum = { $kind } Prüfsumme
|
||||
calculate = Berechnen
|
||||
error = Fehler
|
||||
sidebar-label = Bezeichnung in der Seitenleiste
|
||||
search-application = Nach App-Name suchen
|
||||
|
|
|
|||
|
|
@ -331,3 +331,6 @@ rename-confirm = Μετονομασία
|
|||
checksum = Άθροισμα ελέγχου { $kind }
|
||||
calculate = Υπολογισμός
|
||||
error = Σφάλμα
|
||||
sidebar-label = Ετικέτα πλαϊνής στήλης
|
||||
search-application = Αναζήτηση με όνομα εφαρμογής
|
||||
change-sidebar-label = Αλλαγή ετικέτας πλαϊνής στήλης
|
||||
|
|
|
|||
|
|
@ -418,3 +418,6 @@ rename-confirm = Nimeä uudelleen
|
|||
checksum = { $kind }-tarkistussumma
|
||||
calculate = Laske
|
||||
error = Virhe
|
||||
search-application = Hae sovelluksen nimellä
|
||||
sidebar-label = Sivupalkin nimike
|
||||
change-sidebar-label = Muuta sivupalkin nimikettä
|
||||
|
|
|
|||
0
i18n/fil/cosmic_files.ftl
Normal file
0
i18n/fil/cosmic_files.ftl
Normal file
|
|
@ -456,3 +456,6 @@ rename-confirm = Renommer
|
|||
calculate = Calculer
|
||||
error = Erreur
|
||||
checksum = Somme de contrôle { $kind }
|
||||
sidebar-label = Libellé de la barre latérale
|
||||
search-application = Chercher par nom d'appli
|
||||
change-sidebar-label = Changer le libellé de la barre latérale
|
||||
|
|
|
|||
|
|
@ -446,6 +446,9 @@ context-action-confirm-warning =
|
|||
} fog lefutni.
|
||||
run = Futtatás
|
||||
rename-confirm = Átnevezés
|
||||
checksum = { $kind } ellenőrzőösszeg
|
||||
checksum = { $kind }-ellenőrzőösszeg
|
||||
calculate = Számítás
|
||||
error = Hiba
|
||||
sidebar-label = Oldalsávcímke
|
||||
search-application = Keresés alkalmazásnév alapján
|
||||
change-sidebar-label = Oldalsávcímke módosítása
|
||||
|
|
|
|||
|
|
@ -331,3 +331,6 @@ rename-confirm = Ganti nama
|
|||
checksum = { $kind } checksum
|
||||
calculate = Hitung
|
||||
error = Galat
|
||||
sidebar-label = Label bilah sisi
|
||||
search-application = Cari berdasarkan nama aplikasi
|
||||
change-sidebar-label = Ubah label bilah sisi
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ name = Isem
|
|||
modified = Ittusnifel
|
||||
trashed-on = Yettwakkes ɣer tqecwalt n yiḍumman
|
||||
size = Tiddi
|
||||
details = Talqayt
|
||||
details = Ifatusen
|
||||
pause = Serǧu
|
||||
resume = Kemmel
|
||||
create-archive = Snulfu-d aɣbaṛ
|
||||
|
|
@ -40,7 +40,7 @@ file-already-exists = Afaylu s yisem-agi yella yakan
|
|||
folder-already-exists = Akaram s yisem-agi yella yakan
|
||||
name-hidden = Ismawen ibeddun s "." ad ttwaffren
|
||||
name-invalid = Isem ur yezmir ara ad yili "{ $filename }"
|
||||
cancel = Sefsex
|
||||
cancel = Semmet
|
||||
create = Snulfu-d
|
||||
open = Ldi
|
||||
open-file = Ldi afaylu
|
||||
|
|
@ -72,7 +72,7 @@ comment = Amsefrak n yifuyla i tnarit COSMIC
|
|||
keywords = Akaram;Amsefrak;
|
||||
delete = kkes
|
||||
replace = Semselsi
|
||||
support = Tallalt
|
||||
support = Tallelt
|
||||
settings = Iɣewwaṛen
|
||||
appearance = Timeẓri
|
||||
theme = Asentel
|
||||
|
|
@ -86,10 +86,10 @@ zoom-out = Asemẓi
|
|||
view = Wali
|
||||
menu-settings = Iɣewwaṛen…
|
||||
save = Sekles
|
||||
match-desktop = Amṣada d tnarit
|
||||
match-desktop = Semṣada akked tnarit
|
||||
file = Afaylu
|
||||
new-window = Asfaylu amaynut
|
||||
quit = Tuffɣa
|
||||
quit = Ffeɣ
|
||||
edit = Ẓreg
|
||||
cut = Gzem
|
||||
copy = Nɣel
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ list-directories-first = Pêşî rêgehan lîste bike
|
|||
folder-name = Navê peldankê
|
||||
browse-store = { $store } bigere
|
||||
remove-from-recents = Ji dawiyê rake
|
||||
paste = Pêve bike
|
||||
paste = Pê ve bike
|
||||
empty-trash-title = Jêbirdankê vala bike?
|
||||
menu-settings = Sazkarî...
|
||||
moving =
|
||||
|
|
@ -325,9 +325,12 @@ replace = Biguhêrîne
|
|||
replace-title = "{ $filename }" jixwe di vê cihê de heye
|
||||
unknown-folder = Peldanka nenas
|
||||
write-only = Tenê nivîsandin
|
||||
trashed-on = Li Jêbirdankê hatiye avêtin
|
||||
trashed-on = Jêbirî
|
||||
mixed = Tevlihev
|
||||
type-to-search-select = Pel an peldanka yekem a lihevhatî hildibijêre
|
||||
calculate = Bijmêre
|
||||
error = Şaşetî
|
||||
checksum = Tevahiya { $kind } kontrol bike
|
||||
sidebar-label = Nîşanka benda kêlekê
|
||||
search-application = Li gorî navê sepanê bigere
|
||||
change-sidebar-label = Nîşanka benda kêlekê biguhêrîne
|
||||
|
|
|
|||
|
|
@ -1,6 +1,33 @@
|
|||
appearance = Aparéncia
|
||||
theme = Tèma
|
||||
match-desktop = Assortir al burèu
|
||||
dark = Fosc
|
||||
dark = Escur
|
||||
light = Clar
|
||||
file = Fichièr
|
||||
details = Detalhs
|
||||
name = Nom
|
||||
cancel = Anullar
|
||||
repository = Depaus
|
||||
settings = Configuracion
|
||||
menu-settings = Configuracion...
|
||||
quit = Quitar
|
||||
cosmic-files = Fichièrs de COSMIC
|
||||
comment = Gestionari de fichièrs pel burèu COSMIC
|
||||
save-file = Enregistrar lo fichièr
|
||||
open-with-title = Cossí volètz dobrir « { $name } » ?
|
||||
other-apps = Autras aplicacions
|
||||
context-action-confirm-title = Executar « { $name } » ?
|
||||
run = Executar
|
||||
delete = Suprimir
|
||||
replace = Remplaçar
|
||||
owner = Propietari
|
||||
group = Grop
|
||||
write-execute = Escritura e execucion
|
||||
read-only = Lectura sola
|
||||
read-execute = Lectura e execucion
|
||||
read-write = Lectura e execucion
|
||||
read-write-execute = Lectura, escritura e execucion
|
||||
remove = Suprimir
|
||||
connecting = Connexion...
|
||||
domain = Domeni
|
||||
password = Senhal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
cosmic-files = Pliki COSMIC
|
||||
comment = Menedżer plików pulpitu COSMIC
|
||||
comment = Menedżer plików dla środowiska COSMIC
|
||||
keywords = Katalogi;Pliki;Menedżer;
|
||||
empty-folder = Pusty katalog
|
||||
empty-folder-hidden = Pusty katalog (z ukrytymi plikami)
|
||||
|
|
@ -450,3 +450,6 @@ rename-confirm = Zmień nazwę
|
|||
checksum = { $kind } suma kontrolna
|
||||
calculate = Oblicz
|
||||
error = Błąd
|
||||
sidebar-label = Etykieta panelu bocznego
|
||||
search-application = Wyszukaj po nazwie aplikacji
|
||||
change-sidebar-label = Zmień etykietę panelu bocznego
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ rename-folder = Renomear pasta
|
|||
|
||||
## Change Sidebar Label Dialog
|
||||
|
||||
sidebar-label = Rótulo da barra lateral
|
||||
sidebar-label = Legenda da barra lateral
|
||||
|
||||
## Replace Dialog
|
||||
|
||||
|
|
@ -369,7 +369,7 @@ open-in-terminal = Abrir no terminal
|
|||
move-to-trash = Mover para a lixeira
|
||||
restore-from-trash = Restaurar da lixeira
|
||||
remove-from-sidebar = Remover da barra lateral
|
||||
change-sidebar-label = Alterar rótulo na barra lateral
|
||||
change-sidebar-label = Alterar legenda da barra lateral
|
||||
sort-by-name = Ordenar por nome
|
||||
sort-by-modified = Ordenar por data de modificação
|
||||
sort-by-size = Ordenar por tamanho
|
||||
|
|
@ -454,3 +454,4 @@ rename-confirm = Renomear
|
|||
checksum = Soma de verificação { $kind }
|
||||
calculate = Calcular
|
||||
error = Erro
|
||||
search-application = Pesquisar por nome de aplicativo
|
||||
|
|
|
|||
|
|
@ -383,14 +383,16 @@ clear-recents-history = Очистить историю недавних
|
|||
copy-path = Копировать путь
|
||||
mixed = Смешанные
|
||||
context-action = Контекстная команда
|
||||
context-action-confirm-title = Выполнить «{ $name }»?
|
||||
context-action-confirm-title = Запустить «{ $name }»?
|
||||
context-action-confirm-warning =
|
||||
Команда затронет { $items } { $items ->
|
||||
[one] элемент
|
||||
*[other] элем.
|
||||
}.
|
||||
run = Выполнить
|
||||
run = Запустить
|
||||
rename-confirm = Переименовать
|
||||
checksum = Хеш-сумма { $kind }
|
||||
calculate = Вычислить
|
||||
error = Ошибка
|
||||
sidebar-label = Подпись в боковой панели
|
||||
change-sidebar-label = Изменить подпись
|
||||
|
|
|
|||
|
|
@ -422,3 +422,6 @@ rename-confirm = Byt namn
|
|||
checksum = { $kind }-kontrollsumma
|
||||
calculate = Beräkna
|
||||
error = Fel
|
||||
sidebar-label = Sidofältsetikett
|
||||
search-application = Sök på programnamn
|
||||
change-sidebar-label = Ändra sidofältsetikett
|
||||
|
|
|
|||
|
|
@ -202,17 +202,9 @@ moved =
|
|||
*[other] ไฟล์
|
||||
} จาก "{ $from }" สู่ "{ $to }"
|
||||
renaming = กำลังเปลี่ยนชื่อจาก "{ $from }" เป็น "{ $to }"
|
||||
renamed = Renamed "{ $from }" to "{ $to }"
|
||||
restoring =
|
||||
Restoring { $items } { $items ->
|
||||
[one] item
|
||||
*[other] items
|
||||
} from { trash } ({ $progress })...
|
||||
restored =
|
||||
Restored { $items } { $items ->
|
||||
[one] item
|
||||
*[other] items
|
||||
} from { trash }
|
||||
renamed = เปลี่ยนชื่อจาก "{ $from }" เป็น "{ $to }" แล้ว
|
||||
restoring = กำลังกู้คืน { $items } รายการจาก { trash } ({ $progress })...
|
||||
restored = กู้คืน { $items } รายการจาก { trash } แล้ว
|
||||
unknown-folder = แฟ้มที่ไม่รู้จัก
|
||||
|
||||
## Open with
|
||||
|
|
@ -240,8 +232,8 @@ settings = การตั้งค่า
|
|||
appearance = ลักษณะ
|
||||
theme = ธีม
|
||||
match-desktop = ใช้ตามธีมหน้าจอหลัก
|
||||
dark = ธีมมืด
|
||||
light = ธีมสว่าง
|
||||
dark = มืด
|
||||
light = สว่าง
|
||||
# Context menu
|
||||
add-to-sidebar = เพิ่มเข้าแถบด้านข้าง
|
||||
compress = บีบอัด
|
||||
|
|
@ -306,3 +298,4 @@ sort-newest-first = ไฟล์ใหม่ก่อน
|
|||
sort-oldest-first = ไฟล์เก่าก่อน
|
||||
sort-smallest-to-largest = ขนาดเล็กก่อน
|
||||
sort-largest-to-smallest = ขนาดใหญ่ก่อน
|
||||
remove = ลบ
|
||||
|
|
|
|||
|
|
@ -392,3 +392,6 @@ mixed = Змішано
|
|||
checksum = контрольна сума { $kind }
|
||||
calculate = Вирахувати
|
||||
error = Помилка
|
||||
sidebar-label = Назва бічної панелі
|
||||
search-application = Пошук за назвою застосунку
|
||||
change-sidebar-label = Змінити назву бічної панелі
|
||||
|
|
|
|||
|
|
@ -449,3 +449,6 @@ rename-confirm = 重命名
|
|||
calculate = 计算
|
||||
error = 错误
|
||||
checksum = { $kind } 校验和
|
||||
sidebar-label = 侧边栏标签
|
||||
search-application = 使用应用名称搜索
|
||||
change-sidebar-label = 更改侧边栏标签
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ name-no-slashes = 名稱不能包含斜線
|
|||
## Open/Save Dialog
|
||||
|
||||
cancel = 取消
|
||||
create = 建立
|
||||
create = 創作
|
||||
open = 開啟
|
||||
open-file = 開啟檔案
|
||||
open-folder = 開啟資料夾
|
||||
|
|
@ -396,3 +396,6 @@ rename-confirm = 重新命名
|
|||
checksum = { $kind } 核對和
|
||||
calculate = 計算
|
||||
error = 錯誤
|
||||
sidebar-label = 側邊欄標籤
|
||||
search-application = 依應用程式名稱搜尋
|
||||
change-sidebar-label = 變更側邊欄標籤
|
||||
|
|
|
|||
17
justfile
17
justfile
|
|
@ -12,6 +12,10 @@ cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
|
|||
bin-src := cargo-target-dir / 'release' / name
|
||||
bin-dst := base-dir / 'bin' / name
|
||||
|
||||
thumbnailer-bin-name := name + '-thumbnailer'
|
||||
thumbnailer-bin-src := cargo-target-dir / 'release' / thumbnailer-bin-name
|
||||
thumbnailer-bin-dst := base-dir / 'bin' / thumbnailer-bin-name
|
||||
|
||||
applet-name := name + '-applet'
|
||||
applet-src := cargo-target-dir / 'release' / applet-name
|
||||
applet-dst := base-dir / 'bin' / applet-name
|
||||
|
|
@ -24,6 +28,10 @@ metainfo := APPID + '.metainfo.xml'
|
|||
metainfo-src := 'target/xdgen' / metainfo
|
||||
metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo
|
||||
|
||||
thumbnailer := APPID + '.thumbnailer'
|
||||
thumbnailer-src := 'res' / thumbnailer
|
||||
thumbnailer-dst := clean(rootdir / prefix) / 'share' / 'thumbnailers' / thumbnailer
|
||||
|
||||
icons-src := 'res' / 'icons' / 'hicolor'
|
||||
icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor'
|
||||
|
||||
|
|
@ -45,14 +53,11 @@ clean-dist: clean clean-vendor
|
|||
build-debug *args:
|
||||
cargo build {{args}}
|
||||
cargo build --package {{applet-name}} {{args}}
|
||||
cargo build --package {{thumbnailer-bin-name}} {{args}}
|
||||
|
||||
# Compiles with release profile
|
||||
build-release *args: (build-debug '--release' args)
|
||||
|
||||
# Compiles applet with release profile
|
||||
build-release-applet *args:
|
||||
cargo build --package {{applet-name}} --release {{args}}
|
||||
|
||||
# Compiles release profile with vendored dependencies
|
||||
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
||||
|
||||
|
|
@ -92,9 +97,11 @@ heaptrack *args:
|
|||
# Installs files
|
||||
install:
|
||||
install -Dm0755 {{bin-src}} {{bin-dst}}
|
||||
install -Dm0755 {{thumbnailer-bin-src}} {{thumbnailer-bin-dst}}
|
||||
install -Dm0755 {{applet-src}} {{applet-dst}}
|
||||
install -Dm0644 {{desktop-src}} {{desktop-dst}}
|
||||
install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
|
||||
install -Dm0644 {{thumbnailer-src}} {{thumbnailer-dst}}
|
||||
for size in `ls {{icons-src}}`; do \
|
||||
install -Dm0644 "{{icons-src}}/$size/apps/{{APPID}}.svg" "{{icons-dst}}/$size/apps/{{APPID}}.svg"; \
|
||||
done
|
||||
|
|
@ -105,7 +112,7 @@ install-applet:
|
|||
|
||||
# Uninstalls installed files
|
||||
uninstall:
|
||||
rm -f {{bin-dst}} {{applet-dst}}
|
||||
rm -f {{bin-dst}} {{thumbnailer-bin-dst}} {{applet-dst}} {{thumbnailer-dst}}
|
||||
|
||||
# Vendor dependencies locally
|
||||
vendor:
|
||||
|
|
|
|||
4
res/com.system76.CosmicFiles.thumbnailer
Normal file
4
res/com.system76.CosmicFiles.thumbnailer
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[Thumbnailer Entry]
|
||||
TryExec=cosmic-files-thumbnailer
|
||||
Exec=cosmic-files-thumbnailer %o --size %s %i
|
||||
MimeType=application/vnd.microsoft.portable-executable;application/x-msdownload;
|
||||
254
src/app.rs
254
src/app.rs
|
|
@ -17,12 +17,10 @@ use cosmic::iced::widget::button::focus;
|
|||
use cosmic::iced::widget::scrollable;
|
||||
use cosmic::iced::widget::scrollable::AbsoluteOffset;
|
||||
use cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId};
|
||||
use cosmic::iced::{
|
||||
self, Alignment, Event, Length, Rectangle, Size, Subscription, event, mouse, stream,
|
||||
};
|
||||
use cosmic::iced::{self, Alignment, Event, Length, Size, Subscription, event, mouse, stream};
|
||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||
use cosmic::iced::{
|
||||
Limits, Point,
|
||||
Limits, Point, Rectangle,
|
||||
event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent},
|
||||
platform_specific::runtime::wayland::layer_surface::{
|
||||
IcedMargin, IcedOutput, SctkLayerSurfaceSettings,
|
||||
|
|
@ -608,7 +606,7 @@ pub enum Message {
|
|||
OutputEvent(OutputEvent, WlOutput),
|
||||
Cosmic(app::Action),
|
||||
None,
|
||||
Surface(surface::Action),
|
||||
Surface(surface::Action<Message>),
|
||||
CutPaths(Vec<PathBuf>),
|
||||
}
|
||||
|
||||
|
|
@ -800,7 +798,6 @@ pub struct MounterData(MounterKey, MounterItem);
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum WindowKind {
|
||||
ContextMenu(Entity, widget::Id),
|
||||
Desktop(Entity),
|
||||
DesktopViewOptions,
|
||||
Dialogs(widget::Id),
|
||||
|
|
@ -1593,12 +1590,6 @@ impl App {
|
|||
fn remove_window(&mut self, id: &window::Id) {
|
||||
if let Some(window) = self.windows.remove(id) {
|
||||
match window.kind {
|
||||
WindowKind::ContextMenu(entity, _) => {
|
||||
// Close context menu
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||
tab.context_menu = None;
|
||||
}
|
||||
}
|
||||
WindowKind::Desktop(entity) => {
|
||||
// Remove the tab from the tab model
|
||||
self.tab_model.remove(entity);
|
||||
|
|
@ -1935,21 +1926,6 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn close_context_menus(&mut self) -> Task<Message> {
|
||||
let active = self.tab_model.active();
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(active) {
|
||||
tab.location_context_menu_index = None;
|
||||
if tab.context_menu.is_some() {
|
||||
return self.update(Message::TabMessage(
|
||||
Some(active),
|
||||
tab::Message::ContextMenu(None, None),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn update_nav_model(&mut self) {
|
||||
let mut nav_model = segmented_button::ModelBuilder::default();
|
||||
|
||||
|
|
@ -1963,9 +1939,7 @@ impl App {
|
|||
|
||||
for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {
|
||||
if let Some(path) = favorite.path_opt() {
|
||||
let name = favorite
|
||||
.display_name()
|
||||
.unwrap_or_else(|| fl!("filesystem"));
|
||||
let name = favorite.display_name().unwrap_or_else(|| fl!("filesystem"));
|
||||
nav_model = nav_model.insert(move |b| {
|
||||
b.text(name.clone())
|
||||
.icon(
|
||||
|
|
@ -2865,7 +2839,7 @@ impl Application for App {
|
|||
{
|
||||
nav = nav
|
||||
.window_id_maybe(self.core().main_window_id())
|
||||
.on_surface_action(|m| cosmic::Action::Cosmic(cosmic::app::Action::Surface(m)))
|
||||
.on_surface_action(|action| cosmic::Action::Surface(action.flatten()))
|
||||
}
|
||||
|
||||
let mut nav = nav.into_container();
|
||||
|
|
@ -3133,18 +3107,6 @@ impl Application for App {
|
|||
return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false)));
|
||||
}
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||
if tab.location_context_menu_index.is_some() {
|
||||
tab.location_context_menu_index = None;
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if tab.context_menu.is_some() {
|
||||
return self.update(Message::TabMessage(
|
||||
Some(entity),
|
||||
tab::Message::ContextMenu(None, None),
|
||||
));
|
||||
}
|
||||
|
||||
if tab.edit_location.is_some() {
|
||||
tab.edit_location = None;
|
||||
return Task::none();
|
||||
|
|
@ -3902,7 +3864,7 @@ impl Application for App {
|
|||
Message::Mouse(window_id, _button) => {
|
||||
// Close context menu when clicking outside.
|
||||
if self.core.main_window_id() == Some(window_id) {
|
||||
return self.close_context_menus();
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
Message::MoveTo(entity_opt) => {
|
||||
|
|
@ -4715,7 +4677,7 @@ impl Application for App {
|
|||
));
|
||||
}
|
||||
Message::SearchActivate => {
|
||||
let mut tasks = vec![self.close_context_menus()];
|
||||
let mut tasks = vec![];
|
||||
|
||||
if self.search_get().is_none() {
|
||||
tasks.push(self.search_set_active(Some(String::new())));
|
||||
|
|
@ -4726,7 +4688,7 @@ impl Application for App {
|
|||
return Task::batch(tasks);
|
||||
}
|
||||
Message::SearchClear => {
|
||||
return Task::batch([self.close_context_menus(), self.search_set_active(None)]);
|
||||
return self.search_set_active(None);
|
||||
}
|
||||
Message::SearchInput(input) => {
|
||||
return self.search_set_active(Some(input));
|
||||
|
|
@ -4827,7 +4789,7 @@ impl Application for App {
|
|||
return self.update_config();
|
||||
}
|
||||
Message::TabActivate(entity) => {
|
||||
let mut tasks = vec![self.close_context_menus()];
|
||||
let mut tasks = vec![];
|
||||
|
||||
// Activate new tab
|
||||
self.tab_model.activate(entity);
|
||||
|
|
@ -4889,7 +4851,9 @@ impl Application for App {
|
|||
tasks.push(Task::future(async move {
|
||||
cosmic::action::app(Message::WindowClose)
|
||||
}));
|
||||
} else if let Some(position) = self.tab_model.position(entity) {
|
||||
} else if entity == self.tab_model.active()
|
||||
&& let Some(position) = self.tab_model.position(entity)
|
||||
{
|
||||
let new_position = if position > 0 {
|
||||
position - 1
|
||||
} else {
|
||||
|
|
@ -4926,6 +4890,8 @@ impl Application for App {
|
|||
}
|
||||
Message::TabMessage(entity_opt, tab_message) => {
|
||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||
// The context menu opens on right-button release, so refresh paste availability now
|
||||
let right_click = matches!(tab_message, tab::Message::RightClick(..));
|
||||
|
||||
let tab_commands = match self.tab_model.data_mut::<Tab>(entity) {
|
||||
Some(tab) => tab.update(tab_message, self.modifiers),
|
||||
|
|
@ -4933,6 +4899,9 @@ impl Application for App {
|
|||
};
|
||||
|
||||
let mut commands = Vec::new();
|
||||
if right_click {
|
||||
commands.push(self.update(Message::CheckClipboard));
|
||||
}
|
||||
for tab_command in tab_commands {
|
||||
match tab_command {
|
||||
tab::Command::Action(action) => {
|
||||
|
|
@ -4973,93 +4942,11 @@ impl Application for App {
|
|||
self.update_tab(entity, tab_path, selection_paths),
|
||||
]));
|
||||
}
|
||||
tab::Command::ContextMenu(point_opt, parent_id) => {
|
||||
#[cfg(feature = "wayland")]
|
||||
if let Some(point) = point_opt {
|
||||
if crate::is_wayland() {
|
||||
// Open context menu
|
||||
use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{
|
||||
Anchor, Gravity,
|
||||
};
|
||||
use cosmic::{iced::runtime::platform_specific::wayland::popup::{
|
||||
SctkPopupSettings, SctkPositioner,
|
||||
}, widget::menu::StyleSheet as _};
|
||||
|
||||
let window_id = WindowId::unique();
|
||||
self.windows.insert(
|
||||
window_id,
|
||||
Window::new(WindowKind::ContextMenu(
|
||||
entity,
|
||||
widget::Id::unique(),
|
||||
)),
|
||||
);
|
||||
commands.push(self.update(Message::CheckClipboard));
|
||||
let t = self.core.system_theme();
|
||||
let styling = t.appearance(
|
||||
&cosmic::theme::menu_bar::MenuBarStyle::Default,
|
||||
false,
|
||||
);
|
||||
let rad = styling.menu_border_radius;
|
||||
|
||||
commands.push(self.update(Message::Surface(
|
||||
cosmic::surface::action::app_popup(
|
||||
move |_| cosmic::surface::action::LiveSettings {
|
||||
corners: Some(iced::runtime::platform_specific::wayland::CornerRadius {
|
||||
top_left: rad[0] as u32,
|
||||
top_right: rad[1] as u32,
|
||||
bottom_left: rad[2] as u32,
|
||||
bottom_right: rad[3] as u32,
|
||||
}),
|
||||
..Default::default()
|
||||
}, move |app: &mut Self| -> SctkPopupSettings {
|
||||
let anchor_rect = Rectangle {
|
||||
x: point.x as i32,
|
||||
y: point.y as i32,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
let positioner = SctkPositioner {
|
||||
size: None,
|
||||
anchor_rect,
|
||||
anchor: Anchor::None,
|
||||
gravity: Gravity::BottomRight,
|
||||
reactive: true,
|
||||
..Default::default()
|
||||
};
|
||||
SctkPopupSettings {
|
||||
parent: parent_id.unwrap_or(
|
||||
app.core
|
||||
.main_window_id()
|
||||
.unwrap_or(WindowId::NONE),
|
||||
),
|
||||
id: window_id,
|
||||
positioner,
|
||||
parent_size: None,
|
||||
grab: true,
|
||||
close_with_children: false,
|
||||
input_zone: None,
|
||||
}
|
||||
},
|
||||
None,
|
||||
),
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// Destroy previous popup
|
||||
let mut window_ids = Vec::new();
|
||||
for (window_id, window) in &self.windows {
|
||||
if let WindowKind::ContextMenu(e, _) = &window.kind
|
||||
&& *e == entity
|
||||
{
|
||||
window_ids.push(*window_id);
|
||||
}
|
||||
}
|
||||
for window_id in window_ids {
|
||||
commands.push(self.update(Message::Surface(
|
||||
cosmic::surface::action::destroy_popup(window_id),
|
||||
)));
|
||||
}
|
||||
}
|
||||
tab::Command::Surface(action) => {
|
||||
// re-type the tab's surface action the way its messages are re-typed
|
||||
let action = action
|
||||
.map(move |message| Message::TabMessage(Some(entity), message));
|
||||
commands.push(self.update(Message::Surface(action)));
|
||||
}
|
||||
tab::Command::Delete(paths) => commands.push(self.delete(paths)),
|
||||
tab::Command::DropFiles(to, from) => {
|
||||
|
|
@ -5109,7 +4996,6 @@ impl Application for App {
|
|||
}
|
||||
tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)),
|
||||
tab::Command::OpenInNewTab(path) => {
|
||||
commands.push(self.close_context_menus());
|
||||
commands.push(self.open_tab(Location::Path(path), false, None));
|
||||
}
|
||||
tab::Command::OpenInNewWindow(path) => match env::current_exe() {
|
||||
|
|
@ -5546,12 +5432,6 @@ impl Application for App {
|
|||
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) {
|
||||
// Close location editing if enabled
|
||||
tab.edit_location = None;
|
||||
// Close other context menus.
|
||||
tab.location_context_menu_index = None;
|
||||
return Task::done(cosmic::Action::App(Message::TabMessage(
|
||||
Some(tab_entity),
|
||||
tab::Message::ContextMenu(None, None),
|
||||
)));
|
||||
}
|
||||
}
|
||||
Message::NavMenuAction(action) => match action {
|
||||
|
|
@ -5650,7 +5530,7 @@ impl Application for App {
|
|||
_ => Task::none(),
|
||||
};
|
||||
|
||||
return Task::batch([self.close_context_menus(), open_task]);
|
||||
return open_task;
|
||||
}
|
||||
|
||||
// Open the selected path in a new cosmic-files window.
|
||||
|
|
@ -5733,16 +5613,10 @@ impl Application for App {
|
|||
}
|
||||
|
||||
NavMenuAction::ChangeSidebarLabel(entity) => {
|
||||
if let Some(favorite) = self
|
||||
.nav_model
|
||||
.data::<FavoriteIndex>(entity)
|
||||
.and_then(|FavoriteIndex(favorite_i)| {
|
||||
self.config.favorites.get(*favorite_i)
|
||||
})
|
||||
{
|
||||
let label = favorite
|
||||
.display_name()
|
||||
.unwrap_or_else(|| fl!("filesystem"));
|
||||
if let Some(favorite) = self.nav_model.data::<FavoriteIndex>(entity).and_then(
|
||||
|FavoriteIndex(favorite_i)| self.config.favorites.get(*favorite_i),
|
||||
) {
|
||||
let label = favorite.display_name().unwrap_or_else(|| fl!("filesystem"));
|
||||
return Task::batch([
|
||||
self.dialog_pages
|
||||
.push_back(DialogPage::ChangeSidebarLabel { entity, label }),
|
||||
|
|
@ -5802,35 +5676,31 @@ impl Application for App {
|
|||
.insert(surface_id, Window::new(WindowKind::Desktop(entity)));
|
||||
return Task::batch([
|
||||
command,
|
||||
cosmic::task::message(cosmic::action::cosmic(
|
||||
cosmic::app::Action::Surface(
|
||||
cosmic::surface::action::app_layer_shell(
|
||||
|_| Default::default(),
|
||||
move |_: &mut App| SctkLayerSurfaceSettings {
|
||||
id: surface_id,
|
||||
layer: Layer::Bottom,
|
||||
keyboard_interactivity: KeyboardInteractivity::OnDemand,
|
||||
input_zone: None,
|
||||
anchor: Anchor::TOP
|
||||
| Anchor::BOTTOM
|
||||
| Anchor::LEFT
|
||||
| Anchor::RIGHT,
|
||||
output: IcedOutput::Output(output.clone()),
|
||||
namespace: "cosmic-files-applet".into(),
|
||||
size: Some((None, None)),
|
||||
margin: IcedMargin {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
exclusive_zone: 0,
|
||||
size_limits: Limits::NONE
|
||||
.min_width(1.0)
|
||||
.min_height(1.0),
|
||||
cosmic::task::message(cosmic::Action::Surface(
|
||||
cosmic::surface::action::app_layer_shell(
|
||||
|_| Default::default(),
|
||||
move |_: &mut App| SctkLayerSurfaceSettings {
|
||||
id: surface_id,
|
||||
layer: Layer::Bottom,
|
||||
keyboard_interactivity: KeyboardInteractivity::OnDemand,
|
||||
input_zone: None,
|
||||
anchor: Anchor::TOP
|
||||
| Anchor::BOTTOM
|
||||
| Anchor::LEFT
|
||||
| Anchor::RIGHT,
|
||||
output: IcedOutput::Output(output.clone()),
|
||||
namespace: "cosmic-files-applet".into(),
|
||||
size: Some((None, None)),
|
||||
margin: IcedMargin {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
None,
|
||||
),
|
||||
exclusive_zone: 0,
|
||||
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
|
||||
},
|
||||
None,
|
||||
),
|
||||
)),
|
||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||
|
|
@ -5925,9 +5795,7 @@ impl Application for App {
|
|||
});
|
||||
}
|
||||
Message::Surface(action) => {
|
||||
return cosmic::task::message(cosmic::Action::Cosmic(
|
||||
cosmic::app::Action::Surface(action),
|
||||
));
|
||||
return cosmic::task::message(cosmic::Action::Surface(action));
|
||||
}
|
||||
Message::SaveSortNames => {
|
||||
self.must_save_sort_names = false;
|
||||
|
|
@ -7143,23 +7011,6 @@ impl Application for App {
|
|||
fn view_window(&self, id: WindowId) -> Element<'_, Self::Message> {
|
||||
let content = match self.windows.get(&id) {
|
||||
Some(window) => match &window.kind {
|
||||
WindowKind::ContextMenu(entity, id) => match self.tab_model.data::<Tab>(*entity) {
|
||||
Some(tab) => {
|
||||
return widget::autosize::autosize(
|
||||
menu::context_menu(
|
||||
tab,
|
||||
&self.key_binds,
|
||||
&window.modifiers,
|
||||
self.clipboard_has_content(),
|
||||
&self.config.context_actions,
|
||||
)
|
||||
.map(|x| Message::TabMessage(Some(*entity), x)),
|
||||
id.clone(),
|
||||
)
|
||||
.into();
|
||||
}
|
||||
None => widget::text("Unknown tab ID").into(),
|
||||
},
|
||||
WindowKind::Desktop(entity) => {
|
||||
let mut tab_column = widget::column::with_capacity(3);
|
||||
|
||||
|
|
@ -7219,6 +7070,7 @@ impl Application for App {
|
|||
return self.view_main().map(|message| match message {
|
||||
cosmic::Action::App(app) => app,
|
||||
cosmic::Action::Cosmic(cosmic) => Message::Cosmic(cosmic),
|
||||
cosmic::Action::Surface(action) => Message::Surface(action),
|
||||
cosmic::Action::None => Message::None,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ pub enum TypeToSearch {
|
|||
#[serde(default)]
|
||||
pub struct State {
|
||||
pub sort_names: FxOrderMap<String, (HeadingOptions, bool)>,
|
||||
pub dialog_sort_name: (HeadingOptions, bool),
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
|
|
@ -161,6 +162,7 @@ impl Default for State {
|
|||
(HeadingOptions::Modified, false),
|
||||
)
|
||||
})),
|
||||
dialog_sort_name: (HeadingOptions::Modified, false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
151
src/dialog.rs
151
src/dialog.rs
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use cosmic::app::cosmic::Cosmic;
|
||||
use cosmic::app::{Core, Task, context_drawer};
|
||||
use cosmic::cosmic_config::ConfigSet;
|
||||
use cosmic::iced::core::SmolStr;
|
||||
use cosmic::iced::core::widget::operation;
|
||||
use cosmic::iced::futures::{self, SinkExt};
|
||||
|
|
@ -32,11 +33,13 @@ use std::{env, fmt, fs};
|
|||
use crate::app::{
|
||||
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
|
||||
};
|
||||
use crate::config::{Config, DialogConfig, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch};
|
||||
use crate::config::{
|
||||
Config, DialogConfig, State, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,
|
||||
};
|
||||
use crate::key_bind::key_binds;
|
||||
use crate::localize::LANGUAGE_SORTER;
|
||||
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
|
||||
use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};
|
||||
use crate::tab::{self, HeadingOptions, ItemMetadata, Location, SearchLocation, Tab};
|
||||
use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
|
||||
use crate::{fl, home_dir, menu, mime_icon};
|
||||
|
||||
|
|
@ -251,6 +254,7 @@ impl<M: Send + 'static> Dialog<M> {
|
|||
crate::localize::localize();
|
||||
|
||||
let (config_handler, config) = Config::load();
|
||||
let (state_handler, state) = State::load();
|
||||
|
||||
let mut settings = window::Settings {
|
||||
decorations: false,
|
||||
|
|
@ -285,6 +289,8 @@ impl<M: Send + 'static> Dialog<M> {
|
|||
window_id,
|
||||
config_handler,
|
||||
config,
|
||||
state_handler,
|
||||
state,
|
||||
};
|
||||
|
||||
let (cosmic, cosmic_command) = Cosmic::<App>::init((core, flags));
|
||||
|
|
@ -439,6 +445,8 @@ struct Flags {
|
|||
#[allow(dead_code)]
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
state_handler: Option<cosmic_config::Config>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
/// Messages that are used specifically by our [`App`].
|
||||
|
|
@ -468,7 +476,7 @@ enum Message {
|
|||
SearchActivate,
|
||||
SearchClear,
|
||||
SearchInput(String),
|
||||
Surface(cosmic::surface::Action),
|
||||
Surface(cosmic::surface::Action<Message>),
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
TabMessage(tab::Message),
|
||||
TabRescan(
|
||||
|
|
@ -501,7 +509,7 @@ impl From<AppMessage> for Message {
|
|||
AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn,
|
||||
AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut,
|
||||
AppMessage::NewItem(_entity_opt, true) => Self::NewFolder,
|
||||
AppMessage::Surface(action) => Self::Surface(action),
|
||||
AppMessage::Surface(action) => Self::Surface(action.map(Self::from)),
|
||||
unsupported => {
|
||||
log::warn!("{unsupported:?} not supported in dialog mode");
|
||||
Self::None
|
||||
|
|
@ -541,7 +549,6 @@ struct App {
|
|||
title: String,
|
||||
accept_label: DialogLabel,
|
||||
choices: Vec<DialogChoice>,
|
||||
context_menu_window: Option<window::Id>,
|
||||
context_page: ContextPage,
|
||||
dialog_pages: VecDeque<DialogPage>,
|
||||
dialog_text_input: widget::Id,
|
||||
|
|
@ -871,15 +878,6 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn close_context_menus(&mut self) -> Task<Message> {
|
||||
self.tab.location_context_menu_index = None;
|
||||
if self.tab.context_menu.is_some() {
|
||||
return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn update_nav_model(&mut self) {
|
||||
let mut nav_model = segmented_button::ModelBuilder::default();
|
||||
|
||||
|
|
@ -1043,8 +1041,7 @@ impl Application for App {
|
|||
None,
|
||||
);
|
||||
tab.mode = tab::Mode::Dialog(flags.kind.clone());
|
||||
tab.sort_name = tab::HeadingOptions::Modified;
|
||||
tab.sort_direction = false;
|
||||
(tab.sort_name, tab.sort_direction) = flags.state.dialog_sort_name;
|
||||
|
||||
let key_binds = key_binds(&tab.mode);
|
||||
|
||||
|
|
@ -1054,7 +1051,6 @@ impl Application for App {
|
|||
title,
|
||||
accept_label: DialogLabel::from(accept_label),
|
||||
choices: Vec::new(),
|
||||
context_menu_window: None,
|
||||
context_page: ContextPage::Preview(None, PreviewKind::Selected),
|
||||
dialog_pages: VecDeque::new(),
|
||||
dialog_text_input: widget::Id::new("Dialog Text Input"),
|
||||
|
|
@ -1325,15 +1321,6 @@ impl Application for App {
|
|||
return Task::none();
|
||||
}
|
||||
|
||||
if self.tab.location_context_menu_index.is_some() {
|
||||
self.tab.location_context_menu_index = None;
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if self.tab.context_menu.is_some() {
|
||||
return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));
|
||||
}
|
||||
|
||||
if self.tab.edit_location.is_some() {
|
||||
// Close location editing if enabled
|
||||
self.tab.edit_location = None;
|
||||
|
|
@ -1568,7 +1555,7 @@ impl Application for App {
|
|||
Message::Mouse(window_id, _button) => {
|
||||
// Close context menu when clicking outside.
|
||||
if self.core.main_window_id() == Some(window_id) {
|
||||
return self.close_context_menus();
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
Message::NewFolder => {
|
||||
|
|
@ -1728,7 +1715,7 @@ impl Application for App {
|
|||
)));
|
||||
}
|
||||
Message::SearchActivate => {
|
||||
let mut tasks = vec![self.close_context_menus()];
|
||||
let mut tasks = vec![];
|
||||
|
||||
if self.search_get().is_none() {
|
||||
tasks.push(self.search_set(Some(String::new())));
|
||||
|
|
@ -1739,7 +1726,7 @@ impl Application for App {
|
|||
return Task::batch(tasks);
|
||||
}
|
||||
Message::SearchClear => {
|
||||
return Task::batch([self.close_context_menus(), self.search_set(None)]);
|
||||
return self.search_set(None);
|
||||
}
|
||||
Message::SearchInput(input) => {
|
||||
return self.search_set(Some(input));
|
||||
|
|
@ -1775,94 +1762,9 @@ impl Application for App {
|
|||
self.rescan_tab(selection_paths),
|
||||
]));
|
||||
}
|
||||
tab::Command::ContextMenu(point_opt, parent_id) => {
|
||||
#[cfg(feature = "wayland")]
|
||||
match point_opt {
|
||||
Some(point) => {
|
||||
if crate::is_wayland() {
|
||||
// Open context menu
|
||||
use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{
|
||||
Anchor, Gravity,
|
||||
};
|
||||
use cosmic::iced::runtime::platform_specific::wayland::popup::{
|
||||
SctkPopupSettings, SctkPositioner,
|
||||
};
|
||||
use cosmic::iced::Rectangle;
|
||||
use cosmic::widget::menu::StyleSheet as _;
|
||||
|
||||
let window_id = window::Id::unique();
|
||||
self.context_menu_window = Some(window_id);
|
||||
let autosize_id = widget::Id::unique();
|
||||
let t = self.core.system_theme();
|
||||
let styling = t.appearance(
|
||||
&cosmic::theme::menu_bar::MenuBarStyle::Default,
|
||||
false,
|
||||
);
|
||||
let rad = styling.menu_border_radius;
|
||||
commands.push(self.update(Message::Surface(
|
||||
cosmic::surface::action::app_popup(
|
||||
move |_| cosmic::surface::action::LiveSettings {
|
||||
corners: Some(iced::runtime::platform_specific::wayland::CornerRadius {
|
||||
top_left: rad[0] as u32,
|
||||
top_right: rad[1] as u32,
|
||||
bottom_left: rad[2] as u32,
|
||||
bottom_right: rad[3] as u32,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
move |app: &mut Self| -> SctkPopupSettings {
|
||||
let anchor_rect = Rectangle {
|
||||
x: point.x as i32,
|
||||
y: point.y as i32,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
let positioner = SctkPositioner {
|
||||
size: None,
|
||||
anchor_rect,
|
||||
anchor: Anchor::None,
|
||||
gravity: Gravity::BottomRight,
|
||||
reactive: true,
|
||||
..Default::default()
|
||||
};
|
||||
SctkPopupSettings {
|
||||
parent: parent_id
|
||||
.unwrap_or(app.flags.window_id),
|
||||
id: window_id,
|
||||
positioner,
|
||||
parent_size: None,
|
||||
grab: true,
|
||||
close_with_children: false,
|
||||
input_zone: None,
|
||||
}
|
||||
},
|
||||
Some(Box::new(move |app: &Self| {
|
||||
widget::autosize::autosize(
|
||||
menu::context_menu(
|
||||
&app.tab,
|
||||
&app.key_binds,
|
||||
&app.modifiers,
|
||||
false, // Paste not used in dialogs
|
||||
&app.flags.config.context_actions,
|
||||
)
|
||||
.map(Message::TabMessage)
|
||||
.map(cosmic::Action::App),
|
||||
autosize_id.clone(),
|
||||
)
|
||||
.into()
|
||||
})),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(window_id) = self.context_menu_window.take() {
|
||||
commands.push(self.update(Message::Surface(
|
||||
cosmic::surface::action::destroy_popup(window_id),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
tab::Command::Surface(action) => {
|
||||
let action = action.map(Message::TabMessage);
|
||||
commands.push(self.update(Message::Surface(action)));
|
||||
}
|
||||
tab::Command::Iced(iced_command) => {
|
||||
commands.push(iced_command.0.map(|tab_message| {
|
||||
|
|
@ -1897,6 +1799,17 @@ impl Application for App {
|
|||
self.auto_scroll_speed = None;
|
||||
}
|
||||
}
|
||||
tab::Command::SetSort(_, heading_options, direction) => {
|
||||
self.flags.state.dialog_sort_name = (heading_options, direction);
|
||||
if let Some(state_handler) = self.flags.state_handler.as_ref()
|
||||
&& let Err(err) = state_handler.set::<&(HeadingOptions, bool)>(
|
||||
"dialog_sort_name",
|
||||
&self.flags.state.dialog_sort_name,
|
||||
)
|
||||
{
|
||||
log::warn!("Failed to save dialog sort name: {err:?}");
|
||||
}
|
||||
}
|
||||
unsupported => {
|
||||
log::warn!("{unsupported:?} not supported in dialog mode");
|
||||
}
|
||||
|
|
@ -2017,9 +1930,7 @@ impl Application for App {
|
|||
});
|
||||
}
|
||||
Message::Surface(action) => {
|
||||
return cosmic::task::message(cosmic::Action::Cosmic(
|
||||
cosmic::app::Action::Surface(action),
|
||||
));
|
||||
return cosmic::task::message(cosmic::Action::Surface(action));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
416
src/menu.rs
416
src/menu.rs
|
|
@ -1,16 +1,12 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{Element, theme};
|
||||
use cosmic::app::Core;
|
||||
use cosmic::iced::advanced::widget::text::Style as TextStyle;
|
||||
use cosmic::iced::keyboard::Modifiers;
|
||||
use cosmic::iced::{Alignment, Background, Border, Length};
|
||||
use cosmic::widget::menu::action::MenuAction;
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
|
||||
use cosmic::widget::{
|
||||
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
|
||||
};
|
||||
use cosmic::{Element, theme};
|
||||
#[cfg(feature = "desktop")]
|
||||
use cosmic::widget::{self, responsive_menu_bar};
|
||||
use i18n_embed::LanguageLoader;
|
||||
use mime_guess::Mime;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -27,21 +23,6 @@ use crate::trash::{Trash, TrashExt};
|
|||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
||||
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
|
||||
|
||||
macro_rules! menu_button {
|
||||
($($x:expr),+ $(,)?) => (
|
||||
button::custom(
|
||||
Row::with_children(
|
||||
[$(Element::from($x)),+]
|
||||
)
|
||||
.height(Length::Fixed(24.0))
|
||||
.align_y(Alignment::Center)
|
||||
)
|
||||
.padding([theme::spacing().space_xxs, 16])
|
||||
.width(Length::Fill)
|
||||
.class(theme::Button::MenuItem)
|
||||
);
|
||||
}
|
||||
|
||||
const fn menu_button_optional(
|
||||
label: String,
|
||||
action: Action,
|
||||
|
|
@ -54,82 +35,46 @@ const fn menu_button_optional(
|
|||
}
|
||||
}
|
||||
|
||||
/// A menu action dispatched to the tab the menu was opened on.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TabAction(pub Action);
|
||||
|
||||
impl MenuAction for TabAction {
|
||||
type Message = tab::Message;
|
||||
|
||||
fn message(&self) -> tab::Message {
|
||||
tab::Message::ContextAction(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context_menu<'a>(
|
||||
tab: &Tab,
|
||||
key_binds: &HashMap<KeyBind, Action>,
|
||||
modifiers: &Modifiers,
|
||||
clipboard_paste_available: bool,
|
||||
context_actions: &[ContextActionPreset],
|
||||
) -> Element<'a, tab::Message> {
|
||||
let find_key = |action: &Action| -> String {
|
||||
for (key_bind, key_action) in key_binds {
|
||||
if action == key_action {
|
||||
return key_bind.to_string();
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
};
|
||||
fn key_style(theme: &cosmic::Theme) -> TextStyle {
|
||||
let mut color = theme.cosmic().background(theme.transparent).component.on;
|
||||
color.alpha *= 0.75;
|
||||
TextStyle {
|
||||
color: Some(color.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
fn disabled_style(theme: &cosmic::Theme) -> TextStyle {
|
||||
let mut color = theme.cosmic().background(theme.transparent).component.on;
|
||||
color.alpha *= 0.5;
|
||||
TextStyle {
|
||||
color: Some(color.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
let menu_item = |label, action| {
|
||||
let key = find_key(&action);
|
||||
menu_button!(
|
||||
text::body(label),
|
||||
space::horizontal(),
|
||||
text::body(key).class(theme::Text::Custom(key_style))
|
||||
)
|
||||
.on_press(tab::Message::ContextAction(action))
|
||||
};
|
||||
|
||||
let menu_item_disabled = |label, action: Action| {
|
||||
let key = find_key(&action);
|
||||
menu_button!(
|
||||
text::body(label).class(theme::Text::Custom(disabled_style)),
|
||||
space::horizontal(),
|
||||
text::body(key).class(theme::Text::Custom(disabled_style))
|
||||
)
|
||||
};
|
||||
) -> Vec<menu::Tree<tab::Message>> {
|
||||
let menu_item =
|
||||
|label: String, action: Action| menu::Item::Button(label, None, TabAction(action));
|
||||
let menu_item_disabled =
|
||||
|label: String, action: Action| menu::Item::ButtonDisabled(label, None, TabAction(action));
|
||||
|
||||
// Allow paste when clipboard has data and we're in a location that supports it
|
||||
let can_paste = clipboard_paste_available && tab.location.supports_paste();
|
||||
|
||||
let (sort_name, sort_direction, _) = tab.sort_options();
|
||||
let sort_item = |label, variant| {
|
||||
let key = find_key(&Action::ToggleSort(variant));
|
||||
let leading: Element<'a, tab::Message> = if sort_name == variant {
|
||||
let sort_item = |label: String, variant| {
|
||||
let entry = menu::Entry::new(label, TabAction(Action::ToggleSort(variant)));
|
||||
menu::Item::Entry(if sort_name == variant {
|
||||
let icon_name = if sort_direction {
|
||||
"view-sort-ascending-symbolic"
|
||||
} else {
|
||||
"view-sort-descending-symbolic"
|
||||
};
|
||||
widget::icon::from_name(icon_name).size(14).into()
|
||||
entry.icon(widget::icon::from_name(icon_name).size(14).handle())
|
||||
} else {
|
||||
space::horizontal().width(Length::Fixed(14.0)).into()
|
||||
};
|
||||
menu_button!(
|
||||
leading,
|
||||
space::horizontal().width(Length::Fixed(theme::spacing().space_xxs.into())),
|
||||
text::body(label),
|
||||
space::horizontal(),
|
||||
text::body(key).class(theme::Text::Custom(key_style))
|
||||
)
|
||||
.on_press(tab::Message::ContextAction(Action::ToggleSort(variant)))
|
||||
.into()
|
||||
entry.reserve_icon()
|
||||
})
|
||||
};
|
||||
|
||||
let mut selected_dir = 0;
|
||||
|
|
@ -174,8 +119,8 @@ pub fn context_menu<'a>(
|
|||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, action)| action.matches_selection(selected, selected_dir))
|
||||
.map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into())
|
||||
.collect::<Vec<Element<'a, tab::Message>>>()
|
||||
.map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Parse the desktop entry if it is the only selection
|
||||
#[cfg(feature = "desktop")]
|
||||
|
|
@ -190,7 +135,7 @@ pub fn context_menu<'a>(
|
|||
}
|
||||
});
|
||||
|
||||
let mut children: Vec<Element<_>> = Vec::new();
|
||||
let mut children: Vec<menu::Item<TabAction, String>> = Vec::new();
|
||||
match (&tab.mode, &tab.location) {
|
||||
(
|
||||
tab::Mode::App | tab::Mode::Desktop,
|
||||
|
|
@ -202,108 +147,114 @@ pub fn context_menu<'a>(
|
|||
| Location::Network(_, _, Some(_)),
|
||||
) => {
|
||||
if selected_trash_only {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
children.push(menu_item(fl!("open"), Action::Open));
|
||||
if !Trash::is_empty() {
|
||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash));
|
||||
}
|
||||
} else if let Some(_entry) = selected_desktop_entry {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
} else if let Some(entry) = selected_desktop_entry {
|
||||
children.push(menu_item(fl!("open"), Action::Open));
|
||||
#[cfg(feature = "desktop")]
|
||||
{
|
||||
children.extend(_entry.desktop_actions.into_iter().enumerate().map(
|
||||
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
||||
));
|
||||
children.extend(
|
||||
entry
|
||||
.desktop_actions
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, action)| menu_item(action.name, Action::ExecEntryAction(i))),
|
||||
);
|
||||
}
|
||||
children
|
||||
.push(menu_item(fl!("edit-desktop-entry"), Action::EditDesktopEntry).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||
children.push(menu_item(fl!("cut"), Action::Cut).into());
|
||||
children.push(menu_item(fl!("edit-desktop-entry"), Action::EditDesktopEntry));
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(fl!("rename"), Action::Rename));
|
||||
children.push(menu_item(fl!("cut"), Action::Cut));
|
||||
if modifiers.shift() && !modifiers.control() {
|
||||
children.push(menu_item(fl!("copy-path"), Action::CopyPath).into());
|
||||
children.push(menu_item(fl!("copy-path"), Action::CopyPath));
|
||||
} else {
|
||||
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
||||
children.push(menu_item(fl!("copy"), Action::Copy));
|
||||
}
|
||||
// Should this simply bypass trash and remove the shortcut?
|
||||
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
|
||||
children.push(menu_item(fl!("move-to-trash"), Action::Delete));
|
||||
let action_items = context_action_items(selected, selected_dir);
|
||||
if !action_items.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
children.extend(action_items);
|
||||
}
|
||||
} else if selected > 0 {
|
||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
children.push(menu_item(fl!("open"), Action::Open));
|
||||
}
|
||||
if selected == 1 {
|
||||
children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into());
|
||||
children.push(menu_item(fl!("menu-open-with"), Action::OpenWith));
|
||||
if selected_dir == 1 {
|
||||
children
|
||||
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||
children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal));
|
||||
}
|
||||
}
|
||||
if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) {
|
||||
children.push(
|
||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||
);
|
||||
children.push(menu_item(
|
||||
fl!("open-item-location"),
|
||||
Action::OpenItemLocation,
|
||||
));
|
||||
}
|
||||
// All selected items are directories
|
||||
if selected == selected_dir && matches!(tab.mode, tab::Mode::App) {
|
||||
children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into());
|
||||
children
|
||||
.push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into());
|
||||
children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab));
|
||||
children.push(menu_item(
|
||||
fl!("open-in-new-window"),
|
||||
Action::OpenInNewWindow,
|
||||
));
|
||||
}
|
||||
let action_items = context_action_items(selected, selected_dir);
|
||||
if !action_items.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
children.extend(action_items);
|
||||
}
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
if selected_mount_point == 0 {
|
||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||
children.push(menu_item(fl!("cut"), Action::Cut).into());
|
||||
children.push(menu_item(fl!("rename"), Action::Rename));
|
||||
children.push(menu_item(fl!("cut"), Action::Cut));
|
||||
}
|
||||
if modifiers.shift() && !modifiers.control() {
|
||||
children.push(menu_item(fl!("copy-path"), Action::CopyPath).into());
|
||||
children.push(menu_item(fl!("copy-path"), Action::CopyPath));
|
||||
} else {
|
||||
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
||||
children.push(menu_item(fl!("copy"), Action::Copy));
|
||||
}
|
||||
if selected_mount_point == 0 {
|
||||
children.push(menu_item(fl!("move-to"), Action::MoveTo).into());
|
||||
children.push(menu_item(fl!("move-to"), Action::MoveTo));
|
||||
}
|
||||
children.push(menu_item(fl!("copy-to"), Action::CopyTo).into());
|
||||
children.push(menu_item(fl!("copy-to"), Action::CopyTo));
|
||||
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;
|
||||
selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));
|
||||
if selected_types.is_empty() {
|
||||
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
||||
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
|
||||
children.push(menu_item(fl!("extract-here"), Action::ExtractHere));
|
||||
children.push(menu_item(fl!("extract-to"), Action::ExtractTo));
|
||||
}
|
||||
children.push(menu_item(fl!("compress"), Action::Compress).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("compress"), Action::Compress));
|
||||
children.push(menu::Item::Divider);
|
||||
|
||||
//TODO: Print?
|
||||
children.push(menu_item(fl!("show-details"), Action::Preview).into());
|
||||
children.push(menu_item(fl!("show-details"), Action::Preview));
|
||||
if any_trash_item {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(
|
||||
menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into(),
|
||||
);
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("delete-permanently"), Action::Delete).into());
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(
|
||||
fl!("restore-from-trash"),
|
||||
Action::RestoreFromTrash,
|
||||
));
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(fl!("delete-permanently"), Action::Delete));
|
||||
} else {
|
||||
if matches!(tab.mode, tab::Mode::App) {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children
|
||||
.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar));
|
||||
}
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
if tab.location.is_recents() {
|
||||
children.push(
|
||||
menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(),
|
||||
);
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(
|
||||
fl!("remove-from-recents"),
|
||||
Action::RemoveFromRecents,
|
||||
));
|
||||
children.push(menu::Item::Divider);
|
||||
}
|
||||
if selected_mount_point == 0 {
|
||||
if modifiers.shift() && !modifiers.control() {
|
||||
|
|
@ -312,55 +263,59 @@ pub fn context_menu<'a>(
|
|||
.into(),
|
||||
);
|
||||
} else {
|
||||
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
|
||||
children.push(menu_item(fl!("move-to-trash"), Action::Delete));
|
||||
}
|
||||
} else if selected == 1 {
|
||||
children.push(menu_item(fl!("eject"), Action::Eject).into());
|
||||
children.push(menu_item(fl!("eject"), Action::Eject));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//TODO: need better designs for menu with no selection
|
||||
//TODO: have things like properties but they apply to the folder?
|
||||
if tab.location != Location::Recents {
|
||||
children.push(menu_item(fl!("new-folder"), Action::NewFolder).into());
|
||||
children.push(menu_item(fl!("new-file"), Action::NewFile).into());
|
||||
children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("new-folder"), Action::NewFolder));
|
||||
children.push(menu_item(fl!("new-file"), Action::NewFile));
|
||||
children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal));
|
||||
children.push(menu::Item::Divider);
|
||||
}
|
||||
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll));
|
||||
}
|
||||
if can_paste {
|
||||
children.push(menu_item(fl!("paste"), Action::Paste).into());
|
||||
children.push(menu_item(fl!("paste"), Action::Paste));
|
||||
} else {
|
||||
children.push(menu_item_disabled(fl!("paste"), Action::Paste).into());
|
||||
children.push(menu_item_disabled(fl!("paste"), Action::Paste));
|
||||
}
|
||||
|
||||
//TODO: only show if cosmic-settings is found?
|
||||
if matches!(tab.mode, tab::Mode::Desktop) {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(
|
||||
menu_item(fl!("change-wallpaper"), Action::CosmicSettingsWallpaper).into(),
|
||||
);
|
||||
children.push(
|
||||
menu_item(fl!("desktop-appearance"), Action::CosmicSettingsDesktop).into(),
|
||||
);
|
||||
children.push(
|
||||
menu_item(fl!("display-settings"), Action::CosmicSettingsDisplays).into(),
|
||||
);
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(
|
||||
fl!("change-wallpaper"),
|
||||
Action::CosmicSettingsWallpaper,
|
||||
));
|
||||
children.push(menu_item(
|
||||
fl!("desktop-appearance"),
|
||||
Action::CosmicSettingsDesktop,
|
||||
));
|
||||
children.push(menu_item(
|
||||
fl!("display-settings"),
|
||||
Action::CosmicSettingsDisplays,
|
||||
));
|
||||
}
|
||||
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
// TODO: Nested menu
|
||||
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));
|
||||
if matches!(tab.location, Location::Desktop(..)) {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(
|
||||
menu_item(fl!("desktop-view-options"), Action::DesktopViewOptions).into(),
|
||||
);
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(
|
||||
fl!("desktop-view-options"),
|
||||
Action::DesktopViewOptions,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,24 +330,25 @@ pub fn context_menu<'a>(
|
|||
) => {
|
||||
if selected > 0 {
|
||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
children.push(menu_item(fl!("open"), Action::Open));
|
||||
}
|
||||
if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() {
|
||||
children.push(
|
||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||
);
|
||||
children.push(menu_item(
|
||||
fl!("open-item-location"),
|
||||
Action::OpenItemLocation,
|
||||
));
|
||||
}
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("show-details"), Action::Preview).into());
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(fl!("show-details"), Action::Preview));
|
||||
} else {
|
||||
if dialog_kind.save() {
|
||||
children.push(menu_item(fl!("new-folder"), Action::NewFolder).into());
|
||||
children.push(menu_item(fl!("new-folder"), Action::NewFolder));
|
||||
}
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll));
|
||||
}
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
}
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||
|
|
@ -402,14 +358,14 @@ pub fn context_menu<'a>(
|
|||
(_, Location::Network(..)) => {
|
||||
if selected > 0 {
|
||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
children.push(menu_item(fl!("open"), Action::Open));
|
||||
}
|
||||
} else {
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll));
|
||||
}
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
}
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||
|
|
@ -418,24 +374,26 @@ pub fn context_menu<'a>(
|
|||
}
|
||||
(_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll));
|
||||
}
|
||||
if !Trash::is_empty() {
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
}
|
||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash));
|
||||
}
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu::Item::Divider);
|
||||
}
|
||||
if selected > 0 {
|
||||
children.push(menu_item(fl!("show-details"), Action::Preview).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children
|
||||
.push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("delete-permanently"), Action::Delete).into());
|
||||
children.push(menu_item(fl!("show-details"), Action::Preview));
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(
|
||||
fl!("restore-from-trash"),
|
||||
Action::RestoreFromTrash,
|
||||
));
|
||||
children.push(menu::Item::Divider);
|
||||
children.push(menu_item(fl!("delete-permanently"), Action::Delete));
|
||||
} else {
|
||||
// TODO: Nested menu
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
|
|
@ -445,26 +403,11 @@ pub fn context_menu<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
container(cosmic::widget::menu::menu_column::MenuColumn::with_children(children))
|
||||
.padding(1)
|
||||
//TODO: move style to libcosmic
|
||||
.style(|theme| {
|
||||
let cosmic = theme.cosmic();
|
||||
let component = &cosmic.background(theme.transparent);
|
||||
container::Style {
|
||||
icon_color: Some(component.on.into()),
|
||||
text_color: Some(component.on.into()),
|
||||
background: Some(Background::Color(component.base.into())),
|
||||
border: Border {
|
||||
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
|
||||
width: 1.0,
|
||||
color: component.divider.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.width(Length::Fixed(360.0))
|
||||
.into()
|
||||
let key_binds: HashMap<KeyBind, TabAction> = key_binds
|
||||
.iter()
|
||||
.map(|(key_bind, action)| (key_bind.clone(), TabAction(*action)))
|
||||
.collect();
|
||||
menu::items(&key_binds, children)
|
||||
}
|
||||
|
||||
pub fn dialog_menu(
|
||||
|
|
@ -813,50 +756,33 @@ pub fn menu_bar<'a>(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
|
||||
pub fn location_context_menu(ancestor_index: usize) -> Vec<menu::Tree<tab::Message>> {
|
||||
//TODO: only add some of these when in App mode
|
||||
let children = [
|
||||
menu_button!(text::body(fl!("open-in-new-tab")))
|
||||
.on_press(tab::Message::LocationMenuAction(
|
||||
menu::items(
|
||||
&HashMap::new(),
|
||||
vec![
|
||||
menu::Item::Button(
|
||||
fl!("open-in-new-tab"),
|
||||
None,
|
||||
LocationMenuAction::OpenInNewTab(ancestor_index),
|
||||
))
|
||||
.into(),
|
||||
menu_button!(text::body(fl!("open-in-new-window")))
|
||||
.on_press(tab::Message::LocationMenuAction(
|
||||
),
|
||||
menu::Item::Button(
|
||||
fl!("open-in-new-window"),
|
||||
None,
|
||||
LocationMenuAction::OpenInNewWindow(ancestor_index),
|
||||
))
|
||||
.into(),
|
||||
divider::horizontal::light().into(),
|
||||
menu_button!(text::body(fl!("show-details")))
|
||||
.on_press(tab::Message::LocationMenuAction(
|
||||
),
|
||||
menu::Item::Divider,
|
||||
menu::Item::Button(
|
||||
fl!("show-details"),
|
||||
None,
|
||||
LocationMenuAction::Preview(ancestor_index),
|
||||
))
|
||||
.into(),
|
||||
divider::horizontal::light().into(),
|
||||
menu_button!(text::body(fl!("add-to-sidebar")))
|
||||
.on_press(tab::Message::LocationMenuAction(
|
||||
),
|
||||
menu::Item::Divider,
|
||||
menu::Item::Button(
|
||||
fl!("add-to-sidebar"),
|
||||
None,
|
||||
LocationMenuAction::AddToSidebar(ancestor_index),
|
||||
))
|
||||
.into(),
|
||||
];
|
||||
|
||||
container(column::with_children(children))
|
||||
.padding(1)
|
||||
.style(|theme| {
|
||||
let cosmic = theme.cosmic();
|
||||
let component = &cosmic.background(theme.transparent).component;
|
||||
container::Style {
|
||||
icon_color: Some(component.on.into()),
|
||||
text_color: Some(component.on.into()),
|
||||
background: Some(Background::Color(component.base.into())),
|
||||
border: Border {
|
||||
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
|
||||
width: 1.0,
|
||||
color: component.divider.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.width(Length::Fixed(360.0))
|
||||
.into()
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,8 +214,9 @@ impl<'a, Message, F> OnAutoScroll<'a, Message> for F where F: Fn(Option<f32>) ->
|
|||
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 OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Option<Message> + 'a {}
|
||||
impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Option<Message> + 'a
|
||||
{}
|
||||
|
||||
pub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {}
|
||||
impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {}
|
||||
|
|
@ -563,6 +564,16 @@ fn update<Message: Clone>(
|
|||
state.last_position = position_in;
|
||||
}
|
||||
|
||||
if state.drag_initiated.is_some()
|
||||
&& matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. })
|
||||
)
|
||||
{
|
||||
shell.request_redraw();
|
||||
}
|
||||
|
||||
if let Event::Mouse(mouse::Event::CursorMoved { position }) = event {
|
||||
let virtual_position = Point::new(
|
||||
viewport.x - layout_bounds.x + position.x,
|
||||
|
|
@ -639,6 +650,7 @@ fn update<Message: Clone>(
|
|||
{
|
||||
state.drag_initiated = None;
|
||||
state.prev_click = None;
|
||||
shell.request_redraw();
|
||||
if let Some(message) = widget.on_drag_end.as_ref() {
|
||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||
}
|
||||
|
|
@ -658,7 +670,9 @@ fn update<Message: Clone>(
|
|||
state.prev_click = None;
|
||||
return;
|
||||
}
|
||||
state.drag_initiated = None;
|
||||
if state.drag_initiated.take().is_some() {
|
||||
shell.request_redraw();
|
||||
}
|
||||
if let Some(message) = widget.on_release.as_ref() {
|
||||
shell.publish(message(cursor.position_in(layout_bounds)));
|
||||
|
||||
|
|
@ -785,12 +799,13 @@ fn update<Message: Clone>(
|
|||
}
|
||||
|
||||
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(
|
||||
|mut rect| {
|
||||
rect.x -= layout_bounds.x;
|
||||
rect.y -= layout_bounds.y;
|
||||
rect
|
||||
},
|
||||
)));
|
||||
let rect_opt = drag_rect.intersection(&layout_bounds).map(|mut rect| {
|
||||
rect.x -= layout_bounds.x;
|
||||
rect.y -= layout_bounds.y;
|
||||
rect
|
||||
});
|
||||
if let Some(message) = message(rect_opt) {
|
||||
shell.publish(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,7 +480,9 @@ impl Op {
|
|||
#[cfg(not(feature = "gvfs"))]
|
||||
Err(why) => {
|
||||
_ = from_file.close().await;
|
||||
return Err(why).with_context(|| format!("failed to open {} for writing", self.to.display())).map_err(Into::into);
|
||||
return Err(why)
|
||||
.with_context(|| format!("failed to open {} for writing", self.to.display()))
|
||||
.map_err(Into::into);
|
||||
}
|
||||
#[cfg(feature = "gvfs")]
|
||||
Err(_why) => {
|
||||
|
|
|
|||
278
src/tab.rs
278
src/tab.rs
|
|
@ -1757,11 +1757,11 @@ impl fmt::Debug for TaskWrapper {
|
|||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
Action(Action),
|
||||
Surface(cosmic::surface::Action<Message>),
|
||||
AddNetworkDrive,
|
||||
AddToSidebar(PathBuf),
|
||||
AutoScroll(Option<f32>),
|
||||
ChangeLocation(String, Location, Option<Vec<PathBuf>>),
|
||||
ContextMenu(Option<Point>, Option<window::Id>),
|
||||
Delete(Vec<PathBuf>),
|
||||
DropFiles(PathBuf, ClipboardPaste),
|
||||
ClearRecents,
|
||||
|
|
@ -1792,9 +1792,9 @@ pub enum Message {
|
|||
ClickRelease(Option<usize>),
|
||||
Config(TabConfig),
|
||||
ContextAction(Action),
|
||||
ContextMenu(Option<Point>, Option<window::Id>),
|
||||
LocationContextMenuPoint(Option<Point>),
|
||||
LocationContextMenuIndex(Option<Point>, Option<usize>),
|
||||
RightClickBackground,
|
||||
Surface(cosmic::surface::Action<Message>),
|
||||
LocationContextMenuIndex(Option<usize>),
|
||||
LocationMenuAction(LocationMenuAction),
|
||||
Drag(Option<Rectangle>),
|
||||
DragEnd,
|
||||
|
|
@ -2820,9 +2820,8 @@ pub struct Tab {
|
|||
pub location: Location,
|
||||
pub location_ancestors: Vec<(Location, String)>,
|
||||
pub location_title: String,
|
||||
pub location_context_menu_point: Option<Point>,
|
||||
/// Breadcrumb whose context menu is open, drawn as active while it shows
|
||||
pub location_context_menu_index: Option<usize>,
|
||||
pub context_menu: Option<Point>,
|
||||
pub mode: Mode,
|
||||
pub scroll_opt: Option<AbsoluteOffset>,
|
||||
pub size_opt: Cell<Option<Size>>,
|
||||
|
|
@ -2850,7 +2849,6 @@ pub struct Tab {
|
|||
search_context: Option<SearchContext>,
|
||||
date_time_formatter: DateTimeFormatter<fieldsets::YMDT>,
|
||||
time_formatter: DateTimeFormatter<fieldsets::T>,
|
||||
watch_drag: bool,
|
||||
window_id: Option<window::Id>,
|
||||
large_image_manager: LargeImageManager,
|
||||
}
|
||||
|
|
@ -2967,8 +2965,6 @@ impl Tab {
|
|||
location,
|
||||
location_ancestors,
|
||||
location_title,
|
||||
context_menu: None,
|
||||
location_context_menu_point: None,
|
||||
location_context_menu_index: None,
|
||||
mode: Mode::App,
|
||||
scroll_opt: None,
|
||||
|
|
@ -2997,7 +2993,6 @@ impl Tab {
|
|||
search_context: None,
|
||||
date_time_formatter: date_time_formatter(config.military_time),
|
||||
time_formatter: time_formatter(config.military_time),
|
||||
watch_drag: true,
|
||||
window_id,
|
||||
large_image_manager: LargeImageManager::new(),
|
||||
}
|
||||
|
|
@ -3510,7 +3505,6 @@ impl Tab {
|
|||
self.location = location.normalize();
|
||||
self.location_ancestors = self.location.ancestors();
|
||||
self.location_title = self.location.title();
|
||||
self.context_menu = None;
|
||||
self.edit_location = None;
|
||||
self.items_opt = None;
|
||||
//TODO: remember scroll by location?
|
||||
|
|
@ -3555,7 +3549,6 @@ impl Tab {
|
|||
let mut history_i_opt = None;
|
||||
let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();
|
||||
let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();
|
||||
let last_context_menu = self.context_menu;
|
||||
match message {
|
||||
Message::AddNetworkDrive => {
|
||||
commands.push(Command::AddNetworkDrive);
|
||||
|
|
@ -3590,8 +3583,6 @@ impl Tab {
|
|||
}
|
||||
|
||||
if click_i_opt != self.clicked.take() {
|
||||
self.context_menu = None;
|
||||
self.location_context_menu_index = None;
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for (i, item) in items.iter_mut().enumerate() {
|
||||
if mod_ctrl {
|
||||
|
|
@ -3608,7 +3599,6 @@ impl Tab {
|
|||
}
|
||||
Message::DragEnd => {
|
||||
self.clicked = None;
|
||||
self.watch_drag = true;
|
||||
}
|
||||
Message::DoubleClick(click_i_opt) => {
|
||||
if let Some(clicked_item) = self
|
||||
|
|
@ -3633,9 +3623,7 @@ impl Tab {
|
|||
}
|
||||
Message::Click(click_i_opt) => {
|
||||
self.selected_clicked = false;
|
||||
self.context_menu = None;
|
||||
self.edit_location = None;
|
||||
self.location_context_menu_index = None;
|
||||
if click_i_opt.is_none() {
|
||||
self.clicked = click_i_opt;
|
||||
}
|
||||
|
|
@ -3777,24 +3765,16 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::ContextAction(action) => {
|
||||
// Close context menu
|
||||
self.context_menu = None;
|
||||
|
||||
commands.push(Command::Action(action));
|
||||
}
|
||||
Message::RunContextAction(action) => {
|
||||
self.context_menu = None;
|
||||
|
||||
commands.push(Command::RunContextAction(action));
|
||||
}
|
||||
Message::ContextMenu(point_opt, _) => {
|
||||
Message::RightClickBackground => {
|
||||
self.edit_location = None;
|
||||
self.context_menu = point_opt;
|
||||
self.location_context_menu_index = None;
|
||||
|
||||
//TODO: hack for clearing selecting when right clicking empty space
|
||||
if self.context_menu.is_some()
|
||||
&& self.last_right_click.take().is_none()
|
||||
if self.last_right_click.take().is_none()
|
||||
&& let Some(ref mut items) = self.items_opt
|
||||
{
|
||||
for item in items.iter_mut() {
|
||||
|
|
@ -3802,17 +3782,13 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
}
|
||||
Message::LocationContextMenuPoint(point_opt) => {
|
||||
self.context_menu = None;
|
||||
self.location_context_menu_point = point_opt;
|
||||
Message::Surface(action) => {
|
||||
commands.push(Command::Surface(action));
|
||||
}
|
||||
Message::LocationContextMenuIndex(p, index_opt) => {
|
||||
self.context_menu = None;
|
||||
self.location_context_menu_point = p;
|
||||
self.location_context_menu_index = index_opt;
|
||||
Message::LocationContextMenuIndex(index) => {
|
||||
self.location_context_menu_index = index;
|
||||
}
|
||||
Message::LocationMenuAction(action) => {
|
||||
self.location_context_menu_index = None;
|
||||
let path_for_index = |ancestor_index| {
|
||||
self.location
|
||||
.path_opt()
|
||||
|
|
@ -3862,10 +3838,7 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::Drag(rect_opt) => {
|
||||
self.watch_drag = false;
|
||||
if let Some(rect) = rect_opt {
|
||||
self.context_menu = None;
|
||||
self.location_context_menu_index = None;
|
||||
if self.mode.multiple() {
|
||||
self.select_rect(rect, mod_ctrl, mod_shift);
|
||||
}
|
||||
|
|
@ -4502,13 +4475,11 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::HighlightDeactivate(i) => {
|
||||
self.watch_drag = true;
|
||||
if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) {
|
||||
item.highlighted = false;
|
||||
}
|
||||
}
|
||||
Message::HighlightActivate(i) => {
|
||||
self.watch_drag = true;
|
||||
if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) {
|
||||
item.highlighted = true;
|
||||
}
|
||||
|
|
@ -4534,7 +4505,6 @@ impl Tab {
|
|||
}
|
||||
Message::Scroll(viewport) => {
|
||||
self.scroll_opt = Some(viewport.absolute_offset());
|
||||
self.watch_drag = true;
|
||||
}
|
||||
Message::ScrollTab(scroll_speed) => {
|
||||
commands.push(Command::Iced(
|
||||
|
|
@ -4987,16 +4957,6 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
|
||||
// Update context menu popup
|
||||
if self.context_menu != last_context_menu {
|
||||
if last_context_menu.is_some() {
|
||||
commands.push(Command::ContextMenu(None, self.window_id));
|
||||
}
|
||||
if let Some(point) = self.context_menu {
|
||||
commands.push(Command::ContextMenu(Some(point), self.window_id));
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
|
|
@ -5607,10 +5567,14 @@ impl Tab {
|
|||
}
|
||||
|
||||
let location = self.location.with_path(ancestor.to_path_buf());
|
||||
let mut mouse_area = crate::mouse_area::MouseArea::new(
|
||||
let mouse_area = crate::mouse_area::MouseArea::new(
|
||||
widget::button::custom(row)
|
||||
.padding(space_xxxs)
|
||||
.class(theme::Button::Link)
|
||||
.class(if self.location_context_menu_index == Some(index) {
|
||||
theme::Button::LinkActive
|
||||
} else {
|
||||
theme::Button::Link
|
||||
})
|
||||
.on_press(if ancestor == path {
|
||||
Message::EditLocation(Some(self.location.clone().into()))
|
||||
} else {
|
||||
|
|
@ -5618,21 +5582,6 @@ impl Tab {
|
|||
}),
|
||||
);
|
||||
|
||||
if self.location_context_menu_index.is_some() {
|
||||
mouse_area = mouse_area
|
||||
.on_right_press(move |point_opt| {
|
||||
Message::LocationContextMenuIndex(point_opt, None)
|
||||
})
|
||||
.wayland_on_right_press_window_position();
|
||||
} else {
|
||||
mouse_area = mouse_area
|
||||
.on_right_press_no_capture()
|
||||
.on_right_press(move |point_opt| {
|
||||
Message::LocationContextMenuIndex(point_opt, Some(index))
|
||||
})
|
||||
.wayland_on_right_press_window_position();
|
||||
}
|
||||
|
||||
let mouse_area = if let Location::Path(_) = &self.location {
|
||||
mouse_area
|
||||
.on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf()))
|
||||
|
|
@ -5640,7 +5589,16 @@ impl Tab {
|
|||
mouse_area
|
||||
};
|
||||
|
||||
children.push(self.dnd_dest(&location, mouse_area));
|
||||
// Each breadcrumb carries the menu for its own ancestor index
|
||||
let mut context_menu =
|
||||
widget::context_menu(mouse_area, Some(menu::location_context_menu(index)))
|
||||
.on_open(Message::LocationContextMenuIndex(Some(index)))
|
||||
.on_close(Message::LocationContextMenuIndex(None))
|
||||
.on_surface_action(Message::Surface);
|
||||
if let Some(window_id) = self.window_id {
|
||||
context_menu = context_menu.window_id(window_id);
|
||||
}
|
||||
children.push(self.dnd_dest(&location, context_menu));
|
||||
|
||||
if found_home || overflow {
|
||||
break;
|
||||
|
|
@ -5691,20 +5649,7 @@ impl Tab {
|
|||
column = column.push(heading_rule);
|
||||
}
|
||||
|
||||
let mouse_area = crate::mouse_area::MouseArea::new(column)
|
||||
.on_right_press(Message::LocationContextMenuPoint);
|
||||
|
||||
let mut popover = widget::popover(mouse_area);
|
||||
if let (Some(point), Some(index)) = (
|
||||
self.location_context_menu_point,
|
||||
self.location_context_menu_index,
|
||||
) {
|
||||
popover = popover
|
||||
.popup(menu::location_context_menu(index))
|
||||
.position(widget::popover::Position::Point(point));
|
||||
}
|
||||
|
||||
popover.into()
|
||||
column.into()
|
||||
}
|
||||
|
||||
pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {
|
||||
|
|
@ -5893,18 +5838,13 @@ impl Tab {
|
|||
.height(Length::Fixed(item_height as f32))
|
||||
.width(Length::Fixed(item_width as f32));
|
||||
for button in buttons {
|
||||
if self.context_menu.is_some() {
|
||||
column = column.push(button);
|
||||
} else {
|
||||
column = column.push(
|
||||
mouse_area::MouseArea::new(button)
|
||||
.on_right_press_no_capture()
|
||||
.wayland_on_right_press_window_position()
|
||||
.on_right_press(move |point_opt| {
|
||||
Message::RightClick(point_opt, Some(i))
|
||||
}),
|
||||
);
|
||||
}
|
||||
column = column.push(
|
||||
mouse_area::MouseArea::new(button)
|
||||
.on_right_press_no_capture()
|
||||
.on_right_press(move |point_opt| {
|
||||
Message::RightClick(point_opt, Some(i))
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let column: Element<Message> =
|
||||
|
|
@ -6071,15 +6011,13 @@ impl Tab {
|
|||
Element::from(dnd_grid)
|
||||
});
|
||||
|
||||
let mut mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill))
|
||||
let mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill))
|
||||
.on_press(|_| Message::Click(None))
|
||||
.on_auto_scroll(Message::AutoScroll)
|
||||
.on_drag(move |rect_opt| self.on_drag(rect_opt))
|
||||
.on_drag_end(|_| Message::DragEnd)
|
||||
.show_drag_rect(self.mode.multiple())
|
||||
.on_release(|_| Message::ClickRelease(None));
|
||||
if self.watch_drag {
|
||||
mouse_area = mouse_area.on_drag(Message::Drag);
|
||||
}
|
||||
|
||||
(drag_list, mouse_area.into(), true)
|
||||
}
|
||||
|
|
@ -6309,39 +6247,33 @@ impl Tab {
|
|||
.spacing(space_xxs)
|
||||
};
|
||||
|
||||
let button = |row| {
|
||||
let mouse_area = crate::mouse_area::MouseArea::new(
|
||||
widget::button::custom(row)
|
||||
.width(Length::Fill)
|
||||
.id(item.button_id.clone())
|
||||
.padding([0, space_xxs])
|
||||
.class(button_style(
|
||||
item.selected,
|
||||
item.highlighted,
|
||||
item.cut,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)),
|
||||
)
|
||||
.on_press(move |_| Message::Click(Some(i)))
|
||||
.on_double_click(move |_| Message::DoubleClick(Some(i)))
|
||||
.on_release(move |_| Message::ClickRelease(Some(i)))
|
||||
.on_middle_press(move |_| Message::MiddleClick(i))
|
||||
.on_enter(move || Message::HighlightActivate(i))
|
||||
.on_exit(move || Message::HighlightDeactivate(i));
|
||||
let button =
|
||||
|row| {
|
||||
let mouse_area = crate::mouse_area::MouseArea::new(
|
||||
widget::button::custom(row)
|
||||
.width(Length::Fill)
|
||||
.id(item.button_id.clone())
|
||||
.padding([0, space_xxs])
|
||||
.class(button_style(
|
||||
item.selected,
|
||||
item.highlighted,
|
||||
item.cut,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)),
|
||||
)
|
||||
.on_press(move |_| Message::Click(Some(i)))
|
||||
.on_double_click(move |_| Message::DoubleClick(Some(i)))
|
||||
.on_release(move |_| Message::ClickRelease(Some(i)))
|
||||
.on_middle_press(move |_| Message::MiddleClick(i))
|
||||
.on_enter(move || Message::HighlightActivate(i))
|
||||
.on_exit(move || Message::HighlightDeactivate(i));
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
mouse_area
|
||||
} else {
|
||||
mouse_area
|
||||
.on_right_press_no_capture()
|
||||
.wayland_on_right_press_window_position()
|
||||
.on_right_press(move |point_opt| {
|
||||
Message::RightClick(point_opt, Some(i))
|
||||
})
|
||||
}
|
||||
};
|
||||
mouse_area.on_right_press_no_capture().on_right_press(
|
||||
move |point_opt| Message::RightClick(point_opt, Some(i)),
|
||||
)
|
||||
};
|
||||
|
||||
let button_row = button(row.into());
|
||||
let button_row: Element<_> = if item.metadata.is_dir()
|
||||
|
|
@ -6469,16 +6401,14 @@ impl Tab {
|
|||
let drag_col = (!drag_items.is_empty())
|
||||
.then(|| Element::from(widget::column::with_children(drag_items)));
|
||||
|
||||
let mut mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s]))
|
||||
let mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s]))
|
||||
.with_id(Id::new("list-view"))
|
||||
.on_press(|_| Message::Click(None))
|
||||
.on_auto_scroll(Message::AutoScroll)
|
||||
.on_drag(move |rect_opt| self.on_drag(rect_opt))
|
||||
.on_drag_end(|_| Message::DragEnd)
|
||||
.show_drag_rect(self.mode.multiple())
|
||||
.on_release(|_| Message::ClickRelease(None));
|
||||
if self.watch_drag {
|
||||
mouse_area = mouse_area.on_drag(Message::Drag);
|
||||
}
|
||||
|
||||
(drag_col, mouse_area.into(), true)
|
||||
}
|
||||
|
|
@ -6565,51 +6495,47 @@ impl Tab {
|
|||
.on_back_press(move |_point_opt| Message::GoPrevious)
|
||||
.on_forward_press(move |_point_opt| Message::GoNext)
|
||||
.on_scroll(|delta| respond_to_scroll_direction(delta, modifiers))
|
||||
.on_right_press(move |p| {
|
||||
Message::ContextMenu(
|
||||
if self.context_menu.is_some() { None } else { p },
|
||||
self.window_id,
|
||||
)
|
||||
})
|
||||
.wayland_on_right_press_window_position();
|
||||
.on_right_press(|_| Message::RightClickBackground);
|
||||
|
||||
let mut popover = widget::popover(mouse_area);
|
||||
if let Some(point) = self.context_menu
|
||||
&& (!cfg!(feature = "wayland") || !crate::is_wayland())
|
||||
{
|
||||
let context_menu = menu::context_menu(
|
||||
let items_area: Element<'_, Message> = if can_scroll {
|
||||
// FIXME: new responsive widget will remove the state from the scrollable
|
||||
// id_container with custom id forces the state to be extracted in a diff
|
||||
// pre-processing step
|
||||
widget::id_container(
|
||||
widget::scrollable(mouse_area)
|
||||
.id(self.scrollable_id.clone())
|
||||
.on_scroll(Message::Scroll)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
widget::Id::new(format!("{}-scrollable", self.scrollable_id)),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
mouse_area.into()
|
||||
};
|
||||
|
||||
// Wrap the scrollable, not its content, so the popup anchors in window coordinates
|
||||
let mut context_menu = widget::context_menu(
|
||||
items_area,
|
||||
Some(menu::context_menu(
|
||||
self,
|
||||
key_binds,
|
||||
modifiers,
|
||||
clipboard_paste_available,
|
||||
context_actions,
|
||||
);
|
||||
popover = popover
|
||||
.popup(context_menu)
|
||||
.position(widget::popover::Position::Point(point));
|
||||
)),
|
||||
)
|
||||
.item_width(cosmic::widget::menu::ItemWidth::Uniform(360))
|
||||
.on_surface_action(Message::Surface);
|
||||
if let Some(window_id) = self.window_id {
|
||||
context_menu = context_menu.window_id(window_id);
|
||||
}
|
||||
|
||||
let mut tab_column = widget::column::with_capacity(3);
|
||||
if let Some(location_view) = location_view_opt {
|
||||
tab_column = tab_column.push(location_view);
|
||||
}
|
||||
if can_scroll {
|
||||
tab_column = tab_column.push(
|
||||
// FIXME: new responsive widget will remove the state from the scrollable
|
||||
// id_container with custom id forces the state to be extracted in a diff
|
||||
// pre-processing step
|
||||
widget::id_container(
|
||||
widget::scrollable(popover)
|
||||
.id(self.scrollable_id.clone())
|
||||
.on_scroll(Message::Scroll)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
widget::Id::new(format!("{}-scrollable", self.scrollable_id)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
tab_column = tab_column.push(popover);
|
||||
}
|
||||
tab_column = tab_column.push(context_menu);
|
||||
match &self.location {
|
||||
Location::Trash | Location::Search(SearchLocation::Trash, ..) => {
|
||||
if let Some(items) = self.items_opt()
|
||||
|
|
@ -7171,8 +7097,7 @@ impl Tab {
|
|||
let path = path.clone();
|
||||
|
||||
// Acquire semaphore permit
|
||||
let _permit =
|
||||
THUMB_SEMAPHORE.acquire().await.unwrap();
|
||||
let _permit = THUMB_SEMAPHORE.acquire().await.unwrap();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let start = Instant::now();
|
||||
|
|
@ -7476,6 +7401,19 @@ impl Tab {
|
|||
const fn format_time(&self, time: SystemTime) -> FormatTime<'_> {
|
||||
format_time(time, &self.date_time_formatter, &self.time_formatter)
|
||||
}
|
||||
|
||||
fn on_drag<'a>(&self, rect_opt: Option<Rectangle>) -> Option<Message> {
|
||||
let rect = rect_opt?;
|
||||
// We only want to publish a drag message if the overlapped items of the drag rect change,
|
||||
// otherwise a view rebuild is triggered on every drag event.
|
||||
let changed = self.items_opt.as_ref().is_some_and(|items| {
|
||||
items.iter().any(|item| {
|
||||
let overlaps = item.rect_opt.get().is_some_and(|r| r.intersects(&rect));
|
||||
overlaps != item.overlaps_drag_rect
|
||||
})
|
||||
});
|
||||
changed.then_some(Message::Drag(Some(rect)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: &Modifiers) -> Option<Message> {
|
||||
|
|
|
|||
|
|
@ -175,3 +175,25 @@ pub fn thumbnailer(mime: &Mime) -> Vec<Thumbnailer> {
|
|||
let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();
|
||||
thumbnailer_cache.get(mime)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "desktop"))]
|
||||
mod tests {
|
||||
use super::Thumbnailer;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn command_expands_thumbnailer_arguments() {
|
||||
let thumbnailer = Thumbnailer {
|
||||
exec: "cosmic-files-thumbnailer %o --size %s %i".to_string(),
|
||||
};
|
||||
|
||||
let command = thumbnailer
|
||||
.command(Path::new("input.exe"), Path::new("output.png"), 128)
|
||||
.expect("valid thumbnailer command");
|
||||
assert_eq!(command.get_program(), "cosmic-files-thumbnailer");
|
||||
assert_eq!(
|
||||
command.get_args().collect::<Vec<_>>(),
|
||||
["output.png", "--size", "128", "input.exe"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue