Improved ListView
This commit is contained in:
parent
186c0e1895
commit
c919aa11fa
|
@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::{sleep, JoinHandle};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::{fs, thread};
|
||||
|
||||
#[cfg(feature = "heif")]
|
||||
|
@ -18,7 +18,7 @@ use image::{DynamicImage, ImageBuffer, Rgb};
|
|||
use imagepipe::{ImageSource, Pipeline};
|
||||
#[cfg(feature = "heif")]
|
||||
use libheif_rs::{ColorSpace, HeifContext, RgbChroma};
|
||||
use log::{info, LevelFilter, Record};
|
||||
use log::{debug, info, LevelFilter, Record};
|
||||
|
||||
// #[cfg(feature = "heif")]
|
||||
// use libheif_rs::LibHeif;
|
||||
|
@ -184,12 +184,17 @@ pub fn get_dynamic_image_from_heic(path: &str) -> Result<DynamicImage> {
|
|||
}
|
||||
|
||||
pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug) -> Option<DynamicImage> {
|
||||
let mut start_timer = Instant::now();
|
||||
let mut times = Vec::new();
|
||||
|
||||
let file_handler = match OpenOptions::new().read(true).open(&path) {
|
||||
Ok(t) => t,
|
||||
Err(_e) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
times.push(("After opening", start_timer.elapsed()));
|
||||
start_timer = Instant::now();
|
||||
|
||||
let mut reader = BufReader::new(file_handler);
|
||||
let raw = match rawloader::decode(&mut reader) {
|
||||
|
@ -199,8 +204,14 @@ pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug
|
|||
}
|
||||
};
|
||||
|
||||
times.push(("After decoding", start_timer.elapsed()));
|
||||
start_timer = Instant::now();
|
||||
|
||||
let source = ImageSource::Raw(raw);
|
||||
|
||||
times.push(("After creating source", start_timer.elapsed()));
|
||||
start_timer = Instant::now();
|
||||
|
||||
let mut pipeline = match Pipeline::new_from_source(source) {
|
||||
Ok(pipeline) => pipeline,
|
||||
Err(_e) => {
|
||||
|
@ -208,6 +219,9 @@ pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug
|
|||
}
|
||||
};
|
||||
|
||||
times.push(("After creating pipeline", start_timer.elapsed()));
|
||||
start_timer = Instant::now();
|
||||
|
||||
pipeline.run(None);
|
||||
let image = match pipeline.output_8bit(None) {
|
||||
Ok(image) => image,
|
||||
|
@ -216,12 +230,22 @@ pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug
|
|||
}
|
||||
};
|
||||
|
||||
times.push(("After creating image", start_timer.elapsed()));
|
||||
start_timer = Instant::now();
|
||||
|
||||
let Some(image) = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(image.width as u32, image.height as u32, image.data) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
times.push(("After creating image buffer", start_timer.elapsed()));
|
||||
start_timer = Instant::now();
|
||||
// println!("Properly hashed {:?}", path);
|
||||
Some(DynamicImage::ImageRgb8(image))
|
||||
let res = Some(DynamicImage::ImageRgb8(image));
|
||||
times.push(("After creating dynamic image", start_timer.elapsed()));
|
||||
|
||||
let str_timer = times.into_iter().map(|(name, time)| format!("{name}: {time:?}")).collect::<Vec<_>>().join(", ");
|
||||
debug!("Loading raw image --- {str_timer}");
|
||||
res
|
||||
}
|
||||
|
||||
pub fn split_path(path: &Path) -> (String, String) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{Callabler, GuiState, MainWindow};
|
||||
use czkawka_core::common::IMAGE_RS_EXTENSIONS;
|
||||
use czkawka_core::common::{get_dynamic_image_from_raw_image, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS};
|
||||
use image::DynamicImage;
|
||||
use log::{debug, error};
|
||||
use slint::ComponentHandle;
|
||||
|
@ -50,17 +50,33 @@ fn load_image(image_path: &Path) -> Option<(Duration, image::DynamicImage)> {
|
|||
let image_extension = image_path.extension()?.to_string_lossy().to_lowercase();
|
||||
let extension_with_dot = format!(".{}", image_extension);
|
||||
|
||||
if !IMAGE_RS_EXTENSIONS.contains(&extension_with_dot.as_str()) {
|
||||
let is_raw_image = RAW_IMAGE_EXTENSIONS.contains(&extension_with_dot.as_str());
|
||||
let is_normal_image = IMAGE_RS_EXTENSIONS.contains(&extension_with_dot.as_str());
|
||||
|
||||
if !is_raw_image && !is_normal_image {
|
||||
return None;
|
||||
}
|
||||
let load_img_start_timer = Instant::now();
|
||||
let img = image::open(image_name);
|
||||
|
||||
match img {
|
||||
Ok(img) => Some((load_img_start_timer.elapsed(), img)),
|
||||
Err(e) => {
|
||||
error!("Error while loading image: {}", e);
|
||||
// TODO this needs to be run inside closure
|
||||
let img = if is_normal_image {
|
||||
match image::open(image_name) {
|
||||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
error!("Error while loading image: {}", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else if is_raw_image {
|
||||
if let Some(img) = get_dynamic_image_from_raw_image(image_name) {
|
||||
img
|
||||
} else {
|
||||
error!("Error while loading raw image - not sure why - try to guess");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Used not supported image extension");
|
||||
};
|
||||
|
||||
Some((load_img_start_timer.elapsed(), img))
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ component TabItem {
|
|||
in-out property <CurrentTab> active-tab;
|
||||
in property <string> text;
|
||||
in property <CurrentTab> curr_tab;
|
||||
callback changed_current_tab();
|
||||
|
||||
Rectangle {
|
||||
width: parent.width;
|
||||
horizontal-stretch: 1.0;
|
||||
|
@ -17,6 +19,7 @@ component TabItem {
|
|||
return;
|
||||
}
|
||||
root.active-tab = root.curr-tab;
|
||||
changed_current_tab();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +64,7 @@ component TabItem {
|
|||
export component LeftSidePanel {
|
||||
in-out property <CurrentTab> active-tab;
|
||||
in-out property <bool> scanning;
|
||||
callback changed_current_tab();
|
||||
width: 120px;
|
||||
VerticalLayout {
|
||||
spacing: 20px;
|
||||
|
@ -82,6 +86,7 @@ export component LeftSidePanel {
|
|||
text: "Empty Folders";
|
||||
active-tab <=> root.active-tab;
|
||||
curr_tab: CurrentTab.EmptyFolders;
|
||||
changed_current_tab() => {root.changed_current_tab();}
|
||||
}
|
||||
|
||||
TabItem {
|
||||
|
@ -90,6 +95,7 @@ export component LeftSidePanel {
|
|||
text: "Empty Files";
|
||||
active-tab <=> root.active-tab;
|
||||
curr_tab: CurrentTab.EmptyFiles;
|
||||
changed_current_tab() => {root.changed_current_tab();}
|
||||
}
|
||||
|
||||
TabItem {
|
||||
|
@ -98,6 +104,7 @@ export component LeftSidePanel {
|
|||
text: "Similar Images";
|
||||
active-tab <=> root.active-tab;
|
||||
curr_tab: CurrentTab.SimilarImages;
|
||||
changed_current_tab() => {root.changed_current_tab();}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,40 +9,43 @@ export component MainList {
|
|||
in-out property <[MainListModel]> empty_folder_model;
|
||||
in-out property <[MainListModel]> empty_files_model;
|
||||
in-out property <[MainListModel]> similar_images_model;
|
||||
callback changed_current_tab();
|
||||
|
||||
SelectableTableView {
|
||||
empty_folders := SelectableTableView {
|
||||
visible: root.active-tab == CurrentTab.EmptyFolders;
|
||||
min-width: 200px;
|
||||
height: parent.height;
|
||||
columns: ["Selection", "Folder Name", "Path"];
|
||||
last-column: "Modification Date";
|
||||
column-sizes: [35px, 100px, 350px, 100px];
|
||||
columns: ["Selection", "Folder Name", "Path", "Modification Date"];
|
||||
column-sizes: [35px, 100px, 350px, 150px];
|
||||
values <=> empty-folder-model;
|
||||
parentPathIdx: 2;
|
||||
fileNameIdx: 1;
|
||||
}
|
||||
|
||||
SelectableTableView {
|
||||
empty_files := SelectableTableView {
|
||||
visible: root.active-tab == CurrentTab.EmptyFiles;
|
||||
min-width: 200px;
|
||||
height: parent.height;
|
||||
columns: ["Selection", "File Name", "Path"];
|
||||
last-column: "Modification Date";
|
||||
column-sizes: [35px, 100px, 350px, 100px];
|
||||
columns: ["Selection", "File Name", "Path", "Modification Date"];
|
||||
column-sizes: [35px, 100px, 350px, 150px];
|
||||
values <=> empty-files-model;
|
||||
parentPathIdx: 2;
|
||||
fileNameIdx: 1;
|
||||
}
|
||||
|
||||
SelectableTableView {
|
||||
similar_images := SelectableTableView {
|
||||
visible: root.active-tab == CurrentTab.SimilarImages;
|
||||
min-width: 200px;
|
||||
height: parent.height;
|
||||
columns: ["Selection", "Similarity", "Size", "Dimensions", "File Name", "Path"];
|
||||
last-column: "Modification Date";
|
||||
column-sizes: [35px, 80px, 80px, 80px, 350px, 100px, 100px];
|
||||
columns: ["Selection", "Similarity", "Size", "Dimensions", "File Name", "Path", "Modification Date"];
|
||||
column-sizes: [35px, 80px, 80px, 80px, 100px, 350px, 150px];
|
||||
values <=> similar-images-model;
|
||||
parentPathIdx: 5;
|
||||
fileNameIdx: 4;
|
||||
}
|
||||
changed_current_tab() => {
|
||||
empty_folders.deselect_selected_item();
|
||||
empty_files.deselect_selected_item();
|
||||
similar_images.deselect_selected_item();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,13 +53,18 @@ export component MainWindow inherits Window {
|
|||
horizontal-stretch: 0.0;
|
||||
scanning <=> root.scanning;
|
||||
active-tab <=> root.active-tab;
|
||||
changed_current_tab() => {
|
||||
GuiState.preview-visible = false;
|
||||
main_list.changed_current_tab();
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
horizontal-stretch: 1.0;
|
||||
Rectangle {
|
||||
vertical-stretch: 1.0;
|
||||
MainList {
|
||||
main_list := MainList {
|
||||
x: 0;
|
||||
width: GuiState.preview_visible ? parent.width / 2 : parent.width;
|
||||
height: parent.height;
|
||||
horizontal-stretch: 0.5;
|
||||
|
@ -74,6 +79,7 @@ export component MainWindow inherits Window {
|
|||
width: GuiState.preview_visible ? parent.width / 2 : 0;
|
||||
visible: GuiState.preview_visible;
|
||||
source: GuiState.preview_image;
|
||||
image-fit: ImageFit.contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import {GuiState} from "gui_state.slint";
|
|||
export component SelectableTableView inherits Rectangle {
|
||||
callback item_opened(string);
|
||||
in property <[string]> columns;
|
||||
in property <string> last_column;
|
||||
in-out property <[MainListModel]> values;
|
||||
in-out property <[length]> column_sizes;
|
||||
private property <int> column_number: column-sizes.length + 1;
|
||||
|
@ -25,119 +24,123 @@ export component SelectableTableView inherits Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
padding: 5px;
|
||||
forward-focus: focus-item;
|
||||
HorizontalLayout {
|
||||
ScrollView {
|
||||
VerticalBox {
|
||||
padding: 5px;
|
||||
spacing: 5px;
|
||||
vertical-stretch: 0;
|
||||
for title [idx] in root.columns: HorizontalLayout {
|
||||
width: root.column-sizes[idx];
|
||||
Text {
|
||||
overflow: elide;
|
||||
text: title;
|
||||
}
|
||||
forward-focus: focus-item;
|
||||
HorizontalLayout {
|
||||
padding: 5px;
|
||||
spacing: 5px;
|
||||
vertical-stretch: 0;
|
||||
for title [idx] in root.columns: HorizontalLayout {
|
||||
width: root.column-sizes[idx];
|
||||
Text {
|
||||
overflow: elide;
|
||||
text: title;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1px;
|
||||
background: gray;
|
||||
forward-focus: focus-item;
|
||||
TouchArea {
|
||||
width: 5px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
property <length> cached;
|
||||
Rectangle {
|
||||
width: 1px;
|
||||
background: gray;
|
||||
forward-focus: focus-item;
|
||||
pointer-event(event) => {
|
||||
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
|
||||
self.cached = root.column_sizes[idx];
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if (self.pressed) {
|
||||
root.column_sizes[idx] += (self.mouse-x - self.pressed-x);
|
||||
if (root.column_sizes[idx] < 20px) {
|
||||
root.column_sizes[idx] = 20px;
|
||||
TouchArea {
|
||||
width: 5px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
property <length> cached;
|
||||
forward-focus: focus-item;
|
||||
pointer-event(event) => {
|
||||
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
|
||||
self.cached = root.column_sizes[idx];
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if (self.pressed) {
|
||||
root.column_sizes[idx] += (self.mouse-x - self.pressed-x);
|
||||
if (root.column_sizes[idx] < 20px) {
|
||||
root.column_sizes[idx] = 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
mouse-cursor: ew-resize;
|
||||
}
|
||||
mouse-cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
overflow: elide;
|
||||
text: last-column;
|
||||
}
|
||||
}
|
||||
|
||||
list_view := ListView {
|
||||
min-width: 100px;
|
||||
forward-focus: focus-item;
|
||||
for r [idx] in root.values: Rectangle {
|
||||
border-radius: 5px;
|
||||
list_view := ListView {
|
||||
min-width: 100px;
|
||||
forward-focus: focus-item;
|
||||
height: 20px;
|
||||
background: r.header-row ? ColorPalette.list_view_normal_header_color : (touch-area.has-hover ? (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color) : (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color));
|
||||
touch_area := TouchArea {
|
||||
for r [idx] in root.values: Rectangle {
|
||||
border-radius: 5px;
|
||||
forward-focus: focus-item;
|
||||
clicked => {
|
||||
if (!r.header_row) {
|
||||
r.selected_row = !r.selected_row;
|
||||
if (root.selected-item == -1) {
|
||||
root.selected-item = idx;
|
||||
} else {
|
||||
if (r.selected_row == true) {
|
||||
root.values[root.selected-item].selected_row = false;
|
||||
height: 20px;
|
||||
background: r.header-row ? ColorPalette.list_view_normal_header_color : (touch-area.has-hover ? (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color) : (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color));
|
||||
touch_area := TouchArea {
|
||||
forward-focus: focus-item;
|
||||
clicked => {
|
||||
if (!r.header_row) {
|
||||
r.selected_row = !r.selected_row;
|
||||
if (root.selected-item == -1) {
|
||||
root.selected-item = idx;
|
||||
} else {
|
||||
root.selected-item = -1;
|
||||
if (r.selected_row == true) {
|
||||
root.values[root.selected-item].selected_row = false;
|
||||
root.selected-item = idx;
|
||||
} else {
|
||||
root.selected-item = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.selected_item != -1) {
|
||||
Callabler.load_image_preview(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1]);
|
||||
} else {
|
||||
GuiState.preview-visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.selected_item != -1) {
|
||||
Callabler.load_image_preview(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1]);
|
||||
} else {
|
||||
GuiState.preview-visible = false;
|
||||
}
|
||||
pointer-event(event) => {
|
||||
// TODO this should be clicked by double-click
|
||||
if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) {
|
||||
Callabler.item_opened(r.val[root.parentPathIdx - 1])
|
||||
} else if (event.button == PointerEventButton.middle && event.kind == PointerEventKind.up) {
|
||||
Callabler.item_opened(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
pointer-event(event) => {
|
||||
// TODO this should be clicked by double-click
|
||||
if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) {
|
||||
Callabler.item_opened(r.val[root.parentPathIdx - 1])
|
||||
} else if (event.button == PointerEventButton.middle && event.kind == PointerEventKind.up) {
|
||||
Callabler.item_opened(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
forward-focus: focus-item;
|
||||
CheckBox {
|
||||
visible: !r.header-row;
|
||||
checked: r.checked && !r.header-row;
|
||||
width: root.column-sizes[0];
|
||||
forward-focus: focus-item;
|
||||
toggled => {
|
||||
r.checked = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: 5px;
|
||||
for f [idx] in r.val: Text {
|
||||
width: root.column-sizes[idx + 1];
|
||||
text: f;
|
||||
font-size: 12px;
|
||||
forward-focus: focus-item;
|
||||
CheckBox {
|
||||
visible: !r.header-row;
|
||||
checked: r.checked && !r.header-row;
|
||||
width: root.column-sizes[0];
|
||||
forward-focus: focus-item;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
toggled => {
|
||||
r.checked = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: 5px;
|
||||
for f [idx] in r.val: Text {
|
||||
width: root.column-sizes[idx + 1];
|
||||
text: f;
|
||||
font-size: 12px;
|
||||
forward-focus: focus-item;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function deselect_selected_item() {
|
||||
if (root.selected_item != -1) {
|
||||
root.values[root.selected-item].selected_row = false;
|
||||
root.selected-item = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue