1
0
Fork 0
mirror of synced 2024-05-17 19:03:08 +12:00

Improved ListView

This commit is contained in:
Rafał Mikrut 2023-11-12 16:00:55 +01:00
parent 186c0e1895
commit c919aa11fa
6 changed files with 171 additions and 112 deletions

View file

@ -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) {

View file

@ -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))
}

View file

@ -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();}
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}