UPDATE: Adds get/set views to the user model API (#69)

This commit is contained in:
Nicolás Hatcher Andrés
2024-05-20 17:51:09 +02:00
committed by GitHub
parent d2cba48f8e
commit 49c3b14bf0
11 changed files with 562 additions and 67 deletions

View File

@@ -118,6 +118,8 @@ pub struct Model {
pub(crate) language: Language,
/// The timezone used to evaluate the model
pub(crate) tz: Tz,
/// The view id. A view consist of a selected sheet and ranges.
pub(crate) view_id: u32,
}
// FIXME: Maybe this should be the same as CellReference
@@ -886,6 +888,7 @@ impl Model {
language,
locale,
tz,
view_id: 0,
};
model.parse_formulas();

View File

@@ -15,7 +15,9 @@ use crate::{
language::get_language,
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{Metadata, Selection, SheetState, Workbook, WorkbookSettings, Worksheet},
types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
},
utils::ParsedReference,
};
@@ -35,7 +37,20 @@ fn is_valid_sheet_name(name: &str) -> bool {
impl Model {
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
let mut views = HashMap::new();
for id in view_ids {
views.insert(
**id,
WorksheetView {
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1,
},
);
}
Worksheet {
cols: vec![],
rows: vec![],
@@ -50,12 +65,7 @@ impl Model {
color: Default::default(),
frozen_columns: 0,
frozen_rows: 0,
selection: Selection {
is_selected: false,
row: 1,
column: 1,
range: [1, 1, 1, 1],
},
views,
}
}
@@ -130,7 +140,7 @@ impl Model {
self.parsed_defined_names = parsed_defined_names;
}
// Reparses all formulas and defined names
/// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) {
self.parser
.set_worksheets(self.workbook.get_worksheet_names());
@@ -161,7 +171,8 @@ impl Model {
let sheet_name = format!("{}{}", base_name, index);
// Now we need a sheet_id
let sheet_id = self.get_new_sheet_id();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
self.workbook.worksheets.push(worksheet);
self.reset_parsed_structures();
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
@@ -192,7 +203,8 @@ impl Model {
Some(id) => id,
None => self.get_new_sheet_id(),
};
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
if sheet_index as usize > self.workbook.worksheets.len() {
return Err("Sheet index out of range".to_string());
}
@@ -339,11 +351,14 @@ impl Model {
// "2020-08-06T21:20:53Z
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
let mut views = HashMap::new();
views.insert(0, WorkbookView { sheet: 0 });
// String versions of the locale are added here to simplify the serialize/deserialize logic
let workbook = Workbook {
shared_strings: vec![],
defined_names: vec![],
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
styles: Default::default(),
name: name.to_string(),
settings: WorkbookSettings {
@@ -359,6 +374,7 @@ impl Model {
last_modified: now,
},
tables: HashMap::new(),
views,
};
let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets;
@@ -379,6 +395,7 @@ impl Model {
locale,
language,
tz,
view_id: 0,
};
model.parse_formulas();
Ok(model)

View File

@@ -8,3 +8,4 @@ mod test_row_column;
mod test_styles;
mod test_to_from_bytes;
mod test_undo_redo;
mod test_view;

View File

@@ -0,0 +1,216 @@
#![allow(clippy::unwrap_used)]
use std::collections::HashMap;
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
user_model::SelectedView,
UserModel,
};
#[test]
fn initial_view() {
let model = new_empty_model();
let model = UserModel::from_model(model);
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn set_the_cell_sets_the_range() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_selected_cell(5, 4).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 5, 4));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 5,
column: 4,
range: [5, 4, 5, 4],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn set_the_range_does_not_set_the_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_selected_range(5, 4, 10, 6).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [5, 4, 10, 6],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn add_new_sheet_and_back() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.new_sheet();
assert_eq!(model.get_selected_sheet(), 0);
model.set_selected_cell(5, 4).unwrap();
model.set_selected_sheet(1).unwrap();
assert_eq!(model.get_selected_cell(), (1, 1, 1));
model.set_selected_sheet(0).unwrap();
assert_eq!(model.get_selected_cell(), (0, 5, 4));
}
#[test]
fn set_selected_cell_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.set_selected_cell(-5, 4),
Err("Invalid row: '-5'".to_string())
);
assert_eq!(
model.set_selected_cell(5, -4),
Err("Invalid column: '-4'".to_string())
);
assert_eq!(
model.set_selected_range(-1, 1, 1, 1),
Err("Invalid row: '-1'".to_string())
);
assert_eq!(
model.set_selected_range(1, 0, 1, 1),
Err("Invalid column: '0'".to_string())
);
assert_eq!(
model.set_selected_range(1, 1, LAST_ROW + 1, 1),
Err("Invalid row: '1048577'".to_string())
);
assert_eq!(
model.set_selected_range(1, 1, 1, LAST_COLUMN + 1),
Err("Invalid column: '16385'".to_string())
);
}
#[test]
fn set_selected_cell_errors_wrong_sheet() {
let mut model = new_empty_model();
// forcefully set a wrong index
model.workbook.views.get_mut(&0).unwrap().sheet = 2;
let mut model = UserModel::from_model(model);
// It's returning the wrong number
assert_eq!(model.get_selected_sheet(), 2);
// But we can't set the selected cell anymore
assert_eq!(
model.set_selected_cell(3, 4),
Err("Invalid worksheet index 2".to_string())
);
assert_eq!(
model.set_selected_range(3, 4, 5, 6),
Err("Invalid worksheet index 2".to_string())
);
assert_eq!(
model.set_top_left_visible_cell(3, 4),
Err("Invalid worksheet index 2".to_string())
);
// we can fix it by setting the right cell
model.set_selected_sheet(0).unwrap();
model.set_selected_cell(3, 4).unwrap();
}
#[test]
fn set_visible_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_top_left_visible_cell(100, 12).unwrap();
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 100,
left_column: 12
}
);
let s = serde_json::to_string(&model.get_selected_view()).unwrap();
assert_eq!(
serde_json::from_str::<SelectedView>(&s).unwrap(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 100,
left_column: 12
}
);
}
#[test]
fn set_visible_cell_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.set_top_left_visible_cell(-100, 12),
Err("Invalid row: '-100'".to_string())
);
assert_eq!(
model.set_top_left_visible_cell(100, -12),
Err("Invalid column: '-12'".to_string())
);
}
#[test]
fn errors_no_views() {
let mut model = new_empty_model();
// forcefully remove the view
model.workbook.views = HashMap::new();
// also in the sheet
model.workbook.worksheets[0].views = HashMap::new();
let mut model = UserModel::from_model(model);
// get methods will return defaults
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1
}
);
// set methods won't complain. but won't work either
model.set_selected_sheet(0).unwrap();
model.set_selected_cell(5, 6).unwrap();
assert_eq!(model.get_selected_cell(), (0, 1, 1));
}

View File

@@ -27,6 +27,14 @@ pub struct WorkbookSettings {
pub tz: String,
pub locale: String,
}
/// A Workbook View tracks of the selected sheet for each view
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct WorkbookView {
/// The index of the currently selected sheet.
pub sheet: u32,
}
/// An internal representation of an IronCalc Workbook
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Workbook {
@@ -38,6 +46,7 @@ pub struct Workbook {
pub settings: WorkbookSettings,
pub metadata: Metadata,
pub tables: HashMap<String, Table>,
pub views: HashMap<u32, WorkbookView>,
}
/// A defined name. The `sheet_id` is the sheet index in case the name is local
@@ -48,9 +57,6 @@ pub struct DefinedName {
pub sheet_id: Option<u32>,
}
// TODO: Move to worksheet.rs make frozen_rows/columns private and u32
/// Internal representation of a worksheet Excel object
/// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
@@ -71,12 +77,21 @@ impl Display for SheetState {
}
}
/// Represents the state of the worksheet as seen by the user. This includes
/// details such as the currently selected cell, the visible range, and the
/// position of the viewport.
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Selection {
pub is_selected: bool,
pub struct WorksheetView {
/// The row index of the currently selected cell.
pub row: i32,
/// The column index of the currently selected cell.
pub column: i32,
/// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
pub range: [i32; 4],
/// The row index of the topmost visible cell in the worksheet view.
pub top_row: i32,
/// The column index of the leftmost visible cell in the worksheet view.
pub left_column: i32,
}
/// Internal representation of a worksheet Excel object
@@ -95,7 +110,7 @@ pub struct Worksheet {
pub comments: Vec<Comment>,
pub frozen_rows: i32,
pub frozen_columns: i32,
pub selection: Selection,
pub views: HashMap<u32, WorksheetView>,
}
/// Internal representation of Excel's sheet_data

View File

@@ -3,6 +3,7 @@
use std::{collections::HashMap, fmt::Debug};
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use crate::{
constants,
@@ -18,6 +19,17 @@ use crate::{
utils::is_valid_hex_color,
};
#[derive(Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Debug))]
pub struct SelectedView {
pub sheet: u32,
pub row: i32,
pub column: i32,
pub range: [i32; 4],
pub top_row: i32,
pub left_column: i32,
}
#[derive(Clone, Encode, Decode)]
struct RowData {
row: Option<Row>,
@@ -118,6 +130,7 @@ enum Diff {
old_value: String,
new_value: String,
},
// FIXME: we are missing SetViewDiffs
}
type DiffList = Vec<Diff>;
@@ -249,7 +262,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
}
/// # A wrapper around [`Model`] for a spreadsheet end user.
/// UserModel is a wrapper around Model with undo/redo history, _diffs_ and automatic evaluation.
/// 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.
///
@@ -935,6 +948,165 @@ impl UserModel {
self.model.get_worksheets_properties()
}
/// Returns the selected sheet index
pub fn get_selected_sheet(&self) -> u32 {
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
}
}
/// Returns the selected cell
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
if let Some(view) = worksheet.views.get(&self.model.view_id) {
return (sheet, view.row, view.column);
}
}
// return a safe default
(0, 1, 1)
}
/// Returns selected view
pub fn get_selected_view(&self) -> SelectedView {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
if let Some(view) = worksheet.views.get(&self.model.view_id) {
return SelectedView {
sheet,
row: view.row,
column: view.column,
range: view.range,
top_row: view.top_row,
left_column: view.left_column,
};
}
}
// return a safe default
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1,
}
}
/// Sets the the selected sheet
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Some(view) = self.model.workbook.views.get_mut(&0) {
view.sheet = sheet;
}
Ok(())
}
/// Sets the selected cell
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if !is_valid_column_number(column) {
return Err(format!("Invalid column: '{column}'"));
}
if !is_valid_column_number(row) {
return Err(format!("Invalid row: '{row}'"));
}
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) {
view.row = row;
view.column = column;
view.range = [row, column, row, column];
}
}
Ok(())
}
/// Sets the selected range
pub fn set_selected_range(
&mut self,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<(), String> {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if !is_valid_column_number(start_column) {
return Err(format!("Invalid column: '{start_column}'"));
}
if !is_valid_column_number(start_row) {
return Err(format!("Invalid row: '{start_row}'"));
}
if !is_valid_column_number(end_column) {
return Err(format!("Invalid column: '{end_column}'"));
}
if !is_valid_column_number(end_row) {
return Err(format!("Invalid row: '{end_row}'"));
}
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) {
view.range = [start_row, start_column, end_row, end_column];
}
}
Ok(())
}
/// Sets the value of the first visible cell
pub fn set_top_left_visible_cell(
&mut self,
top_row: i32,
left_column: i32,
) -> Result<(), String> {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if !is_valid_column_number(left_column) {
return Err(format!("Invalid column: '{left_column}'"));
}
if !is_valid_column_number(top_row) {
return Err(format!("Invalid row: '{top_row}'"));
}
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) {
view.top_row = top_row;
view.left_column = left_column;
}
}
Ok(())
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {