diff options
author | MohammadHossein Qanbari <[email protected]> | 2024-05-27 18:06:47 +0200 |
---|---|---|
committer | MohammadHossein Qanbari <[email protected]> | 2024-06-03 17:27:22 +0200 |
commit | 3de85ce8b99e032d5d4920e35dc1bad4e479fa79 (patch) | |
tree | cf574292b38b4c6a0b8ac2914807b5fd8fe0e6dd /examples/quickcontrols/spreadsheets | |
parent | a9d12df9b8e8a29595783557bbbde08025e04244 (diff) |
Add the Spreadsheet example
The example demonstrates a Spreadsheet that provides adding, editing,
and deleting data, and also the ability to write formulas for numeric
data. Also, it's possible to select cells, rows, and columns for
deleting them or their data, copying or cutting the data, and dragging
them to other places. The user can hide columns or rows, and also show
them again. Thanks to the reordering API, columns and rows can be
reordered and also can be reset to the default order.
There is a SpreadModel class which handles the entered data. It only
stores the data of the cells that is provided by the user. It means
that it does not create any empty data structure for empty cells, in
order to reduce memory usage.
Task-number: QTBUG-125767
Pick-to: 6.8
Change-Id: I1d9cc5b4b8d902257e9ed508d4a712b0574490f3
Reviewed-by: Richard Moe Gustavsen <[email protected]>
Diffstat (limited to 'examples/quickcontrols/spreadsheets')
34 files changed, 2865 insertions, 0 deletions
diff --git a/examples/quickcontrols/spreadsheets/CMakeLists.txt b/examples/quickcontrols/spreadsheets/CMakeLists.txt new file mode 100644 index 0000000000..de15fee0d1 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(SpreadsheetsExample VERSION 1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Qml) + +qt_standard_project_setup(REQUIRES 6.8) + +add_subdirectory(Spreadsheets) + +qt_add_executable(${PROJECT_NAME} WIN32 + main.cpp +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt6::Gui + Qt6::Qml + Spreadsheets +) + +qt_add_resources(${PROJECT_NAME} "spareadsheet_icon" + PREFIX "/qt/examples/spreadsheet/icons" + FILES spreadsheet.svg +) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. +# If you are developing for iOS or macOS you should consider setting an +# explicit, fixed bundle identifier manually though. +set_target_properties(${PROJECT_NAME} PROPERTIES +# MACOSX_BUNDLE_GUI_IDENTIFIER "io.qt.examples.Spreadsheets" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +# include(GNUInstallDirs) +install(TARGETS ${PROJECT_NAME} + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/CMakeLists.txt b/examples/quickcontrols/spreadsheets/Spreadsheets/CMakeLists.txt new file mode 100644 index 0000000000..9c2f66c0a0 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/CMakeLists.txt @@ -0,0 +1,47 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(Spreadsheets LANGUAGES CXX) + +find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick Qml) +qt_standard_project_setup(REQUIRES 6.8) + +qt_add_qml_module(${PROJECT_NAME} + URI Spreadsheets + VERSION 1.0 + QML_FILES + Main.qml + TableCell.qml + HeaderToolBar.qml + HelpDialog.qml + SOURCES + datamodel.h datamodel.cpp + spreadcell.h spreadcell.cpp + spreadformula.h spreadformula.cpp + spreadkey.h + spreadmimedataprovider.h spreadmimedataprovider.cpp + spreadmodel.h spreadmodel.cpp + spreadrole.h + RESOURCES + icons/insert_column_left.svg + icons/insert_column_right.svg + icons/insert_row_above.svg + icons/insert_row_below.svg + icons/remove_column.svg + icons/remove_row.svg + icons/pan.svg + icons/paste.svg + icons/copy.svg + icons/cut.svg + icons/help.svg + icons/hide.svg + icons/show.svg + icons/reset_reordering.svg +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt6::Core + Qt6::Quick + Qt6::Qml +) diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/HeaderToolBar.qml b/examples/quickcontrols/spreadsheets/Spreadsheets/HeaderToolBar.qml new file mode 100644 index 0000000000..b0055f12ea --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/HeaderToolBar.qml @@ -0,0 +1,76 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: root + implicitHeight: 40 + implicitWidth: 40 + + property alias panEnabled: panButton.checked + readonly property int __icon_size: 20 + + signal helpRequested() + signal cutRequested() + signal copyRequested() + signal pasteRequested() + + ToolBar { + anchors.fill: parent + + RowLayout { + anchors.fill: parent + + ToolButton { + id: helpButton + icon.source: "icons/help.svg" + icon.width: root.__icon_size + icon.height: root.__icon_size + icon.color: palette.text + onClicked: helpRequested() + } + + ToolButton { + id: panButton + icon.source: "icons/pan.svg" + icon.color: palette.text + icon.width: root.__icon_size + icon.height: root.__icon_size + flat: true + checkable: true + } + + ToolButton { + id: cutButton + icon.source: "icons/cut.svg" + icon.color: palette.text + icon.width: root.__icon_size + icon.height: root.__icon_size + onClicked: cutRequested() + } + + ToolButton { + id: copyButton + icon.source: "icons/copy.svg" + icon.color: palette.text + icon.width: root.__icon_size + icon.height: root.__icon_size + onClicked: copyRequested() + } + + ToolButton { + id: pasteButton + icon.source: "icons/paste.svg" + icon.color: palette.text + icon.width: root.__icon_size + icon.height: root.__icon_size + onClicked: pasteRequested() + } + + Item { Layout.fillWidth: true } + } + } +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/HelpDialog.qml b/examples/quickcontrols/spreadsheets/Spreadsheets/HelpDialog.qml new file mode 100644 index 0000000000..7e592b3d91 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/HelpDialog.qml @@ -0,0 +1,125 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Dialog { + id: root + modal: true + standardButtons: Dialog.Ok + + contentItem: GridLayout { + columns: 3 + columnSpacing: 10 + + Label { + Layout.columnSpan: 3 + text: qsTr("A formula starts with `=` follows with the operator and arguments.\n" + + "Formula could be") + } + + Label { + Layout.leftMargin: 20 + text: qsTr("Cell assignment") + } + Rectangle { + implicitWidth: 90 + implicitHeight: 30 + color: palette.base + border.width: 1 + border.color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("=A1") + } + } + Item { Layout.fillWidth: true } + + Label { + Layout.leftMargin: 20 + text: qsTr("Addition") + } + Rectangle { + implicitWidth: 90 + implicitHeight: 30 + color: palette.base + border.width: 1 + border.color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("=A1+A2") + } + } + Item { Layout.fillWidth: true } + + Label { + Layout.leftMargin: 20 + text: qsTr("Subtraction") + } + Rectangle { + implicitWidth: 90 + implicitHeight: 30 + color: palette.base + border.width: 1 + border.color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("=A1-A2") + } + } + Item { Layout.fillWidth: true } + + Label { + Layout.leftMargin: 20 + text: qsTr("Division") + } + Rectangle { + implicitWidth: 90 + implicitHeight: 30 + color: palette.base + border.width: 1 + border.color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("=A1/A2") + } + } + Item { Layout.fillWidth: true } + + Label { + Layout.leftMargin: 20 + text: qsTr("Multiplication") + } + Rectangle { + implicitWidth: 90 + implicitHeight: 30 + color: palette.base + border.width: 1 + border.color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("=A1*A2") + } + } + Item { Layout.fillWidth: true } + + Label { + Layout.leftMargin: 20 + text: qsTr("Summation") + } + Rectangle { + implicitWidth: 90 + implicitHeight: 30 + color: palette.base + border.width: 1 + border.color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("=SUM A1:A2") + } + } + Item { Layout.fillWidth: true } + } +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/Main.qml b/examples/quickcontrols/spreadsheets/Spreadsheets/Main.qml new file mode 100644 index 0000000000..7e09b17e80 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/Main.qml @@ -0,0 +1,830 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.qmlmodels + +import Spreadsheets + +ApplicationWindow { + width: 960 + height: 720 + visible: true + title: qsTr("Spreadsheets") + + header: HeaderToolBar { + id: toolbar + panEnabled: false + onHelpRequested: helpDialog.open() + onPasteRequested: tableView.pasteFromClipboard() + onCopyRequested: tableView.copyToClipboard() + onCutRequested: tableView.cutToClipboard() + } + + background: Rectangle { + // to make contrast with the cells of the TableView, + // HorizontalHeaderView and VerticalHeaderView + color: Qt.styleHints.colorScheme === Qt.Light ? palette.dark : palette.light + } + + GridLayout { + id: gridlayout + anchors.fill: parent + anchors.margins: 4 + columns: 2 + rows: 2 + columnSpacing: 3 + rowSpacing: 3 + + HorizontalHeaderView { + id: horizontalHeaderView + Layout.row: 0 + Layout.column: 1 + Layout.fillWidth: true + implicitHeight: 36 + clip: true + interactive: toolbar.panEnabled + syncView: tableView + + selectionModel: HeaderSelectionModel { + id: horizontalHeaderSelectionModel + selectionModel: selectionModel + orientation: Qt.Horizontal + } + + movableColumns: true + onColumnMoved: (index, old_column, new_column) => model.mapColumn(index, new_column) + + delegate: Rectangle { + id: horizontalHeaderDelegate + + required property var index + required property bool selected + required property bool current + required property bool containsDrag + readonly property real cellPadding: 8 + + implicitWidth: horizontalTitle.implicitWidth + (cellPadding * 2) + implicitHeight: Math.max(horizontalHeaderView.height, + horizontalTitle.implicitHeight + (cellPadding * 2)) + border { + width: containsDrag || current ? 1 : 0 + color: palette.highlight + } + color: selected ? palette.highlight : palette.button + + gradient: Gradient { + GradientStop { + position: 0 + color: Qt.styleHints.colorScheme === Qt.Light ? horizontalHeaderDelegate.color + : Qt.lighter(horizontalHeaderDelegate.color, 1.3) + } + GradientStop { + position: 1 + color: Qt.styleHints.colorScheme === Qt.Light ? Qt.darker(horizontalHeaderDelegate.color, 1.3) + : horizontalHeaderDelegate.color + } + } + + Label { + id: horizontalTitle + anchors.centerIn: parent + text: model.columnName + } + + MouseArea { + anchors.fill: parent + anchors.margins: horizontalHeaderDelegate.cellPadding / 2 + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function(event) { + if (event.modifiers === Qt.AltModifier) { + event.accepted = false + return + } + } + + onClicked: function(event) { + switch (event.button) { + case Qt.LeftButton: + if (event.modifiers & Qt.ControlModifier) + selectionModel.toggleColumn(index) + else + selectionModel.selectColumn(index) + break + case Qt.RightButton: + columnMenu.column = index + const menu_pos = mapToItem(horizontalHeaderView, -anchors.margins, height + anchors.margins) + columnMenu.popup(menu_pos) + break + } + } + } + } + Menu { + id: columnMenu + + property int column: -1 + + onOpened: { + horizontalHeaderSelectionModel.setCurrent(column) + } + + onClosed: { + horizontalHeaderSelectionModel.setCurrent() + column = -1 + } + + MenuItem { + text: qsTr("Insert 1 column left") + icon { + source: "icons/insert_column_left.svg" + color: palette.highlightedText + } + + onClicked: { + if (columnMenu.column < 0) + return + SpreadModel.insertColumn(columnMenu.column) + } + } + + MenuItem { + text: qsTr("Insert 1 column right") + icon { + source: "icons/insert_column_right.svg" + color: palette.highlightedText + } + + onClicked: { + if (columnMenu.column < 0) + return + SpreadModel.insertColumn(columnMenu.column + 1) + } + } + + MenuItem { + text: selectionModel.hasSelection ? qsTr("Remove selected columns") + : qsTr("Remove column") + icon { + source: "icons/remove_column.svg" + color: palette.text + } + + onClicked: { + if (selectionModel.hasSelection) + SpreadModel.removeColumns(selectionModel.selectedColumns()) + else if (columnMenu.column >= 0) + SpreadModel.removeColumn(columnMenu.column) + } + } + + MenuItem { + text: selectionModel.hasSelection ? qsTr("Hide selected columns") + : qsTr("Hide column") + icon { + source: "icons/hide.svg" + color: palette.text + } + + onClicked: { + if (selectionModel.hasSelection) { + let columns = selectionModel.selectedColumns() + columns.sort(function(lhs, rhs){ return rhs.column - lhs.column }) + for (let i in columns) + tableView.hideColumn(columns[i].column) + selectionModel.clearSelection() + } else { + tableView.hideColumn(columnMenu.column) + } + } + } + + MenuItem { + text: qsTr("Show hidden column(s)") + icon { + source: "icons/show.svg" + color: palette.text + } + + enabled: tableView.hiddenColumnCount + + onClicked: { + tableView.showHiddenColumns() + selectionModel.clearSelection() + } + } + + MenuItem { + text: qsTr("Reset column reordering") + icon { + source: "icons/reset_reordering.svg" + color: palette.text + } + + onClicked: tableView.resetColumnReordering() + } + } + } + + VerticalHeaderView { + id: verticalHeaderView + + Layout.fillHeight: true + implicitWidth: 50 + clip: true + syncView: tableView + interactive: toolbar.panEnabled + movableRows: true + + selectionModel: HeaderSelectionModel { + id: verticalHeaderSelectionModel + selectionModel: selectionModel + orientation: Qt.Vertical + } + + onRowMoved: (index, old_row, new_row) => model.mapRow(index, new_row) + + delegate: Rectangle { + id: verticalHeaderDelegate + + required property var index + required property bool selected + required property bool current + required property bool containsDrag + readonly property real cellPadding: 8 + + implicitHeight: verticalTitle.implicitHeight + (cellPadding * 2) + implicitWidth: Math.max(verticalHeaderView.width, + verticalTitle.implicitWidth + (cellPadding * 2)) + + border { + width: containsDrag || current ? 1 : 0 + color: palette.highlight + } + + color: selected ? palette.highlight : palette.button + + gradient: Gradient { + GradientStop { + position: 0 + color: Qt.styleHints.colorScheme === Qt.Light ? verticalHeaderDelegate.color + : Qt.lighter(verticalHeaderDelegate.color, 1.3) + } + GradientStop { + position: 1 + color: Qt.styleHints.colorScheme === Qt.Light ? Qt.darker(verticalHeaderDelegate.color, 1.3) + : verticalHeaderDelegate.color + } + } + + Label { + id: verticalTitle + anchors.centerIn: parent + text: model.rowName + } + + MouseArea { + anchors.fill: parent + anchors.margins: verticalHeaderDelegate.cellPadding / 2 + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function(event) { + if (event.modifiers === Qt.AltModifier) { + event.accepted = false + return + } + } + + onClicked: function(event) { + switch (event.button) { + case Qt.LeftButton: + if (event.modifiers & Qt.ControlModifier) + selectionModel.toggleRow(index) + else + selectionModel.selectRow(index) + break + case Qt.RightButton: + rowMenu.row = index + const menu_pos = mapToItem(verticalHeaderView, width + anchors.margins, -anchors.margins) + rowMenu.popup(menu_pos) + break + } + } + } + } + Menu { + id: rowMenu + + property int row: -1 + + onOpened: { + verticalHeaderSelectionModel.setCurrent(row) + } + + onClosed: { + verticalHeaderSelectionModel.setCurrent() + row = -1 + } + + MenuItem { + text: qsTr("Insert 1 row above") + icon { + source: "icons/insert_row_above.svg" + color: palette.highlightedText + } + + onClicked: { + if (rowMenu.row < 0) + return + SpreadModel.insertRow(rowMenu.row) + } + } + + MenuItem { + text: qsTr("Insert 1 row bellow") + icon { + source: "icons/insert_row_below.svg" + color: palette.text + } + + onClicked: { + if (rowMenu.row < 0) + return + SpreadModel.insertRow(rowMenu.row + 1) + } + } + + MenuItem { + text: selectionModel.hasSelection ? qsTr("Remove selected rows") + : qsTr("Remove row") + icon { + source: "icons/remove_row.svg" + color: palette.text + } + + onClicked: { + if (selectionModel.hasSelection) + SpreadModel.removeRows(selectionModel.selectedRows()) + else if (rowMenu.row >= 0) + SpreadModel.removeRow(rowMenu.row) + } + } + + MenuItem { + text: selectionModel.hasSelection ? qsTr("Hide selected rows") + : qsTr("Hide row") + icon { + source: "icons/hide.svg" + color: palette.text + } + + onClicked: { + if (selectionModel.hasSelection) { + let rows = selectionModel.selectedRows() + rows.sort(function(lhs, rhs){ return rhs.row - lhs.row }) + for (let i in rows) + tableView.hideRow(rows[i].row) + selectionModel.clearSelection() + } else { + tableView.hideRow(rowMenu.row) + } + } + } + + MenuItem { + text: qsTr("Show hidden row(s)") + icon { + source: "icons/show.svg" + color: palette.text + } + enabled: tableView.hiddenRowCount + + onClicked: { + tableView.showHiddenRows() + selectionModel.clearSelection() + } + } + + MenuItem { + text: qsTr("Reset row reordering") + icon { + source: "icons/reset_reordering.svg" + color: palette.text + } + + onClicked: tableView.resetRowReordering() + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + TableView { + id: tableView + + property int hiddenColumnCount: 0 + property int hiddenRowCount: 0 + + anchors.fill: parent + clip: true + columnSpacing: 2 + rowSpacing: 2 + boundsBehavior: Flickable.StopAtBounds + selectionBehavior: TableView.SelectCells + selectionMode: TableView.ExtendedSelection + selectionModel: selectionModel + interactive: toolbar.panEnabled + model: SpreadModel + + function showHiddenColumns() + { + for (let column = 0; column < columns; ++column) { + if (explicitColumnWidth(column) === 0) + setColumnWidth(column, -1) + } + hiddenColumnCount = 0 + } + + function hideColumn(column) + { + if (column < 0) + return + setColumnWidth(column, 0) + ++hiddenColumnCount + } + + function showHiddenRows() + { + for (let row = 0; row < rows; ++row) { + if (explicitRowHeight(row) === 0) + setRowHeight(row, -1) + } + hiddenRowCount = 0 + } + + function hideRow(row) + { + if (row < 0) + return + setRowHeight(row, 0) + ++hiddenRowCount + } + + function copyToClipboard() + { + mimeDataProvider.reset() + if (selectionModel.hasSelection) { + const source_index = selectionModel.selectedIndexes[0] + mimeDataProvider.sourceCell = cellAtIndex(source_index) + mimeDataProvider.loadSelectedData() + } else { + const current_index = selectionModel.currentIndex + const current_cell = cellAtIndex(current_index) + mimeDataProvider.sourceCell = current_cell + mimeDataProvider.loadDataFromModel(current_cell, current_index, model) + } + } + + function cutToClipboard() + { + mimeDataProvider.reset() + if (selectionModel.hasSelection) { + const source_index = selectionModel.selectedIndexes[0] + mimeDataProvider.sourceCell = cellAtIndex(source_index) + mimeDataProvider.loadSelectedData() + } else { + const current_index = selectionModel.currentIndex + const current_cell = cellAtIndex(current_index) + mimeDataProvider.sourceCell = current_cell + mimeDataProvider.loadDataFromModel(current_cell, current_index, model) + } + mimeDataProvider.includeCutData = true + } + + function pasteFromClipboard() + { + visibleCellsConnection.blockConnection(true) + const current_index = selectionModel.currentIndex + const current_cell = cellAtIndex(current_index) + if (mimeDataProvider.size() === 1) { + if (selectionModel.hasSelection) { + for (let i in selectionModel.selectedIndexes) + mimeDataProvider.saveDataToModel(0, selectionModel.selectedIndexes[i], model) + } else { + const old_cell = mimeDataProvider.cellAt(0) + const old_index = tableView.index(old_cell.y, old_cell.x) + const new_x = old_cell.x + current_cell.x - mimeDataProvider.sourceCell.x + const new_y = old_cell.y + current_cell.y - mimeDataProvider.sourceCell.y + mimeDataProvider.saveDataToModel(0, index(new_y, new_x), model) + } + } else if (mimeDataProvider.size() > 1) { + for (let i = 0; i < mimeDataProvider.size(); ++i) { + let cell_i = mimeDataProvider.cellAt(i) + cell_i.x += current_cell.x - mimeDataProvider.sourceCell.x + cell_i.y += current_cell.y - mimeDataProvider.sourceCell.y + const index_i = index(cell_i.y, cell_i.x) + mimeDataProvider.saveDataToModel(i, index_i, model) + } + } + if (mimeDataProvider.includeCutData) { + for (let i = 0; i < mimeDataProvider.size(); ++i) { + const cell_i = mimeDataProvider.cellAt(i) + model.clearItemData(index(cell_i.y, cell_i.x)) + } + mimeDataProvider.includeCutData = false + } + visibleCellsConnection.blockConnection(false) + visibleCellsConnection.updateViewArea() + } + + function resetColumnReordering() + { + clearColumnReordering() + model.resetColumnMapping() + } + + function resetRowReordering() + { + clearRowReordering() + model.resetRowMapping() + } + + ScrollBar.horizontal: ScrollBar { } + ScrollBar.vertical: ScrollBar { } + + rowHeightProvider: function(row) { + const height = explicitRowHeight(row) + if (height === 0) + return 0 + else if (height > 0) + return Math.max(height, 30) + return implicitRowWidth(row) + } + + columnWidthProvider: function(column) { + const width = explicitColumnWidth(column) + if (width === 0) + return 0 + else if (width > 0) + return Math.max(width, 30) + return implicitColumnWidth(column) + } + + delegate: TableCell { + required property var model + + implicitWidth: 90 + implicitHeight: 36 + text: model.display ?? "" + // We don't create data for empty cells to reduce + // the memory usage in case of huge model. + // If a cell does not have data and it's not highlighted neither + // the model.highlight is undefined which is replaced with false value. + highlight: model.highlight ?? false + edit: model.edit ?? "" + + onCommit: text => model.edit = text + } + + Keys.onPressed: function (event) { + if (event.matches(StandardKey.Copy)) { + copyToClipboard() + } else if (event.matches(StandardKey.Cut)) { + cutToClipboard() + } else if (event.matches(StandardKey.Paste)) { + pasteFromClipboard() + } else if (event.matches(StandardKey.Delete)) { + visibleCellsConnection.blockConnection() + if (selectionModel.hasSelection) + model.clearItemData(selectionModel.selectedIndexes) + else + model.clearItemData(selectionModel.currentIndex) + visibleCellsConnection.blockConnection(false) + visibleCellsConnection.updateViewArea() + } + } + + Connections { + id: visibleCellsConnection + target: SpreadModel + + function onDataChanged(tl, br, roles) + { + updateViewArea() + } + + function updateViewArea() + { + visibleCellsConnection.blockConnection(true) + const topRow = tableView.topRow + const bottomRow = tableView.bottomRow + const leftColumn = tableView.leftColumn + const rightColumn = tableView.rightColumn + SpreadModel.update(topRow, bottomRow, leftColumn, rightColumn) + visibleCellsConnection.blockConnection(false) + } + + function blockConnection(block=true) + { + visibleCellsConnection.enabled = !block + } + } + + MouseArea { + id: dragArea + + property point dragCell: Qt.point(-1, -1) + property bool hadSelection: false + + anchors.fill: parent + drag.axis: Drag.XandYAxis + drag.target: dropArea + acceptedButtons: Qt.LeftButton + cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor + + onPressed: function(mouse) { + mouse.accepted = false + // only when Alt modifier is pressed + if (mouse.modifiers !== Qt.AltModifier) + return + // check cell under press position + const position = Qt.point(mouse.x, mouse.y) + const cell = tableView.cellAtPosition(position, true) + if (cell.x < 0 || cell.y < 0) + return + // check selected indexes + const index = tableView.index(cell.y, cell.x) + hadSelection = selectionModel.hasSelection + if (!hadSelection) + selectionModel.select(index, ItemSelectionModel.Select) + if (!selectionModel.isSelected(index)) + return + // store selected data + mimeDataProvider.reset() + mimeDataProvider.loadSelectedData() + // accept dragging + if (mimeDataProvider.size() > 0) { + mouse.accepted = true + dragCell = cell + } + + dropArea.startDragging() + } + + onReleased: { + dropArea.stopDragging() + // reset selection, if dragging caused the selection + if (!hadSelection) + selectionModel.clearSelection() + hadSelection = false + dragCell = Qt.point(-1, -1) + } + } + } + + DropArea { + id: dropArea + + property point dropCell: Qt.point(-1, -1) + + anchors.fill: tableView + Drag.active: dragArea.drag.active + + function startDragging() + { + // block updating visible area + visibleCellsConnection.blockConnection() + } + + function stopDragging() + { + Drag.drop() + // unblock update visible area + visibleCellsConnection.blockConnection(false) + visibleCellsConnection.updateViewArea() // now update visible area + } + + onDropped: { + const position = Qt.point(dragArea.mouseX, dragArea.mouseY) + dropCell = tableView.cellAtPosition(position, true) + if (dropCell.x < 0 || dropCell.y < 0) + return + if (dragArea.dragCell === dropCell) + return + + tableView.model.clearItemData(selectionModel.selectedIndexes) + for (let i = 0; i < mimeDataProvider.size(); ++i) { + let cell = mimeDataProvider.cellAt(i) + cell.x += dropCell.x - dragArea.dragCell.x + cell.y += dropCell.y - dragArea.dragCell.y + const index = tableView.index(cell.y, cell.x) + mimeDataProvider.saveDataToModel(i, index, tableView.model) + } + mimeDataProvider.reset() + selectionModel.clearSelection() + + const drop_index = tableView.index(dropCell.y, dropCell.x) + selectionModel.setCurrentIndex(drop_index, ItemSelectionModel.Current) + + tableView.model.clearHighlight() + } + + onPositionChanged: { + const position = Qt.point(dragArea.mouseX, dragArea.mouseY) + // cell is the cell that currently mouse is over it + const cell = tableView.cellAtPosition(position, true) + // dropCell is the cell that it was under the mouse's last position + // if the last and current cells are the same, then there is no need + // to update highlight, as nothing is changed since last time. + if (cell === dropCell) + return + // if something is changed, it means that if the current cell is changed, + // then clear highlighted cells and update the dropCell. + tableView.model.clearHighlight() + dropCell = cell + // if the current cell was invalid (mouse is out side of the TableView) + // then no need to update highlight + if (cell.x < 0 || cell.y < 0) + return + // if dragged cell is the same as the (possibly) dropCell + // then no need to highlight any cells + if (dragArea.dragCell === dropCell) + return + // if the dropCell is not the same as the dragging cell and also + // is not the same as the cell at the mouse's last position + // then highlights the target cells + for (let i in selectionModel.selectedIndexes) { + const old_index = selectionModel.selectedIndexes[i] + let cell = tableView.cellAtIndex(old_index) + cell.x += dropCell.x - dragArea.dragCell.x + cell.y += dropCell.y - dragArea.dragCell.y + const new_index = tableView.index(cell.y, cell.x) + tableView.model.setHighlight(new_index, true) + } + } + } + } + } + + SelectionRectangle { + id: selectionRectangle + target: tableView + selectionMode: SelectionRectangle.Auto + + topLeftHandle: Rectangle { + width: 20 + height: 20 + radius: 10 + color: Qt.styleHints.colorScheme === Qt.Light ? palette.highlight.lighter(1.4) + : palette.highlight.darker(1.4) + visible: SelectionRectangle.control.active + } + + bottomRightHandle: Rectangle { + width: 20 + height: 20 + radius: 10 + color: Qt.styleHints.colorScheme === Qt.Light ? palette.highlight.lighter(1.4) + : palette.highlight.darker(1.4) + visible: SelectionRectangle.control.active + } + } + + SpreadSelectionModel { + id: selectionModel + behavior: SpreadSelectionModel.SelectCells + } + + SpreadMimeDataProvider { + id: mimeDataProvider + + property bool includeCutData: false + property point sourceCell: Qt.point(-1, -1) + + function loadSelectedData() + { + for (let i in selectionModel.selectedIndexes) { + const index = selectionModel.selectedIndexes[i] + const cell = tableView.cellAtIndex(index) + loadDataFromModel(cell, index, tableView.model) + } + } + + function resetProvider() + { + sourceCell = Qt.point(-1, -1) + includeCutData = false + reset() + } + } + + HelpDialog { + id: helpDialog + anchors.centerIn: parent + } +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/TableCell.qml b/examples/quickcontrols/spreadsheets/Spreadsheets/TableCell.qml new file mode 100644 index 0000000000..80941ad79a --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/TableCell.qml @@ -0,0 +1,41 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + clip: true + + property alias text: textItem.text + property bool highlight: false + required property bool current + required property bool selected + required property bool editing + required property string edit + + signal commit(text: string) + + readonly property bool __darkMode: Qt.styleHints.colorScheme === Qt.Dark + border { + width: (!editing && current) ? 1 : 0 + color: current ? palette.highlight.darker(__darkMode ? 0.7 : 1.9) : palette.base + } + readonly property color __highlight_color: __darkMode + ? palette.highlight.darker(1.9) + : palette.highlight.lighter(1.9) + color: highlight ? __highlight_color : selected ? palette.highlight : palette.base + + Label { + id: textItem + anchors { fill: parent; margins: 5 } + visible: !root.editing + } + + TableView.editDelegate: TextField { + anchors.fill: root + text: root.edit + TableView.onCommit: root.commit(text) + } +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/datamodel.cpp b/examples/quickcontrols/spreadsheets/Spreadsheets/datamodel.cpp new file mode 100644 index 0000000000..c27b693173 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/datamodel.cpp @@ -0,0 +1,266 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "datamodel.h" +#include "spreadrole.h" + +bool DataModel::empty() const +{ + return m_keys.empty(); +} + +std::pair<SpreadKey, SpreadKey> DataModel::clearHighlight() +{ + SpreadKey top_left{INT_MAX, INT_MAX}; + SpreadKey bottom_right{-1, -1}; + + for (auto it = m_cells.begin(); it != m_cells.end();) { + it.value().set(spread::Role::Hightlight, false); + if (it.key().first < top_left.first) + top_left.first = it.key().first; + if (it.key().second < top_left.second) + top_left.second = it.key().second; + if (bottom_right.first < it.key().first) + bottom_right.first = it.key().first; + if (bottom_right.second < it.key().second) + bottom_right.second = it.key().second; + if (it.value().isNull()) { + m_keys.remove(it.value().id); + auto cit = spread::make_const(m_cells, it); + it = m_cells.erase(cit); + } else { + ++it; + } + } + + return std::make_pair(top_left, bottom_right); +} + +bool DataModel::setHighlight(const SpreadKey &key, bool highlight) +{ + if (auto it = m_cells.find(key); it != m_cells.end()) { + it.value().set(spread::Role::Hightlight, highlight); + if (it.value().isNull()) { + m_keys.remove(it.value().id); + auto cit = spread::make_const(m_cells, it); + m_cells.erase(cit); + } + return true; + } + if (highlight) { + SpreadCell cell; + cell.set(spread::Role::Hightlight, true); + cell.id = ++lastId; + m_cells.insert(key, cell); + m_keys.insert(cell.id, key); + return true; + } + // we skipped false highlight for non-existing cell + // because we don't store cells with only false hightlight data + // to save memory + return false; +} + +QVariant DataModel::getData(int id, int role) const +{ + auto it_key = m_keys.find(id); + if (it_key == m_keys.end()) + return QVariant{}; + const SpreadKey &key = it_key.value(); + auto it_cell = m_cells.find(key); + if (it_cell == m_cells.end()) + return QVariant{}; + return it_cell.value().get(role); +} + +QVariant DataModel::getData(const SpreadKey &key, int role) const +{ + auto it = m_cells.find(key); + return it == m_cells.end() ? QVariant{} : it.value().get(role); +} + +bool DataModel::setData(const SpreadKey &key, const QVariant &value, int role) +{ + // special roles + switch (role) { + case spread::Role::Hightlight: + return setHighlight(key, value.toBool()); + default: break; + } + + // no special handling for the role + if (auto it = m_cells.find(key); it != m_cells.end()) { + it.value().set(role, value); + if (it.value().isNull()) { + clearData(key); + return true; + } + } else { + SpreadCell cell; + cell.set(role, value); + cell.id = ++lastId; + if (!cell.isNull()) { + m_cells.insert(key, cell); + m_keys.insert(cell.id, key); + } + } + + return true; +} + +bool DataModel::clearData(const SpreadKey &key) +{ + auto find_key = [&key](const auto &i) { return i == key; }; + auto it = std::find_if(m_keys.cbegin(), m_keys.cend(), find_key); + if (it == m_keys.cend()) + return 0; + m_keys.erase(it); + return m_cells.remove(key) > 0; +} + +void DataModel::shiftColumns(int from, int count) +{ + if (count > 0) { + // the reason for reverse iteration is because of the coverage of + // the updated keys (bigger keys) and existing keys (next keys) + QMapIterator i(m_cells); + i.toBack(); + while (i.hasPrevious()) { + i.previous(); + if (i.key().second >= from) { + SpreadKey key = i.key(); + SpreadCell cell = i.value(); + m_cells.remove(key); + key.second += count; + m_cells.insert(key, cell); + } + } + } else if (count < 0) { + // the reason for normal iteration is because of the coverage of + // the updated keys (smaller keys) and existing keys (previous keys) + for (auto it = m_cells.begin(); it != m_cells.end(); ++it) { + if (it.key().second >= from) { + SpreadKey key = it.key(); + SpreadCell cell = it.value(); + m_cells.remove(key); + key.second += count; + m_cells.insert(key, cell); + } + } + } + + if (count != 0) { + for (auto it = m_keys.begin(); it != m_keys.end(); ++it) { + SpreadKey &key = it.value(); + if (key.second >= from) + key.second += count; + } + } +} + +void DataModel::removeColumnCells(int column) +{ + for (auto it = m_cells.begin(); it != m_cells.end(); ) { + if (it.key().second == column) { + auto cit = spread::make_const(m_cells, it); + it = m_cells.erase(cit); + } else { + ++it; + } + } + + for (auto it = m_keys.begin(); it != m_keys.end(); ) { + if (it.value().second == column) { + auto cit = spread::make_const(m_keys, it); + it = m_keys.erase(cit); + } else { + ++it; + } + } +} + +void DataModel::shiftRows(int from, int count) +{ + if (count > 0) { + // the reason for reverse iteration is because of the coverage of + // the updated keys (bigger keys) and existing keys (next keys) + QMapIterator i(m_cells); + i.toBack(); + while (i.hasPrevious()) { + if (i.key().first < from) + break; + SpreadKey key = i.key(); + SpreadCell cell = i.value(); + m_cells.remove(key); + key.first += count; + m_cells.insert(key, cell); + } + } else if (count < 0) { + // the reason for normal iteration is because of the coverage of + // the updated keys (smaller keys) and existing keys (previous keys) + for (auto it = m_cells.begin(); it != m_cells.end(); ++it) { + if (it.key().first >= from) { + SpreadKey key = it.key(); + SpreadCell cell = it.value(); + m_cells.remove(key); + key.first += count; + m_cells.insert(key, cell); + } + } + } + + if (count != 0) { + for (auto it = m_keys.begin(); it != m_keys.end(); ++it) { + SpreadKey &key = it.value(); + if (key.first >= from) + key.first += count; + } + } +} + +void DataModel::removeRowCells(int row) +{ + for (auto it = m_cells.begin(); it != m_cells.end(); ) { + if (it.key().first == row) { + auto cit = spread::make_const(m_cells, it); + it = m_cells.erase(cit); + } else { + ++it; + } + } + + for (auto it = m_keys.begin(); it != m_keys.end(); ) { + if (it.value().first == row) { + auto cit = spread::make_const(m_keys, it); + it = m_keys.erase(cit); + } else { + ++it; + } + } +} + +int DataModel::createId(const SpreadKey &key) +{ + auto find_key = [&key](const auto &elem) { return key == elem; }; + auto it = std::find_if(m_keys.begin(), m_keys.end(), find_key); + if (it != m_keys.end()) + return it.key(); + const int id = ++lastId; + m_keys.insert(id, key); + return id; +} + +int DataModel::getId(const SpreadKey &key) const +{ + auto find_key = [&key](const auto &elem) { return key == elem; }; + auto it = std::find_if(m_keys.begin(), m_keys.end(), find_key); + if (it == m_keys.end()) + return 0; + return it.key(); +} + +SpreadKey DataModel::getKey(int id) const +{ + auto it = m_keys.find(id); + return it == m_keys.end() ? SpreadKey{-1, -1} : it.value(); +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/datamodel.h b/examples/quickcontrols/spreadsheets/Spreadsheets/datamodel.h new file mode 100644 index 0000000000..de64f4d0bd --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/datamodel.h @@ -0,0 +1,63 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef DATAMODEL_H +#define DATAMODEL_H + +#include "spreadcell.h" +#include "spreadkey.h" + +/********************************************************** + * The DataModel struct manages the binding of data, keys, + * and ids. There are some special functionalities that are + * only related to data and keys, like + * shifting columns and rows + * inserting column and row + * removing columns and rows, and + * managing only the data. + * This struct is extracted from the SpreadModel, and the + * intention is to simplify the SpreadModel class and also + * encapsulate any data-related concepts. + **********************************************************/ +struct DataModel +{ + bool empty() const; + + /****************************************************** + * Unsets highlight of highlighted data. + * Returns a pair of top-left and bottom-right keys of updated cells + ******************************************************/ + std::pair<SpreadKey, SpreadKey> clearHighlight(); + /****************************************************** + * Sets highlight role of data. + * Returns true if any cell updated, otherwise, false. + ******************************************************/ + bool setHighlight(const SpreadKey &key, bool highlight); + + QVariant getData(int id, int role) const; + QVariant getData(const SpreadKey &key, int role) const; + bool setData(const SpreadKey &key, const QVariant &value, int role); + bool clearData(const SpreadKey &key); + + void shiftColumns(int from, int count); + void removeColumnCells(int column); + + void shiftRows(int from, int count); + void removeRowCells(int row); + + /****************************************************** + * If the key already exists in the model, returns the + * id; otherwise, adds the key, assignes an id, and + * returns the id. + ******************************************************/ + int createId(const SpreadKey &key); + int getId(const SpreadKey &key) const; + SpreadKey getKey(int id) const; + +private: + uint lastId = 0; + QMap<SpreadKey, SpreadCell> m_cells; + QMap<int, SpreadKey> m_keys; +}; + +#endif // DATAMODEL_H diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/copy.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/copy.svg new file mode 100644 index 0000000000..9510d9750f --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/copy.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="File / copy_general_fill"> +<path id="Layer01" fill-rule="evenodd" clip-rule="evenodd" d="M6 20C5.44754 20 5 19.5524 5 19.0003V6C5 5.44772 4.55228 5 4 5C3.44772 5 3 5.44772 3 6V19.0003C3 20.6573 4.34333 22 6 22H16C16.5523 22 17 21.5523 17 21C17 20.4477 16.5523 20 16 20H6Z" fill="#0D0D0D"/> +<rect id="Layer02" x="6" y="2" width="14" height="17" rx="2" fill="#0D0D0D"/> +</g> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/cut.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/cut.svg new file mode 100644 index 0000000000..c87d677894 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/cut.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Edit / cut"> +<path id="Layer01" fill-rule="evenodd" clip-rule="evenodd" d="M19.4142 3H21.2929C21.6834 3 22 3.31658 22 3.70711C22 3.89464 21.9255 4.0745 21.7929 4.20711L15 11L13 9L18.7071 3.29289C18.8946 3.10536 19.149 3 19.4142 3ZM10 12L12 14L9.70711 16.2929C9.68701 16.313 9.66627 16.3321 9.64495 16.3501C9.87301 16.8531 10 17.4117 10 18C10 20.2091 8.20914 22 6 22C3.79086 22 2 20.2091 2 18C2 15.7909 3.79086 14 6 14C6.58827 14 7.14688 14.127 7.64991 14.3551C7.66794 14.3337 7.68701 14.313 7.70711 14.2929L10 12ZM8 18C8 19.1046 7.10457 20 6 20C4.89543 20 4 19.1046 4 18C4 16.8954 4.89543 16 6 16C7.10457 16 8 16.8954 8 18Z" fill="#0D0D0D"/> +<path id="Layer02" fill-rule="evenodd" clip-rule="evenodd" d="M7.64991 9.64495C7.14688 9.87301 6.58827 10 6 10C3.79086 10 2 8.20914 2 6C2 3.79086 3.79086 2 6 2C8.20914 2 10 3.79086 10 6C10 6.58827 9.87301 7.14688 9.64495 7.64992C9.66627 7.66795 9.68701 7.68701 9.70711 7.70711L21.7929 19.7929C21.9255 19.9255 22 20.1054 22 20.2929C22 20.6834 21.6834 21 21.2929 21H19.4142C19.149 21 18.8946 20.8946 18.7071 20.7071L7.70711 9.70711C7.68701 9.68701 7.66794 9.66627 7.64991 9.64495ZM8 6C8 7.10457 7.10457 8 6 8C4.89543 8 4 7.10457 4 6C4 4.89543 4.89543 4 6 4C7.10457 4 8 4.89543 8 6ZM13 12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12C11 11.4477 11.4477 11 12 11C12.5523 11 13 11.4477 13 12Z" fill="#0D0D0D"/> +</g> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/help.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/help.svg new file mode 100644 index 0000000000..a911efaade --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/help.svg @@ -0,0 +1,5 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Navigation / help_question_circle_fill"> +<path id="Layer01" fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM11.0859 13.1913C11.0305 13.6404 11.4243 14 11.8735 14C12.3172 14 12.65 13.6271 12.7863 13.2017C12.8088 13.1315 12.834 13.0635 12.8614 12.9937C12.9699 12.7167 13.2775 12.3375 13.7841 11.8562C14.1605 11.4771 14.4572 11.1161 14.6743 10.7734C14.8914 10.4307 15 10.0187 15 9.5375C15 8.72083 14.7033 8.09375 14.1098 7.65625C13.5164 7.21875 12.8143 7 12.0038 7C11.1787 7 10.5093 7.21875 9.99543 7.65625C9.47937 8.13493 9.18688 8.71516 9.03161 9.20479C8.88562 9.66512 9.26613 10.0625 9.74578 10.0625C10.2209 10.0625 10.5687 9.65217 10.7158 9.19697C10.7473 9.09933 10.7885 9.0118 10.8422 8.94687C11.0955 8.64062 11.4827 8.4875 12.0038 8.4875C12.467 8.4875 12.8143 8.6151 13.0459 8.87031C13.2775 9.12552 13.3933 9.40625 13.3933 9.7125C13.3933 10.0042 13.3065 10.2776 13.1328 10.5328C12.9591 10.788 12.742 11.025 12.4814 11.2437C11.8446 11.8125 11.4537 12.2427 11.309 12.5344C11.2968 12.5589 11.2847 12.5825 11.2728 12.6058L11.2726 12.6061C11.1938 12.7598 11.1218 12.9004 11.0859 13.1913ZM13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17C12.5523 17 13 16.5523 13 16Z" fill="#0D0D0D"/> +</g> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/hide.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/hide.svg new file mode 100644 index 0000000000..08acd995ce --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/hide.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M19.4968 15.7897C20.2836 14.7793 20.9142 13.7515 21.4279 12.9142L21.4279 12.9142C21.6371 12.5732 21.8269 12.2638 22 12C21.8651 11.743 21.7267 11.4676 21.5817 11.179C20.1143 8.25938 17.9737 4 12 4C10.6125 4 9.41059 4.24746 8.36812 4.66102L11.717 8.00986C11.8105 8.00332 11.9048 8 12 8C14.2091 8 16 9.79086 16 12C16 12.0952 15.9967 12.1895 15.9901 12.283L19.4968 15.7897ZM2 12C2.55147 10.6765 3.50324 8.46339 5.28578 6.69999L8.55382 9.96803C8.20193 10.5635 8 11.2582 8 12C8 14.2091 9.79086 16 12 16C12.7418 16 13.4365 15.7981 14.032 15.4462L16.9327 18.347C15.5944 19.3254 13.9735 20 12 20C6.80109 20 4.08117 15.4622 2.47108 12.776C2.3024 12.4945 2.1459 12.2334 2 12ZM10 12C10 11.8208 10.0236 11.6472 10.0677 11.482L12.518 13.9323C12.3528 13.9764 12.1792 14 12 14C10.8954 14 10 13.1046 10 12Z" fill="#0D0D0D"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M2.29289 2.29289C2.68342 1.90237 3.31658 1.90237 3.70711 2.29289L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L2.29289 3.70711C1.90237 3.31658 1.90237 2.68342 2.29289 2.29289Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_column_left.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_column_left.svg new file mode 100644 index 0000000000..a11ce748aa --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_column_left.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM12 16C11.4477 16 11 15.5523 11 15V13H9C8.44772 13 8 12.5523 8 12C8 11.4477 8.44772 11 9 11H11V9C11 8.44772 11.4477 8 12 8C12.5523 8 13 8.44772 13 9V11H15C15.5523 11 16 11.4477 16 12C16 12.5523 15.5523 13 15 13H13V15C13 15.5523 12.5523 16 12 16Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_column_right.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_column_right.svg new file mode 100644 index 0000000000..a11ce748aa --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_column_right.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM12 16C11.4477 16 11 15.5523 11 15V13H9C8.44772 13 8 12.5523 8 12C8 11.4477 8.44772 11 9 11H11V9C11 8.44772 11.4477 8 12 8C12.5523 8 13 8.44772 13 9V11H15C15.5523 11 16 11.4477 16 12C16 12.5523 15.5523 13 15 13H13V15C13 15.5523 12.5523 16 12 16Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_row_above.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_row_above.svg new file mode 100644 index 0000000000..a11ce748aa --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_row_above.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM12 16C11.4477 16 11 15.5523 11 15V13H9C8.44772 13 8 12.5523 8 12C8 11.4477 8.44772 11 9 11H11V9C11 8.44772 11.4477 8 12 8C12.5523 8 13 8.44772 13 9V11H15C15.5523 11 16 11.4477 16 12C16 12.5523 15.5523 13 15 13H13V15C13 15.5523 12.5523 16 12 16Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_row_below.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_row_below.svg new file mode 100644 index 0000000000..a11ce748aa --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/insert_row_below.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM12 16C11.4477 16 11 15.5523 11 15V13H9C8.44772 13 8 12.5523 8 12C8 11.4477 8.44772 11 9 11H11V9C11 8.44772 11.4477 8 12 8C12.5523 8 13 8.44772 13 9V11H15C15.5523 11 16 11.4477 16 12C16 12.5523 15.5523 13 15 13H13V15C13 15.5523 12.5523 16 12 16Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/pan.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/pan.svg new file mode 100644 index 0000000000..2fbcf5639b --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/pan.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V240c0 8.8-7.2 16-16 16s-16-7.2-16-16V64c0-17.7-14.3-32-32-32s-32 14.3-32 32V336c0 1.5 0 3.1 .1 4.6L67.6 283c-16-15.2-41.3-14.6-56.6 1.4s-14.6 41.3 1.4 56.6L124.8 448c43.1 41.1 100.4 64 160 64H304c97.2 0 176-78.8 176-176V128c0-17.7-14.3-32-32-32s-32 14.3-32 32V240c0 8.8-7.2 16-16 16s-16-7.2-16-16V64c0-17.7-14.3-32-32-32s-32 14.3-32 32V240c0 8.8-7.2 16-16 16s-16-7.2-16-16V32z"/></svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/paste.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/paste.svg new file mode 100644 index 0000000000..ceb3e34169 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/paste.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="File / paste_general_fill"> +<path id="Layer01" d="M6 19.875C6 21.0486 6.96431 22 8.15385 22L17.8462 22C19.0357 22 20 21.0486 20 19.875L20 7.125C20 5.95139 19.0357 5 17.8462 5L8.15385 5C6.96431 5 6 5.95139 6 7.125L6 19.875Z" fill="#0D0D0D"/> +<path id="Layer02" fill-rule="evenodd" clip-rule="evenodd" d="M5 18C3.89543 18 3 17.1046 3 16V4C3 2.89543 3.89543 2 5 2H14C15.1046 2 16 2.89543 16 4L7 4C5.89543 4 5 4.89543 5 6L5 18Z" fill="#0D0D0D"/> +</g> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/remove_column.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/remove_column.svg new file mode 100644 index 0000000000..2940d0a3c7 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/remove_column.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM9 11C8.44772 11 8 11.4477 8 12C8 12.5523 8.44772 13 9 13H15C15.5523 13 16 12.5523 16 12C16 11.4477 15.5523 11 15 11H9Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/remove_row.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/remove_row.svg new file mode 100644 index 0000000000..2940d0a3c7 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/remove_row.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM9 11C8.44772 11 8 11.4477 8 12C8 12.5523 8.44772 13 9 13H15C15.5523 13 16 12.5523 16 12C16 11.4477 15.5523 11 15 11H9Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/reset_reordering.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/reset_reordering.svg new file mode 100644 index 0000000000..009579560c --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/reset_reordering.svg @@ -0,0 +1,5 @@ +<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8 12.8182C8 11.814 8.56711 11 9.26667 11H20.0333C23.8809 11 27 15.4772 27 21C27 26.5228 23.8809 31 20.0333 31H12C11.3004 31 10.5 30.186 10.5 29.1818C10.5 28.1777 11.3004 27.3636 12 27.3636H20.0333C22.4818 27.3636 24.4667 24.5145 24.4667 21C24.4667 17.4855 22.4818 14.6364 20.0333 14.6364H9.26667C8.56711 14.6364 8 13.8223 8 12.8182Z" fill="#0D0D0D"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9428 5.72385C13.4635 6.24455 13.4635 7.08877 12.9428 7.60947L8.55228 12L12.9428 16.3905C13.4635 16.9112 13.4635 17.7554 12.9428 18.2761C12.4221 18.7968 11.5779 18.7968 11.0572 18.2761L5.72385 12.9428C5.20315 12.4221 5.20315 11.5779 5.72385 11.0572L11.0572 5.72385C11.5779 5.20315 12.4221 5.20315 12.9428 5.72385Z" fill="#0D0D0D"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M15.7976 29.1818C15.7976 30.186 15.3317 31 14.5953 31H12.631C8.58088 31 5 26.5228 5 21C5 15.4772 5 22.5 5 20C5 19 5.5 18 6.5 18C7.5 18 7.96598 19 7.96598 20H8C8 22 7.9643 17.4855 7.9643 21C7.9643 24.5145 10.0536 27.3636 12.631 27.3636H14.5953C15.3317 27.3636 15.7976 28.1777 15.7976 29.1818Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/icons/show.svg b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/show.svg new file mode 100644 index 0000000000..b8e3141966 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/icons/show.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21.5817 11.179C20.1143 8.25938 17.9737 4 12 4C5.4359 4 3.02564 9.53846 2 12C2.1459 12.2334 2.3024 12.4945 2.47108 12.776C4.08117 15.4622 6.80109 20 12 20C17.0808 20 19.8241 15.5284 21.4279 12.9142C21.6371 12.5732 21.8269 12.2638 22 12C21.8651 11.743 21.7267 11.4676 21.5817 11.179ZM14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12ZM16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z" fill="#0D0D0D"/> +</svg> diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadcell.cpp b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadcell.cpp new file mode 100644 index 0000000000..b15818a426 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadcell.cpp @@ -0,0 +1,69 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "spreadcell.h" +#include "spreadrole.h" +#include "spreadmodel.h" + +bool SpreadCell::isNull() const +{ + return !has(spread::Role::Display) && !has(spread::Role::Hightlight); +} + +bool SpreadCell::has(int role) const +{ + switch (role) { + case spread::Role::Display: + case spread::Role::Edit: + return !text.isNull() && !text.isEmpty(); + case spread::Role::Hightlight: + return highlight; // false highlight equals to no highlight set + default: + return false; + } +} + +void SpreadCell::set(int role, const QVariant &data) +{ + switch (role) { + case spread::Role::Edit: + text = data.toString(); + break; + case spread::Role::Hightlight: + highlight = data.toBool(); + break; + default: + break; + } +} + +QVariant SpreadCell::get(int role) const +{ + switch (role) { + case spread::Role::Edit: + return text; + case spread::Role::Display: { + const QString display_text = displayText(); + return display_text.isNull() ? QVariant{} : display_text; + } + case spread::Role::Hightlight: + return highlight; + default: + return QVariant{}; + } +} + +QString SpreadCell::displayText() const +{ + SpreadModel *model = SpreadModel::instance(); + const Formula formula = model->parseFormulaString(text); + if (!formula.isValid()) + return text; + if (formula.firstOperandId() <= 0) // at least one arg should be available + return "#ERROR!"; + if ((formula.firstOperandId() == id) || (formula.secondOperandId() == id)) + return "#ERROR!"; // found loop + if (formula.includesLoop(model, model->dataModel())) + return "#ERROR!"; + return model->formulaValueText(formula); +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadcell.h b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadcell.h new file mode 100644 index 0000000000..0fb980ab9b --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadcell.h @@ -0,0 +1,26 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SPREADCELL_H +#define SPREADCELL_H + +#include <QVariant> + +struct SpreadCell { + friend struct DataModel; + + bool isNull() const; + bool has(int role) const; + void set(int role, const QVariant &data); + QVariant get(int role) const; + +private: + QString displayText() const; + +private: + uint id = 0; + QString text; + bool highlight = false; +}; + +#endif // SPREADCELL_H diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadformula.cpp b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadformula.cpp new file mode 100644 index 0000000000..87a961f40c --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadformula.cpp @@ -0,0 +1,74 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "spreadformula.h" +#include "datamodel.h" +#include "spreadkey.h" +#include "spreadrole.h" +#include "spreadmodel.h" + +bool Formula::includesLoop(SpreadModel *model, const DataModel *dataModel, QSet<int> *history) const +{ + if (m_operator == Operator::Invalid) + return false; + + if (history == nullptr) { + QSet<int> history; + return includesLoop(model, dataModel, &history); + } + + if (m_operator == Operator::Sum) { + SpreadKey top_left = dataModel->getKey(m_cellIds.first); + SpreadKey bottom_right = dataModel->getKey(m_cellIds.second); + if (bottom_right.first < top_left.first) + std::swap(top_left.first, bottom_right.first); + if (bottom_right.second < top_left.second) + std::swap(top_left.second, bottom_right.second); + for (int row = top_left.first; row <= bottom_right.first; ++row) { + for (int column = top_left.second; column <= bottom_right.second; ++column) { + const int id = dataModel->getId(SpreadKey{row, column}); + if (history->find(id) != history->end()) + return true; + const QString edit_text = dataModel->getData(id, spread::Role::Edit).toString(); + const Formula formula = model->parseFormulaString(edit_text); + if (!formula.isValid()) + continue; + auto it = history->insert(id); + if (formula.includesLoop(model, dataModel, history)) + return true; + auto cit = spread::make_const(*history, it); + history->erase(cit); + } + } + } else { + const int id_1 = m_cellIds.first; + if (history->find(id_1) != history->end()) + return true; + const QString edit_text = dataModel->getData(id_1, spread::Role::Edit).toString(); + const Formula formula = model->parseFormulaString(edit_text); + if (!formula.isValid()) + return false; + auto it = history->insert(id_1); + if (formula.includesLoop(model, dataModel, history)) + return true; + auto cit = spread::make_const(*history, it); + history->erase(cit); + + if (m_operator != Operator::Assign) { + const int id_2 = m_cellIds.second; + if (history->find(id_2) != history->end()) + return true; + const QString edit_text = dataModel->getData(id_2, spread::Role::Edit).toString(); + const Formula formula = model->parseFormulaString(edit_text); + if (!formula.isValid()) + return false; + auto it = history->insert(id_2); + if (formula.includesLoop(model, dataModel, history)) + return true; + auto cit = spread::make_const(*history, it); + history->erase(cit); + } + } + + return false; +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadformula.h b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadformula.h new file mode 100644 index 0000000000..6548da10b3 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadformula.h @@ -0,0 +1,43 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SPREADFORMULA_H +#define SPREADFORMULA_H + +#include <QSet> + +class DataModel; +class SpreadModel; + +struct Formula { + enum class Operator { + Invalid = 0, + Assign, + Add, + Sub, + Div, + Mul, + Sum, + }; + + static Formula create(Operator op, int arg1, int arg2) { + Formula formula; + formula.m_operator = op; + formula.m_cellIds.first = arg1; + formula.m_cellIds.second = arg2; + return formula; + } + + bool isValid() const { return m_operator != Operator::Invalid; } + int firstOperandId() const { return m_cellIds.first; } + int secondOperandId() const { return m_cellIds.second; } + Operator getOperator() const { return m_operator; } + + bool includesLoop(SpreadModel *model, const DataModel *dataModel, QSet<int> *history = nullptr) const; + +private: + Operator m_operator = Operator::Invalid; + std::pair<int, int> m_cellIds = {0, 0}; +}; + +#endif // SPREADFORMULA_H diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadkey.h b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadkey.h new file mode 100644 index 0000000000..741cedb0ee --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadkey.h @@ -0,0 +1,23 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SPREADKEY_H +#define SPREADKEY_H + +#include <utility> + +namespace spread { +// make a const_iterator from an iterator for the same container +// to avoid mixing iterators with const_iterators warning +template <typename Container> +constexpr typename Container::const_iterator make_const(Container c, typename Container::iterator i) +{ + return typename Container::const_iterator(i); +} +} + +// using std::pair<> as SpreadKey for now +// for any further updates it could be anything +using SpreadKey = std::pair<int, int>; + +#endif // SPREADKEY_H diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmimedataprovider.cpp b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmimedataprovider.cpp new file mode 100644 index 0000000000..25a8d1b118 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmimedataprovider.cpp @@ -0,0 +1,88 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "spreadmimedataprovider.h" + +#include <QMimeData> +#include <QGuiApplication> +#include <QClipboard> + +namespace { +static inline constexpr auto MIMETYPE_SPREADMODEL = "application/x-qtexamplespreadmodel"; +static inline constexpr auto MIMETYPE_SELECTEDDATA = "model/data"; +static inline constexpr auto MIMETYPE_TEXT = "text/plain"; +} + +QMimeData *SpreadMimeDataProvider::saveToMimeData() const +{ + if (m_data.empty()) + return nullptr; + + QByteArray data; + QDataStream stream{&data, QDataStream::WriteOnly}; + for (auto it = m_data.begin(); it != m_data.end(); ++it) { + const QPoint &cell = it->first; + const QMap<int, QVariant> &item_data = it->second; + stream << cell.x() << cell.y() << item_data; + } + + QMimeData *mime_data = new QMimeData{}; + mime_data->setData(MIMETYPE_SPREADMODEL, QByteArray{}); + mime_data->setData(MIMETYPE_SELECTEDDATA, data); + return mime_data; +} + +bool SpreadMimeDataProvider::loadFromMimeData(const QMimeData *mimeData) +{ + if (!mimeData) + return false; + + if (!mimeData->hasFormat(MIMETYPE_SPREADMODEL)) + return false; + + QByteArray data = mimeData->data(MIMETYPE_SELECTEDDATA); + QDataStream stream{&data, QDataStream::ReadOnly}; + while (!stream.atEnd()) { + QPoint cell; + QMap<int, QVariant> item_data; + stream >> cell.rx() >> cell.ry() >> item_data; + m_data.push_back(std::make_pair(cell, item_data)); + } + + return true; +} + +bool SpreadMimeDataProvider::saveToClipboard() +{ + QMimeData *mime_data = saveToMimeData(); + if (!mime_data) + return false; + + QGuiApplication::clipboard()->setMimeData(mime_data); + return true; +} + +bool SpreadMimeDataProvider::loadFromClipboard() +{ + const QMimeData *mime_data = QGuiApplication::clipboard()->mimeData(); + if (!mime_data) + return false; + + return loadFromMimeData(mime_data); +} + +bool SpreadMimeDataProvider::saveDataToModel(int index, + const QModelIndex &modelIndex, + QAbstractItemModel *model) const +{ + const QMap<int, QVariant> &item_data = m_data.at(index).second; + return model->setItemData(modelIndex, item_data); +} + +void SpreadMimeDataProvider::loadDataFromModel(const QPoint &cell, + const QModelIndex &index, + const QAbstractItemModel *model) +{ + const QMap<int, QVariant> &item_data = model->itemData(index); + m_data.push_back(std::make_pair(cell, item_data)); +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmimedataprovider.h b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmimedataprovider.h new file mode 100644 index 0000000000..5082890c89 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmimedataprovider.h @@ -0,0 +1,39 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SPREADMIMEDATAPROVIDER_H +#define SPREADMIMEDATAPROVIDER_H + +#include <QObject> +#include <QAbstractItemModel> +#include <QPoint> +#include <QQmlEngine> + +class QMimeData; + +class SpreadMimeDataProvider : public QObject +{ + Q_OBJECT + QML_ELEMENT + +public: + QMimeData *saveToMimeData() const; + bool loadFromMimeData(const QMimeData *mimeData); + + Q_INVOKABLE bool saveToClipboard(); + Q_INVOKABLE bool loadFromClipboard(); + Q_INVOKABLE void reset() { m_data.clear(); } + Q_INVOKABLE int size() const { return m_data.size(); } + Q_INVOKABLE QPoint cellAt(int index) const { return m_data.at(index).first; } + Q_INVOKABLE bool saveDataToModel(int index, + const QModelIndex &modelIndex, + QAbstractItemModel *model) const; + Q_INVOKABLE void loadDataFromModel(const QPoint &cell, + const QModelIndex &index, + const QAbstractItemModel *model); + +private: + std::vector<std::pair<QPoint, QMap<int, QVariant>>> m_data; +}; + +#endif // SPREADMIMEDATAPROVIDER_H diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.cpp b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.cpp new file mode 100644 index 0000000000..081adbb68f --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.cpp @@ -0,0 +1,724 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "spreadmodel.h" +#include "spreadrole.h" + +#include <QPoint> +#include <QRegularExpression> + +int SpreadModel::columnNumberFromName(const QString &text) +{ + if (text.size() == 1) + return getModelColumn(text[0].toLatin1() - 'A'); + else if (text.size() == 2) + return getModelColumn((text[0].toLatin1() - 'A' + 1) * 26 + (text[1].toLatin1() - 'A')); + return 0; +} + +int SpreadModel::rowNumberFromName(const QString &text) +{ + return getModelRow(text.toInt() - 1); +} + +SpreadModel *SpreadModel::instance() +{ + return s_instance; +} + +SpreadModel *SpreadModel::create(QQmlEngine *, QJSEngine *) +{ + if (!s_instance) + s_instance = new SpreadModel {nullptr}; + return s_instance; +} + +void SpreadModel::update(int topRow, int bottomRow, int leftColumn, int rightColumn) +{ + emit dataChanged(index(topRow, leftColumn), index(bottomRow, rightColumn), {spread::Role::Display,}); +} + +void SpreadModel::clearHighlight() +{ + if (m_dataModel.empty()) + return; + + const auto [top_left, bottom_right] = m_dataModel.clearHighlight(); + + emit dataChanged(index(top_left.first, top_left.second), + index(bottom_right.first, bottom_right.second), + {spread::Role::Hightlight,}); +} + +void SpreadModel::setHighlight(const QModelIndex &index, bool highlight) +{ + if (!index.isValid()) + return; + if (m_dataModel.setHighlight(SpreadKey{index.row(), index.column()}, highlight)) + emit dataChanged(index, index, {spread::Role::Hightlight,}); +} + +int SpreadModel::rowCount(const QModelIndex &parent) const +{ + return m_size.first; +} + +int SpreadModel::columnCount(const QModelIndex &parent) const +{ + return m_size.second; +} + +QVariant SpreadModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant{}; + + const int row = index.row(); + const int column = index.column(); + + return m_dataModel.getData(SpreadKey{row, column}, role); +} + +bool SpreadModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) + return false; + + const int row = index.row(); + const int column = index.column(); + + if (m_dataModel.setData(SpreadKey{row, column}, value, role)) + emit dataChanged(index, index); + + return true; +} + +bool SpreadModel::clearItemData(const QModelIndex &index) +{ + if (!index.isValid()) + return false; + if (m_dataModel.clearData(SpreadKey{index.row(), index.column()})) { + emit dataChanged(index, index); + return true; + } + return false; +} + +Qt::ItemFlags SpreadModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; +} + +QHash<int, QByteArray> SpreadModel::roleNames() const +{ + return {{spread::Role::Display, "display"}, + {spread::Role::Edit, "edit"}, + {spread::Role::ColumnName, "columnName"}, + {spread::Role::RowName, "rowName"}, + {spread::Role::Hightlight, "highlight"}}; + +} + +QVariant SpreadModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case spread::Role::ColumnName: + case spread::Role::RowName: + break; + default: + return QVariant{}; + } + + switch (orientation) { + case Qt::Horizontal: { + constexpr char A = 'A'; + const int view_section = getViewColumn(section); + if (view_section < 26) { + return QString{static_cast<char>(view_section + A)}; + } else { + const int first = view_section / 26 - 1; + const int second = view_section % 26; + QString title{static_cast<char>(first + A)}; + title += static_cast<char>(second + A); + return title; + } + } + case Qt::Vertical: { + return getViewRow(section) + 1; + } + } + + return QVariant{}; +} + +bool SpreadModel::insertColumns(int column, int count, const QModelIndex &parent) +{ + if (count != 1) // TODO: implement inserting more than 1 columns + return false; + + beginInsertColumns(QModelIndex{}, column, column + count - 1); + m_size.second += count; + + // update model + m_dataModel.shiftColumns(column, count); + + QMap<int, int> old_columns; + std::swap(old_columns, m_viewColumns); + for (auto it = old_columns.begin(); it != old_columns.end(); ++it) { + const int new_view_column = (it.value() < column) ? it.value() : it.value() + count; + const int new_model_column = (it.key() < column) ? it.key() : it.key() + count; + m_viewColumns.insert(new_model_column, new_view_column); + } + + endInsertColumns(); + return true; +} + +bool SpreadModel::insertRows(int row, int count, const QModelIndex &parent) +{ + if (count != 1) // TODO: implement inserting more than 1 rows + return false; + + beginInsertRows(QModelIndex{}, row, row + count - 1); + m_size.first += count; + + // update model + m_dataModel.shiftRows(row, count); + + endInsertRows(); + return true; +} + +bool SpreadModel::removeColumns(int column, int count, const QModelIndex &parent) +{ + if (count != 1) // TODO: implement removing more than 1 columns + return false; + + beginRemoveColumns(QModelIndex{}, column, column + count - 1); + m_size.second -= count; + + // update model + m_dataModel.removeColumnCells(column); + m_dataModel.shiftColumns(column + 1, -count); + + endRemoveColumns(); + return true; +} + +bool SpreadModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (count != 1) // TODO: implement removing more than 1 rows + return false; + + beginRemoveRows(QModelIndex{}, row, row + count - 1); + m_size.first -= count; + + // update model + m_dataModel.removeRowCells(row); + m_dataModel.shiftRows(row + 1, -count); + + endRemoveRows(); + return true; +} + +bool SpreadModel::clearItemData(const QModelIndexList &indexes) +{ + bool ok = true; + for (const QModelIndex &index : indexes) + ok &= clearItemData(index); + return ok; +} + +bool SpreadModel::removeColumns(QModelIndexList indexes) +{ + auto greater = [](const QModelIndex &lhs, const QModelIndex &rhs) + { + return lhs.column() > rhs.column(); + }; + std::sort(indexes.begin(), indexes.end(), greater); + for (const QModelIndex &index : indexes) + removeColumn(index.column()); + return true; +} + +bool SpreadModel::removeRows(QModelIndexList indexes) +{ + auto greater = [](const QModelIndex &lhs, const QModelIndex &rhs) + { + return lhs.row() > rhs.row(); + }; + std::sort(indexes.begin(), indexes.end(), greater); + for (const QModelIndex &index : indexes) + removeRow(index.row()); + return true; +} + +void SpreadModel::mapColumn(int model, int view) +{ + if (model == view) + m_viewColumns.remove(model); + else + m_viewColumns[model] = view; + emit headerDataChanged(Qt::Horizontal, model, view); +} + +void SpreadModel::mapRow(int model, int view) +{ + if (model == view) + m_viewRows.remove(model); + else + m_viewRows[model] = view; + emit headerDataChanged(Qt::Vertical, model, view); +} + +Formula SpreadModel::parseFormulaString(const QString &qText) +{ + if (qText.isEmpty()) + return Formula{}; + + QRegularExpression pattern_re; + + // is formula + pattern_re.setPattern("^\\s*=.*"); + QRegularExpressionMatch match = pattern_re.match(qText); + if (!match.hasMatch()) + return Formula{}; + + // is Assignment: e.g. =A1 + pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); + match = pattern_re.match(qText); + if (match.hasMatch()) { + const QString column_label = match.captured(1); + const QString row_label = match.captured(2); + const int column = columnNumberFromName(column_label); + const int row = rowNumberFromName(row_label); + const int cell_id = m_dataModel.createId(SpreadKey{row, column}); + return Formula::create(Formula::Operator::Assign, cell_id, 0); + } + + // is Addition: e.g. =A1+A2 + pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\+\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); + match = pattern_re.match(qText); + if (match.hasMatch()) { + // first argument + QString column_label = match.captured(1); + QString row_label = match.captured(2); + int column = columnNumberFromName(column_label); + int row = rowNumberFromName(row_label); + const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); + // second argument + column_label = match.captured(3); + row_label = match.captured(4); + column = columnNumberFromName(column_label); + row = rowNumberFromName(row_label); + const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); + // create formula + return Formula::create(Formula::Operator::Add, cell_id_1, cell_id_2); + } + + // is Subtraction: e.g. =A1-A2 + pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\-\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); + match = pattern_re.match(qText); + if (match.hasMatch()) { + // first argument + QString column_label = match.captured(1); + QString row_label = match.captured(2); + int column = columnNumberFromName(column_label); + int row = rowNumberFromName(row_label); + const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); + // second argument + column_label = match.captured(3); + row_label = match.captured(4); + column = columnNumberFromName(column_label); + row = rowNumberFromName(row_label); + const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); + // create formula + return Formula::create(Formula::Operator::Sub, cell_id_1, cell_id_2); + } + + // is Multiply: e.g. =A1*A2 + pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\*\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); + match = pattern_re.match(qText); + if (match.hasMatch()) { + // first argument + QString column_label = match.captured(1); + QString row_label = match.captured(2); + int column = columnNumberFromName(column_label); + int row = rowNumberFromName(row_label); + const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); + // second argument + column_label = match.captured(3); + row_label = match.captured(4); + column = columnNumberFromName(column_label); + row = rowNumberFromName(row_label); + const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); + // create formula + return Formula::create(Formula::Operator::Mul, cell_id_1, cell_id_2); + } + + // is Division: e.g. =A1/A2 + pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\/\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); + match = pattern_re.match(qText); + if (match.hasMatch()) { + // first argument + QString column_label = match.captured(1); + QString row_label = match.captured(2); + int column = columnNumberFromName(column_label); + int row = rowNumberFromName(row_label); + const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); + // second argument + column_label = match.captured(3); + row_label = match.captured(4); + column = columnNumberFromName(column_label); + row = rowNumberFromName(row_label); + const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); + // create formula + return Formula::create(Formula::Operator::Div, cell_id_1, cell_id_2); + } + + // is Summation: e.g. =SUM A1:A2 + pattern_re.setPattern("^\\s*=\\s*[Ss][Uu][Mm]\\s+([a-zA-Z]+)([1-9][0-9]*)\\s*\\:\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); + match = pattern_re.match(qText); + if (match.hasMatch()) { + // first argument + QString column_label = match.captured(1); + QString row_label = match.captured(2); + int column = columnNumberFromName(column_label); + int row = rowNumberFromName(row_label); + const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); + // second argument + column_label = match.captured(3); + row_label = match.captured(4); + column = columnNumberFromName(column_label); + row = rowNumberFromName(row_label); + const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); + // create formula + return Formula::create(Formula::Operator::Sum, cell_id_1, cell_id_2); + } + + return Formula{}; +} + +QString SpreadModel::formulaValueText(const Formula &formula) +{ + switch (formula.getOperator()) { + case Formula::Operator::Assign: { + const QVariant value = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); + return value.isNull() ? QString{} : value.toString(); + } + case Formula::Operator::Add: { + const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); + const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); + if (value_1.isNull() && value_2.isNull()) + return "0"; + // check int values + bool is_int_1 = true; + bool is_int_2 = true; + const int int_1 = value_1.isNull() ? 0 : value_1.toInt(&is_int_1); + const int int_2 = value_2.isNull() ? 0 : value_2.toInt(&is_int_2); + if (is_int_1 && is_int_2) + return QString::number(int_1 + int_2); + // check double values + bool is_double_1 = true; + bool is_double_2 = true; + const double double_1 = value_1.isNull() ? 0 : value_1.toDouble(&is_double_1); + const double double_2 = value_2.isNull() ? 0 : value_2.toDouble(&is_double_2); + if (is_double_1 && is_double_2) + return QString::number(double_1 + double_2); + return "#ERROR!"; + } + case Formula::Operator::Sub: { + const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); + const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); + if (value_1.isNull() && value_2.isNull()) + return "0"; + // check int values + bool is_int_1 = true; + bool is_int_2 = true; + const int int_1 = value_1.isNull() ? 0 : value_1.toInt(&is_int_1); + const int int_2 = value_2.isNull() ? 0 : value_2.toInt(&is_int_2); + if (is_int_1 && is_int_2) + return QString::number(int_1 - int_2); + // check double values + bool is_double_1 = true; + bool is_double_2 = true; + const double double_1 = value_1.isNull() ? 0 : value_1.toDouble(&is_double_1); + const double double_2 = value_2.isNull() ? 0 : value_2.toDouble(&is_double_2); + if (is_double_1 && is_double_2) + return QString::number(double_1 - double_2); + return "#ERROR!"; + } + case Formula::Operator::Mul: { + const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); + const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); + if (value_1.isNull() || value_2.isNull()) + return "0"; + // check int values + bool is_int_1 = true; + bool is_int_2 = true; + const int int_1 = value_1.toInt(&is_int_1); + const int int_2 = value_2.toInt(&is_int_2); + if (is_int_1 && is_int_2) + return QString::number(int_1 * int_2); + // check double values + bool is_double_1 = true; + bool is_double_2 = true; + const double double_1 = value_1.toDouble(&is_double_1); + const double double_2 = value_2.toDouble(&is_double_2); + if (is_double_1 && is_double_2) + return QString::number(double_1 * double_2); + return "#ERROR!"; + } + case Formula::Operator::Div: { + const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); + const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); + if (value_1.isNull() && value_2.isNull()) + return "#ERROR!"; + // check int values + bool is_int_1 = true; + bool is_int_2 = true; + const int int_1 = value_1.isNull() ? 0 : value_1.toInt(&is_int_1); + const int int_2 = value_2.isNull() ? 0 : value_2.toInt(&is_int_2); + if (is_int_1 && is_int_2) { + if (int_2 == 0) + return "#ERROR!"; + return QString::number(int_1 / int_2); + } + // check double values + bool is_double_1 = true; + bool is_double_2 = true; + const double double_1 = value_1.isNull() ? 0 : value_1.toDouble(&is_double_1); + const double double_2 = value_2.isNull() ? 0 : value_2.toDouble(&is_double_2); + if (is_double_1 && is_double_2) { + if (double_2 == 0) + return "#ERROR!"; + return QString::number(double_1 / double_2); + } + return "#ERROR!"; + } + case Formula::Operator::Sum: { + SpreadKey top_left = m_dataModel.getKey(formula.firstOperandId()); + SpreadKey bottom_right = m_dataModel.getKey(formula.secondOperandId()); + top_left.first = getViewRow(top_left.first); + top_left.second = getViewColumn(top_left.second); + bottom_right.first = getViewRow(bottom_right.first); + bottom_right.second = getViewColumn(bottom_right.second); + if (bottom_right.first < top_left.first) + std::swap(top_left.first, bottom_right.first); + if (bottom_right.second < top_left.second) + std::swap(top_left.second, bottom_right.second); + double sum = 0; + for (int row = top_left.first; row <= bottom_right.first; ++row) { + for (int column = top_left.second; column <= bottom_right.second; ++column) { + const int model_row = getModelRow(row); + const int model_column = getModelColumn(column); + const SpreadKey key {model_row, model_column}; + const QVariant value = m_dataModel.getData(key, spread::Role::Display); + if (value.isNull()) + continue; + bool is_double = false; + const double d = value.toDouble(&is_double); + if (is_double) + sum += d; + else + return "#ERROR!"; + } + } + return QString::number(sum); + } + default: + break; + } + return QString{}; +} + +int SpreadModel::getViewColumn(int modelColumn) const +{ + auto it = m_viewColumns.find(modelColumn); + return (it != m_viewColumns.end()) ? it.value() : modelColumn; +} + +int SpreadModel::getModelColumn(int viewColumn) const +{ + auto find_view_column = [viewColumn](const auto &item) { + return item == viewColumn; + }; + auto it = std::find_if(m_viewColumns.begin(), m_viewColumns.end(), find_view_column); + return it != m_viewColumns.end() ? it.key() : viewColumn; +} + +int SpreadModel::getViewRow(int modelRow) const +{ + auto it = m_viewRows.find(modelRow); + return (it != m_viewRows.end()) ? it.value() : modelRow; +} + +int SpreadModel::getModelRow(int viewRow) const +{ + auto find_view_row = [viewRow](const auto &item){ + return item == viewRow; + }; + auto it = std::find_if(m_viewRows.begin(), m_viewRows.end(), find_view_row); + return (it != m_viewRows.end()) ? it.key() : viewRow; +} + +void SpreadSelectionModel::toggleColumn(int column) +{ + isColumnSelected(column) ? deselectColumn(column) : selectColumn(column, false); +} + +void SpreadSelectionModel::deselectColumn(int column) +{ + const QAbstractItemModel *model = this->model(); + const QModelIndex first = model->index(0, column); + const QModelIndex last = model->index(model->rowCount() - 1, column); + QModelIndexList selectedRows = this->selectedRows(column); + if (selectedRows.empty()) { + select(QItemSelection{first, last}, SelectionFlag::Deselect); + return; + } + + auto topToBottom = [](const QModelIndex &lhs, const QModelIndex &rhs) -> bool + { + return lhs.row() < rhs.row(); + }; + std::sort(selectedRows.begin(), selectedRows.end(), topToBottom); + + QModelIndex index = first; + for (const QModelIndex &selectedRow : selectedRows) { + if (index.row() < selectedRow.row()) + select(QItemSelection{index, model->index(selectedRow.row() - 1, column)}, + SelectionFlag::Deselect); + index = model->index(selectedRow.row() + 1, column); + } + if (index.row() <= last.row()) + select(QItemSelection{index, last}, SelectionFlag::Deselect); +} + +void SpreadSelectionModel::selectColumn(int column, bool clear) +{ + if (clear) + this->clear(); + const QAbstractItemModel *model = this->model(); + const QModelIndex first = model->index(0, column); + const QModelIndex last = model->index(model->rowCount() - 1, column); + select(QItemSelection{first, last}, SelectionFlag::Select); +} + +void SpreadSelectionModel::toggleRow(int row) +{ + isRowSelected(row) ? deselectRow(row) : selectRow(row, false); +} + +void SpreadSelectionModel::deselectRow(int row) +{ + const QAbstractItemModel *model = this->model(); + const QModelIndex first = model->index(row, 0); + const QModelIndex last = model->index(row, model->columnCount() - 1); + QModelIndexList selectedColumns = this->selectedColumns(row); + if (selectedColumns.empty()) { + select(QItemSelection{first, last}, SelectionFlag::Deselect); + return; + } + + auto leftToRight = [](const QModelIndex &lhs, const QModelIndex &rhs) -> bool + { + return lhs.column() < rhs.column(); + }; + std::sort(selectedColumns.begin(), selectedColumns.end(), leftToRight); + + QModelIndex index = first; + for (const QModelIndex &selectedColumn : selectedColumns) { + if (index.column() < selectedColumn.column()) + select(QItemSelection{index, model->index(row, selectedColumn.column() - 1)}, + SelectionFlag::Deselect); + index = model->index(row, selectedColumn.column() + 1); + } + if (index.column() <= last.column()) + select(QItemSelection{index, last}, SelectionFlag::Deselect); +} + +void SpreadSelectionModel::selectRow(int row, bool clear) +{ + if (clear) + this->clear(); + const QAbstractItemModel *model = this->model(); + const QModelIndex first = model->index(row, 0); + const QModelIndex last = model->index(row, model->columnCount() - 1); + select(QItemSelection{first, last}, SelectionFlag::Select); +} + +void SpreadSelectionModel::setBehavior(Behavior behavior) +{ + if (behavior == m_behavior) + return; + m_behavior = behavior; + emit behaviorChanged(); +} + +void HeaderSelectionModel::setCurrent(int current) +{ + switch (m_orientation) { + case Qt::Horizontal: + QItemSelectionModel::setCurrentIndex(model()->index(0, current), SelectionFlag::Current); + break; + case Qt::Vertical: + QItemSelectionModel::setCurrentIndex(model()->index(current, 0), SelectionFlag::Current); + break; + default: + break; + } +} + +void HeaderSelectionModel::setSelectionModel(SpreadSelectionModel *selectionModel) +{ + if (selectionModel == m_selectionModel) + return; + if (m_selectionModel) + disconnect(m_selectionModel); + m_selectionModel = selectionModel; + if (m_selectionModel) + connect(m_selectionModel, &SpreadSelectionModel::selectionChanged, this, + &HeaderSelectionModel::onSelectionChanged); + emit selectionModelChanged(); +} + +void HeaderSelectionModel::setOrientation(Qt::Orientation orientation) +{ + if (orientation == m_orientation) + return; + m_orientation = orientation; + emit orientationChanged(); +} + +void HeaderSelectionModel::onSelectionChanged(const QItemSelection &selected, + const QItemSelection &deselected) +{ + const QAbstractItemModel *model = this->model(); + for (const QModelIndex &index : selected.indexes()) { + switch (m_orientation) { + case Qt::Horizontal: + if (m_selectionModel->isColumnSelected(index.column())) + select(model->index(0, index.column()), SelectionFlag::Select); + break; + case Qt::Vertical: + if (m_selectionModel->isRowSelected(index.row())) + select(model->index(index.row(), 0), SelectionFlag::Select); + break; + } + } + for (const QModelIndex &index : deselected.indexes()) { + switch (m_orientation) { + case Qt::Horizontal: + if (!m_selectionModel->isColumnSelected(index.column())) + select(model->index(0, index.column()), SelectionFlag::Deselect); + break; + case Qt::Vertical: + if (!m_selectionModel->isRowSelected(index.row())) + select(model->index(index.row(), 0), SelectionFlag::Deselect); + break; + } + } +} diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.h b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.h new file mode 100644 index 0000000000..43098c73e5 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.h @@ -0,0 +1,152 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SPREADMODEL_H +#define SPREADMODEL_H + +#include "datamodel.h" +#include "spreadformula.h" + +#include <QQmlEngine> +#include <QAbstractTableModel> +#include <QItemSelectionModel> + + +class DataModel; +class SpreadModel; + +class SpreadModel final : public QAbstractTableModel +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_DISABLE_COPY_MOVE(SpreadModel) + + friend class SpreadSelectionModel; + friend class SpreadCell; + friend struct Formula; + +protected: + explicit SpreadModel(QObject *parent = nullptr) : QAbstractTableModel(parent) { } + +public: + int rowCount() const { return rowCount(QModelIndex{}); } + int columnCount() const { return columnCount(QModelIndex{}); } + + const DataModel *dataModel() { return &m_dataModel; } + + int columnNumberFromName(const QString &text); + int rowNumberFromName(const QString &text); + // returns nullptr if it's not been created yet. + static SpreadModel *instance(); + static SpreadModel *create(QQmlEngine *, QJSEngine *); + +protected: + Q_INVOKABLE void update(int topRow, int bottomRow, int leftColumn, int rightColumn); + Q_INVOKABLE void clearHighlight(); + Q_INVOKABLE void setHighlight(const QModelIndex &index, bool highlight); + Q_INVOKABLE bool clearItemData(const QModelIndexList &indexes); + Q_INVOKABLE bool removeColumns(QModelIndexList indexes); + Q_INVOKABLE bool removeRows(QModelIndexList indexes); + Q_INVOKABLE void mapColumn(int model, int view); + Q_INVOKABLE void mapRow(int model, int view); + Q_INVOKABLE void resetColumnMapping() { m_viewColumns.clear(); } + Q_INVOKABLE void resetRowMapping() { m_viewRows.clear(); } + + // QAbstractItemModel interface + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Q_INVOKABLE bool clearItemData(const QModelIndex &index) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QHash<int, QByteArray> roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex{}) override; + bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex{}) override; + bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex{}) override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex{}) override; + +private: + Formula parseFormulaString(const QString &text); + QString formulaValueText(const Formula &formula); + int getViewColumn(int modelColumn) const; + int getModelColumn(int viewColumn) const; + int getViewRow(int modelRow) const; + int getModelRow(int viewRow) const; + +private: + std::pair<int, int> m_size {1000, 26}; // rows:1-1000, columns:A-Z + DataModel m_dataModel; + QMap<int, int> m_viewColumns; + QMap<int, int> m_viewRows; + static inline SpreadModel *s_instance {nullptr}; +}; + +class SpreadSelectionModel : public QItemSelectionModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(Behavior behavior READ getBehavior WRITE setBehavior NOTIFY behaviorChanged FINAL) + +public: + enum Behavior { + DisabledBehavior, + SelectCells, + SelectColumns, + SelectRows, + }; + Q_ENUM(Behavior) + +public: + Q_INVOKABLE void toggleColumn(int column); + Q_INVOKABLE void deselectColumn(int column); + Q_INVOKABLE void selectColumn(int column, bool clear = true); + Q_INVOKABLE void toggleRow(int row); + Q_INVOKABLE void deselectRow(int row); + Q_INVOKABLE void selectRow(int row, bool clear = true); + +protected: + Behavior getBehavior() const { return m_behavior; } + +protected slots: + void setBehavior(Behavior); + +signals: + void behaviorChanged(); + +private: + Behavior m_behavior = Behavior::DisabledBehavior; +}; + +class HeaderSelectionModel : public QItemSelectionModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(SpreadSelectionModel* selectionModel READ getSelectionModel WRITE setSelectionModel NOTIFY selectionModelChanged FINAL) + Q_PROPERTY(Qt::Orientation orientation READ getOrientation WRITE setOrientation NOTIFY orientationChanged FINAL) + +protected: + Q_INVOKABLE void setCurrent(int current = -1); + SpreadSelectionModel *getSelectionModel() { return m_selectionModel; } + Qt::Orientation getOrientation() const { return m_orientation; } + +protected slots: + void setSelectionModel(SpreadSelectionModel *selectionModel); + void setOrientation(Qt::Orientation orientation); + +private slots: + void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +signals: + void selectionModelChanged(); + void orientationChanged(); + +private: + SpreadSelectionModel *m_selectionModel = nullptr; + Qt::Orientation m_orientation = Qt::Horizontal; +}; + +#endif // SPREADMODEL_H diff --git a/examples/quickcontrols/spreadsheets/Spreadsheets/spreadrole.h b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadrole.h new file mode 100644 index 0000000000..aa1c69138a --- /dev/null +++ b/examples/quickcontrols/spreadsheets/Spreadsheets/spreadrole.h @@ -0,0 +1,23 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SPREADROLE_H +#define SPREADROLE_H + +#include <Qt> + +namespace spread { +enum Role { + // data roles + BeginRole = Qt::DisplayRole, // begin of data roles + Display = Qt::DisplayRole, + Edit = Qt::EditRole, + Hightlight = Qt::UserRole + 1, + EndRole, // end of data roles + // non-data roles + ColumnName, + RowName, +}; +} + +#endif // SPREADROLE_H diff --git a/examples/quickcontrols/spreadsheets/main.cpp b/examples/quickcontrols/spreadsheets/main.cpp new file mode 100644 index 0000000000..5c8b46e326 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/main.cpp @@ -0,0 +1,24 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QGuiApplication> +#include <QQmlApplicationEngine> +#include <QIcon> + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QObject::connect( + &engine, + &QQmlApplicationEngine::objectCreationFailed, + &app, + []() { QCoreApplication::exit(-1); }, + Qt::QueuedConnection); + engine.loadFromModule("Spreadsheets", "Main"); + + app.setWindowIcon(QIcon{":/qt/examples/spreadsheet/icons/spreadsheet.svg"}); + + return app.exec(); +} diff --git a/examples/quickcontrols/spreadsheets/spreadsheet.svg b/examples/quickcontrols/spreadsheets/spreadsheet.svg new file mode 100644 index 0000000000..cc13cbb907 --- /dev/null +++ b/examples/quickcontrols/spreadsheets/spreadsheet.svg @@ -0,0 +1,32 @@ +<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect y="8" width="64" height="44" fill="#989898"/> +<path d="M2 11.1111C2 10.4975 2.49746 10 3.11111 10H5.88889C6.50254 10 7 10.4975 7 11.1111V13.8889C7 14.5025 6.50254 15 5.88889 15H3.11111C2.49746 15 2 14.5025 2 13.8889V11.1111Z" fill="#0F0F0F"/> +<path d="M9 11.1111C9 10.4975 9 10 10.0234 10H61.0185C62 10 62 10.4975 62 11.1111V13.8889C62 14.5025 62 15 61.0185 15H9.98148C9 15 9 14.5025 9 13.8889V11.1111Z" fill="#0D0D0D"/> +<path d="M9 11.1111C9 10.4975 9 10 10.0234 10H61.0185C62 10 62 10.4975 62 11.1111V13.8889C62 14.5025 62 15 61.0185 15H9.98148C9 15 9 14.5025 9 13.8889V11.1111Z" fill="#0D0D0D"/> +<path d="M3.11111 50C2.49746 50 2 50 2 49.3628L2 17.6111C2 17 2.49746 17 3.11111 17H5.88889C6.50254 17 7 17 7 17.6111L7 49.3889C7 50 6.50254 50 5.88889 50H3.11111Z" fill="#0D0D0D"/> +<path d="M9.75 17.5H17.25C17.3864 17.5 17.4384 17.5232 17.4484 17.5285C17.4486 17.5287 17.4488 17.5288 17.449 17.5289C17.4499 17.5304 17.4513 17.533 17.4531 17.5367C17.4646 17.5615 17.4808 17.6153 17.4902 17.7225C17.4996 17.8293 17.5 17.9523 17.5 18.1111V20.8889C17.5 21.0477 17.4996 21.1707 17.4902 21.2775C17.4808 21.3847 17.4646 21.4385 17.4531 21.4633C17.4513 21.467 17.4499 21.4696 17.449 21.4711C17.4488 21.4712 17.4486 21.4713 17.4484 21.4715C17.4384 21.4768 17.3864 21.5 17.25 21.5H9.75C9.61364 21.5 9.56161 21.4768 9.55162 21.4715C9.5514 21.4713 9.5512 21.4712 9.55101 21.4711C9.55007 21.4696 9.54867 21.467 9.54691 21.4633C9.53536 21.4385 9.51924 21.3847 9.50979 21.2775C9.50037 21.1707 9.5 21.0477 9.5 20.8889V17.8333C9.5 17.6813 9.50057 17.5858 9.50723 17.5137C9.55683 17.5062 9.63379 17.5 9.75 17.5Z" stroke="black"/> +<path d="M31.75 17.5H39.25C39.3864 17.5 39.4384 17.5232 39.4484 17.5285C39.4486 17.5287 39.4488 17.5288 39.449 17.5289C39.4499 17.5304 39.4513 17.533 39.4531 17.5367C39.4646 17.5615 39.4808 17.6153 39.4902 17.7225C39.4996 17.8293 39.5 17.9523 39.5 18.1111V20.8889C39.5 21.0477 39.4996 21.1707 39.4902 21.2775C39.4808 21.3847 39.4646 21.4385 39.4531 21.4633C39.4513 21.467 39.4499 21.4696 39.449 21.4711C39.4488 21.4712 39.4486 21.4713 39.4484 21.4715C39.4384 21.4768 39.3864 21.5 39.25 21.5H31.75C31.6136 21.5 31.5616 21.4768 31.5516 21.4715C31.5514 21.4713 31.5512 21.4712 31.551 21.4711C31.5501 21.4696 31.5487 21.467 31.5469 21.4633C31.5354 21.4385 31.5192 21.3847 31.5098 21.2775C31.5004 21.1707 31.5 21.0477 31.5 20.8889V17.8333C31.5 17.6813 31.5006 17.5858 31.5072 17.5137C31.5568 17.5062 31.6338 17.5 31.75 17.5Z" stroke="black"/> +<path d="M42.75 17.5H50.25C50.3864 17.5 50.4384 17.5232 50.4484 17.5285C50.4486 17.5287 50.4488 17.5288 50.449 17.5289C50.4499 17.5304 50.4513 17.533 50.4531 17.5367C50.4646 17.5615 50.4808 17.6153 50.4902 17.7225C50.4996 17.8293 50.5 17.9523 50.5 18.1111V20.8889C50.5 21.0477 50.4996 21.1707 50.4902 21.2775C50.4808 21.3847 50.4646 21.4385 50.4531 21.4633C50.4513 21.467 50.4499 21.4696 50.449 21.4711C50.4488 21.4712 50.4486 21.4713 50.4484 21.4715C50.4384 21.4768 50.3864 21.5 50.25 21.5H42.75C42.6136 21.5 42.5616 21.4768 42.5516 21.4715C42.5514 21.4713 42.5512 21.4712 42.551 21.4711C42.5501 21.4696 42.5487 21.467 42.5469 21.4633C42.5354 21.4385 42.5192 21.3847 42.5098 21.2775C42.5004 21.1707 42.5 21.0477 42.5 20.8889V17.8333C42.5 17.6813 42.5006 17.5858 42.5072 17.5137C42.5568 17.5062 42.6338 17.5 42.75 17.5Z" stroke="black"/> +<path d="M53.75 17.5H61.25C61.3864 17.5 61.4384 17.5232 61.4484 17.5285C61.4486 17.5287 61.4488 17.5288 61.449 17.5289C61.4499 17.5304 61.4513 17.533 61.4531 17.5367C61.4646 17.5615 61.4808 17.6153 61.4902 17.7225C61.4996 17.8293 61.5 17.9523 61.5 18.1111V20.8889C61.5 21.0477 61.4996 21.1707 61.4902 21.2775C61.4808 21.3847 61.4646 21.4385 61.4531 21.4633C61.4513 21.467 61.4499 21.4696 61.449 21.4711C61.4488 21.4712 61.4486 21.4713 61.4484 21.4715C61.4384 21.4768 61.3864 21.5 61.25 21.5H53.75C53.6136 21.5 53.5616 21.4768 53.5516 21.4715C53.5514 21.4713 53.5512 21.4712 53.551 21.4711C53.5501 21.4696 53.5487 21.467 53.5469 21.4633C53.5354 21.4385 53.5192 21.3847 53.5098 21.2775C53.5004 21.1707 53.5 21.0477 53.5 20.8889V17.8333C53.5 17.6813 53.5006 17.5858 53.5072 17.5137C53.5568 17.5062 53.6338 17.5 53.75 17.5Z" stroke="black"/> +<path d="M9.75 38.5H17.25C17.3864 38.5 17.4384 38.5232 17.4484 38.5285C17.4486 38.5287 17.4488 38.5288 17.449 38.5289C17.4499 38.5304 17.4513 38.533 17.4531 38.5367C17.4646 38.5615 17.4808 38.6153 17.4902 38.7225C17.4996 38.8293 17.5 38.9523 17.5 39.1111V41.8889C17.5 42.0477 17.4996 42.1707 17.4902 42.2775C17.4808 42.3847 17.4646 42.4385 17.4531 42.4633C17.4513 42.467 17.4499 42.4696 17.449 42.4711C17.4488 42.4712 17.4486 42.4713 17.4484 42.4715C17.4384 42.4768 17.3864 42.5 17.25 42.5H9.75C9.61364 42.5 9.56161 42.4768 9.55162 42.4715C9.5514 42.4713 9.5512 42.4712 9.55101 42.4711C9.55007 42.4696 9.54867 42.467 9.54691 42.4633C9.53536 42.4385 9.51924 42.3847 9.50979 42.2775C9.50037 42.1707 9.5 42.0477 9.5 41.8889V38.8333C9.5 38.6813 9.50057 38.5858 9.50723 38.5137C9.55683 38.5062 9.63379 38.5 9.75 38.5Z" stroke="black"/> +<path d="M31.75 38.5H39.25C39.3864 38.5 39.4384 38.5232 39.4484 38.5285C39.4486 38.5287 39.4488 38.5288 39.449 38.5289C39.4499 38.5304 39.4513 38.533 39.4531 38.5367C39.4646 38.5615 39.4808 38.6153 39.4902 38.7225C39.4996 38.8293 39.5 38.9523 39.5 39.1111V41.8889C39.5 42.0477 39.4996 42.1707 39.4902 42.2775C39.4808 42.3847 39.4646 42.4385 39.4531 42.4633C39.4513 42.467 39.4499 42.4696 39.449 42.4711C39.4488 42.4712 39.4486 42.4713 39.4484 42.4715C39.4384 42.4768 39.3864 42.5 39.25 42.5H31.75C31.6136 42.5 31.5616 42.4768 31.5516 42.4715C31.5514 42.4713 31.5512 42.4712 31.551 42.4711C31.5501 42.4696 31.5487 42.467 31.5469 42.4633C31.5354 42.4385 31.5192 42.3847 31.5098 42.2775C31.5004 42.1707 31.5 42.0477 31.5 41.8889V38.8333C31.5 38.6813 31.5006 38.5858 31.5072 38.5137C31.5568 38.5062 31.6338 38.5 31.75 38.5Z" stroke="black"/> +<path d="M42.75 38.5H50.25C50.3864 38.5 50.4384 38.5232 50.4484 38.5285C50.4486 38.5287 50.4488 38.5288 50.449 38.5289C50.4499 38.5304 50.4513 38.533 50.4531 38.5367C50.4646 38.5615 50.4808 38.6153 50.4902 38.7225C50.4996 38.8293 50.5 38.9523 50.5 39.1111V41.8889C50.5 42.0477 50.4996 42.1707 50.4902 42.2775C50.4808 42.3847 50.4646 42.4385 50.4531 42.4633C50.4513 42.467 50.4499 42.4696 50.449 42.4711C50.4488 42.4712 50.4486 42.4713 50.4484 42.4715C50.4384 42.4768 50.3864 42.5 50.25 42.5H42.75C42.6136 42.5 42.5616 42.4768 42.5516 42.4715C42.5514 42.4713 42.5512 42.4712 42.551 42.4711C42.5501 42.4696 42.5487 42.467 42.5469 42.4633C42.5354 42.4385 42.5192 42.3847 42.5098 42.2775C42.5004 42.1707 42.5 42.0477 42.5 41.8889V38.8333C42.5 38.6813 42.5006 38.5858 42.5072 38.5137C42.5568 38.5062 42.6338 38.5 42.75 38.5Z" stroke="black"/> +<path d="M53.75 38.5H61.25C61.3864 38.5 61.4384 38.5232 61.4484 38.5285C61.4486 38.5287 61.4488 38.5288 61.449 38.5289C61.4499 38.5304 61.4513 38.533 61.4531 38.5367C61.4646 38.5615 61.4808 38.6153 61.4902 38.7225C61.4996 38.8293 61.5 38.9523 61.5 39.1111V41.8889C61.5 42.0477 61.4996 42.1707 61.4902 42.2775C61.4808 42.3847 61.4646 42.4385 61.4531 42.4633C61.4513 42.467 61.4499 42.4696 61.449 42.4711C61.4488 42.4712 61.4486 42.4713 61.4484 42.4715C61.4384 42.4768 61.3864 42.5 61.25 42.5H53.75C53.6136 42.5 53.5616 42.4768 53.5516 42.4715C53.5514 42.4713 53.5512 42.4712 53.551 42.4711C53.5501 42.4696 53.5487 42.467 53.5469 42.4633C53.5354 42.4385 53.5192 42.3847 53.5098 42.2775C53.5004 42.1707 53.5 42.0477 53.5 41.8889V38.8333C53.5 38.6813 53.5006 38.5858 53.5072 38.5137C53.5568 38.5062 53.6338 38.5 53.75 38.5Z" stroke="black"/> +<path d="M9.75 24.5H17.25C17.3864 24.5 17.4384 24.5232 17.4484 24.5285C17.4486 24.5287 17.4488 24.5288 17.449 24.5289C17.4499 24.5304 17.4513 24.533 17.4531 24.5367C17.4646 24.5615 17.4808 24.6153 17.4902 24.7225C17.4996 24.8293 17.5 24.9523 17.5 25.1111V27.8889C17.5 28.0477 17.4996 28.1707 17.4902 28.2775C17.4808 28.3847 17.4646 28.4385 17.4531 28.4633C17.4513 28.467 17.4499 28.4696 17.449 28.4711C17.4488 28.4712 17.4486 28.4713 17.4484 28.4715C17.4384 28.4768 17.3864 28.5 17.25 28.5H9.75C9.61364 28.5 9.56161 28.4768 9.55162 28.4715C9.5514 28.4713 9.5512 28.4712 9.55101 28.4711C9.55007 28.4696 9.54867 28.467 9.54691 28.4633C9.53536 28.4385 9.51924 28.3847 9.50979 28.2775C9.50037 28.1707 9.5 28.0477 9.5 27.8889V24.8333C9.5 24.6813 9.50057 24.5858 9.50723 24.5137C9.55683 24.5062 9.63379 24.5 9.75 24.5Z" stroke="black"/> +<path d="M31.75 24.5H39.25C39.3864 24.5 39.4384 24.5232 39.4484 24.5285C39.4486 24.5287 39.4488 24.5288 39.449 24.5289C39.4499 24.5304 39.4513 24.533 39.4531 24.5367C39.4646 24.5615 39.4808 24.6153 39.4902 24.7225C39.4996 24.8293 39.5 24.9523 39.5 25.1111V27.8889C39.5 28.0477 39.4996 28.1707 39.4902 28.2775C39.4808 28.3847 39.4646 28.4385 39.4531 28.4633C39.4513 28.467 39.4499 28.4696 39.449 28.4711C39.4488 28.4712 39.4486 28.4713 39.4484 28.4715C39.4384 28.4768 39.3864 28.5 39.25 28.5H31.75C31.6136 28.5 31.5616 28.4768 31.5516 28.4715C31.5514 28.4713 31.5512 28.4712 31.551 28.4711C31.5501 28.4696 31.5487 28.467 31.5469 28.4633C31.5354 28.4385 31.5192 28.3847 31.5098 28.2775C31.5004 28.1707 31.5 28.0477 31.5 27.8889V24.8333C31.5 24.6813 31.5006 24.5858 31.5072 24.5137C31.5568 24.5062 31.6338 24.5 31.75 24.5Z" stroke="black"/> +<path d="M42.75 24.5H50.25C50.3864 24.5 50.4384 24.5232 50.4484 24.5285C50.4486 24.5287 50.4488 24.5288 50.449 24.5289C50.4499 24.5304 50.4513 24.533 50.4531 24.5367C50.4646 24.5615 50.4808 24.6153 50.4902 24.7225C50.4996 24.8293 50.5 24.9523 50.5 25.1111V27.8889C50.5 28.0477 50.4996 28.1707 50.4902 28.2775C50.4808 28.3847 50.4646 28.4385 50.4531 28.4633C50.4513 28.467 50.4499 28.4696 50.449 28.4711C50.4488 28.4712 50.4486 28.4713 50.4484 28.4715C50.4384 28.4768 50.3864 28.5 50.25 28.5H42.75C42.6136 28.5 42.5616 28.4768 42.5516 28.4715C42.5514 28.4713 42.5512 28.4712 42.551 28.4711C42.5501 28.4696 42.5487 28.467 42.5469 28.4633C42.5354 28.4385 42.5192 28.3847 42.5098 28.2775C42.5004 28.1707 42.5 28.0477 42.5 27.8889V24.8333C42.5 24.6813 42.5006 24.5858 42.5072 24.5137C42.5568 24.5062 42.6338 24.5 42.75 24.5Z" stroke="black"/> +<path d="M53.75 24.5H61.25C61.3864 24.5 61.4384 24.5232 61.4484 24.5285C61.4486 24.5287 61.4488 24.5288 61.449 24.5289C61.4499 24.5304 61.4513 24.533 61.4531 24.5367C61.4646 24.5615 61.4808 24.6153 61.4902 24.7225C61.4996 24.8293 61.5 24.9523 61.5 25.1111V27.8889C61.5 28.0477 61.4996 28.1707 61.4902 28.2775C61.4808 28.3847 61.4646 28.4385 61.4531 28.4633C61.4513 28.467 61.4499 28.4696 61.449 28.4711C61.4488 28.4712 61.4486 28.4713 61.4484 28.4715C61.4384 28.4768 61.3864 28.5 61.25 28.5H53.75C53.6136 28.5 53.5616 28.4768 53.5516 28.4715C53.5514 28.4713 53.5512 28.4712 53.551 28.4711C53.5501 28.4696 53.5487 28.467 53.5469 28.4633C53.5354 28.4385 53.5192 28.3847 53.5098 28.2775C53.5004 28.1707 53.5 28.0477 53.5 27.8889V24.8333C53.5 24.6813 53.5006 24.5858 53.5072 24.5137C53.5568 24.5062 53.6338 24.5 53.75 24.5Z" stroke="black"/> +<path d="M9.75 45.5H17.25C17.3864 45.5 17.4384 45.5232 17.4484 45.5285C17.4486 45.5287 17.4488 45.5288 17.449 45.5289C17.4499 45.5304 17.4513 45.533 17.4531 45.5367C17.4646 45.5615 17.4808 45.6153 17.4902 45.7225C17.4996 45.8293 17.5 45.9523 17.5 46.1111V48.8889C17.5 49.0477 17.4996 49.1707 17.4902 49.2775C17.4808 49.3847 17.4646 49.4385 17.4531 49.4633C17.4513 49.467 17.4499 49.4696 17.449 49.4711C17.4488 49.4712 17.4486 49.4713 17.4484 49.4715C17.4384 49.4768 17.3864 49.5 17.25 49.5H9.75C9.61364 49.5 9.56161 49.4768 9.55162 49.4715C9.5514 49.4713 9.5512 49.4712 9.55101 49.4711C9.55007 49.4696 9.54867 49.467 9.54691 49.4633C9.53536 49.4385 9.51924 49.3847 9.50979 49.2775C9.50037 49.1707 9.5 49.0477 9.5 48.8889V45.8333C9.5 45.6813 9.50057 45.5858 9.50723 45.5137C9.55683 45.5062 9.63379 45.5 9.75 45.5Z" stroke="black"/> +<path d="M31.75 45.5H39.25C39.3864 45.5 39.4384 45.5232 39.4484 45.5285C39.4486 45.5287 39.4488 45.5288 39.449 45.5289C39.4499 45.5304 39.4513 45.533 39.4531 45.5367C39.4646 45.5615 39.4808 45.6153 39.4902 45.7225C39.4996 45.8293 39.5 45.9523 39.5 46.1111V48.8889C39.5 49.0477 39.4996 49.1707 39.4902 49.2775C39.4808 49.3847 39.4646 49.4385 39.4531 49.4633C39.4513 49.467 39.4499 49.4696 39.449 49.4711C39.4488 49.4712 39.4486 49.4713 39.4484 49.4715C39.4384 49.4768 39.3864 49.5 39.25 49.5H31.75C31.6136 49.5 31.5616 49.4768 31.5516 49.4715C31.5514 49.4713 31.5512 49.4712 31.551 49.4711C31.5501 49.4696 31.5487 49.467 31.5469 49.4633C31.5354 49.4385 31.5192 49.3847 31.5098 49.2775C31.5004 49.1707 31.5 49.0477 31.5 48.8889V45.8333C31.5 45.6813 31.5006 45.5858 31.5072 45.5137C31.5568 45.5062 31.6338 45.5 31.75 45.5Z" stroke="black"/> +<path d="M42.75 45.5H50.25C50.3864 45.5 50.4384 45.5232 50.4484 45.5285C50.4486 45.5287 50.4488 45.5288 50.449 45.5289C50.4499 45.5304 50.4513 45.533 50.4531 45.5367C50.4646 45.5615 50.4808 45.6153 50.4902 45.7225C50.4996 45.8293 50.5 45.9523 50.5 46.1111V48.8889C50.5 49.0477 50.4996 49.1707 50.4902 49.2775C50.4808 49.3847 50.4646 49.4385 50.4531 49.4633C50.4513 49.467 50.4499 49.4696 50.449 49.4711C50.4488 49.4712 50.4486 49.4713 50.4484 49.4715C50.4384 49.4768 50.3864 49.5 50.25 49.5H42.75C42.6136 49.5 42.5616 49.4768 42.5516 49.4715C42.5514 49.4713 42.5512 49.4712 42.551 49.4711C42.5501 49.4696 42.5487 49.467 42.5469 49.4633C42.5354 49.4385 42.5192 49.3847 42.5098 49.2775C42.5004 49.1707 42.5 49.0477 42.5 48.8889V45.8333C42.5 45.6813 42.5006 45.5858 42.5072 45.5137C42.5568 45.5062 42.6338 45.5 42.75 45.5Z" stroke="black"/> +<path d="M53.75 45.5H61.25C61.3864 45.5 61.4384 45.5232 61.4484 45.5285C61.4486 45.5287 61.4488 45.5288 61.449 45.5289C61.4499 45.5304 61.4513 45.533 61.4531 45.5367C61.4646 45.5615 61.4808 45.6153 61.4902 45.7225C61.4996 45.8293 61.5 45.9523 61.5 46.1111V48.8889C61.5 49.0477 61.4996 49.1707 61.4902 49.2775C61.4808 49.3847 61.4646 49.4385 61.4531 49.4633C61.4513 49.467 61.4499 49.4696 61.449 49.4711C61.4488 49.4712 61.4486 49.4713 61.4484 49.4715C61.4384 49.4768 61.3864 49.5 61.25 49.5H53.75C53.6136 49.5 53.5616 49.4768 53.5516 49.4715C53.5514 49.4713 53.5512 49.4712 53.551 49.4711C53.5501 49.4696 53.5487 49.467 53.5469 49.4633C53.5354 49.4385 53.5192 49.3847 53.5098 49.2775C53.5004 49.1707 53.5 49.0477 53.5 48.8889V45.8333C53.5 45.6813 53.5006 45.5858 53.5072 45.5137C53.5568 45.5062 53.6338 45.5 53.75 45.5Z" stroke="black"/> +<path d="M20 18.1111C20 17.4975 20 17 20.75 17H28.25C29 17 29 17.4975 29 18.1111V20.8889C29 21.5025 29 22 28.25 22H20.75C20 22 20 21.5025 20 20.8889V18.1111Z" fill="#0D0D0D"/> +<path d="M20 39.1111C20 38.4975 20 38 20.75 38H28.25C29 38 29 38.4975 29 39.1111V41.8889C29 42.5025 29 43 28.25 43H20.75C20 43 20 42.5025 20 41.8889V39.1111Z" fill="#0D0D0D"/> +<path d="M20 25.1111C20 24.4975 20 24 20.75 24H28.25C29 24 29 24.4975 29 25.1111V27.8889C29 28.5025 29 29 28.25 29H20.75C20 29 20 28.5025 20 27.8889V25.1111Z" fill="#0D0D0D"/> +<path d="M9 32.1111C9 31.4975 9 31 9.75 31H17.25C18 31 18 31.4975 18 32.1111V34.8889C18 35.5025 18 36 17.25 36H9.75C8.99999 36 9 35.5025 9 34.8889V32.1111Z" fill="#0D0D0D"/> +<path d="M31 32.1111C31 31.4975 31 31 31.75 31H39.25C40 31 40 31.4975 40 32.1111V34.8889C40 35.5025 40 36 39.25 36H31.75C31 36 31 35.5025 31 34.8889V32.1111Z" fill="#0D0D0D"/> +<path d="M20 32.1111C20 31.4975 20 31 20.75 31H28.25C29 31 29 31.4975 29 32.1111V34.8889C29 35.5025 29 36 28.25 36H20.75C20 36 20 35.5025 20 34.8889V32.1111Z" fill="#0D0D0D"/> +<path d="M20 46.1111C20 45.4975 20 45 20.75 45H28.25C29 45 29 45.4975 29 46.1111V48.8889C29 49.5025 29 50 28.25 50H20.75C20 50 20 49.5025 20 48.8889V46.1111Z" fill="#0D0D0D"/> +<path d="M42 32.1111C42 31.4975 42 31 42.75 31H50.25C51 31 51 31.4975 51 32.1111V34.8889C51 35.5025 51 36 50.25 36H42.75C42 36 42 35.5025 42 34.8889V32.1111Z" fill="#0D0D0D"/> +<path d="M53 32.1111C53 31.4975 53 31 53.75 31H61.25C62 31 62 31.4975 62 32.1111V34.8889C62 35.5025 62 36 61.25 36H53.75C53 36 53 35.5025 53 34.8889V32.1111Z" fill="#0D0D0D"/> +</svg> |