merge: epoch-1.9.0 d'upstream
# Conflicts: # Cargo.lock # src/menu.rs
This commit is contained in:
commit
4b5fcfe5d7
26 changed files with 1250 additions and 1132 deletions
851
Cargo.lock
generated
851
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.8.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.8.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]));
|
||||
}
|
||||
}
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
|
|
@ -1,3 +1,9 @@
|
|||
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
|
||||
|
|
|
|||
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
|
||||
|
|
@ -433,4 +433,4 @@ 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 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
|
||||
|
|
|
|||
|
|
@ -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ä
|
||||
|
|
|
|||
|
|
@ -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,7 +325,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = ลบ
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ name-no-slashes = 名稱不能包含斜線
|
|||
## Open/Save Dialog
|
||||
|
||||
cancel = 取消
|
||||
create = 建立
|
||||
create = 創作
|
||||
open = 開啟
|
||||
open-file = 開啟檔案
|
||||
open-folder = 開啟資料夾
|
||||
|
|
|
|||
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;
|
||||
250
src/app.rs
250
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);
|
||||
|
|
@ -4928,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),
|
||||
|
|
@ -4935,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) => {
|
||||
|
|
@ -4975,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) => {
|
||||
|
|
@ -5111,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() {
|
||||
|
|
@ -5548,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 {
|
||||
|
|
@ -5652,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.
|
||||
|
|
@ -5735,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 }),
|
||||
|
|
@ -5804,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"))]
|
||||
|
|
@ -5927,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;
|
||||
|
|
@ -7145,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);
|
||||
|
||||
|
|
@ -7221,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 {}
|
||||
|
|
@ -798,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