diff --git a/example/lightning-orb.png b/example/lightning-orb.png new file mode 100644 index 0000000..b8f2b7d Binary files /dev/null and b/example/lightning-orb.png differ diff --git a/example/tables.js b/example/tables.js new file mode 100644 index 0000000..d6e5ffc --- /dev/null +++ b/example/tables.js @@ -0,0 +1,49 @@ +const libui = require('..'); +const {Text, Image, Checkbox, ProgressBar, Button, LabeledCheckbox, LabeledImage} = + libui.UiTableModel.Fields; + +const win = new libui.UiWindow('Tables example', 800, 600, true); +win.margined = true; + +const model = libui.UiTableModel.fromMetadata({ + name: {type: Text, editable: true, header: 'Name'}, + surname: {type: Text}, + male: {type: Checkbox, editable: true, header: 'Is a male?'}, + picture: {type: Image, value: () => img}, + completed: {type: ProgressBar}, + someProp: {type: LabeledCheckbox, editable: true, label: 'some props'}, + otherProp: {type: LabeledImage, value: () => img, label: 'an Image'}, + sayhi: { + type: Button, + editable: () => 1, + label: 'Say Hi', + click: obj => libui.UiDialogs.msgBox(win, 'HI', obj.name + ' ' + obj.surname) + } +}); + +const data = [ + {name: 'Andrea', surname: 'Parodi', male: true, completed: 30, someProp: true}, + {name: 'Giorgia', surname: 'Parodi', male: false, completed: 75, someProp: false} +]; + +const tb = new libui.UiTable(model.bind(data)); +model.addColumns(tb, 'name', 'surname', 'picture', 'male', 'completed', 'sayhi', + 'someProp', 'otherProp'); + +const vbox = new libui.UiVerticalBox(); +vbox.append(tb, true); +win.setChild(vbox); + +win.onClosing(() => { + win.close(); + libui.stopLoop(); +}); + +let img = null; +async function run() { + img = await libui.UiImage.loadFromPng(__dirname + '/lightning-orb.png'); + win.show(); + libui.startLoop(); +} + +run().catch(err => console.error(err)); diff --git a/index.js b/index.js index 3295ac6..461f9fa 100644 --- a/index.js +++ b/index.js @@ -48,7 +48,7 @@ const { const {UiBox} = require('./js/box'); const {SeparatorBase} = require('./js/separator-base'); const {UiControl} = require('./js/ui-control'); - +const {UiTable} = require('./js/table'); const {UiButton} = require('./js/button'); const {UiWindow} = require('./js/window'); const {UiSlider} = require('./js/slider'); @@ -81,6 +81,13 @@ const {FontDescriptor} = require('./js/font-descriptor'); const {FontAttribute} = require('./js/font-attribute'); const {AttributedString} = require('./js/font-string'); const {OpenTypeFeatures} = require('./js/font-opentype'); +const {UiTableModel} = require('./js/table-model'); +const {UiImage} = require('./js/image'); +const fromMetadata = require('./js/table-model-from-metadata'); + +UiTableModel.fromMetadata = fromMetadata; +UiTableModel.ValueTypes = fromMetadata.ValueTypes; +UiTableModel.Fields = fromMetadata.Fields; function applySetterGetter(...classConstructors) { for (const classConstructor of classConstructors) { @@ -146,19 +153,22 @@ function applySetterGetterAll(doSetter, ...classConstructors) { } // Takes about 3.5ms: -applySetterGetter(UiEntryBase, UiBox, SeparatorBase, UiControl, UiGrid, UiMenuItem, - UiMenu, UiSpinbox, UiHorizontalSeparator, UiVerticalSeparator, - UiRadioButtons, UiProgressBar, UiGroup, UiEntry, UiPasswordEntry, - UiSearchEntry, UiEditableCombobox, UiTimePicker, UiDatePicker, - UiDateTimePicker, UiCombobox, UiColorButton, UiCheckbox, UiWindow, - UiButton, UiLabel, UiForm, UiSlider, UiMultilineEntry, UiHorizontalBox, - UiVerticalBox, UiTab, UiArea, DrawBrush, BrushGradientStop, UiDrawPath, - DrawStrokeParams, UiDrawMatrix, UiAreaKeyEvent, UiAreaMouseEvent, - UiFontButton, FontDescriptor, FontAttribute, DrawTextLayout); +applySetterGetter( + UiImage, UiTableModel, UiTable, UiEntryBase, UiBox, SeparatorBase, UiControl, UiGrid, + UiMenuItem, UiMenu, UiSpinbox, UiHorizontalSeparator, UiVerticalSeparator, + UiRadioButtons, UiProgressBar, UiGroup, UiEntry, UiPasswordEntry, UiSearchEntry, + UiEditableCombobox, UiTimePicker, UiDatePicker, UiDateTimePicker, UiCombobox, + UiColorButton, UiCheckbox, UiWindow, UiButton, UiLabel, UiForm, UiSlider, + UiMultilineEntry, UiHorizontalBox, UiVerticalBox, UiTab, UiArea, DrawBrush, + BrushGradientStop, UiDrawPath, DrawStrokeParams, UiDrawMatrix, UiAreaKeyEvent, + UiAreaMouseEvent, UiFontButton, FontDescriptor, FontAttribute, DrawTextLayout); applySetterGetterAll(true, Point, Color, Size); applySetterGetterAll(false, AreaDrawParams, UiAreaMouseEvent, UiAreaKeyEvent); Object.assign(libui, { + UiImage, + UiTableModel, + UiTable, UiFontButton, FontDescriptor, FontAttribute, diff --git a/js/image.js b/js/image.js new file mode 100644 index 0000000..52d8f66 --- /dev/null +++ b/js/image.js @@ -0,0 +1,40 @@ +const {Image} = require('..'); +const {readFile} = require('fs'); +const parsePng = require('parse-png'); + +/** + * An image. + */ +class UiImage { + /** + * Create a new UiImage object. + * @return {UiImage} + */ + constructor(width, height) { + this._img = Image.create(width, height); + } + + append(pixels, pixelWidth, pixelHeight, byteStride) { + Image.append(this._img, pixels, pixelWidth, pixelHeight, byteStride); + } + + static loadFromPng(pngPath) { + return new Promise((resolve, reject) => { + readFile(pngPath, (err, data) => { + if (err) { + return reject(err); + } + + parsePng(data) + .then(png => { + const img = new UiImage(png.width, png.height); + img.append(png.data, png.width, png.height, png.width * 4); + resolve(img); + }) + .catch(reject); + }); + }); + } +} + +module.exports = {UiImage}; diff --git a/js/table-model-from-metadata.js b/js/table-model-from-metadata.js new file mode 100644 index 0000000..d9d049b --- /dev/null +++ b/js/table-model-from-metadata.js @@ -0,0 +1,199 @@ +const {UiImage} = require('./image'); +const {UiTableModel} = require('./table-model'); + +const always = val => () => val; +const noop = () => undefined; + +const ValueTypes = { + String: 0, + Image: 1, + Int: 2, + Color: 3 +}; + +const Text = { + getter: ({key}) => (data, row) => data[row][key], + setter: ({key}) => (data, row, value) => (data[row][key] = value), + cellType: () => ValueTypes.String, + adder: column => tb => + tb.appendTextColumn(column.header, column.idx, column.idx + 1, null) +}; + +const Image = { + getter: ({value}) => { + if (value instanceof UiImage) { + return always(column.value); + } + + if (typeof value === 'function') { + return value; + } + + return (data, row) => data[row][key]; + }, + setter: () => noop, + cellType: () => ValueTypes.Image, + adder: column => tb => tb.appendImageColumn(column.header, column.idx) +}; + +const Checkbox = { + getter: ({key}) => (data, row) => Number(data[row][key]), + setter: ({key}) => (data, row, value) => (data[row][key] = Boolean(value)), + cellType: () => ValueTypes.Int, + adder: column => tb => + tb.appendCheckboxColumn(column.header, column.idx, column.idx + 1) +}; + +const LabeledCheckbox = { + getter: ({key}) => (data, row) => Number(data[row][key]), + setter: ({key}) => (data, row, value) => (data[row][key] = Boolean(value)), + cellType: () => ValueTypes.Int, + addFurtherColumns: ({column, columnTypes, cellGetters, cellSetters}) => { + const labelIdx = columnTypes.length; + console.log('LabeledCheckbox labelIdx', column.idx, labelIdx); + columnTypes[labelIdx] = ValueTypes.String; + cellGetters[labelIdx] = () => column.label; + cellSetters[labelIdx] = noop; + + columnTypes[labelIdx + 1] = ValueTypes.Int; + cellGetters[labelIdx + 1] = () => 0; + cellSetters[labelIdx + 1] = noop; + + column.labelIdx = labelIdx; + }, + adder: column => tb => { + const textModelColumn = column.labelIdx; + const textEditableModelColumn = column.labelIdx + 1; + + tb.appendCheckboxTextColumn(column.header, column.idx, column.idx + 1, + textModelColumn, textEditableModelColumn, null); + } +}; + +const LabeledImage = { + getter: ({value}) => { + if (value instanceof UiImage) { + return always(column.value); + } + + if (typeof value === 'function') { + return value; + } + + return (data, row) => data[row][key]; + }, + setter: ({key}) => () => 0, + cellType: () => ValueTypes.Image, + addFurtherColumns: ({column, columnTypes, cellGetters, cellSetters}) => { + const labelIdx = columnTypes.length; + console.log('LabeledImage labelIdx', column.idx, labelIdx); + columnTypes[labelIdx] = ValueTypes.String; + cellGetters[labelIdx] = () => { + console.log('column.label', column.label); + return column.label; + }; + cellSetters[labelIdx] = noop; + + columnTypes[labelIdx + 1] = ValueTypes.Int; + cellGetters[labelIdx + 1] = () => 0; + cellSetters[labelIdx + 1] = noop; + + column.labelIdx = labelIdx; + }, + adder: column => tb => { + console.log('adder', column.labelIdx) + const textModelColumn = column.labelIdx; + const textEditableModelColumn = column.labelIdx + 1; + + tb.appendImageTextColumn(column.header, column.idx, textModelColumn, + textEditableModelColumn, null); + } +}; + +const ProgressBar = { + getter: ({key}) => (data, row) => Number(data[row][key]), + setter: ({key}) => (data, row, value) => (data[row][key] = Number(value)), + cellType: () => ValueTypes.Int, + adder: column => tb => tb.appendProgressBarColumn(column.header, column.idx) +}; + +const Button = { + getter: ({label}) => () => String(label), + setter: ({key, click}) => (data, row) => click(data[row]), + cellType: () => ValueTypes.String, + adder: column => tb => + tb.appendButtonColumn(column.header, column.idx, column.idx + 1) +}; + +function fromMetadata(model) { + const numColumns = Object.keys(model).length; + const columnTypes = []; + const cellGetters = []; + const cellSetters = []; + + for (const [key, column] of Object.entries(model)) { + const idx = columnTypes.length; + column.key = key; + column.idx = idx; + + columnTypes[idx] = column.type.cellType(column); + cellGetters[idx] = column.type.getter(column); + cellSetters[idx] = column.type.setter(column); + column.adder = column.type.adder(column); + + if (typeof column.editable === 'function') { + columnTypes[idx + 1] = ValueTypes.Int; + cellGetters[idx + 1] = (data, row) => column.editable(data[row]); + } else { + columnTypes[idx + 1] = ValueTypes.Int; + cellGetters[idx + 1] = always(Number(Boolean(column.editable))); + } + cellSetters[idx + 1] = noop; + + if (typeof column.type.addFurtherColumns === 'function') { + + column.type.addFurtherColumns( + {columnTypes, cellGetters, cellSetters, column}); + } + + if (column.header === undefined) { + column.header = key; + } + } + + return { + fields: model, + addColumns(tb, ...columnNames) { + for (const columnName of columnNames) { + debugger; + this.fields[columnName].adder(tb); + } + }, + bind(data) { + return new UiTableModel({ + numColumns: () => numColumns, + columnType: column => columnTypes[column], + numRows: () => data.length, + cellValue: (row, column) => { + const value = cellGetters[column](data, row); + return value; + }, + setCellValue: (row, column, value) => + cellSetters[column](data, row, value) + }); + } + }; +} + +fromMetadata.ValueTypes = ValueTypes; +fromMetadata.Fields = { + Text, + Image, + Checkbox, + ProgressBar, + LabeledCheckbox, + LabeledImage, + Button +}; + +module.exports = fromMetadata; diff --git a/js/table-model.js b/js/table-model.js new file mode 100644 index 0000000..4a392f1 --- /dev/null +++ b/js/table-model.js @@ -0,0 +1,28 @@ +const {TableModel} = require('..'); + +class UiTableModel { + /** + * Create a new UiTableModel object. + * @param {object} modelHanlder - handler for the new table model + * @return {UiTable} + */ + constructor(modelHanlder) { + this.model = TableModel.create(modelHanlder.numColumns, modelHanlder.numRows, + modelHanlder.columnType, modelHanlder.cellValue, + modelHanlder.setCellValue); + } + + rowInserted(index) { + return TableModel.rowInserted(this.model, index); + } + + rowChanged(index) { + return TableModel.rowChanged(this.model, index); + } + + rowDeleted(index) { + return TableModel.rowDeleted(this.model, index); + } +} + +module.exports = {UiTableModel}; diff --git a/js/table.js b/js/table.js new file mode 100644 index 0000000..8390659 --- /dev/null +++ b/js/table.js @@ -0,0 +1,58 @@ +const {Table} = require('..'); +const {TableModel} = require('..'); +const {UiControl} = require('./ui-control'); + +/** + * A data table. + * @extends UiControl + */ +class UiTable extends UiControl { + /** + * Create a new UiTable object. + * @param {object} model - data model for the new table + * @return {UiTable} + */ + constructor(model) { + super(Table.create(model.model)); + this.model = model; + } + + appendTextColumn(name, textModelColumn, textEditableModelColumn, colorModelColumn) { + return Table.appendTextColumn(this.handle, name, textModelColumn, + textEditableModelColumn, colorModelColumn); + } + + appendImageColumn(name, imageModelColumn) { + return Table.appendImageColumn(this.handle, name, imageModelColumn); + } + + appendImageTextColumn(name, imageModelColumn, textModelColumn, + textEditableModelColumn, colorModelColumn) { + return Table.appendImageTextColumn(this.handle, name, imageModelColumn, + textModelColumn, textEditableModelColumn, + colorModelColumn); + } + + appendCheckboxColumn(name, checkboxModelColumn, checkboxEditableModelColumn) { + return Table.appendCheckboxColumn(this.handle, name, checkboxModelColumn, + checkboxEditableModelColumn); + } + + appendCheckboxTextColumn(name, checkboxModelColumn, checkboxEditableModelColumn, + textModelColumn, textEditableModelColumn, colorModelColumn) { + return Table.appendCheckboxTextColumn( + this.handle, name, checkboxModelColumn, checkboxEditableModelColumn, + textModelColumn, textEditableModelColumn, colorModelColumn); + } + + appendProgressBarColumn(name, progressModelColumn) { + return Table.appendProgressBarColumn(this.handle, name, progressModelColumn); + } + + appendButtonColumn(name, buttonModelColumn, buttonClickableModelColumn) { + return Table.appendButtonColumn(this.handle, name, buttonModelColumn, + buttonClickableModelColumn); + } +} + +module.exports = {UiTable}; diff --git a/package-lock.json b/package-lock.json index cb842f1..28ab021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4616,9 +4616,9 @@ } }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", "dev": true, "optional": true }, @@ -5026,6 +5026,21 @@ "integrity": "sha1-3T+iXtbC78e93hKtm0bBY6opIk4=", "dev": true }, + "parse-png": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-1.1.2.tgz", + "integrity": "sha1-9cKtfHmTSQmGAgooTBmu5FlxH/I=", + "requires": { + "pngjs": "^3.2.0" + }, + "dependencies": { + "pngjs": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.3.3.tgz", + "integrity": "sha512-1n3Z4p3IOxArEs1VRXnZ/RXdfEniAUS9jb68g58FIXMNkPJeZd+Qh4Uq7/e0LVxAQGos1eIUrqrt4FpjdnEd+Q==" + } + } + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", diff --git a/package.json b/package.json index 8720d62..9f4f22a 100755 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "mkdirp": "^0.5.1", "mv": "^2.1.1", "node-gyp": "^3.6.2", + "parse-png": "^1.1.2", "tar": "^4.4.1" }, "devDependencies": { diff --git a/src/image.c b/src/image.c new file mode 100644 index 0000000..9b6a5fa --- /dev/null +++ b/src/image.c @@ -0,0 +1,47 @@ +#include +#include "napi_utils.h" +#include "control.h" +#include "events.h" + +static const char *MODULE = "Image"; + +static void on_img_gc(napi_env env, void *finalize_data, void *finalize_hint) { + uiImage *img = (uiImage *)finalize_data; + /* TODO: uiFreeImage when? */ +} + +LIBUI_FUNCTION(create) { + INIT_ARGS(2); + + ARG_DOUBLE(width, 0); + ARG_DOUBLE(height, 1); + + uiImage *img = uiNewImage(width, height); + napi_value img_external; + + napi_status status = napi_create_external(env, img, on_img_gc, NULL, &img_external); + CHECK_STATUS_THROW(status, napi_create_external); + + return img_external; +} + +LIBUI_FUNCTION(append) { + INIT_ARGS(5); + + ARG_POINTER(uiImage, image, 0); + ARG_BUFFER(pixels, 1); + ARG_INT32(pixelWidth, 2); + ARG_INT32(pixelHeight, 3); + ARG_INT32(byteStride, 4); + printf("%d x %d : %d (len = %d)\n", pixelWidth, pixelHeight, byteStride, pixels__len); + uiImageAppend(image, pixels__ptr, pixelWidth, pixelHeight, byteStride); + return NULL; +} + +napi_value _libui_init_image(napi_env env, napi_value exports) { + DEFINE_MODULE(); + LIBUI_EXPORT(create); + LIBUI_EXPORT(append); + + return module; +} diff --git a/src/includes/modules.h b/src/includes/modules.h index 94e3f31..df88362 100644 --- a/src/includes/modules.h +++ b/src/includes/modules.h @@ -39,3 +39,6 @@ napi_value _libui_init_ui_control(napi_env env, napi_value exports); napi_value _libui_init_area_stroke(napi_env env, napi_value exports); napi_value _libui_init_area_matrix(napi_env env, napi_value exports); napi_value _libui_init_dialogs(napi_env env, napi_value exports); +napi_value _libui_init_table_model(napi_env env, napi_value exports); +napi_value _libui_init_table(napi_env env, napi_value exports); +napi_value _libui_init_image(napi_env env, napi_value exports); diff --git a/src/includes/napi_utils.h b/src/includes/napi_utils.h index 42712e7..17741ed 100644 --- a/src/includes/napi_utils.h +++ b/src/includes/napi_utils.h @@ -59,6 +59,22 @@ } \ } +#define ARG_BUFFER(ARG_NAME, ARG_IDX) \ + void *ARG_NAME##__ptr; \ + size_t ARG_NAME##__len; \ + { \ + napi_status status = \ + napi_get_buffer_info(env, argv[ARG_IDX], &ARG_NAME##__ptr, &ARG_NAME##__len); \ + if (status != napi_ok) { \ + const napi_extended_error_info *result; \ + napi_get_last_error_info(env, &result); \ + char err[1024]; \ + snprintf(err, 1024, "Argument " #ARG_NAME ": %s", result->error_message); \ + napi_throw_type_error(env, NULL, err); \ + return NULL; \ + } \ + } + #define ARG_DOUBLE(ARG_NAME, ARG_IDX) \ double ARG_NAME; \ { \ @@ -139,8 +155,31 @@ extern napi_ref null_ref; if (arg_type == napi_null) { \ ARG_NAME = null_ref; \ } else { \ + napi_value global; \ + status = napi_get_global(env, &global); \ + CHECK_STATUS_THROW(status, napi_get_global); \ + \ + napi_value function_constructor; \ + status = napi_get_named_property(env, global, "Function", &function_constructor); \ + CHECK_STATUS_THROW(status, napi_get_named_property); \ + \ + bool is_function = false; \ + status = napi_instanceof(env, argv[ARG_IDX], function_constructor, &is_function); \ + if (status != napi_ok || !is_function) { \ + napi_throw_type_error(env, NULL, \ + "Argument " #ARG_NAME ": a function was expected"); \ + return NULL; \ + } \ + \ status = napi_create_reference(env, argv[ARG_IDX], 1, &ARG_NAME); \ - CHECK_STATUS_THROW(status, napi_create_reference); \ + if (status != napi_ok) { \ + const napi_extended_error_info *result; \ + napi_get_last_error_info(env, &result); \ + char err[1024]; \ + snprintf(err, 1024, "Argument " #ARG_NAME ": %s", result->error_message); \ + napi_throw_type_error(env, NULL, err); \ + return NULL; \ + } \ } \ } diff --git a/src/module.c b/src/module.c index 1af3b9d..c8a7161 100644 --- a/src/module.c +++ b/src/module.c @@ -39,6 +39,9 @@ static napi_value init_all(napi_env env, napi_value exports) { _libui_init_area_matrix(env, exports); _libui_init_grid(env, exports); _libui_init_dialogs(env, exports); + _libui_init_table_model(env, exports); + _libui_init_table(env, exports); + _libui_init_image(env, exports); _libui_init_tests(env, exports); diff --git a/src/table-model.c b/src/table-model.c new file mode 100644 index 0000000..1648030 --- /dev/null +++ b/src/table-model.c @@ -0,0 +1,348 @@ +#include +#include "napi_utils.h" +#include "control.h" +#include "events.h" + +static const char *MODULE = "TableModel"; + +struct binding_handler { + uiTableModelHandler handler; + napi_ref model_ref; + napi_env env; + napi_async_context context; + napi_ref jsNumColumns; + napi_ref jsColumnType; + napi_ref jsNumRows; + napi_ref jsCellValue; + napi_ref jsSetCellValue; +}; + +static napi_value run_handler_fn(struct binding_handler *bh, napi_ref fn_ref, int argc, + napi_value *argv) { + napi_value fn; + napi_status status = napi_get_reference_value(bh->env, fn_ref, &fn); + + napi_env env = bh->env; + + napi_value resource_object; + status = napi_create_object(env, &resource_object); + CHECK_STATUS_UNCAUGHT(status, napi_create_object, NULL); + + LIBUI_NODE_DEBUG("Calling model handler method"); + + napi_value result; + status = napi_make_callback(env, bh->context, resource_object, fn, argc, argv, &result); + + if (status == napi_pending_exception) { + napi_value last_exception; + napi_get_and_clear_last_exception(env, &last_exception); + napi_fatal_exception(env, last_exception); + return NULL; + } + + CHECK_STATUS_UNCAUGHT(status, napi_make_callback, NULL); + + LIBUI_NODE_DEBUG("Method called"); + + return result; +} + +static int c_numColumns(uiTableModelHandler *mh, uiTableModel *m) { + struct binding_handler *bh = (struct binding_handler *)mh; + + napi_env env = bh->env; + + napi_handle_scope handle_scope; + napi_status status = napi_open_handle_scope(env, &handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_open_handle_scope, 0); + + napi_value result = run_handler_fn(bh, bh->jsNumColumns, 0, NULL); + + if (result == NULL) { + + return 0; + } + + int32_t int_result; + status = napi_get_value_int32(env, result, &int_result); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_int32, 0); + + status = napi_close_handle_scope(env, handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_close_handle_scope, 0); + + return int_result; +} + +static uiTableValueType c_columnType(uiTableModelHandler *mh, uiTableModel *m, int column) { + struct binding_handler *bh = (struct binding_handler *)mh; + napi_env env = bh->env; + + napi_handle_scope handle_scope; + napi_status status = napi_open_handle_scope(env, &handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_open_handle_scope, 0); + + napi_value column_val; + status = napi_create_int32(bh->env, column, &column_val); + CHECK_STATUS_UNCAUGHT(status, napi_create_int32, 0); + + napi_value args[1] = {column_val}; + napi_value result = run_handler_fn(bh, bh->jsColumnType, 1, args); + + if (result == NULL) { + return 0; + } + + int32_t int_result; + status = napi_get_value_int32(env, result, &int_result); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_int32, 0); + + status = napi_close_handle_scope(env, handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_close_handle_scope, 0); + + return (uiTableValueType)int_result; +} + +static int c_numRows(uiTableModelHandler *mh, uiTableModel *m) { + struct binding_handler *bh = (struct binding_handler *)mh; + napi_env env = bh->env; + + napi_handle_scope handle_scope; + napi_status status = napi_open_handle_scope(env, &handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_open_handle_scope, 0); + + napi_value result = run_handler_fn(bh, bh->jsNumRows, 0, NULL); + + if (result == NULL) { + return 0; + } + + int32_t int_result; + status = napi_get_value_int32(env, result, &int_result); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_int32, 0); + + status = napi_close_handle_scope(env, handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_close_handle_scope, 0); + + return int_result; +} + +static uiTableValue *c_cellValue(uiTableModelHandler *mh, uiTableModel *m, int row, int column) { + struct binding_handler *bh = (struct binding_handler *)mh; + uiTableValueType type = c_columnType(mh, m, column); + + napi_env env = bh->env; + napi_handle_scope handle_scope; + napi_status status = napi_open_handle_scope(env, &handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_open_handle_scope, NULL); + + napi_value column_val; + status = napi_create_int32(bh->env, column, &column_val); + CHECK_STATUS_UNCAUGHT(status, napi_create_int32, 0); + + napi_value row_val; + status = napi_create_int32(bh->env, row, &row_val); + CHECK_STATUS_UNCAUGHT(status, napi_create_int32, 0); + + napi_value args[2] = {row_val, column_val}; + + napi_value result = run_handler_fn(bh, bh->jsCellValue, 2, args); + + if (result == NULL) { + return 0; + } + uiTableValue *ret; + + switch (type) { + case uiTableValueTypeString: { + size_t string_len; + napi_status status = napi_get_value_string_utf8(env, result, NULL, 0, &string_len); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_string_utf8, NULL); + char *string = malloc(string_len + 1); + status = napi_get_value_string_utf8(env, result, string, string_len + 1, &string_len); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_string_utf8, NULL); + ret = uiNewTableValueString(string); + free(string); + break; + } + case uiTableValueTypeImage: { + napi_value image_ext; + napi_status status = napi_get_named_property(env, result, "_img", &image_ext); + CHECK_STATUS_UNCAUGHT(status, napi_get_named_property, NULL); + + uiImage *img; + status = napi_get_value_external(env, image_ext, (void **)&img); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_external, NULL); + ret = uiNewTableValueImage(img); + break; + } + case uiTableValueTypeInt: { + int32_t int_result; + napi_status status = napi_get_value_int32(env, result, &int_result); + CHECK_STATUS_UNCAUGHT(status, napi_get_value_int32, NULL); + ret = uiNewTableValueInt(int_result); + break; + } + case uiTableValueTypeColor: { + ret = NULL; + break; + } + default: { ret = NULL; } + } + + status = napi_close_handle_scope(env, handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_close_handle_scope, NULL); + return ret; +} + +#define HACK_uiTableValueTypeNull 424242 + +static void c_setCellValue(uiTableModelHandler *mh, uiTableModel *m, int row, int column, + const uiTableValue *value) { + struct binding_handler *bh = (struct binding_handler *)mh; + uiTableValueType type; + if (value == NULL) { + type = HACK_uiTableValueTypeNull; + } else { + type = uiTableValueGetType(value); + } + + napi_env env = bh->env; + napi_handle_scope handle_scope; + napi_status status = napi_open_handle_scope(env, &handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_open_handle_scope, ); + + napi_value column_val; + status = napi_create_int32(bh->env, column, &column_val); + CHECK_STATUS_UNCAUGHT(status, napi_create_int32, ); + + napi_value row_val; + status = napi_create_int32(bh->env, row, &row_val); + CHECK_STATUS_UNCAUGHT(status, napi_create_int32, ); + + napi_value ret; + + switch (type) { + case HACK_uiTableValueTypeNull: { + napi_status status = napi_get_null(env, &ret); + CHECK_STATUS_UNCAUGHT(status, napi_get_null, ); + break; + } + case uiTableValueTypeString: { + + const char *cell_value = uiTableValueString(value); + ret = make_utf8_string(env, cell_value); + // free(cell_value); + break; + } + case uiTableValueTypeImage: { + ret = make_uint32(env, 0); + break; + } + case uiTableValueTypeInt: { + int32_t int_result = uiTableValueInt(value); + ret = make_int32(env, int_result); + break; + } + case uiTableValueTypeColor: { + ret = make_uint32(env, 0); + break; + } + default: { ret = make_uint32(env, 0); } + } + + napi_value args[3] = {row_val, column_val, ret}; + + run_handler_fn(bh, bh->jsSetCellValue, 3, args); + status = napi_close_handle_scope(env, handle_scope); + CHECK_STATUS_UNCAUGHT(status, napi_close_handle_scope, ); +} + +static void on_model_gc(napi_env env, void *finalize_data, void *finalize_hint) { + uiTableModel *model = (uiTableModel *)finalize_data; + struct binding_handler *handler = (struct binding_handler *)finalize_hint; + /* TODO: decrease to 0 all fns references */ + /* TODO: clean up async context */ + uiFreeTableModel(model); + free(handler); +} + +LIBUI_FUNCTION(create) { + INIT_ARGS(5); + + ARG_CB_REF(numColumns, 0); + ARG_CB_REF(numRows, 1); + ARG_CB_REF(columnType, 2); + ARG_CB_REF(cellValue, 3); + ARG_CB_REF(setCellValue, 4); + + napi_value async_resource_name; + napi_status status = + napi_create_string_utf8(env, "TableModel", NAPI_AUTO_LENGTH, &async_resource_name); + CHECK_STATUS_THROW(status, napi_create_string_utf8); + + napi_async_context context; + status = napi_async_init(env, NULL, async_resource_name, &context); + CHECK_STATUS_THROW(status, napi_async_init); + + struct binding_handler *model_handler = + (struct binding_handler *)malloc(sizeof(struct binding_handler)); + + model_handler->jsNumColumns = numColumns; + model_handler->jsNumRows = numRows; + model_handler->jsColumnType = columnType; + model_handler->jsCellValue = cellValue; + model_handler->jsSetCellValue = setCellValue; + model_handler->handler.NumColumns = c_numColumns; + model_handler->handler.NumRows = c_numRows; + model_handler->handler.ColumnType = c_columnType; + model_handler->handler.CellValue = c_cellValue; + model_handler->handler.SetCellValue = c_setCellValue; + model_handler->env = env; + model_handler->context = context; + + uiTableModel *model = uiNewTableModel((uiTableModelHandler *)model_handler); + + napi_value model_external; + status = napi_create_external(env, model, on_model_gc, model_handler, &model_external); + CHECK_STATUS_THROW(status, napi_create_external); + + status = napi_create_reference(env, model_external, 1, &model_handler->model_ref); + CHECK_STATUS_THROW(status, napi_create_reference); + + return model_external; +} + +LIBUI_FUNCTION(rowInserted) { + INIT_ARGS(2); + ARG_POINTER(uiTableModel, handle, 0); + ARG_INT32(index, 1); + uiTableModelRowInserted(handle, index); + return NULL; +} + +LIBUI_FUNCTION(rowChanged) { + INIT_ARGS(2); + ARG_POINTER(uiTableModel, handle, 0); + ARG_INT32(index, 1); + uiTableModelRowChanged(handle, index); + return NULL; +} + +LIBUI_FUNCTION(rowDeleted) { + INIT_ARGS(2); + ARG_POINTER(uiTableModel, handle, 0); + ARG_INT32(index, 1); + uiTableModelRowDeleted(handle, index); + return NULL; +} + +napi_value _libui_init_table_model(napi_env env, napi_value exports) { + DEFINE_MODULE(); + LIBUI_EXPORT(create); + + LIBUI_EXPORT(rowInserted); + LIBUI_EXPORT(rowChanged); + LIBUI_EXPORT(rowDeleted); + + return module; +} diff --git a/src/table.c b/src/table.c new file mode 100644 index 0000000..850d85f --- /dev/null +++ b/src/table.c @@ -0,0 +1,172 @@ +#include +#include "napi_utils.h" +#include "control.h" +#include "events.h" + +static const char *MODULE = "Table"; + +LIBUI_FUNCTION(create) { + INIT_ARGS(1); + ARG_POINTER(uiTableModel, model, 0); + + uiTableParams params = {model, -1}; + uiControl *ctrl = uiControl(uiNewTable(¶ms)); + + return control_handle_new(env, ctrl, "table"); +} + +LIBUI_FUNCTION(appendTextColumn) { + INIT_ARGS(5); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(textModelColumn, 2); + ARG_INT32(textEditableModelColumn, 3); + ENSURE_NOT_DESTROYED(); + + uiTableTextColumnOptionalParams textParams; + uiTableTextColumnOptionalParams *textParamsP = NULL; + + napi_valuetype arg_type; + napi_status status = napi_typeof(env, argv[4], &arg_type); + CHECK_STATUS_THROW(status, napi_typeof); + + if (arg_type == napi_number) { + ARG_INT32(colorModelColumn, 4); + textParams.ColorModelColumn = colorModelColumn; + textParamsP = &textParams; + } + + uiTableAppendTextColumn(uiTable(handle->control), name, textModelColumn, + textEditableModelColumn, textParamsP); + free(name); + return NULL; +} + +LIBUI_FUNCTION(appendImageColumn) { + INIT_ARGS(3); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(imageModelColumn, 2); + ENSURE_NOT_DESTROYED(); + + uiTableAppendImageColumn(uiTable(handle->control), name, imageModelColumn); + free(name); + return NULL; +} + +LIBUI_FUNCTION(appendImageTextColumn) { + INIT_ARGS(6); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(imageModelColumn, 2); + ARG_INT32(textModelColumn, 3); + ARG_INT32(textEditableModelColumn, 4); + + ENSURE_NOT_DESTROYED(); + + uiTableTextColumnOptionalParams textParams; + uiTableTextColumnOptionalParams *textParamsP = NULL; + + napi_valuetype arg_type; + napi_status status = napi_typeof(env, argv[5], &arg_type); + CHECK_STATUS_THROW(status, napi_typeof); + + if (arg_type == napi_number) { + ARG_INT32(colorModelColumn, 5); + textParams.ColorModelColumn = colorModelColumn; + textParamsP = &textParams; + } + uiTableAppendImageTextColumn(uiTable(handle->control), name, imageModelColumn, textModelColumn, + textEditableModelColumn, textParamsP); + free(name); + return NULL; +} + +LIBUI_FUNCTION(appendCheckboxColumn) { + INIT_ARGS(4); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(checkboxModelColumn, 2); + ARG_INT32(checkboxEditableModelColumn, 3); + + ENSURE_NOT_DESTROYED(); + + uiTableAppendCheckboxColumn(uiTable(handle->control), name, checkboxModelColumn, + checkboxEditableModelColumn); + free(name); + return NULL; +} + +LIBUI_FUNCTION(appendCheckboxTextColumn) { + INIT_ARGS(7); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(checkboxModelColumn, 2); + ARG_INT32(checkboxEditableModelColumn, 3); + ARG_INT32(textModelColumn, 4); + ARG_INT32(textEditableModelColumn, 5); + + ENSURE_NOT_DESTROYED(); + + uiTableTextColumnOptionalParams textParams; + uiTableTextColumnOptionalParams *textParamsP = NULL; + + napi_valuetype arg_type; + napi_status status = napi_typeof(env, argv[6], &arg_type); + CHECK_STATUS_THROW(status, napi_typeof); + + if (arg_type == napi_number) { + ARG_INT32(colorModelColumn, 6); + textParams.ColorModelColumn = colorModelColumn; + textParamsP = &textParams; + } + + uiTableAppendCheckboxTextColumn(uiTable(handle->control), name, checkboxModelColumn, + checkboxEditableModelColumn, textModelColumn, + textEditableModelColumn, textParamsP); + free(name); + return NULL; +} + +LIBUI_FUNCTION(appendProgressBarColumn) { + INIT_ARGS(3); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(progressModelColumn, 2); + + ENSURE_NOT_DESTROYED(); + + uiTableAppendProgressBarColumn(uiTable(handle->control), name, progressModelColumn); + free(name); + return NULL; +} + +LIBUI_FUNCTION(appendButtonColumn) { + INIT_ARGS(4); + ARG_POINTER(struct control_handle, handle, 0); + ARG_STRING(name, 1); + ARG_INT32(buttonModelColumn, 2); + ARG_INT32(buttonClickableModelColumn, 3); + + ENSURE_NOT_DESTROYED(); + + uiTableAppendButtonColumn(uiTable(handle->control), name, buttonModelColumn, + buttonClickableModelColumn); + free(name); + return NULL; +} + +napi_value _libui_init_table(napi_env env, napi_value exports) { + DEFINE_MODULE(); + LIBUI_EXPORT(create); + + LIBUI_EXPORT(appendTextColumn); + LIBUI_EXPORT(appendImageColumn); + LIBUI_EXPORT(appendImageTextColumn); + LIBUI_EXPORT(appendCheckboxColumn); + LIBUI_EXPORT(appendCheckboxTextColumn); + LIBUI_EXPORT(appendProgressBarColumn); + LIBUI_EXPORT(appendButtonColumn); + + return module; +} diff --git a/tests/table-model.js b/tests/table-model.js new file mode 100644 index 0000000..624a418 --- /dev/null +++ b/tests/table-model.js @@ -0,0 +1,24 @@ +const test = require('tape'); +const {TableModel} = require('..'); + +test('TableModel has a create fn', t => { + t.equal(typeof TableModel.create, 'function'); + t.end(); +}); + +test('TableModel create fn require 5 fn args', t => { + const fn = test; + t.throws(() => TableModel.create(42), /Too few arguments/); + t.throws(() => TableModel.create({}, {}, {}, {}, {}), + /Argument numColumns: a function was expected/); + t.throws(() => TableModel.create(fn, {}, {}, {}, {}), + /Argument numRows: a function was expected/); + t.throws(() => TableModel.create(fn, fn, {}, {}, {}), + /Argument columnType: a function was expected/); + t.throws(() => TableModel.create(fn, fn, fn, {}, {}), + /Argument cellValue: a function was expected/); + t.throws(() => TableModel.create(fn, fn, fn, fn, {}), + /Argument setCellValue: a function was expected/); + TableModel.create(fn, fn, fn, fn, fn); + t.end(); +});