qtdeclarative/examples/quickcontrols/spreadsheets/Spreadsheets/Main.qml

840 lines
32 KiB
QML
Raw Normal View History

// 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 containsDrag
readonly property real cellPadding: 8
readonly property bool containsMenu: columnMenu.column === column
implicitWidth: horizontalTitle.implicitWidth + (cellPadding * 2)
implicitHeight: Math.max(horizontalHeaderView.height,
horizontalTitle.implicitHeight + (cellPadding * 2))
border {
width: containsDrag || containsMenu ? 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.leftMargin: horizontalHeaderDelegate.cellPadding / 2
anchors.rightMargin: 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.topMargin: verticalHeaderDelegate.cellPadding / 2
anchors.bottomMargin: 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)
let target_cells = new Set()
if (mimeDataProvider.size() === 1) {
if (selectionModel.hasSelection) {
for (let i in selectionModel.selectedIndexes) {
const selected_index = selectionModel.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)
}
}
if (mimeDataProvider.includeCutData) {
for (let i = 0; i < mimeDataProvider.size(); ++i) {
const cell_i = mimeDataProvider.cellAt(i)
if (!target_cells.has(cell_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
}
}