523 lines
22 KiB
QML
523 lines
22 KiB
QML
// 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: Application.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 implicitRowHeight(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 {
|
|
id: tvDelegate
|
|
|
|
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: tvDelegate.background
|
|
property: "color"
|
|
value: Application.styleHints.colorScheme === Qt.Dark
|
|
? tvDelegate.palette.highlight.darker(1.9)
|
|
: tvDelegate.palette.highlight.lighter(1.9)
|
|
when: tvDelegate.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
|
|
}
|
|
}
|