Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
# the test fixtures the bindings carry: a zip container, which `* text eol=lf`
# above would happily rewrite into an archive nothing can open
*.odt binary
*.ods binary
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- The python, Java, Objective-C and npm bindings style a sheet cell
(`Sheet::set_cell_style`). In Java and Objective-C, a `TableCellStyle` can
now be built and written.

- The sheet editor formats cells. A selection is a rectangle, spanned by a
shift click, a shift arrow, a mouse drag or a header click, and
`odr.editing.format` and `toggle` take `fill` and `align` beside the text
Expand Down
10 changes: 10 additions & 0 deletions apple/include/OdrCoreObjC/ODRDocumentElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ NS_SWIFT_NAME(Sheet)
- (ODRTableCellStyle *)styleForCellAtColumn:(uint32_t)column
row:(uint32_t)row
NS_SWIFT_NAME(style(column:row:));
/// States the non-nil properties of both styles on the cell and leaves the
/// rest: the fill, the horizontal alignment and the text properties
/// `-[ODRText setStyle:error:]` takes. Any other refuses with
/// `ODRErrorUnsupportedOperation`.
- (BOOL)setCellStyle:(ODRTableCellStyle *)cellStyle
textStyle:(ODRTextStyle *)textStyle
atColumn:(uint32_t)column
row:(uint32_t)row
error:(NSError **)error
NS_SWIFT_NAME(setStyle(_:textStyle:column:row:));
@end

/// `odr::SheetCell`.
Expand Down
16 changes: 8 additions & 8 deletions apple/include/OdrCoreObjC/ODRStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,20 +222,20 @@ NS_SWIFT_NAME(TableRowStyle)
NS_SWIFT_NAME(TableCellStyle)
@interface ODRTableCellStyle : NSObject
/// `ODRHorizontalAlign`, boxed.
@property(nonatomic, readonly, nullable) NSNumber *horizontalAlign;
@property(nonatomic, nullable) NSNumber *horizontalAlign;
/// `ODRVerticalAlign`, boxed.
@property(nonatomic, readonly, nullable) NSNumber *verticalAlign;
/// `ODRColor`, boxed in an `NSValue`.
@property(nonatomic, readonly, nullable) NSValue *backgroundColor;
@property(nonatomic, nullable) NSNumber *verticalAlign;
/// `ODRColor`, boxed in an `NSValue`; an alpha of 0 takes a fill away.
@property(nonatomic, nullable) NSValue *backgroundColor;
@property(nonatomic, readonly) ODRDirectionalMeasure *padding;
@property(nonatomic, readonly) ODRDirectionalString *border;
/// `double`, boxed.
@property(nonatomic, readonly, nullable) NSNumber *textRotation;
@property(nonatomic, nullable) NSNumber *textRotation;
/// `BOOL`, boxed.
@property(nonatomic, readonly, nullable) NSNumber *wrapText;
@property(nonatomic, nullable) NSNumber *wrapText;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
/// Every property `nil`, and no side of `padding` or `border` stated.
- (instancetype)init;
@end

/// Graphic style — `odr::GraphicStyle`.
Expand Down
12 changes: 12 additions & 0 deletions apple/src/ODRDocumentElement.mm
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,18 @@ - (ODRTableCellStyle *)styleForCellAtColumn:(uint32_t)column row:(uint32_t)row {
nil);
}

- (BOOL)setCellStyle:(ODRTableCellStyle *)cellStyle
textStyle:(ODRTextStyle *)textStyle
atColumn:(uint32_t)column
row:(uint32_t)row
error:(NSError **)error {
return guarded(error, [&] {
self.handle.as_sheet().set_cell_style(column, row, [cellStyle handle],
[textStyle handle]);
return YES;
});
}

@end

@implementation ODRSheetCell
Expand Down
3 changes: 3 additions & 0 deletions apple/src/ODRPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ NS_ASSUME_NONNULL_BEGIN

@interface ODRTableCellStyle (Private)
+ (instancetype)styleWithHandle:(const odr::TableCellStyle &)handle;
/// The set properties as a `TableCellStyle`; throws `UnsupportedOperation`
/// where `padding` or `border` states a side, which no engine writes.
- (odr::TableCellStyle)handle;
@end

@interface ODRDrawingPath (Private)
Expand Down
38 changes: 38 additions & 0 deletions apple/src/ODRStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#import "ODRInternal.h"
#import "ODRPrivate.h"

#include <odr/exceptions.hpp>
#include <odr/style.hpp>

#include <optional>
Expand Down Expand Up @@ -306,6 +307,43 @@ + (instancetype)styleWithHandle:(const odr::TableRowStyle &)handle {

@implementation ODRTableCellStyle

- (instancetype)init {
if ((self = [super init]) != nil) {
_padding = [ODRDirectionalMeasure
directionalWithHandle:odr::DirectionalStyle<odr::Measure>()];
_border = [ODRDirectionalString
directionalWithHandle:odr::DirectionalStyle<std::string>()];
}
return self;
}

- (odr::TableCellStyle)handle {
if (_padding.right != nil || _padding.top != nil || _padding.left != nil ||
_padding.bottom != nil || _border.right != nil || _border.top != nil ||
_border.left != nil || _border.bottom != nil) {
throw odr::UnsupportedOperation();
}
odr::TableCellStyle result;
if (_horizontalAlign != nil) {
result.horizontal_align =
static_cast<odr::HorizontalAlign>(_horizontalAlign.integerValue);
}
if (_verticalAlign != nil) {
result.vertical_align =
static_cast<odr::VerticalAlign>(_verticalAlign.integerValue);
}
if (_backgroundColor != nil) {
result.background_color = unbox_color(_backgroundColor);
}
if (_textRotation != nil) {
result.text_rotation = _textRotation.doubleValue;
}
if (_wrapText != nil) {
result.wrap_text = _wrapText.boolValue != NO;
}
return result;
}

+ (instancetype)styleWithHandle:(const odr::TableCellStyle &)handle {
ODRTableCellStyle *const result = [[ODRTableCellStyle alloc] init];
result->_horizontalAlign = box_enum(handle.horizontal_align);
Expand Down
5 changes: 5 additions & 0 deletions apple/tests/Fixture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ enum Fixture {
try path("mixed-layout", "odt")
}

/// `ods/span.ods` from the same repository; A1 holds "a".
static func ods() throws -> String {
try path("span", "ods")
}

/// A one-page pdf written to a temporary file, its cross-reference offsets
/// computed so they are right.
static func pdf() throws -> String {
Expand Down
Binary file added apple/tests/Fixtures/span.ods
Binary file not shown.
47 changes: 47 additions & 0 deletions apple/tests/OdrCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,53 @@ final class DocumentSaveTests: XCTestCase {
}
}

func testSetCellStyleFillsACell() throws {
let document = try DecodedFile.decode(path: try Fixture.ods())
.asDocumentFile().document()
let root = try XCTUnwrap(try document.rootElement())
let sheet = try XCTUnwrap(root.firstDescendant(ofType: Sheet.self))

let cellStyle = TableCellStyle()
var yellow = ODRColor(red: 255, green: 255, blue: 0, alpha: 255)
cellStyle.backgroundColor = NSValue(bytes: &yellow, objCType: "{ODRColor=CCCC}")
cellStyle.horizontalAlign = NSNumber(value: HorizontalAlign.center.rawValue)
let textStyle = TextStyle()
textStyle.fontWeight = NSNumber(value: FontWeight.bold.rawValue)
try sheet.setStyle(cellStyle, textStyle: textStyle, column: 0, row: 0)

let saved = try XCTUnwrap(try document.saveToMemory())
let path = URL(fileURLWithPath: try temporaryDirectory())
.appendingPathComponent("styled.ods")
try saved.write(to: path)

let reloaded = try DecodedFile.decode(path: path.path)
.asDocumentFile().document()
let reloadedRoot = try XCTUnwrap(try reloaded.rootElement())
let reloadedSheet = try XCTUnwrap(reloadedRoot.firstDescendant(ofType: Sheet.self))
var fill = ODRColor()
try XCTUnwrap(reloadedSheet.style(column: 0, row: 0).backgroundColor)
.getValue(&fill, size: MemoryLayout<ODRColor>.size)
XCTAssertEqual([fill.red, fill.green, fill.blue], [255, 255, 0])
let cell = try XCTUnwrap(reloadedSheet.cell(column: 0, row: 0))
let text = try XCTUnwrap(cell.firstDescendant(ofType: Text.self))
XCTAssertEqual(text.style.fontWeight?.intValue, FontWeight.bold.rawValue)
}

func testSetCellStyleRefusesWhatNoEngineWrites() throws {
let document = try DecodedFile.decode(path: try Fixture.ods())
.asDocumentFile().document()
let root = try XCTUnwrap(try document.rootElement())
let sheet = try XCTUnwrap(root.firstDescendant(ofType: Sheet.self))

let cellStyle = TableCellStyle()
cellStyle.wrapText = true
XCTAssertThrowsError(
try sheet.setStyle(cellStyle, textStyle: TextStyle(), column: 0, row: 0)
) { error in
XCTAssertEqual((error as NSError).code, ODRError.unsupportedOperation.rawValue)
}
}

func testSaveToMemoryCarriesAnEdit() throws {
let document = try self.document()
XCTAssertTrue(document.isSavable)
Expand Down
5 changes: 3 additions & 2 deletions docs/design/spreadsheet-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ separate scripts. The coordinates are the ones an op names, never a DOM index.

## Cell formatting

Status: `.ods` and `.xlsx` write a cell style, and the sheet editor formats a
selection. The steps land as a stack, in this order:
Status: landed. `.ods` and `.xlsx` write a cell style, the sheet editor
formats a selection, and the bindings take `Sheet::set_cell_style`. The steps
landed as a stack, in this order:

1. The xlsx reader reads what the writer writes: a solid fill from `fgColor`,
theme colours with their `tint`, italic, underline and strikethrough, and
Expand Down
1 change: 1 addition & 0 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ if (ODR_TEST AND NOT ANDROID)
# verbatim, where an absolute path is not found.
RESOURCES NAMESPACE "app/opendocument/core"
"testfixtures/resources/app/opendocument/core/mixed-layout.odt"
"testfixtures/resources/app/opendocument/core/span.ods"
INCLUDE_JARS odr_java "${ODR_JNI_JUNIT_JAR}"
OUTPUT_NAME odr-core-java-tests
)
Expand Down
12 changes: 12 additions & 0 deletions jni/java/app/opendocument/core/Sheet.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ public TableCellStyle cellStyle(int column, int row) {
return cellStyleNative(handle(), column, row);
}

/**
* States the non-null fields of both styles on the cell and leaves the rest. The fill, the
* horizontal alignment and the text keys of {@link Text#setStyle} are written; any other field
* throws {@link OdrException.UnsupportedOperation}.
*/
public void setCellStyle(int column, int row, TableCellStyle cellStyle, TextStyle textStyle) {
setCellStyleNative(handle(), column, row, cellStyle, textStyle);
}

private native String nameNative(long handle);

private native PageLayout pageLayoutNative(long handle);
Expand All @@ -73,4 +82,7 @@ public TableCellStyle cellStyle(int column, int row) {
private native TableRowStyle rowStyleNative(long handle, int row);

private native TableCellStyle cellStyleNative(long handle, int column, int row);

private native void setCellStyleNative(
long handle, int column, int row, TableCellStyle cellStyle, TextStyle textStyle);
}
24 changes: 16 additions & 8 deletions jni/java/app/opendocument/core/TableCellStyle.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package app.opendocument.core;

/** Style of a table cell. Mirrors {@code odr::TableCellStyle}; fields may be {@code null}. */
/**
* Style of a table cell. Mirrors {@code odr::TableCellStyle}; a {@code null} field is one the
* document does not state. A caller builds one for {@link Sheet#setCellStyle}: every field left
* {@code null} is left alone on the cell.
*/
public final class TableCellStyle {
public final HorizontalAlign horizontalAlign;
public final VerticalAlign verticalAlign;
public final Color backgroundColor;
public final DirectionalMeasure padding;
public final DirectionalString border;
public final Double textRotation;
public final Boolean wrapText;
public HorizontalAlign horizontalAlign;
public VerticalAlign verticalAlign;
/** An alpha of 0 takes a fill away. */
public Color backgroundColor;
public DirectionalMeasure padding;
public DirectionalString border;
public Double textRotation;
public Boolean wrapText;

/** Every field {@code null}. */
public TableCellStyle() {}

TableCellStyle(
int horizontalAlign,
Expand Down
3 changes: 3 additions & 0 deletions jni/src/jni_convert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config);
odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config);
/// The fields a Java `TextStyle` states.
odr::TextStyle text_style_from_java(JNIEnv *env, jobject style);
/// The fields a Java `TableCellStyle` states; a padding or a border is
/// refused, since no engine writes one.
odr::TableCellStyle table_cell_style_from_java(JNIEnv *env, jobject style);

/// Optional enum to a Java-side code; -1 encodes absent.
template <typename E> jint enum_code(const std::optional<E> &value) {
Expand Down
14 changes: 14 additions & 0 deletions jni/src/jni_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,20 @@ Java_app_opendocument_core_Sheet_cellStyleNative(JNIEnv *env, jobject,
});
}

extern "C" JNIEXPORT void JNICALL
Java_app_opendocument_core_Sheet_setCellStyleNative(JNIEnv *env, jobject,
jlong handle, jint column,
jint row,
jobject cell_style,
jobject text_style) {
guarded(env, [&] {
element(handle).as_sheet().set_cell_style(
static_cast<std::uint32_t>(column), static_cast<std::uint32_t>(row),
odr_jni::table_cell_style_from_java(env, cell_style),
odr_jni::text_style_from_java(env, text_style));
});
}

// app.opendocument.core.SheetCell

extern "C" JNIEXPORT jobject JNICALL
Expand Down
63 changes: 63 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include "jni_convert.hpp"
#include "odr_jni.hpp"

#include <odr/exceptions.hpp>

#include <cstdarg>
#include <utility>
#include <vector>

namespace odr_jni {
Expand Down Expand Up @@ -339,6 +342,66 @@ odr::TextStyle text_style_from_java(JNIEnv *env, const jobject style) {
return result;
}

odr::TableCellStyle table_cell_style_from_java(JNIEnv *env,
const jobject style) {
odr::TableCellStyle result;
if (style == nullptr) {
return result;
}
jclass cls = env->GetObjectClass(style);
const auto field = [&](const char *name, const char *signature) {
return env->GetObjectField(style, env->GetFieldID(cls, name, signature));
};
const auto take = [&](jobject value, auto convert) {
auto converted = convert(value);
if (value != nullptr) {
env->DeleteLocalRef(value);
}
return converted;
};

for (const auto &[name, signature] :
{std::pair{"padding", "Lapp/opendocument/core/DirectionalMeasure;"},
std::pair{"border", "Lapp/opendocument/core/DirectionalString;"}}) {
if (const jobject value = field(name, signature); value != nullptr) {
env->DeleteLocalRef(value);
env->DeleteLocalRef(cls);
throw odr::UnsupportedOperation();
}
}
result.horizontal_align =
take(field("horizontalAlign", "Lapp/opendocument/core/HorizontalAlign;"),
[&](const jobject value) {
return enum_from_java<odr::HorizontalAlign>(env, value);
});
result.vertical_align =
take(field("verticalAlign", "Lapp/opendocument/core/VerticalAlign;"),
[&](const jobject value) {
return enum_from_java<odr::VerticalAlign>(env, value);
});
result.background_color =
take(field("backgroundColor", "Lapp/opendocument/core/Color;"),
[&](const jobject value) { return color_from_java(env, value); });
result.text_rotation =
take(field("textRotation", "Ljava/lang/Double;"),
[&](const jobject value) -> std::optional<double> {
if (value == nullptr) {
return std::nullopt;
}
jclass boxed = env->GetObjectClass(value);
const jdouble unboxed = env->CallDoubleMethod(
value, env->GetMethodID(boxed, "doubleValue", "()D"));
env->DeleteLocalRef(boxed);
return unboxed;
});
result.wrap_text =
take(field("wrapText", "Ljava/lang/Boolean;"),
[&](const jobject value) { return boolean_from_java(env, value); });

env->DeleteLocalRef(cls);
return result;
}

odr::DirectionalStyle<odr::Measure>
directional_measure_from_java(JNIEnv *env, jobject value) {
odr::DirectionalStyle<odr::Measure> result;
Expand Down
Loading
Loading