UPDATE: Implement copy/paste in the UI
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
843d8beb02
commit
cd54389e91
@@ -18,6 +18,8 @@ chrono-tz = "0.9"
|
||||
regex = "1.0"
|
||||
once_cell = "1.16.0"
|
||||
bitcode = "0.6.0"
|
||||
csv = "1.3.0"
|
||||
csv-sniffer = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
114
base/src/test/user_model/test_paste_csv.rs
Normal file
114
base/src/test/user_model/test_paste_csv.rs
Normal 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), ©.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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -10,3 +10,4 @@ pub use common::UserModel;
|
||||
pub use ui::SelectedView;
|
||||
|
||||
pub use common::BorderArea;
|
||||
pub use common::ClipboardData;
|
||||
|
||||
Reference in New Issue
Block a user