├── .gitignore ├── CMakeLists.txt ├── LICENCE ├── Piper.kdev4 ├── README.md ├── data └── theme.json ├── piper.png ├── ressources ├── add_tab.svg ├── arrow-down-disabled.svg ├── arrow-down-hover.svg ├── arrow-down.svg ├── arrow-up-disabled.svg ├── arrow-up-hover.svg ├── arrow-up.svg ├── resources.qrc ├── star.svg └── style.qss └── src ├── Attribute.cc ├── Attribute.h ├── AttributeMember.cc ├── AttributeMember.h ├── CreatorPopup.cc ├── CreatorPopup.h ├── EditorTab.cc ├── EditorTab.h ├── EditorWidget.cc ├── EditorWidget.h ├── EditorWidget.ui ├── ExportBackend.h ├── JsonExport.cc ├── JsonExport.h ├── Link.cc ├── Link.h ├── MainEditor.cc ├── MainEditor.h ├── MainEditor.ui ├── Node.cc ├── Node.h ├── NodeCreator.cc ├── NodeCreator.h ├── PropertyDelegate.cc ├── PropertyDelegate.h ├── Scene.cc ├── Scene.h ├── ThemeManager.cc ├── ThemeManager.h ├── Types.h ├── View.cc ├── View.h └── main.cc /.gitignore: -------------------------------------------------------------------------------- 1 | .kdev4 2 | build/ 3 | /.vs 4 | /CMakeSettings.json 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Piper) 3 | 4 | # Find includes in corresponding build directories 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | # Instruct CMake to run moc automatically when needed. 7 | set(CMAKE_AUTOMOC ON) 8 | # Instruct CMake to create code from Qt designer ui files 9 | set(CMAKE_AUTOUIC ON) 10 | # Instruct CMake to run Qt ressource 11 | set(CMAKE_AUTORCC ON) 12 | 13 | if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 14 | add_definitions(/permissive-) 15 | endif() 16 | 17 | # Find the QtWidgets library 18 | find_package(Qt5Widgets CONFIG REQUIRED) 19 | 20 | set(piper_lib_src 21 | ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene.cc 22 | ${CMAKE_CURRENT_SOURCE_DIR}/src/View.cc 23 | ${CMAKE_CURRENT_SOURCE_DIR}/src/Node.cc 24 | ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeManager.cc 25 | ${CMAKE_CURRENT_SOURCE_DIR}/src/Attribute.cc 26 | ${CMAKE_CURRENT_SOURCE_DIR}/src/AttributeMember.cc 27 | ${CMAKE_CURRENT_SOURCE_DIR}/src/Link.cc 28 | ${CMAKE_CURRENT_SOURCE_DIR}/src/NodeCreator.cc 29 | ${CMAKE_CURRENT_SOURCE_DIR}/src/CreatorPopup.cc 30 | ${CMAKE_CURRENT_SOURCE_DIR}/src/JsonExport.cc 31 | ${CMAKE_CURRENT_SOURCE_DIR}/src/MainEditor.cc 32 | ${CMAKE_CURRENT_SOURCE_DIR}/src/EditorTab.cc 33 | ${CMAKE_CURRENT_SOURCE_DIR}/src/EditorWidget.cc 34 | ${CMAKE_CURRENT_SOURCE_DIR}/src/PropertyDelegate.cc 35 | ${CMAKE_CURRENT_SOURCE_DIR}/ressources/resources.qrc 36 | ) 37 | 38 | set(piper_editor_src 39 | ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cc 40 | ) 41 | 42 | # Tell CMake to create the helloworld executable 43 | add_library(piper ${piper_lib_src}) 44 | target_include_directories(piper PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) 45 | target_link_libraries(piper Qt5::Widgets) 46 | target_compile_options(piper PRIVATE -Wall) 47 | 48 | add_executable(piper_editor ${piper_editor_src}) 49 | target_link_libraries(piper_editor piper) 50 | target_compile_options(piper_editor PRIVATE -Wall) 51 | 52 | file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) 53 | 54 | # Install the executable 55 | install(TARGETS piper DESTINATION lib) 56 | install(TARGETS piper_editor DESTINATION bin) 57 | install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data DESTINATION bin) 58 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | 2 | CeCILL-C FREE SOFTWARE LICENSE AGREEMENT 3 | 4 | 5 | Notice 6 | 7 | This Agreement is a Free Software license agreement that is the result 8 | of discussions between its authors in order to ensure compliance with 9 | the two main principles guiding its drafting: 10 | 11 | * firstly, compliance with the principles governing the distribution 12 | of Free Software: access to source code, broad rights granted to 13 | users, 14 | * secondly, the election of a governing law, French law, with which 15 | it is conformant, both as regards the law of torts and 16 | intellectual property law, and the protection that it offers to 17 | both authors and holders of the economic rights over software. 18 | 19 | The authors of the CeCILL-C (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) 20 | license are: 21 | 22 | Commissariat � l'Energie Atomique - CEA, a public scientific, technical 23 | and industrial research establishment, having its principal place of 24 | business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France. 25 | 26 | Centre National de la Recherche Scientifique - CNRS, a public scientific 27 | and technological establishment, having its principal place of business 28 | at 3 rue Michel-Ange, 75794 Paris cedex 16, France. 29 | 30 | Institut National de Recherche en Informatique et en Automatique - 31 | INRIA, a public scientific and technological establishment, having its 32 | principal place of business at Domaine de Voluceau, Rocquencourt, BP 33 | 105, 78153 Le Chesnay cedex, France. 34 | 35 | 36 | Preamble 37 | 38 | The purpose of this Free Software license agreement is to grant users 39 | the right to modify and re-use the software governed by this license. 40 | 41 | The exercising of this right is conditional upon the obligation to make 42 | available to the community the modifications made to the source code of 43 | the software so as to contribute to its evolution. 44 | 45 | In consideration of access to the source code and the rights to copy, 46 | modify and redistribute granted by the license, users are provided only 47 | with a limited warranty and the software's author, the holder of the 48 | economic rights, and the successive licensors only have limited liability. 49 | 50 | In this respect, the risks associated with loading, using, modifying 51 | and/or developing or reproducing the software by the user are brought to 52 | the user's attention, given its Free Software status, which may make it 53 | complicated to use, with the result that its use is reserved for 54 | developers and experienced professionals having in-depth computer 55 | knowledge. Users are therefore encouraged to load and test the 56 | suitability of the software as regards their requirements in conditions 57 | enabling the security of their systems and/or data to be ensured and, 58 | more generally, to use and operate it in the same conditions of 59 | security. This Agreement may be freely reproduced and published, 60 | provided it is not altered, and that no provisions are either added or 61 | removed herefrom. 62 | 63 | This Agreement may apply to any or all software for which the holder of 64 | the economic rights decides to submit the use thereof to its provisions. 65 | 66 | 67 | Article 1 - DEFINITIONS 68 | 69 | For the purpose of this Agreement, when the following expressions 70 | commence with a capital letter, they shall have the following meaning: 71 | 72 | Agreement: means this license agreement, and its possible subsequent 73 | versions and annexes. 74 | 75 | Software: means the software in its Object Code and/or Source Code form 76 | and, where applicable, its documentation, "as is" when the Licensee 77 | accepts the Agreement. 78 | 79 | Initial Software: means the Software in its Source Code and possibly its 80 | Object Code form and, where applicable, its documentation, "as is" when 81 | it is first distributed under the terms and conditions of the Agreement. 82 | 83 | Modified Software: means the Software modified by at least one 84 | Integrated Contribution. 85 | 86 | Source Code: means all the Software's instructions and program lines to 87 | which access is required so as to modify the Software. 88 | 89 | Object Code: means the binary files originating from the compilation of 90 | the Source Code. 91 | 92 | Holder: means the holder(s) of the economic rights over the Initial 93 | Software. 94 | 95 | Licensee: means the Software user(s) having accepted the Agreement. 96 | 97 | Contributor: means a Licensee having made at least one Integrated 98 | Contribution. 99 | 100 | Licensor: means the Holder, or any other individual or legal entity, who 101 | distributes the Software under the Agreement. 102 | 103 | Integrated Contribution: means any or all modifications, corrections, 104 | translations, adaptations and/or new functions integrated into the 105 | Source Code by any or all Contributors. 106 | 107 | Related Module: means a set of sources files including their 108 | documentation that, without modification to the Source Code, enables 109 | supplementary functions or services in addition to those offered by the 110 | Software. 111 | 112 | Derivative Software: means any combination of the Software, modified or 113 | not, and of a Related Module. 114 | 115 | Parties: mean both the Licensee and the Licensor. 116 | 117 | These expressions may be used both in singular and plural form. 118 | 119 | 120 | Article 2 - PURPOSE 121 | 122 | The purpose of the Agreement is the grant by the Licensor to the 123 | Licensee of a non-exclusive, transferable and worldwide license for the 124 | Software as set forth in Article 5 hereinafter for the whole term of the 125 | protection granted by the rights over said Software. 126 | 127 | 128 | Article 3 - ACCEPTANCE 129 | 130 | 3.1 The Licensee shall be deemed as having accepted the terms and 131 | conditions of this Agreement upon the occurrence of the first of the 132 | following events: 133 | 134 | * (i) loading the Software by any or all means, notably, by 135 | downloading from a remote server, or by loading from a physical 136 | medium; 137 | * (ii) the first time the Licensee exercises any of the rights 138 | granted hereunder. 139 | 140 | 3.2 One copy of the Agreement, containing a notice relating to the 141 | characteristics of the Software, to the limited warranty, and to the 142 | fact that its use is restricted to experienced users has been provided 143 | to the Licensee prior to its acceptance as set forth in Article 3.1 144 | hereinabove, and the Licensee hereby acknowledges that it has read and 145 | understood it. 146 | 147 | 148 | Article 4 - EFFECTIVE DATE AND TERM 149 | 150 | 151 | 4.1 EFFECTIVE DATE 152 | 153 | The Agreement shall become effective on the date when it is accepted by 154 | the Licensee as set forth in Article 3.1. 155 | 156 | 157 | 4.2 TERM 158 | 159 | The Agreement shall remain in force for the entire legal term of 160 | protection of the economic rights over the Software. 161 | 162 | 163 | Article 5 - SCOPE OF RIGHTS GRANTED 164 | 165 | The Licensor hereby grants to the Licensee, who accepts, the following 166 | rights over the Software for any or all use, and for the term of the 167 | Agreement, on the basis of the terms and conditions set forth hereinafter. 168 | 169 | Besides, if the Licensor owns or comes to own one or more patents 170 | protecting all or part of the functions of the Software or of its 171 | components, the Licensor undertakes not to enforce the rights granted by 172 | these patents against successive Licensees using, exploiting or 173 | modifying the Software. If these patents are transferred, the Licensor 174 | undertakes to have the transferees subscribe to the obligations set 175 | forth in this paragraph. 176 | 177 | 178 | 5.1 RIGHT OF USE 179 | 180 | The Licensee is authorized to use the Software, without any limitation 181 | as to its fields of application, with it being hereinafter specified 182 | that this comprises: 183 | 184 | 1. permanent or temporary reproduction of all or part of the Software 185 | by any or all means and in any or all form. 186 | 187 | 2. loading, displaying, running, or storing the Software on any or 188 | all medium. 189 | 190 | 3. entitlement to observe, study or test its operation so as to 191 | determine the ideas and principles behind any or all constituent 192 | elements of said Software. This shall apply when the Licensee 193 | carries out any or all loading, displaying, running, transmission 194 | or storage operation as regards the Software, that it is entitled 195 | to carry out hereunder. 196 | 197 | 198 | 5.2 RIGHT OF MODIFICATION 199 | 200 | The right of modification includes the right to translate, adapt, 201 | arrange, or make any or all modifications to the Software, and the right 202 | to reproduce the resulting software. It includes, in particular, the 203 | right to create a Derivative Software. 204 | 205 | The Licensee is authorized to make any or all modification to the 206 | Software provided that it includes an explicit notice that it is the 207 | author of said modification and indicates the date of the creation thereof. 208 | 209 | 210 | 5.3 RIGHT OF DISTRIBUTION 211 | 212 | In particular, the right of distribution includes the right to publish, 213 | transmit and communicate the Software to the general public on any or 214 | all medium, and by any or all means, and the right to market, either in 215 | consideration of a fee, or free of charge, one or more copies of the 216 | Software by any means. 217 | 218 | The Licensee is further authorized to distribute copies of the modified 219 | or unmodified Software to third parties according to the terms and 220 | conditions set forth hereinafter. 221 | 222 | 223 | 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION 224 | 225 | The Licensee is authorized to distribute true copies of the Software in 226 | Source Code or Object Code form, provided that said distribution 227 | complies with all the provisions of the Agreement and is accompanied by: 228 | 229 | 1. a copy of the Agreement, 230 | 231 | 2. a notice relating to the limitation of both the Licensor's 232 | warranty and liability as set forth in Articles 8 and 9, 233 | 234 | and that, in the event that only the Object Code of the Software is 235 | redistributed, the Licensee allows effective access to the full Source 236 | Code of the Software at a minimum during the entire period of its 237 | distribution of the Software, it being understood that the additional 238 | cost of acquiring the Source Code shall not exceed the cost of 239 | transferring the data. 240 | 241 | 242 | 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE 243 | 244 | When the Licensee makes an Integrated Contribution to the Software, the 245 | terms and conditions for the distribution of the resulting Modified 246 | Software become subject to all the provisions of this Agreement. 247 | 248 | The Licensee is authorized to distribute the Modified Software, in 249 | source code or object code form, provided that said distribution 250 | complies with all the provisions of the Agreement and is accompanied by: 251 | 252 | 1. a copy of the Agreement, 253 | 254 | 2. a notice relating to the limitation of both the Licensor's 255 | warranty and liability as set forth in Articles 8 and 9, 256 | 257 | and that, in the event that only the object code of the Modified 258 | Software is redistributed, the Licensee allows effective access to the 259 | full source code of the Modified Software at a minimum during the entire 260 | period of its distribution of the Modified Software, it being understood 261 | that the additional cost of acquiring the source code shall not exceed 262 | the cost of transferring the data. 263 | 264 | 265 | 5.3.3 DISTRIBUTION OF DERIVATIVE SOFTWARE 266 | 267 | When the Licensee creates Derivative Software, this Derivative Software 268 | may be distributed under a license agreement other than this Agreement, 269 | subject to compliance with the requirement to include a notice 270 | concerning the rights over the Software as defined in Article 6.4. 271 | In the event the creation of the Derivative Software required modification 272 | of the Source Code, the Licensee undertakes that: 273 | 274 | 1. the resulting Modified Software will be governed by this Agreement, 275 | 2. the Integrated Contributions in the resulting Modified Software 276 | will be clearly identified and documented, 277 | 3. the Licensee will allow effective access to the source code of the 278 | Modified Software, at a minimum during the entire period of 279 | distribution of the Derivative Software, such that such 280 | modifications may be carried over in a subsequent version of the 281 | Software; it being understood that the additional cost of 282 | purchasing the source code of the Modified Software shall not 283 | exceed the cost of transferring the data. 284 | 285 | 286 | 5.3.4 COMPATIBILITY WITH THE CeCILL LICENSE 287 | 288 | When a Modified Software contains an Integrated Contribution subject to 289 | the CeCILL license agreement, or when a Derivative Software contains a 290 | Related Module subject to the CeCILL license agreement, the provisions 291 | set forth in the third item of Article 6.4 are optional. 292 | 293 | 294 | Article 6 - INTELLECTUAL PROPERTY 295 | 296 | 297 | 6.1 OVER THE INITIAL SOFTWARE 298 | 299 | The Holder owns the economic rights over the Initial Software. Any or 300 | all use of the Initial Software is subject to compliance with the terms 301 | and conditions under which the Holder has elected to distribute its work 302 | and no one shall be entitled to modify the terms and conditions for the 303 | distribution of said Initial Software. 304 | 305 | The Holder undertakes that the Initial Software will remain ruled at 306 | least by this Agreement, for the duration set forth in Article 4.2. 307 | 308 | 309 | 6.2 OVER THE INTEGRATED CONTRIBUTIONS 310 | 311 | The Licensee who develops an Integrated Contribution is the owner of the 312 | intellectual property rights over this Contribution as defined by 313 | applicable law. 314 | 315 | 316 | 6.3 OVER THE RELATED MODULES 317 | 318 | The Licensee who develops a Related Module is the owner of the 319 | intellectual property rights over this Related Module as defined by 320 | applicable law and is free to choose the type of agreement that shall 321 | govern its distribution under the conditions defined in Article 5.3.3. 322 | 323 | 324 | 6.4 NOTICE OF RIGHTS 325 | 326 | The Licensee expressly undertakes: 327 | 328 | 1. not to remove, or modify, in any manner, the intellectual property 329 | notices attached to the Software; 330 | 331 | 2. to reproduce said notices, in an identical manner, in the copies 332 | of the Software modified or not; 333 | 334 | 3. to ensure that use of the Software, its intellectual property 335 | notices and the fact that it is governed by the Agreement is 336 | indicated in a text that is easily accessible, specifically from 337 | the interface of any Derivative Software. 338 | 339 | The Licensee undertakes not to directly or indirectly infringe the 340 | intellectual property rights of the Holder and/or Contributors on the 341 | Software and to take, where applicable, vis-�-vis its staff, any and all 342 | measures required to ensure respect of said intellectual property rights 343 | of the Holder and/or Contributors. 344 | 345 | 346 | Article 7 - RELATED SERVICES 347 | 348 | 7.1 Under no circumstances shall the Agreement oblige the Licensor to 349 | provide technical assistance or maintenance services for the Software. 350 | 351 | However, the Licensor is entitled to offer this type of services. The 352 | terms and conditions of such technical assistance, and/or such 353 | maintenance, shall be set forth in a separate instrument. Only the 354 | Licensor offering said maintenance and/or technical assistance services 355 | shall incur liability therefor. 356 | 357 | 7.2 Similarly, any Licensor is entitled to offer to its licensees, under 358 | its sole responsibility, a warranty, that shall only be binding upon 359 | itself, for the redistribution of the Software and/or the Modified 360 | Software, under terms and conditions that it is free to decide. Said 361 | warranty, and the financial terms and conditions of its application, 362 | shall be subject of a separate instrument executed between the Licensor 363 | and the Licensee. 364 | 365 | 366 | Article 8 - LIABILITY 367 | 368 | 8.1 Subject to the provisions of Article 8.2, the Licensee shall be 369 | entitled to claim compensation for any direct loss it may have suffered 370 | from the Software as a result of a fault on the part of the relevant 371 | Licensor, subject to providing evidence thereof. 372 | 373 | 8.2 The Licensor's liability is limited to the commitments made under 374 | this Agreement and shall not be incurred as a result of in particular: 375 | (i) loss due the Licensee's total or partial failure to fulfill its 376 | obligations, (ii) direct or consequential loss that is suffered by the 377 | Licensee due to the use or performance of the Software, and (iii) more 378 | generally, any consequential loss. In particular the Parties expressly 379 | agree that any or all pecuniary or business loss (i.e. loss of data, 380 | loss of profits, operating loss, loss of customers or orders, 381 | opportunity cost, any disturbance to business activities) or any or all 382 | legal proceedings instituted against the Licensee by a third party, 383 | shall constitute consequential loss and shall not provide entitlement to 384 | any or all compensation from the Licensor. 385 | 386 | 387 | Article 9 - WARRANTY 388 | 389 | 9.1 The Licensee acknowledges that the scientific and technical 390 | state-of-the-art when the Software was distributed did not enable all 391 | possible uses to be tested and verified, nor for the presence of 392 | possible defects to be detected. In this respect, the Licensee's 393 | attention has been drawn to the risks associated with loading, using, 394 | modifying and/or developing and reproducing the Software which are 395 | reserved for experienced users. 396 | 397 | The Licensee shall be responsible for verifying, by any or all means, 398 | the suitability of the product for its requirements, its good working 399 | order, and for ensuring that it shall not cause damage to either persons 400 | or properties. 401 | 402 | 9.2 The Licensor hereby represents, in good faith, that it is entitled 403 | to grant all the rights over the Software (including in particular the 404 | rights set forth in Article 5). 405 | 406 | 9.3 The Licensee acknowledges that the Software is supplied "as is" by 407 | the Licensor without any other express or tacit warranty, other than 408 | that provided for in Article 9.2 and, in particular, without any warranty 409 | as to its commercial value, its secured, safe, innovative or relevant 410 | nature. 411 | 412 | Specifically, the Licensor does not warrant that the Software is free 413 | from any error, that it will operate without interruption, that it will 414 | be compatible with the Licensee's own equipment and software 415 | configuration, nor that it will meet the Licensee's requirements. 416 | 417 | 9.4 The Licensor does not either expressly or tacitly warrant that the 418 | Software does not infringe any third party intellectual property right 419 | relating to a patent, software or any other property right. Therefore, 420 | the Licensor disclaims any and all liability towards the Licensee 421 | arising out of any or all proceedings for infringement that may be 422 | instituted in respect of the use, modification and redistribution of the 423 | Software. Nevertheless, should such proceedings be instituted against 424 | the Licensee, the Licensor shall provide it with technical and legal 425 | assistance for its defense. Such technical and legal assistance shall be 426 | decided on a case-by-case basis between the relevant Licensor and the 427 | Licensee pursuant to a memorandum of understanding. The Licensor 428 | disclaims any and all liability as regards the Licensee's use of the 429 | name of the Software. No warranty is given as regards the existence of 430 | prior rights over the name of the Software or as regards the existence 431 | of a trademark. 432 | 433 | 434 | Article 10 - TERMINATION 435 | 436 | 10.1 In the event of a breach by the Licensee of its obligations 437 | hereunder, the Licensor may automatically terminate this Agreement 438 | thirty (30) days after notice has been sent to the Licensee and has 439 | remained ineffective. 440 | 441 | 10.2 A Licensee whose Agreement is terminated shall no longer be 442 | authorized to use, modify or distribute the Software. However, any 443 | licenses that it may have granted prior to termination of the Agreement 444 | shall remain valid subject to their having been granted in compliance 445 | with the terms and conditions hereof. 446 | 447 | 448 | Article 11 - MISCELLANEOUS 449 | 450 | 451 | 11.1 EXCUSABLE EVENTS 452 | 453 | Neither Party shall be liable for any or all delay, or failure to 454 | perform the Agreement, that may be attributable to an event of force 455 | majeure, an act of God or an outside cause, such as defective 456 | functioning or interruptions of the electricity or telecommunications 457 | networks, network paralysis following a virus attack, intervention by 458 | government authorities, natural disasters, water damage, earthquakes, 459 | fire, explosions, strikes and labor unrest, war, etc. 460 | 461 | 11.2 Any failure by either Party, on one or more occasions, to invoke 462 | one or more of the provisions hereof, shall under no circumstances be 463 | interpreted as being a waiver by the interested Party of its right to 464 | invoke said provision(s) subsequently. 465 | 466 | 11.3 The Agreement cancels and replaces any or all previous agreements, 467 | whether written or oral, between the Parties and having the same 468 | purpose, and constitutes the entirety of the agreement between said 469 | Parties concerning said purpose. No supplement or modification to the 470 | terms and conditions hereof shall be effective as between the Parties 471 | unless it is made in writing and signed by their duly authorized 472 | representatives. 473 | 474 | 11.4 In the event that one or more of the provisions hereof were to 475 | conflict with a current or future applicable act or legislative text, 476 | said act or legislative text shall prevail, and the Parties shall make 477 | the necessary amendments so as to comply with said act or legislative 478 | text. All other provisions shall remain effective. Similarly, invalidity 479 | of a provision of the Agreement, for any reason whatsoever, shall not 480 | cause the Agreement as a whole to be invalid. 481 | 482 | 483 | 11.5 LANGUAGE 484 | 485 | The Agreement is drafted in both French and English and both versions 486 | are deemed authentic. 487 | 488 | 489 | Article 12 - NEW VERSIONS OF THE AGREEMENT 490 | 491 | 12.1 Any person is authorized to duplicate and distribute copies of this 492 | Agreement. 493 | 494 | 12.2 So as to ensure coherence, the wording of this Agreement is 495 | protected and may only be modified by the authors of the License, who 496 | reserve the right to periodically publish updates or new versions of the 497 | Agreement, each with a separate number. These subsequent versions may 498 | address new issues encountered by Free Software. 499 | 500 | 12.3 Any Software distributed under a given version of the Agreement may 501 | only be subsequently distributed under the same version of the Agreement 502 | or a subsequent version. 503 | 504 | 505 | Article 13 - GOVERNING LAW AND JURISDICTION 506 | 507 | 13.1 The Agreement is governed by French law. The Parties agree to 508 | endeavor to seek an amicable solution to any disagreements or disputes 509 | that may arise during the performance of the Agreement. 510 | 511 | 13.2 Failing an amicable solution within two (2) months as from their 512 | occurrence, and unless emergency proceedings are necessary, the 513 | disagreements or disputes shall be referred to the Paris Courts having 514 | jurisdiction, by the more diligent Party. 515 | 516 | 517 | Version 1.0 dated 2006-09-05. 518 | 519 | -------------------------------------------------------------------------------- /Piper.kdev4: -------------------------------------------------------------------------------- 1 | [Project] 2 | CreatedFrom=CMakeLists.txt 3 | Manager=KDevCMakeManager 4 | Name=Piper 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](piper.png) 2 | 3 | Piper is a C++/Qt5 library that let your easily create and edit nodes based graphs. 4 | Piper is under the CeCILL-C licence. 5 | 6 | ### 7 | ## Requirement 8 | You need Qt5 and CMake 3.10 or higher 9 | 10 | ### 11 | ## Build instructions 12 | 13 | cd /your/project/build 14 | cmake ../ 15 | make 16 | 17 | ./editor 18 | 19 | ### 20 | ## Example 21 | main.cc is an example of how to use the Piper library in an application. 22 | Note that the example editor need works to be user friendly (i.e. the only way to create a new node is to press '=' key while the scene has the focus). 23 | -------------------------------------------------------------------------------- /data/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": 3 | { 4 | "name": 5 | { 6 | "font": 7 | { 8 | "name": "Noto", 9 | "weight": "Bold", 10 | "size": 12, 11 | "rgba": [255, 255, 255, 255] 12 | } 13 | }, 14 | "border": 15 | { 16 | "normal": 17 | { 18 | "rgba": [50, 50, 50, 255] 19 | }, 20 | "selected": 21 | { 22 | "rgba": [170, 80, 80, 255] 23 | } 24 | }, 25 | "background": 26 | { 27 | "rgba": [80, 80, 80, 255] 28 | }, 29 | "type": 30 | { 31 | "font": 32 | { 33 | "name": "Noto", 34 | "weight": "Normal", 35 | "size": 11, 36 | "rgba": [220, 220, 220, 255] 37 | }, 38 | "background": 39 | { 40 | "rgba": [50, 50, 50, 160] 41 | } 42 | } 43 | }, 44 | "attribute": 45 | { 46 | "minimize": 47 | { 48 | "font": 49 | { 50 | "name": "Noto", 51 | "weight": "Light", 52 | "size": 10, 53 | "rgba": [220, 220, 220, 255] 54 | }, 55 | "connector": 56 | { 57 | "rgba": [0, 0, 0, 255], 58 | "width": 1 59 | } 60 | }, 61 | "normal": 62 | { 63 | "font": 64 | { 65 | "name": "Noto", 66 | "weight": "Normal", 67 | "size": 10, 68 | "rgba": [220, 220, 220, 255] 69 | }, 70 | "connector": 71 | { 72 | "rgba": [0, 0, 0, 255], 73 | "width": 1 74 | } 75 | }, 76 | "highlight": 77 | { 78 | "font": 79 | { 80 | "name": "Noto", 81 | "weight": "Medium", 82 | "size": 10, 83 | "rgba": [220, 220, 220, 255] 84 | }, 85 | "connector": 86 | { 87 | "rgba": [250, 250, 250, 255], 88 | "width": 2 89 | } 90 | }, 91 | "background": 92 | { 93 | "rgba": [60, 60, 60, 255] 94 | }, 95 | "background_alt": 96 | { 97 | "rgba": [70, 70, 70, 255] 98 | } 99 | }, 100 | "data_type": 101 | { 102 | "enable_default": 103 | { 104 | "rgba": [255, 155, 0, 255] 105 | }, 106 | "disable": 107 | { 108 | "rgba": [80, 80, 80, 255] 109 | }, 110 | "neutral": 111 | { 112 | "rgba": [51, 190, 255, 255] 113 | }, 114 | 115 | "enable_custom": 116 | { 117 | "customType": { "rgba": [ 0, 179, 57, 255] } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /piper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leducp/Piper/a51c0cbdbe4c864d279d35aa307b1763ee01bc37/piper.png -------------------------------------------------------------------------------- /ressources/add_tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | Duplicate Add Mono 28 | 29 | 30 | 31 | 33 | 57 | Duplicate Add Mono 59 | Created with Sketch. 61 | 65 | 68 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /ressources/arrow-down-disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 49 | -------------------------------------------------------------------------------- /ressources/arrow-down-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 49 | -------------------------------------------------------------------------------- /ressources/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 45 | 50 | -------------------------------------------------------------------------------- /ressources/arrow-up-disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 49 | -------------------------------------------------------------------------------- /ressources/arrow-up-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 49 | -------------------------------------------------------------------------------- /ressources/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 45 | 50 | -------------------------------------------------------------------------------- /ressources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | add_tab.svg 4 | star.svg 5 | 6 | 7 | style.qss 8 | arrow-up.svg 9 | arrow-up-hover.svg 10 | arrow-up-disabled.svg 11 | arrow-down.svg 12 | arrow-down-hover.svg 13 | arrow-down-disabled.svg 14 | 15 | 16 | -------------------------------------------------------------------------------- /ressources/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 55 | 56 | -------------------------------------------------------------------------------- /ressources/style.qss: -------------------------------------------------------------------------------- 1 | QLineEdit 2 | { 3 | background-color: rgba(0, 0, 0, 0); 4 | border: 0.1ex; 5 | padding: 0.3ex; 6 | border-radius: 0.2ex; 7 | } 8 | 9 | QAbstractSpinBox 10 | { 11 | background-color: rgba(0, 0, 0, 0); 12 | border: 0.1ex; 13 | padding: 0.3ex; 14 | min-width: 7.5ex; 15 | border-radius: 0.2ex; 16 | } 17 | 18 | QSpinBox::up-button, 19 | QDoubleSpinBox::up-button 20 | { 21 | subcontrol-origin: content; 22 | subcontrol-position: right top; 23 | width: 10px; 24 | height: 7px; 25 | border-width: 1px; 26 | } 27 | 28 | QSpinBox::down-button, 29 | QDoubleSpinBox::down-button 30 | { 31 | subcontrol-origin: content; 32 | subcontrol-position: right bottom; 33 | 34 | width: 10px; 35 | height: 7px; 36 | border-width: 1px; 37 | border-top-width: 0; 38 | } 39 | 40 | QSpinBox::up-arrow, 41 | QDoubleSpinBox::up-arrow 42 | { 43 | border-image: url(:/arrow-up.svg); 44 | } 45 | 46 | QSpinBox::up-arrow:hover, 47 | QSpinBox::up-arrow:pressed, 48 | QDoubleSpinBox::up-arrow:hover, 49 | QDoubleSpinBox::up-arrow:pressed 50 | { 51 | border-image: url(:/arrow-up-hover.svg); 52 | } 53 | 54 | QSpinBox::up-arrow:disabled, 55 | QSpinBox::up-arrow:off, 56 | QDoubleSpinBox::up-arrow:disabled, 57 | QDoubleSpinBox::up-arrow:off 58 | { 59 | border-image: url(:/arrow-up-disabled.svg); 60 | } 61 | 62 | QSpinBox::down-arrow, 63 | QDoubleSpinBox::down-arrow 64 | { 65 | border-image: url(:/arrow-down.svg); 66 | } 67 | 68 | QSpinBox::down-arrow:hover, 69 | QSpinBox::down-arrow:pressed, 70 | QDoubleSpinBox::down-arrow:hover, 71 | QDoubleSpinBox::down-arrow:pressed 72 | { 73 | border-image: url(:/arrow-down-hover.svg); 74 | } 75 | 76 | QSpinBox::down-arrow:disabled, 77 | QSpinBox::down-arrow:off, 78 | QDoubleSpinBox::down-arrow:disabled, 79 | QDoubleSpinBox::down-arrow:off 80 | { 81 | border-image: url(:/arrow-down-disabled.svg); 82 | } 83 | -------------------------------------------------------------------------------- /src/Attribute.cc: -------------------------------------------------------------------------------- 1 | #include "Attribute.h" 2 | #include "Link.h" 3 | #include "Node.h" 4 | #include "Scene.h" 5 | #include "ThemeManager.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | namespace piper 16 | { 17 | QDataStream& operator<<(QDataStream& out, AttributeInfo const& info) 18 | { 19 | out << info.name << info.dataType << info.type; 20 | return out; 21 | } 22 | 23 | 24 | QDataStream& operator>>(QDataStream& in, AttributeInfo& info) 25 | { 26 | int type; 27 | in >> info.name >> info.dataType >> type; 28 | info.type = static_cast(type); 29 | return in; 30 | } 31 | 32 | 33 | Attribute::Attribute(QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect) 34 | : QGraphicsItem(parent) 35 | , info_{info} 36 | , bounding_rect_{boundingRect} 37 | , background_rect_{bounding_rect_} 38 | , label_rect_{ 39 | bounding_rect_.left() + 15, bounding_rect_.top(), bounding_rect_.width() - 30, bounding_rect_.height()} 40 | { 41 | AttributeTheme theme = ThemeManager::instance().getAttributeTheme(); 42 | DataTypeTheme typeTheme = ThemeManager::instance().getDataTypeTheme(dataType()); 43 | 44 | minimize_pen_.setStyle(Qt::SolidLine); 45 | minimize_pen_.setWidth(theme.minimize.connector.border_width); 46 | minimize_pen_.setColor(theme.minimize.connector.border_color); 47 | minimize_font_ = theme.minimize.font; 48 | minimize_font_pen_.setStyle(Qt::SolidLine); 49 | minimize_font_pen_.setColor(theme.minimize.font_color); 50 | 51 | normal_pen_.setStyle(Qt::SolidLine); 52 | normal_pen_.setWidth(theme.normal.connector.border_width); 53 | normal_pen_.setColor(theme.normal.connector.border_color); 54 | normal_brush_.setStyle(Qt::SolidPattern); 55 | normal_brush_.setColor(typeTheme.enable); 56 | normal_font_ = theme.normal.font; 57 | normal_font_pen_.setStyle(Qt::SolidLine); 58 | normal_font_pen_.setColor(theme.normal.font_color); 59 | 60 | highlight_pen_.setStyle(Qt::SolidLine); 61 | highlight_pen_.setWidth(theme.highlight.connector.border_width); 62 | highlight_pen_.setColor(theme.highlight.connector.border_color); 63 | highlight_brush_.setStyle(Qt::SolidPattern); 64 | highlight_brush_.setColor(typeTheme.enable); 65 | highlight_font_ = theme.highlight.font; 66 | highlight_font_pen_.setStyle(Qt::SolidLine); 67 | highlight_font_pen_.setColor(theme.highlight.font_color); 68 | 69 | prepareGeometryChange(); 70 | } 71 | 72 | 73 | Attribute::~Attribute() 74 | { 75 | // Disconnect related links. 76 | QVector linksCopy = links_; //Create a copy: links_ as the list is altered while looping 77 | for (auto& link : linksCopy) 78 | { 79 | link->disconnect(); 80 | } 81 | } 82 | 83 | 84 | void Attribute::setColor(QColor const& color) 85 | { 86 | normal_brush_.setColor(color); 87 | highlight_brush_.setColor(color); 88 | update(); 89 | } 90 | 91 | 92 | void Attribute::connect(Link* link) 93 | { 94 | DataTypeTheme typeTheme = ThemeManager::instance().getDataTypeTheme(dataType()); 95 | links_.append(link); 96 | link->setColor(typeTheme.enable); 97 | } 98 | 99 | 100 | void Attribute::updateRectSize(QRectF rectangle) 101 | { 102 | bounding_rect_ = rectangle; 103 | background_rect_ = bounding_rect_; 104 | } 105 | 106 | 107 | void Attribute::refresh() 108 | { 109 | for (auto& link : links_) 110 | { 111 | link->updatePath(); 112 | } 113 | } 114 | 115 | 116 | void Attribute::applyFontStyle(QPainter* painter, DisplayMode mode) 117 | { 118 | switch (mode) 119 | { 120 | case DisplayMode::highlight: 121 | { 122 | painter->setFont(highlight_font_); 123 | painter->setPen(highlight_font_pen_); 124 | break; 125 | } 126 | case DisplayMode::normal: 127 | { 128 | painter->setFont(normal_font_); 129 | painter->setPen(normal_font_pen_); 130 | break; 131 | } 132 | case DisplayMode::minimize: 133 | { 134 | painter->setFont(minimize_font_); 135 | painter->setPen(minimize_font_pen_); 136 | break; 137 | } 138 | } 139 | } 140 | 141 | void Attribute::applyStyle(QPainter* painter, DisplayMode mode) 142 | { 143 | switch (mode) 144 | { 145 | case DisplayMode::highlight: 146 | { 147 | painter->setBrush(highlight_brush_); 148 | painter->setPen(highlight_pen_); 149 | break; 150 | } 151 | case DisplayMode::normal: 152 | { 153 | painter->setBrush(normal_brush_); 154 | painter->setPen(normal_pen_); 155 | break; 156 | } 157 | case DisplayMode::minimize: 158 | { 159 | painter->setBrush(background_brush_); 160 | painter->setPen(minimize_pen_); 161 | break; 162 | } 163 | } 164 | } 165 | 166 | 167 | void Attribute::paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) 168 | { 169 | // NodeAttribute background. 170 | painter->setBrush(background_brush_); 171 | painter->setPen(Qt::NoPen); 172 | painter->drawRect(background_rect_); 173 | 174 | // NodeAttribute label. 175 | applyFontStyle(painter, mode_); 176 | painter->drawText(QPoint(bounding_rect_.left() + 15, bounding_rect_.top() + 20), name()); 177 | int labelWitdth = painter->fontMetrics().width(name()); 178 | label_rect_.setWidth(labelWitdth); 179 | } 180 | 181 | 182 | 183 | AttributeOutput::AttributeOutput(QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect) 184 | : Attribute(parent, info, boundingRect) 185 | { 186 | updateConnectorPosition(); 187 | 188 | // Use data member to store connector position. 189 | setData(false); 190 | 191 | // Update bounding rect to include connector positions 192 | bounding_rect_ = bounding_rect_.united(connector_rect_left_); 193 | bounding_rect_ = bounding_rect_.united(connector_rect_right_); 194 | bounding_rect_ += QMargins(20, 0, 20, 0); 195 | prepareGeometryChange(); 196 | } 197 | 198 | 199 | void AttributeOutput::updateConnectorPosition() 200 | { 201 | // Compute connector rectangle. 202 | qreal const length = bounding_rect_.height() / 4.0; 203 | 204 | connector_rect_left_ = QRectF{bounding_rect_.left() - length - 1, length, length * 2, length * 2}; 205 | 206 | connector_rect_right_ = QRectF{bounding_rect_.right() - length + 1, length, length * 2, length * 2}; 207 | 208 | setData(data_); 209 | } 210 | 211 | 212 | void AttributeOutput::setColor(QColor const& color) 213 | { 214 | Attribute::setColor(color); 215 | for (auto& link : links_) 216 | { 217 | link->setColor(color); 218 | } 219 | } 220 | 221 | 222 | 223 | void AttributeOutput::setData(QVariant const& data) 224 | { 225 | if (data.canConvert(QMetaType::Bool)) 226 | { 227 | data_ = data; 228 | } 229 | else 230 | { 231 | data_ = false; 232 | } 233 | 234 | if (data_.toBool()) 235 | { 236 | connectorRect_ = &connector_rect_left_; 237 | } 238 | else 239 | { 240 | connectorRect_ = &connector_rect_right_; 241 | } 242 | // Compute connector center to position the path. 243 | connectorPos_ = {connectorRect_->x() + connectorRect_->width() / 2.0, 244 | connectorRect_->y() + connectorRect_->height() / 2.0}; 245 | update(); 246 | } 247 | 248 | 249 | void AttributeOutput::paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) 250 | { 251 | // Draw generic part (label and background). 252 | Attribute::paint(painter, nullptr, nullptr); 253 | 254 | applyStyle(painter, mode_); 255 | painter->drawEllipse(*connectorRect_); 256 | } 257 | 258 | 259 | void AttributeOutput::mousePressEvent(QGraphicsSceneMouseEvent* event) 260 | { 261 | Scene* pScene = static_cast(scene()); 262 | 263 | if (connectorRect_->contains(event->pos()) and event->button() == Qt::LeftButton) 264 | { 265 | new_connection_ = new Link(); 266 | new_connection_->connectFrom(this); 267 | new_connection_->setColor(normal_brush_.color()); 268 | pScene->addLink(new_connection_); 269 | 270 | for (auto const& item : pScene->nodes()) 271 | { 272 | item->highlight(this); 273 | } 274 | 275 | return; 276 | } 277 | 278 | if (event->button() == Qt::MiddleButton) 279 | { 280 | setData(not data_.toBool()); 281 | } 282 | 283 | Attribute::mousePressEvent(event); 284 | } 285 | 286 | 287 | void AttributeOutput::mouseMoveEvent(QGraphicsSceneMouseEvent* event) 288 | { 289 | if (new_connection_ == nullptr) 290 | { 291 | // Nothing to do 292 | Attribute::mouseMoveEvent(event); 293 | return; 294 | } 295 | 296 | new_connection_->updatePath(event->scenePos()); 297 | } 298 | 299 | 300 | void AttributeOutput::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) 301 | { 302 | Scene* pScene = static_cast(scene()); 303 | if ((new_connection_ == nullptr) or (event->button() != Qt::LeftButton)) 304 | { 305 | // Nothing to do 306 | Attribute::mouseReleaseEvent(event); 307 | return; 308 | } 309 | 310 | // Disable highlight 311 | for (auto const& item : pScene->nodes()) 312 | { 313 | item->unhighlight(); 314 | } 315 | 316 | AttributeInput* input = qgraphicsitem_cast(scene()->itemAt(event->scenePos(), QTransform())); 317 | if (input != nullptr) 318 | { 319 | if (input->accept(this)) 320 | { 321 | new_connection_->connectTo(input); 322 | new_connection_ = nullptr; // connection finished. 323 | return; 324 | } 325 | } 326 | 327 | // cleanup unfinalized connection. 328 | delete new_connection_; 329 | new_connection_ = nullptr; 330 | } 331 | 332 | 333 | 334 | AttributeInput::AttributeInput(QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect) 335 | : Attribute(parent, info, boundingRect) 336 | { 337 | data_ = false; // Use data member to store connector position. 338 | 339 | updateConnectorPosition(); 340 | 341 | bounding_rect_ += QMargins(2, 0, 2, 0); 342 | prepareGeometryChange(); 343 | 344 | setData(false); 345 | } 346 | 347 | 348 | void AttributeInput::updateConnectorPosition() 349 | { 350 | // Compute input inputTriangle_ 351 | qreal length = bounding_rect_.height() / 4.0; 352 | input_triangle_left_[0] = QPointF(bounding_rect_.left() - 1, length); 353 | input_triangle_left_[1] = QPointF(bounding_rect_.left() + length * 1.5, length * 2); 354 | input_triangle_left_[2] = QPointF(bounding_rect_.left() - 1, length * 3); 355 | 356 | input_triangle_right_[0] = QPointF(bounding_rect_.right() + 1, length); 357 | input_triangle_right_[1] = QPointF(bounding_rect_.right() - length * 1.5, length * 2); 358 | input_triangle_right_[2] = QPointF(bounding_rect_.right() + 1, length * 3); 359 | 360 | setData(data_); 361 | } 362 | 363 | 364 | void AttributeInput::setData(QVariant const& data) 365 | { 366 | if (data.canConvert(QMetaType::Bool)) 367 | { 368 | data_ = data; 369 | } 370 | else 371 | { 372 | data_ = false; 373 | } 374 | 375 | if (data_.toBool()) 376 | { 377 | input_triangle_ = input_triangle_right_; 378 | } 379 | else 380 | { 381 | input_triangle_ = input_triangle_left_; 382 | } 383 | 384 | // Compute connector center to position the path. 385 | qreal x = input_triangle_[0].x(); 386 | qreal y = input_triangle_[2].y() - input_triangle_[0].y(); 387 | connectorPos_ = {x, y}; 388 | update(); 389 | } 390 | 391 | bool AttributeInput::accept(Attribute* attribute) const 392 | { 393 | if (attribute->dataType() != dataType()) 394 | { 395 | // Incompatible type. 396 | return false; 397 | } 398 | 399 | if (attribute->parentItem() == parentItem()) 400 | { 401 | // can't be connected to another attribute of the same item. 402 | return false; 403 | } 404 | 405 | for (auto& link : links_) 406 | { 407 | if (link->from() == attribute) 408 | { 409 | // We are already connected to this guy. 410 | return false; 411 | } 412 | } 413 | 414 | return true; 415 | } 416 | 417 | 418 | void AttributeInput::paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) 419 | { 420 | // Draw generic part (label and background). 421 | Attribute::paint(painter, nullptr, nullptr); 422 | 423 | applyStyle(painter, mode_); 424 | painter->drawConvexPolygon(input_triangle_, 3); 425 | } 426 | 427 | 428 | void AttributeInput::mousePressEvent(QGraphicsSceneMouseEvent* event) 429 | { 430 | if (event->button() == Qt::MiddleButton) 431 | { 432 | setData(not data_.toBool()); 433 | } 434 | Attribute::mousePressEvent(event); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /src/Attribute.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_ATTRIBUTE_H 2 | #define PIPER_ATTRIBUTE_H 3 | 4 | #include 5 | #include 6 | 7 | namespace piper 8 | { 9 | class Link; 10 | 11 | struct AttributeInfo 12 | { 13 | QString name; 14 | QString dataType; 15 | enum Type 16 | { 17 | input = 0, 18 | output = 1, 19 | member = 2 20 | } type; 21 | }; 22 | 23 | QDataStream& operator<<(QDataStream& out, AttributeInfo const& info); 24 | QDataStream& operator>>(QDataStream& in, AttributeInfo& info); 25 | 26 | enum DisplayMode 27 | { 28 | minimize, 29 | normal, 30 | highlight 31 | }; 32 | 33 | 34 | class Attribute : public QGraphicsItem 35 | { 36 | public: 37 | Attribute (QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect); 38 | virtual ~Attribute(); 39 | 40 | AttributeInfo const& info() const { return info_; } 41 | QString const& name() const { return info_.name; } 42 | QString const& dataType() const { return info_.dataType; } 43 | bool isInput() const { return (info_.type == AttributeInfo::Type::input); } 44 | bool isOutput() const { return (info_.type == AttributeInfo::Type::output); } 45 | bool isMember() const { return (info_.type == AttributeInfo::Type::member); } 46 | 47 | void setBackgroundBrush(QBrush const& brush) { background_brush_ = brush; } 48 | virtual void setColor(QColor const& color); 49 | virtual void updateConnectorPosition(){} 50 | 51 | 52 | virtual QPointF connectorPos() const { return QPointF{}; } 53 | virtual bool accept(Attribute*) const { return false; } 54 | void connect(Link* link); 55 | void disconnect(Link* link) { links_.removeAll(link); } 56 | void refresh(); 57 | 58 | // Highlight compatible attributes and geyed out other. 59 | void highlight(); 60 | 61 | // Revert back the highlight state. 62 | void unhighlight(); 63 | 64 | QVariant const& data() const { return data_; } 65 | virtual void setData(QVariant const& data) { data_ = data; } 66 | 67 | void setMode(DisplayMode mode) { mode_ = mode; } 68 | virtual void updateRectSize(QRectF rectangle); 69 | QRectF boundingRect() const override { return bounding_rect_; } 70 | QRectF labelRect() const { return label_rect_; } 71 | virtual qreal getFormBaseWidth() const { return 0; }; 72 | 73 | 74 | // Enable the use of qgraphicsitem_cast with this item. 75 | enum { Type = UserType + 1 }; 76 | int type() const override { return Type; } 77 | 78 | protected: 79 | void paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) override; 80 | 81 | void applyFontStyle(QPainter* painter, DisplayMode mode); 82 | void applyStyle(QPainter* painter, DisplayMode mode); 83 | 84 | AttributeInfo info_; 85 | QVariant data_; 86 | DisplayMode mode_{DisplayMode::normal}; 87 | 88 | QBrush background_brush_; 89 | 90 | QFont minimize_font_; 91 | QPen minimize_font_pen_; 92 | QPen minimize_pen_; 93 | 94 | QFont normal_font_; 95 | QPen normal_font_pen_; 96 | QBrush normal_brush_; 97 | QPen normal_pen_; 98 | 99 | QFont highlight_font_; 100 | QPen highlight_font_pen_; 101 | QBrush highlight_brush_; 102 | QPen highlight_pen_; 103 | 104 | QRectF bounding_rect_; 105 | QRectF background_rect_; 106 | QRectF label_rect_; 107 | 108 | QVector links_; 109 | }; 110 | 111 | 112 | class AttributeOutput : public Attribute 113 | { 114 | public: 115 | AttributeOutput(QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect); 116 | virtual ~AttributeOutput() = default; 117 | 118 | void setColor(QColor const& color) override; 119 | void updateConnectorPosition() override; 120 | void setData(QVariant const& data) override; 121 | QPointF connectorPos() const override { return mapToScene(connectorPos_); } 122 | 123 | protected: 124 | void paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) override; 125 | void mousePressEvent(QGraphicsSceneMouseEvent* event) override; 126 | void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; 127 | void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; 128 | 129 | QRectF connector_rect_left_; 130 | QRectF connector_rect_right_; 131 | QRectF* connectorRect_; 132 | QPointF connectorPos_; 133 | 134 | Link* new_connection_{nullptr}; 135 | }; 136 | 137 | 138 | class AttributeInput : public Attribute 139 | { 140 | public: 141 | AttributeInput(QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect); 142 | virtual ~AttributeInput() = default; 143 | 144 | void setData(QVariant const& data) override; 145 | void updateConnectorPosition() override; 146 | bool accept(Attribute* attribute) const override; 147 | QPointF connectorPos() const override { return mapToScene(connectorPos_); } 148 | 149 | protected: 150 | void paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) override; 151 | void mousePressEvent(QGraphicsSceneMouseEvent* event) override; 152 | 153 | QPointF input_triangle_left_[3]; 154 | QPointF input_triangle_right_[3]; 155 | QPointF* input_triangle_; 156 | QPointF connectorPos_; 157 | }; 158 | } 159 | 160 | #endif 161 | -------------------------------------------------------------------------------- /src/AttributeMember.cc: -------------------------------------------------------------------------------- 1 | #include "AttributeMember.h" 2 | #include "Scene.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace piper 9 | { 10 | MemberForm::MemberForm(QGraphicsItem* parent, QVariant& data, QRectF const& boundingRect, QBrush const& brush) 11 | : QGraphicsProxyWidget(parent) 12 | , data_{data} 13 | , bounding_rect_{boundingRect} 14 | , brush_{brush} 15 | { 16 | } 17 | 18 | void MemberForm::updateFormWidth(qreal width) 19 | { 20 | bounding_rect_.setWidth(width); 21 | widget()->resize(static_cast(width), static_cast(bounding_rect_.height())); 22 | } 23 | 24 | void MemberForm::paint(QPainter* painter, QStyleOptionGraphicsItem const* option, QWidget* widget) 25 | { 26 | painter->setPen(Qt::NoPen); 27 | painter->setBrush(brush_); 28 | painter->drawRoundedRect(bounding_rect_, 8, 8); 29 | 30 | QGraphicsProxyWidget::paint(painter, option, widget); 31 | } 32 | 33 | void MemberForm::mousePressEvent(QGraphicsSceneMouseEvent* event) 34 | { 35 | (void)event; 36 | Scene* pScene = static_cast(scene()); 37 | 38 | for (auto& item : pScene->selectedItems()) 39 | { 40 | item->setSelected(false); 41 | } 42 | } 43 | 44 | void MemberForm::onDataUpdated(QVariant const& data) 45 | { 46 | data_ = data; 47 | int width = widget()->fontMetrics().boundingRect(data.toString()).width(); 48 | AttributeMember* member = qgraphicsitem_cast(parentItem()); 49 | member->setFormBaseWidth(static_cast(width)); 50 | } 51 | 52 | 53 | AttributeMember::AttributeMember(QGraphicsItem* parent, AttributeInfo const& info, const QRect& boundingRect) 54 | : Attribute(parent, info, boundingRect) 55 | { 56 | // Construct the form (area, background color, widget, widgets options etc). 57 | QRectF formRect{0, 0, bounding_rect_.width(), bounding_rect_.height() - 10}; 58 | QBrush brush{{180, 180, 180, 255}, Qt::SolidPattern}; 59 | form_ = new MemberForm(this, data_, formRect, brush); 60 | baseWidth_ = 0; 61 | 62 | QWidget* widget = createWidget(); 63 | if (widget != nullptr) 64 | { 65 | widget->setFont(normal_font_); 66 | widget->resize(static_cast(formRect.width()), static_cast(formRect.height())); 67 | 68 | QFile File(":/style.qss"); 69 | File.open(QFile::ReadOnly); 70 | QString StyleSheet = QLatin1String(File.readAll()); 71 | widget->setStyleSheet(StyleSheet); 72 | form_->setWidget(widget); 73 | } 74 | form_->setPos(boundingRect.right() - formRect.width(), label_rect_.top() + 5); 75 | } 76 | 77 | void AttributeMember::setFormBaseWidth(qreal width) 78 | { 79 | baseWidth_ = width; 80 | } 81 | 82 | void AttributeMember::updateRectSize(QRectF rectangle) 83 | { 84 | bounding_rect_ = rectangle; 85 | background_rect_ = bounding_rect_; 86 | form_->updateFormWidth(bounding_rect_.width() - label_rect_.width() - 20); 87 | form_->setPos(bounding_rect_.right() - form_->boundingRect().width(), label_rect_.top() + 5); 88 | } 89 | 90 | void AttributeMember::setData(QVariant const& data) 91 | { 92 | switch (data.type()) 93 | { 94 | //QJsonValue always returns QVariant::Double with numbers 95 | //send both signals, the widget with corresponding type will receive values 96 | case QVariant::Int: 97 | case QVariant::Double: 98 | { 99 | form_->dataUpdated(data.toInt()); 100 | form_->dataUpdated(data.toDouble()); 101 | break; 102 | } 103 | case QVariant::String: 104 | { 105 | form_->dataUpdated(data.toString()); 106 | break; 107 | } 108 | default: 109 | { 110 | qDebug() << "Incompatible type: " << data << ". Do nothing"; 111 | } 112 | } 113 | } 114 | 115 | QWidget* AttributeMember::createWidget() 116 | { 117 | QStringList supportedTypes; 118 | 119 | supportedTypes << "int" 120 | << "integer" 121 | << "int32_t" 122 | << "int64_t"; 123 | if (supportedTypes.contains(dataType())) 124 | { 125 | QSpinBox* box = new QSpinBox(); 126 | data_ = box->value(); 127 | box->setMaximum(std::numeric_limits::max()); 128 | box->setMinimum(std::numeric_limits::min()); 129 | QObject::connect(box, QOverload::of(&QSpinBox::valueChanged), form_, &MemberForm::onDataUpdated); 130 | QObject::connect(form_, SIGNAL(dataUpdated(int)), box, SLOT(setValue(int))); 131 | return box; 132 | } 133 | 134 | supportedTypes.clear(); 135 | supportedTypes << "float" 136 | << "double" 137 | << "real" 138 | << "float32_t" 139 | << "float64_t"; 140 | if (supportedTypes.contains(dataType())) 141 | { 142 | QDoubleSpinBox* box = new QDoubleSpinBox(); 143 | data_ = box->value(); 144 | box->setMaximum(std::numeric_limits::max()); 145 | box->setMinimum(-std::numeric_limits::max()); 146 | box->setDecimals(10); 147 | QObject::connect( 148 | box, QOverload::of(&QDoubleSpinBox::valueChanged), form_, &MemberForm::onDataUpdated); 149 | QObject::connect(form_, SIGNAL(dataUpdated(double)), box, SLOT(setValue(double))); 150 | return box; 151 | } 152 | 153 | supportedTypes.clear(); 154 | supportedTypes << "string"; 155 | if (supportedTypes.contains(dataType())) 156 | { 157 | QLineEdit* lineEdit = new QLineEdit(); 158 | data_ = lineEdit->text(); 159 | lineEdit->setFont(normal_font_); 160 | QObject::connect(lineEdit, &QLineEdit::textChanged, form_, &MemberForm::onDataUpdated); 161 | QObject::connect(form_, SIGNAL(dataUpdated(QString const&)), lineEdit, SLOT(setText(QString const&))); 162 | return lineEdit; 163 | } 164 | 165 | return nullptr; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/AttributeMember.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_ATTRIBUTE_MEMBER_H 2 | #define PIPER_ATTRIBUTE_MEMBER_H 3 | 4 | #include "Attribute.h" 5 | 6 | #include 7 | 8 | 9 | namespace piper 10 | { 11 | /// \brief Handle the member form (draw the backround and own the dedicated QWidget) 12 | class MemberForm : public QGraphicsProxyWidget 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | MemberForm(QGraphicsItem* parent, QVariant& data, QRectF const& boundingRect, QBrush const& brush); 18 | virtual ~MemberForm() = default; 19 | 20 | QRectF boundingRect() const override { return bounding_rect_; } 21 | void updateFormWidth(qreal data); 22 | 23 | signals: 24 | void dataUpdated(int); 25 | void dataUpdated(double); 26 | void dataUpdated(QString const&); 27 | 28 | public slots: 29 | void onDataUpdated(QVariant const& data); 30 | 31 | protected: 32 | void paint(QPainter* painter, QStyleOptionGraphicsItem const*, QWidget*) override; 33 | void mousePressEvent(QGraphicsSceneMouseEvent* event) override; 34 | 35 | private: 36 | QVariant& data_; // reference on attribute's data 37 | QRectF bounding_rect_; 38 | QBrush brush_; 39 | }; 40 | 41 | ///\brief A Node attribute that can be edited by the user. 42 | class AttributeMember : public Attribute 43 | { 44 | public: 45 | AttributeMember(QGraphicsItem* parent, AttributeInfo const& info, QRect const& boundingRect); 46 | virtual ~AttributeMember() = default; 47 | 48 | // Set the data by working closely with the MemberForm class. 49 | void setData(QVariant const& data) override; 50 | void updateRectSize(QRectF rectangle) override; 51 | void setFormBaseWidth(qreal width); 52 | qreal getFormBaseWidth() const override { return baseWidth_; }; 53 | 54 | private: 55 | QWidget* createWidget(); 56 | 57 | MemberForm* form_; 58 | qreal baseWidth_; 59 | }; 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /src/CreatorPopup.cc: -------------------------------------------------------------------------------- 1 | #include "CreatorPopup.h" 2 | #include "NodeCreator.h" 3 | #include "Scene.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace piper 11 | { 12 | CreatorPopup::CreatorPopup(View* view) 13 | : QLineEdit(view) 14 | , view_{view} 15 | { 16 | model_ = new QStringListModel(); 17 | QCompleter* completer = new QCompleter(model_, this); 18 | completer->setCaseSensitivity(Qt::CaseInsensitive); 19 | completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); 20 | completer->setMaxVisibleItems(20); 21 | completer->popup()->setStyleSheet( 22 | "background:transparent; " 23 | "border: 1px solid #ff9b00; " 24 | "color: #F2F2F2; " 25 | "selection-background-color: #E68A00; " 26 | ); 27 | setCompleter(completer); 28 | setStyleSheet( 29 | "QLineEdit { " 30 | "background:transparent; " 31 | "border: 1px solid #ff9b00; " 32 | "color: #F2F2F2; " 33 | "}" 34 | ); 35 | 36 | QObject::connect(this, &QLineEdit::returnPressed, this, &CreatorPopup::onReturnPressed); 37 | 38 | popdown(); 39 | } 40 | 41 | 42 | void CreatorPopup::popup() 43 | { 44 | // Adjust size and populate content. 45 | QStringList types; 46 | QSize targetSize = size(); 47 | for (auto const& item : NodeCreator::instance().availableItems()) 48 | { 49 | QSize fontSize = fontMetrics().boundingRect(item.type).size(); 50 | types << item.type; 51 | targetSize.setWidth(std::max(targetSize.width(), fontSize.width() + 30)); // +30px for margin 52 | } 53 | model_->setStringList(types); 54 | resize(targetSize); 55 | 56 | QPoint position = parentWidget()->mapFromGlobal(QCursor::pos()); 57 | move(position); 58 | clear(); 59 | show(); 60 | setFocus(); 61 | 62 | // display all solution at first glance. 63 | completer()->complete(); 64 | } 65 | 66 | 67 | void CreatorPopup::popdown() 68 | { 69 | hide(); 70 | clear(); 71 | view_->setFocus(); 72 | } 73 | 74 | 75 | void CreatorPopup::onReturnPressed() 76 | { 77 | QPointF scenePos = view_->mapToScene(pos()); 78 | Scene* piperScene = static_cast(view_->scene()); 79 | 80 | QString type = text(); 81 | popdown(); 82 | 83 | QString nextName = type + "_" + QString::number(piperScene->nodes().size()); 84 | Node* node = NodeCreator::instance().createItem(type, nextName, "", scenePos); 85 | if (node != nullptr) 86 | { 87 | piperScene->addNode(node); 88 | } 89 | } 90 | 91 | 92 | void CreatorPopup::focusOutEvent(QFocusEvent*) 93 | { 94 | popdown(); 95 | } 96 | 97 | 98 | bool CreatorPopup::event(QEvent* event) 99 | { 100 | if (event->type() == QEvent::KeyPress) 101 | { 102 | QKeyEvent *keyEvent = static_cast(event); 103 | if (keyEvent->key() == Qt::Key_Tab) 104 | { 105 | QModelIndex index = completer()->currentIndex(); 106 | QString type = model_->itemData(index)[Qt::EditRole].toString(); 107 | setText(type); 108 | return true; 109 | } 110 | } 111 | return QWidget::event(event); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/CreatorPopup.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_CREATOR_POPUP_H 2 | #define PIPER_CREATOR_POPUP_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "View.h" 9 | 10 | namespace piper 11 | { 12 | class CreatorPopup : public QLineEdit 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | CreatorPopup(View* view); 18 | virtual ~CreatorPopup() = default; 19 | 20 | void popup(); 21 | void popdown(); 22 | 23 | public slots: 24 | void onReturnPressed(); 25 | 26 | protected: 27 | void focusOutEvent(QFocusEvent*) override; 28 | bool event(QEvent *event) override; 29 | 30 | private: 31 | QStringListModel* model_; 32 | View* view_; 33 | }; 34 | } 35 | 36 | #endif 37 | 38 | -------------------------------------------------------------------------------- /src/EditorTab.cc: -------------------------------------------------------------------------------- 1 | #include "EditorTab.h" 2 | #include "EditorWidget.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace piper 10 | { 11 | 12 | TabHeaderEdit::TabHeaderEdit(QString const& text) 13 | : QLineEdit(text) 14 | { } 15 | 16 | 17 | void TabHeaderEdit::mousePressEvent(QMouseEvent *event) 18 | { 19 | QLineEdit::mousePressEvent(event); 20 | QWidget::mousePressEvent(event); // let the parent do its work too ! 21 | } 22 | 23 | 24 | TabNameValidator::TabNameValidator(QObject* parent) 25 | : QValidator(parent) 26 | { } 27 | 28 | 29 | QValidator::State TabNameValidator::validate(QString& input, int& pos) const 30 | { 31 | for (auto const& c : input) 32 | { 33 | // Refuse non plain ascii name. 34 | if (c.unicode() > 127) 35 | { 36 | return QValidator::Invalid; 37 | } 38 | } 39 | 40 | return QValidator::Acceptable; 41 | } 42 | 43 | 44 | EditorTab::EditorTab(QWidget* parent) 45 | : QTabWidget(parent) 46 | , tabNameValidator_(new TabNameValidator(this)) 47 | { 48 | // Create adder button 49 | QPushButton* tb = new QPushButton(); 50 | tb->setFlat(true); 51 | tb->setIcon(QIcon(":/icon/add_tab.svg")); 52 | tb->setStyleSheet("* { icon-size: 30px 30px; }"); 53 | setCornerWidget(tb, Qt::TopLeftCorner); 54 | 55 | // Ensure that button will be always visible. 56 | setStyleSheet("QTabBar::tab { height: 40px; }"); 57 | tb->setMinimumSize(40, 40); 58 | tabBar()->setMinimumSize(40, 40); 59 | 60 | // Connect signal to manage tab creation/rename/deletion 61 | QObject::connect(tb, &QPushButton::clicked, this, &EditorTab::createNewEditorTab); 62 | QObject::connect(this, &QTabWidget::tabCloseRequested, this, &EditorTab::closeEditorTab); 63 | } 64 | 65 | 66 | QString EditorTab::name(int32_t index) const 67 | { 68 | TabHeaderEdit* edit = static_cast(tabBar()->tabButton(index, QTabBar::LeftSide)); 69 | return edit->text(); 70 | } 71 | 72 | 73 | void EditorTab::setName(int32_t index, QString const& name) 74 | { 75 | TabHeaderEdit* edit = static_cast(tabBar()->tabButton(index, QTabBar::LeftSide)); 76 | edit->setText(name); 77 | } 78 | 79 | 80 | EditorWidget* EditorTab::createNewEditorTab() 81 | { 82 | EditorWidget* editor = new EditorWidget(); 83 | int32_t index = addTab(editor, ""); 84 | 85 | QString defaultText = "unamed pipeline"; 86 | tabNameValidator_->fixup(defaultText); 87 | 88 | TabHeaderEdit* edit = new TabHeaderEdit(defaultText); 89 | edit->setFrame(false); 90 | edit->setStyleSheet("QLineEdit { background:transparent; }"); 91 | edit->setValidator(tabNameValidator_); 92 | tabBar()->setTabButton(index, QTabBar::LeftSide, edit); 93 | QObject::connect(edit, &QLineEdit::editingFinished, this, &EditorTab::tabNameEdited); 94 | 95 | return editor; 96 | } 97 | 98 | 99 | void EditorTab::closeEditorTab(int32_t index) 100 | { 101 | widget(index)->deleteLater(); // Note: removeTab do not destroy the widget. 102 | } 103 | 104 | 105 | void EditorTab::tabNameEdited() 106 | { 107 | if (currentWidget() != nullptr) 108 | { 109 | currentWidget()->setFocus(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/EditorTab.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_TAB_H 2 | #define PIPER_TAB_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace piper 10 | { 11 | class EditorWidget; 12 | 13 | class TabHeaderEdit : public QLineEdit 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | TabHeaderEdit(QString const& text); 19 | virtual ~TabHeaderEdit() = default; 20 | 21 | void mousePressEvent(QMouseEvent *event) override; 22 | }; 23 | 24 | 25 | class TabNameValidator : public QValidator 26 | { 27 | Q_OBJECT 28 | 29 | public: 30 | TabNameValidator(QObject* parent = nullptr); 31 | QValidator::State validate(QString& input, int& pos) const override; 32 | }; 33 | 34 | 35 | class EditorTab : public QTabWidget 36 | { 37 | Q_OBJECT 38 | public: 39 | EditorTab(QWidget* parent = nullptr); 40 | virtual ~EditorTab() = default; 41 | 42 | QString name(int32_t index) const; 43 | void setName(int32_t index, QString const& name); 44 | 45 | public slots: 46 | EditorWidget* createNewEditorTab(); 47 | void closeEditorTab(int32_t index); 48 | void tabNameEdited(); 49 | 50 | private: 51 | TabNameValidator* tabNameValidator_; 52 | }; 53 | } 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/EditorWidget.cc: -------------------------------------------------------------------------------- 1 | #include "EditorWidget.h" 2 | #include "PropertyDelegate.h" 3 | #include "ui_EditorWidget.h" 4 | 5 | #include "Scene.h" 6 | #include "Node.h" 7 | #include "Link.h" 8 | #include "NodeCreator.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace piper 15 | { 16 | EditorWidget::EditorWidget(QWidget* parent) 17 | : QWidget(parent) 18 | , ui_(new Ui::EditorWidget) 19 | , scene_(new Scene(this)) 20 | { 21 | srand(time(0)); 22 | 23 | ui_->setupUi(this); 24 | ui_->view->setScene(scene_); 25 | 26 | QObject::connect(ui_->stage_add, &QPushButton::clicked, this, &EditorWidget::onAddStage); 27 | QObject::connect(ui_->stage_rm, &QPushButton::clicked, this, &EditorWidget::onRmStage); 28 | QObject::connect(ui_->stage_color, &QPushButton::clicked, this, &EditorWidget::onColorStage); 29 | 30 | ui_->stages->setModel(scene_->stages()); 31 | ui_->stages->setEditTriggers(QAbstractItemView::AnyKeyPressed | 32 | QAbstractItemView::DoubleClicked); 33 | ui_->stages->setDragDropMode(QAbstractItemView::InternalMove); 34 | 35 | ui_->modes->setModel(scene_->modes()); 36 | ui_->modes->setEditTriggers(QAbstractItemView::AnyKeyPressed | 37 | QAbstractItemView::DoubleClicked); 38 | ui_->modes->setDragDropMode(QAbstractItemView::InternalMove); 39 | QObject::connect(ui_->mode_add, &QPushButton::clicked, this, &EditorWidget::onAddMode); 40 | QObject::connect(ui_->mode_rm, &QPushButton::clicked, this, &EditorWidget::onRmMode); 41 | QObject::connect(ui_->modes, &QListView::clicked, scene_, &Scene::onModeSelected); 42 | //QObject::connect(ui_->modes, &QListView::customContextMenuRequested, scene_, &Scene::onModeSetDefault); 43 | QObject::connect(ui_->modes, &QListView::doubleClicked, scene_, &Scene::onModeSetDefault); 44 | 45 | QHash allFrom; 46 | for (auto const& item : NodeCreator::instance().availableItems()) 47 | { 48 | auto it = allFrom.find(item.from); 49 | if (it == allFrom.end()) 50 | { 51 | it = allFrom.insert(item.from, new QTreeWidgetItem({item.from})); 52 | } 53 | 54 | (*it)->addChild(new QTreeWidgetItem({item.type, item.category})); 55 | } 56 | 57 | for (auto const& root : allFrom) 58 | { 59 | ui_->items->addTopLevelItem(root); 60 | } 61 | 62 | QObject::connect(ui_->items, &QTreeWidget::itemDoubleClicked, 63 | [&](QTreeWidgetItem* item) 64 | { 65 | if (item->child(0) != nullptr) 66 | { 67 | // Only leaf are relevants 68 | return; 69 | } 70 | 71 | // Alias for a easier reading 72 | QString const type = item->text(0); 73 | 74 | // Center of the view in the scene coordinates 75 | QPointF const scenePos = ui_->view->mapToScene(ui_->view->viewport()->rect().center()); 76 | 77 | QString nextName = type + "_" + QString::number(scene_->nodes().size()); 78 | Node* node = NodeCreator::instance().createItem(type, nextName, "", scenePos); 79 | if (node != nullptr) 80 | { 81 | scene_->addNode(node); 82 | } 83 | }); 84 | 85 | show(); // required to initialize the view in case of import. 86 | } 87 | 88 | 89 | void EditorWidget::onExport(ExportBackend& backend) 90 | { 91 | scene_->onExport(backend); 92 | } 93 | 94 | 95 | void EditorWidget::onAddStage() 96 | { 97 | QColor nextColor = generateRandomColor(); 98 | 99 | QString nextName = "stage" + QString::number(scene_->stages()->rowCount()); 100 | 101 | // Add item 102 | QStandardItem* item = new QStandardItem(); 103 | item->setData(nextColor, Qt::DecorationRole); 104 | item->setData(nextName, Qt::DisplayRole); 105 | item->setDropEnabled(false);; 106 | scene_->stages()->appendRow(item); 107 | 108 | // Enable item selection and put it edit mode 109 | QModelIndex index = scene_->stages()->indexFromItem(item); 110 | ui_->stages->setCurrentIndex(index); 111 | ui_->stages->edit(index); 112 | } 113 | 114 | 115 | void EditorWidget::onRmStage() 116 | { 117 | int row = ui_->stages->currentIndex().row(); 118 | scene_->stages()->removeRows(row, 1); 119 | scene_->onStageUpdated(); 120 | } 121 | 122 | 123 | void EditorWidget::onColorStage() 124 | { 125 | QModelIndex index = ui_->stages->currentIndex(); 126 | QColor current = scene_->stages()->data(index, Qt::DecorationRole).value(); 127 | 128 | QColor newColor = QColorDialog::getColor(current); 129 | scene_->stages()->setData(index, newColor, Qt::DecorationRole); 130 | } 131 | 132 | 133 | void EditorWidget::onAddMode() 134 | { 135 | QString nextName = "mode" + QString::number(scene_->modes()->rowCount()); 136 | QModelIndex index = scene_->addMode(nextName); 137 | ui_->modes->setCurrentIndex(index); 138 | ui_->modes->edit(index); 139 | } 140 | 141 | 142 | void EditorWidget::onRmMode() 143 | { 144 | int row = ui_->modes->currentIndex().row(); 145 | scene_->modes()->removeRows(row, 1); 146 | } 147 | 148 | 149 | QDataStream& operator<<(QDataStream& out, EditorWidget const& editor) 150 | { 151 | out << *editor.scene_; 152 | return out; 153 | } 154 | 155 | 156 | QDataStream& operator>>(QDataStream& in, EditorWidget& editor) 157 | { 158 | in >> *editor.scene_; 159 | return in; 160 | } 161 | 162 | 163 | void EditorWidget::loadJson(QJsonObject& json) 164 | { 165 | scene_->onImportJson(json); 166 | QModelIndex index = scene_->modes()->index(0, 0); 167 | if (index.isValid()) 168 | { 169 | ui_->modes->setCurrentIndex(index); 170 | scene_->onModeSelected(index); 171 | } 172 | 173 | ui_->view->goHome(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/EditorWidget.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_EDITOR_WIDGET_H 2 | #define PIPER_EDITOR_WIDGET_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Ui 8 | { 9 | class EditorWidget; 10 | } 11 | 12 | namespace piper 13 | { 14 | class Scene; 15 | class ExportBackend; 16 | 17 | class EditorWidget : public QWidget 18 | { 19 | Q_OBJECT 20 | 21 | friend QDataStream& operator<<(QDataStream& out, EditorWidget const& editor); 22 | friend QDataStream& operator>>(QDataStream& in, EditorWidget& editor); 23 | 24 | public: 25 | EditorWidget(QWidget* parent = nullptr); 26 | virtual ~EditorWidget() = default; 27 | 28 | void onExport(ExportBackend& backend); 29 | void loadJson(QJsonObject& json); 30 | 31 | public slots: 32 | void onAddStage(); 33 | void onRmStage(); 34 | void onColorStage(); 35 | 36 | void onAddMode(); 37 | void onRmMode(); 38 | 39 | private: 40 | Ui::EditorWidget* ui_; 41 | Scene* scene_; 42 | }; 43 | 44 | QDataStream& operator<<(QDataStream& out, EditorWidget const& editor); 45 | QDataStream& operator>>(QDataStream& in, EditorWidget& editor); 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/EditorWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EditorWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1025 10 | 704 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Qt::ClickFocus 21 | 22 | 23 | Qt::Horizontal 24 | 25 | 26 | 27 | 28 | 1 29 | 0 30 | 31 | 32 | 33 | Qt::ScrollBarAlwaysOff 34 | 35 | 36 | Qt::ScrollBarAlwaysOff 37 | 38 | 39 | QPainter::Antialiasing|QPainter::HighQualityAntialiasing|QPainter::NonCosmeticDefaultPen|QPainter::SmoothPixmapTransform|QPainter::TextAntialiasing 40 | 41 | 42 | QGraphicsView::FullViewportUpdate 43 | 44 | 45 | 46 | 47 | 2 48 | 49 | 50 | 51 | Stages 52 | 53 | 54 | 55 | 56 | 57 | Add 58 | 59 | 60 | 61 | 62 | 63 | 64 | Rm 65 | 66 | 67 | 68 | 69 | 70 | 71 | Color 72 | 73 | 74 | 75 | 76 | 77 | 78 | QAbstractItemView::NoEditTriggers 79 | 80 | 81 | true 82 | 83 | 84 | true 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Modes 93 | 94 | 95 | 96 | 97 | 98 | Rm 99 | 100 | 101 | 102 | 103 | 104 | 105 | Add 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Page 117 | 118 | 119 | 120 | 121 | 122 | true 123 | 124 | 125 | 2 126 | 127 | 128 | true 129 | 130 | 131 | 200 132 | 133 | 134 | true 135 | 136 | 137 | true 138 | 139 | 140 | 141 | Item 142 | 143 | 144 | 145 | 146 | Category 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | piper::View 161 | QGraphicsView 162 |
View.h
163 |
164 |
165 | 166 | 167 |
168 | -------------------------------------------------------------------------------- /src/ExportBackend.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_EXPORT_BACKEND_H 2 | #define PIPER_EXPORT_BACKEND_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "Types.h" 9 | 10 | namespace piper 11 | { 12 | class ExportBackend 13 | { 14 | public: 15 | // init() is called befre anything else. 16 | virtual void init(QString const& filename) = 0; 17 | 18 | // finalize() is called when the export is finished. 19 | virtual void finalize(QString const& filename) = 0; 20 | 21 | // Start a new pipeline 22 | virtual void startPipeline(QString const& pipelineName) = 0; 23 | 24 | // Called when the pipeline was fully exported. 25 | virtual void endPipeline(QString const& pipelineName) = 0; 26 | 27 | // Stages are written from first to last. 28 | virtual void writeStages(QVector const& stages) = 0; 29 | 30 | // Each node is composed of its metadata and a map attributes/value 31 | virtual void writeNode(QString const& type, QString const& name, QString const& stage, QHash const& attributes) = 0; 32 | 33 | // one call per link 34 | virtual void writeLink(QString const& from, QString const& output, QString const& to, QString const& input, QString const& type) = 0; 35 | 36 | // one call per mode 37 | virtual void writeMode(QString const& name, QHash const& config) = 0; 38 | 39 | // one call per pipeline. 40 | virtual void writeDefaultMode(QString const& name) = 0; 41 | }; 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/JsonExport.cc: -------------------------------------------------------------------------------- 1 | #include "JsonExport.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace piper 8 | { 9 | void JsonExport::init(QString const&) 10 | { 11 | 12 | } 13 | 14 | void JsonExport::finalize(QString const& filename) 15 | { 16 | QJsonDocument document(root_); 17 | 18 | QFile io(filename); 19 | if (not io.open(QIODevice::WriteOnly)) 20 | { 21 | qDebug() << "Error while opening" << io.fileName(); 22 | return; 23 | } 24 | 25 | io.write(document.toJson()); 26 | } 27 | 28 | void JsonExport::startPipeline(QString const&) 29 | { 30 | pipeline_ = QJsonObject(); // cleanup 31 | nodes_ = QJsonObject(); 32 | links_ = QJsonArray(); 33 | modes_ = QJsonObject(); 34 | } 35 | 36 | void JsonExport::endPipeline(QString const& pipelineName) 37 | { 38 | pipeline_["Nodes"] = nodes_; 39 | pipeline_["Links"] = links_; 40 | pipeline_["Modes"] = modes_; 41 | root_[pipelineName] = pipeline_; 42 | } 43 | 44 | 45 | void JsonExport::writeStages(QVector const& stages) 46 | { 47 | QJsonArray stagesArray; 48 | for (auto const& stage : stages) 49 | { 50 | stagesArray.append(stage); 51 | } 52 | pipeline_["Stages"] = stagesArray; 53 | } 54 | 55 | 56 | void JsonExport::writeNode(QString const& type, QString const& name, QString const& stage, QHash const& attributes) 57 | { 58 | QJsonObject node; 59 | for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) 60 | { 61 | if (it.key() == "type") { qWarning() << "type is a reserved attribute. Skipping."; continue; } 62 | if (it.key() == "stage") { qWarning() << "stage is a reserved attribute. Skipping."; continue; } 63 | node[it.key()] = QJsonValue::fromVariant(it.value()); 64 | } 65 | 66 | node["type"] = type; 67 | node["stage"] = stage; 68 | nodes_[name] = node; 69 | } 70 | 71 | 72 | void JsonExport::writeLink(QString const& from, QString const& output, QString const& to, QString const& input, QString const& type) 73 | { 74 | QJsonObject link; 75 | link["from"] = from; 76 | link["out"] = output; 77 | link["to"] = to; 78 | link["in"] = input; 79 | link["type"] = type; 80 | links_.append(link); 81 | } 82 | 83 | 84 | void JsonExport::writeMode(QString const& name, QHash const& config) 85 | { 86 | QJsonObject mode; 87 | QJsonObject configuration; 88 | mode["default"] = "Enable"; 89 | for (auto it = config.constBegin(); it != config.constEnd(); ++it) 90 | { 91 | QString modeString; 92 | switch (it.value()) 93 | { 94 | case Mode::enable: { continue; } 95 | case Mode::disable: { modeString = "Disable"; break; } 96 | case Mode::neutral: { modeString = "Neutral"; break; } 97 | default: { continue; } 98 | } 99 | configuration[it.key()] = modeString; 100 | } 101 | mode["configuration"] = configuration; 102 | modes_[name] = mode; 103 | } 104 | 105 | void JsonExport::writeDefaultMode(QString const& name) 106 | { 107 | QJsonValue defaultMode = name; 108 | modes_["default"] = defaultMode; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/JsonExport.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_JSON_EXPORT_H 2 | #define PIPER_JSON_EXPORT_H 3 | 4 | #include "ExportBackend.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace piper 10 | { 11 | class JsonExport : public ExportBackend 12 | { 13 | public: 14 | JsonExport() = default; 15 | virtual ~JsonExport() = default; 16 | 17 | // init() is called befre anything else. 18 | void init(QString const& filename) override; 19 | 20 | // finalize() is called when the export is finished. 21 | void finalize(QString const& filename) override; 22 | 23 | // Start a new pipeline 24 | void startPipeline(QString const&) override; 25 | 26 | // Called when the pipeline was fully exported. 27 | void endPipeline(QString const& pipelineName) override; 28 | 29 | // Stages 30 | void writeStages(QVector const& stages) override; 31 | 32 | // Each node is composed of its metadata and a map attributes/value 33 | void writeNode(QString const& type, QString const& name, QString const& stage, QHash const& attributes) override; 34 | 35 | // one call per link 36 | void writeLink(QString const& from, QString const& output, QString const& to, QString const& input, QString const& type) override; 37 | 38 | // Mode 39 | void writeMode(QString const& name, QHash const& config) override; 40 | 41 | // Default mode. 42 | void writeDefaultMode(QString const& name) override; 43 | 44 | private: 45 | QJsonObject root_; 46 | QJsonObject pipeline_; 47 | QJsonObject nodes_; 48 | QJsonArray links_; 49 | QJsonObject modes_; 50 | }; 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/Link.cc: -------------------------------------------------------------------------------- 1 | #include "Link.h" 2 | #include "Node.h" 3 | #include "Scene.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | namespace piper 14 | { 15 | Link::Link() 16 | { 17 | setFlag(QGraphicsItem::ItemIsSelectable); 18 | setFlag(QGraphicsItem::ItemIsFocusable); 19 | 20 | pen_.setStyle(Qt::SolidLine); 21 | pen_.setWidth(2); 22 | 23 | selected_.setStyle(Qt::SolidLine); 24 | selected_.setColor({255, 180, 180, 255}); //TODO theme manager 25 | selected_.setWidth(3); 26 | } 27 | 28 | 29 | Link::~Link() 30 | { 31 | disconnect(); 32 | Scene* pScene = static_cast(scene()); 33 | pScene->removeLink(this); 34 | } 35 | 36 | 37 | void Link::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) 38 | { 39 | if (isSelected()) 40 | { 41 | setPen(selected_); 42 | } 43 | else 44 | { 45 | setPen(pen_); 46 | } 47 | 48 | if (to_ != nullptr) 49 | { 50 | updatePath(); 51 | } 52 | QGraphicsPathItem::paint(painter, option, widget); 53 | } 54 | 55 | 56 | void Link::connectFrom(Attribute* from) 57 | { 58 | from_ = from; 59 | from_->connect(this); 60 | } 61 | 62 | 63 | void Link::connectTo(Attribute* to) 64 | { 65 | to_ = to; 66 | to_->connect(this); 67 | updatePath(); 68 | } 69 | 70 | 71 | void Link::disconnect() 72 | { 73 | if (from_ != nullptr) 74 | { 75 | from_->disconnect(this); 76 | from_ = nullptr; 77 | } 78 | 79 | if (to_ != nullptr) 80 | { 81 | to_->disconnect(this); 82 | to_ = nullptr; 83 | } 84 | } 85 | 86 | 87 | bool Link::isConnected() 88 | { 89 | if ((from_ == nullptr) or (to_ == nullptr)) 90 | { 91 | return false; 92 | } 93 | return true; 94 | } 95 | 96 | 97 | void Link::updatePath() 98 | { 99 | updatePath(to_->connectorPos()); 100 | } 101 | 102 | 103 | void Link::updatePath(QPointF const& end) 104 | { 105 | updatePath(from_->connectorPos(), end); 106 | setZValue(-1); // force path to be under nodes 107 | } 108 | 109 | 110 | void Link::setColor(QColor const& color) 111 | { 112 | pen_.setColor(color); 113 | } 114 | 115 | 116 | void Link::mousePressEvent(QGraphicsSceneMouseEvent* event) 117 | { 118 | Scene* pScene = static_cast(scene()); 119 | 120 | setSelected(true); 121 | 122 | // disconnect from end. 123 | to_->disconnect(this); 124 | 125 | // snap the path end to this point. 126 | updatePath(event->scenePos()); 127 | 128 | // highlight available connections 129 | for (auto& node : pScene->nodes()) 130 | { 131 | node->highlight(from_); 132 | } 133 | } 134 | 135 | 136 | void Link::mouseMoveEvent(QGraphicsSceneMouseEvent* event) 137 | { 138 | // snap the path end to this point. 139 | updatePath(event->scenePos()); 140 | } 141 | 142 | 143 | void Link::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) 144 | { 145 | Scene* pScene = static_cast(scene()); 146 | 147 | // Disable highlight 148 | for (auto& node : pScene->nodes()) 149 | { 150 | node->unhighlight(); 151 | } 152 | 153 | // try to connect to the destinaton. 154 | AttributeInput* input = qgraphicsitem_cast(scene()->itemAt(event->scenePos(), QTransform())); 155 | if (input != nullptr) 156 | { 157 | if (input->accept(from_)) 158 | { 159 | connectTo(input); 160 | } 161 | } 162 | else 163 | { 164 | connectTo(to_); // reset connection 165 | } 166 | } 167 | 168 | 169 | void Link::updatePath(QPointF const& start, QPointF const& end) 170 | { 171 | qreal dx = (end.x() - start.x()) * 0.5; 172 | qreal dy = (end.y() - start.y()); 173 | QPointF c1{start.x() + dx, start.y() + dy * 0}; 174 | QPointF c2{start.x() + dx, start.y() + dy * 1}; 175 | 176 | QPainterPath path; 177 | path.moveTo(start); 178 | path.cubicTo(c1, c2, end); 179 | 180 | setPath(path); 181 | } 182 | 183 | 184 | void Link::computeControlPoint(QPointF const& p0, QPointF const& p1, QPointF const& p2, double t, 185 | QPointF& ctrl1, QPointF& ctrl2) 186 | { 187 | using namespace std; 188 | 189 | double d01 = sqrt(pow(p1.x()-p0.x(), 2) + pow(p1.y() - p0.y(), 2)); 190 | double d12 = sqrt(pow(p2.x()-p1.x(), 2) + pow(p2.y() - p1.y(), 2)); 191 | 192 | double fa = t * d01 / (d01 + d12); // scaling factor for triangle Ta 193 | double fb = t * d12 / (d01 + d12); // ditto for Tb, simplifies to fb=t-fa 194 | 195 | double p1x = p1.x() - fa * (p2.x() - p0.x()); // x2-x0 is the width of triangle T 196 | double p1y = p1.y() - fa * (p2.y() - p0.y()); // y2-y0 is the height of T 197 | ctrl1.setX(p1x); 198 | ctrl1.setY(p1y); 199 | 200 | double p2x = p1.x() + fb * (p2.x() - p0.x()); 201 | double p2y = p1.y() + fb * (p2.y() - p0.y()); 202 | ctrl2.setX(p2x); 203 | ctrl2.setY(p2y); 204 | } 205 | 206 | 207 | void Link::drawSplines(QVector const& waypoints, double t) 208 | { 209 | // Compute control points 210 | QVector controlPoints; 211 | for (int i = 0; i < waypoints.size() - 2; i += 1) 212 | { 213 | QPointF c1, c2; 214 | computeControlPoint(waypoints.at(i), waypoints.at(i + 1), waypoints.at(i + 2), t, 215 | c1, c2); 216 | controlPoints << c1 << c2; 217 | } 218 | auto nextWaypoint = waypoints.cbegin(); 219 | auto ctrl = controlPoints.cbegin(); 220 | 221 | // Prepare path -> first spline is a quadratic bezier curve 222 | QPainterPath path; 223 | path.moveTo(*(nextWaypoint++)); 224 | path.quadTo(*(ctrl++), *(nextWaypoint++)); 225 | 226 | // draw others 227 | for (int i = 2; i < waypoints.size() - 1; i += 1) 228 | { 229 | path.cubicTo(*ctrl, *(ctrl+1), *(nextWaypoint++)); 230 | ctrl += 2; 231 | } 232 | 233 | // finalize: last one is a quadratic bezier (like the first one) 234 | path.quadTo(*ctrl, *nextWaypoint); 235 | setPath(path); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Link.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_LINK_H 2 | #define PIPER_LINK_H 3 | 4 | #include "Attribute.h" 5 | 6 | #include 7 | 8 | namespace piper 9 | { 10 | class Link : public QGraphicsPathItem 11 | { 12 | public: 13 | Link(); 14 | virtual ~Link(); 15 | 16 | void connectFrom(Attribute* from); 17 | void connectTo(Attribute* to); 18 | void disconnect(); 19 | bool isConnected(); 20 | 21 | void updatePath(); 22 | void updatePath(QPointF const& end); 23 | 24 | void setColor(QColor const& color); 25 | 26 | Attribute const* from() const { return from_; } 27 | Attribute const* to() const { return to_; } 28 | 29 | private: 30 | 31 | void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; 32 | 33 | void mousePressEvent(QGraphicsSceneMouseEvent* event) override; 34 | void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; 35 | void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; 36 | 37 | void updatePath(QPointF const& start, QPointF const& end); 38 | 39 | // Compute bezier control point to 'glue' properly two bezier curves 40 | void computeControlPoint(QPointF const& p0, QPointF const& p1, QPointF const& p2, double t, 41 | QPointF& ctrl1, QPointF& ctrl2); 42 | void drawSplines(QVector const& waypoints, double t); 43 | 44 | QPen pen_; 45 | QPen selected_; 46 | 47 | Attribute* from_{nullptr}; 48 | Attribute* to_{nullptr}; 49 | }; 50 | } 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /src/MainEditor.cc: -------------------------------------------------------------------------------- 1 | #include "MainEditor.h" 2 | #include "ui_MainEditor.h" 3 | #include "EditorWidget.h" 4 | #include "JsonExport.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace piper 11 | { 12 | MainEditor::MainEditor(QWidget* parent) 13 | : QMainWindow(parent) 14 | , ui_(new Ui::MainEditor) 15 | { 16 | ui_->setupUi(this); 17 | ui_->editor_tab->createNewEditorTab(); 18 | 19 | QObject::connect(ui_->actionsave, &QAction::triggered, this, &MainEditor::onSave); 20 | QObject::connect(ui_->actionsave_on, &QAction::triggered, this, &MainEditor::onSaveOn); 21 | QObject::connect(ui_->actionload, &QAction::triggered, this, &MainEditor::onLoad); 22 | QObject::connect(ui_->actionshowhelp, &QAction::triggered, this, &MainEditor::onShowHelp); 23 | QObject::connect(ui_->actionexport_json, &QAction::triggered, this, &MainEditor::onExportJson); 24 | QObject::connect(ui_->actionimport_json, &QAction::triggered, this, &MainEditor::onImportJson); 25 | } 26 | 27 | 28 | void MainEditor::onSaveOn() 29 | { 30 | QString filename = QFileDialog::getSaveFileName(this,tr("Save"), "", tr("Piper project (*.piper);;All Files (*)")); 31 | if (filename.isEmpty()) 32 | { 33 | return; // nothing to do: user abort. 34 | } 35 | 36 | writeProjectFile(filename); 37 | } 38 | 39 | 40 | void MainEditor::onSave() 41 | { 42 | if (project_filename_.isEmpty()) 43 | { 44 | project_filename_ = QFileDialog::getSaveFileName(this,tr("Save"), "", tr("Piper project (*.piper);;All Files (*)")); 45 | } 46 | 47 | if (project_filename_.isEmpty()) 48 | { 49 | return; // nothing to do: user abort. 50 | } 51 | 52 | writeProjectFile(project_filename_); 53 | } 54 | 55 | 56 | void MainEditor::onLoad() 57 | { 58 | project_filename_ = QFileDialog::getOpenFileName(this,tr("Load"), "", tr("Piper project (*.piper);;All Files (*)")); 59 | if (project_filename_.isEmpty()) 60 | { 61 | return; // nothing to do: user abort. 62 | } 63 | 64 | loadProjectFile(project_filename_); 65 | } 66 | 67 | 68 | void MainEditor::onExportJson() 69 | { 70 | QString filename = QFileDialog::getSaveFileName(this,tr("Export"), "", tr("JSON (*.json);;All Files (*)")); 71 | if (filename.isEmpty()) 72 | { 73 | return; // nothing to do: user abort. 74 | } 75 | 76 | JsonExport backend; 77 | backend.init(filename); 78 | 79 | for (int i = 0; i < ui_->editor_tab->count(); ++i) 80 | { 81 | QString pipeline = ui_->editor_tab->name(i); 82 | backend.startPipeline(pipeline); 83 | 84 | // Export tab content. 85 | EditorWidget* editor = static_cast(ui_->editor_tab->widget(i)); 86 | editor->onExport(backend); 87 | 88 | backend.endPipeline(pipeline); 89 | } 90 | 91 | backend.finalize(filename); 92 | } 93 | 94 | 95 | void MainEditor::onImportJson() 96 | { 97 | QString jsonFile = QFileDialog::getOpenFileName(this,tr("Load"), "", tr("Piper json (*.json);;All Files (*)")); 98 | if (jsonFile.isEmpty()) 99 | { 100 | return; // nothing to do: user abort. 101 | } 102 | 103 | loadJson(jsonFile); 104 | } 105 | 106 | 107 | void MainEditor::onShowHelp() 108 | { 109 | QString help; 110 | help += "Press \"=\" to add a new node.\n"; 111 | help += "Right click on a node to set its stage and current mode configuration\n"; 112 | help += "Press mouse middle click on an input or output slot to reverse it\n"; 113 | help += "Double click on a mode to set it as the default one\n"; 114 | 115 | QMessageBox msgBox; 116 | msgBox.setText(help); 117 | msgBox.exec(); 118 | } 119 | 120 | 121 | void MainEditor::loadProjectFile(const QString& filename) 122 | { 123 | QFile file(filename); 124 | file.open(QIODevice::ReadOnly); 125 | 126 | QDataStream in(&file); 127 | 128 | // reset editor - clear tabs 129 | while (ui_->editor_tab->count()) 130 | { 131 | ui_->editor_tab->removeTab(ui_->editor_tab->currentIndex()); 132 | } 133 | 134 | 135 | // load tab 136 | int tab_count; 137 | in >> tab_count; 138 | for (int i = 0; i < tab_count; ++i) 139 | { 140 | EditorWidget* editor = ui_->editor_tab->createNewEditorTab(); 141 | QString tab_name; 142 | in >> tab_name >> *editor; 143 | ui_->editor_tab->setName(i, tab_name); 144 | } 145 | } 146 | 147 | 148 | void MainEditor::writeProjectFile(const QString& filename) 149 | { 150 | QFile file(filename); 151 | file.open(QIODevice::WriteOnly | QIODevice::Truncate); 152 | 153 | QDataStream out(&file); 154 | 155 | // Save tab number 156 | out << ui_->editor_tab->count(); 157 | for (int i = 0; i < ui_->editor_tab->count(); ++i) 158 | { 159 | // Save tab name. 160 | out << ui_->editor_tab->name(i); 161 | 162 | // Save tab content. 163 | EditorWidget* editor = static_cast(ui_->editor_tab->widget(i)); 164 | out << *editor; 165 | } 166 | } 167 | 168 | 169 | void MainEditor::loadJson(const QString& filename) 170 | { 171 | QFile file(filename); 172 | file.open(QIODevice::ReadOnly | QIODevice::Text); 173 | 174 | QByteArray dataJson = file.readAll(); 175 | file.close(); 176 | 177 | QJsonParseError errorPtr; 178 | QJsonDocument doc = QJsonDocument::fromJson(dataJson, &errorPtr); 179 | if (doc.isNull()) { 180 | qDebug() << "Parse failed of " << filename; 181 | } 182 | 183 | QJsonObject rootObj = doc.object(); 184 | 185 | qDebug() << "size " << rootObj.size(); 186 | qDebug() << rootObj.keys(); 187 | 188 | // reset editor - clear tabs 189 | while (ui_->editor_tab->count()) 190 | { 191 | ui_->editor_tab->removeTab(ui_->editor_tab->currentIndex()); 192 | } 193 | 194 | 195 | int pipelineNumber = rootObj.size(); 196 | for (int i = 0; i < pipelineNumber; ++i) 197 | { 198 | EditorWidget* editor = ui_->editor_tab->createNewEditorTab(); 199 | ui_->editor_tab->setName(i, rootObj.keys()[i]); 200 | 201 | QJsonObject pipelineJson = rootObj[rootObj.keys()[i]].toObject(); 202 | editor->loadJson(pipelineJson); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/MainEditor.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_MAIN_EDITOR_H 2 | #define PIPER_MAIN_EDITOR_H 3 | 4 | #include 5 | 6 | namespace Ui 7 | { 8 | class MainEditor; 9 | } 10 | 11 | namespace piper 12 | { 13 | class MainEditor : public QMainWindow 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | explicit MainEditor(QWidget* parent = nullptr); 19 | virtual ~MainEditor() = default; 20 | 21 | public slots: 22 | void onSave(); 23 | void onSaveOn(); 24 | void onLoad(); 25 | void onShowHelp(); 26 | void onImportJson(); 27 | void onExportJson(); 28 | 29 | private: 30 | void writeProjectFile(QString const& filename); 31 | void loadProjectFile(QString const& filename); 32 | void loadJson(QString const& filename); 33 | 34 | Ui::MainEditor* ui_; 35 | QString project_filename_; 36 | }; 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /src/MainEditor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1147 10 | 821 11 | 12 | 13 | 14 | Piper editor 15 | 16 | 17 | 18 | 19 | 20 | 21 | -1 22 | 23 | 24 | true 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 1147 39 | 30 40 | 41 | 42 | 43 | 44 | File 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | help 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | save 65 | 66 | 67 | Ctrl+S 68 | 69 | 70 | 71 | 72 | open 73 | 74 | 75 | Ctrl+O 76 | 77 | 78 | 79 | 80 | export to JSON 81 | 82 | 83 | 84 | 85 | save on 86 | 87 | 88 | Ctrl+Shift+S 89 | 90 | 91 | 92 | 93 | import from JSON 94 | 95 | 96 | 97 | 98 | show 99 | 100 | 101 | 102 | 103 | 104 | piper::EditorTab 105 | QTabWidget 106 |
EditorTab.h
107 | 1 108 |
109 |
110 | 111 | 112 | 113 | 114 |
115 | -------------------------------------------------------------------------------- /src/Node.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Node.h" 8 | #include "Link.h" 9 | #include "AttributeMember.h" 10 | #include "ThemeManager.h" 11 | 12 | namespace piper 13 | { 14 | constexpr int attributeHeight = 30; 15 | constexpr int baseHeight = 35; 16 | constexpr int baseWidth = 250; 17 | 18 | 19 | NodeName::NodeName(QGraphicsItem* parent) : QGraphicsTextItem(parent) 20 | { 21 | QTextOption options; 22 | options.setWrapMode(QTextOption::NoWrap); 23 | document()->setDefaultTextOption(options); 24 | } 25 | 26 | 27 | void NodeName::adjustPosition() 28 | { 29 | setPos(-(boundingRect().width() - parentItem()->boundingRect().width()) * 0.5, -boundingRect().height()); 30 | } 31 | 32 | void NodeName::keyPressEvent(QKeyEvent* e) 33 | { 34 | if (e->key() == Qt::Key_Return) 35 | { 36 | clearFocus(); 37 | return; 38 | } 39 | 40 | // Handle event (text change) and recompute position 41 | QGraphicsTextItem::keyPressEvent(e); 42 | adjustPosition(); 43 | } 44 | 45 | 46 | Node::Node(QString const& type, QString const& name, QString const& stage) 47 | : QGraphicsItem(nullptr) 48 | , bounding_rect_{0, 0, baseWidth, baseHeight} 49 | , name_{new NodeName(this)} 50 | , type_{type} 51 | , stage_{stage} 52 | , mode_{Mode::enable} 53 | , width_{baseWidth} 54 | , height_{baseHeight} 55 | , attributes_{} 56 | { 57 | // Configure item behavior. 58 | setFlag(QGraphicsItem::ItemIsMovable); 59 | setFlag(QGraphicsItem::ItemIsSelectable); 60 | setFlag(QGraphicsItem::ItemIsFocusable); 61 | 62 | // Configure node name 63 | name_->setTextInteractionFlags(Qt::TextEditorInteraction); 64 | setName(name); 65 | 66 | createStyle(); 67 | 68 | type_rect_ = QRectF{1, 17, width_ - 2.0, attributeHeight}; 69 | height_ += attributeHeight; // Give some space for type section 70 | } 71 | 72 | 73 | Node::~Node() 74 | { 75 | Scene* pScene = static_cast(scene()); 76 | pScene->removeNode(this); 77 | } 78 | 79 | 80 | void Node::highlight(Attribute* emitter) 81 | { 82 | for (auto& attr : attributes_) 83 | { 84 | if (attr == emitter) 85 | { 86 | // special case: do not change emitter mode. 87 | continue; 88 | } 89 | 90 | if (attr->accept(emitter)) 91 | { 92 | attr->setMode(DisplayMode::highlight); 93 | } 94 | else 95 | { 96 | attr->setMode(DisplayMode::minimize); 97 | } 98 | attr->update(); 99 | } 100 | } 101 | 102 | 103 | void Node::unhighlight() 104 | { 105 | for (auto& attr : attributes_) 106 | { 107 | attr->setMode(DisplayMode::normal); 108 | attr->update(); 109 | } 110 | } 111 | 112 | 113 | void Node::createAttributes(QVector const& attributesInfo) 114 | { 115 | if (not attributes_.empty()) 116 | { 117 | qWarning() << "Creating attributes in multiples call is not supported."; 118 | return; 119 | } 120 | 121 | // Compute width. 122 | QFont attributeFont = ThemeManager::instance().getAttributeTheme().normal.font; 123 | QFontMetrics metrics(attributeFont); 124 | QRect boundingRect{0, 0, width_ - 32, attributeHeight}; // -30 to keep space for attribute custom display / -2 for node border 125 | for (auto const& info : attributesInfo) 126 | { 127 | boundingRect = boundingRect.united(metrics.boundingRect(info.name)); 128 | } 129 | // Adjust bounding rect position / width / height. 130 | boundingRect.setTopLeft({0, 0}); 131 | boundingRect.setWidth(boundingRect.width() + 30); // add space again 132 | boundingRect.setHeight(attributeHeight); 133 | 134 | // Adjust node width 135 | width_ = boundingRect.width() + 2; 136 | type_rect_.setWidth(boundingRect.width()); 137 | 138 | // Create attributes 139 | for (auto const& info : attributesInfo) 140 | { 141 | Attribute* attr{nullptr}; 142 | if (info.name == "stage") 143 | { 144 | qWarning() << "Stage is a reserved attribute: skipping"; 145 | continue; 146 | } 147 | switch (info.type) 148 | { 149 | case AttributeInfo::Type::input: 150 | { 151 | attr = new AttributeInput(this, info, boundingRect); 152 | break; 153 | } 154 | case AttributeInfo::Type::output: 155 | { 156 | attr = new AttributeOutput(this, info, boundingRect); 157 | break; 158 | } 159 | case AttributeInfo::Type::member: 160 | { 161 | attr = new AttributeMember(this, info, boundingRect); 162 | break; 163 | } 164 | } 165 | attr->setPos(1, 17 + attributeHeight * (attributes_.size() + 1)); 166 | if (attributes_.size() % 2) 167 | { 168 | attr->setBackgroundBrush(attribute_brush_); 169 | } 170 | else 171 | { 172 | attr->setBackgroundBrush(attribute_alt_brush_); 173 | } 174 | height_ += attributeHeight; 175 | bounding_rect_ = QRectF(0, 0, width_, height_); 176 | bounding_rect_ += QMargins(1, 1, 1, 1); 177 | attributes_.append(attr); 178 | } 179 | 180 | prepareGeometryChange(); 181 | // readjust name position. 182 | name_->adjustPosition(); 183 | 184 | } 185 | 186 | 187 | void Node::createStyle() 188 | { 189 | NodeTheme node_theme = ThemeManager::instance().getNodeTheme(); 190 | AttributeTheme attribute_theme = ThemeManager::instance().getAttributeTheme(); 191 | 192 | qint32 border = 2; 193 | 194 | background_brush_.setStyle(Qt::SolidPattern); 195 | background_brush_.setColor(node_theme.background); 196 | 197 | pen_.setStyle(Qt::SolidLine); 198 | pen_.setWidth(border); 199 | pen_.setColor(node_theme.border); 200 | 201 | pen_selected_.setStyle(Qt::SolidLine); 202 | pen_selected_.setWidth(border); 203 | pen_selected_.setColor(node_theme.border_selected); 204 | 205 | name_->setFont(node_theme.name_font); 206 | name_->setDefaultTextColor(node_theme.name_color); 207 | name_->adjustPosition(); 208 | 209 | attribute_brush_.setStyle(Qt::SolidPattern); 210 | attribute_brush_.setColor(attribute_theme.background); 211 | attribute_alt_brush_.setStyle(Qt::SolidPattern); 212 | attribute_alt_brush_.setColor(attribute_theme.background_alt); 213 | 214 | type_brush_.setStyle(Qt::SolidPattern); 215 | type_brush_.setColor(node_theme.type_background); 216 | type_pen_.setStyle(Qt::SolidLine); 217 | type_pen_.setColor(node_theme.type_color); 218 | type_font_ = node_theme.type_font; 219 | } 220 | 221 | 222 | QRectF Node::boundingRect() const 223 | { 224 | return bounding_rect_; 225 | } 226 | 227 | 228 | void Node::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) 229 | { 230 | // Base shape. 231 | painter->setBrush(background_brush_); 232 | 233 | if (isSelected()) 234 | { 235 | painter->setPen(pen_selected_); 236 | } 237 | else 238 | { 239 | painter->setPen(pen_); 240 | } 241 | 242 | qint32 radius = 10; 243 | type_rect_.setWidth(width_ - 2.0); 244 | painter->drawRoundedRect(0, 0, width_, height_, radius, radius); 245 | 246 | // type background. 247 | painter->setBrush(type_brush_); 248 | painter->setPen(Qt::NoPen); 249 | painter->drawRect(type_rect_); 250 | 251 | // type label. 252 | painter->setFont(type_font_); 253 | painter->setPen(type_pen_); 254 | painter->drawText(type_rect_, Qt::AlignCenter, type_); 255 | 256 | updateWidth(); 257 | } 258 | 259 | 260 | void Node::mousePressEvent(QGraphicsSceneMouseEvent* event) 261 | { 262 | // Force selected node on top layer 263 | for (auto& item : scene()->items()) 264 | { 265 | if (item->zValue() > 1) 266 | { 267 | item->setZValue(1); 268 | } 269 | } 270 | setZValue(2); 271 | 272 | QGraphicsItem::mousePressEvent(event); 273 | } 274 | 275 | void Node::mouseMoveEvent(QGraphicsSceneMouseEvent* event) 276 | { 277 | for (auto& attr : attributes_) 278 | { 279 | attr->refresh(); // let the attribute refresh their data if required. 280 | } 281 | 282 | QGraphicsItem::mouseMoveEvent(event); 283 | } 284 | 285 | 286 | QString Node::name() const 287 | { 288 | return name_->toPlainText(); 289 | } 290 | 291 | 292 | void Node::setName(QString const& name) 293 | { 294 | name_->setPlainText(name); 295 | 296 | // Compute position 297 | name_->adjustPosition(); 298 | } 299 | 300 | 301 | void Node::setMode(Mode mode) 302 | { 303 | mode_ = mode; 304 | 305 | for (auto& attribute : attributes_) 306 | { 307 | DataTypeTheme theme = ThemeManager::instance().getDataTypeTheme(attribute->dataType()); 308 | 309 | if (attribute->isOutput()) 310 | { 311 | switch (mode) 312 | { 313 | case Mode::enable: { attribute->setColor(theme.enable); break; } 314 | case Mode::disable: { attribute->setColor(theme.disable); break; } 315 | case Mode::neutral: { attribute->setColor(theme.neutral); break; } 316 | } 317 | } 318 | } 319 | } 320 | 321 | void Node::updateWidth() 322 | { 323 | QList widths{name_->sceneBoundingRect().width(), baseWidth}; 324 | for (auto& attribute : attributes_) 325 | { 326 | if (attribute->isMember()) 327 | { 328 | widths.append(attribute->getFormBaseWidth() + attribute->labelRect().right() + 20); 329 | } 330 | } 331 | 332 | std::sort(widths.begin(), widths.end(), std::greater()); 333 | width_ = static_cast(widths[0]); 334 | for (auto& attribute : attributes_) 335 | { 336 | QRectF rectangle = attribute->boundingRect(); 337 | QPointF pose = QPointF{1, rectangle.top()}; 338 | rectangle.setTopLeft(pose); 339 | rectangle.setWidth(width_ - 3.0); 340 | attribute->updateRectSize(rectangle); 341 | attribute->updateConnectorPosition(); 342 | } 343 | bounding_rect_ = QRectF(0, 0, width_, height_); 344 | name_->adjustPosition(); 345 | } 346 | 347 | void Node::keyPressEvent(QKeyEvent* event) 348 | { 349 | if (isSelected()) 350 | { 351 | constexpr qreal moveFactor = 5; 352 | if ((event->key() == Qt::Key::Key_Up) and (event->modifiers() == Qt::NoModifier)) 353 | { 354 | moveBy(0, -moveFactor); 355 | } 356 | if ((event->key() == Qt::Key::Key_Down) and (event->modifiers() == Qt::NoModifier)) 357 | { 358 | moveBy(0, moveFactor); 359 | } 360 | if ((event->key() == Qt::Key::Key_Left) and (event->modifiers() == Qt::NoModifier)) 361 | { 362 | moveBy(-moveFactor, 0); 363 | } 364 | if ((event->key() == Qt::Key::Key_Right) and (event->modifiers() == Qt::NoModifier)) 365 | { 366 | moveBy(moveFactor, 0); 367 | } 368 | 369 | return; 370 | } 371 | 372 | QGraphicsItem::keyPressEvent(event); 373 | } 374 | 375 | 376 | void Node::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) 377 | { 378 | Scene* pScene = static_cast(scene()); 379 | QMenu menu; 380 | 381 | menu.setStyleSheet("" 382 | "QMenu::separator" 383 | "{" 384 | "height: 1px;" 385 | "background-color: #505F69;" 386 | "color: #F0F0F0;" 387 | "}" 388 | ); 389 | 390 | 391 | // Create stage menu entries. 392 | menu.addSection("Stage"); 393 | 394 | for (int i = 0; i < pScene->stages()->rowCount(); ++i) 395 | { 396 | QString stage = pScene->stages()->item(i, 0)->data(Qt::DisplayRole).toString(); 397 | QAction* stageAction = menu.addAction(stage); 398 | stageAction->setCheckable(true); 399 | stageAction->setData(stage); 400 | if (stage == stage_) 401 | { 402 | stageAction->setChecked(true); 403 | } 404 | 405 | QObject::connect(stageAction, &QAction::triggered, 406 | [this, pScene, stage](bool isChecked) 407 | { 408 | if (isChecked) 409 | { 410 | stage_ = stage; 411 | } 412 | else 413 | { 414 | stage_ = ""; 415 | } 416 | 417 | pScene->onStageUpdated(); 418 | }); 419 | } 420 | 421 | 422 | QStandardItem* currentMode = nullptr; 423 | for (int i = 0; i < pScene->modes()->rowCount(); ++i) 424 | { 425 | QStandardItem* mode = pScene->modes()->item(i, 0); 426 | 427 | // Search the current mode. 428 | if (mode->data(Qt::UserRole + 1).toBool() == false) 429 | { 430 | continue; 431 | } 432 | 433 | currentMode = mode; 434 | } 435 | 436 | if (currentMode != nullptr) 437 | { 438 | menu.addSection(currentMode->data(Qt::DisplayRole).toString()); 439 | 440 | auto updateMode = [this, currentMode](enum Mode mode) 441 | { 442 | // Apply mode on display 443 | this->setMode(mode); 444 | 445 | // Save mode 446 | QHash nodeMode = currentMode->data(Qt::UserRole + 2).toHash(); 447 | nodeMode[this->name()] = mode; 448 | currentMode->setData(nodeMode, Qt::UserRole + 2); 449 | }; 450 | 451 | QAction* enable = menu.addAction("Enable"); 452 | enable->setCheckable(true); 453 | if (mode_ == Mode::enable) { enable->setChecked(true); } 454 | QObject::connect(enable, &QAction::triggered, std::bind(updateMode, Mode::enable)); 455 | 456 | QAction* disable = menu.addAction("Disable"); 457 | disable->setCheckable(true); 458 | if (mode_ == Mode::disable) { disable->setChecked(true); } 459 | QObject::connect(disable, &QAction::triggered, std::bind(updateMode, Mode::disable)); 460 | 461 | QAction* neutral = menu.addAction("Neutral"); 462 | neutral->setCheckable(true); 463 | if (mode_ == Mode::neutral) { neutral->setChecked(true); } 464 | QObject::connect(neutral, &QAction::triggered, std::bind(updateMode, Mode::neutral)); 465 | } 466 | 467 | (void) menu.exec(event->screenPos()); 468 | } 469 | 470 | 471 | QDataStream& operator<<(QDataStream& out, Node const& node) 472 | { 473 | // Save node data 474 | out << node.type_ << node.name() << node.stage_ << node.pos(); 475 | 476 | // save node attributes 477 | out << node.attributes().size(); 478 | for (auto const& attr: node.attributes()) 479 | { 480 | out << attr->info() << attr->data(); 481 | } 482 | 483 | return out; 484 | } 485 | 486 | 487 | QDataStream& operator>>(QDataStream& in, Node& node) 488 | { 489 | // load node data 490 | QPointF pos; 491 | QString name; 492 | in >> node.type_ >> name >>node.stage_ >> pos; 493 | node.setPos(pos); 494 | node.setName(name); 495 | 496 | // load node attributes 497 | int attributesSize; 498 | in >> attributesSize; 499 | QVector attributesInfo; 500 | QVector attributesData; 501 | for (int j = 0; j < attributesSize; ++j) 502 | { 503 | AttributeInfo info; 504 | QVariant data; 505 | in >> info >> data; 506 | attributesInfo.append(info); 507 | attributesData.append(data); 508 | } 509 | 510 | node.createAttributes(attributesInfo); 511 | auto data = attributesData.constBegin(); 512 | for (auto const& attribute : node.attributes()) 513 | { 514 | attribute->setData(*data); 515 | ++data; 516 | } 517 | return in; 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/Node.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_NODE_H 2 | #define PIPER_NODE_H 3 | 4 | #include "Scene.h" 5 | #include "Attribute.h" 6 | #include "Types.h" 7 | 8 | namespace piper 9 | { 10 | class NodeName : public QGraphicsTextItem 11 | { 12 | public: 13 | NodeName(QGraphicsItem* parent); 14 | virtual ~NodeName() = default; 15 | 16 | void adjustPosition(); 17 | 18 | protected: 19 | void keyPressEvent(QKeyEvent* e) override; 20 | }; 21 | 22 | class Node : public QGraphicsItem 23 | { 24 | friend Link* connect(QString const& from, QString const& out, QString const& to, QString const& in); 25 | friend QDataStream& operator<<(QDataStream& out, Node const& node); 26 | friend QDataStream& operator>>(QDataStream& in, Node& node); 27 | 28 | public: 29 | Node (QString const& type = "", QString const& name = "", QString const& stage = ""); 30 | virtual ~Node(); 31 | 32 | // highlight attribute that are compatible with dataType 33 | void highlight(Attribute* emitter); 34 | void unhighlight(); 35 | 36 | QString& stage() { return stage_; } // TODO const it (currently required for stage edition) 37 | QString name() const; 38 | QString const& nodeType() const { return type_; } 39 | 40 | void setMode(Mode mode); 41 | void setName(QString const& name); 42 | void setBackgroundColor(QColor const& color) 43 | { 44 | background_brush_.setColor(color); 45 | update(); 46 | } 47 | void updateWidth(); 48 | 49 | // Create attributes of this item. 50 | void createAttributes(QVector const& attributesInfo); 51 | 52 | QVector const& attributes() const { return attributes_; } 53 | 54 | QVector& attributes() { return attributes_; } 55 | 56 | protected: 57 | QRectF boundingRect() const override; 58 | void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; 59 | 60 | void mousePressEvent(QGraphicsSceneMouseEvent* event) override; 61 | void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; 62 | void keyPressEvent(QKeyEvent* event) override; 63 | void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; 64 | 65 | 66 | private: 67 | void createStyle(); 68 | 69 | QRectF bounding_rect_; 70 | 71 | NodeName* name_; 72 | QString type_; 73 | QString stage_; 74 | Mode mode_; 75 | 76 | qint32 width_; 77 | qint32 height_; 78 | 79 | QBrush background_brush_; 80 | QPen pen_; 81 | QPen pen_selected_; 82 | 83 | QBrush attribute_brush_; 84 | QBrush attribute_alt_brush_; 85 | 86 | QPen type_pen_; 87 | QBrush type_brush_; 88 | QFont type_font_; 89 | QRectF type_rect_; 90 | 91 | QVector attributes_; 92 | }; 93 | 94 | Link* connect(QString const& from, QString const& out, QString const& to, QString const& in); 95 | 96 | QDataStream& operator<<(QDataStream& out, Node const& node); 97 | QDataStream& operator>>(QDataStream& in, Node& node); 98 | } 99 | 100 | #endif 101 | 102 | -------------------------------------------------------------------------------- /src/NodeCreator.cc: -------------------------------------------------------------------------------- 1 | #include "NodeCreator.h" 2 | 3 | #include 4 | 5 | namespace piper 6 | { 7 | NodeCreator& NodeCreator::instance() 8 | { 9 | static NodeCreator creator_; 10 | return creator_; 11 | } 12 | 13 | void NodeCreator::addItem(Item const& item) 14 | { 15 | auto it = available_items_.find(item.type); 16 | if (it != available_items_.end()) 17 | { 18 | qDebug() << "Can't add the item. Type" << item.type << "already exists."; 19 | return; 20 | } 21 | auto newItem = available_items_.insert(item.type, item); 22 | if (newItem->from == "") { newItem->from = "unknown"; } 23 | if (newItem->category == "") { newItem->category = "unknown"; } 24 | } 25 | 26 | Node* NodeCreator::createItem(QString const& type, QString const& name, QString const& stage, const QPointF& pos) 27 | { 28 | auto it = available_items_.find(type); 29 | if (it == available_items_.end()) 30 | { 31 | qDebug() << "Can't create the item" << name << ". Type" << type << "is unknown"; 32 | return nullptr; 33 | } 34 | 35 | Node* node = new Node(type, name, stage); 36 | node->setPos(pos); 37 | node->createAttributes(it->attributes); 38 | node->setToolTip(it->help); 39 | return node; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NodeCreator.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_NODE_CREATOR_H 2 | #define PIPER_NODE_CREATOR_H 3 | 4 | #include "Node.h" 5 | 6 | namespace piper 7 | { 8 | struct Item 9 | { 10 | QString type; // Item type - shall be unique! 11 | QString help; // Displayed text as a tooltip 12 | QString from; // The library that add the item 13 | QString category; // Item category to sort them in the interface 14 | QVector attributes; // Describe the behavior 15 | }; 16 | 17 | class NodeCreator 18 | { 19 | public: 20 | static NodeCreator& instance(); 21 | 22 | QList availableItems() const { return available_items_.values(); } 23 | void addItem(Item const& item); 24 | Node* createItem(QString const& type, QString const& name, QString const& stage, QPointF const& pos); 25 | 26 | private: 27 | NodeCreator() = default; 28 | virtual ~NodeCreator() = default; 29 | 30 | QHash available_items_; 31 | }; 32 | } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/PropertyDelegate.cc: -------------------------------------------------------------------------------- 1 | #include "PropertyDelegate.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace piper 8 | { 9 | QWidget* StagePropertyDelegate::createEditor(QWidget* parent, QStyleOptionViewItem const& option, QModelIndex const& index) const 10 | { 11 | QComboBox* cb = new QComboBox(parent); 12 | 13 | int row = 0; 14 | QModelIndex i = stages_->index(row, 0); 15 | while (i.isValid()) 16 | { 17 | QString stage = stages_->data(i, Qt::DisplayRole).toString(); 18 | cb->addItem(stage); 19 | 20 | ++row; 21 | i = stages_->index(row, 0); 22 | } 23 | 24 | connect(cb, static_cast(&QComboBox::currentIndexChanged),this, &StagePropertyDelegate::onIndexChange); 25 | return cb; 26 | } 27 | 28 | 29 | void StagePropertyDelegate::setEditorData(QWidget* editor, QModelIndex const& index) const 30 | { 31 | QComboBox* cb = qobject_cast(editor); 32 | QString value = index.data(Qt::EditRole).toString(); 33 | int idx = cb->findText(value); 34 | if (idx >= 0) 35 | { 36 | cb->setCurrentIndex(idx); 37 | } 38 | 39 | cb->showPopup(); 40 | } 41 | 42 | 43 | void StagePropertyDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, QModelIndex const& index) const 44 | { 45 | QComboBox* cb = qobject_cast(editor); 46 | model->setData(index, cb->currentText(), Qt::EditRole); 47 | } 48 | 49 | 50 | void StagePropertyDelegate::onIndexChange() 51 | { 52 | QComboBox* cb = static_cast(sender()); 53 | emit commitData(cb); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/PropertyDelegate.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_PROPERTY_DELEGATE_H 2 | #define PIPER_PROPERTY_DELEGATE_H 3 | 4 | #include 5 | 6 | class QStandardItemModel; 7 | 8 | namespace piper 9 | { 10 | class StagePropertyDelegate : public QStyledItemDelegate 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, QModelIndex const& index) const override; 16 | void setEditorData(QWidget* editor, QModelIndex const& index) const override; 17 | void setModelData(QWidget* editor, QAbstractItemModel* model, QModelIndex const& index) const override; 18 | 19 | void setStageModel(QStandardItemModel const* stages) { stages_ = stages; } 20 | 21 | public slots: 22 | void onIndexChange(); 23 | 24 | private: 25 | QStandardItemModel const* stages_; 26 | }; 27 | } 28 | 29 | #endif 30 | 31 | -------------------------------------------------------------------------------- /src/Scene.cc: -------------------------------------------------------------------------------- 1 | #include "Scene.h" 2 | #include "Node.h" 3 | #include "Link.h" 4 | #include "ExportBackend.h" 5 | #include "NodeCreator.h" 6 | #include "ThemeManager.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | namespace piper 22 | { 23 | QStack Scene::undoStack_{}; 24 | QStack Scene::redoStack_{}; 25 | 26 | Scene::Scene (QObject* parent) 27 | : QGraphicsScene(0, 0, 32000, 32000, parent) 28 | { 29 | // Prepare stage model 30 | stages_ = new QStandardItemModel(this); 31 | stages_->insertColumns(0, 1); 32 | QObject::connect(stages_, &QStandardItemModel::itemChanged, this, &Scene::onStageUpdated); 33 | 34 | // Prepare mode model 35 | modes_ = new QStandardItemModel(this); 36 | modes_->insertColumns(0, 1); 37 | QObject::connect(modes_, &QStandardItemModel::rowsRemoved, this, &Scene::onModeRemoved); 38 | } 39 | 40 | 41 | Scene::~Scene() 42 | { 43 | // Manually delete nodes and links because order are important 44 | QVector deleteNodes = nodes_; 45 | for (auto& node : deleteNodes) 46 | { 47 | delete node; 48 | } 49 | 50 | QVector deleteLinks = links_; 51 | for (auto& link : deleteLinks) 52 | { 53 | delete link; 54 | } 55 | } 56 | 57 | void Scene::drawBackground(QPainter* painter, QRectF const& rect) 58 | { 59 | QBrush brush(Qt::SolidPattern); 60 | brush.setColor({40, 40, 40}), 61 | painter->fillRect(rect, brush); 62 | 63 | QPen pen; 64 | pen.setColor({100, 100, 100}); 65 | pen.setWidth(2); 66 | painter->setPen(pen); 67 | 68 | constexpr int gridSize = 20; 69 | qreal left = int(rect.left()) - (int(rect.left()) % gridSize); 70 | qreal top = int(rect.top()) - (int(rect.top()) % gridSize); 71 | QVector points; 72 | for (qreal x = left; x < rect.right(); x += gridSize) 73 | { 74 | for (qreal y = top; y < rect.bottom(); y += gridSize) 75 | { 76 | points.append(QPointF(x,y)); 77 | } 78 | } 79 | 80 | painter->drawPoints(points.data(), points.size()); 81 | } 82 | 83 | 84 | void Scene::keyReleaseEvent(QKeyEvent* keyEvent) 85 | { 86 | if (keyEvent->key() == Qt::Key::Key_Delete) 87 | { 88 | QByteArray copiedScene = copyCurrentScene(); 89 | undoStack_.push(copiedScene); 90 | redoStack_ = QStack(); 91 | 92 | for (auto& item : selectedItems()) 93 | { 94 | delete item; 95 | } 96 | } 97 | 98 | // destroy orphans link 99 | QVector deleteLinks = links_; 100 | for (auto& link : deleteLinks) 101 | { 102 | if (not link->isConnected()) 103 | { 104 | delete link; 105 | } 106 | } 107 | } 108 | 109 | QByteArray Scene::copyCurrentScene() 110 | { 111 | QByteArray copyCurrentScene; 112 | QDataStream stream(©CurrentScene, QIODevice::WriteOnly | QIODevice::Truncate); 113 | stream << nodes_.size(); 114 | for (auto const& node : nodes_) 115 | { 116 | stream << *node; 117 | } 118 | 119 | stream << links_.size(); 120 | for (auto const& link : links_) 121 | { 122 | stream << static_cast(link->from()->parentItem())->name() << link->from()->name(); 123 | stream << static_cast(link->to()->parentItem())->name() << link->to()->name(); 124 | } 125 | 126 | return copyCurrentScene; 127 | } 128 | 129 | void Scene::loadSceneFromStack(QStack stack) 130 | { 131 | QVector deleteNodes = nodes_; 132 | for (auto& node : deleteNodes) 133 | { 134 | delete node; 135 | } 136 | 137 | QVector deleteLinks = links_; 138 | for (auto& link : deleteLinks) 139 | { 140 | delete link; 141 | } 142 | 143 | struct LinkData 144 | { 145 | QString from; 146 | QString output; 147 | QString to; 148 | QString input; 149 | }; 150 | 151 | QByteArray previousScene = stack.top(); 152 | QDataStream stream(previousScene); 153 | QList copies; 154 | QList links; 155 | 156 | // preload nodes and links 157 | int nodeCount; 158 | stream >> nodeCount; 159 | for (int i = 0; i < nodeCount; ++i) 160 | { 161 | Node* node = new Node(); 162 | stream >> *node; 163 | copies << node; 164 | } 165 | 166 | int linkCount; 167 | stream >> linkCount; 168 | for (int i = 0; i < linkCount; ++i) 169 | { 170 | LinkData link; 171 | stream >> link.from >> link.output >> link.to >> link.input; 172 | links << link; 173 | } 174 | 175 | for (auto const& copy : copies) 176 | { 177 | addNode(copy); 178 | } 179 | 180 | for (auto const& link : links) 181 | { 182 | connect(link.from, link.output, link.to, link.input); 183 | } 184 | } 185 | 186 | void Scene::undo() 187 | { 188 | if(not undoStack_.empty()) 189 | { 190 | QByteArray currentScene = copyCurrentScene(); 191 | redoStack_.push(currentScene); 192 | 193 | loadSceneFromStack(undoStack_); 194 | 195 | undoStack_.pop(); 196 | 197 | onStageUpdated(); 198 | 199 | } 200 | } 201 | 202 | void Scene::redo() 203 | { 204 | if(not redoStack_.empty()) 205 | { 206 | QByteArray currentScene = copyCurrentScene(); 207 | undoStack_.push(currentScene); 208 | 209 | loadSceneFromStack(redoStack_); 210 | 211 | redoStack_.pop(); 212 | 213 | onStageUpdated(); 214 | } 215 | } 216 | 217 | 218 | void Scene::resetStagesColor() 219 | { 220 | QColor default_background = ThemeManager::instance().getNodeTheme().background; 221 | for (auto& node : nodes_) 222 | { 223 | node->setBackgroundColor(default_background); 224 | } 225 | } 226 | 227 | 228 | void Scene::updateStagesColor(QString const& stage, QColor const& color) 229 | { 230 | for (auto& node : nodes_) 231 | { 232 | if (node->stage() == stage) 233 | { 234 | node->setBackgroundColor(color); 235 | } 236 | } 237 | } 238 | 239 | 240 | void Scene::onStageUpdated() 241 | { 242 | int row = 0; 243 | QModelIndex index = stages_->index(row, 0); 244 | 245 | resetStagesColor(); 246 | while (index.isValid()) 247 | { 248 | QString stage = stages_->data(index, Qt::DisplayRole).toString(); 249 | QColor color = stages_->data(index, Qt::DecorationRole).value(); 250 | updateStagesColor(stage, color); 251 | 252 | ++row; 253 | index = stages_->index(row, 0); 254 | } 255 | } 256 | 257 | 258 | 259 | void Scene::addNode(Node* node) 260 | { 261 | addItem(node); 262 | nodes_.append(node); 263 | } 264 | 265 | 266 | void Scene::removeNode(Node* node) 267 | { 268 | // Remove from mode 269 | for (int i = 0; i < modes_->rowCount(); ++i) 270 | { 271 | QStandardItem* mode = modes_->item(i, 0); 272 | QHash nodeMode = mode->data(Qt::UserRole + 2).toHash(); 273 | nodeMode.remove(node->name()); 274 | mode->setData(nodeMode, Qt::UserRole + 2); 275 | } 276 | 277 | removeItem(node); 278 | nodes_.removeAll(node); 279 | } 280 | 281 | 282 | void Scene::addLink(Link* link) 283 | { 284 | addItem(link); 285 | links_.append(link); 286 | } 287 | 288 | 289 | void Scene::removeLink(Link* link) 290 | { 291 | removeItem(link); 292 | links_.removeAll(link); 293 | } 294 | 295 | 296 | void Scene::connect(QString const& from, QString const& out, QString const& to, QString const& in) 297 | { 298 | auto const nodeFrom = std::find_if(nodes().begin(), nodes().end(), 299 | [&](Node const* node) { return (node->name() == from); } 300 | ); 301 | auto const nodeTo = std::find_if(nodes().begin(), nodes().end(), 302 | [&](Node const* node) { return (node->name() == to); } 303 | ); 304 | 305 | if (nodeFrom == nodes().end()) 306 | { 307 | QString error = "Node " + from + " (from) not found"; 308 | links_import_errors_.append(error); 309 | return; 310 | } 311 | 312 | if (nodeTo == nodes().end()) 313 | { 314 | QString error = "Node " + to + " (to) not found"; 315 | links_import_errors_.append(error); 316 | return; 317 | } 318 | 319 | Attribute* attrOut{nullptr}; 320 | for (auto& attr : (*nodeFrom)->attributes()) 321 | { 322 | if (attr->isOutput() and (attr->name() == out)) 323 | { 324 | attrOut = attr; 325 | break; 326 | } 327 | } 328 | 329 | Attribute* attrIn{nullptr}; 330 | for (auto& attr : (*nodeTo)->attributes()) 331 | { 332 | if (attr->isInput() and (attr->name() == in)) 333 | { 334 | attrIn = attr; 335 | break; 336 | } 337 | } 338 | 339 | if (attrIn == nullptr) 340 | { 341 | QString error = "Cannot find attribute " + in + " (in) in the node" + to; 342 | links_import_errors_.append(error); 343 | return; 344 | } 345 | 346 | if (attrOut == nullptr) 347 | { 348 | QString error = "Cannot find attribute " + out + " (out) in the node" + from; 349 | links_import_errors_.append(error); 350 | return; 351 | } 352 | 353 | if (not attrIn->accept(attrOut)) 354 | { 355 | QString error = "Cannot connect node " + from + " to node " + to + ". Type mismatch"; 356 | return; 357 | } 358 | 359 | Link* link = new Link; 360 | link->connectFrom(attrOut); 361 | link->connectTo(attrIn); 362 | addLink(link); 363 | } 364 | 365 | 366 | QModelIndex Scene::addMode(QString const& name) 367 | { 368 | // Add item 369 | QStandardItem* item = new QStandardItem(); 370 | item->setData(name, Qt::DisplayRole); 371 | item->setDropEnabled(false);; 372 | modes_->appendRow(item); 373 | 374 | // Enable item selection and put it edit mode 375 | QModelIndex index = modes_->indexFromItem(item); 376 | onModeSelected(index); 377 | return index; 378 | } 379 | 380 | 381 | void Scene::onExport(ExportBackend& backend) 382 | { 383 | // -------- stages -------- // 384 | QVector stagesArray; 385 | for (int i = 0; i < stages()->rowCount(); ++i) 386 | { 387 | QString stage = stages()->item(i, 0)->data(Qt::DisplayRole).toString(); 388 | stagesArray.append(stage); 389 | } 390 | backend.writeStages(stagesArray); 391 | 392 | // -------- nodes -------- // 393 | for (auto const& node : nodes_) 394 | { 395 | QHash attr; 396 | for (auto const& attribute : node->attributes()) 397 | { 398 | if (not attribute->isMember()) 399 | { 400 | continue; 401 | } 402 | 403 | attr.insert(attribute->name(), attribute->data()); 404 | } 405 | 406 | backend.writeNode(node->nodeType(), node->name(), node->stage(), attr); 407 | } 408 | 409 | // -------- links -------- // 410 | for (auto const& link : links_) 411 | { 412 | Node const* from = static_cast(link->from()->parentItem()); 413 | Node const* to = static_cast(link->to()->parentItem()); 414 | backend.writeLink(from->name(), link->from()->name(), to->name(), link->to()->name(), link->from()->dataType()); 415 | } 416 | 417 | // -------- modes -------- // 418 | for (int i = 0; i < modes()->rowCount(); ++i) 419 | { 420 | QStandardItem* mode = modes()->item(i, 0); 421 | QString modeName = mode->data(Qt::DisplayRole).toString(); 422 | if (mode->data(Qt::DecorationRole).isValid()) 423 | { 424 | backend.writeDefaultMode(modeName); 425 | } 426 | 427 | QHash config = mode->data(Qt::UserRole + 2).toHash(); 428 | QHash exportConfig; 429 | for (auto it = config.constBegin(); it != config.constEnd(); ++it) 430 | { 431 | exportConfig[it.key()] = static_cast(it.value().toInt()); 432 | } 433 | 434 | backend.writeMode(modeName, exportConfig); 435 | } 436 | } 437 | 438 | 439 | void Scene::onModeSelected(QModelIndex const& index) 440 | { 441 | // Reset select state. 442 | for (int i = 0; i < modes_->rowCount(); ++i) 443 | { 444 | modes_->item(i, 0)->setData(false, Qt::UserRole + 1); 445 | } 446 | 447 | QStandardItem* currentMode = modes_->itemFromIndex(index); 448 | currentMode->setData(true, Qt::UserRole + 1); 449 | 450 | // Update node display. 451 | QHash nodeMode = currentMode->data(Qt::UserRole + 2).toHash(); 452 | for (auto& node : nodes_) 453 | { 454 | auto it = nodeMode.find(node->name()); 455 | if (it == nodeMode.end()) 456 | { 457 | // Default mode is enabled. 458 | node->setMode(Mode::enable); 459 | continue; 460 | } 461 | 462 | node->setMode(static_cast(it.value().toInt())); 463 | } 464 | } 465 | 466 | 467 | void Scene::onModeSetDefault(QModelIndex const& index) 468 | { 469 | // Reset select state. 470 | for (int i = 0; i < modes_->rowCount(); ++i) 471 | { 472 | modes_->item(i, 0)->setData(QVariant(), Qt::DecorationRole); 473 | } 474 | 475 | QStandardItem* currentMode = modes_->itemFromIndex(index); 476 | currentMode->setData(QIcon(":/icon/star.svg"), Qt::DecorationRole); 477 | } 478 | 479 | 480 | void Scene::onModeRemoved() 481 | { 482 | for (auto& node : nodes_) 483 | { 484 | node->setMode(Mode::enable); 485 | } 486 | } 487 | 488 | 489 | QDataStream& operator<<(QDataStream& out, Scene const& scene) 490 | { 491 | // save stages. 492 | out << scene.stages()->rowCount(); 493 | for (int i = 0; i < scene.stages()->rowCount(); ++i) 494 | { 495 | out << *scene.stages()->item(i, 0); 496 | } 497 | 498 | // save modes. 499 | out << scene.modes()->rowCount(); 500 | for (int i = 0; i < scene.modes()->rowCount(); ++i) 501 | { 502 | out << *scene.modes()->item(i, 0); 503 | } 504 | 505 | // save nodes. 506 | out << scene.nodes().size(); 507 | for (auto const& node : scene.nodes()) 508 | { 509 | out << *node; 510 | } 511 | 512 | // save links. 513 | out << scene.links().size(); 514 | for (auto const& link : scene.links()) 515 | { 516 | out << static_cast(link->from()->parentItem())->name() << link->from()->name(); 517 | out << static_cast(link->to()->parentItem())->name() << link->to()->name(); 518 | } 519 | 520 | return out; 521 | } 522 | 523 | 524 | QDataStream& operator>>(QDataStream& in, Scene& scene) 525 | { 526 | // Load stages. 527 | int stageCount; 528 | in >> stageCount; 529 | for (int i = 0; i < stageCount; ++i) 530 | { 531 | QStandardItem* item = new QStandardItem(); 532 | in >> *item; 533 | scene.stages()->setItem(i, item); 534 | } 535 | 536 | // Load modes. 537 | int modeCount; 538 | in >> modeCount; 539 | for (int i = 0; i < modeCount; ++i) 540 | { 541 | QStandardItem* item = new QStandardItem(); 542 | in >> *item; 543 | scene.modes()->setItem(i, item); 544 | } 545 | 546 | // Load nodes. 547 | int nodeCount; 548 | in >> nodeCount; 549 | for (int i = 0; i < nodeCount; ++i) 550 | { 551 | Node* node = new Node(); 552 | in >> *node; 553 | scene.addNode(node); 554 | } 555 | 556 | // Load links. 557 | int linkCount; 558 | in >> linkCount; 559 | for (int i = 0; i < linkCount; ++i) 560 | { 561 | QString from, output; 562 | in >> from >> output; 563 | 564 | QString to, input; 565 | in >> to >> input; 566 | 567 | scene.connect(from, output, to, input); 568 | } 569 | 570 | scene.onStageUpdated(); 571 | 572 | return in; 573 | } 574 | 575 | 576 | void Scene::onImportJson(QJsonObject& json) 577 | { 578 | // load stages 579 | QJsonArray stages= json["Stages"].toArray(); 580 | for (int i = 0; i < stages.size(); ++i) 581 | { 582 | QStandardItem* item = new QStandardItem(); 583 | item->setData(generateRandomColor(), Qt::DecorationRole); 584 | item->setData(stages[i].toString(), Qt::DisplayRole); 585 | item->setDropEnabled(false);; 586 | stages_->appendRow(item); 587 | } 588 | 589 | QJsonObject steps = json["Steps"].toObject(); 590 | loadNodesJson(steps); 591 | 592 | // Organize nodes following their stages. 593 | onStageUpdated(); 594 | placeNodesDefaultPosition(); 595 | 596 | QJsonArray links= json["Links"].toArray(); 597 | loadLinksJson(links); 598 | 599 | QJsonObject modes = json["Modes"].toObject(); 600 | loadModesJson(modes); 601 | 602 | 603 | // Display import report if something wrong happened. 604 | if (nodes_import_errors_.isEmpty() and links_import_errors_.isEmpty()) 605 | { 606 | return; 607 | } 608 | 609 | QString errors; 610 | errors += "Nodes:\n"; 611 | for (auto const& err : nodes_import_errors_) { errors += (err + "\n"); } 612 | errors += "Links:\n"; 613 | for (auto const& err : links_import_errors_) { errors += (err + "\n"); } 614 | QMessageBox::warning(nullptr, "Import report", errors); 615 | } 616 | 617 | 618 | void Scene::loadNodesJson(QJsonObject& steps) 619 | { 620 | for (QString stepName : steps.keys()) 621 | { 622 | QJsonObject step = steps[stepName].toObject(); 623 | QString type = step["type"].toString(); 624 | 625 | Node* node = NodeCreator::instance().createItem(type, stepName, "", {0, 0}); 626 | if (node == nullptr) 627 | { 628 | QString error = "Cannot create node " + stepName + ": type " + type + " is unknown."; 629 | nodes_import_errors_.append(error); 630 | continue; 631 | } 632 | 633 | for (QString member : step.keys()) 634 | { 635 | if (member == "type") 636 | { 637 | continue; 638 | } 639 | if (member == "stage") 640 | { 641 | node->stage() = step["stage"].toString(); 642 | continue; 643 | } 644 | 645 | QVector& attributes = node->attributes(); 646 | 647 | auto cmp = [&member](Attribute* attr) { return attr->name() == member; }; 648 | 649 | auto itObj = std::find_if(attributes.begin(), attributes.end(), cmp); 650 | if (itObj != attributes.end()) 651 | { 652 | (*itObj)->setData(QVariant(step[member].toVariant())); 653 | } 654 | else 655 | { 656 | QString error = "Attribute " + member + " not found: version mismatch ?"; 657 | nodes_import_errors_.append(error); 658 | } 659 | } 660 | 661 | addNode(node); 662 | } 663 | } 664 | 665 | 666 | void Scene::loadLinksJson(QJsonArray& links) 667 | { 668 | for (int i = 0; i < links.size(); ++i) 669 | { 670 | QJsonObject l = links[i].toObject(); 671 | QString from = l["from"].toString(); 672 | QString output = l["out"].toString(); 673 | QString to = l["to"].toString(); 674 | QString input = l["in"].toString(); 675 | connect(from, output, to, input); 676 | } 677 | } 678 | 679 | 680 | void Scene::loadModesJson(QJsonObject& modes) 681 | { 682 | QString defaultMode; 683 | for (auto mode : modes.keys()) 684 | { 685 | if (mode == "default") 686 | { 687 | // special case: the default key define the default mode to use at startup. 688 | defaultMode = modes.value(mode).toString(); 689 | continue; 690 | } 691 | 692 | QStandardItem* item = new QStandardItem(); 693 | item->setData(mode, Qt::DisplayRole); 694 | item->setDropEnabled(false); 695 | 696 | // Store mode configuration 697 | QHash nodeMode = item->data(Qt::UserRole + 2).toHash(); 698 | QJsonObject modeObject = modes[mode].toObject(); 699 | QJsonObject modeConfig = modeObject["configuration"].toObject(); 700 | for (auto node : modeConfig.keys()) 701 | { 702 | auto fromString = [](QString const& modeIn) 703 | { 704 | if (modeIn == "Neutral") 705 | { 706 | return static_cast(Mode::neutral); 707 | } 708 | if (modeIn == "Disable") 709 | { 710 | return static_cast(Mode::disable); 711 | } 712 | return static_cast(Mode::enable); 713 | }; 714 | 715 | nodeMode[node] = fromString(modeConfig[node].toString()); 716 | } 717 | item->setData(nodeMode, Qt::UserRole + 2); 718 | 719 | modes_->appendRow(item); 720 | } 721 | 722 | // apply default mode value. 723 | for (int i = 0; i < modes_->rowCount(); ++i) 724 | { 725 | QStandardItem* item = modes_->item(i, 0); 726 | if (item->data(Qt::DisplayRole).toString() == defaultMode) 727 | { 728 | onModeSetDefault(modes_->indexFromItem(item)); 729 | break; 730 | } 731 | } 732 | } 733 | 734 | 735 | void Scene::placeNodesDefaultPosition() 736 | { 737 | QGraphicsView const* view = views().at(0); 738 | QPointF const scenePos = view->mapToScene(view->viewport()->rect().center()); 739 | 740 | struct stageInfo {int column; int size;}; 741 | QMap stageIndex; 742 | 743 | int row = 0; 744 | QModelIndex index = stages_->index(row, 0); 745 | 746 | while (index.isValid()) 747 | { 748 | QString stage = stages_->data(index, Qt::DisplayRole).toString(); 749 | stageIndex.insert(stage, {row, 0}); 750 | 751 | ++row; 752 | index = stages_->index(row, 0); 753 | } 754 | 755 | // Handle steps without stage 756 | stageIndex.insert("", {row, 0}); 757 | 758 | for (auto n : nodes_) 759 | { 760 | qreal x = scenePos.x() + 300 * stageIndex[n->stage()].column; 761 | qreal y = scenePos.y() + 200 * stageIndex[n->stage()].size; 762 | 763 | stageIndex[n->stage()].size++; 764 | 765 | n->setPos(x ,y); 766 | x += 300; 767 | y += 150; 768 | } 769 | } 770 | 771 | 772 | QColor generateRandomColor() 773 | { 774 | // procedural color generator: the gold ratio 775 | static double nextColorHue = 1.0 / (rand() % 100); // don't need a proper random here 776 | constexpr double golden_ratio_conjugate = 0.618033988749895; // 1 / phi 777 | nextColorHue += golden_ratio_conjugate; 778 | nextColorHue = std::fmod(nextColorHue, 1.0); 779 | 780 | QColor nextColor; 781 | nextColor.setHsvF(nextColorHue, 0.5, 0.99); 782 | return nextColor; 783 | } 784 | } 785 | -------------------------------------------------------------------------------- /src/Scene.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_SCENE_H 2 | #define PIPER_SCENE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace piper 11 | { 12 | class Link; 13 | class Node; 14 | class ExportBackend; 15 | 16 | class Scene : public QGraphicsScene 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | Scene(QObject *parent = nullptr); 22 | virtual ~Scene(); 23 | 24 | void resetStagesColor(); 25 | void updateStagesColor(QString const& stage, QColor const& color); 26 | 27 | void addNode(Node* node); 28 | void removeNode(Node* node); 29 | QVector const& nodes() const { return nodes_; } 30 | 31 | QByteArray copyCurrentScene(); 32 | void loadSceneFromStack(QStack stack); 33 | void undo(); 34 | void redo(); 35 | void addLink(Link* link); 36 | void removeLink(Link* link); 37 | QVector const& links() const { return links_; } 38 | void connect(QString const& from, QString const& out, QString const& to, QString const& in); 39 | 40 | QModelIndex addMode(QString const& name); 41 | 42 | QStandardItemModel* stages() const { return stages_; } 43 | QStandardItemModel* modes() const { return modes_; } 44 | 45 | void onExport(ExportBackend& backend); 46 | void onImportJson(QJsonObject& json); 47 | 48 | public slots: 49 | void onModeSelected(QModelIndex const& index); 50 | void onModeSetDefault(QModelIndex const& index); 51 | void onModeRemoved(); 52 | 53 | void onStageUpdated(); 54 | 55 | protected: 56 | void drawBackground(QPainter *painter, const QRectF &rect) override; 57 | void keyReleaseEvent(QKeyEvent *keyEvent) override; 58 | 59 | private: 60 | void loadNodesJson(QJsonObject& steps); 61 | void loadLinksJson(QJsonArray& links); 62 | void loadModesJson(QJsonObject& modes); 63 | void placeNodesDefaultPosition(); 64 | 65 | QVector nodes_; 66 | QVector links_; 67 | 68 | QVector nodes_import_errors_; 69 | QVector links_import_errors_; 70 | 71 | QStandardItemModel* stages_; 72 | QStandardItemModel* modes_; 73 | 74 | static QStack undoStack_; 75 | static QStack redoStack_; 76 | }; 77 | 78 | QDataStream& operator<<(QDataStream& out, Scene const& scene); 79 | QDataStream& operator>>(QDataStream& in, Scene& scene); 80 | QColor generateRandomColor(); 81 | } 82 | 83 | #endif 84 | 85 | -------------------------------------------------------------------------------- /src/ThemeManager.cc: -------------------------------------------------------------------------------- 1 | #include "ThemeManager.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace piper 11 | { 12 | piper::ThemeManager& ThemeManager::instance() 13 | { 14 | static ThemeManager instance; 15 | return instance; 16 | } 17 | 18 | 19 | NodeTheme ThemeManager::getNodeTheme() const 20 | { 21 | return node_theme_; 22 | } 23 | 24 | 25 | AttributeTheme ThemeManager::getAttributeTheme() const 26 | { 27 | return attribute_theme_; 28 | } 29 | 30 | 31 | DataTypeTheme ThemeManager::getDataTypeTheme(QString const& dataType) 32 | { 33 | if (data_type_themes_.contains(dataType)) 34 | { 35 | return data_type_themes_[dataType]; 36 | } 37 | return data_type_themes_["default"]; 38 | } 39 | 40 | 41 | bool ThemeManager::load(const QString& theme_filename) 42 | { 43 | QFile io(theme_filename); 44 | if (not io.open(QIODevice::ReadOnly)) 45 | { 46 | qWarning() << "Can't open theme file" << theme_filename; 47 | return false; 48 | } 49 | 50 | 51 | QByteArray file_data = io.readAll(); 52 | QJsonParseError error; 53 | QJsonDocument theme_document = QJsonDocument::fromJson(file_data, &error); 54 | if (error.error != QJsonParseError::NoError) 55 | { 56 | qWarning() << "Error while parsing theme file:" << error.errorString(); 57 | return false; 58 | } 59 | 60 | if (not parseNode(theme_document.object())) 61 | { 62 | return false; 63 | } 64 | 65 | if (not parseAttribute(theme_document.object())) 66 | { 67 | return false; 68 | } 69 | 70 | if (not parseDataType(theme_document.object())) 71 | { 72 | return false; 73 | } 74 | 75 | return true; 76 | } 77 | 78 | 79 | bool ThemeManager::parseNode(QJsonObject const& json) 80 | { 81 | if (not (json.contains("node") and json["node"].isObject())) 82 | { 83 | qWarning() << "Can't parse node"; 84 | return false; 85 | } 86 | QJsonObject node = json["node"].toObject(); 87 | 88 | QJsonObject name = node["name"].toObject(); 89 | node_theme_.name_font = parseFont(name["font"].toObject()); 90 | node_theme_.name_color = parseColor(name["font"].toObject()); 91 | 92 | node_theme_.background = parseColor(node["background"].toObject()); 93 | 94 | QJsonObject border = node["border"].toObject(); 95 | node_theme_.border = parseColor(border["normal"].toObject()); 96 | node_theme_.border_selected = parseColor(border["selected"].toObject());; 97 | 98 | QJsonObject type = node["type"].toObject(); 99 | node_theme_.type_font = parseFont(type["font"].toObject()); 100 | node_theme_.type_color = parseColor(type["font"].toObject()); 101 | node_theme_.type_background = parseColor(type["background"].toObject());; 102 | 103 | return true; 104 | } 105 | 106 | 107 | bool ThemeManager::parseAttribute(QJsonObject const& json) 108 | { 109 | if (not (json.contains("attribute") and json["attribute"].isObject())) 110 | { 111 | qWarning() << "Can't parse attribute"; 112 | return false; 113 | } 114 | QJsonObject attribute = json["attribute"].toObject(); 115 | 116 | attribute_theme_.background = parseColor(attribute["background"].toObject()); 117 | attribute_theme_.background_alt = parseColor(attribute["background_alt"].toObject()); 118 | 119 | auto parseMode = [this](QJsonObject const& json, AttributeTheme::Mode& mode) 120 | { 121 | mode.font = parseFont(json["font"].toObject()); 122 | mode.font_color = parseColor(json["font"].toObject()); 123 | 124 | QJsonObject connector = json["connector"].toObject(); 125 | mode.connector.border_color = parseColor(json["connector"].toObject()); 126 | mode.connector.border_width = connector["width"].toInt(); 127 | }; 128 | 129 | parseMode(attribute["minimize"].toObject(), attribute_theme_.minimize); 130 | parseMode(attribute["normal"].toObject(), attribute_theme_.normal); 131 | parseMode(attribute["highlight"].toObject(), attribute_theme_.highlight); 132 | 133 | return true; 134 | } 135 | 136 | 137 | bool ThemeManager::parseDataType(QJsonObject const& json) 138 | { 139 | if (not (json.contains("data_type") and json["data_type"].isObject())) 140 | { 141 | qWarning() << "Can't parse data type"; 142 | return false; 143 | } 144 | QJsonObject data_type = json["data_type"].toObject(); 145 | 146 | DataTypeTheme theme; 147 | theme.enable = parseColor(data_type["enable_default"].toObject()); 148 | theme.disable = parseColor(data_type["disable"].toObject()); 149 | theme.neutral = parseColor(data_type["neutral"].toObject()); 150 | data_type_themes_["default"] = theme; 151 | 152 | QJsonObject enable_custom = data_type["enable_custom"].toObject(); 153 | for (auto const& key : enable_custom.keys()) 154 | { 155 | theme.enable = parseColor(enable_custom[key].toObject()); 156 | data_type_themes_[key] = theme; 157 | } 158 | 159 | return true; 160 | } 161 | 162 | 163 | QColor ThemeManager::parseColor(QJsonObject const& json) 164 | { 165 | QJsonArray rgba = json["rgba"].toArray(); 166 | return QColor{ rgba[0].toInt(), rgba[1].toInt(), rgba[2].toInt(), rgba[3].toInt()}; 167 | } 168 | 169 | 170 | QFont ThemeManager::parseFont(QJsonObject const& json) 171 | { 172 | QString name = json["name"].toString(); 173 | QString weight = json["weight"].toString(); 174 | int size = json["size"].toInt(); 175 | 176 | QFont::Weight qweight = QFont::Normal; 177 | if (weight == "Bold") 178 | { 179 | qweight = QFont::Bold; 180 | } 181 | else if (weight == "Medium") 182 | { 183 | qweight = QFont::Medium; 184 | } 185 | else if (weight == "Normal") 186 | { 187 | qweight = QFont::Normal; 188 | } 189 | else if (weight == "Light") 190 | { 191 | qweight = QFont::Light; 192 | } 193 | else 194 | { 195 | qWarning() << "Unknown font weight" << weight; 196 | } 197 | 198 | return QFont{ name, size, qweight }; 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/ThemeManager.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_THEME_MANAGER_H 2 | #define PIPER_THEME_MANAGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace piper 9 | { 10 | struct NodeTheme 11 | { 12 | QFont name_font; 13 | QColor name_color; 14 | 15 | QColor background; 16 | 17 | QFont type_font; 18 | QColor type_color; 19 | QColor type_background; 20 | 21 | QColor border; 22 | QColor border_selected; 23 | }; 24 | 25 | struct AttributeTheme 26 | { 27 | QColor background; 28 | QColor background_alt; 29 | 30 | struct Mode 31 | { 32 | QFont font; 33 | QColor font_color; 34 | struct 35 | { 36 | int border_width; 37 | QColor border_color; 38 | } connector; 39 | }; 40 | 41 | Mode minimize; 42 | Mode normal; 43 | Mode highlight; 44 | }; 45 | 46 | 47 | struct DataTypeTheme 48 | { 49 | QColor enable; 50 | QColor disable; 51 | QColor neutral; 52 | }; 53 | 54 | 55 | class ThemeManager 56 | { 57 | public: 58 | static ThemeManager& instance(); 59 | 60 | bool load(QString const& theme_filename); 61 | NodeTheme getNodeTheme() const; 62 | AttributeTheme getAttributeTheme() const; 63 | DataTypeTheme getDataTypeTheme(QString const& dataType); 64 | 65 | private: 66 | ThemeManager() = default; 67 | 68 | bool parseNode(QJsonObject const& json); 69 | bool parseAttribute(QJsonObject const& json); 70 | bool parseDataType(QJsonObject const& json); 71 | QColor parseColor(QJsonObject const& json); 72 | QFont parseFont(QJsonObject const& json); 73 | 74 | NodeTheme node_theme_; 75 | AttributeTheme attribute_theme_; 76 | QHash data_type_themes_; 77 | }; 78 | } 79 | 80 | 81 | #endif 82 | 83 | -------------------------------------------------------------------------------- /src/Types.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_TYPES_H 2 | #define PIPER_TYPES_H 3 | 4 | namespace piper 5 | { 6 | // Possible modes of a node during execution 7 | enum Mode 8 | { 9 | enable, // The node is enable and run at its nominal behavior 10 | disable, // The node is disable and shall not interact with the pipeline (dead end) 11 | neutral // The node shall interact with the pipeline in a neutral step (i.e. passthrough, add 0, multiply with 1, etc.) 12 | }; 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/View.cc: -------------------------------------------------------------------------------- 1 | #include "View.h" 2 | #include "Scene.h" 3 | #include "Node.h" 4 | #include "Link.h" 5 | #include "CreatorPopup.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace piper 14 | { 15 | QByteArray View::copy_{}; 16 | 17 | 18 | View::View(QWidget* parent) 19 | : QGraphicsView(parent) 20 | { 21 | setFocusPolicy(Qt::ClickFocus); 22 | setDragMode(QGraphicsView::RubberBandDrag); 23 | 24 | creator_ = new CreatorPopup(this); 25 | } 26 | 27 | 28 | void View::goHome() 29 | { 30 | fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio); 31 | } 32 | 33 | 34 | void View::wheelEvent(QWheelEvent* event) 35 | { 36 | setTransformationAnchor(QGraphicsView::AnchorUnderMouse); 37 | 38 | constexpr qreal inFactor = 1.15; 39 | constexpr qreal outFactor = 1 / inFactor; 40 | 41 | qreal zoomFactor = outFactor; 42 | if (event->delta() > 0) 43 | { 44 | zoomFactor = inFactor; 45 | } 46 | 47 | scale(zoomFactor, zoomFactor); 48 | } 49 | 50 | 51 | void View::keyPressEvent(QKeyEvent* event) 52 | { 53 | // Keyboard zoom 54 | setTransformationAnchor(QGraphicsView::AnchorViewCenter); 55 | constexpr qreal inFactor = 1.15; 56 | constexpr qreal outFactor = 1 / inFactor; 57 | 58 | if ((event->key() == Qt::Key::Key_Plus) and (event->modifiers() & Qt::ControlModifier)) 59 | { 60 | scale(inFactor, inFactor); 61 | event->accept(); 62 | } 63 | if ((event->key() == Qt::Key::Key_Minus) and (event->modifiers() & Qt::ControlModifier)) 64 | { 65 | scale(outFactor, outFactor); 66 | event->accept(); 67 | } 68 | if (event->key() == Qt::Key::Key_Equal) 69 | { 70 | creator_->popup(); 71 | event->accept(); 72 | } 73 | if (event->key() == Qt::Key::Key_Escape) 74 | { 75 | goHome(); 76 | event->accept(); 77 | } 78 | if ((event->key() == Qt::Key::Key_C) and (event->modifiers() & Qt::ControlModifier)) 79 | { 80 | copy(); 81 | event->accept(); 82 | } 83 | if ((event->key() == Qt::Key::Key_V) and (event->modifiers() & Qt::ControlModifier)) 84 | { 85 | paste(); 86 | event->accept(); 87 | } 88 | if ((event->key() == Qt::Key::Key_Z) and (event->modifiers() & Qt::ControlModifier)) 89 | { 90 | undo(); 91 | event->accept(); 92 | } 93 | if ((event->key() == Qt::Key::Key_Y) and (event->modifiers() & Qt::ControlModifier)) 94 | { 95 | redo(); 96 | event->accept(); 97 | } 98 | 99 | QGraphicsView::keyPressEvent(event); 100 | } 101 | 102 | 103 | void View::mousePressEvent(QMouseEvent* event) 104 | { 105 | if (event->button() == Qt::MiddleButton) 106 | { 107 | pan_ = true; 108 | panStartX_ = event->x(); 109 | panStartY_ = event->y(); 110 | viewport()->setCursor(Qt::ClosedHandCursor); // use viewport to workaround a refresh bug 111 | event->accept(); 112 | return; 113 | } 114 | QGraphicsView::mousePressEvent(event); 115 | } 116 | 117 | 118 | void View::mouseMoveEvent(QMouseEvent* event) 119 | { 120 | if (pan_) 121 | { 122 | horizontalScrollBar()->setValue(horizontalScrollBar()->value() - (event->x() - panStartX_)); 123 | verticalScrollBar()->setValue( verticalScrollBar()->value() - (event->y() - panStartY_)); 124 | panStartX_ = event->x(); 125 | panStartY_ = event->y(); 126 | event->accept(); 127 | return; 128 | } 129 | QGraphicsView::mouseMoveEvent(event); 130 | } 131 | 132 | 133 | void View::mouseReleaseEvent(QMouseEvent* event) 134 | { 135 | if (event->button() == Qt::MiddleButton) 136 | { 137 | pan_ = false; 138 | viewport()->setCursor(Qt::ArrowCursor); // use viewport to workaround a refresh bug 139 | event->accept(); 140 | return; 141 | } 142 | QGraphicsView::mouseReleaseEvent(event); 143 | } 144 | 145 | 146 | void View::copy() 147 | { 148 | Scene* pScene = static_cast(scene()); 149 | 150 | QList nodes; 151 | for (Node* node : pScene->nodes()) 152 | { 153 | if (node->isSelected()) 154 | { 155 | nodes << node; 156 | } 157 | } 158 | 159 | QList links; 160 | for (Link* link : pScene->links()) 161 | { 162 | if (link->isSelected()) 163 | { 164 | links << link; 165 | } 166 | } 167 | 168 | 169 | QDataStream stream(©_, QIODevice::WriteOnly | QIODevice::Truncate); 170 | stream << nodes.size(); 171 | for (auto const& node : nodes) 172 | { 173 | stream << *node; 174 | } 175 | 176 | stream << links.size(); 177 | for (auto const& link : links) 178 | { 179 | stream << static_cast(link->from()->parentItem())->name() << link->from()->name(); 180 | stream << static_cast(link->to()->parentItem())->name() << link->to()->name(); 181 | } 182 | } 183 | 184 | 185 | void View::paste() 186 | { 187 | Scene* pScene = static_cast(scene()); 188 | 189 | // deselect and move to back all current items in the scene 190 | for (auto& item : pScene->selectedItems()) 191 | { 192 | item->setSelected(false); 193 | item->setZValue(-1); 194 | } 195 | 196 | struct LinkData 197 | { 198 | QString from; 199 | QString output; 200 | QString to; 201 | QString input; 202 | }; 203 | 204 | QDataStream stream(copy_); 205 | QList copies; 206 | QList links; 207 | 208 | // preload nodes and links 209 | int nodeCount; 210 | stream >> nodeCount; 211 | for (int i = 0; i < nodeCount; ++i) 212 | { 213 | Node* node = new Node(); 214 | stream >> *node; 215 | copies << node; 216 | } 217 | 218 | int linkCount; 219 | stream >> linkCount; 220 | for (int i = 0; i < linkCount; ++i) 221 | { 222 | LinkData link; 223 | stream >> link.from >> link.output >> link.to >> link.input; 224 | links << link; 225 | } 226 | 227 | 228 | // compute unique name and insert nodes 229 | for (auto const& copy : copies) 230 | { 231 | for (auto const& node : pScene->nodes()) 232 | { 233 | if (copy->name() == node->name()) 234 | { 235 | QString oldName = copy->name(); 236 | QString newName = copy->name() + "_" + QString::number(pScene->nodes().size()); 237 | copy->setName(newName); 238 | 239 | for (auto& link : links) 240 | { 241 | if (link.from == oldName) { link.from = newName; } 242 | if (link.to == oldName) { link.to = newName; } 243 | qDebug() << "rename link !" << oldName << " " << newName; 244 | } 245 | break; 246 | } 247 | } 248 | copy->setSelected(true); 249 | copy->setZValue(1); 250 | pScene->addNode(copy); 251 | } 252 | 253 | // copy links 254 | for (auto const& link : links) 255 | { 256 | pScene->connect(link.from, link.output, link.to, link.input); 257 | } 258 | 259 | // get current curson position 260 | QPointF cursorScene = mapToScene(mapFromGlobal(QCursor::pos())); 261 | QPointF deltaToCursor; 262 | 263 | // get delta from cursor and pasted items 264 | QList items = pScene->selectedItems(); 265 | QRectF boundingRectangle; 266 | for (auto& item : items) 267 | { 268 | boundingRectangle = boundingRectangle.united(item->sceneBoundingRect()); 269 | } 270 | 271 | deltaToCursor = cursorScene - boundingRectangle.topLeft(); 272 | 273 | // move pasted item to cursor position 274 | for (auto& item : items) 275 | { 276 | item->moveBy(deltaToCursor.x(), deltaToCursor.y()); 277 | } 278 | 279 | 280 | // refresh 281 | pScene->onStageUpdated(); 282 | } 283 | 284 | 285 | void View::undo() 286 | { 287 | Scene* pScene = static_cast(scene()); 288 | pScene->undo(); 289 | } 290 | 291 | void View::redo() 292 | { 293 | Scene* pScene = static_cast(scene()); 294 | pScene->redo(); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/View.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPER_VIEW_H 2 | #define PIPER_VIEW_H 3 | 4 | #include 5 | #include 6 | 7 | 8 | namespace piper 9 | { 10 | class CreatorPopup; 11 | 12 | class View : public QGraphicsView 13 | { 14 | public: 15 | View(QWidget* parent = nullptr); 16 | virtual ~View() = default; 17 | 18 | // Center the view on the items. 19 | void goHome(); 20 | 21 | protected: 22 | void wheelEvent(QWheelEvent* event) override; 23 | void keyPressEvent(QKeyEvent * event) override; 24 | 25 | void mouseMoveEvent(QMouseEvent *event) override; 26 | void mousePressEvent(QMouseEvent *event) override; 27 | void mouseReleaseEvent(QMouseEvent *event) override; 28 | 29 | private: 30 | void copy(); 31 | void paste(); 32 | void undo(); 33 | void redo(); 34 | 35 | CreatorPopup* creator_; 36 | bool pan_{false}; 37 | int panStartX_{}; 38 | int panStartY_{}; 39 | 40 | static QByteArray copy_; 41 | }; 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include "MainEditor.h" 2 | #include "NodeCreator.h" 3 | #include "ThemeManager.h" 4 | #include 5 | 6 | using namespace piper; 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | // Create node types for instance 11 | NodeCreator::instance().addItem( 12 | { 13 | "SinWave", 14 | "Sinus generator", 15 | "Example", 16 | "Generator", 17 | { 18 | {"output", "float", AttributeInfo::Type::output}, 19 | {"amplitude", "float", AttributeInfo::Type::member}, 20 | {"frequency", "float", AttributeInfo::Type::member}, 21 | } 22 | }); 23 | 24 | NodeCreator::instance().addItem( 25 | { 26 | "Random", 27 | "", 28 | "", 29 | "", 30 | { 31 | {"output", "float", AttributeInfo::Type::output}, 32 | {"min", "float", AttributeInfo::Type::member}, 33 | {"max", "float", AttributeInfo::Type::member}, 34 | } 35 | }); 36 | 37 | NodeCreator::instance().addItem( 38 | { 39 | "Add", 40 | "", 41 | "", 42 | "", 43 | { 44 | {"inputA", "float", AttributeInfo::Type::input}, 45 | {"inputB", "float", AttributeInfo::Type::input}, 46 | {"output", "float", AttributeInfo::Type::output}, 47 | } 48 | }); 49 | 50 | NodeCreator::instance().addItem( 51 | { 52 | "LowPass", 53 | "", 54 | "Example", 55 | "Filters", 56 | { 57 | {"inputA", "float", AttributeInfo::Type::input}, 58 | {"output", "float", AttributeInfo::Type::output}, 59 | {"Fc", "float", AttributeInfo::Type::member}, 60 | } 61 | }); 62 | 63 | NodeCreator::instance().addItem( 64 | { 65 | "cast", 66 | "Transform a float to integer \nWarning! take care of precision loss!", 67 | "", 68 | "", 69 | { 70 | {"input", "float", AttributeInfo::Type::input}, 71 | {"output", "int", AttributeInfo::Type::output} 72 | } 73 | }); 74 | 75 | NodeCreator::instance().addItem( 76 | { 77 | "cast", 78 | "", 79 | "", 80 | "", 81 | { 82 | {"input", "float", AttributeInfo::Type::input}, 83 | {"output", "customType", AttributeInfo::Type::output} 84 | } 85 | }); 86 | 87 | NodeCreator::instance().addItem( 88 | { 89 | "probe", 90 | "Record updates in the telemetry system", 91 | "", 92 | "", 93 | { 94 | {"input", "float", AttributeInfo::Type::input}, 95 | } 96 | }); 97 | 98 | NodeCreator::instance().addItem( 99 | { 100 | "probe", 101 | "Record updates in the telemetry system", 102 | "", 103 | "utilities", 104 | { 105 | {"input", "int", AttributeInfo::Type::input}, 106 | } 107 | }); 108 | 109 | NodeCreator::instance().addItem( 110 | { 111 | "probe", 112 | "Record updates in the telemetry system", 113 | "", 114 | "utilities", 115 | { 116 | {"input", "customType", AttributeInfo::Type::input}, 117 | } 118 | }); 119 | 120 | // Load theme 121 | if (not ThemeManager::instance().load("data/theme.json")) 122 | { 123 | return 1; 124 | } 125 | 126 | QApplication app(argc, argv); 127 | Q_INIT_RESOURCE(resources); 128 | MainEditor editor; 129 | editor.show(); 130 | 131 | return app.exec(); 132 | } 133 | --------------------------------------------------------------------------------