UPDATE: Implement copy/paste in the UI

This commit is contained in:
Nicolás Hatcher
2024-10-15 22:57:00 +02:00
committed by Nicolás Hatcher Andrés
parent 843d8beb02
commit cd54389e91
12 changed files with 648 additions and 15 deletions

View File

@@ -58,4 +58,5 @@ pub mod mock_time;
pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use user_model::BorderArea;
pub use user_model::ClipboardData;
pub use user_model::UserModel;

View File

@@ -11,6 +11,7 @@ mod test_keyboard_navigation;
mod test_on_area_selection;
mod test_on_expand_selected_range;
mod test_on_paste_styles;
mod test_paste_csv;
mod test_rename_sheet;
mod test_row_column;
mod test_styles;

View File

@@ -0,0 +1,114 @@
use crate::{expressions::types::Area, UserModel};
#[test]
fn csv_paste() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 7, 7, "=SUM(B4:D7)").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 7, 7), Ok("0".to_string()));
// paste some numbers in B4:C7
let csv = "1,2,3\n4,5,6";
let area = Area {
sheet: 0,
row: 4,
column: 2,
width: 1,
height: 1,
};
model.paste_csv_string(&area, csv).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string())
);
}
#[test]
fn tsv_crlf_paste() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 7, 7, "=SUM(B4:D7)").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 7, 7), Ok("0".to_string()));
// paste some numbers in B4:C7
let csv = "1\t2\t3\r\n4\t5\t6";
let area = Area {
sheet: 0,
row: 4,
column: 2,
width: 1,
height: 1,
};
model.paste_csv_string(&area, csv).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string())
);
}
#[test]
fn copy_paste_internal() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*3+1").unwrap();
// set A1 bold
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
model.update_range_style(&range, "font.b", "true").unwrap();
model
.set_user_input(0, 2, 1, "A season of faith, \"perfection\"")
.unwrap();
// Select A1:B2 and copy
model.set_selected_range(1, 1, 2, 2).unwrap();
let copy = model.copy_to_clipboard().unwrap();
assert_eq!(
copy.csv,
"42,127\n\"A season of faith, \"\"perfection\"\"\",\n"
);
assert_eq!(copy.range, (1, 1, 2, 2));
model.set_selected_cell(4, 4).unwrap();
// paste in cell D4 (4, 4)
model
.paste_from_clipboard((1, 1, 2, 2), &copy.data)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("=D4*3+1".to_string()));
assert_eq!(
model.get_formatted_cell_value(0, 4, 5),
Ok("127".to_string())
);
// cell D4 must be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(style_d4.font.b);
model.undo().unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("".to_string()));
// cell D4 must not be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(!style_d4.font.b);
model.redo().unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("=D4*3+1".to_string()));
assert_eq!(
model.get_formatted_cell_value(0, 4, 5),
Ok("127".to_string())
);
// cell D4 must be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(style_d4.font.b);
}

View File

@@ -1,13 +1,15 @@
#![deny(missing_docs)]
use std::{collections::HashMap, fmt::Debug};
use std::{collections::HashMap, fmt::Debug, io::Cursor};
use csv::{ReaderBuilder, WriterBuilder};
use csv_sniffer::Sniffer;
use serde::{Deserialize, Serialize};
use crate::{
constants,
expressions::{
types::Area,
types::{Area, CellReferenceIndex},
utils::{is_valid_column_number, is_valid_row},
},
model::Model,
@@ -21,6 +23,23 @@ use crate::{
use crate::user_model::history::{
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
};
/// Data for the clipboard
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
pub type ClipboardTuple = (i32, i32, i32, i32);
#[derive(Serialize, Deserialize)]
pub struct ClipboardCell {
text: String,
style: Style,
}
#[derive(Serialize, Deserialize)]
pub struct Clipboard {
pub(crate) csv: String,
pub(crate) data: ClipboardData,
pub(crate) range: (i32, i32, i32, i32),
}
#[derive(Serialize, Deserialize)]
pub enum BorderType {
@@ -976,7 +995,7 @@ impl UserModel {
/// See also:
/// * [Model::get_style_for_cell]
#[inline]
pub fn get_cell_style(&mut self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
pub fn get_cell_style(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
self.model.get_style_for_cell(sheet, row, column)
}
@@ -1209,6 +1228,174 @@ impl UserModel {
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
}
/// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view();
let sheet = selected_area.sheet;
let mut wtr = WriterBuilder::new().from_writer(vec![]);
let mut data = HashMap::new();
let [row_start, column_start, row_end, column_end] = selected_area.range;
for row in row_start..=row_end {
let mut data_row = HashMap::new();
let mut text_row = Vec::new();
for column in column_start..=column_end {
let text = self.get_formatted_cell_value(sheet, row, column)?;
let content = self.get_cell_content(sheet, row, column)?;
let style = self.get_cell_style(sheet, row, column)?;
data_row.insert(
column,
ClipboardCell {
text: content,
style,
},
);
text_row.push(text);
}
wtr.write_record(text_row).unwrap();
data.insert(row, data_row);
}
let csv = String::from_utf8(wtr.into_inner().unwrap()).unwrap();
Ok(Clipboard {
csv,
data,
range: (row_start, column_start, row_end, column_end),
})
}
/// Paste text that we copied
pub fn paste_from_clipboard(
&mut self,
source_range: ClipboardTuple,
clipboard: &ClipboardData,
) -> Result<(), String> {
let mut diff_list = Vec::new();
let view = self.get_selected_view();
let (source_first_row, source_first_column, _, _) = source_range;
let sheet = view.sheet;
let [selected_row, selected_column, _, _] = view.range;
for (source_row, data_row) in clipboard {
let delta_row = source_row - source_first_row;
let target_row = selected_row + delta_row;
for (source_column, value) in data_row {
let delta_column = source_column - source_first_column;
let target_column = selected_column + delta_column;
// We are copying the value in
// (source_row, source_column) to (target_row , target_column)
// References in formulas are displaced
// remain in the copied area
let source = &CellReferenceIndex {
sheet,
column: *source_column,
row: *source_row,
};
let target = &CellReferenceIndex {
sheet,
column: target_column,
row: target_row,
};
let new_value = self
.model
.extend_copied_value(&value.text, source, target)?;
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(target_row, target_column)
.cloned();
let old_style = self
.model
.get_style_for_cell(sheet, target_row, target_column)?;
self.model
.set_user_input(sheet, target_row, target_column, new_value.clone())?;
self.model
.set_cell_style(sheet, target_row, target_column, &value.style)?;
diff_list.push(Diff::SetCellValue {
sheet,
row: target_row,
column: target_column,
new_value,
old_value: Box::new(old_value),
});
diff_list.push(Diff::SetCellStyle {
sheet,
row: target_row,
column: target_column,
old_value: Box::new(old_style),
new_value: Box::new(value.style.clone()),
});
}
}
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Paste a csv-string into the model
pub fn paste_csv_string(&mut self, area: &Area, csv: &str) -> Result<(), String> {
let mut diff_list = Vec::new();
let sheet = area.sheet;
let mut row = area.row;
// Create a sniffer with default settings
let mut sniffer = Sniffer::new();
let mut csv_reader = Cursor::new(csv);
// Sniff the CSV metadata
let metadata = sniffer
.sniff_reader(&mut csv_reader)
.map_err(|_| "Failed")?;
// Reset the cursor to the beginning after sniffing
csv_reader.set_position(0);
let mut reader = ReaderBuilder::new()
.delimiter(metadata.dialect.delimiter)
.has_headers(false)
.from_reader(csv_reader);
for record in reader.records() {
match record {
Ok(r) => {
let mut column = area.column;
for value in &r {
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned();
// let old_style = self.model.get_style_for_cell(sheet, row, column)?;
self.model
.set_user_input(sheet, row, column, value.to_string())?;
diff_list.push(Diff::SetCellValue {
sheet,
row,
column,
new_value: value.to_string(),
old_value: Box::new(old_value),
});
column += 1;
}
}
Err(_) => {
// skip
continue;
}
};
row += 1;
}
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {

View File

@@ -10,3 +10,4 @@ pub use common::UserModel;
pub use ui::SelectedView;
pub use common::BorderArea;
pub use common::ClipboardData;