1/* 2 * SPDX-FileCopyrightText: 2014-2015 Andreas Cord-Landwehr <cordlandwehr@kde.org> 3 * 4 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 5 */ 6 7import QtQuick 2.1 8import QtQuick.Controls 1.3 9import QtQuick.Layouts 1.0 10import QtQuick.Dialogs 1.0 11import QtQml.StateMachine 1.0 as DSM 12import org.kde.rocs.graphtheory 1.0 13 14Item { 15 id: root 16 width: 800 17 height: 600 18 19 focus: true 20 21 // element create/remove actions 22 signal createNode(real x, real y, int typeIndex); 23 signal createEdge(Node from, Node to, int typeIndex); 24 signal deleteNode(Node node); 25 signal deleteEdge(Edge edge); 26 27 // exec dialog signals 28 signal showNodePropertiesDialog(Node node); 29 signal showEdgePropertiesDialog(Edge edge); 30 31 Connections { 32 target: selectMoveAction 33 onToggled: { 34 dsmSelectMove.running = selectMoveAction.checked 35 } 36 } 37 Connections { 38 target: addEdgeAction 39 onToggled: { 40 dsmCreateEdge.running = addEdgeAction.checked 41 } 42 } 43 44 Keys.onPressed: { 45 switch(event.key) { 46 case Qt.Key_Escape: 47 scene.clearSelection(); 48 event.accepted = true; 49 break; 50 case Qt.Key_Delete: 51 scene.deleteSelected() 52 event.accepted = true; 53 break; 54 case Qt.Key_A: //CTRL+A 55 if (event.modifiers & Qt.ControlModifier) { 56 scene.selectAll(); 57 event.accepted = true; 58 } 59 break; 60 default: 61 break; 62 } 63 } 64 65 ExclusiveGroup { 66 id: editToolButton 67 } 68 ColumnLayout { 69 id: toolbar 70 ToolButton { 71 action: SelectMoveAction { 72 id: selectMoveAction 73 exclusiveGroup: editToolButton 74 checked: true 75 } 76 } 77 ToolButton { 78 action: AddNodeAction { 79 id: addNodeAction 80 exclusiveGroup: editToolButton 81 } 82 } 83 ToolButton { 84 action: AddEdgeAction { 85 id: addEdgeAction 86 exclusiveGroup: editToolButton 87 onCreateEdge: root.createEdge(from, to, edgeTypeSelector.currentIndex) 88 } 89 } 90 ToolButton { 91 action: DeleteAction { 92 id: deleteAction 93 exclusiveGroup: editToolButton 94 } 95 } 96 } 97 ScrollView { 98 id: sceneScrollView 99 width: root.width - toolbar.width - 20 100 height: root.height 101 anchors { 102 left: toolbar.right 103 leftMargin: 10 104 } 105 106 107 Rectangle { // white background 108 color: "white" 109 width: parent.width 110 height: parent.height 111 } 112 113 Item { 114 id: scene 115 width: sceneScrollView.width - 30 116 height: sceneScrollView.height - 20 117 z: -10 // must lie behind everything else 118 signal deleteSelected(); 119 signal startMoveSelected(); 120 signal finishMoveSelected(); 121 signal updateSelection(); 122 signal createEdgeUpdateFromNode(); 123 signal createEdgeUpdateToNode(); 124 function clearSelection() 125 { 126 selectionRect.from = Qt.point(0, 0) 127 selectionRect.to = Qt.point(0, 0) 128 updateSelection(); 129 } 130 function setEdgeFromNode() 131 { 132 addEdgeAction.to = null 133 createEdgeUpdateFromNode(); 134 } 135 function selectAll() 136 { 137 selectionRect.from = Qt.point(0, 0) 138 selectionRect.to = Qt.point(width,height) 139 updateSelection(); 140 } 141 142 MouseArea { 143 id: sceneAction 144 anchors.fill: parent 145 z: -10 // must lie behind everything else 146 147 property variant lastMouseClicked: Qt.point(0, 0) 148 property variant lastMousePressed: Qt.point(0, 0) 149 property variant lastMouseReleased: Qt.point(0, 0) 150 property variant lastMousePosition: Qt.point(0, 0) 151 property bool nodePressed: false // if true, the current mouse-press event started at a node 152 153 onClicked: { 154 lastMouseClicked = Qt.point(mouse.x, mouse.y) 155 if (addNodeAction.checked) { 156 mouse.accepted = true 157 createNode(mouse.x, mouse.y, nodeTypeSelector.currentIndex); 158 return 159 } 160 } 161 onPressed: { 162 lastMousePressed = Qt.point(mouse.x, mouse.y) 163 lastMousePosition = Qt.point(mouse.x, mouse.y) 164 } 165 onPositionChanged: { 166 lastMousePosition = Qt.point(mouse.x, mouse.y) 167 } 168 onReleased: { 169 lastMouseReleased = Qt.point(mouse.x, mouse.y) 170 sceneAction.nodePressed = false 171 } 172 } 173 174 SelectionRectangle { 175 id: selectionRect 176 visible: false 177 } 178 179 Line { 180 id: createLineMarker 181 visible: false 182 fromX: sceneAction.lastMousePressed.x 183 fromY: sceneAction.lastMousePressed.y 184 toX: sceneAction.lastMousePosition.x 185 toY: sceneAction.lastMousePosition.y 186 } 187 188 Repeater { 189 model: edgeModel 190 EdgeItem { 191 id: edgeItem 192 edge: model.dataRole 193 z: -1 // edges must be below nodes 194 195 EdgePropertyItem { 196 anchors.centerIn: parent 197 edge: model.dataRole 198 } 199 200 MouseArea { 201 anchors.fill: parent 202 propagateComposedEvents: true 203 onPressed: { 204 if (deleteAction.checked) { 205 deleteEdge(edge) 206 mouse.accepted = true 207 } 208 } 209 onDoubleClicked: { 210 showEdgePropertiesDialog(edgeItem.edge); 211 } 212 } 213 } 214 } 215 216 Repeater { 217 model: nodeModel 218 NodeItem { 219 id: nodeItem 220 node: model.dataRole 221 highlighted: addEdgeAction.from == node || addEdgeAction.to == node 222 property bool __modifyingPosition: false 223 property variant __moveStartedPosition: Qt.point(0, 0) 224 Connections { 225 target: selectionRect 226 onChanged: { 227 if (selectMoveAction.checked && selectionRect.contains(x, y)) { 228 highlighted = true 229 } else { 230 highlighted = false 231 } 232 } 233 } 234 Connections { 235 target: scene 236 onUpdateSelection: { 237 if (selectionRect.contains(x, y)) { 238 highlighted = true 239 } else { 240 highlighted = false 241 } 242 } 243 onDeleteSelected: { 244 if (highlighted) { 245 deleteNode(node) 246 } 247 } 248 onStartMoveSelected: { 249 if (!highlighted) { 250 return 251 } 252 __moveStartedPosition.x = node.x 253 __moveStartedPosition.y = node.y 254 node.x = Qt.binding(function() { return __moveStartedPosition.x + sceneAction.lastMousePosition.x - sceneAction.lastMousePressed.x }) 255 node.y = Qt.binding(function() { return __moveStartedPosition.y + sceneAction.lastMousePosition.y - sceneAction.lastMousePressed.y }) 256 } 257 onFinishMoveSelected: { 258 if (!highlighted) { 259 return 260 } 261 node.x = __moveStartedPosition.x + sceneAction.lastMousePosition.x - sceneAction.lastMousePressed.x 262 node.y = __moveStartedPosition.y + sceneAction.lastMousePosition.y - sceneAction.lastMousePressed.y 263 __moveStartedPosition.x = 0 264 __moveStartedPosition.y = 0 265 } 266 onCreateEdgeUpdateFromNode: { 267 if (nodeItem.contains(Qt.point(sceneAction.lastMousePressed.x, sceneAction.lastMousePressed.y))) { 268 node.highlighted = true 269 addEdgeAction.from = node 270 } 271 } 272 onCreateEdgeUpdateToNode: { 273 if (nodeItem.contains(Qt.point(sceneAction.lastMouseReleased.x, sceneAction.lastMouseReleased.y))) { 274 node.highlighted = true 275 addEdgeAction.to = node 276 } 277 } 278 } 279 onXChanged: { 280 if (x < 10) { 281 nodeItem.__modifyingPosition = true; 282 var delta = Math.max((10 - x), 10) 283 scene.width += delta; 284 nodeItem.__modifyingPosition = false; 285 return; 286 } 287 if (x + width + 10 > scene.width) { 288 nodeItem.__modifyingPosition = true; 289 var delta = Math.max(scene.width - (x + width) + 10, 10); 290 scene.width += delta; 291 nodeItem.__modifyingPosition = false; 292 return; 293 } 294 } 295 onYChanged: { 296 if (y < 10) { 297 nodeItem.__modifyingPosition = true; 298 var delta = (10 - y) 299 scene.height += delta; 300 nodeItem.__modifyingPosition = false; 301 return; 302 } 303 if (y + height + 10 > scene.height) { 304 nodeItem.__modifyingPosition = true; 305 var delta = Math.max(scene.height - (y + height) + 10, 10); 306 scene.height += delta; 307 nodeItem.__modifyingPosition = false; 308 return; 309 } 310 } 311 NodePropertyItem { 312 anchors.centerIn: parent 313 node: model.dataRole 314 } 315 316 Drag.active: dragArea.drag.active 317 MouseArea { 318 id: dragArea 319 anchors.fill: parent 320 propagateComposedEvents: true 321 drag.target: { // only enable drag when move/select checked 322 selectMoveAction.checked ? parent : undefined 323 } 324 Loader { 325 id: nodeDialogLoader 326 } 327 onDoubleClicked: { 328 showNodePropertiesDialog(nodeItem.node); 329 mouse.accepted = true 330 } 331 onPressed: { 332 // never handle undefined actions signals 333 if (!(selectMoveAction.checked || deleteAction.checked)) { 334 mouse.accepted = false 335 return 336 } 337 if (deleteAction.checked) { 338 deleteNode(nodeItem.node) 339 mouse.accepted = true 340 return 341 } 342 // single-node move action: directly handle it 343 sceneAction.nodePressed = true 344 if (selectMoveAction.checked && !nodeItem.highlighted) { 345 scene.clearSelection() 346 nodeItem.highlighted = true 347 mouse.accepted = true 348 return 349 } 350 // multi-node move action: gets handled by state-machine 351 if (selectMoveAction.checked && nodeItem.highlighted) { 352 mouse.accepted = false 353 return 354 } 355 } 356 onReleased: { 357 sceneAction.nodePressed = false 358 } 359 } 360 } 361 } 362 } 363 } 364 365 RowLayout { 366 id: extraToolbarCreateNode 367 visible: addNodeAction.checked 368 anchors { 369 top: sceneScrollView.top 370 right:sceneScrollView.right 371 topMargin: 10 372 rightMargin: 5 373 } 374 ComboBox { 375 id: nodeTypeSelector 376 model: nodeTypeModel 377 textRole: "titleRole" 378 } 379 } 380 RowLayout { 381 id: extraToolbarCreateEdge 382 visible: addEdgeAction.checked 383 anchors { 384 top: sceneScrollView.top 385 right:sceneScrollView.right 386 topMargin: 10 387 rightMargin: 5 388 } 389 ComboBox { 390 id: edgeTypeSelector 391 model: edgeTypeModel 392 textRole: "titleRole" 393 } 394 } 395 396 // state machine only for select/move 397 DSM.StateMachine { 398 id: dsmSelectMove 399 initialState: smStateIdle 400 running: true 401 DSM.State { 402 id: smStateIdle 403 DSM.SignalTransition { 404 targetState: smStateMoving 405 signal: sceneAction.onPressed 406 guard: sceneAction.nodePressed 407 } 408 DSM.SignalTransition { 409 targetState: smStateSelecting 410 signal: sceneAction.onPressed 411 guard: !sceneAction.nodePressed 412 } 413 DSM.SignalTransition { 414 signal: sceneAction.onClicked 415 onTriggered: { 416 scene.clearSelection 417 } 418 } 419 } 420 DSM.State { 421 id: smStateSelecting 422 DSM.SignalTransition { 423 signal: sceneAction.onPositionChanged 424 onTriggered: { 425 selectionRect.visible = true 426 selectionRect.to = sceneAction.lastMousePosition 427 } 428 } 429 DSM.SignalTransition { 430 targetState: smStateIdle 431 signal: sceneAction.onReleased 432 } 433 onEntered: { 434 scene.clearSelection() 435 selectionRect.from = sceneAction.lastMousePressed 436 } 437 onExited: { 438 selectionRect.visible = false 439 } 440 } 441 DSM.State { 442 id: smStateMoving 443 DSM.SignalTransition { 444 targetState: smStateIdle 445 signal: sceneAction.onReleased 446 } 447 onEntered: { 448 scene.startMoveSelected(); 449 } 450 onExited: { 451 scene.finishMoveSelected() 452 } 453 } 454 } 455 456 // state machine solely for edge creation 457 DSM.StateMachine { 458 id: dsmCreateEdge 459 initialState: ceStateIdle 460 running: false 461 DSM.State { 462 id: ceStateIdle 463 DSM.SignalTransition { 464 targetState: ceStateCreateEdgeTo 465 signal: sceneAction.onPressed 466 } 467 onExited: { 468 addEdgeAction.to = null 469 scene.createEdgeUpdateFromNode() 470 } 471 } 472 DSM.State { 473 id: ceStateCreateEdgeTo 474 DSM.SignalTransition { 475 targetState: ceStateIdle 476 signal: sceneAction.onReleased 477 } 478 onEntered: { 479 createLineMarker.visible = true 480 } 481 onExited: { 482 scene.createEdgeUpdateToNode() 483 addEdgeAction.apply() 484 createLineMarker.visible = false 485 } 486 } 487 } 488} 489