diff --git a/src/activities/balancebox/balancebox.js b/src/activities/balancebox/balancebox.js index 7514027ad..8997eac8d 100644 --- a/src/activities/balancebox/balancebox.js +++ b/src/activities/balancebox/balancebox.js @@ -1,451 +1,449 @@ /* GCompris - balancebox.js * * Copyright (C) 2014-2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ /* ToDo: - levels, levels, levels - - make sensitivity configurable? - - catch case of user levelset without or empty levels file - - editor: detect and warn invalid levels (no start, no goal) + - make sensitivity configurable */ .pragma library .import QtQuick 2.0 as Quick .import GCompris 1.0 as GCompris .import Box2D 2.0 as Box2D Qt.include("balancebox_common.js") var dataset = null; // Parameters that control the ball's dynamics var m = 0.2; // without ppm-correction: 10 var g = 9.81; // without ppm-correction: 50.8 var box2dPpm = 32; // pixelsPerMeter used in Box2D's world var boardSizeM = 0.9; // board's real edge length, fixed to 90 cm var boardSizePix = 500; // board's current size in pix (acquired dynamically) var dpiBase=139; +var boardSizeBase = 760; var curDpi = null; var pixelsPerMeter = null; var vFactor = pixelsPerMeter / box2dPpm; // FIXME: calculate! var step = 20; // time step (in ms) var friction = 0.15; var restitution = 0.3; // rebounce factor // stuff for keyboard based tilting var keyboardTiltStep = 0.5; // degrees var keyboardTimeStep = 20; // ms var lastKey; var keyboardIsTilting = false; // tilting or resetting to horizontal var debugDraw = false; var currentLevel = 0; var numberOfLevel = 0; var items; var level; var map; // current map var goal = null; var holes = new Array(); var walls = new Array(); var contacts = new Array(); var ballContacts = new Array(); var goalUnlocked; var lastContact; var ballContacts; var wallComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/Wall.qml"); var contactComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/BalanceContact.qml"); var balanceItemComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/BalanceItem.qml"); var contactIndex = -1; function start(items_) { items = items_; currentLevel = 0; reconfigureScene(); if (items.mode === "play") { if (GCompris.ApplicationInfo.isMobile) { // lock screen orientation GCompris.ApplicationInfo.setRequestedOrientation(0); if (GCompris.ApplicationInfo.getNativeOrientation() === Qt.PortraitOrientation) { /* * Adjust tilting if native orientation != landscape. * * Note: As of Qt 5.4.1 QTiltSensor as well as QRotationSensor * report on Android * isFeatureSupported(AxesOrientation) == false. * Therefore we honour rotation manually. */ items.tilt.swapAxes = true; items.tilt.invertX = true; } } var levelsFile = builtinFile; if (items.levelSet === "user") levelsFile = userFile; dataset = items.parser.parseFromUrl(levelsFile, validateLevels); if (dataset == null) { console.error("Balancebox: Error loading levels from " + levelsFile + ", can't continue!"); return; } } else { // testmode: dataset = [items.testLevel]; } numberOfLevel = dataset.length; initLevel(); } function reconfigureScene() { // set up dynamic variables for movement: - pixelsPerMeter = boardSizePix / boardSizeM / (items.dpi / dpiBase); - //pixelsPerMeter = (items.mapWrapper.length / 760) * boardSizePix / boardSizeM; + pixelsPerMeter = (items.mapWrapper.length / boardSizeBase) * boardSizePix / boardSizeM; vFactor = pixelsPerMeter / box2dPpm; console.log("Starting: mode=" + items.mode + " pixelsPerM=" + items.world.pixelsPerMeter + " timeStep=" + items.world.timeStep + " posIterations=" + items.world.positionIterations + " velIterations=" + items.world.velocityIterations + " boardSizePix" + boardSizePix + " (real " + items.mapWrapper.length + ")" + " pixelsPerMeter=" + pixelsPerMeter + " vFactor=" + vFactor + " dpi=" + items.dpi + " nativeOrientation=" + GCompris.ApplicationInfo.getNativeOrientation()); } function sinDeg(num) { return Math.sin(num/180*Math.PI); } function moveBall() { // console.log("tilt: " + items.tilt.xRotation + "/" + items.tilt.yRotation ); var dt = step / 1000; var dvx = ((m*g*dt) * sinDeg(items.tilt.yRotation)) / m; var dvy = ((m*g*dt) * sinDeg(items.tilt.xRotation)) / m; /* console.log("moving ball: dv: " + items.ball.body.linearVelocity.x + "/" + items.ball.body.linearVelocity.y + " -> " + (items.ball.body.linearVelocity.x+dvx) + "/" + (items.ball.body.linearVelocity.y+dvy)); */ items.ball.body.linearVelocity.x += dvx * vFactor; items.ball.body.linearVelocity.y += dvy * vFactor; checkBallContacts(); } function checkBallContacts() { for (var k = 0; k < ballContacts.length; k++) { if (items.ball.x > ballContacts[k].x - items.ballSize/2 && items.ball.x < ballContacts[k].x + items.ballSize/2 && items.ball.y > ballContacts[k].y - items.ballSize/2 && items.ball.y < ballContacts[k].y + items.ballSize/2) { // collision if (ballContacts[k].categories == items.holeType) finishBall(false, ballContacts[k].x, ballContacts[k].y); else if (ballContacts[k].categories == items.goalType && goalUnlocked) finishBall(true, ballContacts[k].x, ballContacts[k].y); else if (ballContacts[k].categories == items.buttonType) { if (!ballContacts[k].pressed && ballContacts[k].orderNum == lastContact + 1) { ballContacts[k].pressed = true; lastContact = ballContacts[k].orderNum; console.log("unlocked next contact " + lastContact + " length=" + contacts.length); if (lastContact == contacts.length) { console.log("door unlocked"); goalUnlocked = true; goal.imageSource = baseUrl + "/door.svg"; } } } } } } function finishBall(won, x, y) { items.timer.stop(); items.keyboardTimer.stop(); items.ball.x = x; items.ball.y = y; items.ball.scale = 0.4; items.ball.body.linearVelocity = Qt.point(0, 0); if (won) items.bonus.good("flower"); else items.bonus.bad("flower"); } function stop() { // reset everything tearDown(); // unlock screen orientation if (GCompris.ApplicationInfo.isMobile) GCompris.ApplicationInfo.setRequestedOrientation(-1); } function createObject(component, properties) { var p = properties; p.world = items.world; var object = component.createObject(items.mapWrapper, p); return object; } function initMap() { var modelMap = new Array(); goalUnlocked = true; items.mapWrapper.rows = map.length; items.mapWrapper.columns = map[0].length; console.log("creating map of size " + items.mapWrapper.rows + "/" + items.mapWrapper.columns); for (var row = 0; row < map.length; row++) { for (var col = 0; col < map[row].length; col++) { var x = col * items.cellSize; var y = row * items.cellSize; var orderNum = (map[row][col] & 0xFF00) >> 8; // debugging: if (debugDraw) { try { var rect = Qt.createQmlObject( "import QtQuick 2.0;Rectangle{" +"width:" + items.cellSize +";" +"height:" + items.cellSize+";" +"x:" + x + ";" +"y:" + y +";" +"color: \"transparent\";" +"border.color: \"blue\";" +"border.width: 1;" +"}", items.mapWrapper); } catch (e) { console.error("Error creating object: " + e); } } if (map[row][col] & NORTH) { walls.push(createObject(wallComponent, {x: x-items.wallSize/2, y: y-items.wallSize/2, width: items.cellSize + items.wallSize, height: items.wallSize, shadow: false})); } if (map[row][col] & SOUTH) { walls.push(createObject(wallComponent, {x: x-items.wallSize/2, y: y+items.cellSize-items.wallSize/2, width: items.cellSize+items.wallSize, height: items.wallSize, shadow: false})); } if (map[row][col] & EAST) { walls.push(createObject(wallComponent, {x: x+items.cellSize-items.wallSize/2, y: y-items.wallSize/2, width: items.wallSize, height: items.cellSize+items.wallSize, shadow: false})); } if (map[row][col] & WEST) { walls.push(createObject(wallComponent, {x: x-items.wallSize/2, y: y-items.wallSize/2, width: items.wallSize, height: items.cellSize+items.wallSize, shadow: false})); } if (map[row][col] & START) { items.ball.x = col * items.cellSize + items.wallSize ; items.ball.y = row * items.cellSize + items.wallSize; } if (map[row][col] & GOAL) { var goalX = col * items.cellSize + items.wallSize/2; var goalY = row * items.cellSize + items.wallSize/2; goal = createObject(balanceItemComponent, { x: goalX, y: goalY, width: items.cellSize - items.wallSize, height: items.cellSize - items.wallSize, imageSource: baseUrl + "/door_closed.svg", categories: items.goalType, sensor: true}); } if (map[row][col] & HOLE) { var holeX = col * items.cellSize + items.wallSize; var holeY = row * items.cellSize + items.wallSize; holes.push(createObject(balanceItemComponent, { x: holeX, y: holeY, width: items.ballSize, height: items.ballSize, imageSource: baseUrl + "/hole.svg", density: 0, friction: 0, restitution: 0, categories: items.holeType, sensor: true})); } if (orderNum > 0) { var contactX = col * items.cellSize + items.wallSize/2; var contactY = row * items.cellSize + items.wallSize/2; goalUnlocked = false; contacts.push(createObject(contactComponent, { x: contactX, y: contactY, width: items.cellSize - items.wallSize, height: items.cellSize - items.wallSize, pressed: false, density: 0, friction: 0, restitution: 0, categories: items.buttonType, sensor: true, orderNum: orderNum, text: level.targets[orderNum-1]})); } } } if (goalUnlocked && goal) // if we have no contacts at all goal.imageSource = baseUrl + "/door.svg"; } function addBallContact(item) { if (ballContacts.indexOf(item) !== -1) return; ballContacts.push(item); } function removeBallContact(item) { var index = ballContacts.indexOf(item); if (index > -1) ballContacts.splice(index, 1); } function tearDown() { items.ball.body.linearVelocity = Qt.point(0, 0); items.timer.stop(); items.keyboardTimer.stop(); if (holes.length > 0) { for (var i = 0; i< holes.length; i++) holes[i].destroy(); holes.length = 0; } if (walls.length > 0) { for (var i = 0; i< walls.length; i++) walls[i].destroy(); walls.length = 0; } if (contacts.length > 0) { for (var i = 0; i< contacts.length; i++) contacts[i].destroy(); contacts.length = 0; } lastContact = 0; if (goal) goal.destroy(); goal = null; items.tilt.xRotation = 0; items.tilt.yRotation = 0; items.ball.scale = 1; ballContacts = new Array(); } function initLevel(testLevel) { items.bar.level = currentLevel + 1; // reset everything tearDown(); level = dataset[currentLevel]; map = level.map initMap(); items.timer.start(); } // keyboard tilting stuff: function keyboardHandler() { if (keyboardIsTilting) { if (lastKey == Qt.Key_Left) items.tilt.yRotation -= keyboardTiltStep; else if (lastKey == Qt.Key_Right) items.tilt.yRotation += keyboardTiltStep; else if (lastKey == Qt.Key_Up) items.tilt.xRotation -= keyboardTiltStep; else if (lastKey == Qt.Key_Down) items.tilt.xRotation += keyboardTiltStep; items.keyboardTimer.start(); } else {// is resetting // yRotation: if (items.tilt.yRotation < 0) items.tilt.yRotation = Math.min(items.tilt.yRotation + keyboardTiltStep, 0); else if (items.tilt.yRotation > 0) items.tilt.yRotation = Math.max(items.tilt.yRotation - keyboardTiltStep, 0); // xRotation: if (items.tilt.xRotation < 0) items.tilt.xRotation = Math.min(items.tilt.xRotation + keyboardTiltStep, 0); else if (items.tilt.xRotation > 0) items.tilt.xRotation = Math.max(items.tilt.xRotation - keyboardTiltStep, 0); // resetting done? if (items.tilt.yRotation != 0 || items.tilt.xRotation != 0) items.keyboardTimer.start(); } } function processKeyPress(key) { if (key == Qt.Key_Left || key == Qt.Key_Right || key == Qt.Key_Up || key == Qt.Key_Down) { lastKey = key; keyboardIsTilting = true; items.keyboardTimer.stop(); keyboardHandler(); } } function processKeyRelease(key) { if (key == Qt.Key_Left || key == Qt.Key_Right || key == Qt.Key_Up || key == Qt.Key_Down) { lastKey = key; keyboardIsTilting = false; items.keyboardTimer.stop(); keyboardHandler(); } } function nextLevel() { if(numberOfLevel <= ++currentLevel ) { currentLevel = 0 } initLevel(); } function previousLevel() { if(--currentLevel < 0) { currentLevel = numberOfLevel - 1 } initLevel(); } diff --git a/src/activities/balancebox/editor/BalanceboxEditor.qml b/src/activities/balancebox/editor/BalanceboxEditor.qml index c3c2c8754..0efb8f229 100644 --- a/src/activities/balancebox/editor/BalanceboxEditor.qml +++ b/src/activities/balancebox/editor/BalanceboxEditor.qml @@ -1,654 +1,654 @@ /* GCompris - BalanceboxEditor.qml * * Copyright (C) 2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.1 import QtGraphicalEffects 1.0 import GCompris 1.0 import QtQuick.Controls 1.0 import QtQuick.Controls.Styles 1.3 import "../../../core" import ".." import "balanceboxeditor.js" as Activity Item { id: editor property string filename: Activity.userFile property bool isDialog: true property ActivityBase currentActivity property var testBox property bool isTesting: false /** * Emitted when the config dialog has been closed. */ signal close /** * Emitted when the config dialog has been started. */ signal start signal stop Keys.onEscapePressed: { console.log("XXX editor onEscape"); if (!isTesting) { if (Activity.levelChanged) Activity.warnUnsavedChanges(home, function() {}); else home() } else event.accepted = false; } onStart: { console.log("XXX Editor onStart"); if (!isTesting) Activity.initEditor(props); else stopTesting(); } onStop: {console.log("XXX Editor onStop");} Component.onCompleted: console.log("XXX editor complete " + filename); QtObject { id: props property int columns: 10 property int rows: 10 property int currentTool property alias editor: editor property alias mapModel: mapModel property alias mapWrapper: mapWrapper property int cellSize: mapWrapper.length / Math.min(mapWrapper.rows, mapWrapper.columns) property int wallSize: cellSize / 5 property int ballSize: cellSize - 2*wallSize property alias toolBox: toolBox property string contactValue: "1" property int lastOrderNum: 0 property alias file: file property alias parser: parser property alias bar: bar // singletons: property int lastGoalIndex: -1 property int lastBallIndex: -1 } function startTesting() { console.log("BalanceboxEditor: entering testing mode"); editor.isTesting = true; testBox.mode = "test"; testBox.testLevel = Activity.modelToLevel(); back(testBox); } function stopTesting() { console.log("BalanceboxEditor: stopping testing mode"); editor.isTesting = false; testBox.mode = "play"; testBox.testLevel = null; } Rectangle { id: background anchors.fill: parent Component.onCompleted: { console.log("XXX editor completed"); } File { id: file onError: console.error("File error: " + msg); } JsonParser { id: parser onError: console.error("Balanceboxeditor: Error parsing JSON: " + msg); } Column { id: toolBox2 anchors.top: mapWrapper.top anchors.left: mapWrapper.right - anchors.leftMargin: 10 - anchors.topMargin: 20 - spacing: 5 - width: (background.width - mapWrapper.width - props.wallSize - 20) / 2 + anchors.leftMargin: 10 * ApplicationInfo.ratio + anchors.topMargin: 20 * ApplicationInfo.ratio + spacing: 5 * ApplicationInfo.ratio + width: (background.width - mapWrapper.width - props.wallSize - 20 * ApplicationInfo.ratio) / 2 height: parent.height // anchors.topMargin: 20 Button { id: saveButton width: parent.width height: props.cellSize style: GCButtonStyle {} text: "Save" onClicked: Activity.saveModel(); } Button { id: testButton width: parent.width height: props.cellSize style: GCButtonStyle {} text: "Test" onClicked: editor.startTesting(); } } Column { id: toolBox anchors.top: mapWrapper.top - anchors.topMargin: 20 + anchors.topMargin: 20 * ApplicationInfo.ratio anchors.left: parent.left anchors.leftMargin: 10 width: (mapWrapper.x - 20) - spacing: 5 + spacing: 5 * ApplicationInfo.ratio Component.onCompleted: clearTool.selected = true; function setCurrentTool(item) { props.currentTool = item.type; if (clearTool !== item) clearTool.selected = false; if (hWallTool !== item) hWallTool.selected = false; if (vWallTool !== item) vWallTool.selected = false; if (holeTool !== item) holeTool.selected = false; if (ballTool !== item) ballTool.selected = false; if (contactTool !== item) contactTool.selected = false; if (goalTool !== item) goalTool.selected = false; console.log("XXX new current tool: " + item); } EditorTool { id: clearTool type: Activity.TOOL_CLEAR anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize - 2 onSelectedChanged: { if (selected) { toolBox.setCurrentTool(clearTool); } } Image { id: clear source: "qrc:/gcompris/src/core/resource/cancel.svg" width: props.cellSize - 4 height: props.cellSize - 4 anchors.centerIn: parent anchors.margins: 3 } } EditorTool { id: hWallTool type: Activity.TOOL_H_WALL anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize onSelectedChanged: { if (selected) { toolBox.setCurrentTool(hWallTool); } } Wall { id: hWall width: props.cellSize height: props.wallSize anchors.centerIn: parent anchors.margins: 3 } } EditorTool { id: vWallTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_V_WALL onSelectedChanged: { if (selected) { toolBox.setCurrentTool(vWallTool); } } Wall { id: vWall width: props.wallSize height: props.cellSize - 4 anchors.centerIn: parent anchors.margins: 3 } } EditorTool { id: holeTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_HOLE onSelectedChanged: { if (selected) { toolBox.setCurrentTool(holeTool); } } BalanceItem { id: hole width: props.cellSize - props.wallSize / 2 height: props.cellSize - props.wallSize / 2 anchors.centerIn: parent anchors.margins: props.wallSize / 2 visible: true imageSource: Activity.baseUrl + "/hole.svg" } } EditorTool { id: ballTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_BALL onSelectedChanged: { if (selected) { toolBox.setCurrentTool(ballTool); } } BalanceItem { id: ball width: props.cellSize - props.wallSize / 2 height: parent.height - props.wallSize / 2 anchors.centerIn: parent anchors.margins: props.wallSize / 2 visible: true imageSource: Activity.baseUrl + "/ball.svg" } } EditorTool { id: goalTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_GOAL onSelectedChanged: { if (selected) { toolBox.setCurrentTool(goalTool); } } BalanceItem { id: goal width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize anchors.centerIn: parent anchors.margins: props.wallSize / 2 z: 1 imageSource: Activity.baseUrl + "/door.svg" } } EditorTool { id: contactTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_CONTACT onSelectedChanged: { if (selected) { toolBox.setCurrentTool(contactTool); } } Row { id: contactToolRow spacing: 5 - width: contact.width + contactTextInput + spacing + width: contact.width + contactTextInput.width + spacing anchors.centerIn: parent BalanceContact { id: contact width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize anchors.margins: props.wallSize / 2 pressed: false orderNum: 99 text: props.contactValue z: 1 } SpinBox { id: contactTextInput width: contact.width * 2 height: contact.height value: props.contactValue maximumValue: 99 minimumValue: 1 decimals: 0 horizontalAlignment: Qt.AlignHCenter font.family: GCSingletonFontLoader.fontLoader.name font.pixelSize: height / 2 onValueChanged: if (value != props.contactValue) props.contactValue = value; } } } } ListModel { id: mapModel } Rectangle { id: mapWrapper property double margin: 20 property int columns: props.columns property int rows: props.rows property double length: Math.min(background.height - 2*mapWrapper.margin, background.width - 2*mapWrapper.margin); color: "#E3DEDB" width: length height: length anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter Grid { id: mapGrid columns: mapWrapper.columns rows: mapWrapper.rows anchors.fill: parent width: parent.width height: parent.height spacing: 0 Repeater { id: mapGridRepeater model: mapModel//mapGrid.columns * mapGrid.rows delegate: Item { // cell wrapper id: cell width: props.cellSize height: props.cellSize property bool highlighted: false Loader { id: northWallLoader active: value & Activity.NORTH width: props.cellSize + props.wallSize height: props.wallSize anchors.top: parent.top anchors.left: parent.left anchors.topMargin: -props.wallSize / 2 anchors.leftMargin: -props.wallSize / 2 sourceComponent: Wall { id: northWall shadow: false anchors.centerIn: parent z: 1 } } Loader { id: eastWallLoader active: value & Activity.EAST || (cell.highlighted && props.currentTool === Activity.TOOL_V_WALL) width: props.wallSize height: props.cellSize + props.wallSize anchors.bottom: parent.bottom anchors.right: parent.right anchors.bottomMargin: -props.wallSize / 2 anchors.rightMargin: -props.wallSize / 2 sourceComponent: Wall { id: eastWall anchors.centerIn: parent shadow: false z: 1 } } Loader { id: southWallLoader active: value & Activity.SOUTH || (cell.highlighted && props.currentTool === Activity.SOUTH) width: props.cellSize + props.wallSize height: props.wallSize anchors.bottom: parent.bottom anchors.left: parent.left anchors.bottomMargin: -props.wallSize / 2 anchors.leftMargin: -props.wallSize / 2 sourceComponent: Wall { id: southWall anchors.centerIn: parent shadow: false z: 1 } } Loader { id: westWallLoader active: value & Activity.WEST width: props.wallSize height: props.cellSize + props.wallSize anchors.bottom: parent.bottom anchors.left: parent.left anchors.bottomMargin: -props.wallSize / 2 anchors.leftMargin: -props.wallSize / 2 sourceComponent: Wall { id: westWall anchors.centerIn: parent shadow: false z: 1 } } Loader { id: doorLoader active: value & Activity.GOAL || (cell.highlighted && props.currentTool === Activity.TOOL_GOAL) anchors.centerIn: parent width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize sourceComponent: BalanceItem { id: goal anchors.centerIn: parent z: 1 imageSource: Activity.baseUrl + "/door.svg" } } Loader { id: holeLoader active: value & Activity.HOLE || (cell.highlighted && props.currentTool === Activity.TOOL_HOLE) anchors.centerIn: parent sourceComponent: BalanceItem { id: hole width: props.ballSize height:props.ballSize anchors.centerIn: parent z: 1 imageSource: Activity.baseUrl + "/hole.svg" } } Loader { id: ballLoader active: value & Activity.START || (cell.highlighted && props.currentTool === Activity.TOOL_BALL) anchors.centerIn: parent sourceComponent: BalanceItem { id: ball width: props.ballSize height:props.ballSize anchors.centerIn: parent visible: true imageSource: Activity.baseUrl + "/ball.svg" z: 1 } } Loader { id: contactLoader active: (orn > 0) || (cell.highlighted && props.currentTool === Activity.TOOL_CONTACT) width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize anchors.centerIn: parent sourceComponent: BalanceContact { id: contact anchors.centerIn: parent visible: true pressed: false orderNum: orn text: contactValue z: 1 } } Rectangle { // bounding rect id: cellRect width: props.cellSize height: props.cellSize color: "transparent" border.width: 1 border.color: cell.highlighted ? "yellow": "lightgray" z: 10 MouseArea { id: cellMouse anchors.fill: parent hoverEnabled: ApplicationInfo.isMobile ? false : true onEntered: cell.highlighted = true onExited: cell.highlighted = false onClicked: { activity.focus = true; Activity.modifyMap(props, row, col); } } } } } } // right: Wall { id: rightWall width: props.wallSize height: parent.height + props.wallSize anchors.left: mapWrapper.right anchors.leftMargin: - props.wallSize/2 anchors.top: parent.top anchors.topMargin: -props.wallSize/2 shadow: false } // bottom: Wall { id: bottomWall width: parent.width + props.wallSize height: props.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - props.wallSize/2 anchors.top: parent.bottom anchors.topMargin: -props.wallSize/2 shadow: false } // top: Wall { id: topWall width: parent.width + props.wallSize height: props.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - props.wallSize/2 anchors.top: parent.top anchors.topMargin: -props.wallSize/2 shadow: false } // left: Wall { id: leftWall width: props.wallSize height: parent.height + props.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - props.wallSize/2 anchors.top: parent.top anchors.topMargin: -props.wallSize/2 shadow: false } } } Bar { id: bar content: BarEnumContent { value: home | level } // FIXME: add dedicated editor help? onPreviousLevelClicked: { if (Activity.currentLevel > 0) { if (Activity.levelChanged) Activity.warnUnsavedChanges(Activity.previousLevel, function() {}); else Activity.previousLevel(); } } onNextLevelClicked: { if (Activity.levelChanged) Activity.warnUnsavedChanges(Activity.nextLevel, function() {}); else Activity.nextLevel(); } onHomeClicked: { if (Activity.levelChanged) Activity.warnUnsavedChanges(activity.home, function() {}); else activity.home() } } }