Qt QML click under the bounding box - c++

I'm trying to do the following.
So far the only solution I found is to create my button from C++ and to use setMask. Is there any other way in order to achieve the same results but just using QML?
My shape button:
import QtQuick 2.0
import QtQuick.Shapes 1.14
Item {
id: sideButtonID
property alias mouseX: mouseArea.mouseX
property alias mouseY: mouseArea.mouseY
readonly property bool pressed: containsMouse && mouseArea.pressed
property point topLeftOffset: Qt.point(0, 0)
property point topRightOffset: Qt.point(0, 0)
property point bottomRightOffset: Qt.point(0, 0)
property point bottomLeftOffset: Qt.point(0, 0)
property var colorPressed: "green"
property var colorSelected: "red"
property var colorUnselected: "darkorange"
signal clicked
property var coordinate: generateButtonShapeCoordinates(x,y,width,height)
property point topLeft: coordinate.topLeft
property point topRight: coordinate.topRight
property point bottomRight: coordinate.bottomRight
property point bottomLeft: coordinate.bottomLeft
function generateButtonShapeCoordinates(x, y, width, height){
var topLeft = Qt.point(x-width/2 - topLeftOffset.x, y-height-topLeftOffset.y)
var topRight = Qt.point(x+width/2-topRightOffset.x, y-height-topRightOffset.y)
var bottomRight = Qt.point(x+width/2-bottomRightOffset.x, y-bottomRightOffset.y)
var bottomLeft = Qt.point(x - width/2-bottomLeftOffset.x, y-bottomLeftOffset.y)
return {
topLeft : topLeft,
topRight : topRight,
bottomRight : bottomRight,
bottomLeft : bottomLeft
};
}
function inside(point, polygon) {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
var x = point[0], y = point[1];
var inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0], yi = polygon[i][1];
var xj = polygon[j][0], yj = polygon[j][1];
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
property bool containsMouse: {
var topLeft = Qt.point(0-topLeftOffset.x, 0-topLeftOffset.y)
var topRigh = Qt.point(width-topRightOffset.x, 0-topRightOffset.y)
var bottomRight = Qt.point(width-bottomRightOffset.x, height-bottomRightOffset.y)
var bottomLeft = Qt.point(0-bottomLeftOffset.x, height-bottomLeftOffset.y)
var polygon = [[topLeft.x,topLeft.y],[topRigh.x,topRigh.y],[bottomRight.x,bottomRight.y],[bottomLeft.x,bottomLeft.y]]
var point = [mouseX, mouseY]
return inside(point, polygon)
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: if (sideButtonID.containsMouse) sideButtonID.clicked()
}
Shape {
antialiasing: true
vendorExtensionsEnabled: true
asynchronous: true
anchors.centerIn: parent
ShapePath {
id: shapepathId
strokeWidth: -1
fillColor: sideButtonID.pressed ? sideButtonID.colorPressed : (sideButtonID.containsMouse ? sideButtonID.colorSelected : sideButtonID.colorUnselected)
startX: sideButtonID.topLeft.x; startY: sideButtonID.topLeft.y
PathLine { x: sideButtonID.topRight.x; y: sideButtonID.topRight.y }
PathLine { x: sideButtonID.bottomRight.x; y: sideButtonID.bottomRight.y }
PathLine { x: sideButtonID.bottomLeft.x; y: sideButtonID.bottomLeft.y }
}
Component.onCompleted: {
console.log("TopLeft: "+sideButtonID.topLeft)
console.log("TopRight: "+sideButtonID.topRight)
console.log("BottomLeft: "+sideButtonID.bottomLeft)
console.log("BottomRight: "+sideButtonID.bottomRight)
}
}
}
The usage of my button:
SideButtons {
z: 100
id: sideButtonID
width: 130
height: 248
anchors.verticalCenterOffset: 9
anchors.horizontalCenterOffset: -255
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
bottomRightOffset: Qt.point(100,0)
onClicked: {
print("X: "+mouseX)
print("Y: "+mouseY)
print("clicked")
}
}
Some of the options that I've heard so far but there are not good in my case.
using an image with a transparent background (sure but I'm going to have the same problem)
creating a C++ class which will inherit QAbstractButton (I would prefer to avoid as much as I can usage of C++ in the Ui side)
usage of a Shape in combination with MouseArea (I'm doing this already but I am encountering the bounding box problem.
Is there any other way which I don't know yet about?

Related

Phantom Button in SpriteKit Scene

I have a simple game in which players get three rounds to achieve the highest score . The gameScene exists inside a SwiftUI View and is created like this:
var gameScene: SKScene {
let scene = NyonindoGameScene(
size: CGSize(
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height
)
)
scene.viewModel = self.viewModel
scene.scaleMode = .aspectFill
return scene
}
It is called from the body of the view (inside a GeometryReader inside a ZStack) using SpriteView(). The code was working great until I tested on a new iPhone 13, which gave me all kinds of quirky and unexpected behaviors. I won't elaborate on them now as I have fixed most, but I am still left with a "phantom" start button. It is designed to display different text depending on the round being played (viz.: "Start," "Try Again," "Last Chance") using a var that is accurately counting rounds. However, I get this at the end of the first round:
When this Frankenstein button gets tapped, the new round begins. HOWEVER, SKPhysicsContactDelegate didBegin(_:) does not get called and collisions are ignored. (In my general bafflement here, I don't know if this is a separate issue or one that will go away when I solve the overlapping button problem.)
In any case, here is the relevant code for the startButton:
func addStartButton(text: String) {
startButton.removeFromParent() // added as one of many failed remedies
let startButtonLabel = SKLabelNode(text: text)
startButtonLabel.fontName = SKFont.bold
startButtonLabel.fontSize = 40.0
startButtonLabel.fontColor = UIColor.white
startButtonLabel.position = CGPoint(x: 0, y: -12)
startButton.position = CGPoint(x:self.frame.midX, y:self.frame.midY)
startButton.zPosition = 3
startButton.addChild(startButtonLabel)
addChild(startButton)
}
The initial start button is called like this in didMove(to view: SKView):
if attempts == 0 {
addStartButton(text: "Start")
}
And the buttons for the second and third round are called inside a gameOver() function like this:
if attempts == 1 {
startButton.removeFromParent() // again, this is overkill as it gets removed before this...
let text: String = "Try Again!"
addStartButton(text: text)
}
if attempts == 2 {
startButton.removeFromParent()
let text: String = "Last Chance!"
addStartButton(text: text)
}
I originally had a switch statement instead of the two if statements, but that generated the same problem. Print statements to the console suggest that only one button is being called for each round, but the results suggest something different.
Any clues? (Apologies if I haven't provided enough code for an assessment.)
why are you removing the button? change it's label:
class TTESTGameScene: SKScene {
var allBoxes: [SKSpriteNode] = []
var startButton: SKShapeNode = SKShapeNode(rect: CGRect(x: 0, y: 0, width: 200, height: 43), cornerRadius: 20)
override func didMove(to view: SKView) {
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
view.allowsTransparency = true
self.backgroundColor = .clear
view.alpha = 1.0
view.isOpaque = true
view.backgroundColor = SKColor.clear.withAlphaComponent(0.0)
let nextButton = SKShapeNode(rect: CGRect(x: 0, y: view.frame.maxY - 40, width: 66, height: 33), cornerRadius: 20)
nextButton.fillColor = .yellow
nextButton.name = "nextButton"
let nextLabel = SKLabelNode(text: "")
nextLabel.fontSize = 40.0
nextLabel.fontColor = UIColor.white
nextLabel.position = CGPoint(x: 0, y: -12)
nextButton.addChild(nextLabel)
addChild(nextButton)
startButton.fillColor = .red
startButton.name = "startButton"
let startButtonLabel = SKLabelNode(text: "000")
startButtonLabel.fontSize = 30.0
startButtonLabel.fontColor = UIColor.white
startButtonLabel.horizontalAlignmentMode = .center
startButtonLabel.position = CGPoint(x: startButton.frame.size.width/2, y: 10)
startButtonLabel.name = "startButtonLabel"
startButton.position = CGPoint(x:self.frame.midX - startButton.frame.size.width/2, y:self.frame.midY)
startButton.zPosition = 3
startButton.addChild(startButtonLabel)
addChild(startButton)
}
var attempts: Int = 0
func nextLevel() {
//startButton.removeFromParent() // added as one of many failed remedies
var text = ""
if attempts == 0 {
text = "Start"
}
else if attempts == 1 {
text = "Try Again!"
}
else if attempts == 2 {
text = "Last Chance!"
}
if let label = startButton.childNode(withName: "//startButtonLabel") as? SKLabelNode {
label.text = text
attempts += 1
attempts = attempts > 2 ? 0:attempts
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self.view)
let sceneTouchPoint = self.convertPoint(fromView: location)
let touchedNode = self.atPoint(sceneTouchPoint)
print(touchedNode.name)
if touchedNode.name == "nextButton" {
nextLevel()
}
}
}
// A sample SwiftUI creating a GameScene and sizing it
// at 300x400 points
struct TTESTContentView: View {
var scene: SKScene {
let scene = TTESTGameScene()
scene.size = CGSize(width: 300, height: 400)
scene.scaleMode = .aspectFill
return scene
}
var body: some View {
SpriteView(scene: scene)
.frame(width: 300, height: 400)
//.ignoresSafeArea()
}
}
struct ContentViewTest_Previews: PreviewProvider {
static var previews: some View {
TTESTContentView()
}
}

how to make infinite background in spriteKit swift 4

I try to make an endless background through the nodes, but the background has not become infinite and is interrupted, the third background is not yet shown. After the first show, the number of nodes in the scene grows, how can this be fixed?
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var bgNode: SKNode!
var overlay: SKNode!
var overlayWidth: CGFloat!
//var viewSize: CGSize!
var levelPositionX: CGFloat = 0.0
//var speed: CGFloat = 5.5
override func didMove(to view: SKView) {
setupNode()
//viewSize = CGSize(width: frame.size.width, height:
frame.size.height )
}
func setupNode() {
let worldNode = childNode(withName: "World")!
bgNode = worldNode.childNode(withName: "Background")!
overlay = bgNode.childNode(withName: "Overlay")!.copy() as!
SKNode
overlayWidth = overlay.calculateAccumulatedFrame().width
}
func createBackgroundOverlay() {
let backgroundOverlay = overlay.copy() as! SKNode
backgroundOverlay.position = CGPoint(x: 0.0, y: 0.0)
bgNode.addChild(backgroundOverlay)
levelPositionX += overlayWidth
}
func update() {
bgNode.position.x -= 5
if bgNode.position.x <= -self.frame.size.width {
bgNode.position.x = self.frame.size.width * 2
createBackgroundOverlay()
}
}
override func update(_ currentTime: TimeInterval) {
update()
}
In my endless runner game, I have implemented an endless background and a ground(or floor) much similar to your app. Below I shall discuss the steps i have used in my game.
Step 1: In your GameScene.swift file add these variables.
var backgroundSpeed: CGFloat = 80.0 // speed may vary as you like
var deltaTime: TimeInterval = 0
var lastUpdateTimeInterval: TimeInterval = 0
Step 2: In GameScene file, make setUpBackgrouds method as follows
func setUpBackgrounds() {
//add background
for i in 0..<3 {
// add backgrounds, my images were namely, bg-0.png, bg-1.png, bg-2.png
let background = SKSpriteNode(imageNamed: "bg-\(i).png")
background.anchorPoint = CGPoint.zero
background.position = CGPoint(x: CGFloat(i) * size.width, y: 0.0)
background.size = self.size
background.zPosition = -5
background.name = "Background"
self.addChild(background)
}
for i in 0..<3 {
// I have used one ground image, you can use 3
let ground = SKSpriteNode(imageNamed: "Screen.png")
ground.anchorPoint = CGPoint(x: 0, y: 0)
ground.size = CGSize(width: self.size.width, height: ground.size.height)
ground.position = CGPoint(x: CGFloat(i) * size.width, y: 0)
ground.zPosition = 1
ground.name = "ground"
self.addChild(ground)
}
}
Step 3: Now we have to capture timeIntervals from update method
override func update(_ currentTime: TimeInterval) {
if lastUpdateTimeInterval == 0 {
lastUpdateTimeInterval = currentTime
}
deltaTime = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime
}
Step 4: Here comes the most important part, moving our backgrounds and groungFloor by enumerating child nodes. Add these two methods in GameScene.swift file.
func updateBackground() {
self.enumerateChildNodes(withName: "Background") { (node, stop) in
if let back = node as? SKSpriteNode {
let move = CGPoint(x: -self.backgroundSpeed * CGFloat(self.deltaTime), y: 0)
back.position += move
if back.position.x < -back.size.width {
back.position += CGPoint(x: back.size.width * CGFloat(3), y: 0)
}
}
}
}
func updateGroundMovement() {
self.enumerateChildNodes(withName: "ground") { (node, stop) in
if let back = node as? SKSpriteNode {
let move = CGPoint(x: -self.backgroundSpeed * CGFloat(self.deltaTime), y: 0)
back.position += move
if back.position.x < -back.size.width {
back.position += CGPoint(x: back.size.width * CGFloat(3), y: 0)
}
}
}
}
Step 5: At this point you should get this error:"Binary operator '+=' cannot be applied to two 'CGPoint' operands" in updateBackground and updateGroundMovement methods.
Now we need to implement operator overloading to resolve this problem. Create a new Swift File and name it Extensions.swift and then implement as follows:
// Extensions.swift
import CoreGraphics
import SpriteKit
public func + (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
public func += (left: inout CGPoint, right: CGPoint) {
left = left + right
}
Step 6: call setUpBackgrounds method in didMove(toView:)
override func didMove(to view: SKView) {
setUpBackgrounds()
}
Step 7: Finally call the updateBackground and updateGroundMovement methods in update(_ currentTime) method. updated code is given below:
override func update(_ currentTime: TimeInterval) {
if lastUpdateTimeInterval == 0 {
lastUpdateTimeInterval = currentTime
}
deltaTime = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime
//MARK:- Last step:- add these methods here
updateBackground()
updateGroundMovement()
}

Zoom Qt3D's Camera

How to implement two-finger pinch gesture handling for Qt3D Camera's FOV?
There are FirstPersonCameraController and OrbitCameraController camera controllers which handles mouse/touch pad events. The latter even have zoomLimit property, but its meaning is not what I need to zoom scene (from inside a cubemap, camera position is fixed to (0, 0, 0)). I use the former one. It correctly handles mouse drag and single-finger touch events, but not deals with two-finger pinch-like gesture.
Can I customize in simple way PinchArea to interact with Qt3D's Camera? Or Qt Quick's API are orthogonal in this sense to Qt3D's API?
Use the pinchUpdated event of PinchArea to find information about the Pinch: according to the doc
The pinch parameter provides information about the pinch gesture,
including the scale, center and angle of the pinch. These values
reflect changes only since the beginning of the current gesture, and
therefore are not limited by the minimum and maximum limits in the
pinch property.
So you should be able to do something like:
Camera {
id: myCamera
}
PinchArea {
onPinchUpdated: {
myCamera.fieldOfView = pinch.scale*someFactor
}
}
This can be done in any custom QML you have that has access to the Pinch and the Camera.
If it is a custom script, you can alwazs pass the camera as a property
property Camera myCamera
I ended up with combination of PinchArea and MouseArea:
import QtQml 2.2
import QtQuick 2.9
import QtQuick.Scene3D 2.0
import Qt3D.Core 2.9
import Qt3D.Extras 2.9
import Qt3D.Render 2.9
import Qt3D.Input 2.1
import Qt3D.Logic 2.0
import PanoEntity 1.0
import Utility 1.0
Item {
property url panorama
onPanoramaChanged: {
if (panorama.toString().length !== 0) {
if (panoEntity.setPanorama(panorama)) {
basicCamera.position = Qt.vector3d(0.0, 0.0, 0.0)
basicCamera.upVector = Qt.vector3d(0.0, 0.0, 1.0)
basicCamera.viewCenter = panoEntity.pano.dir
pinchArea.updateFov(defaultFov)
}
}
}
property Image previewImage
property real fovMin: 20.0
property real defaultFov: 60.0
property real fovMax: 90.0
property real sensitivity: 1.5
property real pinchSensitivity: 0.5
Scene3D {
id: scene3d
anchors.fill: parent
aspects: ["render", "input", "logic"]
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio // basicCamera.aspectRatio = width / height
multisample: true
Entity {
Camera {
id: basicCamera
projectionType: CameraLens.PerspectiveProjection
nearPlane: 0.1
farPlane: 2.0 * Math.SQRT2
}
RenderSettings {
id: renderSettings
renderPolicy: scene3d.visible ? RenderSettings.Always : RenderSettings.OnDemand
ForwardRenderer {
camera: basicCamera
clearColor: "transparent"
}
}
InputSettings {
id: inputSettings
}
components: [renderSettings, inputSettings]
PanoEntity {
id: panoEntity
MemoryBarrier {
waitFor: MemoryBarrier.All
}
}
}
}
PinchArea {
id: pinchArea
anchors.fill: parent
function updateFov(newFov) {
if (newFov > fovMax) {
newFov = fovMax
} else if (newFov < fovMin) {
newFov = fovMin
}
var eps = 1.0 / 60.0
if (Math.abs(basicCamera.fieldOfView - newFov) > eps) {
basicCamera.fieldOfView = newFov
zoomer.fov = newFov
}
}
function updatePinch(pinch) {
updateFov(basicCamera.fieldOfView * (1.0 + (pinch.previousScale - pinch.scale) * pinchSensitivity))
}
onPinchUpdated: {
updatePinch(pinch)
}
onPinchFinished: {
updatePinch(pinch)
}
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
property point startPoint
function updateView(mouse) {
basicCamera.pan((startPoint.x - mouse.x) * sensitivity * basicCamera.fieldOfView / width, Qt.vector3d(0.0, 0.0, 1.0))
basicCamera.tilt((mouse.y - startPoint.y) * sensitivity * basicCamera.fieldOfView / height)
startPoint = Qt.point(mouse.x, mouse.y)
}
onPressed: {
startPoint = Qt.point(mouse.x, mouse.y)
}
onPositionChanged: {
updateView(mouse)
}
onReleased: {
updateView(mouse)
}
}
}
Zoomer {
id: zoomer
fovMax: parent.fovMax
defaultFov: parent.defaultFov
fovMin: parent.fovMin
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 45
onFovChanged: {
pinchArea.updateFov(fov);
}
}
Shortcut {
context: Qt.WindowShortcut
enabled: scene3d.visible
sequence: StandardKey.Save
onActivated: {
panoEntity.pano.dir = basicCamera.viewCenter
panoEntity.pano.up = basicCamera.upVector
panoEntity.pano.version = 7
if (!panoEntity.updatePanorama()) {
console.log("Unable to update panorama %1".arg(panorama))
} else if (previewImage && Utility.fileExists(previewImage.source)) {
scene3d.grabToImage(function(grabResult) {
if (!grabResult.saveToFile(Utility.toLocalFile(previewImage.source))) {
console.log("Unable save preview to file: %1".arg(previewImage.source))
}
}, Qt.size(512, 512))
}
}
}
}

draw animated circle in swift 3

Refrence : https://stackoverflow.com/a/26578895/6619234
how to erase and redraw circle on click evnet?
i tried to call addCircleView method on click event but circle is overlapping every time.
class CircleClosing: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath : UIBezierPath!
circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 5)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 20.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
}
override func layoutSubviews()
{
let frame = self.layer.bounds
circleLayer.frame = frame
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder)
{ super.init(coder: aDecoder) }
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
}
Add in your subview
func addCircleView() {
var circleView : CircleClosing!
let diceRoll = CGFloat(510) //CGFloat(Int(arc4random_uniform(7))*50)
let diceRolly = CGFloat(70)
let circleWidth = CGFloat(40)
let circleHeight = circleWidth
// Create a new CircleView
circleView = CircleClosing(frame:CGRect(x:diceRoll,y: diceRolly,width: circleWidth,height: circleHeight) )
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 20.0)
}
Thanks in Advance
var circleView : CircleClosing!
func addCircleView() {
let diceRoll = CGFloat(510) //CGFloat(Int(arc4random_uniform(7))*50)
let diceRolly = CGFloat(70)
let circleWidth = CGFloat(40)
let circleHeight = circleWidth
//Add this line here to remove from superview
circleView.removeFromSuperview()
circleView = CircleClosing(frame:CGRect(x:diceRoll,y: diceRolly,width: circleWidth,height: circleHeight) )
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 20.0)
}

QML GridView doesn't reflect changes in C++ model

I follow the Using C++ Models with Qt Quick Views and AbstractItemModel example project in Qt 5.5. Screen class is the model data; ScreenManager is derived from QAbstractListModel acting as the model source in QML GridView. In the GridView, I also add a MouseArea and some animation in delegate to implement items drag and drop.
My expected result is when dragging a screen around, whenever it's on top of another screen, the target screen will be moved to the last position of dragged screen until button release to drop the dragged screen. All the movement should use the animation declared in QML.
Now it could show screens correctly and recognize the selected screen. But it fails to swap the dragged and dropped screens. The underlying list has swapped the element but it doesn't reflect to the view.
A similar question is this, I try to use beginMoveRows and endMoveRows. But my program crashes on calling endMoveRows. layoutChanged would rearrange the whole model. Because I have animation on grid item movement. layoutChanged would cause non affected screens shifting from top left to their original position.
Edit: endMoveRows crash is caused by invalid operation described here.
Edit: the GridView has a 3 * 5 items. Since it's in a QList, I assume I only need to move on rows.
Screen.h
class Screen
{
public:
Screen(QString name, int gridId, bool active = false);
QString name() const;
int gridId() const;
bool active() const;
void setActive(bool a);
private:
QString m_name;
int m_gridId;
bool m_active;
};
ScreenManager.h
#include "Screen.h"
#include <QAbstractListModel>
class ScreenManager : public QAbstractListModel
{
Q_OBJECT
public:
enum ScreenRoles {
NameRole = Qt::UserRole + 1,
GridIDRole,
ActiveRole
};
ScreenManager();
void addScreen(const Screen& screen);
int rowCount(const QModelIndex& parent = QModelIndex()) const;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
Q_INVOKABLE int getScreenGridId(int index);
Q_INVOKABLE bool getScreenActive(int index);
Q_INVOKABLE void swapScreens(int index1, int index2);
protected:
QHash<int, QByteArray> roleNames() const;
private:
QList<Screen> m_screens;
};
ScreenManager.cpp
#include "ScreenManager.h"
#include "Screen.h"
ScreenManager::ScreenManager()
{
int index = 0;
for (;index < 15; index++) {
addScreen(Screen(QString ("Screen%1").arg(index), index, true));
}
}
void ScreenManager::addScreen(const Screen& screen)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_screens << screen;
endInsertRows();
}
int ScreenManager::rowCount(const QModelIndex& parent) const {
Q_UNUSED(parent);
return m_screens.count();
}
QVariant ScreenManager::data(const QModelIndex& index, int role) const
{
if (index.row() < 0 || index.row() >= m_screens.count())
return QVariant();
const Screen& screen = m_screens[index.row()];
if (role == NameRole)
return screen.name();
else if (role == GridIDRole)
return screen.gridId();
else if (role == ActiveRole)
return screen.active();
return QVariant();
}
QHash<int, QByteArray> ScreenManager::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[GridIDRole] = "gridId";
roles[ActiveRole] = "active";
return roles;
}
int ScreenManager::getScreenGridId(int index)
{
return m_screens.at(index).gridId();
}
bool ScreenManager::getScreenActive(int index)
{
return m_screens.at(index).active();
}
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
bool r = beginMoveRows(QModelIndex(), min, min, QModelIndex(), max);
r = beginMoveRows(QModelIndex(), max-1, max-1, QModelIndex(), min);
m_screens.swap(index1, index2);
endMoveRows();
}
QML GridView related code
ScreenManager {
id : screenManager
}
GridView {
id: gridView
x: 82
y: 113
width: cellWidth * 5
height: cellHeight * 3
clip: true
anchors.bottom: parent.bottom
anchors.bottomMargin: 70
anchors.topMargin: 100
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
flickableDirection: Flickable.HorizontalAndVerticalFlick
cellWidth: 90; cellHeight: 90;
property bool ignoreMovementAnimation: true
MouseArea {
id: gridViewMouseArea
hoverEnabled: true
preventStealing : true
property int currentGridId: -1
property int preIndex
property int index: gridView.indexAt(mouseX, mouseY)
anchors.fill: parent
onPressAndHold: {
currentGridId = screenManager.getScreenGridId(index)
preIndex = index
preIndexBackup = preIndex
gridView.ignoreMovementAnimation = false
}
onReleased: currentGridId = -1
onPositionChanged: {
if (currentGridId != -1 && index != -1 && index != preIndex) {
if (screenManager.getScreenActive(index)) {
screenManager.swapScreens(preIndex, index)
preIndex = index
}
}
}
}
model: screenManager
delegate: Component {
Item {
id: gridViewDelegate
width: gridView.cellWidth; height: gridView.cellHeight
Image {
id: itemImage
parent: gridView
x: gridViewDelegate.x + 5
y: gridViewDelegate.y + 5
width: gridViewDelegate.width - 10
height: gridViewDelegate.height - 10;
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/res/image/screen_icon.png"
visible: active
Text {
text: name
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
anchors.fill: parent;
border.color: "grey"
border.width: 6
color: "transparent"; radius: 5
visible: itemImage.state === "active"
}
// specify the movement's animation for non-active screen icons
Behavior on x {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
Behavior on y {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
// specify the shaking animation for non-active screen icons when hold one icon
SequentialAnimation on rotation {
NumberAnimation { to: 2; duration: 60 }
NumberAnimation { to: -2; duration: 120 }
NumberAnimation { to: 0; duration: 60 }
running: gridViewMouseArea.currentGridId != -1 && itemImage.state !== "active"
loops: Animation.Infinite
alwaysRunToEnd: true
}
// specify the active screen's new position and size
states: State {
name: "active"
when: gridViewMouseArea.currentGridId == gridId
PropertyChanges {
target: itemImage
x: gridViewMouseArea.mouseX - width/2
y: gridViewMouseArea.mouseY - height/2
scale: 0.5
z: 10
}
}
// specify the scale speed for the active screen icon
transitions: Transition {
NumberAnimation { property: "scale"; duration: 200}
}
}
}
}
}
main.cpp
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qmlRegisterType<ScreenManager>("com.gui", 1, 0, "ScreenManager");
QQmlApplicationEngine engine(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
It turns out I need to deal some corner cases to avoid no-op or invalid move operation.
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
m_screens.swap(index1, index2);
beginMoveRows(QModelIndex(), max, max, QModelIndex(), min);
endMoveRows();
if (max - min > 1) {
beginMoveRows(QModelIndex(), min + 1, min + 1, QModelIndex(), max + 1);
endMoveRows();
}
}