2605 lines
89 KiB
Rust
2605 lines
89 KiB
Rust
#![deny(missing_docs)]
|
||
|
||
use std::{collections::HashMap, fmt::Debug, io::Cursor};
|
||
|
||
use csv::{ReaderBuilder, WriterBuilder};
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::{
|
||
constants::{self, LAST_COLUMN, LAST_ROW},
|
||
expressions::{
|
||
types::{Area, CellReferenceIndex},
|
||
utils::{is_valid_column_number, is_valid_row},
|
||
},
|
||
model::Model,
|
||
types::{
|
||
Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties,
|
||
SheetState, Style, VerticalAlignment,
|
||
},
|
||
utils::is_valid_hex_color,
|
||
};
|
||
|
||
use crate::user_model::history::{
|
||
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
|
||
};
|
||
|
||
use super::border_utils::is_max_border;
|
||
/// 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) sheet: u32,
|
||
pub(crate) range: (i32, i32, i32, i32),
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, PartialEq)]
|
||
pub enum BorderType {
|
||
All,
|
||
Inner,
|
||
Outer,
|
||
Top,
|
||
Right,
|
||
Bottom,
|
||
Left,
|
||
CenterH,
|
||
CenterV,
|
||
None,
|
||
}
|
||
|
||
/// This is the struct for a border area
|
||
#[derive(Serialize, Deserialize)]
|
||
pub struct BorderArea {
|
||
pub(crate) item: BorderItem,
|
||
pub(crate) r#type: BorderType,
|
||
}
|
||
|
||
fn boolean(value: &str) -> Result<bool, String> {
|
||
match value {
|
||
"true" => Ok(true),
|
||
"false" => Ok(false),
|
||
_ => Err(format!("Invalid value for boolean: '{value}'.")),
|
||
}
|
||
}
|
||
|
||
fn color(value: &str) -> Result<Option<String>, String> {
|
||
if value.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
if !is_valid_hex_color(value) {
|
||
return Err(format!("Invalid color: '{value}'."));
|
||
}
|
||
Ok(Some(value.to_owned()))
|
||
}
|
||
|
||
fn horizontal(value: &str) -> Result<HorizontalAlignment, String> {
|
||
match value {
|
||
"center" => Ok(HorizontalAlignment::Center),
|
||
"centerContinuous" => Ok(HorizontalAlignment::CenterContinuous),
|
||
"distributed" => Ok(HorizontalAlignment::Distributed),
|
||
"fill" => Ok(HorizontalAlignment::Fill),
|
||
"general" => Ok(HorizontalAlignment::General),
|
||
"justify" => Ok(HorizontalAlignment::Justify),
|
||
"left" => Ok(HorizontalAlignment::Left),
|
||
"right" => Ok(HorizontalAlignment::Right),
|
||
_ => Err(format!(
|
||
"Invalid value for horizontal alignment: '{value}'."
|
||
)),
|
||
}
|
||
}
|
||
|
||
fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
||
match value {
|
||
"bottom" => Ok(VerticalAlignment::Bottom),
|
||
"center" => Ok(VerticalAlignment::Center),
|
||
"distributed" => Ok(VerticalAlignment::Distributed),
|
||
"justify" => Ok(VerticalAlignment::Justify),
|
||
"top" => Ok(VerticalAlignment::Top),
|
||
_ => Err(format!("Invalid value for vertical alignment: '{value}'.")),
|
||
}
|
||
}
|
||
|
||
fn update_style(old_value: &Style, style_path: &str, value: &str) -> Result<Style, String> {
|
||
let mut style = old_value.clone();
|
||
match style_path {
|
||
"font.b" => {
|
||
style.font.b = boolean(value)?;
|
||
}
|
||
"font.i" => {
|
||
style.font.i = boolean(value)?;
|
||
}
|
||
"font.u" => {
|
||
style.font.u = boolean(value)?;
|
||
}
|
||
"font.strike" => {
|
||
style.font.strike = boolean(value)?;
|
||
}
|
||
"font.color" => {
|
||
style.font.color = color(value)?;
|
||
}
|
||
"font.size_delta" => {
|
||
// This is a special case, we need to add the value to the current size
|
||
let size_delta: i32 = value
|
||
.parse()
|
||
.map_err(|_| format!("Invalid value for font size: '{value}'."))?;
|
||
let new_size = style.font.sz + size_delta;
|
||
if new_size < 1 {
|
||
return Err(format!("Invalid value for font size: '{new_size}'."));
|
||
}
|
||
style.font.sz = new_size;
|
||
}
|
||
"fill.bg_color" => {
|
||
style.fill.bg_color = color(value)?;
|
||
style.fill.pattern_type = "solid".to_string();
|
||
}
|
||
"fill.fg_color" => {
|
||
style.fill.fg_color = color(value)?;
|
||
style.fill.pattern_type = "solid".to_string();
|
||
}
|
||
"num_fmt" => {
|
||
value.clone_into(&mut style.num_fmt);
|
||
}
|
||
"alignment" => {
|
||
if !value.is_empty() {
|
||
return Err(format!("Alignment must be empty, but found: '{value}'."));
|
||
}
|
||
style.alignment = None;
|
||
}
|
||
"alignment.horizontal" => match style.alignment {
|
||
Some(ref mut s) => s.horizontal = horizontal(value)?,
|
||
None => {
|
||
let alignment = Alignment {
|
||
horizontal: horizontal(value)?,
|
||
..Default::default()
|
||
};
|
||
style.alignment = Some(alignment)
|
||
}
|
||
},
|
||
"alignment.vertical" => match style.alignment {
|
||
Some(ref mut s) => s.vertical = vertical(value)?,
|
||
None => {
|
||
let alignment = Alignment {
|
||
vertical: vertical(value)?,
|
||
..Default::default()
|
||
};
|
||
style.alignment = Some(alignment)
|
||
}
|
||
},
|
||
"alignment.wrap_text" => match style.alignment {
|
||
Some(ref mut s) => s.wrap_text = boolean(value)?,
|
||
None => {
|
||
let alignment = Alignment {
|
||
wrap_text: boolean(value)?,
|
||
..Default::default()
|
||
};
|
||
style.alignment = Some(alignment)
|
||
}
|
||
},
|
||
_ => {
|
||
return Err(format!("Invalid style path: '{style_path}'."));
|
||
}
|
||
}
|
||
Ok(style)
|
||
}
|
||
|
||
/// # A wrapper around [`Model`] for a spreadsheet end user.
|
||
/// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management.
|
||
///
|
||
/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
|
||
///
|
||
/// Automatic evaluation means that actions like setting a value on a cell or deleting a column
|
||
/// will evaluate the model if needed.
|
||
///
|
||
/// It is meant to be used by UI applications like Web IronCalc or TironCalc.
|
||
///
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```rust
|
||
/// # use ironcalc_base::UserModel;
|
||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||
/// let mut model = UserModel::new_empty("model", "en", "UTC")?;
|
||
/// model.set_user_input(0, 1, 1, "=1+1")?;
|
||
/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2");
|
||
/// model.undo()?;
|
||
/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "");
|
||
/// model.redo()?;
|
||
/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2");
|
||
/// # Ok(())
|
||
/// # }
|
||
/// ```
|
||
pub struct UserModel {
|
||
pub(crate) model: Model,
|
||
history: History,
|
||
send_queue: Vec<QueueDiffs>,
|
||
pause_evaluation: bool,
|
||
}
|
||
|
||
impl Debug for UserModel {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.debug_struct("UserModel").finish()
|
||
}
|
||
}
|
||
|
||
impl UserModel {
|
||
/// Creates a user model from an existing model
|
||
pub fn from_model(model: Model) -> UserModel {
|
||
UserModel {
|
||
model,
|
||
history: History::default(),
|
||
send_queue: vec![],
|
||
pause_evaluation: false,
|
||
}
|
||
}
|
||
|
||
/// Creates a new UserModel.
|
||
///
|
||
/// See also:
|
||
/// * [Model::new_empty]
|
||
pub fn new_empty(name: &str, locale_id: &str, timezone: &str) -> Result<UserModel, String> {
|
||
let model = Model::new_empty(name, locale_id, timezone)?;
|
||
Ok(UserModel {
|
||
model,
|
||
history: History::default(),
|
||
send_queue: vec![],
|
||
pause_evaluation: false,
|
||
})
|
||
}
|
||
|
||
/// Creates a model from it's internal representation
|
||
///
|
||
/// See also:
|
||
/// * [Model::from_bytes]
|
||
pub fn from_bytes(s: &[u8]) -> Result<UserModel, String> {
|
||
let model = Model::from_bytes(s)?;
|
||
Ok(UserModel {
|
||
model,
|
||
history: History::default(),
|
||
send_queue: vec![],
|
||
pause_evaluation: false,
|
||
})
|
||
}
|
||
|
||
/// Returns the internal representation of a model
|
||
///
|
||
/// See also:
|
||
/// * [Model::to_json_str]
|
||
pub fn to_bytes(&self) -> Vec<u8> {
|
||
self.model.to_bytes()
|
||
}
|
||
|
||
/// Returns the internal model
|
||
pub fn get_model(&self) -> &Model {
|
||
&self.model
|
||
}
|
||
|
||
/// Returns the workbook name
|
||
pub fn get_name(&self) -> String {
|
||
self.model.workbook.name.clone()
|
||
}
|
||
|
||
/// Sets the name of a workbook
|
||
pub fn set_name(&mut self, name: &str) {
|
||
self.model.workbook.name = name.to_string();
|
||
}
|
||
|
||
/// Undoes last change if any, places the change in the redo list and evaluates the model if needed
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::redo]
|
||
pub fn undo(&mut self) -> Result<(), String> {
|
||
if let Some(diff_list) = self.history.undo() {
|
||
self.apply_undo_diff_list(&diff_list)?;
|
||
self.send_queue.push(QueueDiffs {
|
||
r#type: DiffType::Undo,
|
||
list: diff_list.clone(),
|
||
});
|
||
};
|
||
Ok(())
|
||
}
|
||
|
||
/// Redoes the last undone change, places the change in the undo list and evaluates the model if needed
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::redo]
|
||
pub fn redo(&mut self) -> Result<(), String> {
|
||
if let Some(diff_list) = self.history.redo() {
|
||
self.apply_diff_list(&diff_list)?;
|
||
self.send_queue.push(QueueDiffs {
|
||
r#type: DiffType::Redo,
|
||
list: diff_list.clone(),
|
||
});
|
||
};
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns true if there are items to be undone
|
||
pub fn can_undo(&self) -> bool {
|
||
!self.history.undo_stack.is_empty()
|
||
}
|
||
|
||
/// Returns true if there are items to be redone
|
||
pub fn can_redo(&self) -> bool {
|
||
!self.history.redo_stack.is_empty()
|
||
}
|
||
|
||
/// Pauses automatic evaluation.
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::evaluate]
|
||
/// * [UserModel::resume_evaluation]
|
||
pub fn pause_evaluation(&mut self) {
|
||
self.pause_evaluation = true;
|
||
}
|
||
|
||
/// Resumes automatic evaluation.
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::evaluate]
|
||
/// * [UserModel::pause_evaluation]
|
||
pub fn resume_evaluation(&mut self) {
|
||
self.pause_evaluation = false;
|
||
}
|
||
|
||
/// Forces an evaluation of the model
|
||
///
|
||
/// See also:
|
||
/// * [Model::evaluate]
|
||
/// * [UserModel::pause_evaluation]
|
||
pub fn evaluate(&mut self) {
|
||
self.model.evaluate()
|
||
}
|
||
|
||
/// Returns the list of pending diffs and removes them from the queue
|
||
///
|
||
/// This is used together with [apply_external_diffs](UserModel::apply_external_diffs) to keep two remote models
|
||
/// in sync.
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::apply_external_diffs]
|
||
pub fn flush_send_queue(&mut self) -> Vec<u8> {
|
||
// This can never fail :O:
|
||
let q = bitcode::encode(&self.send_queue);
|
||
self.send_queue = vec![];
|
||
q
|
||
}
|
||
|
||
/// This are external diffs that need to be applied to the model
|
||
///
|
||
/// This is used together with [flush_send_queue](UserModel::flush_send_queue) to keep two remote models in sync
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::flush_send_queue]
|
||
pub fn apply_external_diffs(&mut self, diff_list_str: &[u8]) -> Result<(), String> {
|
||
if let Ok(queue_diffs_list) = bitcode::decode::<Vec<QueueDiffs>>(diff_list_str) {
|
||
for queue_diff in queue_diffs_list {
|
||
if matches!(queue_diff.r#type, DiffType::Redo) {
|
||
self.apply_diff_list(&queue_diff.list)?;
|
||
} else {
|
||
self.apply_undo_diff_list(&queue_diff.list)?;
|
||
}
|
||
}
|
||
} else {
|
||
return Err("Error parsing diff list".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Set the input in a cell
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_user_input]
|
||
pub fn set_user_input(
|
||
&mut self,
|
||
sheet: u32,
|
||
row: i32,
|
||
column: i32,
|
||
value: &str,
|
||
) -> Result<(), String> {
|
||
if !is_valid_column_number(column) {
|
||
return Err("Invalid column".to_string());
|
||
}
|
||
if !is_valid_row(row) {
|
||
return Err("Invalid row".to_string());
|
||
}
|
||
let old_value = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.cell(row, column)
|
||
.cloned();
|
||
self.model
|
||
.set_user_input(sheet, row, column, value.to_string())?;
|
||
|
||
self.evaluate_if_not_paused();
|
||
|
||
let mut diff_list = vec![Diff::SetCellValue {
|
||
sheet,
|
||
row,
|
||
column,
|
||
new_value: value.to_string(),
|
||
old_value: Box::new(old_value),
|
||
}];
|
||
let style = self.model.get_style_for_cell(sheet, row, column)?;
|
||
|
||
let line_count = value.split('\n').count() as f64;
|
||
let row_height = self.model.get_row_height(sheet, row)?;
|
||
// This is in sync with the front-end auto fit row
|
||
let font_size = style.font.sz as f64;
|
||
let line_height = font_size * 1.5;
|
||
let cell_height = (line_count - 1.0) * line_height + 8.0 + font_size;
|
||
if cell_height > row_height {
|
||
diff_list.push(Diff::SetRowHeight {
|
||
sheet,
|
||
row,
|
||
new_value: cell_height,
|
||
old_value: row_height,
|
||
});
|
||
self.model.set_row_height(sheet, row, cell_height)?;
|
||
}
|
||
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns the content of a cell
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_cell_content]
|
||
#[inline]
|
||
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, String> {
|
||
self.model.get_cell_content(sheet, row, column)
|
||
}
|
||
|
||
/// Returns the formatted value of a cell
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_formatted_cell_value]
|
||
#[inline]
|
||
pub fn get_formatted_cell_value(
|
||
&self,
|
||
sheet: u32,
|
||
row: i32,
|
||
column: i32,
|
||
) -> Result<String, String> {
|
||
self.model.get_formatted_cell_value(sheet, row, column)
|
||
}
|
||
|
||
/// Returns the type of the cell
|
||
///
|
||
/// See also
|
||
/// * [Model::get_cell_type]
|
||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<CellType, String> {
|
||
self.model.get_cell_type(sheet, row, column)
|
||
}
|
||
|
||
/// Adds new sheet
|
||
///
|
||
/// See also:
|
||
/// * [Model::new_sheet]
|
||
pub fn new_sheet(&mut self) -> Result<(), String> {
|
||
let (name, index) = self.model.new_sheet();
|
||
self.set_selected_sheet(index)?;
|
||
self.push_diff_list(vec![Diff::NewSheet { index, name }]);
|
||
Ok(())
|
||
}
|
||
|
||
/// Deletes sheet by index
|
||
///
|
||
/// See also:
|
||
/// * [Model::delete_sheet]
|
||
pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||
|
||
self.push_diff_list(vec![Diff::DeleteSheet {
|
||
sheet,
|
||
old_data: Box::new(worksheet.clone()),
|
||
}]);
|
||
|
||
let sheet_count = self.model.workbook.worksheets.len() as u32;
|
||
// If we are deleting the last sheet we need to change the selected sheet
|
||
if sheet == sheet_count - 1 && sheet_count > 1 {
|
||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||
view.sheet = sheet_count - 2;
|
||
};
|
||
}
|
||
|
||
self.model.delete_sheet(sheet)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Renames a sheet by index
|
||
///
|
||
/// See also:
|
||
/// * [Model::rename_sheet_by_index]
|
||
pub fn rename_sheet(&mut self, sheet: u32, new_name: &str) -> Result<(), String> {
|
||
let old_value = self.model.workbook.worksheet(sheet)?.name.clone();
|
||
if old_value == new_name {
|
||
return Ok(());
|
||
}
|
||
self.model.rename_sheet_by_index(sheet, new_name)?;
|
||
self.push_diff_list(vec![Diff::RenameSheet {
|
||
index: sheet,
|
||
old_value,
|
||
new_value: new_name.to_string(),
|
||
}]);
|
||
Ok(())
|
||
}
|
||
|
||
/// Hides sheet by index
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_sheet_state]
|
||
/// * [UserModel::unhide_sheet]
|
||
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||
let sheet_count = self.model.workbook.worksheets.len() as u32;
|
||
for index in 1..sheet_count {
|
||
let sheet_index = (sheet + index) % sheet_count;
|
||
if self.model.workbook.worksheet(sheet_index)?.state == SheetState::Visible {
|
||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||
view.sheet = sheet_index;
|
||
};
|
||
break;
|
||
}
|
||
}
|
||
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
|
||
self.push_diff_list(vec![Diff::SetSheetState {
|
||
index: sheet,
|
||
new_value: SheetState::Hidden,
|
||
old_value,
|
||
}]);
|
||
self.model.set_sheet_state(sheet, SheetState::Hidden)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Un hides sheet by index
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_sheet_state]
|
||
/// * [UserModel::hide_sheet]
|
||
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
|
||
self.push_diff_list(vec![Diff::SetSheetState {
|
||
index: sheet,
|
||
new_value: SheetState::Visible,
|
||
old_value,
|
||
}]);
|
||
self.model.set_sheet_state(sheet, SheetState::Visible)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Sets sheet color
|
||
///
|
||
/// Note: an empty string will remove the color
|
||
///
|
||
/// See also
|
||
/// * [Model::set_sheet_color]
|
||
/// * [UserModel::get_worksheets_properties]
|
||
pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), String> {
|
||
let old_value = match &self.model.workbook.worksheet(sheet)?.color {
|
||
Some(c) => c.clone(),
|
||
None => "".to_string(),
|
||
};
|
||
self.model.set_sheet_color(sheet, color)?;
|
||
self.push_diff_list(vec![Diff::SetSheetColor {
|
||
index: sheet,
|
||
old_value,
|
||
new_value: color.to_string(),
|
||
}]);
|
||
Ok(())
|
||
}
|
||
|
||
/// Removes cells contents and style
|
||
///
|
||
/// See also:
|
||
/// * [Model::cell_clear_all]
|
||
pub fn range_clear_all(&mut self, range: &Area) -> Result<(), String> {
|
||
let sheet = range.sheet;
|
||
// TODO: full rows/columns
|
||
let mut diff_list = Vec::new();
|
||
for row in range.row..range.row + range.height {
|
||
for column in range.column..range.column + range.width {
|
||
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.cell_clear_all(sheet, row, column)?;
|
||
diff_list.push(Diff::CellClearAll {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_value),
|
||
old_style: Box::new(old_style),
|
||
});
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
/// Deletes the content in cells, but keeps the style
|
||
///
|
||
/// See also:
|
||
/// * [Model::cell_clear_contents]
|
||
pub fn range_clear_contents(&mut self, range: &Area) -> Result<(), String> {
|
||
let sheet = range.sheet;
|
||
let mut diff_list = Vec::new();
|
||
// TODO: full rows/columns
|
||
for row in range.row..range.row + range.height {
|
||
for column in range.column..range.column + range.width {
|
||
let old_value = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.cell(row, column)
|
||
.cloned();
|
||
self.model.cell_clear_contents(sheet, row, column)?;
|
||
diff_list.push(Diff::CellClearContents {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_value),
|
||
});
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
fn clear_column_formatting(
|
||
&mut self,
|
||
sheet: u32,
|
||
column: i32,
|
||
diff_list: &mut Vec<Diff>,
|
||
) -> Result<(), String> {
|
||
let old_value = self.model.get_column_style(sheet, column)?;
|
||
self.model.delete_column_style(sheet, column)?;
|
||
diff_list.push(Diff::DeleteColumnStyle {
|
||
sheet,
|
||
column,
|
||
old_value: Box::new(old_value),
|
||
});
|
||
|
||
let data_rows: Vec<i32> = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.sheet_data
|
||
.keys()
|
||
.copied()
|
||
.collect();
|
||
let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
|
||
|
||
// Delete the formatting in all non empty cells
|
||
for row in data_rows {
|
||
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
|
||
// We can always assume that style with style_index 0 exists and it is the default
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: Box::new(Some(old_style)),
|
||
});
|
||
} else {
|
||
let old_style = self.model.get_style_for_cell(sheet, row, column)?;
|
||
if old_style != Style::default() {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: Box::new(None),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
// Delete the formatting in all cells with a row style
|
||
for row in styled_rows {
|
||
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row.r, column)? {
|
||
// We can always assume that style with style_index 0 exists and it is the default
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row.r, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row: row.r,
|
||
column,
|
||
old_style: Box::new(Some(old_style)),
|
||
});
|
||
} else {
|
||
let old_style = self.model.get_style_for_cell(sheet, row.r, column)?;
|
||
if old_style != Style::default() {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row.r, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row: row.r,
|
||
column,
|
||
old_style: Box::new(None),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn clear_row_formatting(
|
||
&mut self,
|
||
sheet: u32,
|
||
row: i32,
|
||
diff_list: &mut Vec<Diff>,
|
||
) -> Result<(), String> {
|
||
let old_value = self.model.get_row_style(sheet, row)?;
|
||
self.model.delete_row_style(sheet, row)?;
|
||
diff_list.push(Diff::DeleteRowStyle {
|
||
sheet,
|
||
row,
|
||
old_value: Box::new(old_value),
|
||
});
|
||
|
||
// Delete the formatting in all non empty cells
|
||
let columns: Vec<i32> = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.sheet_data
|
||
.get(&row)
|
||
.map(|row_data| row_data.keys().copied().collect())
|
||
.unwrap_or_default();
|
||
for column in columns {
|
||
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
|
||
// We can always assume that style with style_index 0 exists and it is the default
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: Box::new(Some(old_style)),
|
||
});
|
||
} else {
|
||
let old_style = self.model.get_style_for_cell(sheet, row, column)?;
|
||
if old_style != Style::default() {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: Box::new(None),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Removes cells styles and formatting, but keeps the content
|
||
///
|
||
/// See also:
|
||
/// * [UserModel::range_clear_all]
|
||
/// * [UserModel::range_clear_contents]
|
||
pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
|
||
let sheet = range.sheet;
|
||
let mut diff_list = Vec::new();
|
||
if range.row == 1 && range.height == LAST_ROW {
|
||
for column in range.column..range.column + range.width {
|
||
self.clear_column_formatting(sheet, column, &mut diff_list)?;
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
return Ok(());
|
||
}
|
||
if range.column == 1 && range.width == LAST_COLUMN {
|
||
for row in range.row..range.row + range.height {
|
||
self.clear_row_formatting(sheet, row, &mut diff_list)?;
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
return Ok(());
|
||
}
|
||
for row in range.row..range.row + range.height {
|
||
for column in range.column..range.column + range.width {
|
||
if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
|
||
// We can always assume that style with style_index 0 exists and it is the default
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: Box::new(Some(old_style)),
|
||
});
|
||
} else {
|
||
let old_style = self.model.get_style_for_cell(sheet, row, column)?;
|
||
if old_style != Style::default() {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(sheet)?
|
||
.set_cell_style(row, column, 0)?;
|
||
diff_list.push(Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: Box::new(None),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
/// Inserts `row_count` blank rows starting at `row` (both 0-based).
|
||
///
|
||
/// Parameters
|
||
/// * `sheet` – worksheet index.
|
||
/// * `row` – first row to insert.
|
||
/// * `row_count` – number of rows (> 0).
|
||
///
|
||
/// History: the method pushes `row_count` [`crate::user_model::history::Diff::InsertRow`]
|
||
/// items **all using the same `row` index**. Replaying those diffs (undo / redo)
|
||
/// is therefore immune to the row-shifts that happen after each individual
|
||
/// insertion.
|
||
///
|
||
/// See also [`Model::insert_rows`].
|
||
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||
self.model.insert_rows(sheet, row, row_count)?;
|
||
|
||
let diff_list = vec![Diff::InsertRows {
|
||
sheet,
|
||
row,
|
||
count: row_count,
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Inserts `column_count` blank columns starting at `column` (0-based).
|
||
///
|
||
/// Parameters
|
||
/// * `sheet` – worksheet index.
|
||
/// * `column` – first column to insert.
|
||
/// * `column_count` – number of columns (> 0).
|
||
///
|
||
/// History: pushes one [`crate::user_model::history::Diff::InsertColumn`]
|
||
/// per inserted column, all with the same `column` value, preventing index
|
||
/// drift when the diffs are reapplied.
|
||
///
|
||
/// See also [`Model::insert_columns`].
|
||
pub fn insert_columns(
|
||
&mut self,
|
||
sheet: u32,
|
||
column: i32,
|
||
column_count: i32,
|
||
) -> Result<(), String> {
|
||
self.model.insert_columns(sheet, column, column_count)?;
|
||
|
||
let diff_list = vec![Diff::InsertColumns {
|
||
sheet,
|
||
column,
|
||
count: column_count,
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Deletes `row_count` rows starting at `row`.
|
||
///
|
||
/// History: a [`crate::user_model::history::Diff::DeleteRow`] is created for
|
||
/// each row, ordered **bottom → top**. Undo therefore recreates rows from
|
||
/// top → bottom and redo removes them bottom → top, avoiding index drift.
|
||
///
|
||
/// See also [`Model::delete_rows`].
|
||
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||
let mut old_data = Vec::new();
|
||
// Collect data for all rows to be deleted
|
||
for r in row..row + row_count {
|
||
let mut row_data = None;
|
||
for rd in &worksheet.rows {
|
||
if rd.r == r {
|
||
row_data = Some(rd.clone());
|
||
break;
|
||
}
|
||
}
|
||
let data = match worksheet.sheet_data.get(&r) {
|
||
Some(s) => s.clone(),
|
||
None => HashMap::new(),
|
||
};
|
||
old_data.push(RowData {
|
||
row: row_data,
|
||
data,
|
||
});
|
||
}
|
||
|
||
self.model.delete_rows(sheet, row, row_count)?;
|
||
|
||
let diff_list = vec![Diff::DeleteRows {
|
||
sheet,
|
||
row,
|
||
count: row_count,
|
||
old_data,
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Deletes `column_count` columns starting at `column`.
|
||
///
|
||
/// History: pushes one [`crate::user_model::history::Diff::DeleteColumn`]
|
||
/// per column, **right → left**, so replaying the list is always safe with
|
||
/// respect to index shifts.
|
||
///
|
||
/// See also [`Model::delete_columns`].
|
||
pub fn delete_columns(
|
||
&mut self,
|
||
sheet: u32,
|
||
column: i32,
|
||
column_count: i32,
|
||
) -> Result<(), String> {
|
||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||
let mut old_data = Vec::new();
|
||
// Collect data for all columns to be deleted
|
||
for c in column..column + column_count {
|
||
let mut column_data = None;
|
||
for col in &worksheet.cols {
|
||
if c >= col.min && c <= col.max {
|
||
column_data = Some(Col {
|
||
min: c,
|
||
max: c,
|
||
width: col.width,
|
||
custom_width: col.custom_width,
|
||
style: col.style,
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
|
||
let mut data = HashMap::new();
|
||
for (row_idx, row_data) in &worksheet.sheet_data {
|
||
if let Some(cell) = row_data.get(&c) {
|
||
data.insert(*row_idx, cell.clone());
|
||
}
|
||
}
|
||
|
||
old_data.push(ColumnData {
|
||
column: column_data,
|
||
data,
|
||
});
|
||
}
|
||
|
||
self.model.delete_columns(sheet, column, column_count)?;
|
||
|
||
let diff_list = vec![Diff::DeleteColumns {
|
||
sheet,
|
||
column,
|
||
count: column_count,
|
||
old_data,
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Moves a column horizontally and adjusts formulas
|
||
pub fn move_column_action(
|
||
&mut self,
|
||
sheet: u32,
|
||
column: i32,
|
||
delta: i32,
|
||
) -> Result<(), String> {
|
||
let diff_list = vec![Diff::MoveColumn {
|
||
sheet,
|
||
column,
|
||
delta,
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.model.move_column_action(sheet, column, delta)?;
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Moves a row vertically and adjusts formulas
|
||
pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
|
||
let diff_list = vec![Diff::MoveRow { sheet, row, delta }];
|
||
self.push_diff_list(diff_list);
|
||
self.model.move_row_action(sheet, row, delta)?;
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Sets the width of a group of columns in a single diff list
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_column_width]
|
||
pub fn set_columns_width(
|
||
&mut self,
|
||
sheet: u32,
|
||
column_start: i32,
|
||
column_end: i32,
|
||
width: f64,
|
||
) -> Result<(), String> {
|
||
let mut diff_list = Vec::new();
|
||
for column in column_start..=column_end {
|
||
let old_value = self.model.get_column_width(sheet, column)?;
|
||
diff_list.push(Diff::SetColumnWidth {
|
||
sheet,
|
||
column,
|
||
new_value: width,
|
||
old_value,
|
||
});
|
||
self.model.set_column_width(sheet, column, width)?;
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
/// Sets the height of a range of rows in a single diff list
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_row_height]
|
||
pub fn set_rows_height(
|
||
&mut self,
|
||
sheet: u32,
|
||
row_start: i32,
|
||
row_end: i32,
|
||
height: f64,
|
||
) -> Result<(), String> {
|
||
let mut diff_list = Vec::new();
|
||
for row in row_start..=row_end {
|
||
let old_value = self.model.get_row_height(sheet, row)?;
|
||
diff_list.push(Diff::SetRowHeight {
|
||
sheet,
|
||
row,
|
||
new_value: height,
|
||
old_value,
|
||
});
|
||
self.model.set_row_height(sheet, row, height)?;
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
/// Gets the height of a row
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_row_height]
|
||
#[inline]
|
||
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
|
||
self.model.get_row_height(sheet, row)
|
||
}
|
||
|
||
/// Gets the width of a column
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_column_width]
|
||
#[inline]
|
||
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
|
||
self.model.get_column_width(sheet, column)
|
||
}
|
||
|
||
/// Returns the number of frozen rows in the sheet
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_frozen_rows_count()]
|
||
#[inline]
|
||
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, String> {
|
||
self.model.get_frozen_rows_count(sheet)
|
||
}
|
||
|
||
/// Returns the number of frozen columns in the sheet
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_frozen_columns_count()]
|
||
#[inline]
|
||
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, String> {
|
||
self.model.get_frozen_columns_count(sheet)
|
||
}
|
||
|
||
/// Sets the number of frozen rows in sheet
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_frozen_rows()]
|
||
pub fn set_frozen_rows_count(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> {
|
||
let old_value = self.model.get_frozen_rows_count(sheet)?;
|
||
self.push_diff_list(vec![Diff::SetFrozenRowsCount {
|
||
sheet,
|
||
new_value: frozen_rows,
|
||
old_value,
|
||
}]);
|
||
self.model.set_frozen_rows(sheet, frozen_rows)
|
||
}
|
||
|
||
/// Sets the number of frozen columns in sheet
|
||
///
|
||
/// See also:
|
||
/// * [Model::set_frozen_columns()]
|
||
pub fn set_frozen_columns_count(
|
||
&mut self,
|
||
sheet: u32,
|
||
frozen_columns: i32,
|
||
) -> Result<(), String> {
|
||
let old_value = self.model.get_frozen_columns_count(sheet)?;
|
||
self.push_diff_list(vec![Diff::SetFrozenColumnsCount {
|
||
sheet,
|
||
new_value: frozen_columns,
|
||
old_value,
|
||
}]);
|
||
self.model.set_frozen_columns(sheet, frozen_columns)
|
||
}
|
||
|
||
/// Paste `styles` in the selected area
|
||
pub fn on_paste_styles(&mut self, styles: &[Vec<Style>]) -> Result<(), String> {
|
||
let styles_height = styles.len() as i32;
|
||
let styles_width = styles[0].len() as i32;
|
||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||
view.sheet
|
||
} else {
|
||
return Ok(());
|
||
};
|
||
let range = if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||
view.range
|
||
} else {
|
||
return Ok(());
|
||
}
|
||
} else {
|
||
return Ok(());
|
||
};
|
||
|
||
// If the pasted area is smaller than the selected area we increase it
|
||
let [row_start, column_start, row_end, column_end] = range;
|
||
let last_row = row_end.max(row_start + styles_height - 1);
|
||
let last_column = column_end.max(column_start + styles_width - 1);
|
||
|
||
let mut diff_list = Vec::new();
|
||
for row in row_start..=last_row {
|
||
for column in column_start..=last_column {
|
||
let row_index = ((row - row_start) % styles_height) as usize;
|
||
let column_index = ((column - column_start) % styles_width) as usize;
|
||
let style = &styles[row_index][column_index];
|
||
let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||
self.model.set_cell_style(sheet, row, column, style)?;
|
||
diff_list.push(Diff::SetCellStyle {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_value),
|
||
new_value: Box::new(style.clone()),
|
||
});
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
|
||
// select the pasted range
|
||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||
view.range = [row_start, column_start, last_row, last_column];
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
// Updates the style of a cell, adding the new style to the diff list
|
||
fn update_single_cell_style(
|
||
&mut self,
|
||
sheet: u32,
|
||
row: i32,
|
||
column: i32,
|
||
style_path: &str,
|
||
value: &str,
|
||
diff_list: &mut Vec<Diff>,
|
||
) -> Result<(), String> {
|
||
// This is the value in the cell itself
|
||
let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||
|
||
// This takes into account row or column styles. If none of those are present, it will return the default style
|
||
let old_style = self.get_cell_style(sheet, row, column)?;
|
||
let new_style = update_style(&old_style, style_path, value)?;
|
||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||
diff_list.push(Diff::SetCellStyle {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_value),
|
||
new_value: Box::new(new_style),
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Updates the range with a cell style.
|
||
/// See also:
|
||
/// * [Model::set_cell_style]
|
||
pub fn update_range_style(
|
||
&mut self,
|
||
range: &Area,
|
||
style_path: &str,
|
||
value: &str,
|
||
) -> Result<(), String> {
|
||
let sheet = range.sheet;
|
||
let mut diff_list = Vec::new();
|
||
if range.row == 1 && range.height == LAST_ROW {
|
||
// Full columns
|
||
let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
|
||
// We need all the rows in the column to update the style
|
||
// NB: This is too much, this is all the rows that have values
|
||
let data_rows: Vec<i32> = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.sheet_data
|
||
.keys()
|
||
.copied()
|
||
.collect();
|
||
for column in range.column..range.column + range.width {
|
||
// we set the style of the full column
|
||
let old_style = self.model.get_column_style(sheet, column)?;
|
||
let style = match old_style.as_ref() {
|
||
Some(s) => s,
|
||
None => &Style::default(),
|
||
};
|
||
let style = update_style(style, style_path, value)?;
|
||
self.model.set_column_style(sheet, column, &style)?;
|
||
diff_list.push(Diff::SetColumnStyle {
|
||
sheet,
|
||
column,
|
||
old_value: Box::new(old_style),
|
||
new_value: Box::new(style),
|
||
});
|
||
|
||
// We need to update the styles in all cells that have a row style
|
||
for row_s in styled_rows.iter() {
|
||
let row = row_s.r;
|
||
self.update_single_cell_style(
|
||
sheet,
|
||
row,
|
||
column,
|
||
style_path,
|
||
value,
|
||
&mut diff_list,
|
||
)?;
|
||
}
|
||
|
||
// Update style in all cells that have different styles
|
||
// FIXME: We need a better way to transverse of cells in a column
|
||
for &row in &data_rows {
|
||
if let Some(data_row) =
|
||
self.model.workbook.worksheet(sheet)?.sheet_data.get(&row)
|
||
{
|
||
if data_row.get(&column).is_some() {
|
||
// If the cell has non empty content it will always have some style
|
||
self.update_single_cell_style(
|
||
sheet,
|
||
row,
|
||
column,
|
||
style_path,
|
||
value,
|
||
&mut diff_list,
|
||
)?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if range.column == 1 && range.width == LAST_COLUMN {
|
||
// Full rows
|
||
let styled_columns = &self.model.workbook.worksheet(sheet)?.cols.clone();
|
||
for row in range.row..range.row + range.height {
|
||
// Now update style in all cells that are not empty
|
||
let columns: Vec<i32> = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.sheet_data
|
||
.get(&row)
|
||
.map(|row_data| row_data.keys().copied().collect())
|
||
.unwrap_or_default();
|
||
for column in columns {
|
||
self.update_single_cell_style(
|
||
sheet,
|
||
row,
|
||
column,
|
||
style_path,
|
||
value,
|
||
&mut diff_list,
|
||
)?;
|
||
}
|
||
|
||
// We need to go through all the cells that have a column style and merge the styles
|
||
for col in styled_columns.iter() {
|
||
for column in col.min..col.max + 1 {
|
||
self.update_single_cell_style(
|
||
sheet,
|
||
row,
|
||
column,
|
||
style_path,
|
||
value,
|
||
&mut diff_list,
|
||
)?;
|
||
}
|
||
}
|
||
|
||
// Finally update the style of the row
|
||
let old_style = self.model.get_row_style(sheet, row)?;
|
||
let style = match old_style.as_ref() {
|
||
Some(s) => s,
|
||
None => &Style::default(),
|
||
};
|
||
let style = update_style(style, style_path, value)?;
|
||
self.model.set_row_style(sheet, row, &style)?;
|
||
diff_list.push(Diff::SetRowStyle {
|
||
sheet,
|
||
row,
|
||
old_value: Box::new(old_style),
|
||
new_value: Box::new(style),
|
||
});
|
||
}
|
||
} else {
|
||
for row in range.row..range.row + range.height {
|
||
for column in range.column..range.column + range.width {
|
||
self.update_single_cell_style(
|
||
sheet,
|
||
row,
|
||
column,
|
||
style_path,
|
||
value,
|
||
&mut diff_list,
|
||
)?;
|
||
}
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns the style for a cell
|
||
///
|
||
/// Cells share a border, so the left border of B1 is the right border of A1
|
||
/// In the object structure the borders of the cells might be difference,
|
||
/// We always pick the "heaviest" border.
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_style_for_cell]
|
||
pub fn get_cell_style(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
|
||
let mut style = self.model.get_style_for_cell(sheet, row, column)?;
|
||
|
||
// We need to check if the adjacent cells have a "heavier" border
|
||
let border_top = if row > 1 {
|
||
self.model
|
||
.get_style_for_cell(sheet, row - 1, column)?
|
||
.border
|
||
.bottom
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let border_right = if column < LAST_COLUMN {
|
||
self.model
|
||
.get_style_for_cell(sheet, row, column + 1)?
|
||
.border
|
||
.left
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let border_bottom = if row < LAST_ROW {
|
||
self.model
|
||
.get_style_for_cell(sheet, row + 1, column)?
|
||
.border
|
||
.top
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let border_left = if column > 1 {
|
||
self.model
|
||
.get_style_for_cell(sheet, row, column - 1)?
|
||
.border
|
||
.right
|
||
} else {
|
||
None
|
||
};
|
||
|
||
if is_max_border(style.border.top.as_ref(), border_top.as_ref()) {
|
||
style.border.top = border_top;
|
||
}
|
||
|
||
if is_max_border(style.border.right.as_ref(), border_right.as_ref()) {
|
||
style.border.right = border_right;
|
||
}
|
||
|
||
if is_max_border(style.border.bottom.as_ref(), border_bottom.as_ref()) {
|
||
style.border.bottom = border_bottom;
|
||
}
|
||
|
||
if is_max_border(style.border.left.as_ref(), border_left.as_ref()) {
|
||
style.border.left = border_left;
|
||
}
|
||
|
||
Ok(style)
|
||
}
|
||
|
||
/// Fills the cells from `source_area` until `to_row`.
|
||
/// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards)
|
||
pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: i32) -> Result<(), String> {
|
||
let mut diff_list = Vec::new();
|
||
let sheet = source_area.sheet;
|
||
let row1 = source_area.row;
|
||
let column1 = source_area.column;
|
||
let width1 = source_area.width;
|
||
let height1 = source_area.height;
|
||
|
||
// Check first all parameters are valid
|
||
if self.model.workbook.worksheet(sheet).is_err() {
|
||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
||
}
|
||
|
||
if !is_valid_column_number(column1) {
|
||
return Err(format!("Invalid column: '{column1}'"));
|
||
}
|
||
if !is_valid_row(row1) {
|
||
return Err(format!("Invalid row: '{row1}'"));
|
||
}
|
||
if !is_valid_column_number(column1 + width1 - 1) {
|
||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
||
}
|
||
if !is_valid_row(row1 + height1 - 1) {
|
||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
||
}
|
||
|
||
if !is_valid_row(to_row) {
|
||
return Err(format!("Invalid row: '{to_row}'"));
|
||
}
|
||
|
||
// anchor_row is the first row that repeats in each case.
|
||
let anchor_row;
|
||
let sign;
|
||
// this is the range of rows we are going to fill
|
||
let row_range: Vec<i32>;
|
||
|
||
if to_row >= row1 + height1 {
|
||
// we go downwards, we start from `row1 + height1` to `to_row`,
|
||
anchor_row = row1;
|
||
sign = 1;
|
||
row_range = (row1 + height1..to_row + 1).collect();
|
||
} else if to_row < row1 {
|
||
// we go upwards, starting from `row1 - `` all the way to `to_row`
|
||
anchor_row = row1 + height1 - 1;
|
||
sign = -1;
|
||
row_range = (to_row..row1).rev().collect();
|
||
} else {
|
||
return Err("Invalid parameters for autofill".to_string());
|
||
}
|
||
|
||
for column in column1..column1 + width1 {
|
||
let mut index = 0;
|
||
for row_ref in &row_range {
|
||
// Save value and style first
|
||
let row = *row_ref;
|
||
let old_value = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.cell(row, column)
|
||
.cloned();
|
||
let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||
|
||
// compute the new value and set it
|
||
let source_row = anchor_row + index;
|
||
let target_value = self
|
||
.model
|
||
.extend_to(sheet, source_row, column, row, column)?;
|
||
self.model
|
||
.set_user_input(sheet, row, column, target_value.to_string())?;
|
||
|
||
// Compute the new style and set it
|
||
let new_style = self.model.get_style_for_cell(sheet, source_row, column)?;
|
||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||
|
||
// Add the diffs
|
||
diff_list.push(Diff::SetCellStyle {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_style),
|
||
new_value: Box::new(new_style),
|
||
});
|
||
diff_list.push(Diff::SetCellValue {
|
||
sheet,
|
||
row,
|
||
column,
|
||
new_value: target_value.to_string(),
|
||
old_value: Box::new(old_value),
|
||
});
|
||
|
||
index = (index + sign) % height1;
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate();
|
||
Ok(())
|
||
}
|
||
|
||
/// Fills the cells from `source_area` until `to_column`.
|
||
/// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
|
||
pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
|
||
let mut diff_list = Vec::new();
|
||
let sheet = source_area.sheet;
|
||
let first_row = source_area.row;
|
||
let first_column = source_area.column;
|
||
let last_column = first_column + source_area.width - 1;
|
||
let last_row = first_row + source_area.height - 1;
|
||
|
||
// Check first all parameters are valid
|
||
if self.model.workbook.worksheet(sheet).is_err() {
|
||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
||
}
|
||
|
||
if !is_valid_column_number(first_column) {
|
||
return Err(format!("Invalid column: '{first_column}'"));
|
||
}
|
||
if !is_valid_row(first_row) {
|
||
return Err(format!("Invalid row: '{first_row}'"));
|
||
}
|
||
if !is_valid_column_number(last_column) {
|
||
return Err(format!("Invalid column: '{last_column}'"));
|
||
}
|
||
if !is_valid_row(last_row) {
|
||
return Err(format!("Invalid row: '{last_row}'"));
|
||
}
|
||
|
||
if !is_valid_row(to_column) {
|
||
return Err(format!("Invalid row: '{to_column}'"));
|
||
}
|
||
|
||
// anchor_column is the first column that repeats in each case.
|
||
let anchor_column;
|
||
let sign;
|
||
// this is the range of columns we are going to fill
|
||
let column_range: Vec<i32>;
|
||
|
||
if to_column > last_column {
|
||
// we go right, we start from `1 + width` to `to_column`,
|
||
anchor_column = first_column;
|
||
sign = 1;
|
||
column_range = (last_column + 1..to_column + 1).collect();
|
||
} else if to_column < first_column {
|
||
// we go left, starting from `column1 - `` all the way to `to_column`
|
||
anchor_column = last_column;
|
||
sign = -1;
|
||
column_range = (to_column..first_column).rev().collect();
|
||
} else {
|
||
return Err("Invalid parameters for autofill".to_string());
|
||
}
|
||
|
||
for row in first_row..=last_row {
|
||
let mut index = 0;
|
||
for column_ref in &column_range {
|
||
let column = *column_ref;
|
||
// Save value and style first
|
||
let old_value = self
|
||
.model
|
||
.workbook
|
||
.worksheet(sheet)?
|
||
.cell(row, column)
|
||
.cloned();
|
||
let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||
|
||
// compute the new value and set it
|
||
let source_column = anchor_column + index;
|
||
let target_value = self
|
||
.model
|
||
.extend_to(sheet, row, source_column, row, column)?;
|
||
self.model
|
||
.set_user_input(sheet, row, column, target_value.to_string())?;
|
||
|
||
let new_style = self.model.get_style_for_cell(sheet, row, source_column)?;
|
||
// Compute the new style and set it
|
||
|
||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||
|
||
// Add the diffs
|
||
diff_list.push(Diff::SetCellStyle {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_style),
|
||
new_value: Box::new(new_style),
|
||
});
|
||
|
||
diff_list.push(Diff::SetCellValue {
|
||
sheet,
|
||
row,
|
||
column,
|
||
new_value: target_value.to_string(),
|
||
old_value: Box::new(old_value),
|
||
});
|
||
|
||
index = (index + sign) % source_area.width;
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate();
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns information about the sheets
|
||
///
|
||
/// See also:
|
||
/// * [Model::get_worksheets_properties]
|
||
#[inline]
|
||
pub fn get_worksheets_properties(&self) -> Vec<SheetProperties> {
|
||
self.model.get_worksheets_properties()
|
||
}
|
||
|
||
/// Set the gid lines in the worksheet to visible (`true`) or hidden (`false`)
|
||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
||
let old_value = self.model.workbook.worksheet(sheet)?.show_grid_lines;
|
||
self.model.set_show_grid_lines(sheet, show_grid_lines)?;
|
||
|
||
self.push_diff_list(vec![Diff::SetShowGridLines {
|
||
sheet,
|
||
new_value: show_grid_lines,
|
||
old_value,
|
||
}]);
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns true in the grid lines for
|
||
pub fn get_show_grid_lines(&self, sheet: u32) -> Result<bool, String> {
|
||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
||
}
|
||
|
||
/// Returns the largest column in the row less than a column whose cell has a non empty value.
|
||
/// If there are none it returns `None`.
|
||
/// This is useful when rendering a part of a worksheet to know which cells spill over
|
||
pub fn get_last_non_empty_in_row_before_column(
|
||
&self,
|
||
sheet: u32,
|
||
row: i32,
|
||
column: i32,
|
||
) -> Result<Option<i32>, String> {
|
||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||
let data = worksheet.sheet_data.get(&row);
|
||
if let Some(row_data) = data {
|
||
let mut last_column = None;
|
||
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||
columns.sort_unstable();
|
||
for col in columns {
|
||
if col < column {
|
||
if let Some(cell) = worksheet.cell(row, col) {
|
||
if matches!(cell, Cell::EmptyCell { .. }) {
|
||
continue;
|
||
}
|
||
}
|
||
last_column = Some(col);
|
||
}
|
||
}
|
||
Ok(last_column)
|
||
} else {
|
||
Ok(None)
|
||
}
|
||
}
|
||
|
||
/// Returns the smallest column in the row larger than "column" whose cell has a non empty value.
|
||
/// If there are none it returns `None`.
|
||
/// This is useful when rendering a part of a worksheet to know which cells spill over
|
||
pub fn get_first_non_empty_in_row_after_column(
|
||
&self,
|
||
sheet: u32,
|
||
row: i32,
|
||
column: i32,
|
||
) -> Result<Option<i32>, String> {
|
||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||
let data = worksheet.sheet_data.get(&row);
|
||
if let Some(row_data) = data {
|
||
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||
// We sort the keys to ensure we are going from left to right
|
||
columns.sort_unstable();
|
||
for col in columns {
|
||
if col > column {
|
||
if let Some(cell) = worksheet.cell(row, col) {
|
||
if matches!(cell, Cell::EmptyCell { .. }) {
|
||
continue;
|
||
}
|
||
}
|
||
return Ok(Some(col));
|
||
}
|
||
}
|
||
}
|
||
Ok(None)
|
||
}
|
||
|
||
/// 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().delimiter(b'\t').from_writer(vec![]);
|
||
|
||
let mut data = HashMap::new();
|
||
let [row_start, column_start, row_end, column_end] = selected_area.range;
|
||
let dimension = self.model.workbook.worksheet(sheet)?.dimension();
|
||
let row_end = row_end.min(dimension.max_row);
|
||
let column_end = column_end.min(dimension.max_column);
|
||
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.model.get_style_for_cell(sheet, row, column)?;
|
||
data_row.insert(
|
||
column,
|
||
ClipboardCell {
|
||
text: content,
|
||
style,
|
||
},
|
||
);
|
||
text_row.push(text);
|
||
}
|
||
wtr.write_record(text_row)
|
||
.map_err(|e| format!("Error while processing csv: {e}"))?;
|
||
data.insert(row, data_row);
|
||
}
|
||
|
||
let csv = String::from_utf8(
|
||
wtr.into_inner()
|
||
.map_err(|e| format!("Processing error: '{e}'"))?,
|
||
)
|
||
.map_err(|e| format!("Error converting from utf8: '{e}'"))?;
|
||
|
||
Ok(Clipboard {
|
||
csv: csv.trim().to_string(),
|
||
data,
|
||
sheet,
|
||
range: (row_start, column_start, row_end, column_end),
|
||
})
|
||
}
|
||
|
||
/// Paste text that we copied
|
||
pub fn paste_from_clipboard(
|
||
&mut self,
|
||
source_sheet: u32,
|
||
source_range: ClipboardTuple,
|
||
clipboard: &ClipboardData,
|
||
is_cut: bool,
|
||
) -> Result<(), String> {
|
||
let mut diff_list = Vec::new();
|
||
let view = self.get_selected_view();
|
||
let (source_first_row, source_first_column, source_last_row, source_last_column) =
|
||
source_range;
|
||
let sheet = view.sheet;
|
||
let [selected_row, selected_column, _, _] = view.range;
|
||
let mut max_row = selected_row;
|
||
let mut max_column = selected_column;
|
||
let area = &Area {
|
||
sheet,
|
||
row: source_first_row,
|
||
column: source_first_column,
|
||
width: source_last_column - source_first_column + 1,
|
||
height: source_last_row - source_first_row + 1,
|
||
};
|
||
for (source_row, data_row) in clipboard {
|
||
let delta_row = source_row - source_first_row;
|
||
let target_row = selected_row + delta_row;
|
||
max_row = max_row.max(target_row);
|
||
for (source_column, value) in data_row {
|
||
let delta_column = source_column - source_first_column;
|
||
let target_column = selected_column + delta_column;
|
||
max_column = max_column.max(target_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 = if is_cut {
|
||
self.model
|
||
.move_cell_value_to_area(&value.text, source, target, area)?
|
||
} else {
|
||
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_cell_style_or_none(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()),
|
||
});
|
||
}
|
||
}
|
||
if is_cut {
|
||
for row in source_first_row..=source_last_row {
|
||
for column in source_first_column..=source_last_column {
|
||
let old_value = self
|
||
.model
|
||
.workbook
|
||
.worksheet(source_sheet)?
|
||
.cell(row, column)
|
||
.cloned();
|
||
|
||
diff_list.push(Diff::CellClearContents {
|
||
sheet: source_sheet,
|
||
row,
|
||
column,
|
||
old_value: Box::new(old_value),
|
||
});
|
||
self.model.cell_clear_contents(source_sheet, row, column)?;
|
||
}
|
||
}
|
||
}
|
||
self.push_diff_list(diff_list);
|
||
// select the pasted area
|
||
self.set_selected_range(selected_row, selected_column, max_row, max_column)?;
|
||
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;
|
||
let mut column = area.column;
|
||
let mut csv_reader = Cursor::new(csv);
|
||
csv_reader.set_position(0);
|
||
let mut reader = ReaderBuilder::new()
|
||
.delimiter(b'\t')
|
||
.has_headers(false)
|
||
.from_reader(csv_reader);
|
||
for record in reader.records() {
|
||
match record {
|
||
Ok(r) => {
|
||
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);
|
||
// select the pasted area
|
||
self.set_selected_range(area.row, area.column, row - 1, column - 1)?;
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns the list of defined names
|
||
pub fn get_defined_name_list(&self) -> Vec<(String, Option<u32>, String)> {
|
||
self.model.workbook.get_defined_names_with_scope()
|
||
}
|
||
|
||
/// Delete an existing defined name
|
||
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
|
||
let old_value = self.model.get_defined_name_formula(name, scope)?;
|
||
let diff_list = vec![Diff::DeleteDefinedName {
|
||
name: name.to_string(),
|
||
scope,
|
||
old_value,
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.model.delete_defined_name(name, scope)?;
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Create a new defined name
|
||
pub fn new_defined_name(
|
||
&mut self,
|
||
name: &str,
|
||
scope: Option<u32>,
|
||
formula: &str,
|
||
) -> Result<(), String> {
|
||
self.model.new_defined_name(name, scope, formula)?;
|
||
let diff_list = vec![Diff::CreateDefinedName {
|
||
name: name.to_string(),
|
||
scope,
|
||
value: formula.to_string(),
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
/// Updates a defined name
|
||
pub fn update_defined_name(
|
||
&mut self,
|
||
name: &str,
|
||
scope: Option<u32>,
|
||
new_name: &str,
|
||
new_scope: Option<u32>,
|
||
new_formula: &str,
|
||
) -> Result<(), String> {
|
||
let old_formula = self.model.get_defined_name_formula(name, scope)?;
|
||
let diff_list = vec![Diff::UpdateDefinedName {
|
||
name: name.to_string(),
|
||
scope,
|
||
old_formula: old_formula.to_string(),
|
||
new_name: new_name.to_string(),
|
||
new_scope,
|
||
new_formula: new_formula.to_string(),
|
||
}];
|
||
self.push_diff_list(diff_list);
|
||
self.model
|
||
.update_defined_name(name, scope, new_name, new_scope, new_formula)?;
|
||
self.evaluate_if_not_paused();
|
||
Ok(())
|
||
}
|
||
|
||
// **** Private methods ****** //
|
||
|
||
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
|
||
self.send_queue.push(QueueDiffs {
|
||
r#type: DiffType::Redo,
|
||
list: diff_list.clone(),
|
||
});
|
||
self.history.push(diff_list);
|
||
}
|
||
|
||
fn evaluate_if_not_paused(&mut self) {
|
||
if !self.pause_evaluation {
|
||
self.model.evaluate();
|
||
}
|
||
}
|
||
|
||
fn apply_undo_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
|
||
let mut needs_evaluation = false;
|
||
for diff in diff_list.iter().rev() {
|
||
match diff {
|
||
Diff::SetCellValue {
|
||
sheet,
|
||
row,
|
||
column,
|
||
new_value: _,
|
||
old_value,
|
||
} => {
|
||
needs_evaluation = true;
|
||
match *old_value.clone() {
|
||
Some(value) => {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(*sheet)?
|
||
.update_cell(*row, *column, value)?;
|
||
}
|
||
None => {
|
||
self.model.cell_clear_all(*sheet, *row, *column)?;
|
||
}
|
||
}
|
||
}
|
||
Diff::SetColumnWidth {
|
||
sheet,
|
||
column,
|
||
new_value: _,
|
||
old_value,
|
||
} => self.model.set_column_width(*sheet, *column, *old_value)?,
|
||
Diff::SetRowHeight {
|
||
sheet,
|
||
row,
|
||
new_value: _,
|
||
old_value,
|
||
} => self.model.set_row_height(*sheet, *row, *old_value)?,
|
||
Diff::CellClearContents {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value,
|
||
} => {
|
||
needs_evaluation = true;
|
||
if let Some(value) = *old_value.clone() {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(*sheet)?
|
||
.update_cell(*row, *column, value)?;
|
||
}
|
||
}
|
||
Diff::CellClearAll {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value,
|
||
old_style,
|
||
} => {
|
||
needs_evaluation = true;
|
||
if let Some(value) = *old_value.clone() {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(*sheet)?
|
||
.update_cell(*row, *column, value)?;
|
||
self.model
|
||
.set_cell_style(*sheet, *row, *column, old_style)?;
|
||
}
|
||
}
|
||
Diff::SetCellStyle {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value,
|
||
new_value: _,
|
||
} => {
|
||
if let Some(old_style) = old_value.as_ref() {
|
||
self.model
|
||
.set_cell_style(*sheet, *row, *column, old_style)?;
|
||
} else {
|
||
// If the cell did not have a style there was nothing on it
|
||
self.model.cell_clear_all(*sheet, *row, *column)?;
|
||
}
|
||
}
|
||
Diff::InsertRows { sheet, row, count } => {
|
||
self.model.delete_rows(*sheet, *row, *count)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::DeleteRows {
|
||
sheet,
|
||
row,
|
||
count: _,
|
||
old_data,
|
||
} => {
|
||
needs_evaluation = true;
|
||
self.model
|
||
.insert_rows(*sheet, *row, old_data.len() as i32)?;
|
||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||
for (i, row_data) in old_data.iter().enumerate() {
|
||
let r = *row + i as i32;
|
||
if let Some(row_style) = row_data.row.clone() {
|
||
worksheet.rows.push(row_style);
|
||
}
|
||
worksheet.sheet_data.insert(r, row_data.data.clone());
|
||
}
|
||
}
|
||
Diff::InsertColumns {
|
||
sheet,
|
||
column,
|
||
count,
|
||
} => {
|
||
self.model.delete_columns(*sheet, *column, *count)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::DeleteColumns {
|
||
sheet,
|
||
column,
|
||
count: _,
|
||
old_data,
|
||
} => {
|
||
needs_evaluation = true;
|
||
self.model
|
||
.insert_columns(*sheet, *column, old_data.len() as i32)?;
|
||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||
for (i, col_data) in old_data.iter().enumerate() {
|
||
let c = *column + i as i32;
|
||
for (row, cell) in &col_data.data {
|
||
worksheet.update_cell(*row, c, cell.clone())?;
|
||
}
|
||
if let Some(col) = &col_data.column {
|
||
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
|
||
let style = col.style;
|
||
worksheet.set_column_width_and_style(c, width, style)?;
|
||
}
|
||
}
|
||
}
|
||
Diff::SetFrozenRowsCount {
|
||
sheet,
|
||
new_value: _,
|
||
old_value,
|
||
} => self.model.set_frozen_rows(*sheet, *old_value)?,
|
||
Diff::SetFrozenColumnsCount {
|
||
sheet,
|
||
new_value: _,
|
||
old_value,
|
||
} => self.model.set_frozen_columns(*sheet, *old_value)?,
|
||
Diff::NewSheet { index, name: _ } => {
|
||
self.model.delete_sheet(*index)?;
|
||
if *index > 0 {
|
||
self.set_selected_sheet(*index - 1)?;
|
||
}
|
||
}
|
||
Diff::RenameSheet {
|
||
index,
|
||
old_value,
|
||
new_value: _,
|
||
} => {
|
||
self.model.rename_sheet_by_index(*index, old_value)?;
|
||
}
|
||
Diff::SetSheetColor {
|
||
index,
|
||
old_value,
|
||
new_value: _,
|
||
} => {
|
||
self.model.set_sheet_color(*index, old_value)?;
|
||
}
|
||
Diff::SetShowGridLines {
|
||
sheet,
|
||
old_value,
|
||
new_value: _,
|
||
} => {
|
||
self.model.set_show_grid_lines(*sheet, *old_value)?;
|
||
}
|
||
Diff::CreateDefinedName {
|
||
name,
|
||
scope,
|
||
value: _,
|
||
} => {
|
||
self.model.delete_defined_name(name, *scope)?;
|
||
}
|
||
Diff::DeleteDefinedName {
|
||
name,
|
||
scope,
|
||
old_value,
|
||
} => {
|
||
self.model.new_defined_name(name, *scope, old_value)?;
|
||
}
|
||
Diff::UpdateDefinedName {
|
||
name,
|
||
scope,
|
||
old_formula,
|
||
new_name,
|
||
new_scope,
|
||
new_formula: _,
|
||
} => {
|
||
self.model.update_defined_name(
|
||
new_name,
|
||
*new_scope,
|
||
name,
|
||
*scope,
|
||
old_formula,
|
||
)?;
|
||
}
|
||
Diff::SetSheetState {
|
||
index,
|
||
old_value,
|
||
new_value: _,
|
||
} => self.model.set_sheet_state(*index, old_value.clone())?,
|
||
Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style,
|
||
} => {
|
||
if let Some(value) = old_style.as_ref() {
|
||
self.model.set_cell_style(*sheet, *row, *column, value)?;
|
||
} else {
|
||
self.model.cell_clear_all(*sheet, *row, *column)?;
|
||
}
|
||
}
|
||
Diff::DeleteSheet { sheet, old_data } => {
|
||
needs_evaluation = true;
|
||
let sheet_name = &old_data.name.clone();
|
||
let sheet_index = *sheet;
|
||
let sheet_id = old_data.sheet_id;
|
||
self.model
|
||
.insert_sheet(sheet_name, sheet_index, Some(sheet_id))?;
|
||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||
for (row, row_data) in &old_data.sheet_data {
|
||
for (column, cell) in row_data {
|
||
worksheet.update_cell(*row, *column, cell.clone())?;
|
||
}
|
||
}
|
||
worksheet.rows = old_data.rows.clone();
|
||
worksheet.cols = old_data.cols.clone();
|
||
worksheet.show_grid_lines = old_data.show_grid_lines;
|
||
worksheet.frozen_columns = old_data.frozen_columns;
|
||
worksheet.frozen_rows = old_data.frozen_rows;
|
||
worksheet.state = old_data.state.clone();
|
||
worksheet.color = old_data.color.clone();
|
||
worksheet.merge_cells = old_data.merge_cells.clone();
|
||
worksheet.shared_formulas = old_data.shared_formulas.clone();
|
||
self.model.reset_parsed_structures();
|
||
|
||
self.set_selected_sheet(sheet_index)?;
|
||
}
|
||
Diff::SetColumnStyle {
|
||
sheet,
|
||
column,
|
||
old_value,
|
||
new_value: _,
|
||
} => match old_value.as_ref() {
|
||
Some(s) => self.model.set_column_style(*sheet, *column, s)?,
|
||
None => {
|
||
self.model.delete_column_style(*sheet, *column)?;
|
||
}
|
||
},
|
||
Diff::SetRowStyle {
|
||
sheet,
|
||
row,
|
||
old_value,
|
||
new_value: _,
|
||
} => {
|
||
if let Some(s) = old_value.as_ref() {
|
||
self.model.set_row_style(*sheet, *row, s)?;
|
||
} else {
|
||
self.model.delete_row_style(*sheet, *row)?;
|
||
}
|
||
}
|
||
Diff::DeleteColumnStyle {
|
||
sheet,
|
||
column,
|
||
old_value,
|
||
} => {
|
||
if let Some(s) = old_value.as_ref() {
|
||
self.model.set_column_style(*sheet, *column, s)?;
|
||
} else {
|
||
self.model.delete_column_style(*sheet, *column)?;
|
||
}
|
||
}
|
||
Diff::DeleteRowStyle {
|
||
sheet,
|
||
row,
|
||
old_value,
|
||
} => {
|
||
if let Some(s) = old_value.as_ref() {
|
||
self.model.set_row_style(*sheet, *row, s)?;
|
||
} else {
|
||
self.model.delete_row_style(*sheet, *row)?;
|
||
}
|
||
}
|
||
Diff::MoveColumn {
|
||
sheet,
|
||
column,
|
||
delta,
|
||
} => {
|
||
// For undo, we apply the opposite move
|
||
self.model
|
||
.move_column_action(*sheet, *column + *delta, -*delta)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::MoveRow { sheet, row, delta } => {
|
||
// For undo, we apply the opposite move
|
||
self.model.move_row_action(*sheet, *row + *delta, -*delta)?;
|
||
needs_evaluation = true;
|
||
}
|
||
}
|
||
}
|
||
if needs_evaluation {
|
||
self.evaluate_if_not_paused();
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Applies diff list
|
||
fn apply_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
|
||
let mut needs_evaluation = false;
|
||
for diff in diff_list {
|
||
match diff {
|
||
Diff::SetCellValue {
|
||
sheet,
|
||
row,
|
||
column,
|
||
new_value,
|
||
old_value: _,
|
||
} => {
|
||
needs_evaluation = true;
|
||
self.model
|
||
.set_user_input(*sheet, *row, *column, new_value.to_string())?;
|
||
}
|
||
Diff::SetColumnWidth {
|
||
sheet,
|
||
column,
|
||
new_value,
|
||
old_value: _,
|
||
} => {
|
||
self.model.set_column_width(*sheet, *column, *new_value)?;
|
||
}
|
||
Diff::SetRowHeight {
|
||
sheet,
|
||
row,
|
||
new_value,
|
||
old_value: _,
|
||
} => {
|
||
self.model.set_row_height(*sheet, *row, *new_value)?;
|
||
}
|
||
Diff::CellClearContents {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: _,
|
||
} => {
|
||
self.model.cell_clear_contents(*sheet, *row, *column)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::CellClearAll {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: _,
|
||
old_style: _,
|
||
} => {
|
||
self.model.cell_clear_all(*sheet, *row, *column)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::SetCellStyle {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_value: _,
|
||
new_value,
|
||
} => self
|
||
.model
|
||
.set_cell_style(*sheet, *row, *column, new_value)?,
|
||
Diff::InsertRows { sheet, row, count } => {
|
||
self.model.insert_rows(*sheet, *row, *count)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::DeleteRows {
|
||
sheet,
|
||
row,
|
||
count,
|
||
old_data: _,
|
||
} => {
|
||
self.model.delete_rows(*sheet, *row, *count)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::InsertColumns {
|
||
sheet,
|
||
column,
|
||
count,
|
||
} => {
|
||
self.model.insert_columns(*sheet, *column, *count)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::DeleteColumns {
|
||
sheet,
|
||
column,
|
||
count,
|
||
old_data: _,
|
||
} => {
|
||
self.model.delete_columns(*sheet, *column, *count)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::SetFrozenRowsCount {
|
||
sheet,
|
||
new_value,
|
||
old_value: _,
|
||
} => self.model.set_frozen_rows(*sheet, *new_value)?,
|
||
Diff::SetFrozenColumnsCount {
|
||
sheet,
|
||
new_value,
|
||
old_value: _,
|
||
} => self.model.set_frozen_columns(*sheet, *new_value)?,
|
||
Diff::DeleteSheet { sheet, old_data: _ } => {
|
||
self.model.delete_sheet(*sheet)?;
|
||
if *sheet > 0 {
|
||
self.set_selected_sheet(*sheet - 1)?;
|
||
}
|
||
}
|
||
Diff::NewSheet { index, name } => {
|
||
self.model.insert_sheet(name, *index, None)?;
|
||
self.set_selected_sheet(*index)?;
|
||
}
|
||
Diff::RenameSheet {
|
||
index,
|
||
old_value: _,
|
||
new_value,
|
||
} => {
|
||
self.model.rename_sheet_by_index(*index, new_value)?;
|
||
}
|
||
Diff::SetSheetColor {
|
||
index,
|
||
old_value: _,
|
||
new_value,
|
||
} => {
|
||
self.model.set_sheet_color(*index, new_value)?;
|
||
}
|
||
Diff::SetShowGridLines {
|
||
sheet,
|
||
old_value: _,
|
||
new_value,
|
||
} => {
|
||
self.model.set_show_grid_lines(*sheet, *new_value)?;
|
||
}
|
||
Diff::CreateDefinedName { name, scope, value } => {
|
||
self.model.new_defined_name(name, *scope, value)?
|
||
}
|
||
Diff::DeleteDefinedName {
|
||
name,
|
||
scope,
|
||
old_value: _,
|
||
} => self.model.delete_defined_name(name, *scope)?,
|
||
Diff::UpdateDefinedName {
|
||
name,
|
||
scope,
|
||
old_formula: _,
|
||
new_name,
|
||
new_scope,
|
||
new_formula,
|
||
} => self.model.update_defined_name(
|
||
name,
|
||
*scope,
|
||
new_name,
|
||
*new_scope,
|
||
new_formula,
|
||
)?,
|
||
Diff::SetSheetState {
|
||
index,
|
||
old_value: _,
|
||
new_value,
|
||
} => self.model.set_sheet_state(*index, new_value.clone())?,
|
||
Diff::CellClearFormatting {
|
||
sheet,
|
||
row,
|
||
column,
|
||
old_style: _,
|
||
} => {
|
||
self.model
|
||
.workbook
|
||
.worksheet_mut(*sheet)?
|
||
.set_cell_style(*row, *column, 0)?;
|
||
}
|
||
Diff::SetColumnStyle {
|
||
sheet,
|
||
column,
|
||
old_value: _,
|
||
new_value,
|
||
} => {
|
||
self.model.set_column_style(*sheet, *column, new_value)?;
|
||
}
|
||
Diff::SetRowStyle {
|
||
sheet,
|
||
row,
|
||
old_value: _,
|
||
new_value,
|
||
} => {
|
||
self.model.set_row_style(*sheet, *row, new_value)?;
|
||
}
|
||
Diff::DeleteColumnStyle {
|
||
sheet,
|
||
column,
|
||
old_value: _,
|
||
} => {
|
||
self.model.delete_column_style(*sheet, *column)?;
|
||
}
|
||
Diff::DeleteRowStyle {
|
||
sheet,
|
||
row,
|
||
old_value: _,
|
||
} => {
|
||
self.model.delete_row_style(*sheet, *row)?;
|
||
}
|
||
Diff::MoveColumn {
|
||
sheet,
|
||
column,
|
||
delta,
|
||
} => {
|
||
self.model.move_column_action(*sheet, *column, *delta)?;
|
||
needs_evaluation = true;
|
||
}
|
||
Diff::MoveRow { sheet, row, delta } => {
|
||
self.model.move_row_action(*sheet, *row, *delta)?;
|
||
needs_evaluation = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if needs_evaluation {
|
||
self.evaluate_if_not_paused();
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use crate::{
|
||
types::{HorizontalAlignment, VerticalAlignment},
|
||
user_model::common::{horizontal, vertical},
|
||
};
|
||
|
||
#[test]
|
||
fn test_vertical() {
|
||
let all = vec![
|
||
VerticalAlignment::Bottom,
|
||
VerticalAlignment::Center,
|
||
VerticalAlignment::Distributed,
|
||
VerticalAlignment::Justify,
|
||
VerticalAlignment::Top,
|
||
];
|
||
for a in all {
|
||
assert_eq!(vertical(&format!("{a}")), Ok(a));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_horizontal() {
|
||
let all = vec![
|
||
HorizontalAlignment::Center,
|
||
HorizontalAlignment::CenterContinuous,
|
||
HorizontalAlignment::Distributed,
|
||
HorizontalAlignment::Fill,
|
||
HorizontalAlignment::General,
|
||
HorizontalAlignment::Justify,
|
||
HorizontalAlignment::Left,
|
||
HorizontalAlignment::Right,
|
||
];
|
||
for a in all {
|
||
assert_eq!(horizontal(&format!("{a}")), Ok(a));
|
||
}
|
||
}
|
||
}
|