QML Drag & drop in a grid - drag-and-drop

I have an item (a rectangle) which can be moved through the different positions of the grid (see code below).
The reference to decide whether the rectangle is in one cell or another is the top-left (x=0;y=0) of the rectangle. I want the reference to be the center of the rectangle. Any ideas?
Find the code below:
import QtQuick 2.5
import QtPositioning 5.5
import QtLocation 5.6
import QtGraphicalEffects 1.0
import QtQml 2.2
import QtQuick.Controls 1.0
Rectangle{
id: mainContainer
width:800; height:width
color: "red"
Grid {
id:gridA
anchors.centerIn: parent
width: parent.width; height:width;
columns: 2; rows: 2; spacing: 2
verticalItemAlignment: Grid.AlignHCenter
horizontalItemAlignment: Grid.AlignVCenter
Repeater {
id:cells
model: gridA.columns*gridA.rows
property int draggedItemIndex: -1
Rectangle {
width: gridA.width/gridA.columns; height: gridA.height/gridA.rows
color: "white"
DropArea {
id: dragTarget
property alias dropProxy: dragTarget
anchors.fill: parent
keys: [ "KEY_A" ]
Rectangle {
id: dropRectangle
anchors.fill: parent
states: [
State {
when: dragTarget.containsDrag
PropertyChanges { target: dropRectangle; color: "grey"}
PropertyChanges { target: cells; draggedItemIndex: index}
}
]
}
}
}
}
}
Rectangle{
id: icon
property var draggedIndex: 0
width: parent.width/3; height:width
color:"blue"
Drag.active: dragArea.drag.active
Drag.keys: [ "KEY_A" ]
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: icon
cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onReleased: {
console.log("Drag: " + cells.draggedItemIndex)
if (cells.draggedItemIndex != icon.draggedIndex) {
icon.draggedIndex = cells.draggedItemIndex
cells.draggedItemIndex = -1
}
icon.x = cells.itemAt(icon.draggedIndex).x
icon.y = cells.itemAt(icon.draggedIndex).y
}
}
}
}

You have the Drag-attached property, which has the hotSpot-property of type PointF.
Set this property to Qt.point(width / 2, height / 2)

Related

Donut chart with custom tooltip using Chartist in React

I am creating a donut chart using Chartist but unable to add a custom tooltip with resize slice on the mouse hover event. Chartist "Draw" method is rerender on changing the position of mouse on hover of the chart
I pasted my code as below:
/* eslint-disable no-unused-vars */
import React, {useState} from 'react'
import PropTypes from 'prop-types'
import ChartistGraph from 'react-chartist'
import Chartist from 'chartist'
import styles from './MyChart.scss'
import {formatNumber} from '../../../../utils/formatter'
import MyChartInfo from './MyChartInfo'
function MyChart ({ dataSeries, getDetailsTable }) {
const [changePercent, setChangePercent] = useState(0)
const [showToolTip, setShowToolTip] = useState(false)
const [myChartInfoDetails, setMyChartInfoDetails] = useState({sectorName: '', changePercent: ''})
const [toolTipPosition, setToolTipPosition] = useState({})
function setToolTip () {
const PopOverData = [myChartInfoDetails.sectorName || '', `% Return: ${myChartInfoDetails.changePercent || ''}`, '(Click to view top & worst performing equities)']
return (<MyChartInfo dataTable={PopOverData} style={toolTipPosition} />)
}
let pieOptions = {
width: '520px',
height: '520px',
donut: true,
donutWidth: 150,
donutSolid: false,
startAngle: 270,
showLabel: true,
chartPadding: 60,
labelOffset: 105,
labelDirection: 'explode',
labelInterpolationFnc: function (value) {
return value
}
}
let pieData = {
series: dataSeries && dataSeries.length > 0 ? dataSeries.map(item => Math.abs(item.value)) : [100],
labels: dataSeries && dataSeries.length > 0 ? dataSeries.map(item => item.name.concat('<br>', formatNumber(item.value, {precision: 2, negSign: true, posSign: true}))) : ['']
}
// eslint-disable-next-line no-unused-vars
const listner = {
created: (chart) => {
const chartPie = document.querySelectorAll('.ct-chart-donut .ct-slice-donut')
chartPie && chartPie.forEach((pie) => {
pie.addEventListener('mouseenter', (e) => {
e.stopPropagation()
pie.setAttribute('style', 'stroke-width: 170px;')
console.log('hover aa ', changePercent, Math.abs(pie.getAttribute('ct:value')))
setChangePercent(Math.abs(pie.getAttribute('ct:value')))
let topPosition = e.layerY + 30
let leftPosition = e.layerX + 30
setToolTipPosition({top: topPosition, left: leftPosition})
const myChartData = dataSeries.find(sector => Math.abs(sector.value) === Math.abs(pie.getAttribute('ct:value')))
setSectorDetails({sectorName: myChartData.name, changePercent: myChartData.value})
setShowToolTip(true)
})
pie.addEventListener('mouseleave', (e) => {
pie.setAttribute('style', 'stroke-width: 150px;')
setShowToolTip(false)
})
pie.addEventListener('click', (e) => {
const Id = dataSeries.find(sector => Math.abs(sector.value) === Math.abs(pie.getAttribute('ct:value'))).id.toString()
console.log('click ', Id, pie.getAttribute('ct:value'))
getDetailsTable(Id)
})
})
},
draw: (data) => {
// console.log('data', data)
if (data.type === 'slice') {
// Get the total path length in order to use for dash array animation
var pathLength = data.element._node.getTotalLength()
// Set a dasharray that matches the path length as prerequisite to animate dashoffset
data.element.attr({
'stroke-dasharray': pathLength + 'px ' + pathLength + 'px'
})
let noOfNonZeros = dataSeries.length > 0 && dataSeries.filter(e => e.value > 0).length
// Create animation definition while also assigning an ID to the animation for later sync usage
var animationDefinition = {
'stroke-dashoffset': {
id: 'anim' + data.index,
dur: 1,
from: -pathLength + 'px',
to: noOfNonZeros > 1 ? '2px' : '0px',
easing: Chartist.Svg.Easing.easeOutQuint,
// We need to use `fill: 'freeze'` otherwise our animation will fall back to initial (not visible)
fill: 'freeze'
}
}
// If this was not the first slice, we need to time the animation so that it uses the end sync event of the previous animation
if (data.index !== 0) {
animationDefinition['stroke-dashoffset'].begin = 'anim' + (data.index - 1) + '.end'
}
// We need to set an initial value before the animation starts as we are not in guided mode which would do that for us
data.element.attr({
'stroke-dashoffset': -pathLength + 'px'
})
// We can't use guided mode as the animations need to rely on setting begin manually
// See http://gionkunz.github.io/chartist-js/api-documentation.html#chartistsvg-function-animate
data.element.animate(animationDefinition, false)
if (dataSeries.length === 0) {
data.element._node.setAttribute('data-value', 'no-composition')
}
}
if (data.type === 'label') {
let textHtml = ['<p>', data.text, '</p>'].join('')
let multilineText = Chartist.Svg('svg').foreignObject(
textHtml,
{
style: 'overflow: visible;',
x: data.x - 30,
y: data.y - 30,
width: 90,
height: 30
},
'ct-label'
)
data.element.replace(multilineText)
}
}
}
return (
<div data-testid='gPieChart' id='performance_chart'>
{<ChartistGraph
data={pieData}
options={pieOptions}
type='Pie'
style={{ width: '100%', height: '100%' }}
className={styles.MyChart}
listener={listner}
/>
}
{showToolTip && setToolTip()}
</div>
)
}
MyChart.propTypes = {
dataSeries: PropTypes.array,
getDetailsTable: PropTypes.func
}
export default MyChart
Output
I want to add a custom tooltip with resize slice functionality on mouse hover event

Cannot set scale type to custom scale in ChartJS

I followed the ChartJS documentation for creating a custom scale:
https://www.chartjs.org/docs/master/samples/advanced/derived-axis-type.html
First I defined the custom scale as its own class extending the base Scale interface:
import { Scale, LinearScale, BubbleDataPoint, Chart, ChartTypeRegistry, ScatterDataPoint} from 'chart.js';
export default class SquareRootAxis extends Scale {
static defaults: {} = {};
_startValue: any;
_valueRange: number;
constructor(cfg: { id: string; type: string; ctx: CanvasRenderingContext2D; chart: Chart<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint)[], unknown>; }) {
super(cfg);
this._startValue = undefined;
this._valueRange = 0;
}
parse(raw: any, index: number) {
const value = LinearScale.prototype.parse.apply(this, [raw, index]);
return isFinite(value) && value > 0 ? value : null;
}
determineDataLimits() {
this.min = 0.5
this.max=31
}
buildTicks() {
const userDefinedTicks = [1, 2, 3, 5, 7, 10, 15, 20, 25, 30];
const ticks = [];
for (const tick of userDefinedTicks) {
ticks.push({
value: tick
});
}
return ticks;
}
/**
* #protected]
*/
configure() {
const start = this.min;
super.configure();
this._startValue = Math.pow(start, 1/2);
this._valueRange = Math.pow(this.max, 1/2) - Math.pow(start, 1/2);
}
getPixelForValue(value: number | undefined) {
if (value === undefined ) {
value = this.min;
}
return this.getPixelForDecimal(value === this.min ? 0 :
(Math.pow(value, 1/2) - this._startValue) / this._valueRange);
}
getValueForPixel(pixel: number) {
const decimal = this.getDecimalForPixel(pixel);
return Math.pow(this._startValue + decimal * this._valueRange, 2);
}
}
I then imported the custom scale Class and used it below:
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Scatter } from 'react-chartjs-2';
import SquareRootAxis from './sqaureRootAxis';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
SquareRootAxis
);
SquareRootAxis.id = 'squareRoot';
export function Chart(props: { chartData: ChartData }) {
return <Scatter
data={props.chartData}
options={{
responsive: true,
scales: {
x: {
type: 'squareRoot',
},
},
}}
/>;
}
But when I set my chart to the custom scale name, I get the following TS error:
Type '"squareRoot"' is not assignable to type '"time" | "linear" | "logarithmic" | "category" | "timeseries"'.ts(2322)
index.esm.d.ts(3618, 27): The expected type comes from property 'type' which is declared here on type '_DeepPartialObject<{ type: "time"; } & Omit<CartesianScaleOptions
It seems despite following the docs, the ChartJS scale type only recognizes the built-in scales. What am I doing wrong?
First in the sample code it looks like you have a typo in your import:
import SquareRootAxis from './sqaureRootAxis.
Then it seems you need to register the new scale, which I cannot see in your code, as follows:
chart.register(SquareRootAxis);
For some reason this is commented at the bottom of the code in your link, and is also explained here:
https://www.chartjs.org/docs/latest/developers/axes.html

QML Change Size of Dropitem on Drop

I want to change the size of a Rectangle in the QML drag and drop example on drop to the size of the drop area. But somehow the Rectangle will not render to the new size although it takes the width and height values.
DragTile.qml
import QtQuick 2.0
Item
{
id: rootItem
property int boxWidth: 0
property int boxHeight: 0
property Item dragParent
width: boxWidth
height: boxHeight
MouseArea
{
id: mouseArea
width: rootItem.boxWidth
height: rootItem.boxHeight
drag.target: msgTile
onReleased:
{
parent = msgTile.Drag.target !== null ? msgTile.Drag.target : rootItem
rootItem.boxHeight = parent.boxHeight
rootItem.boxWidth = parent.boxWidth
}
Rectangle
{
id: msgTile
color: "cyan"
width: mouseArea.width
height: mouseArea.height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
Drag.active: mouseArea.drag.active
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
//draging
states: State
{
when: mouseArea.drag.active
ParentChange { target: msgTile; parent: dragParent } // draw on top while draging
AnchorChanges { target: msgTile; anchors.verticalCenter: undefined; anchors.horizontalCenter: undefined }
}
}
}
}
The dragsource as well as the droparea take the size of a parent rectangle
in Main.qml
Rectangle
{
x: 100
y: 100
width: 100
height: 300
border.width: 1
DragSource
{
msgCount: 2
msgCapacity: 10
dragParent: topItem
}
}
Rectangle
{
x: 400
y: 100
width: 150
height: 300
border.width: 1
MsgDrop
{
msgCapacity: 12
}
}
The size of the droparea is a little bit larger, than the size of the dragtiles
Is there any order of rendering that I am missing?
I would be very happy for some advice.
Best regards

How to reorder children of QML Row (using drag and drop)?

Let's say, I have a QML Row which has some children objects (not necessarily of the same type). I would like to be able to reorder them at will. Bonus is, to be able to implement drag and drop behavior for the user. Here is a code sample, a Row with different components which I would like to reorder:
import QtQuick 2.5
import QtQuick.Controls 1.4
ApplicationWindow {
visible: true
width: 300
height: 300
Component {
id: rect
Rectangle {
color: "blue"
}
}
Component {
id: rectWithText
Rectangle {
property alias text: txt.text
color: "red"
Text {
id: txt
}
}
}
Row {
id: r
Component.onCompleted: {
rect.createObject(r,{"width": 60,"height":40})
rectWithText.createObject(r,{"width": 100,"height":40,text:"foo"})
rect.createObject(r,{"width": 40,"height":40})
}
}
}
In QtWidgets, there are some functions like layout->removeWidget(widget), layout->insertWidget(index,widget) and so on, but they seem to be missing in QML.
I found some examples that used ListView/GridView and model/delegate approach, but they usually can't handle different components and thus they are not really viable as a replacement for a Row.
Is there a way to do that in QML or am I out of luck?
This is a bit of a hack but works regardless. It does not require the use of a model.
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.1
Window {
visible: true
function shuffle(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
Column {
id: column
anchors.fill: parent
anchors.centerIn: parent
Button {
text: "shuffle"
onClicked: {
var array = [button1, button2, button3]
for (var a in array) { array[a].parent = null }
array = shuffle(array)
for (a in array) { array[a].parent = column }
}
}
Button {
id: button1
text: "I am button 1"
}
Button {
id: button2
text: "I am button 2 "
}
Button {
id: button3
text: "I am button 3"
}
}
}
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.1
Window {
visible: true
Component {
id: buttonComponent
Button { text: "I'm a button" }
}
Component {
id: labelComponent
Label { text: "I'm a label" }
}
property var buttonModel: [buttonComponent, labelComponent, buttonComponent, buttonComponent,labelComponent]
function shuffle(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
Column {
anchors.fill: parent
anchors.centerIn: parent
Repeater {
model: buttonModel
Loader {
sourceComponent: modelData
}
}
Button {
text: "shuffle"
onClicked: buttonModel = shuffle(buttonModel)
}
}
}
You can also use a ListView in the same way. That gives you even more flexibility when animating Items.

Is there any DatePicker control for Qt 5?

I'm writing my first QML/Javascript app for QtQuick 2.0.
I need to place a DatePicker control, but I haven't found any control like that under QtQuick.Controls -and nowhere, in fact-.
I'm starting to believe there is no way to call a 'native' DatePicker in QML. Do I have to implement one or there is exist one?
Just in case anyone else stumbles upon this, there is a Calendar element in QtQuick.Controls by now. This should make the implementation of such a Datepicker a lot easier: http://qt-project.org/doc/qt-5/qml-qtquick-controls-calendar.html
Well, I had to make my own control. It is called Datepicker.
It is intented to used in this way:
import QtQuick.Controls 1.1
ApplicationWindow {
id: main
Datepicker {
id: myDate
activeWindow: main
width: 200
}
}
It asumes you are using it from a Window object, and needs the parent reference to show the datepicker in the correct position (it shows a calendar in a new window).
You can download the source code from:
https://bitbucket.org/camolin3/datepicker
This is the first version and need a lot of polish to be ready, but is a start point.
**Code for DatePicker. Try this one **
*TextField {
id: textDate
x: 10
y: 42
width: 175
height: 33
placeholderText: qsTr("Text Field")
text:Qt.formatDate(cal.selectedDate, "dd-MM-yyyy")
font.bold: true
font.family:"times new roman"
font.pointSize: 12
}
Button {
id: button
x: 198
y: 42
width: 25
height: 29
Image {
x: -4
y: -4
id: img
width: 36
height: 44
source: "/Images/calendar-512.png"
}
onClicked:{
cal.visible=true
}
}
Calendar{
id:cal
x: 10
y: 82
width: 220
height: 205
visible: false
selectedDate: new Date()
onClicked: {
textDate.text=Qt.formatDate(cal.selectedDate, "dd-MM-yyyy");
cal.visible=false
}
}*
I wrote another date picker, relying purely on the JavaScript Date object as follows. It adheres to material design.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.15
import QtQuick.Controls.Material.impl 2.15
Pane {
id: control
property int radius: 2
background: Rectangle {
color: control.Material.background
radius: control.Material.elevation > 0 ? control.radius : 0
layer.enabled: control.enabled && control.Material.elevation > 0
layer.effect: ElevationEffect {
elevation: control.Material.elevation
}
}
}
And then, here is it
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls.Material 2.15
Rectangle {
id: root
property int month: selectedDate.getMonth()
property int year: selectedDate.getFullYear()
property int day: selectedDate.getDate()
property int weekday: selectedDate.getDay()
property int daysInMonth: new Date(year, month + 1, 0).getDate()
property date selectedDate: new Date()
property int _start_weekday: new Date(year, month, 1).getDay()
implicitWidth: layout.width
implicitHeight: layout.height
color: "transparent"
ColumnLayout {
id: layout
Rectangle {
id: title
implicitHeight: 40
Layout.fillWidth: true
color: "transparent"
Text {
anchors.fill: parent
anchors.leftMargin: 8
text: selectedDate.toLocaleDateString(Qt.locale(), "ddd, MMM d")
font.pixelSize: 40
}
}
Rectangle {
color: "transparent"
implicitHeight: 28
Layout.fillWidth: true
}
Rectangle {
id: controls
color: "transparent"
implicitHeight: 24
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 16
Layout.alignment: Qt.AlignHCenter
Text {
text: selectedDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
font.pixelSize: 16
}
RoundButton {
id: monthButton
contentItem: MIcon {
icon: "expand_more"
color: "black"
size: 16
}
Material.elevation: 0
onClicked: {
monthMenu.open()
}
Menu {
id: monthMenu
Repeater {
model: 12
MenuItem {
text: new Date(2020, index, 1).toLocaleDateString(Qt.locale(), "MMMM")
onTriggered: {
set_month(index)
monthMenu.close()
}
}
}
}
}
// 2 button to change month <>
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: 8
RoundButton {
Material.background: 'transparent'
contentItem: MIcon {
icon: 'navigate_before'
color: 'black'
size: 16
}
onClicked: {
set_month(month - 1)
}
}
RoundButton {
Material.background: 'transparent'
contentItem: MIcon {
icon: 'navigate_next'
color: 'black'
size: 16
}
onClicked: {
set_month(month + 1)
}
}
}
}
}
Rectangle {
color: "transparent"
implicitHeight: 16
Layout.fillWidth: true
}
Rectangle {
color: Material.accent
implicitHeight: 1
Layout.fillWidth: true
}
// Sunday - Saturday
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
spacing: 4
Repeater {
model: 7
Rectangle { // just for spacing
width: 40
height: 40
color: "transparent"
Text {
anchors.centerIn: parent
Layout.fillWidth: true
text: new Date(2020, 0, index - 2).toLocaleDateString(Qt.locale(), "ddd").slice(0, 1)
font.pixelSize: 16
horizontalAlignment: Text.AlignHCenter
}
}
}
}
// calendar
GridLayout {
id: grid
columns: 7
rows: 6
columnSpacing: 4
rowSpacing: 4
Repeater {
model: 42
delegate: Rectangle {
color: default_color()
radius: 20
border.width: 1
border.color: is_selected() ? Material.accent : "transparent"
width: 40
height: 40
Text {
anchors.centerIn: parent
text: get_day()
color: in_current_month() ? 'black' : 'gray'
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
cursorShape = Qt.PointingHandCursor
color = Material.color(Material.accent, Material.Shade200)
}
onExited: {
cursorShape = Qt.ArrowCursor
color = default_color()
}
onClicked: {
var _day = get_day()
if (!in_current_month()){
if (index < _start_weekday) {
set_month(month - 1)
} else {
set_month(month + 1)
}
}
set_day(_day)
}
}
function default_color() {
return 'transparent'
}
function in_current_month() {
return index >= _start_weekday && (index - _start_weekday) < daysInMonth
}
function is_selected() {
return day == get_day() && in_current_month()
}
function get_day() {
var this_day = index - _start_weekday + 1
if (this_day > daysInMonth) {
return this_day - daysInMonth
} else if (this_day < 1) {
return new Date(year, month, 0).getDate() + this_day
} else {
return this_day
}
}
}
}
}
}
function set_month(month) {
var days_in = new Date(year, month + 1, 0).getDate()
var new_day = Math.min(day, days_in)
selectedDate = new Date(year, month, new_day)
}
function set_day(day) {
day = Math.min(day, daysInMonth)
selectedDate = new Date(year, month, day)
}
}