Compare commits
39 Commits
v0.3.1
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3f0f33c2 | ||
|
|
1ff0c38aa5 | ||
|
|
e5a2db4d8c | ||
|
|
fc7335707a | ||
|
|
4095b7db6e | ||
|
|
dd9ca4224d | ||
|
|
5aa7617e97 | ||
|
|
a10d1f4615 | ||
|
|
1e8441a674 | ||
|
|
b2c5027f56 | ||
|
|
91984dc920 | ||
|
|
74be62823d | ||
|
|
edd00096b6 | ||
|
|
d764752f16 | ||
|
|
ce6c908dc7 | ||
|
|
6ee450709a | ||
|
|
23ab5dfef2 | ||
|
|
7e54cb6aa2 | ||
|
|
857ebabf16 | ||
|
|
f0af3048b7 | ||
|
|
99125f1fea | ||
|
|
f96481feb8 | ||
|
|
dc8bb6da21 | ||
|
|
d866e283e9 | ||
|
|
8a54f45d75 | ||
|
|
42d557d485 | ||
|
|
293f7c6de6 | ||
|
|
38325b0bb9 | ||
|
|
282ed16f0d | ||
|
|
fd744d28a3 | ||
|
|
9a717daf04 | ||
|
|
84bf859c2c | ||
|
|
e57101f279 | ||
|
|
264fcac63c | ||
|
|
7777f8e5d6 | ||
|
|
6aa73171c7 | ||
|
|
8051913b2d | ||
|
|
cfa38548d5 | ||
|
|
9787721c5a |
143
.github/workflows/pypi.yml
vendored
Normal file
143
.github/workflows/pypi.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: Upload component to Python Package Index
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
manylinux: auto
|
||||
working-directory: bindings/python
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: bindings/python/dist
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x64, x86]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
architecture: ${{ matrix.target }}
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
working-directory: bindings/python
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: bindings/python/dist
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, aarch64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
working-directory: bindings/python
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: bindings/python/dist
|
||||
|
||||
sdist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
working-directory: bindings/python
|
||||
- name: Upload sdist
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: bindings/python/dist
|
||||
|
||||
publish-to-test-pypi:
|
||||
if: ${{ github.event.inputs.release != 'true' }}
|
||||
name: >-
|
||||
Publish Python 🐍 distribution 📦 to Test PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs: [linux, windows, macos, sdist]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: bindings/python/
|
||||
- name: Publish distribution 📦 to Test PyPI
|
||||
uses: PyO3/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TEST_API_TOKEN }}
|
||||
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
|
||||
with:
|
||||
command: upload
|
||||
args: --skip-existing *
|
||||
working-directory: bindings/python
|
||||
|
||||
publish-pypi:
|
||||
if: ${{ github.event.inputs.release == 'true' }}
|
||||
name: >-
|
||||
Publish Python 🐍 distribution 📦 to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs: [linux, windows, macos, sdist]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: bindings/python/
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: PyO3/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
|
||||
with:
|
||||
command: upload
|
||||
args: --skip-existing *
|
||||
working-directory: bindings/python
|
||||
@@ -8,12 +8,20 @@
|
||||
- New document server (Thanks Dani!)
|
||||
- New function FORMULATEXT
|
||||
- Name Manager ([#212](https://github.com/ironcalc/IronCalc/pull/212) [#220](https://github.com/ironcalc/IronCalc/pull/220))
|
||||
- Add context menu. We can now insert rows and columns. Freeze and unfreeze rows and columns. Delete rows and columns [#271]
|
||||
- Add nodejs bindings [#254]
|
||||
- Add python bindings for all platforms
|
||||
- Add is split into the product and widget
|
||||
- Add Python documentation [#260]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed several issues with pasting content
|
||||
- Fixed several issues with borders
|
||||
- Fixed bug where columns and rows could be resized to negative width and height, respectively
|
||||
- Undo/redo when add/delete sheet now works [#270]
|
||||
- Numerous small fixes
|
||||
- Multiple fixes to the documentation
|
||||
|
||||
## [0.2.0] - 2024-11-06 (The HN release)
|
||||
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1070,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"ironcalc_base",
|
||||
"serde",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "ironcalc_base"
|
||||
version = "0.3.0"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://www.ironcalc.com"
|
||||
repository = "https://github.com/ironcalc/ironcalc/"
|
||||
description = "Open source spreadsheet engine"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ironcalc_base::{types::CellType, Model};
|
||||
use ironcalc_base::{Model, types::CellType};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ironcalc_base::{cell::CellValue, Model};
|
||||
use ironcalc_base::{Model, cell::CellValue};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut model = Model::new_empty("hello-world", "en", "UTC")?;
|
||||
|
||||
@@ -136,6 +136,33 @@ impl Model {
|
||||
}),
|
||||
);
|
||||
|
||||
// In the list of columns:
|
||||
// * Keep all the columns to the left
|
||||
// * Displace all the columns to the right
|
||||
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
|
||||
let mut new_columns = Vec::new();
|
||||
for col in worksheet.cols.iter_mut() {
|
||||
// range under study
|
||||
let min = col.min;
|
||||
let max = col.max;
|
||||
if column > max {
|
||||
// If the range under study is to our left, this is a noop
|
||||
} else if column <= min {
|
||||
// If the range under study is to our right, we displace it
|
||||
col.min = min + column_count;
|
||||
col.max = max + column_count;
|
||||
} else {
|
||||
// If the range under study is in the middle we augment it
|
||||
col.max = max + column_count;
|
||||
}
|
||||
new_columns.push(col.clone());
|
||||
}
|
||||
// TODO: If in a row the cell to the right and left have the same style we should copy it
|
||||
|
||||
worksheet.cols = new_columns;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
expressions::{
|
||||
parser::{
|
||||
move_formula::ref_is_in_area,
|
||||
stringify::{to_string, to_string_displaced, DisplaceData},
|
||||
stringify::{DisplaceData, to_string, to_string_displaced},
|
||||
walk::forward_references,
|
||||
},
|
||||
types::{Area, CellReferenceIndex, CellReferenceRC},
|
||||
|
||||
@@ -149,14 +149,16 @@ impl Lexer {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_left), position))
|
||||
.set_error(&format!("Failed parsing row {}", row_left), position));
|
||||
}
|
||||
};
|
||||
let row_right = match row_right.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_right), position))
|
||||
return Err(self.set_error(
|
||||
&format!("Failed parsing row {}", row_right),
|
||||
position,
|
||||
));
|
||||
}
|
||||
};
|
||||
if row_left > LAST_ROW {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
stringify::{stringify_reference, DisplaceData},
|
||||
Node, Reference,
|
||||
stringify::{DisplaceData, stringify_reference},
|
||||
};
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::{
|
||||
to_rc_format, to_string, to_string_displaced, DisplaceData,
|
||||
DisplaceData, to_rc_format, to_string, to_string_displaced,
|
||||
};
|
||||
use crate::expressions::parser::{Node, Parser};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::parser::move_formula::{MoveContext, move_formula};
|
||||
use crate::expressions::types::{Area, CellReferenceRC};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{move_formula::ref_is_in_area, Node};
|
||||
use super::{Node, move_formula::ref_is_in_area};
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some(e),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
for token in tokens {
|
||||
@@ -391,11 +391,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
if l_exp <= p.exponent_digit_count {
|
||||
if !(number_index < 0 && digit.kind == '#') {
|
||||
let c = if number_index < 0 {
|
||||
if digit.kind == '?' {
|
||||
' '
|
||||
} else {
|
||||
'0'
|
||||
}
|
||||
if digit.kind == '?' { ' ' } else { '0' }
|
||||
} else {
|
||||
exponent_part[number_index as usize]
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::{
|
||||
formatter::format::format_number,
|
||||
locale::{get_locale, Locale},
|
||||
locale::{Locale, get_locale},
|
||||
};
|
||||
|
||||
fn get_default_locale() -> &'static Locale {
|
||||
|
||||
@@ -31,7 +31,7 @@ impl Model {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
let day = date.day() as f64;
|
||||
@@ -54,7 +54,7 @@ impl Model {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
let month = date.month() as f64;
|
||||
@@ -87,7 +87,7 @@ impl Model {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
@@ -192,7 +192,7 @@ impl Model {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
let year = date.year() as f64;
|
||||
@@ -216,7 +216,7 @@ impl Model {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -266,7 +266,7 @@ impl Model {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Invalid date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
// 693_594 is computed as:
|
||||
@@ -296,7 +296,7 @@ impl Model {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Invalid date".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
// 693_594 is computed as:
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
use std::f64::consts::FRAC_2_PI;
|
||||
|
||||
use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
|
||||
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words};
|
||||
|
||||
// R0/S0 on [0, 2.00]
|
||||
const R02: f64 = 1.562_499_999_999_999_5e-2; // 0x3F8FFFFF, 0xFFFFFFFD
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
use std::f64::consts::FRAC_2_PI;
|
||||
|
||||
use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
|
||||
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words};
|
||||
|
||||
// R0/S0 on [0,2]
|
||||
const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
use super::{
|
||||
bessel_j0_y0::{j0, y0},
|
||||
bessel_j1_y1::{j1, y1},
|
||||
bessel_util::{split_words, FRAC_2_SQRT_PI},
|
||||
bessel_util::{FRAC_2_SQRT_PI, split_words},
|
||||
};
|
||||
|
||||
// Special cases are:
|
||||
@@ -232,11 +232,7 @@ pub(crate) fn jn(n: i32, x: f64) -> f64 {
|
||||
}
|
||||
}
|
||||
};
|
||||
if sign == 1 {
|
||||
-b
|
||||
} else {
|
||||
b
|
||||
}
|
||||
if sign == 1 { -b } else { b }
|
||||
}
|
||||
|
||||
// Yn returns the order-n Bessel function of the second kind.
|
||||
@@ -321,9 +317,5 @@ pub(crate) fn yn(n: i32, x: f64) -> f64 {
|
||||
}
|
||||
b
|
||||
};
|
||||
if sign > 0 {
|
||||
b
|
||||
} else {
|
||||
-b
|
||||
}
|
||||
if sign > 0 { b } else { -b }
|
||||
}
|
||||
|
||||
@@ -45,9 +45,5 @@ pub(crate) fn erf(x: f64) -> f64 {
|
||||
}
|
||||
|
||||
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
|
||||
if x < 0.0 {
|
||||
res - 1.0
|
||||
} else {
|
||||
1.0 - res
|
||||
}
|
||||
if x < 0.0 { res - 1.0 } else { 1.0 - res }
|
||||
}
|
||||
|
||||
@@ -698,7 +698,7 @@ impl Model {
|
||||
error: error.0,
|
||||
origin: cell,
|
||||
message: error.1,
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
CalcResult::Number(ipmt)
|
||||
@@ -762,7 +762,7 @@ impl Model {
|
||||
error: error.0,
|
||||
origin: cell,
|
||||
message: error.1,
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
CalcResult::Number(ppmt)
|
||||
@@ -1075,7 +1075,7 @@ impl Model {
|
||||
error,
|
||||
origin: cell,
|
||||
message,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1096,7 +1096,7 @@ impl Model {
|
||||
error,
|
||||
origin: cell,
|
||||
message,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1634,7 +1634,7 @@ impl Model {
|
||||
error: error.0,
|
||||
origin: cell,
|
||||
message: error.1,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1702,7 +1702,7 @@ impl Model {
|
||||
error: error.0,
|
||||
origin: cell,
|
||||
message: error.1,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1750,11 +1750,7 @@ impl Model {
|
||||
rate = 1.0
|
||||
};
|
||||
let value = if rate == 1.0 {
|
||||
if period == 1.0 {
|
||||
cost
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if period == 1.0 { cost } else { 0.0 }
|
||||
} else {
|
||||
cost * (1.0 - rate).powf(period - 1.0)
|
||||
};
|
||||
|
||||
@@ -257,10 +257,10 @@ impl Model {
|
||||
{
|
||||
match defined_name {
|
||||
ParsedDefinedName::CellReference(reference) => {
|
||||
return CalcResult::Number(reference.sheet as f64 + 1.0)
|
||||
return CalcResult::Number(reference.sheet as f64 + 1.0);
|
||||
}
|
||||
ParsedDefinedName::RangeReference(range) => {
|
||||
return CalcResult::Number(range.left.sheet as f64 + 1.0)
|
||||
return CalcResult::Number(range.left.sheet as f64 + 1.0);
|
||||
}
|
||||
ParsedDefinedName::InvalidDefinedNameFormula => {
|
||||
return CalcResult::Error {
|
||||
@@ -296,7 +296,7 @@ impl Model {
|
||||
error: Error::NAME,
|
||||
origin: cell,
|
||||
message: format!("Name not found: {name}"),
|
||||
}
|
||||
};
|
||||
}
|
||||
arg => {
|
||||
// Now it should be the name of a sheet
|
||||
|
||||
@@ -388,7 +388,7 @@ impl Model {
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
let max_row = dimension.max_row;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
expressions::{
|
||||
parser::{parse_range, Node},
|
||||
parser::{Node, parse_range},
|
||||
token::Error,
|
||||
types::CellReferenceIndex,
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
text_util::{substitute, text_after, text_before, Case},
|
||||
text_util::{Case, substitute, text_after, text_before},
|
||||
util::from_wildcard_to_regex,
|
||||
};
|
||||
|
||||
@@ -368,7 +368,7 @@ impl Model {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Empty cell".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -629,7 +629,7 @@ impl Model {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Expecting number".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
|
||||
@@ -57,8 +57,8 @@ mod test;
|
||||
#[cfg(test)]
|
||||
pub mod mock_time;
|
||||
|
||||
pub use model::get_milliseconds_since_epoch;
|
||||
pub use model::Model;
|
||||
pub use model::get_milliseconds_since_epoch;
|
||||
pub use user_model::BorderArea;
|
||||
pub use user_model::ClipboardData;
|
||||
pub use user_model::UserModel;
|
||||
|
||||
@@ -10,11 +10,11 @@ use crate::{
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::{
|
||||
move_formula::{move_formula, MoveContext},
|
||||
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
|
||||
Node, Parser,
|
||||
move_formula::{MoveContext, move_formula},
|
||||
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
|
||||
},
|
||||
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
|
||||
token::{Error, OpCompare, OpProduct, OpSum, OpUnary, get_error_by_name},
|
||||
types::*,
|
||||
utils::{self, is_valid_column_number, is_valid_identifier, is_valid_row},
|
||||
},
|
||||
@@ -24,8 +24,8 @@ use crate::{
|
||||
},
|
||||
functions::util::compare_values,
|
||||
implicit_intersection::implicit_intersection,
|
||||
language::{get_language, Language},
|
||||
locale::{get_locale, Currency, Locale},
|
||||
language::{Language, get_language},
|
||||
locale::{Currency, Locale, get_locale},
|
||||
types::*,
|
||||
utils as common,
|
||||
};
|
||||
@@ -1872,12 +1872,29 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Returns the style for cell (`sheet`, `row`, `column`)
|
||||
/// If the cell does not have a style defined we check the row, otherwise the column and finally a default
|
||||
pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
|
||||
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||
let style = self.workbook.styles.get_style(style_index)?;
|
||||
Ok(style)
|
||||
}
|
||||
|
||||
/// Returns the style defined in a cell if any.
|
||||
pub fn get_cell_style_or_none(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<Style>, String> {
|
||||
let style = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.map(|c| self.workbook.styles.get_style(c.get_style()))
|
||||
.transpose();
|
||||
style
|
||||
}
|
||||
|
||||
/// Returns an internal binary representation of the workbook
|
||||
///
|
||||
/// See also:
|
||||
@@ -2161,6 +2178,73 @@ impl Model {
|
||||
Err("Defined name not found".to_string())
|
||||
}
|
||||
}
|
||||
/// Returns the style object of a column, if any
|
||||
pub fn get_column_style(&self, sheet: u32, column: i32) -> Result<Option<Style>, String> {
|
||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||
let cols = &worksheet.cols;
|
||||
for col in cols {
|
||||
if column >= col.min && column <= col.max {
|
||||
if let Some(style_index) = col.style {
|
||||
let style = self.workbook.styles.get_style(style_index)?;
|
||||
return Ok(Some(style));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
Err("Invalid sheet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the style object of a row, if any
|
||||
pub fn get_row_style(&self, sheet: u32, row: i32) -> Result<Option<Style>, String> {
|
||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||
let rows = &worksheet.rows;
|
||||
for r in rows {
|
||||
if row == r.r {
|
||||
let style = self.workbook.styles.get_style(r.s)?;
|
||||
return Ok(Some(style));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
Err("Invalid sheet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a column with style
|
||||
pub fn set_column_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
style: &Style,
|
||||
) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_column_style(column, style_index)
|
||||
}
|
||||
|
||||
/// Sets a row with style
|
||||
pub fn set_row_style(&mut self, sheet: u32, row: i32, style: &Style) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_row_style(row, style_index)
|
||||
}
|
||||
|
||||
/// Deletes the style of a column if the is any
|
||||
pub fn delete_column_style(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.delete_column_style(column)
|
||||
}
|
||||
|
||||
/// Deletes the style of a row if there is any
|
||||
pub fn delete_row_style(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||
self.workbook.worksheet_mut(sheet)?.delete_row_style(row)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -8,14 +8,14 @@ use crate::{
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::{
|
||||
stringify::{rename_sheet_in_node, to_rc_format},
|
||||
Parser,
|
||||
stringify::{rename_sheet_in_node, to_rc_format},
|
||||
},
|
||||
types::CellReferenceRC,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
model::{Model, ParsedDefinedName, get_milliseconds_since_epoch},
|
||||
types::{
|
||||
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
|
||||
},
|
||||
@@ -301,7 +301,7 @@ impl Model {
|
||||
};
|
||||
if sheet_index >= sheet_count {
|
||||
return Err("Sheet index too large".to_string());
|
||||
}
|
||||
};
|
||||
self.workbook.worksheets.remove(sheet_index as usize);
|
||||
self.reset_parsed_structures();
|
||||
Ok(())
|
||||
|
||||
@@ -150,7 +150,7 @@ pub fn format_number(value: f64, format_code: &str, locale: &str) -> Formatted {
|
||||
text: "#ERROR!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Invalid locale".to_string()),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
formatter::format::format_number(value, format_code, locale)
|
||||
|
||||
@@ -4,8 +4,6 @@ use crate::{
|
||||
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles},
|
||||
};
|
||||
|
||||
// TODO: Move Styles and all related types from crate::types here
|
||||
// Not doing it right now to not have conflicts with exporter branch
|
||||
impl Styles {
|
||||
fn get_font_index(&self, font: &Font) -> Option<i32> {
|
||||
for (font_index, item) in self.fonts.iter().enumerate() {
|
||||
|
||||
@@ -37,6 +37,7 @@ mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_move_formula;
|
||||
mod test_quote_prefix;
|
||||
mod test_row_column_styles;
|
||||
mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
|
||||
@@ -206,9 +206,11 @@ fn test_delete_column_width() {
|
||||
let (sheet, column) = (0, 5);
|
||||
let normal_width = model.get_column_width(sheet, column).unwrap();
|
||||
// Set the width of one column to 5 times the normal width
|
||||
assert!(model
|
||||
.set_column_width(sheet, column, normal_width * 5.0)
|
||||
.is_ok());
|
||||
assert!(
|
||||
model
|
||||
.set_column_width(sheet, column, normal_width * 5.0)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// delete it
|
||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
||||
|
||||
@@ -179,52 +179,60 @@ fn test_move_formula_rectangle() {
|
||||
width: 2,
|
||||
height: 20,
|
||||
};
|
||||
assert!(model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 3,
|
||||
row: 1,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_err());
|
||||
assert!(model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 2,
|
||||
row: 1,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_ok());
|
||||
assert!(model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
row: 20,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_ok());
|
||||
assert!(model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
row: 21,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_err());
|
||||
assert!(
|
||||
model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 3,
|
||||
row: 1,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 2,
|
||||
row: 1,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
row: 20,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
model
|
||||
.move_cell_value_to_area(
|
||||
value,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
row: 21,
|
||||
},
|
||||
target,
|
||||
area
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
32
base/src/test/test_row_column_styles.rs
Normal file
32
base/src/test/test_row_column_styles.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{constants::DEFAULT_COLUMN_WIDTH, test::util::new_empty_model};
|
||||
|
||||
#[test]
|
||||
fn test_model_set_cells_with_values_styles() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
let style_base = model.get_style_for_cell(0, 1, 1).unwrap();
|
||||
let mut style = style_base.clone();
|
||||
style.font.b = true;
|
||||
|
||||
model.set_column_style(0, 10, &style).unwrap();
|
||||
|
||||
assert!(model.get_style_for_cell(0, 21, 10).unwrap().font.b);
|
||||
|
||||
model.delete_column_style(0, 10).unwrap();
|
||||
|
||||
// There are no styles in the column
|
||||
assert!(model.workbook.worksheets[0].cols.is_empty());
|
||||
|
||||
// lets change the column width and check it does not affect the style
|
||||
model
|
||||
.set_column_width(0, 10, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
model.set_column_style(0, 10, &style).unwrap();
|
||||
|
||||
model.delete_column_style(0, 10).unwrap();
|
||||
|
||||
// There are no styles in the column
|
||||
assert!(model.workbook.worksheets[0].cols.len() == 1);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ mod test_autofill_columns;
|
||||
mod test_autofill_rows;
|
||||
mod test_border;
|
||||
mod test_clear_cells;
|
||||
mod test_column_style;
|
||||
mod test_defined_names;
|
||||
mod test_delete_row_column_formatting;
|
||||
mod test_diff_queue;
|
||||
mod test_evaluation;
|
||||
mod test_general;
|
||||
@@ -13,9 +15,11 @@ mod test_on_area_selection;
|
||||
mod test_on_expand_selected_range;
|
||||
mod test_on_paste_styles;
|
||||
mod test_paste_csv;
|
||||
mod test_recursive;
|
||||
mod test_rename_sheet;
|
||||
mod test_row_column;
|
||||
mod test_sheet_state;
|
||||
mod test_sheets_undo_redo;
|
||||
mod test_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel};
|
||||
use crate::{UserModel, constants::DEFAULT_COLUMN_WIDTH};
|
||||
|
||||
#[test]
|
||||
fn add_undo_redo() {
|
||||
@@ -9,7 +9,7 @@ fn add_undo_redo() {
|
||||
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
|
||||
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
|
||||
model
|
||||
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
||||
.set_columns_width(1, 5, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
||||
.unwrap();
|
||||
model.new_sheet().unwrap();
|
||||
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
|
||||
@@ -25,9 +25,6 @@ fn add_undo_redo() {
|
||||
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
|
||||
|
||||
model.delete_sheet(1).unwrap();
|
||||
|
||||
assert!(!model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
BorderArea, UserModel,
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
expressions::{types::Area, utils::number_to_column},
|
||||
types::{Border, BorderItem, BorderStyle},
|
||||
BorderArea, UserModel,
|
||||
};
|
||||
|
||||
// checks there are no borders in the sheet
|
||||
@@ -520,14 +520,19 @@ fn borders_top() {
|
||||
.unwrap();
|
||||
model.set_area_with_border(range, &border_area).unwrap();
|
||||
check_borders(&model);
|
||||
for row in 5..9 {
|
||||
for row in 4..9 {
|
||||
for column in 6..9 {
|
||||
let style = model.get_cell_style(0, row, column).unwrap();
|
||||
let border_item = BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let bottom = if row == 8 {
|
||||
let bottom = if row != 4 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
};
|
||||
let top = if row != 5 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -537,7 +542,7 @@ fn borders_top() {
|
||||
diagonal_down: false,
|
||||
left: None,
|
||||
right: None,
|
||||
top: Some(border_item.clone()),
|
||||
top,
|
||||
bottom,
|
||||
diagonal: None,
|
||||
};
|
||||
@@ -647,12 +652,12 @@ fn borders_right() {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let left = if column == 6 {
|
||||
let left = if column != 9 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
};
|
||||
let right = if column == 9 {
|
||||
let right = if column != 8 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -705,7 +710,7 @@ fn borders_bottom() {
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
// The top will also have a value for all but the first one
|
||||
let top = if row == 5 {
|
||||
let bottom = if row != 8 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -715,8 +720,8 @@ fn borders_bottom() {
|
||||
diagonal_down: false,
|
||||
left: None,
|
||||
right: None,
|
||||
top,
|
||||
bottom: Some(border_item.clone()),
|
||||
top: None,
|
||||
bottom,
|
||||
diagonal: None,
|
||||
};
|
||||
assert_eq!(style.border, expected_border);
|
||||
@@ -751,18 +756,13 @@ fn borders_left() {
|
||||
model.set_area_with_border(range, &border_area).unwrap();
|
||||
|
||||
for row in 5..9 {
|
||||
for column in 5..9 {
|
||||
for column in 6..9 {
|
||||
let style = model.get_cell_style(0, row, column).unwrap();
|
||||
let border_item = BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let left = if column == 5 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
};
|
||||
let right = if column == 8 {
|
||||
let left = if column != 6 {
|
||||
None
|
||||
} else {
|
||||
Some(border_item.clone())
|
||||
@@ -771,13 +771,29 @@ fn borders_left() {
|
||||
diagonal_up: false,
|
||||
diagonal_down: false,
|
||||
left,
|
||||
right,
|
||||
right: None,
|
||||
top: None,
|
||||
bottom: None,
|
||||
diagonal: None,
|
||||
};
|
||||
assert_eq!(style.border, expected_border);
|
||||
}
|
||||
// Column 5 has a border to the right, of course:
|
||||
let style = model.get_cell_style(0, row, 5).unwrap();
|
||||
let border_item = BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#FF5566".to_string()),
|
||||
};
|
||||
let expected_border = Border {
|
||||
diagonal_up: false,
|
||||
diagonal_down: false,
|
||||
left: None,
|
||||
right: Some(border_item.clone()),
|
||||
top: None,
|
||||
bottom: None,
|
||||
diagonal: None,
|
||||
};
|
||||
assert_eq!(style.border, expected_border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,10 +1034,7 @@ fn border_top() {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#F2F2F2".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
model._get_cell_actual_border("C4").bottom,
|
||||
Some(border_item)
|
||||
);
|
||||
assert_eq!(model._get_cell_actual_border("C4").top, Some(border_item));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{expressions::types::Area, UserModel};
|
||||
use crate::{UserModel, expressions::types::Area};
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
|
||||
504
base/src/test/user_model/test_column_style.rs
Normal file
504
base/src/test/user_model/test_column_style.rs
Normal file
@@ -0,0 +1,504 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
use crate::constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
#[test]
|
||||
fn column_width() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
assert!(!style.font.b);
|
||||
assert!(!style.font.u);
|
||||
assert!(!style.font.strike);
|
||||
assert_eq!(style.font.color, Some("#000000".to_owned()));
|
||||
|
||||
// Set the whole column style and check it works
|
||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(style.font.b);
|
||||
|
||||
// undo and check it works
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(!style.font.b);
|
||||
|
||||
// redo and check it works
|
||||
model.redo().unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(style.font.b);
|
||||
|
||||
// change the column width and check it does not affect the style
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||
assert!(style.font.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_style() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let cell_g123 = Area {
|
||||
sheet: 0,
|
||||
row: 123,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
// Set G123 background to red
|
||||
model
|
||||
.update_range_style(&cell_g123, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Now set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Get the style of G123
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
// Check G123 has the column style now
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_column() {
|
||||
// We set the row style, then a column style
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Check G3 has the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
// undo twice. Color must be default
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_row() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let default_style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Check G3 has the row style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check G3 has the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check G3 has the default_style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, default_style.fill.bg_color);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_column_column() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_c_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let column_e_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_5_range = Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
|
||||
// Test E5 has the default style
|
||||
let style = model.get_cell_style(0, 5, 5).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width_column_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
DEFAULT_COLUMN_WIDTH * 2.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn height_row_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_rows_height(0, 10, 10, DEFAULT_ROW_HEIGHT * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let row_10_range = Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&row_10_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_row_height(0, 10).unwrap(),
|
||||
2.0 * DEFAULT_ROW_HEIGHT
|
||||
);
|
||||
model.undo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_row_height(0, 10).unwrap(),
|
||||
2.0 * DEFAULT_ROW_HEIGHT
|
||||
);
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_row_height(0, 10).unwrap(), DEFAULT_ROW_HEIGHT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_row_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g12 = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let row_12_range = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// Set G12 background to red
|
||||
model
|
||||
.update_range_style(&cell_g12, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&row_12_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
|
||||
model.undo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_column_style_then_cell() {
|
||||
// We check that if we set a cell style in a column that already has a style
|
||||
// the styles compound
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g12 = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
// Set G12 background to red
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&cell_g12, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_row_style_then_cell() {
|
||||
// We check that if we set a cell style in a column that already has a style
|
||||
// the styles compound
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g12 = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let row_12_range = Area {
|
||||
sheet: 0,
|
||||
row: 12,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// Set G12 background to red
|
||||
model
|
||||
.update_range_style(&row_12_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&cell_g12, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_style_then_row_alignment() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
model
|
||||
.update_range_style(&row_3_range, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
// check the row alignment does not affect the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_style_then_width() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
// Check column width worked:
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
DEFAULT_COLUMN_WIDTH * 2.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_column_column() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_c_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let column_e_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_5_range = Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// update the row style
|
||||
model
|
||||
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// update the column style
|
||||
model
|
||||
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
|
||||
.unwrap();
|
||||
|
||||
// test E5 has the column style
|
||||
let style = model.get_cell_style(0, 5, 5).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
|
||||
}
|
||||
@@ -396,3 +396,30 @@ fn undo_redo() {
|
||||
Ok("Hola!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_scope_to_first_sheet() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.new_sheet().unwrap();
|
||||
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
||||
model
|
||||
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
|
||||
.unwrap();
|
||||
model
|
||||
.new_defined_name("myName", None, "Sheet1!$A$1")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(1, 2, 1),
|
||||
Ok("Hello world!".to_string())
|
||||
);
|
||||
|
||||
model
|
||||
.update_defined_name("myName", None, "myName", Some(0), "Sheet1!$A$1")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(1, 2, 1),
|
||||
Ok("#NAME?".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
243
base/src/test/user_model/test_delete_row_column_formatting.rs
Normal file
243
base/src/test/user_model/test_delete_row_column_formatting.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW},
|
||||
expressions::types::Area,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn delete_column_formatting() {
|
||||
// We are going to delete formatting in column G (7)
|
||||
// There are cells with their own styles
|
||||
// There are rows with their own styles
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let cell_g123 = Area {
|
||||
sheet: 0,
|
||||
row: 123,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// Set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Set G123 background to red
|
||||
model
|
||||
.update_range_style(&cell_g123, "fill.bg_color", "#FF5533")
|
||||
.unwrap();
|
||||
|
||||
// Set the style of the whole row
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Delete the column formatting
|
||||
model.range_clear_formatting(&column_g_range).unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
// Check the style of the whole row is still there
|
||||
let style = model.get_cell_style(0, 3, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
// Check the style of the whole column is now gone
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#FF5533".to_owned()));
|
||||
|
||||
// Check G3 is the row style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
// Check G40 is the column style
|
||||
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
// Check the style of G123 is now what it was before
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
// Check the style of the whole row is still there
|
||||
let style = model.get_cell_style(0, 3, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||
|
||||
// Check the style of the whole column is now gone
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_width() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
// Set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
// Delete the column formatting
|
||||
model.range_clear_formatting(&column_g_range).unwrap();
|
||||
// This does not change the column width
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
2.0 * DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
|
||||
model.undo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
2.0 * DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_column_width(0, 7).unwrap(),
|
||||
2.0 * DEFAULT_COLUMN_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_row_style_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||
.unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_123_range = Area {
|
||||
sheet: 0,
|
||||
row: 123,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let delete_range = Area {
|
||||
sheet: 0,
|
||||
row: 120,
|
||||
column: 5,
|
||||
width: 20,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
// Set the style of the whole column
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&row_123_range, "fill.bg_color", "#111222")
|
||||
.unwrap();
|
||||
|
||||
model.range_clear_formatting(&delete_range).unwrap();
|
||||
|
||||
// check G123 is empty
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
// uno clear formatting
|
||||
model.undo().unwrap();
|
||||
|
||||
// G123 has the row style
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#111222".to_owned()));
|
||||
|
||||
// undo twice
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
|
||||
// check G123 is empty
|
||||
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_row_row_height_undo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
let row_3_range = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: LAST_COLUMN,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.set_rows_height(0, 3, 3, DEFAULT_ROW_HEIGHT * 2.0)
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.update_range_style(&row_3_range, "fill.bg_color", "#111222")
|
||||
.unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
// check G3 has the column style
|
||||
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn send_queue() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
let send_queue = model1.flush_send_queue();
|
||||
|
||||
@@ -34,7 +34,7 @@ fn apply_external_diffs_wrong_str() {
|
||||
fn queue_undo_redo() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
assert!(model1.undo().is_ok());
|
||||
assert!(model1.redo().is_ok());
|
||||
@@ -57,8 +57,8 @@ fn queue_undo_redo_multiple() {
|
||||
// do a bunch of things
|
||||
model1.set_frozen_columns_count(0, 5).unwrap();
|
||||
model1.set_frozen_rows_count(0, 6).unwrap();
|
||||
model1.set_column_width(0, 7, 300.0).unwrap();
|
||||
model1.set_row_height(0, 23, 123.0).unwrap();
|
||||
model1.set_columns_width(0, 7, 7, 300.0).unwrap();
|
||||
model1.set_rows_height(0, 23, 23, 123.0).unwrap();
|
||||
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
|
||||
|
||||
for row in 1..5 {
|
||||
@@ -157,7 +157,9 @@ fn new_sheet() {
|
||||
#[test]
|
||||
fn wrong_diffs_handled() {
|
||||
let mut model = UserModel::from_model(new_empty_model());
|
||||
assert!(model
|
||||
.apply_external_diffs("Hello world".as_bytes())
|
||||
.is_err());
|
||||
assert!(
|
||||
model
|
||||
.apply_external_diffs("Hello world".as_bytes())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::CellType;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn set_user_input_errors() {
|
||||
@@ -59,7 +59,7 @@ fn insert_remove_rows() {
|
||||
// Insert some data in row 5 (and change the style)
|
||||
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
|
||||
// Change the height of the column
|
||||
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
|
||||
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
|
||||
|
||||
// remove the row
|
||||
assert!(model.delete_row(0, 5).is_ok());
|
||||
@@ -95,7 +95,7 @@ fn insert_remove_columns() {
|
||||
// Insert some data in row 5 (and change the style) in E1
|
||||
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
|
||||
// Change the width of the column
|
||||
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
|
||||
assert!(model.set_columns_width(0, 5, 5, 3.0 * column_width).is_ok());
|
||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
||||
|
||||
// remove the column
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{
|
||||
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH,
|
||||
LAST_COLUMN,
|
||||
},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH, LAST_COLUMN},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::Fill;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn simple_pasting() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{expressions::types::Area, UserModel};
|
||||
use crate::{UserModel, expressions::types::Area};
|
||||
|
||||
#[test]
|
||||
fn csv_paste() {
|
||||
|
||||
42
base/src/test/user_model/test_recursive.rs
Normal file
42
base/src/test/user_model/test_recursive.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel, constants::LAST_ROW, expressions::types::Area, test::util::new_empty_model,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn two_columns() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
// Set style in column C (column 3)
|
||||
let column_c_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
model
|
||||
.update_range_style(&column_c_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
model.set_user_input(0, 5, 3, "2").unwrap();
|
||||
|
||||
// Set Style in column G (column 7)
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
model.set_user_input(0, 5, 6, "42").unwrap();
|
||||
// Set formula in G5: =F5*C5
|
||||
model.set_user_input(0, 5, 7, "=F5*C5").unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 7).unwrap(), "84");
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -59,7 +59,7 @@ fn simple_delete_column() {
|
||||
model.set_user_input(0, 1, 5, "3").unwrap();
|
||||
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
|
||||
model
|
||||
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_column(0, 5).unwrap();
|
||||
@@ -116,7 +116,7 @@ fn simple_delete_row() {
|
||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||
|
||||
model
|
||||
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_row(0, 15).unwrap();
|
||||
@@ -172,3 +172,42 @@ fn row_heigh_increases_automatically() {
|
||||
.unwrap();
|
||||
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_row_evaluates() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
|
||||
|
||||
assert!(model.insert_row(0, 1).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||
|
||||
model.delete_row(0, 1).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_column_evaluates() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
|
||||
|
||||
assert!(model.insert_column(0, 1).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||
|
||||
model.delete_column(0, 1).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
|
||||
52
base/src/test/user_model/test_sheets_undo_redo.rs
Normal file
52
base/src/test/user_model/test_sheets_undo_redo.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn basic_undo_redo() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
|
||||
model.new_sheet().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
{
|
||||
let props = model.get_worksheets_properties();
|
||||
assert_eq!(props.len(), 1);
|
||||
let view = model.get_selected_view();
|
||||
assert_eq!(view.sheet, 0);
|
||||
}
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
{
|
||||
let props = model.get_worksheets_properties();
|
||||
assert_eq!(props.len(), 2);
|
||||
let view = model.get_selected_view();
|
||||
|
||||
assert_eq!(view.sheet, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_undo() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
|
||||
model.new_sheet().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
model.set_user_input(1, 1, 1, "42").unwrap();
|
||||
model.set_user_input(1, 1, 2, "=A1*2").unwrap();
|
||||
model.delete_sheet(1).unwrap();
|
||||
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 1);
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
expressions::types::Area,
|
||||
types::{Alignment, HorizontalAlignment, VerticalAlignment},
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -436,3 +436,47 @@ fn false_removes_value() {
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_clear_formatting() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// bold
|
||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
|
||||
.unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
assert_eq!(
|
||||
style.alignment.unwrap().horizontal,
|
||||
HorizontalAlignment::CenterContinuous
|
||||
);
|
||||
|
||||
model.range_clear_all(&range).unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
assert_eq!(style.alignment, None);
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
assert_eq!(
|
||||
style.alignment.unwrap().horizontal,
|
||||
HorizontalAlignment::CenterContinuous
|
||||
);
|
||||
model.redo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
assert_eq!(style.alignment, None);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{test::util::new_empty_model, UserModel};
|
||||
use crate::{UserModel, test::util::new_empty_model};
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
|
||||
let model_bytes = model1.to_bytes();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{test::util::new_empty_model, UserModel};
|
||||
use crate::{UserModel, test::util::new_empty_model};
|
||||
|
||||
#[test]
|
||||
fn simple_undo_redo() {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
test::util::new_empty_model,
|
||||
user_model::SelectedView,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
UserModel,
|
||||
constants::{DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{expressions::types::Area, types::Border, BorderArea, UserModel};
|
||||
use crate::{BorderArea, UserModel, expressions::types::Area, types::Border};
|
||||
|
||||
impl UserModel {
|
||||
pub fn _set_cell_border(&mut self, cell: &str, color: &str) {
|
||||
|
||||
@@ -323,6 +323,19 @@ pub struct Style {
|
||||
pub quote_prefix: bool,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Style {
|
||||
alignment: None,
|
||||
num_fmt: "general".to_string(),
|
||||
fill: Fill::default(),
|
||||
font: Font::default(),
|
||||
border: Border::default(),
|
||||
quote_prefix: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NumFmt {
|
||||
pub num_fmt_id: i32,
|
||||
|
||||
@@ -50,8 +50,9 @@ impl Units {
|
||||
fn get_units_from_format_string(num_fmt: &str) -> Option<Units> {
|
||||
let mut parser = Parser::new(num_fmt);
|
||||
parser.parse();
|
||||
let parts = parser.parts.first()?;
|
||||
// We only care about the first part (positive number)
|
||||
match &parser.parts[0] {
|
||||
match parts {
|
||||
ParsePart::Number(part) => {
|
||||
if part.percent > 0 {
|
||||
Some(Units::Percentage {
|
||||
|
||||
507
base/src/user_model/border.rs
Normal file
507
base/src/user_model/border.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
expressions::types::Area,
|
||||
};
|
||||
|
||||
use super::{
|
||||
BorderArea, UserModel, border_utils::is_max_border, common::BorderType, history::Diff,
|
||||
};
|
||||
|
||||
impl UserModel {
|
||||
fn update_single_cell_border(
|
||||
&mut self,
|
||||
border_area: &BorderArea,
|
||||
cell: (u32, i32, i32),
|
||||
range: (i32, i32, i32, i32),
|
||||
diff_list: &mut Vec<Diff>,
|
||||
) -> Result<(), String> {
|
||||
let (sheet, row, column) = cell;
|
||||
let (first_row, first_column, last_row, last_column) = range;
|
||||
|
||||
let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||||
let mut new_value = match &old_value {
|
||||
Some(value) => value.clone(),
|
||||
None => Default::default(),
|
||||
};
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Right => {
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Bottom => {
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Left => {
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterH => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::None => {
|
||||
new_value.border.top = None;
|
||||
new_value.border.right = None;
|
||||
new_value.border.bottom = None;
|
||||
new_value.border.left = None;
|
||||
}
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &new_value)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(new_value),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_rows_with_border(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
first_row: i32,
|
||||
last_row: i32,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
for row in first_row..=last_row {
|
||||
let old_value = self.model.get_row_style(sheet, row)?;
|
||||
let mut new_value = match &old_value {
|
||||
Some(value) => value.clone(),
|
||||
None => Default::default(),
|
||||
};
|
||||
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => {
|
||||
if row == first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Right => {
|
||||
// noop
|
||||
}
|
||||
BorderType::Bottom => {
|
||||
if row == last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Left => {
|
||||
// noop
|
||||
}
|
||||
BorderType::CenterH => {
|
||||
if row != first_row {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::None => {
|
||||
new_value.border.top = None;
|
||||
new_value.border.right = None;
|
||||
new_value.border.bottom = None;
|
||||
new_value.border.left = None;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to go throw each non-empty cell in the row
|
||||
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_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(first_row, 1, last_row, LAST_COLUMN),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
|
||||
self.model.set_row_style(sheet, row, &new_value)?;
|
||||
diff_list.push(Diff::SetRowStyle {
|
||||
sheet,
|
||||
row,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(new_value),
|
||||
});
|
||||
}
|
||||
// TODO: We need to check the rows above and below. also any non empty cell in the rows above and below.
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_columns_with_border(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
first_column: i32,
|
||||
last_column: i32,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
// 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();
|
||||
let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
|
||||
for column in first_column..=last_column {
|
||||
let old_value = self.model.get_column_style(sheet, column)?;
|
||||
let mut new_value = match &old_value {
|
||||
Some(value) => value.clone(),
|
||||
None => Default::default(),
|
||||
};
|
||||
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => {
|
||||
// noop
|
||||
}
|
||||
BorderType::Right => {
|
||||
if column == last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Bottom => {
|
||||
// noop
|
||||
}
|
||||
BorderType::Left => {
|
||||
if column == first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterH => {
|
||||
new_value.border.top = Some(border_area.item.clone());
|
||||
new_value.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
if column != first_column {
|
||||
new_value.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
new_value.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::None => {
|
||||
new_value.border.top = None;
|
||||
new_value.border.right = None;
|
||||
new_value.border.bottom = None;
|
||||
new_value.border.left = None;
|
||||
}
|
||||
}
|
||||
// We need to go through each non empty cell in the 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() {
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(1, first_column, LAST_ROW, last_column),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We also need to overwrite those that have a row style
|
||||
for row_s in styled_rows.iter() {
|
||||
let row = row_s.r;
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(1, first_column, LAST_ROW, last_column),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
|
||||
self.model.set_column_style(sheet, column, &new_value)?;
|
||||
diff_list.push(Diff::SetColumnStyle {
|
||||
sheet,
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(new_value),
|
||||
});
|
||||
}
|
||||
// We need to check the borders of the column to the left and the column to the right
|
||||
// We also need to check every non-empty cell in the columns to the left and right
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the border in an area of cells.
|
||||
/// When setting the border we need to check if the adjacent cells have a "heavier" border
|
||||
/// If that is the case we need to change it
|
||||
pub fn set_area_with_border(
|
||||
&mut self,
|
||||
range: &Area,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let sheet = range.sheet;
|
||||
let first_row = range.row;
|
||||
let first_column = range.column;
|
||||
let last_row = first_row + range.height - 1;
|
||||
let last_column = first_column + range.width - 1;
|
||||
if first_row == 1 && last_row == LAST_ROW {
|
||||
// full columns
|
||||
self.set_columns_with_border(sheet, first_column, last_column, border_area)?;
|
||||
return Ok(());
|
||||
}
|
||||
if first_column == 1 && last_column == LAST_COLUMN {
|
||||
// full rows
|
||||
self.set_rows_with_border(sheet, first_row, last_row, border_area)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut diff_list = Vec::new();
|
||||
for row in first_row..=last_row {
|
||||
for column in first_column..=last_column {
|
||||
self.update_single_cell_border(
|
||||
border_area,
|
||||
(sheet, row, column),
|
||||
(first_row, first_column, last_row, last_column),
|
||||
&mut diff_list,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// bottom of the cells above the first
|
||||
if first_row > 1
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Top,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let row = first_row - 1;
|
||||
for column in first_column..=last_column {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
if is_max_border(Some(&border_area.item), old_value.border.bottom.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.bottom = None;
|
||||
} else {
|
||||
style.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cells to the right
|
||||
if last_column < LAST_COLUMN
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Right,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let column = last_column + 1;
|
||||
for row in first_row..=last_row {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
// If the border in the adjacent cell is "heavier" we change it
|
||||
if is_max_border(Some(&border_area.item), old_value.border.left.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.left = None;
|
||||
} else {
|
||||
style.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cells bellow
|
||||
if last_row < LAST_ROW
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Bottom,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let row = last_row + 1;
|
||||
for column in first_column..=last_column {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
if is_max_border(Some(&border_area.item), old_value.border.top.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.top = None;
|
||||
} else {
|
||||
style.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cells to the left
|
||||
if first_column > 1
|
||||
&& [
|
||||
BorderType::All,
|
||||
BorderType::None,
|
||||
BorderType::Outer,
|
||||
BorderType::Left,
|
||||
]
|
||||
.contains(&border_area.r#type)
|
||||
{
|
||||
let column = first_column - 1;
|
||||
for row in first_row..=last_row {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||
if is_max_border(Some(&border_area.item), old_value.border.right.as_ref()) {
|
||||
let mut style = old_value.clone();
|
||||
if border_area.r#type == BorderType::None {
|
||||
style.border.right = None;
|
||||
} else {
|
||||
style.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(Some(old_value)),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
|
||||
use crate::types::{Cell, Col, Row, SheetState, Style};
|
||||
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub(crate) struct RowData {
|
||||
@@ -39,11 +39,17 @@ pub(crate) enum Diff {
|
||||
old_value: Box<Option<Cell>>,
|
||||
old_style: Box<Style>,
|
||||
},
|
||||
CellClearFormatting {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_style: Box<Option<Style>>,
|
||||
},
|
||||
SetCellStyle {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Style>,
|
||||
old_value: Box<Option<Style>>,
|
||||
new_value: Box<Style>,
|
||||
},
|
||||
// Column and Row diffs
|
||||
@@ -59,6 +65,28 @@ pub(crate) enum Diff {
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
SetColumnStyle {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Style>>,
|
||||
new_value: Box<Style>,
|
||||
},
|
||||
SetRowStyle {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
old_value: Box<Option<Style>>,
|
||||
new_value: Box<Style>,
|
||||
},
|
||||
DeleteColumnStyle {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Style>>,
|
||||
},
|
||||
DeleteRowStyle {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
old_value: Box<Option<Style>>,
|
||||
},
|
||||
InsertRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
@@ -77,6 +105,10 @@ pub(crate) enum Diff {
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
old_data: Box<Worksheet>,
|
||||
},
|
||||
SetFrozenRowsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
@@ -87,9 +119,6 @@ pub(crate) enum Diff {
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
},
|
||||
NewSheet {
|
||||
index: u32,
|
||||
name: String,
|
||||
@@ -168,11 +197,6 @@ impl History {
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.redo_stack = vec![];
|
||||
self.undo_stack = vec![];
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod border;
|
||||
mod border_utils;
|
||||
mod common;
|
||||
mod history;
|
||||
|
||||
@@ -156,7 +156,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::language::get_language;
|
||||
use crate::locale::{get_locale, Locale};
|
||||
use crate::locale::{Locale, get_locale};
|
||||
|
||||
fn get_test_locale() -> &'static Locale {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -108,37 +108,120 @@ impl Worksheet {
|
||||
self.cols = vec![Col {
|
||||
min: 1,
|
||||
max: constants::LAST_COLUMN,
|
||||
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
|
||||
custom_width: true,
|
||||
width: constants::DEFAULT_COLUMN_WIDTH,
|
||||
custom_width: false,
|
||||
style: Some(style_index),
|
||||
}];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
|
||||
let width = constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR;
|
||||
let width = self
|
||||
.get_column_width(column)
|
||||
.unwrap_or(constants::DEFAULT_COLUMN_WIDTH);
|
||||
self.set_column_width_and_style(column, width, Some(style_index))
|
||||
}
|
||||
|
||||
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
|
||||
// FIXME: This is a HACK
|
||||
let custom_format = style_index != 0;
|
||||
for r in self.rows.iter_mut() {
|
||||
if r.r == row {
|
||||
r.s = style_index;
|
||||
r.custom_format = true;
|
||||
r.custom_format = custom_format;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.rows.push(Row {
|
||||
height: constants::DEFAULT_ROW_HEIGHT / constants::ROW_HEIGHT_FACTOR,
|
||||
r: row,
|
||||
custom_format: true,
|
||||
custom_height: true,
|
||||
custom_format,
|
||||
custom_height: false,
|
||||
s: style_index,
|
||||
hidden: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_row_style(&mut self, row: i32) -> Result<(), String> {
|
||||
let mut index = None;
|
||||
for (i, r) in self.rows.iter().enumerate() {
|
||||
if r.r == row {
|
||||
index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(i) = index {
|
||||
if let Some(r) = self.rows.get_mut(i) {
|
||||
r.s = 0;
|
||||
r.custom_format = false;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_column_style(&mut self, column: i32) -> Result<(), String> {
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Column number '{column}' is not valid."));
|
||||
}
|
||||
let cols = &mut self.cols;
|
||||
|
||||
let mut index = 0;
|
||||
let mut split = false;
|
||||
for c in cols.iter_mut() {
|
||||
let min = c.min;
|
||||
let max = c.max;
|
||||
if min <= column && column <= max {
|
||||
//
|
||||
split = true;
|
||||
break;
|
||||
}
|
||||
if column < min {
|
||||
// We passed, there is nothing to delete
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if split {
|
||||
let min = cols[index].min;
|
||||
let max = cols[index].max;
|
||||
let custom_width = cols[index].custom_width;
|
||||
let width = cols[index].width;
|
||||
let pre = Col {
|
||||
min,
|
||||
max: column - 1,
|
||||
width,
|
||||
custom_width,
|
||||
style: cols[index].style,
|
||||
};
|
||||
let col = Col {
|
||||
min: column,
|
||||
max: column,
|
||||
width,
|
||||
custom_width,
|
||||
style: None,
|
||||
};
|
||||
let post = Col {
|
||||
min: column + 1,
|
||||
max,
|
||||
width,
|
||||
custom_width,
|
||||
style: cols[index].style,
|
||||
};
|
||||
cols.remove(index);
|
||||
if column != max {
|
||||
cols.insert(index, post);
|
||||
}
|
||||
if custom_width {
|
||||
cols.insert(index, col);
|
||||
}
|
||||
if column != min {
|
||||
cols.insert(index, pre);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_cell_style(
|
||||
&mut self,
|
||||
row: i32,
|
||||
@@ -285,11 +368,12 @@ impl Worksheet {
|
||||
|
||||
/// Changes the width of a column.
|
||||
/// * If the column does not a have a width we simply add it
|
||||
/// * If it has, it might be part of a range and we ned to split the range.
|
||||
/// * If it has, it might be part of a range and we need to split the range.
|
||||
///
|
||||
/// Fails if column index is outside allowed range or width is negative.
|
||||
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
|
||||
self.set_column_width_and_style(column, width, None)
|
||||
let style = self.get_column_style(column)?;
|
||||
self.set_column_width_and_style(column, width, style)
|
||||
}
|
||||
|
||||
pub(crate) fn set_column_width_and_style(
|
||||
@@ -309,7 +393,7 @@ impl Worksheet {
|
||||
min: column,
|
||||
max: column,
|
||||
width: width / constants::COLUMN_WIDTH_FACTOR,
|
||||
custom_width: true,
|
||||
custom_width: width != constants::DEFAULT_COLUMN_WIDTH,
|
||||
style,
|
||||
};
|
||||
let mut index = 0;
|
||||
@@ -319,7 +403,9 @@ impl Worksheet {
|
||||
let max = c.max;
|
||||
if min <= column && column <= max {
|
||||
if min == column && max == column {
|
||||
c.style = style;
|
||||
c.width = width / constants::COLUMN_WIDTH_FACTOR;
|
||||
c.custom_width = width != constants::DEFAULT_COLUMN_WIDTH;
|
||||
return Ok(());
|
||||
}
|
||||
split = true;
|
||||
@@ -383,6 +469,23 @@ impl Worksheet {
|
||||
Ok(constants::DEFAULT_COLUMN_WIDTH)
|
||||
}
|
||||
|
||||
/// Returns the column style index if present
|
||||
pub fn get_column_style(&self, column: i32) -> Result<Option<i32>, String> {
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Column number '{column}' is not valid."));
|
||||
}
|
||||
|
||||
let cols = &self.cols;
|
||||
for col in cols {
|
||||
let min = col.min;
|
||||
let max = col.max;
|
||||
if column >= min && column <= max {
|
||||
return Ok(col.style);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Returns non empty cells in a column
|
||||
pub fn column_cell_references(&self, column: i32) -> Result<Vec<CellReferenceIndex>, String> {
|
||||
let mut column_cell_references: Vec<CellReferenceIndex> = Vec::new();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
name = "ironcalc_nodejs"
|
||||
version = "0.3.1"
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
tab_spaces = 2
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
|
||||
use napi::{self, JsUnknown, Result, bindgen_prelude::*};
|
||||
use serde::Serialize;
|
||||
|
||||
use ironcalc::{
|
||||
base::{
|
||||
types::{CellType, Style},
|
||||
Model as BaseModel,
|
||||
types::{CellType, Style},
|
||||
},
|
||||
error::XlsxError,
|
||||
export::{save_to_icalc, save_to_xlsx},
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
|
||||
use napi::{self, JsUnknown, Result, bindgen_prelude::*};
|
||||
|
||||
use ironcalc::base::{
|
||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||
expressions::types::Area,
|
||||
types::{CellType, Style},
|
||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -161,6 +161,28 @@ impl UserModel {
|
||||
self.model.range_clear_contents(&range).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "rangeClearFormatting")]
|
||||
pub fn range_clear_formatting(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<()> {
|
||||
let range = Area {
|
||||
sheet,
|
||||
row: start_row,
|
||||
column: start_column,
|
||||
width: end_column - start_column + 1,
|
||||
height: end_row - start_row + 1,
|
||||
};
|
||||
self
|
||||
.model
|
||||
.range_clear_formatting(&range)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "insertRow")]
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||
@@ -181,19 +203,31 @@ impl UserModel {
|
||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "setRowHeight")]
|
||||
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<()> {
|
||||
#[napi(js_name = "setRowsHeight")]
|
||||
pub fn set_rows_height(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row_start: i32,
|
||||
row_end: i32,
|
||||
height: f64,
|
||||
) -> Result<()> {
|
||||
self
|
||||
.model
|
||||
.set_row_height(sheet, row, height)
|
||||
.set_rows_height(sheet, row_start, row_end, height)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[napi(js_name = "setColumnWidth")]
|
||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<()> {
|
||||
#[napi(js_name = "setColumnsWidth")]
|
||||
pub fn set_columns_width(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column_start: i32,
|
||||
column_end: i32,
|
||||
width: f64,
|
||||
) -> Result<()> {
|
||||
self
|
||||
.model
|
||||
.set_column_width(sheet, column, width)
|
||||
.set_columns_width(sheet, column_start, column_end, width)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "pyroncalc"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
|
||||
[lib]
|
||||
|
||||
210
bindings/python/docs/api_reference.rst
Normal file
210
bindings/python/docs/api_reference.rst
Normal file
@@ -0,0 +1,210 @@
|
||||
|
||||
API Reference
|
||||
-------------
|
||||
|
||||
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.
|
||||
|
||||
|
||||
.. method:: evaluate()
|
||||
|
||||
Evaluates the model. This needs to be done after each change, otherwise the model might be on a broken state.
|
||||
|
||||
.. method:: set_user_input(sheet: int, row: int, column: int, value: str)
|
||||
|
||||
Sets an input in a cell, as would be done by a user typing into a spreadsheet cell.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index (first row is 1).
|
||||
:param column: The 1-based column index (column “A” is 1).
|
||||
:param value: The value to set, e.g. ``"123"`` or ``"=A1*2"``.
|
||||
|
||||
.. method:: clear_cell_contents(sheet: int, row: int, column: int)
|
||||
|
||||
Removes the content of the cell but leaves the style intact.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index (first row is 1).
|
||||
:param column: The 1-based column index (column “A” is 1).
|
||||
|
||||
.. method:: get_cell_content(sheet: int, row: int, column: int) -> str
|
||||
|
||||
Returns the raw content of a cell. If the cell contains a formula,
|
||||
the returned string starts with ``"="``.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:param column: The 1-based column index.
|
||||
:returns: The raw content, or an empty string if the cell is empty.
|
||||
|
||||
.. method:: get_cell_type(sheet: int, row: int, column: int) -> PyCellType
|
||||
|
||||
Returns the type of the cell (number, boolean, string, error, etc.).
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:param column: The 1-based column index.
|
||||
:rtype: PyCellType
|
||||
|
||||
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
|
||||
|
||||
Returns the cell’s value as a formatted string, taking into
|
||||
account any number/currency/date formatting.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:param column: The 1-based column index.
|
||||
:returns: Formatted string of the cell’s value.
|
||||
|
||||
.. method:: set_cell_style(sheet: int, row: int, column: int, style: PyStyle)
|
||||
|
||||
Sets the style of the cell at (sheet, row, column).
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:param column: The 1-based column index.
|
||||
:param style: A PyStyle object specifying the style.
|
||||
|
||||
.. method:: get_cell_style(sheet: int, row: int, column: int) -> PyStyle
|
||||
|
||||
Retrieves the style of the specified cell.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:param column: The 1-based column index.
|
||||
:returns: A PyStyle object describing the cell’s style.
|
||||
|
||||
.. method:: insert_rows(sheet: int, row: int, row_count: int)
|
||||
|
||||
Inserts new rows.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The position before which new rows are inserted (1-based).
|
||||
:param row_count: The number of rows to insert.
|
||||
|
||||
.. method:: insert_columns(sheet: int, column: int, column_count: int)
|
||||
|
||||
Inserts new columns.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param column: The position before which new columns are inserted (1-based).
|
||||
:param column_count: The number of columns to insert.
|
||||
|
||||
.. method:: delete_rows(sheet: int, row: int, row_count: int)
|
||||
|
||||
Deletes a range of rows.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The starting row to delete (1-based).
|
||||
:param row_count: How many rows to delete.
|
||||
|
||||
.. method:: delete_columns(sheet: int, column: int, column_count: int)
|
||||
|
||||
Deletes a range of columns.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param column: The starting column to delete (1-based).
|
||||
:param column_count: How many columns to delete.
|
||||
|
||||
.. method:: get_column_width(sheet: int, column: int) -> float
|
||||
|
||||
Retrieves the width of a given column.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param column: The 1-based column index.
|
||||
:rtype: float
|
||||
|
||||
.. method:: get_row_height(sheet: int, row: int) -> float
|
||||
|
||||
Retrieves the height of a given row.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:rtype: float
|
||||
|
||||
.. method:: set_column_width(sheet: int, column: int, width: float)
|
||||
|
||||
Sets the width of a given column.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param column: The 1-based column index.
|
||||
:param width: The desired width (float).
|
||||
|
||||
.. method:: set_row_height(sheet: int, row: int, height: float)
|
||||
|
||||
Sets the height of a given row.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row: The 1-based row index.
|
||||
:param height: The desired height (float).
|
||||
|
||||
.. method:: get_frozen_columns_count(sheet: int) -> int
|
||||
|
||||
Returns the number of columns frozen (pinned) on the left side of the sheet.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:rtype: int
|
||||
|
||||
.. method:: get_frozen_rows_count(sheet: int) -> int
|
||||
|
||||
Returns the number of rows frozen (pinned) at the top of the sheet.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:rtype: int
|
||||
|
||||
.. method:: set_frozen_columns_count(sheet: int, column_count: int)
|
||||
|
||||
Sets how many columns are frozen (pinned) on the left.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param column_count: The number of frozen columns (0-based).
|
||||
|
||||
.. method:: set_frozen_rows_count(sheet: int, row_count: int)
|
||||
|
||||
Sets how many rows are frozen (pinned) at the top.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param row_count: The number of frozen rows (0-based).
|
||||
|
||||
.. method:: get_worksheets_properties() -> List[PySheetProperty]
|
||||
|
||||
Returns a list of :class:`PySheetProperty` describing each worksheet’s
|
||||
name, visibility state, ID, and tab color.
|
||||
|
||||
:rtype: list of PySheetProperty
|
||||
|
||||
.. method:: set_sheet_color(sheet: int, color: str)
|
||||
|
||||
Sets the tab color of a sheet. Use an empty string to clear the color.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param color: A color in “#RRGGBB” format, or empty to remove color.
|
||||
|
||||
.. method:: add_sheet(sheet_name: str)
|
||||
|
||||
Creates a new sheet with the specified name.
|
||||
|
||||
:param sheet_name: The name to give the new sheet.
|
||||
|
||||
.. method:: new_sheet()
|
||||
|
||||
Creates a new sheet with an auto-generated name.
|
||||
|
||||
.. method:: delete_sheet(sheet: int)
|
||||
|
||||
Deletes the sheet at the given index.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
|
||||
.. method:: rename_sheet(sheet: int, new_name: str)
|
||||
|
||||
Renames the sheet at the given index.
|
||||
|
||||
:param sheet: The sheet index (0-based).
|
||||
:param new_name: The new sheet name.
|
||||
|
||||
.. method:: test_panic()
|
||||
|
||||
A test method that deliberately panics in Rust.
|
||||
Used for testing panic handling at the method level.
|
||||
|
||||
:raises WorkbookError: (wrapped Rust panic)
|
||||
@@ -1,13 +1,20 @@
|
||||
IronCalc: The democratization of spreadsheets
|
||||
=============================================
|
||||
IronCalc
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
IronCalc is a spreadsheet engine that allows you to create, modify and safe spreadsheets.
|
||||
installation
|
||||
usage_examples
|
||||
top_level_methods
|
||||
api_reference
|
||||
objects
|
||||
|
||||
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.
|
||||
|
||||
A simple example that creates a model, sets a formula, evaluates it and gets the result back:
|
||||
|
||||
.. literalinclude:: examples/simple.py
|
||||
:language: python
|
||||
:language: python
|
||||
|
||||
|
||||
9
bindings/python/docs/installation.rst
Normal file
9
bindings/python/docs/installation.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can simply do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install ironcalc
|
||||
|
||||
32
bindings/python/docs/objects.rst
Normal file
32
bindings/python/docs/objects.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
Objects
|
||||
-------
|
||||
|
||||
The following examples
|
||||
|
||||
|
||||
``WorkbookError``
|
||||
^^^^^^^^^^^^^^^^^
|
||||
Exceptions of type ``WorkbookError`` are raised whenever there is a problem with
|
||||
the workbook (e.g., invalid parameters, file I/O error, or even a Rust panic).
|
||||
You can catch these exceptions in Python as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ironcalc import WorkbookError
|
||||
|
||||
try:
|
||||
# Some operation on PyModel
|
||||
pass
|
||||
except WorkbookError as e:
|
||||
print("Caught a workbook error:", e)
|
||||
|
||||
``PyCellType``
|
||||
^^^^^^^^^^^^^^
|
||||
Represents the type of a cell (e.g., number, string, boolean, etc.). You can
|
||||
check the type of a cell with :meth:`PyModel.get_cell_type`.
|
||||
|
||||
``PyStyle``
|
||||
^^^^^^^^^^^
|
||||
Represents the style of a cell (font, bold, number formats, alignment, etc.).
|
||||
You can get/set these styles with :meth:`PyModel.get_cell_style`
|
||||
and :meth:`PyModel.set_cell_style`.
|
||||
6
bindings/python/docs/top_level_methods.rst
Normal file
6
bindings/python/docs/top_level_methods.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
Top Level Methods
|
||||
-----------------
|
||||
|
||||
.. autofunction:: ironcalc.create
|
||||
.. autofunction:: ironcalc.load_from_xlsx
|
||||
.. autofunction:: ironcalc.load_from_icalc
|
||||
37
bindings/python/docs/usage_examples.rst
Normal file
37
bindings/python/docs/usage_examples.rst
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
Usage Examples
|
||||
--------------
|
||||
|
||||
Creating an Empty Model
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ironcalc as ic
|
||||
|
||||
model = ic.create("My Workbook", "en", "UTC")
|
||||
|
||||
Loading from XLSX
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ironcalc as ic
|
||||
|
||||
model = ic.load_from_xlsx("example.xlsx", "en", "UTC")
|
||||
|
||||
Modifying and Saving
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
model = ic.create("model", "en", "UTC")
|
||||
model.set_user_input(0, 1, 1, "123")
|
||||
model.set_user_input(0, 1, 2, "=A1*2")
|
||||
model.evaluate()
|
||||
|
||||
# Save to XLSX
|
||||
model.save_to_xlsx("updated.xlsx")
|
||||
|
||||
# Or save to the binary format
|
||||
model.save_to_icalc("my_workbook.icalc")
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Office/Business :: Financial :: Spreadsheet",
|
||||
"Topic :: Office/Business :: Financial :: Spreadsheet",
|
||||
]
|
||||
authors = [
|
||||
{ name = "Nicolás Hatcher", email = "nicolas@theuniverse.today" },
|
||||
|
||||
@@ -2,8 +2,8 @@ use pyo3::exceptions::PyException;
|
||||
use pyo3::{create_exception, prelude::*, wrap_pyfunction};
|
||||
|
||||
use types::{PySheetProperty, PyStyle};
|
||||
use xlsx::base::types::Style;
|
||||
use xlsx::base::Model;
|
||||
use xlsx::base::types::Style;
|
||||
|
||||
use xlsx::export::{save_to_icalc, save_to_xlsx};
|
||||
use xlsx::import;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "wasm"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
|
||||
description = "IronCalc Web bindings"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/ironcalc/ironcalc"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
@@ -7,6 +7,15 @@ https://www.npmjs.com/package/@ironcalc/wasm?activeTab=readme
|
||||
|
||||
## Building
|
||||
|
||||
Dependencies:
|
||||
|
||||
* Rust
|
||||
* wasm-pack
|
||||
* TypeScript
|
||||
* Python
|
||||
* binutils (for make)
|
||||
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::{
|
||||
prelude::{wasm_bindgen, JsError},
|
||||
JsValue,
|
||||
prelude::{JsError, wasm_bindgen},
|
||||
};
|
||||
|
||||
use ironcalc_base::{
|
||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
|
||||
types::{CellType, Style},
|
||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||
};
|
||||
|
||||
fn to_js_error(error: String) -> JsError {
|
||||
@@ -174,6 +174,27 @@ impl Model {
|
||||
self.model.range_clear_contents(&range).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "rangeClearFormatting")]
|
||||
pub fn range_clear_formatting(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let range = Area {
|
||||
sheet,
|
||||
row: start_row,
|
||||
column: start_column,
|
||||
width: end_column - start_column + 1,
|
||||
height: end_row - start_row + 1,
|
||||
};
|
||||
self.model
|
||||
.range_clear_formatting(&range)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "insertRow")]
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||
@@ -194,17 +215,29 @@ impl Model {
|
||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setRowHeight")]
|
||||
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), JsError> {
|
||||
#[wasm_bindgen(js_name = "setRowsHeight")]
|
||||
pub fn set_rows_height(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row_start: i32,
|
||||
row_end: i32,
|
||||
height: f64,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_row_height(sheet, row, height)
|
||||
.set_rows_height(sheet, row_start, row_end, height)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setColumnWidth")]
|
||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), JsError> {
|
||||
#[wasm_bindgen(js_name = "setColumnsWidth")]
|
||||
pub fn set_columns_width(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column_start: i32,
|
||||
column_end: i32,
|
||||
width: f64,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_column_width(sheet, column, width)
|
||||
.set_columns_width(sheet, column_start, column_end, width)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
@@ -271,6 +304,37 @@ impl Model {
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
// This two are only used when we want to compute the automatic width of a column or height of a row
|
||||
#[wasm_bindgen(js_name = "getRowsWithData")]
|
||||
pub fn get_rows_with_data(&self, sheet: u32, column: i32) -> Result<Vec<i32>, JsError> {
|
||||
let sheet_data = &self
|
||||
.model
|
||||
.get_model()
|
||||
.workbook
|
||||
.worksheet(sheet)
|
||||
.map_err(to_js_error)?
|
||||
.sheet_data;
|
||||
Ok(sheet_data
|
||||
.iter()
|
||||
.filter(|(_, data)| data.contains_key(&column))
|
||||
.map(|(row, _)| *row)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getColumnsWithData")]
|
||||
pub fn get_columns_with_data(&self, sheet: u32, row: i32) -> Result<Vec<i32>, JsError> {
|
||||
Ok(self
|
||||
.model
|
||||
.get_model()
|
||||
.workbook
|
||||
.worksheet(sheet)
|
||||
.map_err(to_js_error)?
|
||||
.sheet_data
|
||||
.get(&row)
|
||||
.map(|row_data| row_data.keys().copied().collect())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
||||
pub fn update_range_style(
|
||||
&mut self,
|
||||
|
||||
@@ -20,7 +20,7 @@ test('Row height', () => {
|
||||
let model = new Model('Workbook1', 'en', 'UTC');
|
||||
assert.strictEqual(model.getRowHeight(0, 3), DEFAULT_ROW_HEIGHT);
|
||||
|
||||
model.setRowHeight(0, 3, 32);
|
||||
model.setRowsHeight(0, 3, 3, 32);
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
||||
|
||||
model.undo();
|
||||
@@ -29,7 +29,7 @@ test('Row height', () => {
|
||||
model.redo();
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
||||
|
||||
model.setRowHeight(0, 3, 320);
|
||||
model.setRowsHeight(0, 3, 3, 320);
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 320);
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ test("Add sheets", (t) => {
|
||||
test("invalid sheet index throws an exception", () => {
|
||||
const model = new Model('Workbook1', 'en', 'UTC');
|
||||
assert.throws(() => {
|
||||
model.setRowHeight(1, 1, 100);
|
||||
model.setRowsHeight(1, 1, 1, 100);
|
||||
}, {
|
||||
name: 'Error',
|
||||
message: 'Invalid sheet index',
|
||||
@@ -106,7 +106,7 @@ test("invalid sheet index throws an exception", () => {
|
||||
test("invalid column throws an exception", () => {
|
||||
const model = new Model('Workbook1', 'en', 'UTC');
|
||||
assert.throws(() => {
|
||||
model.setRowHeight(0, -1, 100);
|
||||
model.setRowsHeight(0, -1, 0, 100);
|
||||
}, {
|
||||
name: 'Error',
|
||||
message: "Row number '-1' is not valid.",
|
||||
@@ -115,7 +115,7 @@ test("invalid column throws an exception", () => {
|
||||
|
||||
test("floating column numbers get truncated", () => {
|
||||
const model = new Model('Workbook1', 'en', 'UTC');
|
||||
model.setRowHeight(0.8, 5.2, 100.5);
|
||||
model.setRowsHeight(0.8, 5.2, 5.5, 100.5);
|
||||
|
||||
assert.strictEqual(model.getRowHeight(0.11, 5.99), 100.5);
|
||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
||||
|
||||
@@ -6,4 +6,4 @@ lang: en-US
|
||||
|
||||
# How to Contribute
|
||||
|
||||
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action), join our [Discord Channel](https://discord.gg/sjaefMWE) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
|
||||
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action), join our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
|
||||
|
||||
@@ -12,10 +12,6 @@ Although IronCalc is ready for use, it’s important to understand its current l
|
||||
|
||||
IronCalc currently does not implement arrays or array formulas. These are planned and are coming very soon, as they are the highest priority on the engine side.
|
||||
|
||||
## **Name Manager** <Badge type="info" text="Planned" />
|
||||
|
||||
While IronCalc supports importing and exporting defined names, it does not yet allow you to create, delete, or update them in the UI. This feature is expected to be implemented shortly.
|
||||
|
||||
## **Only English Supported**
|
||||
|
||||
The MVP version of IronCalc supports only the English language. However, version 1.0 will include support for three languages: **English**, **German**, and **Spanish**.
|
||||
|
||||
@@ -6,7 +6,20 @@ lang: en-US
|
||||
|
||||
# Name Manager
|
||||
|
||||
::: warning
|
||||
**Note:** This draft page is under construction 🚧
|
||||
:::
|
||||
The **Name Manager** makes working with specific cells or ranges easier by letting you assign custom names and set their scope.
|
||||
|
||||
## How to Use It
|
||||
|
||||
1. Click the **Named Ranges** button in the toolbar.
|
||||
- A dialog will open.
|
||||
2. Click **Add New**.
|
||||
3. Input a name to identify the range.
|
||||
4. Set the scope:
|
||||
- **Global**: Applies to the entire workbook.
|
||||
- **Sheet-specific**: Applies only to the selected sheet.
|
||||
5. Click the check icon to save your changes.
|
||||
|
||||
## Managing Named Ranges
|
||||
|
||||
- Use **Edit** to modify name, range, or scope.
|
||||
- Use **Delete** to remove ranges when they’re no longer needed.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "generate_locale"
|
||||
version = "0.1.0"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { StorybookConfig } from "@storybook/react-vite";
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
|
||||
@@ -14,9 +14,8 @@ npm install
|
||||
|
||||
## Local development
|
||||
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
## Linter and formatting
|
||||
@@ -39,20 +38,9 @@ npm run test
|
||||
|
||||
Warning: There is only the testing infrastructure in place.
|
||||
|
||||
## Deploy
|
||||
## Build package
|
||||
|
||||
Deploying is a bit of a manual hassle right now:
|
||||
To build a deployable frontend:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Please copy the `inroncalc.svg` icon and the models you want to have as 'examples' in the internal 'ic' format.
|
||||
I normally compress the wasm and js files with brotli
|
||||
|
||||
```
|
||||
brotli wasm_bg-*****.wasm
|
||||
```
|
||||
|
||||
Copy to the final destination and you are good to go.
|
||||
914
webapp/IronCalc/package-lock.json
generated
914
webapp/IronCalc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "@ironcalc/workbook",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"type": "module",
|
||||
"main": "./dist/ironcalc.js",
|
||||
"module": "./dist/ironcalc.js",
|
||||
"source": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && tsc",
|
||||
"check": "biome check ./src",
|
||||
"check-write": "biome check --write ./src",
|
||||
"test": "vitest run",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook": "storybook dev -p 6006 --no-open",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -27,19 +26,18 @@
|
||||
"react-i18next": "^15.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@chromatic-com/storybook": "^3.2.4",
|
||||
"@storybook/addon-essentials": "^8.6.0-alpha.0",
|
||||
"@storybook/addon-interactions": "^8.6.0-alpha.0",
|
||||
"@storybook/addon-onboarding": "^8.6.0-alpha.0",
|
||||
"@storybook/blocks": "^8.6.0-alpha.0",
|
||||
"@storybook/react": "^8.6.0-alpha.0",
|
||||
"@storybook/react-vite": "^8.6.0-alpha.0",
|
||||
"@storybook/test": "^8.6.0-alpha.0",
|
||||
"@storybook/addon-essentials": "^8.5.3",
|
||||
"@storybook/addon-interactions": "^8.5.3",
|
||||
"@storybook/blocks": "^8.5.3",
|
||||
"@storybook/react": "^8.5.3",
|
||||
"@storybook/react-vite": "^8.5.3",
|
||||
"@storybook/test": "^8.5.3",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"storybook": "^8.6.0-alpha.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"storybook": "^8.5.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.5",
|
||||
@@ -47,9 +45,9 @@
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"@types/react": "^18.0.0 || ^19.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
289
webapp/IronCalc/src/components/CellContextMenu.tsx
Normal file
289
webapp/IronCalc/src/components/CellContextMenu.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { Menu, MenuItem, styled } from "@mui/material";
|
||||
import {
|
||||
BetweenHorizontalStart,
|
||||
BetweenVerticalStart,
|
||||
ChevronRight,
|
||||
Snowflake,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { theme } from "../theme";
|
||||
|
||||
const red_color = theme.palette.error.main;
|
||||
|
||||
interface CellContextMenuProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
anchorEl: HTMLDivElement | null;
|
||||
onInsertRowAbove: () => void;
|
||||
onInsertRowBelow: () => void;
|
||||
onInsertColumnLeft: () => void;
|
||||
onInsertColumnRight: () => void;
|
||||
onFreezeColumns: () => void;
|
||||
onFreezeRows: () => void;
|
||||
onUnfreezeColumns: () => void;
|
||||
onUnfreezeRows: () => void;
|
||||
onDeleteRow: () => void;
|
||||
onDeleteColumn: () => void;
|
||||
row: number;
|
||||
column: string;
|
||||
}
|
||||
|
||||
const CellContextMenu = (properties: CellContextMenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
open,
|
||||
onClose,
|
||||
anchorEl,
|
||||
onInsertRowAbove,
|
||||
onInsertRowBelow,
|
||||
onInsertColumnLeft,
|
||||
onInsertColumnRight,
|
||||
onFreezeColumns,
|
||||
onFreezeRows,
|
||||
onUnfreezeColumns,
|
||||
onUnfreezeRows,
|
||||
onDeleteRow,
|
||||
onDeleteColumn,
|
||||
row,
|
||||
column,
|
||||
} = properties;
|
||||
const [freezeMenuOpen, setFreezeMenuOpen] = useState(false);
|
||||
const freezeRef = useRef(null);
|
||||
|
||||
const [insertRowMenuOpen, setInsertRowMenuOpen] = useState(false);
|
||||
const insertRowRef = useRef(null);
|
||||
|
||||
const [insertColumnMenuOpen, setInsertColumnMenuOpen] = useState(false);
|
||||
const insertColumnRef = useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMenu
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: 6,
|
||||
}}
|
||||
>
|
||||
<StyledMenuItem
|
||||
ref={insertColumnRef}
|
||||
onClick={() => setInsertColumnMenuOpen(true)}
|
||||
>
|
||||
<BetweenVerticalStartStyled />
|
||||
<ItemNameStyled>{t("cell_context.insert_column")}</ItemNameStyled>
|
||||
<ChevronRightStyled />
|
||||
</StyledMenuItem>
|
||||
<StyledMenuItem
|
||||
ref={insertRowRef}
|
||||
onClick={() => setInsertRowMenuOpen(true)}
|
||||
>
|
||||
<BetweenHorizontalStartStyled />
|
||||
<ItemNameStyled>{t("cell_context.insert_row")}</ItemNameStyled>
|
||||
<ChevronRightStyled />
|
||||
</StyledMenuItem>
|
||||
<MenuDivider />
|
||||
<StyledMenuItem ref={freezeRef} onClick={() => setFreezeMenuOpen(true)}>
|
||||
<StyledSnowflake />
|
||||
<ItemNameStyled>{t("cell_context.freeze")}</ItemNameStyled>
|
||||
<ChevronRightStyled />
|
||||
</StyledMenuItem>
|
||||
<MenuDivider />
|
||||
<StyledMenuItem onClick={onDeleteRow}>
|
||||
<StyledTrash />
|
||||
<ItemNameStyled style={{ color: red_color }}>
|
||||
{t("cell_context.delete_row", { row })}
|
||||
</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
<StyledMenuItem onClick={onDeleteColumn}>
|
||||
<StyledTrash />
|
||||
<ItemNameStyled style={{ color: red_color }}>
|
||||
{t("cell_context.delete_column", { column })}
|
||||
</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
</StyledMenu>
|
||||
<StyledMenu
|
||||
open={insertRowMenuOpen}
|
||||
onClose={() => setInsertRowMenuOpen(false)}
|
||||
anchorEl={insertRowRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
setInsertRowMenuOpen(false);
|
||||
onInsertRowAbove();
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>{t("cell_context.insert_row_above")}</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
setInsertRowMenuOpen(false);
|
||||
onInsertRowBelow();
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>{t("cell_context.insert_row_below")}</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
</StyledMenu>
|
||||
<StyledMenu
|
||||
open={insertColumnMenuOpen}
|
||||
onClose={() => setInsertColumnMenuOpen(false)}
|
||||
anchorEl={insertColumnRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
setInsertColumnMenuOpen(false);
|
||||
onInsertColumnLeft();
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>
|
||||
{t("cell_context.insert_column_before")}
|
||||
</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
setInsertColumnMenuOpen(false);
|
||||
onInsertColumnRight();
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>
|
||||
{t("cell_context.insert_column_after")}
|
||||
</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
</StyledMenu>
|
||||
<StyledMenu
|
||||
open={freezeMenuOpen}
|
||||
onClose={() => setFreezeMenuOpen(false)}
|
||||
anchorEl={freezeRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
onFreezeColumns();
|
||||
setFreezeMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>
|
||||
{t("cell_context.freeze_columns", { column })}
|
||||
</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
onFreezeRows();
|
||||
setFreezeMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>
|
||||
{t("cell_context.freeze_rows", { row })}
|
||||
</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
<MenuDivider />
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
onUnfreezeColumns();
|
||||
setFreezeMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>{t("cell_context.unfreeze_columns")}</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
onUnfreezeRows();
|
||||
setFreezeMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<ItemNameStyled>{t("cell_context.unfreeze_rows")}</ItemNameStyled>
|
||||
</StyledMenuItem>
|
||||
</StyledMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BetweenVerticalStartStyled = styled(BetweenVerticalStart)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: ${theme.palette.grey[900]};
|
||||
padding-right: 10px;
|
||||
`;
|
||||
|
||||
const BetweenHorizontalStartStyled = styled(BetweenHorizontalStart)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: ${theme.palette.grey[900]};
|
||||
padding-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledSnowflake = styled(Snowflake)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: ${theme.palette.grey[900]};
|
||||
padding-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledTrash = styled(Trash2)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: ${red_color};
|
||||
padding-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledMenu = styled(Menu)({
|
||||
"& .MuiPaper-root": {
|
||||
borderRadius: 8,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
"& .MuiList-padding": {
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
font-size: 14px;
|
||||
width: calc(100% - 8px);
|
||||
min-width: 172px;
|
||||
margin: 0px 4px;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
height: 32px;
|
||||
`;
|
||||
|
||||
const MenuDivider = styled("div")`
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
border-top: 1px solid ${theme.palette.grey[200]};
|
||||
`;
|
||||
|
||||
const ItemNameStyled = styled("div")`
|
||||
font-size: 12px;
|
||||
color: ${theme.palette.grey[900]};
|
||||
flex-grow: 2;
|
||||
`;
|
||||
|
||||
const ChevronRightStyled = styled(ChevronRight)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`;
|
||||
|
||||
export default CellContextMenu;
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { BookOpen, Plus, X } from "lucide-react";
|
||||
import { BookOpen, PackageOpen, Plus, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { theme } from "../../theme";
|
||||
import NamedRangeActive from "./NamedRangeActive";
|
||||
@@ -79,65 +79,79 @@ function NameManagerDialog(properties: NameManagerDialogProperties) {
|
||||
</Cross>
|
||||
</StyledDialogTitle>
|
||||
<StyledDialogContent>
|
||||
<StyledRangesHeader>
|
||||
<StyledBox>{t("name_manager_dialog.name")}</StyledBox>
|
||||
<StyledBox>{t("name_manager_dialog.range")}</StyledBox>
|
||||
<StyledBox>{t("name_manager_dialog.scope")}</StyledBox>
|
||||
</StyledRangesHeader>
|
||||
<NameListWrapper>
|
||||
{definedNameList.map((definedName, index) => {
|
||||
const scopeName = definedName.scope
|
||||
? worksheets[definedName.scope].name
|
||||
: "[global]";
|
||||
if (index === editingNameIndex) {
|
||||
{(definedNameList.length > 0 || editingNameIndex !== -2) && (
|
||||
<StyledRangesHeader>
|
||||
<StyledBox>{t("name_manager_dialog.name")}</StyledBox>
|
||||
<StyledBox>{t("name_manager_dialog.range")}</StyledBox>
|
||||
<StyledBox>{t("name_manager_dialog.scope")}</StyledBox>
|
||||
</StyledRangesHeader>
|
||||
)}
|
||||
{definedNameList.length === 0 && editingNameIndex === -2 ? (
|
||||
<EmptyStateMessage>
|
||||
<IconWrapper>
|
||||
<PackageOpen />
|
||||
</IconWrapper>
|
||||
{t("name_manager_dialog.empty_message1")}
|
||||
<br />
|
||||
{t("name_manager_dialog.empty_message2")}
|
||||
</EmptyStateMessage>
|
||||
) : (
|
||||
<NameListWrapper>
|
||||
{definedNameList.map((definedName, index) => {
|
||||
const scopeName =
|
||||
definedName.scope !== undefined
|
||||
? worksheets[definedName.scope].name
|
||||
: "[global]";
|
||||
if (index === editingNameIndex) {
|
||||
return (
|
||||
<NamedRangeActive
|
||||
worksheets={worksheets}
|
||||
name={definedName.name}
|
||||
scope={scopeName}
|
||||
formula={definedName.formula}
|
||||
key={definedName.name + definedName.scope}
|
||||
onSave={(
|
||||
newName,
|
||||
newScope,
|
||||
newFormula,
|
||||
): string | undefined => {
|
||||
const scope_index = worksheets.findIndex(
|
||||
(s) => s.name === newScope,
|
||||
);
|
||||
const scope = scope_index >= 0 ? scope_index : undefined;
|
||||
try {
|
||||
updateDefinedName(
|
||||
definedName.name,
|
||||
definedName.scope,
|
||||
newName,
|
||||
scope,
|
||||
newFormula,
|
||||
);
|
||||
setEditingNameIndex(-2);
|
||||
} catch (e) {
|
||||
return `${e}`;
|
||||
}
|
||||
}}
|
||||
onCancel={() => setEditingNameIndex(-2)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NamedRangeActive
|
||||
worksheets={worksheets}
|
||||
<NamedRangeInactive
|
||||
name={definedName.name}
|
||||
scope={scopeName}
|
||||
formula={definedName.formula}
|
||||
key={definedName.name + definedName.scope}
|
||||
onSave={(
|
||||
newName,
|
||||
newScope,
|
||||
newFormula,
|
||||
): string | undefined => {
|
||||
const scope_index = worksheets.findIndex(
|
||||
(s) => s.name === newScope,
|
||||
);
|
||||
const scope = scope_index > 0 ? scope_index : undefined;
|
||||
try {
|
||||
updateDefinedName(
|
||||
definedName.name,
|
||||
definedName.scope,
|
||||
newName,
|
||||
scope,
|
||||
newFormula,
|
||||
);
|
||||
setEditingNameIndex(-2);
|
||||
} catch (e) {
|
||||
return `${e}`;
|
||||
}
|
||||
showOptions={editingNameIndex === -2}
|
||||
onEdit={() => setEditingNameIndex(index)}
|
||||
onDelete={() => {
|
||||
deleteDefinedName(definedName.name, definedName.scope);
|
||||
}}
|
||||
onCancel={() => setEditingNameIndex(-2)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NamedRangeInactive
|
||||
name={definedName.name}
|
||||
scope={scopeName}
|
||||
formula={definedName.formula}
|
||||
key={definedName.name + definedName.scope}
|
||||
showOptions={editingNameIndex === -2}
|
||||
onEdit={() => setEditingNameIndex(index)}
|
||||
onDelete={() => {
|
||||
deleteDefinedName(definedName.name, definedName.scope);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</NameListWrapper>
|
||||
})}
|
||||
</NameListWrapper>
|
||||
)}
|
||||
{editingNameIndex === -1 && (
|
||||
<NamedRangeActive
|
||||
worksheets={worksheets}
|
||||
@@ -230,6 +244,39 @@ const NameListWrapper = styled(Stack)`
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const EmptyStateMessage = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: ${theme.palette.grey["600"]};
|
||||
font-family: "Inter";
|
||||
z-index: 0;
|
||||
margin: auto 0px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
background-color: ${theme.palette.grey["100"]};
|
||||
color: ${theme.palette.grey["600"]};
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -26,7 +26,7 @@ function SheetTab(props: SheetTabProps) {
|
||||
const { name, color, selected, workbookState, onSelected } = props;
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
const colorButton = useRef(null);
|
||||
const colorButton = useRef<HTMLDivElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -57,12 +57,12 @@ function SheetTab(props: SheetTabProps) {
|
||||
<TabWrapper
|
||||
$color={color}
|
||||
$selected={selected}
|
||||
onClick={(event) => {
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
onSelected();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
onPointerDown={(event: React.PointerEvent) => {
|
||||
// If it is in browse mode stop he event
|
||||
const cell = workbookState.getEditingCell();
|
||||
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user