// 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 Spreadsheets ApplicationWindow { width: 960 height: 720 visible: true title: qsTr("Spreadsheets") header: HeaderToolBar { id: toolbar 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 ColumnHeaderView { id: horizontalHeaderView Layout.row: 0 Layout.column: 1 Layout.fillWidth: true implicitHeight: 36 clip: true syncView: tableView spreadSelectionModel: _spreadSelectionModel enableShowHideAction: tableView.hiddenColumnCount onHideRequested: (column) => tableView.hideColumn(column) onShowRequested: () => tableView.showHiddenColumns() onResetReorderingRequested: () => tableView.resetColumnReordering() } RowHeaderView { id: verticalHeaderView Layout.fillHeight: true implicitWidth: 50 clip: true syncView: tableView spreadSelectionModel: _spreadSelectionModel enableShowHideAction: tableView.hiddenRowCount onHideRequested: (row) => tableView.hideRow(row) onShowRequested: () => tableView.showHiddenRows() onResetReorderingRequested: () => tableView.resetRowReordering() } Item { Layout.fillWidth: true Layout.fillHeight: true ScrollView { id: scrollView anchors.fill: parent contentItem: TableView { id: tableView property int hiddenColumnCount: 0 property int hiddenRowCount: 0 clip: true columnSpacing: 2 rowSpacing: 2 boundsBehavior: Flickable.StopAtBounds selectionBehavior: TableView.SelectCells selectionMode: TableView.ExtendedSelection selectionModel: _spreadSelectionModel 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 (_spreadSelectionModel.hasSelection) { const source_index = _spreadSelectionModel.selectedIndexes[0] mimeDataProvider.sourceCell = cellAtIndex(source_index) mimeDataProvider.loadSelectedData() } else { const current_index = _spreadSelectionModel.currentIndex const current_cell = cellAtIndex(current_index) mimeDataProvider.sourceCell = current_cell mimeDataProvider.loadDataFromModel(current_cell, current_index, model) } } function cutToClipboard() { mimeDataProvider.reset() if (_spreadSelectionModel.hasSelection) { const source_index = _spreadSelectionModel.selectedIndexes[0] mimeDataProvider.sourceCell = cellAtIndex(source_index) mimeDataProvider.loadSelectedData() } else { const current_index = _spreadSelectionModel.currentIndex const current_cell = cellAtIndex(current_index) mimeDataProvider.sourceCell = current_cell mimeDataProvider.loadDataFromModel(current_cell, current_index, model) } for (let i = 0; i < mimeDataProvider.size(); ++i) { const cell_i = mimeDataProvider.cellAt(i) model.clearItemData(index(cell_i.y, cell_i.x)) } } function pasteFromClipboard() { visibleCellsConnection.blockConnection(true) const current_index = _spreadSelectionModel.currentIndex const current_cell = cellAtIndex(current_index) let target_cells = new Set() if (mimeDataProvider.size() === 1) { if (_spreadSelectionModel.hasSelection) { for (let i in _spreadSelectionModel.selectedIndexes) { const selected_index = _spreadSelectionModel.selectedIndexes[i] mimeDataProvider.saveDataToModel(0, selected_index, model) target_cells.add(tableView.cellAtIndex(selected_index)) } } else { const old_cell = mimeDataProvider.cellAt(0) let new_cell = Qt.point(old_cell.x, old_cell.y) new_cell.x += current_cell.x - mimeDataProvider.sourceCell.x new_cell.y += current_cell.y - mimeDataProvider.sourceCell.y mimeDataProvider.saveDataToModel(0, index(new_cell.y, new_cell.x), model) target_cells.add(new_cell) } } 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 mimeDataProvider.saveDataToModel(i, index(cell_i.y, cell_i.x), model) target_cells.add(cell_i) } } visibleCellsConnection.blockConnection(false) visibleCellsConnection.updateViewArea() } function resetColumnReordering() { clearColumnReordering() model.resetColumnMapping() } function resetRowReordering() { clearRowReordering() model.resetRowMapping() } 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: TableViewDelegate { implicitWidth: 90 implicitHeight: 36 leftPadding: 4 topPadding: 4 // This binding is used to avoid reimplementing whole background and // updates only the background.color when the color scheme has changed // for the target cells of drop event. Binding { target: background property: "color" value: Qt.styleHints.colorScheme === Qt.Dark ? palette.highlight.darker(1.9) : palette.highlight.lighter(1.9) when: model.highlight ?? false } } 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 (_spreadSelectionModel.hasSelection) model.clearItemData(_spreadSelectionModel.selectedIndexes) else model.clearItemData(_spreadSelectionModel.currentIndex) visibleCellsConnection.blockConnection(false) visibleCellsConnection.updateViewArea() } } Connections { id: visibleCellsConnection target: SpreadModel function onDataChanged(tl, br, roles) { updateViewArea() } // The model is updated, then the visible area needs to be updated as well. // Maybe some cells need to get the display data again // due to their data, if it's a formula. function updateViewArea() { for (let row = tableView.topRow; row <= tableView.bottomRow; ++row) { for (let column = tableView.leftColumn; column <= tableView.rightColumn; ++column) { SpreadModel.update(row, column) } } } // Blocks/unblocks the connection. This function is useful when // some actions may update a large amount of data in the model, // or may update a cell which affects other cells, // for example clipboard actions and drag/drop actions. // Block the connection, update the model, unblock the connection, // and the call the updateViewArea() function to update the view. 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 = _spreadSelectionModel.hasSelection if (!hadSelection) _spreadSelectionModel.select(index, ItemSelectionModel.Select) if (!_spreadSelectionModel.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) _spreadSelectionModel.clearSelection() hadSelection = false dragCell = Qt.point(-1, -1) } } } } DropArea { id: dropArea property point dropCell: Qt.point(-1, -1) // This property keeps the interactive value to restore it after // dragging is finished. The reason is that when the interactive // mode is true, it steals the events and prevents the drop area // from working. property bool restoreInteractiveValue: tableView.interactive anchors.fill: parent Drag.active: dragArea.drag.active enabled: dragArea.drag.active function startDragging() { restoreInteractiveValue = tableView.interactive tableView.interactive = false // block updating visible area visibleCellsConnection.blockConnection() } function stopDragging() { Drag.drop() // unblock update visible area visibleCellsConnection.blockConnection(false) visibleCellsConnection.updateViewArea() // now update visible area tableView.interactive = restoreInteractiveValue } function isDropPossible(dropCell) { 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) if (!index.valid) return false } return true } 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 if (!isDropPossible(dropCell)) { tableView.model.clearHighlight() return } tableView.model.clearItemData(_spreadSelectionModel.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() _spreadSelectionModel.clearSelection() const drop_index = tableView.index(dropCell.y, dropCell.x) _spreadSelectionModel.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 (!isDropPossible(cell)) { tableView.model.clearHighlight() 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 _spreadSelectionModel.selectedIndexes) { const old_index = _spreadSelectionModel.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: 12 height: 12 radius: 6 color: palette.highlight.lighter(1.4) border.width: 2 border.color: palette.base visible: SelectionRectangle.control.active } bottomRightHandle: Rectangle { width: 12 height: 12 radius: 6 color: palette.highlight.lighter(1.4) border.width: 2 border.color: palette.base visible: SelectionRectangle.control.active } } SpreadSelectionModel { id: _spreadSelectionModel behavior: SpreadSelectionModel.SelectCells } SpreadMimeDataProvider { id: mimeDataProvider property point sourceCell: Qt.point(-1, -1) function loadSelectedData() { for (let i in _spreadSelectionModel.selectedIndexes) { const index = _spreadSelectionModel.selectedIndexes[i] const cell = tableView.cellAtIndex(index) loadDataFromModel(cell, index, tableView.model) } } function resetProvider() { sourceCell = Qt.point(-1, -1) reset() } } HelpDialog { id: helpDialog anchors.centerIn: parent } }