qpwgraph-0.6.1/PaxHeaders/CMakeLists.txt0000644000000000000000000000013214532447230015171 xustar0030 mtime=1701465752.090364752 30 atime=1701465752.090364752 30 ctime=1701465752.090364752 qpwgraph-0.6.1/CMakeLists.txt0000644000175000001440000000563314532447230016430 0ustar00rncbcusers00000000000000cmake_minimum_required (VERSION 3.15) project(qpwgraph VERSION 0.6.1 DESCRIPTION "A PipeWire Graph Qt GUI Interface" HOMEPAGE_URL "https://gitlab.freedesktop.org/rncbc/qpwgraph" LANGUAGES C CXX) execute_process ( COMMAND git describe --tags --dirty --abbrev=6 WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_DESCRIBE_OUTPUT RESULT_VARIABLE GIT_DESCRIBE_RESULT OUTPUT_STRIP_TRAILING_WHITESPACE) if (GIT_DESCRIBE_RESULT EQUAL 0) set (GIT_VERSION "${GIT_DESCRIBE_OUTPUT}") string (REGEX REPLACE "^[^0-9]+" "" GIT_VERSION "${GIT_VERSION}") string (REGEX REPLACE "^1_" "" GIT_VERSION "${GIT_VERSION}") string (REGEX REPLACE "^[_vV]+" "" GIT_VERSION "${GIT_VERSION}") string (REGEX REPLACE "-g" "git." GIT_VERSION "${GIT_VERSION}") string (REGEX REPLACE "[_|-]" "." GIT_VERSION "${GIT_VERSION}") execute_process ( COMMAND git rev-parse --abbrev-ref HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_REVPARSE_OUTPUT RESULT_VARIABLE GIT_REVPARSE_RESULT OUTPUT_STRIP_TRAILING_WHITESPACE) if (GIT_REVPARSE_RESULT EQUAL 0 AND NOT GIT_REVPARSE_OUTPUT STREQUAL "main") set (GIT_VERSION "${GIT_VERSION} [${GIT_REVPARSE_OUTPUT}]") endif () set (PROJECT_VERSION "${GIT_VERSION}") endif () # Enable ALSA MIDI support option. option (CONFIG_ALSA_MIDI "Enable ALSA MIDI support (default=yes)" 1) # Enable system-tray icon support option. option (CONFIG_SYSTEM_TRAY "Enable system-tray icon support (default=yes)" 1) # Enable Wayland support option. option (CONFIG_WAYLAND "Enable Wayland support (EXPERIMENTAL) (default=no)" 0) # Enable Qt6 build preference. option (CONFIG_QT6 "Enable Qt6 build (default=yes)" 1) if (CMAKE_BUILD_TYPE MATCHES "Debug") set (CONFIG_DEBUG 1) set (CONFIG_BUILD_TYPE "debug") else () set (CONFIG_DEBUG 0) set (CONFIG_BUILD_TYPE "release") set (CMAKE_BUILD_TYPE "Release") endif () include (GNUInstallDirs) # Check for Qt... if (CONFIG_QT6) find_package (Qt6 QUIET) if (NOT Qt6_FOUND) set (CONFIG_QT6 0) endif () endif () if (CONFIG_QT6) find_package (QT QUIET NAMES Qt6) else () find_package (QT QUIET NAMES Qt5) endif () find_package (Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Xml Svg) if (CONFIG_SYSTEM_TRAY) find_package (Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Network) endif () add_subdirectory (src) # Configuration status macro (SHOW_OPTION text value) if (${value}) message ("${text}: yes") else () message ("${text}: no") endif () endmacro () message ("\n ${PROJECT_NAME} ${PROJECT_VERSION} (Qt ${QT_VERSION})") message ("\n Build target . . . . . . . . . . . . . . . . . . .: ${CONFIG_BUILD_TYPE}\n") show_option (" ALSA MIDI support . . . . . . . . . . . . . . . ." CONFIG_ALSA_MIDI) show_option (" System-tray icon support . . . . . . . . . . . . ." CONFIG_SYSTEM_TRAY) message ("\n Install prefix . . . . . . . . . . . . . . . . . .: ${CMAKE_INSTALL_PREFIX}\n") qpwgraph-0.6.1/PaxHeaders/ChangeLog0000644000000000000000000000013214532447230014203 xustar0030 mtime=1701465752.090364752 30 atime=1701465752.090364752 30 ctime=1701465752.090364752 qpwgraph-0.6.1/ChangeLog0000644000175000001440000001711614532447230015441 0ustar00rncbcusers00000000000000qpwgraph - A PipeWire Graph Qt GUI Interface -------------------------------------------- 0.6.1 2023-12-01 An End-of-Autumn'23 Release. - Introduce Help > Enable ALSA MIDI runtime option, now permitting to disable the ALSA MIDI/Sequencer graph conveniency in a whim. - Disconnect all pinned connections when patchbay is deactivated, subject to Patchbay > Auto Disconnect option. - Fix a potential port duplication when recycled under the same node and reusing a previous port id. - Don't unpin connections that are manually disconnected, when patchbay is deactivated and auto-pin is off. 0.6.0 2023-11-08 An Autumn'23 Release. - Improved Patchbay / Exclusive mode scan enforcement. - Hopefully fixes the hideous random crashes caused by very short lived nodes, recycled by reusing the very same ids. - Cope with nodes that can possibly remain with the very same name but different ids. - Added deactivated (-d, --deactivated) and non-exclusive patchbay (-n, --nonexclusive) command line options. - Fixed unique/single instance support (Qt >= 6.6). 0.5.3 2023-09-08 An end-of-summer'23 release. - Added user contributed documentation: How To Use The Patchbay. - Fix condition for saving node name aliases. 0.5.2 2023-08-05 A high-summer'23 release. - Ctrl+left or middle-button click-dragging for panning, is now a lot smoother, hopefully. - Click-dragging with the mouse middle-button is for panning only, not to start a selection anymore. - Add Ctrl+Q to Quit action 0.5.1 2023-07-17 A summer'23 hot-fix release. - Fixed segfault on initialization that was affecting Qt5 builds. 0.5.0 2023-07-16 Yet another summer'23 release. - Completely refactored the internal PipeWire node registry logic, just to have unique node names, as seen fit to purpose to solve an old undefined behavior to positioning and Patchbay persistence of multiple nodes with the very same and exact name. - Fixed the main PipeWire registry thread-safety, into a two-level critical section, hopefully preventing the race-conditions that are the suspected cause to some rare crashes. 0.4.5 2023-07-10 A summer'23 release. - Split non-physical terminal device nodes for monitor and control ports, adding the suffix "[Monitor]" and/or "[Control]" resp. to the node name. - Fixed the dimming of new connections when Patchbay/Edit mode is on and Patchbay/Auto Pin is off. 0.4.4 2023-06-18 A late-spring'23 regression. - Split devices for capture/monitor and playback ports. (REGRESSION) 0.4.3 2023-06-17 A late-spring'23 release. - Split devices for capture/monitor and playback ports. 0.4.2 2023-04-02 An early-spring'23 release. - Soft incremental bounds constraints now imposed to all new and old nodes positioning. - Attempt to auto-start minimized to system-tray icon, if enabled, when restoring a desktop session (eg. after logout, shutdown or restart). 0.4.1 2023-03-03 A late-winter'23 release. - Attempt to make port labels as short as possible. - Fixed a possible crash when several PW objects (nodes and ports) are created and destroyed in fast succession. 0.4.0 2023-02-25 A mid-winter'23 release. - Node names now have the "media.name" property as a bracketed suffix; when given and applicable. - Node icons now reflecting their proper application/theme icons or else, a bland and generic default taken from the "client.api" property (eg. "pw", "jack" or "pulse"). - Introducing touch pinch-gesture for zooming. - Bumping copyright headers to the brand new year. 0.3.9 2022-12-27 An end-of-year'22 release - Whether to draw connectors through or around nodes is now an user preference option (cf. View > Connect Through Nodes). 0.3.8 2022-11-19 A mid-autumn'22 release. - Allow middle mouse button for grabbing and dragging the canvas. 0.3.7 2022-10-22 An autumn'22 release. - Fixed the system-tray icon tooltip to always reflect current main window title, usually the current patchbay name. - Make up visual immediate feedback connectlons. 0.3.6 2022-09-24 An early-autumn'22 release. - View / Repel Overlapping Nodes option added. 0.3.5 2022-08-20 A thirteenth beta release. - Patchbay/Scan menu command removed as redundand. - Added Patchbay/Auto Pin connections option (issue #56). - Add current system user-name to the singleton/unique application instance identifier. 0.3.4 2022-07-08 A twelfth beta release. - Fixed repainting of pinned/unpinned connections when switching patchbay profiles and Patchbay/Edit mode is on. 0.3.3 2022-07-06 An eleventh beta release. - Patchbay/Edit mode introduced: pinning and unpinning connections to and from current patchbay is now implemented. - Original Graph/Connect and Disconnect keyboard shortcuts, [Ins] and [Del], are now added to the existing ones, respectively. 0.3.2 2022-06-13 A tenth beta release. - Fixed initial nodes layout positioning, now back to the former spiraled away from the center. 0.3.1 2022-05-29 A ninth beta release. - Only ask to quit an activated patchbay when actually quitting the application (not just closing a patchbay). - Graph/Connect and Disconnect keyboard shortcuts changed from [Ins] and [Del], to [Ctrl+C] and [Ctrl+D] respectively; also added [F2] as brand new keyboard shortcut for Edit/Rename... 0.3.0 2022-05-21 An eighth beta release. - Fixed document dirtiness (modified state) when making connections and/or disconnections on a clear and new patchbay. - Attempt to save and possibly restore different node positions and aliases when former original node name is non-unique. 0.2.6 2022-04-23 A seventh beta release. - Patchbay now treats multiple nodes and respective ports with the same name as one, applying the same rule. 0.2.5 2022-04-06 A sixth beta release. - Prevent an graph refresh or update as much as possible while in some canvas editing business (fixes issue #29). - Possibly fix a random segfault when rendering connection lines ahead of time (possibly mitigating issue #26). 0.2.4 2022-03-19 A fifth beta release. - Whether to enable the system-tray icon option has been added to main menu (cf. Help > System Tray Icon). - Allow the Patchbay toolbar to also have a vertical orientation, on the left and right areas of the main window. - Added a barebones man page to install procedure. - Added missing file code to desktop exec entry. 0.2.3 2022-03-12 A fourth beta release. - Added start minimized (-m, --minimized) command line option. - Main application icon is now presented in scalable format (SVG). 0.2.2 2022-03-02 A thrice beta than before. - Application ID changed from org.freedesktop.rncbc.qpwgraph to org.rncbc.qpwgraph (affecting appdata/metainfo and mime/types). - Fixed system-tray to show the main window up when minimized. 0.2.1 2022-02-26 Just a second beta. - Patchbay feature introduced: save connections to file; restore connections from file and maintain when activated; disconnect all others when activated in exclusive mode. - Migrated command line parsing to QCommandLineParser/Option (Qt >= 5.2). 0.2.0 2022-01-16 Enter first beta. - Retry/recover from PipeWire service errors/outages automatically. - Nodes and port renames (titles aka aliases) are now persistent. - Corrected appdata file suffix to .metainfo.xml 0.1.3 2022-01-13 A Winter'22 Release. - Updated and renamed appdata and desktop files. 0.1.2 2021-12-31 One third alpha. - ALSA MIDI (Sequencer) support is now opted in by default. 0.1.1 2021-12-18 One second alpha. - Added libpipewire (and headers) version information to about box. - Added icons, desktop and appstream data to installation. 0.1.0 2021-12-06 One first alpha. qpwgraph-0.6.1/PaxHeaders/LICENSE.md0000644000000000000000000000013214532447230014035 xustar0030 mtime=1701465752.090364752 30 atime=1701465752.090364752 30 ctime=1701465752.090364752 qpwgraph-0.6.1/LICENSE.md0000644000175000001440000004302514532447230015271 0ustar00rncbcusers00000000000000GNU General Public License ========================== _Version 2, June 1991_ _Copyright © 1989, 1991 Free Software Foundation, Inc.,_ _51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA_ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. ### Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: **(1)** copyright the software, and **(2)** offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. ### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION **0.** This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The “Program”, below, refers to any such program or work, and a “work based on the Program” means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term “modification”.) Each licensee is addressed as “you”. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. **1.** You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. **2.** You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: * **a)** You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. * **b)** You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. * **c)** If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. **3.** You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: * **a)** Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, * **b)** Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, * **c)** Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. **4.** You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. **5.** You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. **6.** Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. **7.** If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. **8.** If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. **9.** The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and “any later version”, you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. **10.** If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. ### NO WARRANTY **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ### How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w` and `show c` should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w` and `show c`; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a “copyright disclaimer” for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. qpwgraph-0.6.1/PaxHeaders/README.md0000644000000000000000000000013214532447230013710 xustar0030 mtime=1701465752.090364752 30 atime=1701465752.090364752 30 ctime=1701465752.090364752 qpwgraph-0.6.1/README.md0000644000175000001440000000360014532447230015137 0ustar00rncbcusers00000000000000# qpwgraph - A PipeWire Graph Qt GUI Interface ![Screenshot](src/images/qpwgraph_screenshot-1.png) **qpwgraph** is a graph manager dedicated to [PipeWire](https://pipewire.org), using the [Qt C++ framework](https://qt.io), based and pretty much like the same of [QjackCtl](https://qjackctl.sourceforge.io). Source code repository: https://gitlab.freedesktop.org/rncbc/qpwgraph Upstream author: Rui Nuno Capela . ## Prerequisites **qpwgraph** software prerequisites for building are a C++17 compiler (_g++_), the [Qt C++ framework](https://qt.io) (_qt6-qtbase-devel_ or _qt5-qtbase-devel_) and of course the [PipeWire API](https://pipewire.org) C development libraries and headers (_pipewire-devel_). Optionally on build configure time, [ALSA](https://www.alsa-project.org) development libraries and headers (_alsa-devel_) are also required if ALSA MIDI (Sequencer) support is desired (`cmake -DCONFIG_ALSA_MIDI=[1|ON]`...). ## Building **qpwgraph** uses the [CMake](https://cmake.org) build system, version 3.15 or newer. On the source distribution top directory: cmake [-DCMAKE_INSTALL_PREFIX=] -B build cmake --build build [--parallel ] After successful build you may test run it immedialy as follows: build/src/qpwgraph If you may install it permanently, then run, optionally as root: [sudo] cmake --install build Note that the default installation path (\<_prefix_\>) is `/usr/local` . Enjoy. ## Documentation * [User Manual](docs/qpwgraph-user_manual.md) * [How To Use The Patchbay](docs/qpwgraph_patchbay-user_manual.md) ## License **qpwgraph** is free, open-source software, distributed under the terms of the GNU General Public License ([GPL](https://www.gnu.org/copyleft/gpl.html)) version 2 or later. ## Copyright Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. qpwgraph-0.6.1/PaxHeaders/docs0000644000000000000000000000013214532447230013304 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/docs/0000755000175000001440000000000014532447230014611 5ustar00rncbcusers00000000000000qpwgraph-0.6.1/docs/PaxHeaders/qpwgraph_patchbay-user_manual.md0000644000000000000000000000013214532447230021720 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/docs/qpwgraph_patchbay-user_manual.md0000644000175000001440000000370514532447230023155 0ustar00rncbcusers00000000000000# How To Use The Patchbay ![Patchbay](../src/images/itemPatchbay.png) Once a connection patchbay established, it is possible to store those connections in a patchbay configuration that can be then restored when loaded back. ## *Activated* button ![Activated](../src/images/itemActivate.png) The *Activated* button is simply to activate or not the loaded patchbay. The patchbay must be activated in order to access all the other functionalities. If checked, connections stored in the loaded patchbay will be restored. All other connections will stay as they were before the load and activation of the patchbay. If and when unchecked, all previously pinned connections will be dropped. ## *Exclusive* button ![Exclusive](../src/images/itemExclusive.png) The *Exclusive* button goal it to determine if the connections stored in the loaded and activated patchbay will be the only active connections or if previous connections are allowed. If checked, connections not stored in the loaded and activated patchbay will be removed. If unchecked, the loaded and activated patchbay will only create the stored connections. ## *Edit* button ![Edit](../src/images/itemEdit.png) If checked, the buttons *Pin* and *Unpin* are available. Those functions are only for connections, so at least one connection (line between an input and an output) has to be selected. ### *Pin* button ![Pin](../src/images/itemPin.png) Makes the connection persistant in the currently loaded and activated patchbay. ### *Unpin* button ![Unpin](../src/images/itemUnpin.png) Used to make the connection temporary. The unpinned connection will not be dropped if the current patchbay is deactivated. ### Auto Pin option If checked, all manual connections will be pinned to the current patchbay and persistant when activated. ### Auto Disconnect option If checked, all pinned connections will be automatically disconnected when the current patchbay is deactivated. --- Credits: @Lootre (a.k.a. Thomas Lachat). qpwgraph-0.6.1/docs/PaxHeaders/qpwgraph-user_manual.md0000644000000000000000000000013214532447230020045 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/docs/qpwgraph-user_manual.md0000644000175000001440000000730414532447230021301 0ustar00rncbcusers00000000000000# qpwgraph User Manual **qpwgraph** is a graph manager dedicated to [PipeWire](https://pipewire.org), using the [Qt C++ framework](https://qt.io), based and pretty much like the same of [QjackCtl](https://qjackctl.sourceforge.io). The source code is available [on freedesktop.org's GitLab](https://gitlab.freedesktop.org/rncbc/qpwgraph) and also mirrored [on GitHub](https://github.com/rncbc/qpwgraph). The core of the interface is a canvas showing all relevant nodes from PipeWire, with their available ports. ## Ports Ports are directional, they can be either: * Source ports (i.e. output). Located at the right-most edge of a node, they generate an audio/video/midi stream. * Sink ports (i.e. input). Located at the left-most edge of a node, they consume an audio/video/midi stream. Ports also have different types: * Audio (default color: green) * Video (default color: blue) * PipeWire/JACK MIDI (default color: red) * ALSA MIDI (default color: purple) Ports of the same type and opposite directions can be connected. ## Keyboard and mouse shortcuts ### Navigation | Interaction | Action | |----------------------------|-----------------------------| | Middle click and drag | Pan the canvas | | Ctrl + left click and drag | Pan the canvas | | Mouse scroll | Pan the canvas vertically | | Alt + mouse scroll | Pan the canvas horizontally | | Ctrl + mouse scroll | Zoom the canvas | | Ctrl + Plus | Zoom in the canvas | | Ctrl + Minus | Zoom out the canvas | | Ctrl + 1 | Zoom to 100% | | Ctrl + 0 | Zoom to fit contents | ### Selection | Interaction | Action | |-----------------------|-------------------------------| | Left click | Select a node or a port | | Left click and drag | Rectangular selection | | Shift + click or drag | Add to the selection | | Ctrl + click or drag | Invert (toggle) the selection | | Ctrl + A | Select all | | Ctrl + Shift + A | Select none | | Ctrl + I | Invert selection | ### Linking You can link ports by left click and dragging from one port to another. You can also: | Interaction | Action | |-------------|------------------------------------| | Insert | Link the selected nodes or ports | | Ctrl + C | Link the selected nodes or ports | | Delete | Unlink the selected nodes or ports | | Ctrl + D | Unlink the selected nodes or ports | ### Misc. | Interaction | Action | |------------------|----------------------------| | Ctrl + Z | Undo | | Ctrl + Shift + Z | Redo | | F2 | Rename a node or a port | | Double-click | Rename a node or a port | | Ctrl + M | Toggle menu bar visibility | | Ctrl + Q | Quit | | F5 | Refresh (usually not needed, as the canvas is updated automatically) | ## Configuration file qpwgraph will remember the position of each node, as well as their custom names. The configuration is located at `$XDG_CONFIG_HOME/rncbc.org/qpwgraph.conf` (usually `~/.config/rncbc.org/qpwgraph.conf`). The configuration is saved when the application closes. ## Patchbay qpwgraph can remember the current connections and apply them again in a later moment. This feature is called Patchbay, and it is further documented at [How To Use The Patchbay](qpwgraph_patchbay-user_manual.md) --- Credits: @denilsonsa (a.k.a. Denilson Sá Maia). qpwgraph-0.6.1/PaxHeaders/src0000644000000000000000000000013214532447230013143 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.091364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/0000755000175000001440000000000014532447230014450 5ustar00rncbcusers00000000000000qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_canvas.h0000644000000000000000000000013214532447230016555 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_canvas.h0000644000175000001440000002012414532447230020004 0ustar00rncbcusers00000000000000// qpwgraph_canvas.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_canvas_h #define __qpwgraph_canvas_h #include #include "qpwgraph_command.h" #include // Forward decls. class QGraphicsScene; class QRubberBand; class QUndoStack; class QSettings; class QGraphicsProxyWidget; class QLineEdit; class QMouseEvent; class QWheelEvent; class QKeyEvent; class QGestureEvent; class QPinchGesture; class qpwgraph_patchbay; // Define if cleanup of legacy node names is needed (v0.5.0)... #undef CONFIG_CLEANUP_NODE_NAMES //---------------------------------------------------------------------------- // qpwgraph_canvas -- Canvas graphics scene/view. class qpwgraph_canvas : public QGraphicsView { Q_OBJECT public: // Constructor. qpwgraph_canvas(QWidget *parent = nullptr); // Destructor. ~qpwgraph_canvas(); // Accessors. QGraphicsScene *scene() const; QUndoStack *commands() const; void setSettings(QSettings *settings); QSettings *settings() const; qpwgraph_patchbay *patchbay() const; // Patchbay auto-pin accessors. void setPatchbayAutoPin(bool on); bool isPatchbayAutoPin() const; // Patchbay auto-disconnect accessors. void setPatchbayAutoDisconnect(bool on); bool isPatchbayAutoDisconnect() const; // Patchbay edit-mode accessors. void setPatchbayEdit(bool on); bool isPatchbayEdit() const; void patchbayEdit(); bool canPatchbayPin() const; bool canPatchbayUnpin() const; void patchbayPin(); void patchbayUnpin(); // Canvas methods. void addItem(qpwgraph_item *item); void removeItem(qpwgraph_item *item); // Current item accessor. qpwgraph_item *currentItem() const; // Connection predicates. bool canConnect() const; bool canDisconnect() const; // Edit predicates. bool canRenameItem() const; // Zooming methods. void setZoom(qreal zoom); qreal zoom() const; void setZoomRange(bool zoomrange); bool isZoomRange() const; // Clean-up all un-marked nodes... void resetNodes(uint node_type); void clearNodes(uint node_type); // Special node finders. qpwgraph_node *findNode( uint id, qpwgraph_item::Mode mode, uint type = 0) const; QList findNodes( const qpwgraph_node::NodeNameKey& name_key) const; QList findNodes( const QString& name, qpwgraph_item::Mode mode, uint type = 0) const; void releaseNode(qpwgraph_node *node); // Whether it's in the middle of something... bool isBusy() const; // Port (dis)connections dispatcher. void emitConnectPorts( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect); // Port (dis)connections notifiers. void emitConnected(qpwgraph_port *port1, qpwgraph_port *port2); void emitDisconnected(qpwgraph_port *port1, qpwgraph_port *port2); // Rename notifiers. void emitRenamed(qpwgraph_item *item, const QString& name); // Graph canvas state methods. bool restoreState(); bool saveState() const; // Repel overlapping nodes... void setRepelOverlappingNodes(bool on); bool isRepelOverlappingNodes() const; void repelOverlappingNodes(qpwgraph_node *node, qpwgraph_move_command *move_command = nullptr, const QPointF& delta = QPointF()); void repelOverlappingNodesAll( qpwgraph_move_command *move_command = nullptr); // Graph colors management. void setPortTypeColor(uint port_type, const QColor& color); const QColor& portTypeColor(uint port_type); void updatePortTypeColors(uint port_type = 0); void clearPortTypeColors(); // Clear all selection. void clearSelection(); // Clear all state. void clear(); // Snap into position helper. QPointF snapPos(qreal x, qreal y) const; #ifdef CONFIG_CLEANUP_NODE_NAMES static bool cleanupNodeName(QString& name); #endif signals: // Node factory notifications. void added(qpwgraph_node *node); void updated(qpwgraph_node *node); void removed(qpwgraph_node *node); // Port (dis)connection notifications. void connected(qpwgraph_port *port1, qpwgraph_port *port2); void disconnected(qpwgraph_port *port1, qpwgraph_port *port2); void connected(qpwgraph_connect *connect); // Generic change notification. void changed(); // Rename notification. void renamed(qpwgraph_item *item, const QString& name); public slots: // Dis/connect selected items. void connectItems(); void disconnectItems(); // Select actions. void selectAll(); void selectNone(); void selectInvert(); // Edit actions. void renameItem(); // Discrete zooming actions. void zoomIn(); void zoomOut(); void zoomFit(); void zoomReset(); // Update all nodes. void updateNodes(); // Update all connectors. void updateConnects(); protected slots: // Rename item slots. void textChanged(const QString&); void editingFinished(); protected: // Item finder (internal). qpwgraph_item *itemAt(const QPointF& pos) const; // Port (dis)connection commands. void connectPorts( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect); // Mouse event handlers. void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void mouseDoubleClickEvent(QMouseEvent *event); void wheelEvent(QWheelEvent *event); // Keyboard event handler. void keyPressEvent(QKeyEvent *event); // Gesture event handlers. bool event(QEvent *event); bool gestureEvent(QGestureEvent *event); void pinchGesture(QPinchGesture *pinch); // Graph node/port key helpers. QString nodeKey(qpwgraph_node *node, int n = 0) const; QString portKey(qpwgraph_port *port) const; void addNodeKeys(qpwgraph_node *node); void removeNodeKeys(qpwgraph_node *node); // Zoom in rectangle range. void zoomFitRange(const QRectF& range_rect); // Graph node/port state methods. bool restoreNode(qpwgraph_node *node); bool saveNode(qpwgraph_node *node) const; bool restorePort(qpwgraph_port *port); bool savePort(qpwgraph_port *port) const; // Renaming editor position and size updater. void updateEditorGeometry(); // Bounding margins/limits... const QRectF& boundingRect(bool reset = false); void boundingPos(QPointF& pos); // Snap into position helper. void snapPos(QPointF& pos) const; #ifdef CONFIG_CLEANUP_NODE_NAMES void cleanupNodeNames(const char *group); #endif private: // Mouse pointer dragging states. enum DragState { DragNone = 0, DragStart, DragMove, DragScroll }; // Instance variables. QGraphicsScene *m_scene; DragState m_state; QPointF m_pos; qpwgraph_item *m_item; qpwgraph_connect *m_connect; QRubberBand *m_rubberband; qreal m_zoom; bool m_zoomrange; bool m_gesture; qpwgraph_node::NodeIds m_node_ids; qpwgraph_node::NodeNames m_node_names; QList m_nodes; QUndoStack *m_commands; QSettings *m_settings; qpwgraph_patchbay *m_patchbay; bool m_patchbay_edit; bool m_patchbay_autopin; bool m_patchbay_autodisconnect; QList m_selected; int m_selected_nodes; bool m_repel_overlapping_nodes; // Graph port colors. QHash m_port_colors; // Item renaming stuff. qpwgraph_item *m_edit_item; QLineEdit *m_editor; int m_edited; // Original node position (for move command). QPointF m_pos1; // Allowed auto-scroll margins/limits (for move command). QRectF m_rect1; }; #endif // __qpwgraph_canvas_h // end of qpwgraph_canvas.h qpwgraph-0.6.1/src/PaxHeaders/mimetypes0000644000000000000000000000013214532447230015157 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/mimetypes/0000755000175000001440000000000014532447230016464 5ustar00rncbcusers00000000000000qpwgraph-0.6.1/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.png0000644000000000000000000000013214532447230027636 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/mimetypes/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.png0000644000175000001440000000303114532447230031063 0ustar00rncbcusers00000000000000PNG  IHDR szz pHYsodtEXtSoftwarewww.inkscape.org<IDATX[l\W}fL&/;ZˋP7q?ܓlqܳ+_hJo*xsy7<|`UU</WDo*:1qTTTp5TUϾ`ڶ^?>==rƣS=|CizߔR"H,;;H"",Zm^)uxuO]cY>L R M|2U@k@ uT v0 7`a%YIH6o'jzfcS$箐YY_ZP)Yz@X3zzH}=@9J~j'5~! uZJBRF3gpEw=NGFF.b5?K 0.G8F."^RLN*i ocv6Lg{854JM=-/o~#Iɀ(qnBτc G:<}HtbsB\z[6!Ŗ^_%>=Nf)C}Cv>jNQ>G:JYřSض_;tqn (K(mX:tGeQQQFy_ۧ=44t8S@Z)u3F,pK"qyl|wu:d2?99Pv֭l "I{, -Ҫ5#I$樬P[WK+7^5c;8A;ZkFk1AhRmfy9B {MNz}OpXV5~ `xU5+1Rגd_g _>@W*<1Ecx/d2,,ڒciffeڤ>={9hiPm;{ǎ;_^^R) .NzJ2E>@߾‹_䷿{ڇh vԬ*؄\<,m-0 +4PU?pÍYYeԧ^…dsyxՓTx<~(ب|I4Mv?0P00 `@0']m9]h`&6Q͢F;}@.|4 p2 0 1p aȍaj|MJzK.NzIIENDB`qpwgraph-0.6.1/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.xml0000644000000000000000000000013214532447230021463 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/mimetypes/org.rncbc.qpwgraph.xml0000644000175000001440000000046014532447230022713 0ustar00rncbcusers00000000000000 qpwgraph patchbay qpwgraph-0.6.1/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.svg0000644000000000000000000000013214532447230027651 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/mimetypes/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.svg0000644000175000001440000003270214532447230031105 0ustar00rncbcusers00000000000000 qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_node.h0000644000000000000000000000013214532447230016227 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_node.h0000644000175000001440000000767514532447230017476 0ustar00rncbcusers00000000000000// qpwgraph_node.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_node_h #define __qpwgraph_node_h #include "qpwgraph_port.h" #include #include // Forward decls. class QStyleOptionGraphicsItem; //---------------------------------------------------------------------------- // qpwgraph_node -- Node graphics item. class qpwgraph_node : public qpwgraph_item { public: // Constructor. qpwgraph_node(uint id, const QString& name, Mode mode, uint type = 0); // Destructor.. ~qpwgraph_node(); // Graphics item type. enum { Type = QGraphicsItem::UserType + 1 }; int type() const { return Type; } // Accessors. uint nodeId() const; void setNodeName(const QString& name); const QString& nodeName() const; void setNodeMode(Mode mode); Mode nodeMode() const; void setNodeType(uint type); uint nodeType() const; void setNodeIcon(const QIcon& icon); const QIcon& nodeIcon() const; void setNodeLabel(const QString& label); const QString& nodeLabel() const; QString nodeNameLabel() const; void setNodeTitle(const QString& title); const QString& nodeTitle() const; // Port-list methods. qpwgraph_port *addPort(uint id, const QString& name, Mode mode, int type = 0); qpwgraph_port *addInputPort(uint id, const QString& name, int type = 0); qpwgraph_port *addOutputPort(uint id, const QString& name, int type = 0); void removePort(qpwgraph_port *port); void removePorts(); // Port finder (by id/name, mode and type) qpwgraph_port *findPort(uint id, Mode mode, uint type = 0); qpwgraph_port *findPort(const QString& name, Mode mode, uint type = 0); // Port-list accessor. const QList& ports() const; // Reset port markings, destroy if unmarked. void resetPorts(); // Path/shape updater. void updatePath(); // Node hash key (by id). class NodeIdKey : public IdKey { public: // Constructors. NodeIdKey(uint id, Mode mode, uint type = 0) : IdKey(id, mode, type) {} NodeIdKey(qpwgraph_node *node) : IdKey(node->nodeId(), node->nodeMode(), node->nodeType()) {} }; typedef QMultiHash NodeIds; // Node hash key (by name). class NodeNameKey : public NameKey { public: // Constructors. NodeNameKey (const QString& name, Mode mode, uint type = 0) : NameKey(name, mode, type) {} NodeNameKey(qpwgraph_node *node) : NameKey(node->nodeName(), node->nodeMode(), node->nodeType()) {} }; typedef QMultiHash NodeNames; // Rectangular editor extents. QRectF editorRect() const; protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); QVariant itemChange(GraphicsItemChange change, const QVariant& value); private: // Instance variables. uint m_id; QString m_name; Mode m_mode; uint m_type; QIcon m_icon; QString m_label; QString m_title; QGraphicsPixmapItem *m_pixmap; QGraphicsTextItem *m_text; qpwgraph_port::PortIds m_port_ids; qpwgraph_port::PortNames m_port_names; QList m_ports; }; #endif // __qpwgraph_node_h // end of qpwgraph_node.h qpwgraph-0.6.1/src/PaxHeaders/images0000644000000000000000000000013214532447230014410 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.091364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/0000755000175000001440000000000014532447230015715 5ustar00rncbcusers00000000000000qpwgraph-0.6.1/src/images/PaxHeaders/itemPulse.png0000644000000000000000000000013214532447230017142 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/itemPulse.png0000644000175000001440000000263214532447230020375 0ustar00rncbcusers00000000000000PNG  IHDRw= pHYs  ~tEXtSoftwarewww.inkscape.org<'IDATHMLTWcfE"$Zc)Dn0)Ii* c.LFmHHB fA *O'#8μwAjw&w9OB*J$IF`q. af@"@Ŋ@$ٸX 0m8{x YIBBS!FPx |Y^^~ӧbl%t㲲> $sspn['N?l˒1y{^\~}jE3bϸ[􊊊I;S\1@Pa~HNNN'P(@&P}ؘ˲\{+B؉ş | 4=z722"Ξ=VU$hS7۷owFFF&9ccca@ ` :܎J RSSsjC--- 128ݛ@0UUU팍;rH7xn^ ߿CMӦ:;;III;` IqZaaa۷D"#_@lg@$333gΝk֬-e6b10Ll6K `2eY$!,Kqeff]ףs\ Tuiv2Err>/j*3`5r!mbb^$LVU]XXM69Eu=*Giʶm2zzzudb  6m ~70jᰶyӮ]ޔeY|4%"8λ@+^'zdzrAQեLaxϝ;vCv].0==.**A`؋PQQǎ@u?r;55=|U!PgPEa#-u'@meeaԩS.`ZZIvE)))͍O\.lmkkuuuO~>bIO lwQZRR/rСCoQ,QϷ{^n`^ r4HdO߁)!Dذ3$8F)fGZ)$SeI6"!2KJRW{dZNgҞIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/viewZoomReset.png0000644000000000000000000000013214532447230020015 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/viewZoomReset.png0000644000175000001440000000250414532447230021246 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8]LSguҌVBEe3-D&:űyAb23fwKf!fK%jJ;&XBʂZ>WKB?hOw0['9y.# "B0 I (|b۶mյwޝ=rl6w{{{3p?~X lE=u+0224$Av܅ ?:11qk2vÇ뺻q8 =4::_XXH fX]]x<=cccgk`=ٳgff^^||TUg@ pzǎ777=@EfggT*Cyll޼)CVoZVE" M-*H0??$IKh4VTzj0ER)J$NWLj"#CBd2\ N˙ENcƍ,JO|(F.d2\N-LOO3111:0=h GTV(%!AXY'N <{M$CkVsv6޵ˢ8yd-`^x$~oy<=/bU4@YYY!NΝ+25T*z֭'NA4} jjjN744>n޼imSt +ZlXZT*h,AV )㉒C>̳X,o<|;ƟfggAadXq㷪|ՅJf֭Bnn.(JbIGGGX,vqA.bZ EGgg'Rkk3 ۷oh,j5@`nq 6߫B2Hru"0A_^{q8y;66L&ZQQv"ñIT{֭[R 8p̽K555IN\b hZv˪M:t(.‡@RjʎOOѺҵc=Obdd$Օ>uTPQQݕv8^`~8 xqӦM\ѣGmذ&bkT߯( Z sR~♙{HlNNNY[[Q84`Mss󻕕wckkWg-[Iab.,,`Pu:ϯZ8>>AϺUU5ꛚ^ BZb% 4M`xxXK$/NQdYfrrRKRNCUURTj4'& Hii-I\AAA*z}Ĭ,"{LMM+V[}}}m^~Eٳg߿J[gg;zŘU;z{ѨP__B$AE[cO[Nؽ{+^pXX,dD_{;30 ].lGQnww1d4ωq̙ĉ'];w&'ZgwHjlvR(ի?dbvvFko|d,SW^߾}n%_EѓMsvojWX)ȲLoo/GlO 1A4E!s wڎ @H`nRmqEe~t:YUUrrY "?R$Iq' 2cٍ7~|-_PIvYx0gWLt0nf|,Sk4bx 8nAXhM⌚ ۃǽjn`OZwYǂㆵFtF'~|~f-Z&yhe@ wmXT,|T*L)edjt/Vf خT*ևZkv? 8 <3"""ђ*NrVKbh{9{e{ڋpw}}1 Hoτ " `J9Zee 1F8 p 9PFvށ1vT8wڵSֶ(+H$7nI$dee̐˗/r~1HSvBe]]]444آ(B(NLL]qzzjoo:;;|>- >}Zz677w| 24͊bJN b1:vvvx<fGGGj@ EQ?Y;p8E) 0  ܺu .4M k?%˽cÇ/u]Z4\t黥nEBqBt]G6Ž{6#E(}}}_׻\.AeܼytBKollU|t:ŵ5 ڪQUZu8vuuIccc:={fo޽X;ԁ6;;<55E!b&p8U$I Tb:njj1M,p8Vҷ`Ν;b1f&@)c̖fNa۫n߾n<}tD$Z[[kRRNQMuu٪EߐBDQdbHJ6Mr^Ma,=oB @&wؚO\./(?#fo1K(]6 ce1`ȑ'!EGZA D IENDB`qpwgraph-0.6.1/src/images/PaxHeaders/fileOpen.png0000644000000000000000000000013214532447230016734 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/fileOpen.png0000644000175000001440000000141214532447230020162 0ustar00rncbcusers00000000000000PNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8Քka?w4z*[k8Y:;. b)Tҡ!CV-E:̐C!%*E=0g~v{}~⫀YX@-׀׀#Ph W4gu7nw0-~nmmҀEZPdttp{2D8(J< UUl5{ 9cuN |aUY \] 1y@6wP؋TIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/viewZoomTool.png0000644000000000000000000000013214532447230017650 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/viewZoomTool.png0000644000175000001440000000237014532447230021102 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<uIDAT8]LSg紕2,6G9 ?XHf6[rH425:xټX9,&d-!,Q"0c"MX O "QLAhiLG%{7{>x}7a`*^4h1P]QQqԩSu+,..yLj ӯ7EQ<~…oΜtiiI`6)((@:::^\oWQ`y=j׾?typpή >UUc$mo;v$II;wez#||gN_v5]SS3| g{%tCCCLųej CnWv(r1` P{ngΝ#G5`P~ĉ:UUr5@PU;q`X,l5R UUUP( x(DRVtcǎ9FqڊE`cAh4A_sH`@ҵ`T<GEzȫݩrrfZ@l6u2ӿ&(l{ss<8 L&55j2Y _3 $+LddCfH'D,nZm{+++3LvkH&ZdLR[__EccѣQ͛_ȂvYjkk=l0Į.V[F"hwn||U#ܝrdܙ@[VWWrxI(Jru|KdlYZK@x/rSmT!vQ%᜘ VϪ4Sv˙: 47m ^'?j$AVvWѺV1?DMKojo|W'$m~wIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/qpwgraph.svg0000644000000000000000000000013214532447230017037 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/qpwgraph.svg0000644000175000001440000002407014532447230020272 0ustar00rncbcusers00000000000000 qpwgraph-0.6.1/src/images/PaxHeaders/editUndo.png0000644000000000000000000000013214532447230016746 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/editUndo.png0000644000175000001440000000126114532447230020176 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<.IDAT8?hSA?y/jQC B J*v!BonB7AJij:8,BLJK0V?w𮱄Tk~w~sp"%155<.X le}i20g棩J|GgiM]!ذ@8rll8# D"+++dK@Wv\zxk333K_u,P(DV.kkkkKu~,y 9#{4M.3T*n ۶Bi۶4 #\ PhmKujx<4˲RKXx{;=+ =)r߁y,e_=`b ;, o6?ħL& |T ;N/DQwvZ}Fͷ:]+iV^8 |Vz~ J}I.U x"2>(wBމ&o(kIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/viewCenter.png0000644000000000000000000000013214532447230017306 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/viewCenter.png0000644000175000001440000000234714532447230020544 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<dIDAT8_Lg|+t$ۂMBb!Ӵ,hVNӫaZb̔ M14fbt"&`@ii +_u}18Nޜs''<9:l:@@Hz dXzNj6M"ٚU@ :Q fzѸTQO@,;>oϬfy$_Kwޝrh4 0 hAl%I>}ONl~,,7D"###${ ɀ-_cnj2j۶m+EEEM`&p@v%fX<@`&9(xW};zsӉ'N{b2 0ogLp8Fv,^T8wɹy@ `0$ "0z\ce(X,M--W'&!`LuuuYkn;8Dp ܜs?9&fٚd @X絀$ X3 VMp+++_ߵkW<~82<<vطl_wҥEQN>}ٳo X'P4M[p8[ZZrm6Q[[yyࡔj9ƩΝ;r:kfUsVVV]۷7[$kY7333 p{u}JJ(r\jOOO8SBu]466{4M!D46{t]*X .BjДOUU50 Z/q#>>P(mkk^Ll@{ݏqmǏqBq-/@ywwP(l1g'''~S`Ѐ{t-N{{r^́@ p8Bf%FWt)!evr6-Z#sssnO`&CȳAϒw8fffTٯ+rp>)(()Rw%%%%.;;;d2( .I~޽>>>>.@< 3("8;%ιINr#<B|BH~7 2X` }\ pv!fR\FUU|}HG0094 }SӴQapٳ=pϷx|uuU.//p8,;;; ]j*366KGGYYPP *++kjjfn޼8TU֭[dTU= X wuuM\~Dfչ>oۉX,Fnn.+++wvvyQ! ୊oln{9IKz^v- v( G}Ǧ&>r\{6Gg _YYY?~<TRՊGMf^j5uttd2ZT~ͦTU֍V+}fSR*RRcj1& N"WDD(J̤ `~~TrGcM(F'2D‹sGxxx "D;k-vq]wzR(8 `kkk2xzN^n9jT*'yE<0 ֲtZQq~~6B}1RhZ#"z="|cL^;k|?x<_־F>kཕԽ{o>NVVVt+b1ccc㱵9ֆp8HӶgϞIۯJ%iB^vwwUEQH$R8)I,޺u@ r><t:+W(JejJɲnG0rT0( ID]]AogggyMtxxH.ˉj5\|v5OKKk||7H a^tIov(>33ӚjfggwD8Xny.3ͽ=\__7&%i$[[[Ǐ-E"stt4|5'2(Dr(KT|>&=x@鑑r4M},?@@(NNN_mW` (L0  TnoibbJH rljS$"S CuFغL =kZHd) <"`AY39"8©ߪiNp$Ҵ6!uڰSGRdRgr]q]]]l.ɟߋ,TP/<|[jV\ս`/׾eIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/itemAlsamidi.png0000644000000000000000000000013214532447230017575 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/itemAlsamidi.png0000644000175000001440000000177714532447230021041 0ustar00rncbcusers00000000000000PNG  IHDRw= pHYs  ~tEXtSoftwarewww.inkscape.org<IDATHK\W?pO1ETB7",ƅ.Ņ 7 MwN NȄ,2qqƾ_2)mE7{ι~o7#hghU2Nէ)+VЛ`(8JB)#@/@8@U_sʾ|63࢔;;;%|yt:޺u10|2tZ& r1LFc=27R<11qsqƅkip |jjJTA*rw/^ŋMTVZv"yxRJµaVcii{7I)5qlի*RJiBM5dցՙycccF.# C/Ѫj<|puu1 !h]ہk5>>dzzkh*<}jgPOJW*/{{{uK۷o_>L,\$ b)e-_j$+q8Slul6ѡY) 0Ԋ"[[[Gl8񓓓mympBe -z &S@Zf6~ 7qtv 5Ew~3777RV7 TIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/itemEdit.png0000644000000000000000000000013214532447230016737 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/itemEdit.png0000644000175000001440000000137314532447230020173 0ustar00rncbcusers00000000000000PNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8MKQOb?[hwmҮ/ku/ܸRR*dwj6҅h%(mv3Xk e2I.Qc <ϙ9DFF)4E@ S6C~2huOچfgg^wx "u_vt|cu||,KKK9 Hp5ذ,˒L&#2??zccQW}>HOOOKT4M 7vM7Uh4:*Jd2NNN8::boo/ "odZrl6+aH"Uy<uu~^] ۨQ: }:p8j 0TXjh\e6ukhhhСC9,/,--}b,2^W޽{8 (,^hp۩ )FQl,K'pxΝ#===2 shT!8n>-o;gې(e߾?|+뿯-욭[nNE';?eNR^^~ft/57 Z`N;zxSph-殍7h+OkٳgƻqQ[uXb`^3|*|T)V*F'544ׁTݻπ@/(>QŇ_|yVU  ܕ01L }"Ij:م>WUZVqA,7n ONNJf9ҔS䗗@߯H4u7-dZOb2̬ͺx;~Go9z/;"_ouYYY.uRIIɗc$8V9G9D?zlQUNg?KKK37mz011~'yN-(ꛚƂt:e5vvv}Sv+8VXXRBK"1 %%ǎBN;qĻ4,///WR Ioҵ X\rl۶Wz^W;ƀzߥHbHҕ u#۷or* b0]z[YY-˲8ya!$|vz 3##HE=` $IRڮ ---@ A^M:,  QVVhϞ=ju}oݡ wh\DRW\)AYbŚ AxkNLQ vLm@ٿz*0pʈJXf͓*C7N2XBD4Õ5PUJ+Wyr.@wG]ȴo9f1eSYoxUp,ؘZww/orFjz1۶VIanܷ@iqJB]#7y BluUsE΄)5@ݧ͍>NO/mk5ZM/QbdF)H˅ۦU_nrpxHx;7f}.ȉeDhxq4/pe?)7.In9Ǥp` =S3l+"ZKdXZ^W A^)~+IENDB`qpwgraph-0.6.1/src/images/PaxHeaders/itemPatchbay.png0000644000000000000000000000013214532447230017605 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/itemPatchbay.png0000644000175000001440000000346314532447230021043 0ustar00rncbcusers00000000000000PNG  IHDR szz pHYsodtEXtSoftwarewww.inkscape.org<IDATXŗ]l3c80`(Pƀ !qVTEU_[Ҕ䡉@Q*R*RV) &PZ;`X۰{o>X^#3sι9ϢVVVc9ql7xn/JbjVkUIx?~_8~/6b"-77`p'>뺠xn3~w}+w#ah _NviFf6G֭1n޼wW;Ӷ1_&)c;D"FUUv7CCd7/.z?ĹNxO518Їr+WT~-~?lYVLGZ uRI=FqKrw4@rqUt TMc+;֩( A3Su<4DZ1&vFU!xUv.Ķ3i: X@pHEw? e $ Et\a d|lctx`իgx[[۟e{tI?[89 x4'0)P3dRy6+پm@Aw3g~u2BH)qD"իW SNP,Z%Zٳg㔕ǑR.ԬmB_L[CCøM;vPQ /~yxH#]+2yZ[[RYǞ!^jj)u]INA4EKhztl& H$%+iѵN\Tʿ[;p`a9ú(+_E=<었\jgb|7?}-'d5Uxb%WݰTTUEx4}..woD.^}2tJ<%Dڄ"jSn.{74'^! B4UEB EA49@P EE,7}#u u]/S)4/AIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/qpwgraph_screenshot-1.png0000644000000000000000000000013214532447230021417 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.091364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/qpwgraph_screenshot-1.png0000644000175000001440000041120614532447230022653 0ustar00rncbcusers00000000000000PNG  IHDR5 eXIfII* {JFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?y2 V"mg jx G]Vg!A!bib8E^k= #)E/;@㶉Ugґ#АӹIW~=>WRE,W[z};@n:SR~=ϋPr&3 –PpHDA98@9;@Q[-ZBTsm&6N_*feeI9;@no-g)YIЅ}iE F<_ݰfVAG2o@˜dU8K)e?41e'i;LDceQ n-`Y {k{f";L yo-S$3ApXE2޸V~=r`v*d8yhMc֭ =Cn O+_v+E,LW]ߥ=?R OQz?@xO1`FX8`k<)_H j?gMJ~Ev q3*[=imh=_ EQQ{dP"Z[a HIFۏ?\luf[d\a19[Gw$*j4K!oEO,=?CE EPEPEPEPLS})7Ҁ/ҟLR)QEQEQEQEjU^kmեGkƌ1"ʁ[޺kw>XlUmIDGKW<<֪ӵ_y{~/Z l.*KI,@xϗ":9 űɽmm3ar?V7zԆ8|$zjǪ\[;(1 ( iRv'hi#'n]<(nGly S8ٓR6}>Np# 斱'1xc0(((Kd@_OE~((()-gk[B\;ZH۸?6AkVҵ *b `k^k-iU5˻v{]EkT%` 7BY~76Mu-dukS{+B=Fĝ*Y2:5*a=4O(x>qXgE|8hNu!D~f0=v&68ր+O'˺X!|d+H2Zb5Ū~9.{VTfq)VRbrx&ש//m PmBˎ@p^i&I zIĉ)X֔4̿.Bu + h'lvVɴyXhYb&=:Vg(kpGZ ѓH+<O.dN`Id铊ǯGBlbcn NZwRWk=(b2OB 6Ϳc,~,UDQGx2BԊѳ+gw/]io7W ǸkcVXG.7 SߣDntRU X'o<$v9^ ZsDL=dT_vtb8!X? У5&)jsAk1[z6qe$ܲnp9 [U3\">j lFVG4lgq"Ie$Z\*; f \\ ?@M+媫u-ȅ诂*Op{`qS+xd؃ 1@jU:~2??Dx`[~g'lrvS4 c]HaS|մWk S$DN(Xʖ?]=?3W!$Bm}~#>;&mʌށER*_Zۥ".NI^WQ𮓪,_iVhW[}J~c- A:OWPxrG;#? EƆUU?AV{k2͜WjVG)10C SM=e E+qF'^m"x~ly-rXKSF.oĭ E[S})_>U2NJѢZD,¨$t[$uN(3Xx36Z?'{+@a l`R~X>%:3ښ|G2qYԇ:2O+]CKj,ryfU݈Ǩ]I(q΄լH HBҾi"t n oT8+s7OyGY\H+{v\U! }1ϵƶD RƮC0a;TF0=bhi" +y<}h{b'ǑfiVۜWPl w'jDWk|G%8p??Z݃ `@*(aG=Os袘¼7otZ UǴzJ*`GMaɒϊM\>%׮t]>h#]Q$߻Ϲx#ڎ{{kGH=*f;EB{X('a9yԩBg,Վ*1oGca_3RX۹cF(oijgӤvf?77Gz=D)c~_:2T_]v =M09u-ev& :Mmȝ-s^Ewcqm9FݼgV:OFOFa-#'*͒w]rs[NJǵqHJn*J̩EIZJ楅Zu6"B>S})7Ҙď\|tR2IA$!/ҝ@W?|uoMxh^ǁTuY巵 cV Ukx]}?Կ`/zT|wqXxmmVn1\Skr N??zR?]t_l6~P@$*|7As TW* t4F);F21xo?zԆ |:xDɎ>鮥#Fȇrm#8jY$,bb"E;4ڼZ=+߼u8+ؑqCj*VuM)\(#cmeeT8^Igm\iZϕTǾwFxJ+b4Z炵^) ~a'WaCLxՅX:ډˊAE ϑk&MsҽvvvJF8*8I#j${)tJ3ԖBDgGp ''4Q 4yfJL<眤^V⴬Ĥ 3 ,*;?}=),hT*DQԞI(F8U5|MooN*ub^(DL7+dW)R[$%ԡ탅<뛕. S?'8 mo`fH@$(w'RjIjȓɻ{P%"U#(=7ҟLS}*/ҟLR)UMFPA`*3/7+Z},2v,N{*eW"t?\TfD[sN6ǹ{Zik)F"aB8;)It⤉i?'ygYqdM^޴.b A gG+ ynX(`tG&-D_*Q jrAS(e9dKLc$[X?4W)il8%Tt D# ley'XhVBf#N?:U,,@<'(N : A3d}*7| ~7r,V㑏sB%wXԳ* \I &VIxw)nd'ya_ʁnYrʹO4vqma8t'-4c/4ҭEne9є<?Sя8rϰ⤶ys#tjDo RBatSJU lzj(tmHB'ӂEU__f @]:wZUֶ7ɬdYk] յ]r*N;n/So_>S(G` $;k^>YE֥ii䯖mO~6~uѡb%|??Ʀ4'ty ō[.Yz222y#һJ0=)ww<[RO#6 R^ +P"P9;ZX*6w)FzIum/QL_y/Ÿ,K4eV CL/iZ- TK!9_Ki7i2m+'g5b*SM'8&|Vj-s5Whȼttw;2MPwxÚmrnHnWywSLVG$4#-(%boY;Ek Z83un~hb3*)f8dne7&vJl b ^z>uN==MHM#ܜsZI7>?JkF_:/ }gW U]Wg+cVcҼ8Y0æA?JJ)6Q((ܯn6옩cfJOsVp튢/UM7RQFMFn_M/K"TJ}QEI!8 ED&17GVϵHB -Fmf,c]J*/<KEEx}ߩ2}J@ 0) ̙dUf\qN*Fz ZB)s3,v,QMcjgBlcjܰ=ָ/V^#ngQpá#K-E^[y ыII 0Wdt8Uӭf?#e1]?_Μ%Rᷞ$&> cXso-e8cHo|)Ib0ed^Eg*P亴]}CINMg]3##o5NkmRbX}ʏG5n 9U3p5uXϠ>o*PF`?`|֥d_#RD?l/ /r}?-hxI{/>!WvAϮjƗoQUn^ȯ5c}ƥ4\]B\RK8NO+ּTy5VcR~c>e_:_MTTU{RKXR)DH`1dvG']y|@h^l4y|*'l4vG']y|@ij'l48?>Oʠd͓'-)|Ӓ9HOl4y|*'l4vG']y|@hxTwNHKZl`~F.?'GڥCܜqH dYp_\Mledh82GZIFrm@^(!3 IDATxYpg; @.*mUp;18D̼̋:a{2Q*WjU[R-(Q b߷/Y @B~ \ޛ7o.'|R6u@ <3ki6bbeY"\T*JԢ$q% ݻd>Xq( ժ c&4MJmS()@Oist]'Js=L\w;7C\Fu^u8Dz, ,xk155Ż {G\ϏeYܾ}4f 1(bJw]vMSS fffo~y뭷0M._۷1UU 躎iyz-*iRT0M7xJj{{aYccc JZ{:Ü?DHRD"Ο? j5qVkXSNq-9~8.]btt7|!>S7o2==ZXX`ll >@$_@o @ @Ipu)W"p0TUH>|u9rup,w]Ν;T*RBQ_@y9(8Ȳ8⇲,}}}aIJ,$I"`6i p4 oH q]{ k޽64Möm`p]A{M H$ؿ?`6e*:@0׭[fAt]t:ou]TUwK0@ >ZR4ݻ8p`C?8Ίѵkmq1 >G,[w=۶ș|VbR_UZ|wݿ@ >}@ ϒu]@@ gU &@ P @4 _n+ZS.)J444凶*iT*ȲL*"~f'8J%<DH(_"I&[~eYr92,L&b[v6s\XE2kzggg1 D"ACCx zJ8- аe3qj 4442.}J @4 ʡC _R099 gzz/J(<"P xQjp277GP 0<>,bQ]ƞ={T*h ua$I"q9dYfqqqp|l6K24M"bV+ -m\.r˲ N322+hhq(b1LĶm{SymFp{]EngnnW_}4f޽˲0MÏ믿NTΝ;S.}^*(7xN\&P.)˨}f˩/E*N*b000:---D"jEono۶Qw/OO4G?ĉJ%6'&&"癞frrB@kk+XZ0W^ŋ>|ίw*Jş_۶mpnnge>ن2>Jei-k]E#9r˲bݻ8LOO3>>{ yAZ(j+q岿$I~n[,cll;wں)!;660Mnx)fgg8Ãdd2IXDUU^to#ogے$M;N< @uQai:̡!m۶c>`6 òLzzzhoo;wҲ."?X,qG㬿B!ǏS(VA*r0V<|zpo>O(!J%0i6lۦS*} :SLMMQ.9tgΜu]FFFv,sAz-Ξ=0t|>"..^ȹs8|cGUU SSS199I[[oݻw9y$? 4I$,,,099ɷm^uN>M__$Jqĉ7xcǎ9rȦ"a]/s/MB:ƍ?O?Eu[oŎ;d2ܻw\ɓ'4~{n~_388;s$lvSϜս%,SըVܻw,bjjj1$+[oE:f||s~8pwyGry^|E?CmFkk+?O%Rq coZEdLs{طoNgg'bVFGGY\\_'? gΜ˜8qUUy:{I\zǏH__^mFTumYJXElFu\=200ѣGٳg?سgfffo}[9sk׮qQ(OMMM~twws}bӼ+WbΝ/ErK455 "Ba~h}w~7j[[*.(靈, MHj5?29p8L<DznBO$麎$IB`]"w%8kh4JV0 t]'N^M4s E!(}"ƍn M}6|1ڵ9]/us' N<,ˌpN88 /D6{9o?~܏̞>}Mصk׮]l߾۷o:{Z* TbH?rMĊ+/.xvSTR)^jޗzi*oSSSu)j,..Fq]׏VXŎ;8~>sNN<ǐH$YR%سg`\.GKKrCLuʕ+,..{nN:E?ccc8q^9{,Ν^FGGsα~FFF6l۞T*tuu.={&:;;yWܻw_|L&imm套^brr@ 7Mzzz|Z*ɲL:}SϜ ĉLOOϟ_B4Zo?'e /fѣ|dll6oʼn'\r~,sNbئYW^err̢(?jvqq|B@KK gΜ֭[rIN:'N矧cǎ=d;^ߣPehh\.p8̮]6|lk٢Kw#Ic6xuŋ+r9vӧgtt'Npq8}4=t]'8j5e3g(===;vP(,aa,|Y/T*1<<<0K0d׮]tuuȲL0dffߩ-,,<2+}? aY)Vi233/H$3>>_(]Et]' wM,,,9RїKݼp`0wkw>N~,b>qmsfu>/ѱb{MMM\t>$I~Jw#455 I|~vEOOPMw˲L\뺴fz?Tò,GZ~e8&R,7_8C8!L0::ʹsHGGoƲL^~R\~_W~tH$qL{{;.\ʕ+d2Gfڲ,n޼IOOH,\~p]B\zqg߾},,,w^y};]-<8[o??@{{;B>ZF2Ν;+m6'ƍ;֋iZX,r#ʡC(J~38u]gddĿGt_2/_UUٻw/{ƍr9"/7oO>Au=JVc[z$T(\=eY 8qb vpgE>'{^{`l4ebb޽{سgL۷oѱz=. /3^,w/xnu/b(IMjDQ?Y|.ԡ!lf݄a_VUdY&> )/ji/r|z݇jt6AL#Ihm9lۦ];9{z6潯( ewo^@Ӵs!I 7_K0V "`0tyK/yn$5>΋j5.\@oo/.\l߾}KafN8 j/ Ϸia?AUU? + iW탷# 8.o&;v#`^vލj5,SpxzG";jտ}^a8ސ_۶Oio$>;wR;eٿ ިbMO$Fc5C^M\Y΅ 8r/_֭[ɓ'}So;A8KeBo^WY=cgwxmzFLOO388H.{9? qe8+GFp8{gҳ62334)˾ |0 j~n}^_?+C[9T*ዖ]]][f`}S.x|^WЧ~J,%pf?===9r䉖?wn ܻwE&''H$|5m\yyWY1bK_<e/F}r9) t:emq(l7|;wX,,?|2###޽m~2GRQR>dM}P oHI* AEC0 nܸgϞۑ7ʋ155ə3g3駟{w'Zk.qvxxctt3g|jmrԍGO>g``]Q3gΈ@@@ ޣ>6TY&3iBd @ c?a p9"dJ@:F$qL&C6%ci/RH$7W^!R*p]EQ8r`x#2 R Ip~J s-"B?={P*,~/r=t]T*1;;ihFoo/@_ٳV' !2hZF&,,,jt.:::&܌$ bbf&&&444 0Mx\UUH5X[Lܹ͛j"{n9Ј^I8pȲ=ӧOjs}zַFwL_+_lB333~以YnݺݻZ{͛7ib6 @ @DWbΝl۶mEtI.<}r]J[Ocv] B0u0V<~$xx\F&zr8Lp-HWea䇇InF0C dWq% pGp_^s //AE!(,/U,I\(he;aUE[%NťT'DU=LF~}۶1- ݲ0dC"U]JsF>O P x;3o7p]-E0Pyhka0q$m/ DJvr]ܥUs忮.v]ltLZ~mՉӶ1l{۶~uAw]t,I•$$YFeJDQ*U%(DU 2I$tbDQJ`YhTbSģ'hEk)]^-l'wÉ ^!R]+/o4i/;w+Y!Dz@ @fg@r. 7Qq̹9f*ׯ#G" iR}IUq FU%  Dg -_[`Xmu☪)THeZmSs͚b. NgY*QN/ZtFT,'eYQFxYh,eG7C^|%V|V'Z]űmL~.?êyRAPuvRq\IuSUbz_{I P x(Q<բ$e{$Wff7]ZyKs)1:;9Sm/su۞\f QκrXjK36ei85]uuŧ]^RH[\NJ MM:DX60jo'M6hq~TppC@@ ?/Ǭa_ '-]e/@Uqlhy6? XY*}W|z_5]]V X^YMjƵ)~,(R>R4xhLcK3E׻.S P REI}V8gGt#|8_w3G}^Hz B _pZA=jpVy zzm苲olޔ ((|jLVy l?7z6ElFWRfn6,(|~.WSE8gn,~ې= @UUiiiP(䗐6,(|N0 ժ/}QsnTQ4Mgn<}}}477u:E`#68p3vb;w|(lX P Jh\ƍ,,,(U Bر]vDn2nmfbbd2I24͇7#ZF f_N*BӴ4'8eq ²,?eyj'z0۶m[1SnŲ, 0 fffr/״[nٳg㏉b癟G$4}}}:tf,>`ΝظCZETRMm(p'% EA$۶YבHEBJd2,;v@$2sssl߾B@Vc׮]meYfffh4iT\‰> ۶$E 1mmm466200@$ɓ졵>v~;;,!2\O>|>?߿_q߿ρd2j7P4M?rz_@@KI˲(T*jaFer̶mrHa /yטarrn8rWݿwl۶|>ݻw{.CeտW[--ˢRCNTtۮ'FET"b`!ۇim,j5ژ'L}v._4444 DQo(]]]PGٲgG. R'j6Jr?kB$ahhL&W҂aLNN"2~WVT2W\0 >whmmLOOcǎG:z8aBD]o7'o:& L&,'O X/w7EQ??( ?p])uǏ./Hass3px͢ҟW (|s@\!RݣBi~z:DrK/(,,,p߿ɓ')˴yOZaCCok?q.,xByJbu +Rln%XRJ6$VooieǺg9H5 @/~[m8OƱ+6Rʂ묓Gm9(R+ $W@@@+eTdp8ϝzT\Ope`}fTa-md$%2,K^R(tZƇ?Ky@..a9ڸD%ZD(j#$;<9;GFPp eL@ VX3((2MƸ9N.^_@MkốHw?@@`K@^ZN볰 W*co"!{3mFOL%FIGz]zQ';Pke˜%B*bcf6m$I"hئmr2ms"~9sP'wwhPZC|P g) :n]QQo{βM}O |6@Iꕂ"h^wjG-ŻEnMZt:Ky0b3\p02 -ifOly)OVқETz=4w$aD(A{iޝ"RT0MZ[FۥcQ"LD,ȒLZKj3 D~>)}a,, eX}!`-66e8`fW@`ε)=\~]A%$XދLխRlhSqtG'IIvM wRB}>Fb866CH|bR*u?!N[T 5rrÑU)1Jv#g Ҥ51d DE%i @͘>ƈ1BڈZ d˜@CtMr& Bl'W]2FL K>t]d,I5ɰ1c\i ɡ9^ jvm̵ȨD NkKvsmf>XËyKKia/2õaR-9rn A h rI&蠃%hS Ti \%a|uFX, oq~! ;!oD'VG7r2 xs'GBai;#?/ 1 E\\䪼fΠ=KBddE yNt۬(Av `jp,.H4ԏI@ @ X]vqMBКoy:Eq\W"c;'˴ޖ&3;xl<myBnm( Dp]}B\<6߮F꣄ޢ*@h4J4E4_mFevB tH)ZoY}n/ {*m !>z)B&!S{ mWQoW@X, kj3^$3em("lW @ `]Ed2ʦ|*LZVmJ˲===d2"C8ҭ)Ŏi~WQ/[mn}D (f^X,"I]]]EU6@_<]j[8R/RV/|U%VfVؚ'O8A"  (P @-0 E4Mj>{}$MMMܹs+ /I.cpp])JA_uww <癚Z??P(NIUU)J8שj޽V"?_͝;whiiarrSN166F0Dehoo痿%G^#>vŶmt]Gu?P"z6լ 5[]r#N  A 4y5t]G$;$Fl0 fa6P( W4`GX B Oyu]*tUUo~éSD"ΒhjjZ7.pyd2LMMN)Jݻj"}vܹC>q?bDss3m322ݻ|2݌su$߿y8Ty뱹G,"_ۭpzzYݖ$FR#\v??y^usyѣG.###9|ߧ)Br&2P@ r%vIOOXlS x4 cA\B +hhhѣ(RG!u$;wٳ(BZG8ʲL\СCa,~º$I _-$:a055eYsa^|EwBw}gRոt333;vRDP ˡiyW7[k )4M#JQ.Q]e=2 II&bybJFKK }~f4?g/ڹof 8oOPχ,oxK.CuuD"`Пf-.^{9l& / 8 o>8*gra.^ȞpP IDAT={d2yx(>}ժ߀ ^TT?`C$ ?Q/|xXq;Bt/b >(ѶUMӨjT*C Igg'DwwCx uzN>뺼oݻ]1ɶm{[oԽ~!g H$';͔V̾/+Yjk} }Z P^bPH-J}Ke0bяJD8 ?<֛ud*™3g6݀zTJ@@@`d#q[VqqQXz !!#UiiDgO}8V$_XX7MbwX, "pkث'j 5I=1#)m8+u%EGvk~{ NI9RBkE7Z .@@@#{+]1f>!ACCF3x->L 0& u1%1R`Pr-4IC4vYZ #Z{9{Y^$]$Mbkir/va <3]}'ca?pLōZh)$VbWJb-\'ʊȈ<|[JA gKK%fM/:y*O8HPl6Ib4/[3I..63 vhӮ3s4{Iw폓$ճE}1g tLlaS k;dX TqϾ[/_-%PJJ#nfö- ۚ>lPQ ??Lww7y,Bشԣ}'->' $Xb';IBGYv\iGwđnL칖(%PJJju#i](,//8m V95dyUUuT*E.bH:NҘ>>M]j Y/h g * M2*j\@1r丕(%%TUŲ,۱,kSA֑c5fxkbu˲HӤRdv5yuL&C{{;Ga||l~lz6O01KOO\.\K(%%f0<*z=Y]kQ{5M{hy-6Zbvx!y4M#N}vǹ#u3&/ox?[o+{ /@))F7 m} ÂNQx:t1V/TU4q]7xl[=W,l؋m?) RRR8ʕ+ vy%Y|p#EQ9sg}65z< 0q=F6zxV+%PJJ*1~Et/.6pi:;;XZZb``EQ^ۋi+ޫ\.d8s177G__ߊ+ tttގa u(ˤī" ~͛]QNH}[g<1R$&^ (\z۶..\V*p]i4O=r5ٱc\4ioog||>Ǐ7Je,//y7_U\s=eY_%TUZ244DR\.s\/~ uḮZض8w,-J=~p71>nVX\;(!PJc Ðt]P(066Fww7b+W.i3*4}6=cccPV, P(0==9x رNN:իWy'? ioPTbTU"gΜ!^z%~dؽ{7cccۛn ;UUMQQzT ME1帕(%4͆36333ܹBiܼy 1 &m [ofϞ=LMMqz{{m۶N&!R.9|0oSHLMMىo&<3!(;v,Y5RA`Yֺbjp!x8k;>_%B` cF!!Ps-,.. uQnHIcmۜ9s^2Q.W,]rGq~L4ȵ0 ̋/P3ȣW rwyZ&W纮͞={fI}K R7"4MZڒ ߟٸ=hpr r`147idG7Ϝ9qʕ+\zE,Zs- RR0Du:::V]|k)(xKΤĆ{6b|:6ku ( mݴK6mh'c5{kB{BZZFZ=yB<ֶ"BAZ]HPH2s(J8C{{ VC$SSSҜ%%%PJ y-B/Os7?q{68IjP($LS }gff7o.lu(%֭[[۷1xWIRɟ β}koo399Ν;O~ݻ\.ىy߿RC8x SSSdYػw/oƶmv9<|MǞ85YAWJI,.%uA͏1ն_mSrK@*Qvdvzt 40B<)=(QS؟ۏBԔB)h Xb Oc&ϵ=hvT?ّAIϳg)SOotcJ2)1x0 nݺdݽ\UU) N.V%O?Va,ui~_!oS?3~m)J>W^jdӧOs%~'}4'w5y@))){P"{5#Tb2ۭh YC󌚡q6cz)-'<: Zx3K)(1latp˽~a9o cc4z;>P1ϱ,jme`|4 =˔J%JfZ^^0di;ǏT*rQvEX_*msȑWTW|u],"388HRapp4ǎK 뺎aLMMqɤ60j6$JI}պ > !?qC12QT\wS N֙fu z6T_dp^P 0g%ם=/|*a. CC؁M@T ~}|`0.  Wl6ZcVFm4B_|q9?~!w^mۖ\spر;o|~;vXcfcVJG5x~+ y((xx-@7.?szF 09܎גܣ8-Si& kX߹sgc8tZ&K2ZDl}VA;-%PJJcb&zMӒ<[BnIW뎵(xǵk{\OT*aI @ =ZM8RT P$ K`vbJ|AE2Q sDM D[SJJbil۶~ ƻ/ZMWyea4Qh(顷 eBJekk\e.r1[FykY.=V]6q?bwFQ umX?)) RRwW>ĉLNNR.:j1n >Cs#쳞:N{{;\N#߹s|ٝMZM~} Z٣\1Mc6bΔ7ti..2bpm=TTfQ3\]X S1uj}}K/q;^qڴ6ZoC1y-q^+ƥ70:u\[JJ5Mò,ۓ8x"a[!p+%R),LfEYهA..f?̰9LHvzЧ!$2n]1]љffyI/? Rj _g"3iX;Xȁ B j!YO{}ߗSJ@eEMd2wp}0[kxFYUU 0 L\RWd uLSNEaQj$Q +L{,ˌj8V4 Rws]ӧO{أn?^R{9t]]]d2 [7{\=<aNp7<F0A`6aRՒ䦵G64MLXQE@JJcA.R ˲p]7eײZ fon[ |[E0 H躞Koqx<q4f3FZM4tvv&@)HK=S.mTU%Sհ,b7_0T*E*=/_͛Os ~w^^x|痿% sss={_~(,//5>LsaV8x[eխLMMAWW=R,|X#Fx8y5@)H.qinݺEgg'GerrUU9tЖ#kW^eii^xׯi={zέ[0M, .NY^^ڵkPxۿ[>tԚ{<[Ra8C\&$>- xL p###\vv,B1 mmmyƨVݻt:M^g~~a\Pڜ඙:΂\.N[}ϐ(%bݻYXX`޽y0$Ʃ !f|_T*a6b\.|˲`~~$˨GeffL&8e1::ʶmVd5>̥-eu,7Ci,--q%0D4IRIP`67 ~{{dY]'|+}טW_ʕ+ttt*.\w;.]Buڸq7K8Qqm;BRscߨ*.\ɓ,--@WW+<y3o6/j<E={ȑ#;wnFFFeZ{]~R3{m&ˡiTU e%qݬ0 4My*SSSi4MT*>o}jܸq/g?'|_xq\.f7 |->VU5Yvoΰm) RRX>LWW===+kq4/1 EQHR.\f|uߧG!NNIR a6ܹbȫ뺸?e߾}<3e_nznJ@)'Hݝ@I+4m! k5Qq> blA<,2J+xQɍ84Mi07qLˆ(TUvA>^咺w4(JWWB:::;ofhooO~>ws'm儢yB%OJk| ;3Å}סx[uG]Sr4GL&eYI9ܸx2fe``UUW_l{fFQ] =nvH@) RRO 64 IDATUK=>0Mf}(4! Aao,>0du?;gVOŤoB0y:0 }I5,}1hL>tyc>( )EdUmŧy9B6*ORWS.Sx%Fdt.gG0]Ehk~捔a RpkA=2Bj5ùCa08tiޫTXn.Ŕ+"&i,xRF^):n=m'y iҡzӾucO&Ul%GS0$TZ4LE!i4>g^G4Ul~ CLUn]K%Fijy: ҠR!m9@~?Z4Co-2@t6ġ|um PcCC|[ t$JI=D@܌ i#ԅ0@= :xB` 4({v,,ϣ=ʪ*˾h¼s,f<d)]G@/k8BPu&4˾OQ swHuu>uRF(7KQ0q@( Aښ "* /-qoF4u\K:݆irq՘v!VU ץMMuN2jYV1M&4v⥱G+ UTJk5,=; =\IVc.ڿklp r>Fi2 r&7 w](\`IJv]H6ˌuA286$ RT!yt' 1Mʾϡls*VӲ}Fu<MQ4>m.}EeQ  K(6i=^@Sg2.0}r0dYB`* 2B0D4Ŕn$KXJJP>cKKw, !uu ~ȮUk$fgo1$;خo}3@w9ȫ*6Йٺ]=eth*`Y*?6c]= X4,Ea5MA@5X}&]SUpvFmaȀeqqfXQl+ yMC2ƒX*vԃ[M%ze1d5 H`9W1hl,n. ` Kó,T@DBǗ0KIIRcܺſ1wTچ,10p 5lDZ[x(-}6-J˜Nbm'`%U*܊5"p]}nxފq}Gr PCTwc4x}?ymOO'~<:Y.V(ݵ 9ưQAc]J)} _xPC>qk5AMj2yl!Ȩ* i &u^)9_t6˄Ю(&i_Q4g].cYfe( i7~wQzn4=1TԎʅ wy(F~tѣ-+K (%2]ZgX!!h,pÐ2tG{iaH51M:5s/J%-*L= lQMQTUQȩ*=NJUI*R)ҚF6 ԀkîTQUrh0nI/ 鏲}]47]\zd4 PurƩrb>08ePs㏩s;l;tH̀(_^e4TSoUԂo0i).BQ>V,.j"mvTIU:>oIAYSNW*9v@( nw32FR6m/`7n/P8vܑ#w]"{C2O>RI<EXX[2(4r hď"]Q̘'bN*)Ea&z",.,0<6/aF׷o7|.9q-(ɰeOxDq((N4\"9`"3EHkѸit"*tYѸVTi2 EaV`>Q `;6>m 9gq(6foC.>u2ہ(qq{& @)-f` V[SpR-8^gJejU}t|(CI*WgyU*LxEUe$!nU*bZ,Wl2y;;B}ŵk}RR 8} ]:Z߭Vlb?d(>2y>;,aGeetU0p`O&Cq<!VSٗN~Ƃ--5ץ4 h Cy(aެ ] )zs_ff TÐeozKbW8Ҭ(ݣ}4Ð ir+jX C:4 UQ85 S0Uss<{ ǎ]7rh.6zPJJԺ|z!nK<#  A\u]Nd>(RUnz_okmӮ똪SMKclCQ(j}Xp. #j=v^O{c4PUQ5MQF_yOx=?t* &lNiz=uGR)0d96>A|iץ]smm_c,>ǵGYkEaв<G^͘@6zcVJ_sikYY^G J>r˧O/5%E o(ʻM_) 8Q׆+M4Ue. =r+Ib}xrDX~=pyZMEi3MLEaeQ CR)&l r9n.>D@U,\!؛ɐv2-e^L#f,梱 Yg8jd0DD q{M n3sV@ußxM^7'zӌAuI6Zާ54;>'y9>9_EWaB+:2~ 4Çiko'AyEA7 _~}DMz6ddabYECRT^,]Qb>O-BEZT^b5ץ0I9S0`@A0SQPSz7 `>$J=MLLp\MQݨ>immmڵL&bٳay(!0onkZd={n,xVI.\0 HRP6rs{k,>ZOQ,ccc %ddwSW)I&qML4 M0Mt:SbO=gY\v-<*v3zp`67oq lFUU18{T ȁ<ynܸAgg'o6d``SN1<x)ٌޚ^qoZ]4 0 t]V42x mƱF4 #ԘZm&7ݷ^2z3;dY T MV@@;n1jF^qyN<$׮]cΝE AwJ™3g| Krs;pMvB}mnʕ+ڵzݻtje_O?2LMMD*biiZƯگݽesy{a[K}Vϔi>c̚qg_YY>F#c/`adY\MZ~O(&^ػm&z,#PJԚ<4BFGGԗ^z;wriF틚o*yo۶ ]}7|377 /O=Νömo|@&add>Ο?϶m۸}6رzJ댎rM9q:୷V1>.ښ;xj emp?s v/o%n<=O^(\EQ' rm\p!Ʉi===eP(W 0~\+z-wy^5*< XQ@ӴX]4-10NJA0 /4uN:ʼn't:˃dZ1._aOj ͌[F?($Z!PŸY /8/όϳg,ˢ箳݌k5و-Z=TU]隦%7?o>mY[ IiJ#ZT^m!5uzO o@4'[Ȫ[/RO$>fв,vyRz=VE?F-6SUUlߦ;$=|kTje2s#O=&lM'Mk4Jm>[)y:q1I?} @( πmMTEW[lpۓ xi{(xi/rؾ]kUq0}7ny-HpL6h58]>MbXWlGO͠h e4Tڨ"T*q)\ɓe.fggikk?1joۘIVḱJx"l޽R`sgE?jdÛnF ;5_ q<( 8Կo_c=!w,w߿%|MY])MYR ?,/159w/OTbvv]]]:tl&277GRanngttM8{,?@}(tvvǩS7 (B6e޽PT8x ]]]!X\\g|k_͛d2۩T*yz{{j+ #~/&&&yf~$y?T35g/n6x Ff--Zޮc ur'K4DJ`AP @ %l XYʙ y*ChTtZ*6P5TP}Uz ޛq]ݿ\k_F b!"H(#Q(+$A3!G8^EhOߌFH H6vW^{~*T76y:**;+fVֽY'5]슍l`,aVUpl6|';^oQ5l:::Bttt|]CYoaOT"LR*$< Phkk0FhCV#6b]%IB4 PuYXX@QFxC" 5٩GUo}u"t۶VTUBwK O [m c6`؄CK?] ti>BcΙHJ1g`-ZȭxYW,|lTDD Wsq)蕂UFh8^0@_SFdޱ֪PU k\>+Vz~3ޗ^jm33T/]"cb6?FQDDh*x3VZ"co۶j5Z[[#ضm-5>̹sҗO~ܹsDQxg8y$۷o7  rAZ[[r>|.կxx,o|3?[CCC曌FnD"Ν;הa n6a5/>|(p x+fʎLW ]=7 ECp쳠Val\ǥE.bTl"xSrpw\ 8Jq T=wrNա+Y=A#F"vf ;@BJ4*)a>?!~=o-c5d*0Oy<|9OUU?΁hmm 5<,R,nH1ub#80Dh4d%qndIǂJWEQAZngfݏn+ L>g$9BW[Z~/H=.?'ȝ8|,`49qI"*Imp\Q 0hQtǡUQ:elX>+5[ Nw^,˷5筊c[Dc;4hkk[wٳuˤ ۷q\s===k@(?~ͼ|d/>|A`}2F<\tb$΀{nJHRFQUuؗW*о}dyso_lY,UpEaѲu K Idtնq)]'(^G%vfLf_>8o ~?EVرce^C{宥s+[WJnνQU;Dž/>P@ @*bǎTրh3KEE! DD"C,. ,.֩+hY]oY}M,ke&Fj+\]kN 1~S cW)9 '\n } eٟ܏,7gL(B$݈\?>\ڠ7ب>/ةIHn΋soGmEZFF\ٚq%},4kwb]n#@-1 X'DɌ isD٦x, !!x^ar@QL_m BP-ܟpk=Ձqw$nE]a_P)9TS>ɾ}[ ^]QeNVVzK`烿'T`8LSOqVc(u@ѰgA\u9]>og~ըF  LISXj+qݫP{nŪͩ˱\ ˱]۱7mfo7:Cmnz:6]jUL 21tszlO6=ay Q*j\EP(XE|_d{@h{Xs ]!'eWBih]*[ҪvhN梉>j.RR"L2*"'kЦ5KDO@p: Zw X'I$H` xQ~7qʽ\u] vt?qaj1iP[).ڸ#"\ԿOH f+ Ǯª[oG`\ ǽ8nׁ:+fk莎n׼׬GfBJKQFIG]ɕ?}l5 Q@J,ʍ$HHy}4jUf֞eƞal&ehs:hHIr &۩%Rp sxg#9(,q-;o#$'u""z*i{ڦb-ApvjE#PQ-@+cQzDdME;GBEP_cǮ"%ܪvpu*rYzwSf _~uYXG.exuuJdw;g#d@H*% 9)#djj8V5kU.BVB;t* ;^_Iz0 p3q8  6K511gwyN61 "wI*J@ˡdq.;HH />.U}}x1.nTm1j^6Ϣ $O)kvF KsDͪ:BP Լ֮bl5☞u +g ?HĢ1ԀY]ЮpŽi趎i_}7m˶mQ+T y;OM;:H a1LBLRS$$1%")(d65@DUD(A)BJ &q/dC= *yȆژU渴|jʑ#.JM@^l,ɗ OD< ^To->p4 Xc:; XRPƽ#-yJ8,cMY5ײְ֎װs4Ġ~֫ЮgchXHQ }BGDĞgED9X÷d}%~uЗ_]ͯ[(H~IGTqms: DAP:`-Y1,"(91 "*"ճU@ h4@@ mtwwH$n:Dx&mP3kfhN,l-c&QKTP֨GKGq\v'۟$LPDW%H* @%&Lj*Q$Aju1P(0]浹 R/}>:||෉bb1B[9ǻ'$ ln`+Q&ӓZ+VvmP/y"ji-XCc@|٨_7hY#w.K=U0+@re;wl@H{\E+j|+ ݰldE'lt+qWqsTUZ-H1tb0Wc8les9Y(VU/#b8^["+EwNJeeX!!B!)DX7hM'=d[:?zG|m׈&Gb^Ix-q@ {%,qO1&b/m2v);+) 8Gz1umc"NY$y֛hҞYQzDU&6\h6XC>o409˟| IDgg'R Yh\myXcNܽ ֯ş|iu:]vqQjXu :-[ Bdhkk#7rj|-hnvͦfHIT 5XeGOΖN"޾9=JyZ\eSύSYydWM2ۂm (OIhƏn +[SO9-`-[%񂎵dy4DYDI gsj0h^*) X9 GwP;T\%8\4)UF2:8 1M/젶N@5=3۲e\[5,k ff(nVgw+$>oݗXQ~}U6%IBQ0xx/>| 󘦹2Y73`6sQjy'XE(mmm^}i[ `0@ mE: g,( @`0بns?:Jժ2]&_3Sqji;M2;;>xX4F,f3\\T tdZoWQ`|ujxy3)ʸKeW.>)҉4hHn*l5Kw㟍3_kCA,BEf'ՠdNX8L_i X"k+~ !$Ǻ4),]I H4Eoj`B܋Gu]RJ#xBJ/,ȮF#kVe;w]md |2b*;Tܩ[dbpfffؽ{w3EQ A`=p+KnVBNيYp75}N8y KENO1V#mP:hӗ@GY޸Atέٳ6]fZw@[kHvƽ,)P*.OslDt e ؙ Jր[хQa9>w|=5BUZE=ΟF:;=J"{]WIo[z6u mڬC߼! cn%4,~|W>q,b||;wb1J[<h\T?k|3빎D16O :H_͗ Pŵ+x{wPvcZ&UXa"K%F 5~H;ddBl{p8ܰ5'NrjO(v<ʿoHR +F@Ot,bd~BZyFɠNAM<:@ߡ>ҩ4Tj..5_UON?z쏈Fk\}Igxns8q 9cl?tu =00@kk+`Q">|u:RTu0T@ pK+֋/2>>9s Tafff0 U ݻIۇ$IT*,FGG49z(Pl6(v3g033Þ={H Xiw '!4)J񆕧f, V½n/۵)e `:ՋEJS,T:Idx)>9 P0cxw]FgGs ~gGA<oTh[+f)[)/yYa=̂@Ũ*hQ*y.H/Jsyүx><ۺ]8e@ @"hJ%4Mk,?nE1 d i"guu|>OOO I<V9r.]Į]oJ% ÌLOO{nERp*p#G /8tuu+( GL&åKezzYennYxG+1۶-/>  n5 5ƅ&V'+UPt:KO G p  ?ƹsKyL=?Gn" P0 ,VUs9wjBuv@ 8Rn@0OO[p`(хQF# lNB@ @<ox7uA}t3Kcn IDATsls}[$IB!\%J!" ~YBFXvuu!I+++100 0222$1==M"`jj'O244(./^$s% >|>wfnnrQzzzgfffMحNzsu]wM`ˍV*{o8ಪW> fjlog_bZPUEUXoFLd*?űc=9|#|uĒ1nMxuUίG+kT f 6~Z;["K%NTJ0==➫^w8û˼>:|t:oV/`0A["N=uK\0L&?l4Mjarr|>---"I]73m}i:( 1 dZF40<&p8,^:,˨iAX.{e X,h^0 BUUD"Z1l2 H)O܋qA>֙Q,)pbRII!}'IƓib,]ͽDZcJ{tˤPRq I\P;C!Zc$cIR@QYW\˯RX-h?@wO/g3??L{[uEw# A\9ʟ۷Np]ӛwJnZ^^fHD___cYfA nݩOP ^"X3IՏxA[Z,TM+8z mVmj]."D-^^˱)0aOJ.GaG|$~Z^yX$pu6߃:=ɑ#8=nwK[kpw\O'yr7|E?xP(!$M PJ*y#XWBk>mDAsJrtMƗY&e2Yd&?JqԪ5~:lKn۾IlZo"m^dja4-({؟ϟ OtL% CP5Y9ə3݌dGāONQS9- nSpv`mþܾ7:}sſoл"FDgR^Qs,"֒՚$WԽȂ D\"B@5]%,bm4A;4jJԈ'_NͬQ5\_`.7Ri0a:NVͪYmP\ZDX`8ben%s} ED k5˩Sf>*@ @h8SqaXh vy "ƼAߣ1,1e\D˶ cž]*r\*Z ^!Dxgp؀Pk"hh{njEͪԖjH1 c*!z^zB=~Ysm#|b>eKqe2 _yB@Z¶mCH N,CU<-K^S( F_VQ6ʴm džٙI/CK@ H̕x:u穮oL&o\[^|E񱮏ѻh4zT2hIeBgЗ-cw#XljhƔU0gL/ıV, X NA@.HQ 휆qtc@ fUܚXRBªYKH TEWsQTyl\کJFAPԬ9gb.H +o,Йdk?a|Ww{G@_nMt[geCi&3ވx+@a yyeޛy5bD\sFDE)Es5e A֪uٵՊ560eO>@X$d @<ϱg\qO4͗ E%ԠZ9?ٕ5=Pj/Pd,ļSX./S(X(BU Dh 0$%HƓx#:g+,;o}mnvvqU.a OѻXrHFF/f_|/[6*`L0cDrzl BtV6 Yא6.h/%8 TY;݇qb;_bm4RL&iZh[Jt/yX,F,+êd~Bs^]$'hw#&7׍o9YޞzW'^eP乞1@2!3m92u3'ءmӷd"[6u_=lM l,J̧ԁfeͶh:&?c? bR;YB;C] t.!I1$bl/OP^z e [. rZ.x +~`u䔗D"8AB4~ۏr8EVSϏʇR}Ɠkc&mq|4U˽GG=ζ6eCq(Ph8 -sLȕr̝dHM6+gZ?C/A4%סlygOF(<$}2m73lǦgDhmiЏ6yg^~]]!pP1l~ ޹b9S)䄌#2rB&Ę6puF8梉^) "vFIqEEP:H)Gmж0LiupRLB<7tH$4䄌1o*vA#=vJbW0lcCi]DQ/W{w>g=1 @t..1e̙$]bH#@jyH8(UQ3kXq˹ˬV9? )Ct:NuГLfRjL'yc ޝyD>7AwG7HZE1L);|#|ɴf6YVY&˓y7.*m Gi}7_ `P(eU1cty`{x;vv _  5CTK=ʑ# /|T2uY:Ñ#L/Os(uo|l[*/ e??un)=L s;V.uq\š<gE )#a7bgc7p-{Kc"ks]Y zNGn6.ڤ8Y@{N]:=*Ɣ9g"$Ġ3 A1*7U/2oLKbG˪JVf(U 5Fo`v`ǶB!ԀT+j\-+ﳒ_X)R6t Ň؝M{g;h 5' 2.px0˳J;M6%~FnL&9g򭧾+{,cPYX)r|ߧR`1PtiMQdImkk5?[IJf *A:@&! +U |\.\浩89}N:yY"0c4;3pt(!#ij]'H*08{{y,p(Mo#o\kku }x:(D$Y5.AXW\ʽœoF  i8ymj"NRp71Q.^nAXSpiPE..pjBE i 0ԋ$z)JIޜjf_ULiB%FWF6!;D:f(9 Hv'ޢJY5ޟ_+gq(rW1+΍_a ;M=~-J,)*Ek~\uڶMTX,޴Fftf?]}m}$I"j#~>O$9y$OZbYunm뫸suF""( >B:V"45dQK~Oos\OS) EFVx'éaxx,N<ߐvvxмg*?Et4rhZV:c 9wq}YIh۵/䛼:*;|'\g9c1ޘxO;/~D#N%ZV.jf2[$-k(eC*j|v@!˹Qyd28e:RB@Z4Ơ9[ ?ȁUw, D"b1Qf-u~FGwu_k ??.0 ^us wĭ7Y>ݿB)K<' ݳg,--.{f'zEQ0 cKԍmٸ\)dYFUU @'agHτLLDttSg8Ln QlfP3IW?C"GuZhaL&^/.' 'x6,AR]|["10`Ze[Re7&>iŮ{5M< }.B!n C#?Ϸk&a`YbrL X RT8u{nD-"i(^mYΟ?Ooo/===8$I,˜>}FaȲБ,cY'Od߾}Α#Ghkk#ɰL2$Ř`ddx<αcg||~q1>>N6mcH\|rLGGdYZ[[_eL\& 6Vpn@Ifj~sE=6KĻ|tGNyhTJd>7ry *-O +ڭ֨z̮βR^a4NĎH:Ძ{dД1}l6<Ì066ƹsxeGtwwa팏3::ݻT*_5}}}:t ( \|;vo~C:ȑ#>tuuq):˗dii,--oM<ȑ#/H&umhi ,ˍgz?/c(iw =H.vg_߿Hctq)JUNb%B=lb^R:]?_sjE 9-GV2f(1O 88xc MW2yl9tc[-廻K{`0OwYS4 |KEt!۶,//k.(Jil&8߿_LOO#a0??OZT*?ΟN4M{mS((J288oLI1l`0߿l68,,,011A"@e2 /"sss,--EGGh1,KKKڵ UUm1v\>/p:3{<6 Wxl[aF?#dȎ<}r,k*%f˳>B MӐ З#HId(e!kT[X)PYI`9|0SS߽zd6S.y7ٵk+++h4`}5˲FyilGQ(Z QQUrLZ% iZcg6( Oݺ&tqjٕDz๧[[[QbH,C4JRcq]< RVX,눢ظ׍@"oO>$]]]$Ac?t=pS77o~g??UQ>K,3[T.qp3g6 N lY$x` eT'..7V&\9Q2P,`(>ġ!mI A~3L퉷&l~fj/TH#ӽLRn h 묀u]il4l$_7g7>kbb9F[ڮѥͿ$IfۭoaX0vjo>zѫ['/܏~[yOm68s־{,cRI iAa IDAT :gYVCGɞ={ng.[躀:wSkbEѰ*o7tv3>=џ$ ͤo6caZ&I ,9zRD7dB%RK حz..Z%rS $+CGޙGU_yޫ}/]$x6fKKH !Bt3ӧcΙdLI&sҝ$! IhBll}_T{ux$˲ 6u$z {z\rR:tݻy: YoZMsb[۶244Dsa3vſlO˥"WRb}XU+j+wݝw^-SdÇ`(.hθ11 `d}LKq75a)(@㵰26FitMJ0]R'H*(@z0WWyt˙> ' P*ԏ2(BTr6|]]?[vSoW‚nm5GrkV34F#{@WN|>_~UpޟD!)'*Q`we#FΝDg"0:0IX/pK/!e $o&[0'&pBb~xp8\(svM]/ pYYEKYp:q_辫vPI dE/GX+Xh;Q*jݵ^XDjJE&3zd&IZIsv,=c=caD`U49((;r制o4 ;mNVыҰ$$GOm;Cl m/%%_m\g Ru)5[gB u}އe(9X2I͆b(nX(uDTXrq2BzdRQQEavm.%w^Hb!iu $2K0 :QWQH:UV+:1 NݎU8}8gWWx :k ~*,y(4ݘ^4ǮMST.fE ʝ;q?9FLKH/G<?ُpj[F|$Oٯ@)1b):|ӉB0 L¸DT*5e\yx?~ ͞@UUYp!Ÿ\)xm^,%K.}.3! b1$Q娢W + zXm&ZYx2`bGs5]QC`-ŵ<>lVV51nA^q;lx`08+HkiN ⽶[ynsbۯk;)1[ظKYl8SEWԣT-ʯ 0 àGSU0  R)HZQU .IbXU)')>E.0W} `XG",2{<|bXqKa>B͆dtr!E#I: &c*~R+VMؽ{7+W=goRW磖fa0e86L,x8cIA(&͉݉dh4!K8;~Ԅ& sp9\WzŠ':Bkn]EPv3Kzilr+fr\d2,O[vsq>7Pd+L &K UT\ ,QML&de:&^ےɰvF`p \w-E Nh8{Op݉ČkrbPhN=l/KZ( LfFKU]s ds~p:33 p˕Z͕+-uu8czG{e<>NCcEcU?>*_˹6^?^7ǧw%ot(5D8GGwθ ^G(jpR?r*ki* \'f؈n L\Wa`XQaj&.g%}y{kNi9tXl6[O Wp g'BDN2 j e?. 844C>166ƚ5k4 A:&HoOdz9 >q.~UUI&SCW Mא5X7C!" O!歿aY2Plcbe: TgUy}yKr$*G9}9{6E"nۓ3x֯_?JEi>.^wYڨ3^}#}h#(#8?aaIԙ򓐐`TxY܋?rD˅kٲJtCgV^o}US|EaV|վ4G|?_~'>8ȁr }}}vn\{RUh4b-h8@&aݺu+PZZJii)[n&j@/E) ׋ɨJoHxӴG (lUyTTSWv;;wQ24;%C4"m6"NZ"-2.jl5xKTanۓ,Ǔl,k?,47ʿ ^vt`wnָ|TWt86eGdi,( +I/eY9K(N&ld6Oqqqr+LX'#ǹ{ ֲvSQY!Nn?DC" gʫ^-ֆɉ<()`0#5k#"Hu%Ӥ5͹~<J/[ J[[n".TtE~+Vֆ륶6ʪI&+VL&s l"P!\YI+iUa85L[񨙾mR])S/eeJt:_O23Q,XMR dz"=;ʹBe(g#gb`|x'tI"@gg-@AyPhp]C5D\y:GNrQ"INmT2<7{e!+zZGi2&';#$f<U444 @]]Gt! XXXH(0 zw_|\7~6hrr 앒'I( ="mq"A캝z{=r}47sr.Ł;N,uUUXDIeRaNO2!3 HFW#žb >'v=?*:p﷿ϱclmMߠ>ӵEE_{?unyӝ]vg& ses|x\Ghr)tˆd,A8j^_2ʀBTK-!zMTd+ a`N{b7A_"}& h%#$ԈL|<7fQWWwAt) #4v!['E\~ LmLa֟$ :c1"LڌEN7)dUAMM=AvEҍ|$eJ1bN)1H:8Jh5ѷ`Eov9μ\AƠ=Ύtȱ.}>)3ʸ.7xnx>-khmmeÆ iuuuw}S6و+V\0JW]]=Fw&:ٖfSk.6vJ, ,bp<3Ύ|plMxDwA] `99ʻ%xt[C6P*zZGtX -XVRSIݜ>m`Hf12RPQ u& q-vP |w5 ݖP!~-i-MqǍhM@+${KIGVFr(p~\.\NnvU nCN;K,iZ-H)b3D+3A%)Aik)3dW.o{E3W=+KTg/>kHv:tNތdK.4c2bP(D0$ | 3P6rES@!Guv fjZL^eGrINyHFUq904)?ǽ :xyQ,~ I\\6T@!!wH~h5E$#v+?ƒ%,)\jbq9NUCE]hzh7Xx?GGQS*UBŮb|e+rvsέW`zD:+5ImEi Y ߽ŢYhagEwf=(wGiTދ ,w x<v{kbvcPbh^s y%/RSU 'ݎC$\.d (UFv2@l6N'ns&T#ҧ XˬaA@]1d[ =sVhq '8)A]"CDUN%0;RjD%ݒƻыg8KCKyb՛?`W.΢;)&]ydLϯ{x<@bX+,P-US*[z?GqR|?Dp/܋)F.F5lHӒ21 17rB|bQV]'%gGF`+Hy\K\ȃ2jڢ8;UE<Se`ОhgK3e8Qbb(nvK0e#,$^kGQUl6~D$`-~әeMa})@JMc;xϗ5wr}bMgv{A:i:QB͆jb̩k\Hi.RG)VQ['#Mp>>/C\m_e0683c!h|_ rp\TWqFFzf+8Ak '-I)ڢmJH 0!Hzg=E"j|5-\faۯxM:š,I5Ih+ڶ6ƦM?47BzcGucm)=JMyͬ|yjkdMHN!J"J;:-7c*_VbChi\7 nbuC <<=TKj+(C  QcR5Ȇ貎kASWQUAJIǧDfA, %XWyJR|+f&@Ar5z|]/3>e7$s ՌfLL)MqfJ lm;Ⓢ{{h#^ug+˩SrclpjJkxiz{߲m-{"Oyz<3Omc,:RKarg0}R8nlwr:xlS.Ez:OEfmDb\u7[xM|*ѴP!z-^kKFJJ&#dEAD Th4-{ج6,IrJ' SUVW+$-éS `= eeu^}>vub~ʅr>[Yܲ`yyrf 6>bIp N|g=)3:rlܦ G>.U _B{!AWcc9>F J*M@v ub"2U$VzR'/1+|g6FMuw3/ eΪPY^yU䚾CfB}-^NSWf .;mr8y-N%V^i IDATGdד:;@#c2Lfu\$ݚ6ߛs_B'4z\VcDRTϸ~aH@*D(QR*aլX +69txv鳔/(͓Zzm5wpbyI &C!taGJR6laI^w+: )-*S\#%KQUd$VՌ=C6l,Y7SSō6}"4 űbret݃yA:)f.tOiv5sm~: ,ӛ.rzqǧ WI&G>ttb$ ,> vԈڎ2tK͔ѰWۉ~2S'X5`-k8h #ctEV2smA1/Kʍ n%uNpJNKk]x\$J]˙g(.,o.K]WO]]XR\Q4'ҾA(/)͸v H:TUlLF|^_ -^nGTsJ⼌`1G~a`6I0+?u9Q c".`&W rN/_A* !Mـn'馏IAƇ$3Eg   @S5l - *k(# jXErId:2XfWKDԨjvCDiX.&b䑐&O4At '8^iG@`I:B(0a_>^>2[9׮n[` q?j-6#OuAap$LQ(ԗF$awnm{[oqs#@`V(k2gβ};Nַ?ϩ(G@:;x!™0[moPXKI-G͌geL!Ӗҍ5d%q<=gMA\s"$sDfxLNe@1C0GEMp48tfp/wlmL#3J~^lrjվjݬYw]ܫ'Kk[+Y}S6KqwPH1&}N4SMqO=yM0@=2h :s+c>7AxANʨmb>CqdeTLmq:}~|n߸^Tf9K J7y{E ?#K,._KbN_݋y93' kdlCE tAUUŔk幍m|fޟCxuϫ|,i\2/7GY6hk gҟٮT*^*tx9T'YݎrƪeSj3K?C]?C_x?WSS\w7|i%-5t'NLGy]ҍܱ fHF9{? |eWh55J4cZIl)jS6wq _AKhq^;|wpgѝ+w5hrԨگ"%C,lZK6y$0յ\MjedJpoK|p@Á(8K3j>ñX,X QzAh,o%K/9;r?j x닼eMaM9KsSM|Guw*lNk_+7n'tͪrv̌uu»o6~ʲJv{rS(QJ-5 c'@{FDWILJk KQ u:w]7D^6G):jTⷠ )8C28n @j貎"N6{"q(GQM 'Nn;Mi΍ ?'vܪ稪E4dYe..Gf JsS@.Z;Ux77},:?#Myn.(^Ϝ /}^꭪$G۽w;ޥF/E FtC'&۳wZߡ0]ʍ|y);y$$֔WǴOq-""+򀌭fYDT,4 .9j :芎8뜤Τp68Kn vTBKt0Z0Ms6B cJB%6}<ۃosos֨OSgNg)-)gJػwo.K6(Xߟ+]A`^EU rtZO2"Y; v Ϯy no7|T]eS9_(}C}c82x:>`|l˥IIqEG4m6>ýY] SQ^ӑ`<36E%bѢg–9E†2`t"N d:3hlوt2Oq՜nX2],2j;i5=FAa#G(_\>g+ohNaaYBuu5NsF(`*)v=<@POcQTCs+VtG?O!;yY~gȪ]S "Zxaދ<=͇r++|穹su/{{A*X#]G2&D(+(;G$]y7;C;w`GC6R =Tµ\׻}|u2hy9}6a-j@"!8(adh =Lc;7ـ77h]o05O*kJ8&( Tj@qdpݨJ&!dE"0gF}m6X M"@$,Kn;l`Ϭ?D"b1VI(yxUV•>ﺒ:ỼeIÚ5f3y?^{}݈++W_t0FT*6`q};==Zp+/~܋WXo-šbSw=peB7Ѱכ]G"G[<)S;~;vٍ">{-H! ¾}LQrޱF;rN]S/Db-hGh;IˢV +/vt`]`Vށ^*+=d^9 ̓Onݛnmd׮]u] !2pQ?O?ͯ~+0pB>0eeeȲӧ B߿}YA̙3,\ۍi믳h"0ap{_Ȥiq^ϒ9^䕽 2w, [l<)q?"csډHcq#OP9`$e#I2]z8? !&pkɭp 3y <>lsS͎^QkEԈJ5qg@ GmKD$'u#C,ZfZhEcAWtQ@t$$1Tk[ uLERUp8c[ 2m I(-2%"3璘*nf}Qe9W200띱j:-L)Skwg=HjO~EiӜJgȟ(ާK)Nd{vX\5S{vfEShna[6zzXY5OPU^G"a٦Q DdѣNTDXQIUDHMw9}0^=#% aÂ`PU$:b+'t5Ҏ:b]DA߂N4Ɣ5CorW] ye+]sE6{S< & $IZ A" X,TVVEg###|K_0(-5;)Yr%arMͅޅyP^2]V;_Y8o~?U9xf3e, 99}X-Vڮmad FO{/вUXK0(ٲ۽-[h0hnf*AƓ!DQ*u2Ԉ&J7A-WZimqc3FMXqY\( zJGPT$ha =cU@IJw+nѓ:U\3xdn_`bh캫ʛ-o2=S>uÁɶ+[:Pŋ a| O~hFׅ|l>f:||0/\+_\E~s7q ](VɊ =6ƪU%;U*:;) ~ @>E"VW R,htC34bc;ڸƺu=%%rh^pbI(Oq| @lJG"xn`J0jB9_]UGO8 Mi4vK#:N':~R@ z!tI,GL* +^n˻45_yުnϚ` xC^/pF=C<G$bH*k:rj8E 'u>Oy\ #9-`,.m~8mNT.缮rێo㖦[>YBIc_>vƨ[CAA%X&ÉK5_jMՔw^Ē1"J}S,$"26V0,ez "oCˑiF h.lf+u9@C፮7CCC(1?K)3(AX-,`89rWS OQSUC4pa;$:z_9s _u n8+-5U@p  O?U.6J|\ v9so#oŠ/`,x^l~|V'%\Et'IgҸv7G=qj!-ĪUʿ XpQf;͇]rM&VW͏|Ft:M@ as(R@"= \+\`@p[ ePaof/~ȗk<&-q Otơ7cL3=hԉMWBt( ;rL&w ҧYܸAٵeeׄc`kV:3럙$%&I EAUUbqΎ-?a $D5>2/?Dxu|3G|ZF.p6<_KE\ oz+@%^mS^pm>A*g3aƺz{(KX]{WK0$Ч==76p sy%/&oVᘲQup&`p8N|W{d\K\M갊$ڽG;`UxxQf" h1 4 Ef:;h(]f}i~s7ԇذ/6bDQ$kg>Kqq+H 0!C$SIJ$(qnDM7ً!S^Dh XP T1ZYQ e Xޕf꼋^ɶϧ\4Qvvr!_X88]v/(Q(=?{ߣwmvq嵟Fhmg[6FƨUl {/pI+^ggN <JCˍ#::'Oнpʆ"gus{e%V!K`-"Dl56Dk$Q=:u`hʰD ,aK&\$Ւ^#5h"***r% ߲ۖ)O|?湕Q^V>t) wqd'Ot4X__n;wu]'Nsl1=:)=RȌJA IPUeFOghvOy8ٸLD9tE 0'_$X`.+dᑛK>(Ԋ_g」*<ύ:Vw+tPKIL0a`8as۳gϜ=g=;{5cO08`#" I-V:pUNR+st{?j IDATO>|\LZ~S3; ۱vp0v QpmTUgESa;6~~? 9&"oZ)ʋzY2$mH4̶:k"Z p;t ^]kD 8I"oQ<~u#zUڑ IVE-VG츍6,עIFVZ={ᯯ g;~ƃ3}tKe7:ޠ+Y_|~dKkv_I(puռ˙yT)xWzюi ?9 \Q{-d0m tM6Vd/ڿ HuoNsss&{x ݁!ݻ^/ wol=\>:n3nѷ˗}A^*%)%Q_|JYw?[;RbИ}UQ\XL0{r,Z%iְESY7A߼zEDJ#S֡$JXq#{6awl 4?' "6n:% KR>'I㟫;6MDMFӄ `v~g|\>>:r#>]i,XtD;ulN-ln'7;+@I+ҜBT%sUPi5 ;w2:'SW`<'0;OӴ0M37amI[Wfp @ihd~kvfRL7if}{8EUUEӴLjA2g<Ofb 2eM24;w^Q%̒B߳}Q?C\Xj'v=EU.\ u8sx tpM׳{ߧAi`Y2(/j\LdL/a۱m М'>Iie^f ZJ0\8Z:ӽ/NOPsXil']Z *3?ѵ`qlsceX%BD,bo^~ooi\xI͔b~;Ɇ |c7(*,:-G6e‚Vwy߇iWrI}B7u zQeʡC ͼa:$IB -mX,Ʋehiika``jz{{3_n QPP[oi&vŒ%K8p6J$IiZeq|In&Z[['H)))add]1M~x<w>UW]Ecc#ORTT_oa^~edYf?~דH$())appOAA 0 $DP(eYȲ3>g7~^⥩ Aشh~77ߌ$^ z<6`br71|b'ó@`Q}ޣs9z** o^ &pCc?102R֘5TzPE  $'݁qˢ[rf=]GvXQ /bl ~b@RN8t50 vЦ0#g;uϭj/m45o>-ŷq%kIs~JknfgΡ>x<rrrp ?~x<1rdDQ@+{oʎQ% 3 L̶egjbɒ%$It].jkk/X9Cii)$ (ꫯW^8;w0 ~H$ظq#---۷7RUU /@}}=/\s5ɱcxYp!EEEX o[,B$9v> T H066Ɓhmmeɒ%d2Ioo/DHÇ, ?ϑ#G{g&b/{ٶ94dvAaqbDQe7-sel&ǗÞ=/Gw,,\Ȗ-sYGuݳ}?Ϻ:n,宥o޽;/g+TM8`L *"^1 #S0 "LU) eL ˱Ne?1cƓr$$yȫl=|si0( ~xEӴ)]t^/@`+Cml LƗ6|p!>6|>D숢u[x8}>mgppC3UјȦMqp S]9{:Cq zzX%wQRP2;/UbZrS%r/o:gkalovb:1Y޴SgLfA`Ao7&Vpt1~LΉ y(2uw!tLa?7]vӴ:We(5K/n6nkFa~y^fgҊ <?Nt9k/u9oDQdxxUV`={ dީdOɖXrI/ϸhѢ)K|k1e;!X9dLKE~4ã+}?Ds>w._U"Ejtź qt({`m? TD 2 *ii5{O|[X'GjJ֕buy;v,Oo;6cdXfl)IOOQLPz}ߓ}99OV8薎fkh>:hd M}~/\|H Y~a'e7Md :@g;,($XŬ^I('D8'O%s.y-Fmvڽ Ԧ7iC›ǩ֜Rgڣ턍0erE"jjٰx9r9,lAX]ކ ;سK/Mx3/󒖞V䬘=˱޳PJ섍x<@QVyŷԇvT#6u H4kx(zTaA XqF 2搉 ؎1A)Q-}=S'>2/w/%%szm!~KRv֬X]{|)3E<ô$[PTkrLA@9_F􈘣&J~L{,7ٷG` PA? ɿ$5Hp,Z֋֮tC.݂cR8nnUUvz&xAz~e< <:r9h3BA[EP o_.W'u4eLH {;ڰ*O:2VKNJZ&u9u(>K[~d- ga1f5h#Ē1b3mFAа-r`{] c&>Q A|h6VytdyD[qߒC>7CGg.ƅs.Nq.a:໠0;d$$JD[*v Rۆ`k |<"Vry<'coĐJ/֐+qiRm)\}o-l rd׸˷؇v\sO`Xpi,)(!$䰜i,oikPUAO#ؗ nb( (T.71T{uv!* %>'40 ApyRGRm:OD\wZbv p):\DNnN | 9R9r%%דK[sG_5M׸$ˢllMuՙ(#nZ[ȷY^W6l,EAƼYIX $93VQ]kYn^Oo kC) 3J68IGsSt 0 Rե)WC,rTq%N~KZ}n)KYp4x\Wpw\yHxyxI6s*zR&OJZU**x#.zF]jJp _ugt7 \O;nȹCksNq7lbF_%|MMC( DnCX_Ka 9 c&r5le\)(al-xkH~ɵpRO6lEXk9qz=j/n)TE.-+p)lBa<^O&"5,ۯڡZ/pؽk{[Ga)N+pbv?q* sr>Q ~@ޝ2eD!nƉ#Ӿ/ "[ʷgm~M`eiRG@aYdrZFh] godK !)վj+0!;GݢdD ZCN6bP6MfG_b2Nq4]rL@{&^+j䶋q})ZCuI$SA;!$~~_GsFO/M7阍+YY$&]hryBx$zxޓ=~|{ BTUS}|fgxhC|)%&*t+EX/DQ1ШR_xfN\{ ʼIb989 xIx ׳+ գBhԬ"읕-q(QK*ql p P.bZ&x%Y8S'$E"ڋƣzRqݽܲ6\K)s;Ǟ@tI ZS'jOb?sDg(/SSƹK! LOP ^O;=h^?gDj_!lg҉ GLlVtc3 1P*_G9D"95~x R})lzڪ;/p@q8<e>< 5uqu8JZb ީٺ&El| cƐr%^ )ХPr$MJX8 t6_B)RHMm61 IDATj݃ nZb]ETyHX/?|ܻ^jΙ7V|ʊ9qK5_ 3N&K qlF^W)92z/ct}h=fct=:z>j&u $lF?~"'[N.KDv(섍cֺ֦nN2]ˡ܅2ZS1M7n< N:搉>G1e 1 QqtA֑n6RRŤ>SKypOg nwAFDk$x뽤PUn5k*D"*]t qk<ƣgµȒ=ᡭQ*>esf+ѱ(I;϶mE*I=[Iߜp0 X/\M„OirӲlOoj"rss|zVI]c˯t/ck~|#ljC5^ףرåN˪rX:L&Ϲ Xerrr8viN%𗭈BNUp: 4/[rތ4rJKBx .H G~{,XFHOy;܁$Sҥa6q-9>=qNٺmcb$IBQt@Ţ:\JK^'#e̟,۶MX'Nخ1M~0y J"hmZ&)SA09qyv&lBʑGe՘㩿'odd;wL&؋ϟN L?c"MӦ T]*nFo:dk%a&Q* qRրq pr@X XA\Ve(; ryyyҚ/Q!~G+o'_R4C-dY({m6H>4N&ߛ\b6Ξ;=NzCCCfR(I ]קK@)%L&dNz |>ߌgM 76_ڥ"w(TĢE^l[kQP;|RZs9GO瓭6moy)JdJ1~ma;pR)Xr%K;t28f1bĸP-RmnG;&9IA: sDPƓe #!)SfmpwǷ=g>CeyE_x%|r'iZҔ_jm\I3tww322ؘc&h~:jjjxY~=|r^|E^3<͛)++#?< P^^Ξ={Xp!ofl޼@ e۶m,^H$ix<$yyy;{$ 0H&10;( xܺ^wZ7׀.圁l QzȲ@۴\$Jܱ( j)Ao/n"m}o\ Bg}`!y8vJq&IIa>\4mߒ$avF麎Ʉix<4MCUU ȜnO6ٛafJL$BNYYί>ݯdV,Y2!r>O zrB(|c5ƎZw]]SAΓ"fnSPX_9vxdP*t:qɨ6o?_5 .fǻ~L~h>8eFyWTUUeBy" , //-[LkKt8zγ1 xKf tL *^>|/Q[BI]2MΘ{졠:EL{eYFuTU4 `g, a{nEaXiKܲ 0O϶̽LzL&3V\7tTC=imXQI8 ,!qs~3dskg?,Z~Uײ~Yk9v;O⏱a|>b]eee$ |>䊞 :MMMAٵkV¶m<7|3ǎKgg'/ NcccFWpUWqq^/˖-`0i477 QU+V Ifg sfK8̓ 'ohy-)؎_% P@h'O~{s֥RJsKilbvhBi;v젢X,n'dΚ5k8p6l`llL9YeaQ$XdIk֬\!' <<'ee|#8CEEEXfϙue,3=KzMh~yiYt+G2p2,aKd2sM5؂փ[zq | UU~(++˸]7F;F(BUU*++1 Yx1ǎ;$??g}l<_V߳f{BNJ$D" GIII:Hgg'EQhooȑ#H$e zzzhooGbT۹^x)((@4rssٷoeer qA,X8AA2m322(ɦ=C} AjsSͼ%pz+Ŷ&lZv^|EnX~$[Mq#,)w~iϷ@0 hll$LieeeMEEE\wu׭[-[2ׯu|rdYqXj)3-28ȑ#:<`[]]Mnn7#${z3uG@II$ke6 Dx{(Zf#>τ+g,]M8ղ,ǎ1v >@1MQH臭M,=ZR4OgO'+ifNXlR <R^A\ y;|%"EgL_?,\F(:m𗽹NMM P(v.6P(4p2dHߦiNyM"|ڵk3 ۖgb+( S-{ﴮ>EQD"v~SNe%fr[aѢE,\pBkN ȴ4{>L>.6)ny3ÒG j;a}5c9y2RD8N-q)0LF_ŎٔӺΥ.Φ9]=A~DŽb!|I'zE;:x7"Φʎ›o _@ fMr><~3Ub8ɹ\t?)/TY3>÷^_ eeO_9o(};q>ʞWi"GxSȜg5T\eepLgHT^ŒKd 0{MmYDa-#'tU6P&xJfA* Nz Qtc+8(B6X s!}J\qp, cWmrd P(x9 .G

>zDxpᡇ(rILʍ +yq::xk_Ԋy|\fTWo~3`({44HndxqPU@ @Qak׬L5_dг)w:ag'Pb5ꚫ.Xo)Xu(M7,W3 RBT ݶ,zMAFL2UP*׶4*U"Xf:|$m~Xl7p:Rg ?_{51.)&k1syW܌$J|j֫ߢ$\BMq_ X7ޏW87nQ#D6`qv"D-$1j21&itR%|Y&"4b5c1}>*</R"L²IE,ӡi*XMߏhMmFI\i79"9QDreib=AҶ)Vq@ܦiĎ#߲qrҹ}`> UH6 #w$6}>ZS)A]*U%&H0; ?NxBMAG{3]i4M&JO%i찓tթ`0xґ&9/H҇þ}//'T^΋]]y_ND7L2{$0hY'6M._>! |퓅LuyxaH$L&MnObuH6J1<<&9bԲHՄm/$mӤAwr$o 4^,Z$Hiڄ)F>?pu=[8s|2FEQx<Jy/},Xpn<{Yv aa,}(ɜ9sM;*x8Q@ž#77#gޟ9f:&cLY(":"eɸ7%,^LӤ z(Byy):z)JJJ"LkƢEG>›oɾ}Gehmm駟fժUp R)qW_o~>| Ç{nn6D"f>?]aOWrM r{߯o TVQW-?} -j_rۊfbRα1l!` IDAT8xOP6xAX~Ӥ4&g%AfRq%* y6w#O,ct`RQMsm3bYnl mBDt(zL֙3/AI%DQ8L.P*W4Mqp mtad=D%I8-$6СiiM}>tm; wQ53*2]Dmą7Rv|]uOι)N}8qͱiHu 4}}f<KyҒF5~hz)1ixE"T*nl _&$2 7s~2N;- EQZy<Ѱ T*N+@$N$$YFTfB9U9}Z5,--~Zs,@/Xx&w={(-Ϥp|=\(_:FeTnFȃ2biR*񜇇 -2~-eP| Sr*PUZ_dc2!) sMa%%%߿ly zmQUU<l1$N>jձpBN8&&MDii),cHKK(?wTsAd"::4.ut./Š+P^x;RS)J/E'Y\;K0JB֪ؒ~F~)1 3;1 ASSHm"? u]}ݎio'_oaApC]w]2MrK<Ōχ=2Z7]ՃH;F x`~{ACP h5D>5TRyPD v;111C5G^e())ARx A4IC'jZZ~w!'&&fHЅ hXO[T{?ӤYөßqkѭ5zVLY6K/R=t|ٌJQEs{3;kw閺%#G@]8#>* 7P./5D-/t:I>4&HBؙiG@ <74Q<-zD߳!;e'hSh!;T8*x8aJbJ0 Xt*-*IE@7o~W _x=nH1b¡]!jހ/K( R+oy ]Oj'X`wڎǖL!N ޣh4%|oi±xr擘L1#H5]5%3oμ+Ox498@[w](h%6_X >JBҰX,3J?]Oax_bH޹`TV ù??aWK &^y5z ~`QLH@ko+S?,D?͖FjB*꿳A2(^-1V[EGWR^Di`gCC*e9HA-"I2HX €&Ne. LMba)]wA-=in'~ZLdpKndA>7L+\Fs׽#^"Eb7ʠfGv#L4hQiW#w9q(V~md_}kz)+GZ8ѮhZ#ɍe\8oX1&FLdrdmX,|拢_>Qҩ.NA ^<w`H0@'B=oNJ+Ȉ`1HVt5:_{yyc c㷾vD܏b&-gkVN!xʹ7^c1 Huuܚ|+.#yyf;@ Nlo1H_U(MӢii;oRk))dj!Ox ^ y>Ṏ$a5n"7U ŧ`5Q/- RUj* u?I4фʤ4ń4 !DTZv/bhM"SF4/x Z@4zMԩ=i puDލƦA]8pJmy|'r`Ŕvo׹JdD%F!ђ=3IINA:j zsC"ȟee7g}x<;\Rbl7>[=F"xf3pd4YТ=;y#Oqէ Զuy)*'`ab~| z Xk5܂an#yX{5b1E^++27𕌯0qJb}>oO,(^@\L\RWG?減Y:w)CQp\ axqtuqxpܸn|_ 44jGh#D|>\ YOOTgH_upJNakAFQG$TZX NR^^ ` "E? Hl톘ǧ<vҴRuc,.^|Iah@cb?j{/8 7JT*^l&22t&HWa6IJLb466200O kZ&NbMtttl4d4ݦwegCY1kŨW j*r\W%wׯGէbjT_}jg²eX;ʵhA;d]261d87EĤ Jm VV!EfafG/eE{RBF79$0RAo=$hNퟧ7YrDD8U[&9I CEG=@^Nj$E"l>15M5נI$! z i4./;Š+Ш5S|/lz4 / JU*z!LY.N 8z֞+>FtDGGr|7\|FF#pZO\I{|=ZFsZ))\_K_{Y.YJ?#O>EJrʨ]l k2xUUPvtu76bfLYJTT?B6Uo3%b 'g#ٚ,s\TU񮏉IJ8s19e9DO04jDX+.iȒ18Ć!cqE=iU*8E=+?AZV2lDEE ޮUG'F"BFz(ݕc I(ûB_mp R.[>S}nOBv@UBL">Ƞ{Eg,18@s+E @0X5xNzM"B@~z A^MgӮ?F_E @Gow@|a'+zk)8bE <(kDi9]m{KE^sc`նU<YvNG'w&NxU?䡾>Dr.'9!ad#)tհe[6s?K^"9)8S||N(ᏎZp:Hz:5 >\@Ef3`ZýaxxGs JhvxQUhxOzM"'+F(^X+ҪP  [Ɛc@vx= H}/[{@mQҨ0DO>hv{AZԛhx/ܳn!JkFb5UO]^f`;ȪX҂Q#h׸5VY &銓.gNccF4 sStR"FtVOeS%~;%ts3cz'Nnܻ9tAf0:4 ݿ@Cc41LE ynR'P3$Pet6{1N2_ۇڬSA*xZ=(Nmoi@Mo_BSiquKzuz#D4V CZY{ҋ:VŋӋ< #[Ѧ%cԂQucCa5[y!^2? : j*,_( Z>ЊMR^-W'q!Ap[m|q VUO\RK Þ?P@&]Cel>Ga|+(+2=Z#y RS\ MMn;̖-e2f.kcCU{SǸqCⓚOZLi2eA(i .GRm CjBG.B1 \@Xj+THM~/"F vA"QN-0f": P+j䮐hP;1Ɉ8]ErHceiO0>q<ԯ5<E74G!)! ^Gwx IDAT?36S&іAWwJʿG|\{M^Yٿ? F(|#l 07o.l}mǶ1#oQ($/|/Q5\MoX30Fg;X C 0=g:Jwc̕W'PAasfjkkY1^z38s珺~A ;w?C/]DDޝ>'a[y,11 A/Js:cWYg?2sQppgZWj8[QR.-LEB6mDww74hmXuخfqJcZ4HHHVEMzF_~AJT iidf27s.oy=4Uc=WمE!A(.GR$~<5) ZUuN}^&}T4V~<%/'dEƺ|QS SX<.ƾFv4`kVEOO N[+Ehp[0|'Oڞ׈gx/#%v߹$P/(A [̐It:ӃI(x$g49~3 DFFb v]:hΏ0Hk_c\8f /0.+^ 6;yο eWMup}o=o|' ?'5oJFZjeQ^Kco#*erdxhv4XO,SPV5zŦ&^x*)(i?wocZѴ!j#ZׂZFGGjs@(h4<iI 84.{DhH7*ȌdaB{GPj2+<9RSH/0. $EG!~n\==-:<yYytƾF^* ,ͻ\p\n?ښ8{,H]9ˈsq0k7͌,XIBl^᥊X eA}]񏑟Ūϖr5cVs5"LσBjz<gJCC&(|~G´iTuVFnpfoL}`Wc罶,=oj sϹb=v&\Hlkƻ$e 'Ll]=m˺uDyXɹDDD |MMlh#sXID"˖<*ùS808V{ϷʿEbBb8F? :e]Mg xa᫵Q儨b/HN#')>.g!oyǦ?F /0^K(d oz[>(ו3tE˲ zy[h:ʒdf\dָ5ZH]l8['JlL$8<s ^z/|UU F #0Hs߅%PSY͚Ckv ZO?ůgmRcVz2':y}.uX1uo|֦VS23>Iĉ|Z)1/vN{6(,)mmlLNѬGIKN(o_(w/Šf(n͛;qRWN #Lǂj8_T*.6@fS&&}f7Ɍ T6G6 B*A݅wM!ז;fK [v/ ~s_3{(9OY'hmOh<-Ip컰XEmW-돭d=sb?*-uc,K(lj'TSdga أίMQ8}N)ICՅ)Q?ɾBʽbezʞ"5%+)[Y z$5]5|R }]}ܚv+YDFF"=aS& Ns撟|}4^ᜇ)/ X=+V\q!Fx*~?3+gĠ5y?ϟe{v R ÁUoaO@>q@dAK1ȊL/`_>J <.bo-/r+ }?-IIJ O0.aF I:·?dɤ%آmlxS򉏊R+a6/:j{ a푵<8QhtmbE ҳ.w^Zvz x̾F#nֽ|r8 ڥRGK҂R kjְ.VL]ARbRx‡&aqBOX}5[Kd|ww1 !+2vrMSi>.~~ΞNb.=U+^G;oR镽|^9e˘8~ɜ(({ڰOk>ej>LFJZWk6ٱHY"""Ƭ[vL-(((|\1ӾAbBbx‡&aqC#jxa !2Dk"I46p^ 죪iIFzJ[QcnkjWt8ݬ޻]g>C-iT֊[ryQQǼy|[$U45z6\OFb w,uױ^1mE#ӄeFtD4LzG>{CMleo^Ua\dEhUÖ&}3.뢯6 _8ZɊLe[%?'RyIIJd'+2'O7orM`K}/?x}d<;Y:dgjǔye/ytO: &67lf<:0 @N\SSXs=Aoɏ ?#12$k8'(ܣZ'LNJcl wŔ)U:xTU`yYy\")V[,]{l6֫Ǭ]8adC,L&e{v"Ρwnu;-;'INLߵEF'L 6/~/66",Wv}Dx¸(|.,eT%QA`|x5`[~mQ^R~^$v8aߙV%}~죮55kh8-6!qTTUm(]GS}ߘ f3 y Ғ<0 #k>' H '!syk[|}í¸(bm.G>v]W[~Q73t9畽l'?+_t|%1졪O? xpރ *<ݶۭxbRSRXne[%l򧂂5]5yy^zi&aqmf޹> sgSUMEMg:\.)ĦGkz5ܝA:;/ffO}NϕFݣ)ϐ|ћKr~>M _2>O>lu6_ ߞm" >^zo<^86eoģywՓV5L #͚97ƞ7X>}9~AJT p0. n{TDC+jIHwN>ׯ[3ړXVd_E19o&9ٴV]w99&g/|f[E|lEJMt8:xqۋ<@+gY2&pՓ?YeyY/Ju*3 `a\=z6Wmf^<"t|~yvγcև5 A&_[@@i$5̝;N sGL+vV_;2[{^E*Wm9gEzjis[R$5m9,_v=-{gD(N,cgN?~JHOOȑ#zi&El SQQA[[vɄZLt{p n; =^$0L5z zCv7" ,"FF= k|s7զ_a؈$c]'920tSvS\\=-zO#$8|0+ilci'+2k G>ǧ~]6ﯨzT<ϐ6gg#ysICȟ ILb9 Y54436'M5QG{,VˠwȝqwR_|MP/`gNG[[)))TWWS[[KTTGeDQD0k֬&""EGG/&22χJv#"T [p!$VQ%Hi ,|8qNaaa>o aƨyl?Nrr2&)3Ki4|ȑ#L0!3& &>KʖUkylctOaINRhP088HUUNVKWWZX]]]DGGp8x7ygo~***I^^۷o'55|k׮%--ݻwcZJ}}}0)22^D5k֠( ɸ\.aB***(((`l߾N<ɦMլY>m vv_;3'~Θ<#AGO/|ٖBv;DDDn:JKKى(v%""F,ː:G(TbYѲ6(hAljS7[v:}'x(!e;f,)].6neMbݱz4]26O:qE?z^:.v㓚Op;]+Z:TP !Lsy8.Y_a`43w6Ƀ IDAT `llBb?'^O||< 0n8>sRRR(++?'!!Dcc#̟?JA;@Rx0LTUUѣGbppBjkk4773gmۆ`2}t֮]Knn.ׯg޼yttt`6bƍ0~xoɓ'1|駈HJJ ܌ 555H 9(=7* χrsJbjZOY7DI&RZλrMr$B1$Dn7դrJA`ٲec23g(2w\\. yf"##?~|zqҤI=o?KB&0:3:g}?< }.vT=pgP 8z{9r$ nd2ik֬U5l¡C*]8 TWW3vXTU%33_}vaΝTUUqN''J<^"n7,ƍ%999z-YYY466bXػw/vE۷3g:::QU(Mv{ݼ\2+6'^k_g5L)2Bz~N'x<bcc !СCuhߋ?Ͳr [HLLl6G?222-**: )))R['ÞØS;Z-Y&յLMgܨqH@ z{ sdPʠTGښ@ l64Mԗjs&&hIw $~<ߓNC" W$<brEOf2MNf"q2C_/xwD$!Ge۷MGg{vfGacsf>jx[P i_+Ѓ:B52g'%1d+A=nfMl#ⵋ>!)B{_ȇ,˴i 6l wޮX J\##6rjdY g@v߲EGii)v`0E&*5u:tMI"2aF>: :j>222qqq rq뭷bۣ[[m+7'MwtdOզWYk%ӇMew1|[̯wG$!ގDEAko' &kVB99-*܉&><|9.ݮyV z>L ;`b"pՊifh3hJOv¶=۸g=fۯ~ՃrkSQ|{**tQQLNd x2fn硩p8P4?o3cL9ߔS4:_m6v=)""xi$ȖLEEiKK8 &"7زmU2s#Ua***n n_#BNTU7Dez6j**$('=If|&yyd%fqcэML]Y2_HeΩqyAĴ7NE5 ;{ԛ@^7+ L"-[կp:ĢeqsͧM.t]gwnnZJE<\0=6L̵2thup;[X߸b4[nadq X㡺l |$4]=6\9hꅀk48tum%F!ՑʰaM\l.+TIX`*PjҨt>xfs]Q[.h .bD[[ mF{}=L,ʮ=D[$%u*6OKy9 ;~w}>l{HOǟ@LL V̜V'KJ\L9o۾^t+>$I§i(Y0 #Q$d"44-T ȑrKKөy&O *q]ul]jVA@l6DEAu\&"R?,B+2w.ii '&f O̲gز{ 1?3r vo˶m|[-|C{"Sb11{"xq}'OW9J6~z1d%TMk6q9>tד 6fd: bԅQ!mɎ)tOTsyUlTU%'Xd (hݎSפ O>3?bΆ[v{l #g޶ _8(< 2X b1[;z.O/9g_T})򯪪(*l9xT6E!4  $a; Huf2"* z`~A&.b;e2UUTD$QUt iE$1f2dub j6Q6FQU~YnFnIvJ*263&~~oqԆjQ:b]:^ %P,3=rrdP40a%%,YF>Ġ۩v6n`yrv-xNˡSUU&DQbG;aƀY035e*NQKXF\}G  A"3]*Um2aE<]dYTBXZYFEשmvyytQZ[ZIӢSUlS hN Mɳ۩1 :f3uNՊ$_ܽ{qgdDI`ũرv~;U?da~|XtԋOuWuMUiea@}8[e,d&&baχShVR:J&Ei f׋nQ aEd]'Qc2q0"jE"V+{ܒD,#w+=z5{ywۻؼ6n̾z߯=cc/>j75jo%&XXNzQYHv ևYyh%-5-z;VUΘjg5hlauj\e$%&@[ojWT4Uk%ϙ% 7mΘAc=b6}Jy-{\[%/3+v#@oV8UY"(^'C?-)99',+lGRU_T;uPYBJ| v$aL=ÈA(bhK$1&Ѓ:`+-'o )AE~_D3&͌ 1c<ӱF/.`:Θ ț6 5Mw}@@&sܼЉ⢵vHaZ!&w}H&[w;&ԱS]N5Tdl[wE4_g_X۰iCI3ufWsrXgNOԗt]ǯyr)oV`CiQP:-dD{j6Qo;}cc둭amZv⛖ovf7ClC6e%fQ[LnJ.i)i$'$wƣJ&>51vANU~D'3f Iz^+wN*}(¦Mr\''#lG;xZ=ߎU,4F2U@w,Z ;NILfB&7&Cc"5!XW,ĹLIc>}9EvS\\[ t-ƛ_hmCOm"Aa?!-I?wߩ6dU1܈A@h'RD`1>&/6H؃-6(! ""b'qS0f,ÿIMbVEp_},]ۋo'3!CS~;]ֶbǎT$)q>&СdKBmQ !tEGbͱX4I\tNz:-aͲDJ%##xl6)o[:6ł Ptӌ, ?дZ}b]:ުxݗ3w\6-5| |[?=yFٮQlHDJJ >p8U؇!~=<NӉV9_4o~+K/ae?3uba ujﴉNw"Ou/e:H. =$WYqE 1۫seʕ[ٿx^HJEHfwԗ@ZL|BkkrONN6piß(G_& ]_ƺ7:8Lf$ϖw:yt|Q~ȲLSGS&33e&CrNll,6-:yMV.+9x$j<>qb}#q~V(JE7@厖#m2HLLdРA$&&HBBB#oMKZ%MGbB1e02#TeFzz:8ۿgf~wb/HOEGg>b_ƾݓA~?^*Sx lnrJbz[L1\Sz 111 wh o~˝d1}:=:}?Yb|KdΡsAXqpR{)3gh/xs+cݍg@' OMKQTnw3ii}fnz^Z[[l@HDRRIIIQŦjscKᝊwŌ_`t|*\?~W, B= ГI'f=6)UnZ8wn>%K!zB`ka \5*tQmwݔߑ7̼1ikiÒMKHsk9g1~gh(A w5/0:+_ѧ}>M0*6J+TALLLgݲ;ۙ_*tI"tneQ2c*_vTJ.#u)wKmYC%?wvrۈ(+phjp88zKy>yN{ZdL56mW?cg5ƶxTdn\F>Yj1 /?}bv>ss7LN8_Gbw*P'LyJW>01k,q~;g66zHESP%09F]FRb%30 ٹw''ٶ_>/؝FH`[ o8*^Y O{/۲wPEf荰fU*>!7zQfζwVZO-/fpx0׏(;F#7Z[7|j׀A @xn\΍%7RpԚ>m*S+ⱉ@r=M{ńJKTwTfś8=NnyYYƖ)'}SNEEV^W⡲NYIt5?s:jjDUW;M7w>O58F0s@3.f0KtdmO o^ޮxO9N]xDŽ!挛/џ8)T|-7fȸqGe81MOMSojR^3=#!/'/`lڻ;d萡=ʲ[y{46pk8̪g}N?K-Dn]~TWr{1KfdEfŌ4)S.5_g_ec%om},9kG_KjJf?tڵN)X7dNJGR[y%bc[2A)z yڧr+7nB%%}-Xj7gL3^uF1p*ӟ_˨۝r+1Kf-?EZle^ζwh>̍Cnذw9loΊ+xtңc(MK`qD21+g\#7CZ˝w2hI0p*0%]Mxe+]p7#8j1Yoȍnϐv8#0d>/}'`4T?O/Y{ M k<{gZO-/}$-3F˽_7\9ʓ&h*6?JFD4xa RSi}3gn4/QoϢ^7&KV  `+Wxx,FhCP v#l{Rk)g1h >Cx^kqM5Ѥ ys՛<:QSO|6=ṨG*xmk\x-L?xkta^jqG,i ]ꋁ ~9cu϶7e= \8G*ǎdr+5M/H)aXh5~=CxhIMܭVoX%7:(1d'ܵMqa{#{FMG Ϭxg+dsfL]Йʼu 9l.]@0sUUeq~gAߍ+ƅOʧb̒'E31_գ6pk_$#(9ȟqbBj5@.QZxnsܜq3c1ȟ(&>9Gl2sgɝ<$$3"{Hh9ffgj7i& wS:z ɟκu,-_mYQ:Gߞ=,^2gWL1H{52c{`^"A^-b/c|xc[@u#ϓv y[?~Z9"#)h`cF>q8nr NꄁP!,F>@Jr ::_ƚFOB-k2e;{iC+y{ܑs E*AEjws{<2u!I˱$)q!IA/?w`30b yjSRJ"!&% xns%.ƨLp {XS/vAfl t:xyܚ}+9| pe?K*Qc#3=3: m]v;.'2;v젱<$lm سAf3999$%%X]a^rd X `A^+ǧ=N3[lg<:Q#3,B5ͬ8}G222 Osu/0D5bp{'}Oh5ϒf΄9=@+/"ϼy$ S 6nQ/Rumq*ݩ~ gCZZiii8,K6x)r,ݴ7ȟBNR ׽e`5[1zO=n|!8 @{j05a*kRm{*x.v9eѺEܔz w׺V?ǬY\>򨍋ξ}慨sLL̀lSUp8Lcc#(o)y @||<tF_w{`w~H|1 4JsJi6aŇ7hbЙ/|Kq`$*H:,@5ݏDZWF0I&S5͵?rW]L5!j^:w (MӢ ,(B[[MMM=Œt]q!t]G7LȲlFjkkٵk䠪*$!2$cZH8d2EIٌ(TTT0vXdYfժU$''JCC477chhh`P^^NNN$77XL& ---zOA@4$I"SUU(1j(bc{w'xx`Z ;tv;@ +ONVVJҨoѣGs!vW_baɒ%Kuu5SEEQhhh  zɡ7xۣl& i---x<:t(C aݺu$%%!IRTn ?~<ٳC~zZZZp:HDjj*W_}5455IZZ.Jss3Æ fi 0tPlBMM n;uW";[By'I  M|\ NVZ>HFb3ᑉDKT6廃q##7xcw 3of|xDQSS=ła :dhȢH3)w GwSx 70kx !l~-ƍCQ>3 ôK~~~ )BLL mmmh餣ILL `AX,|>|>`0UUݎdٌdd2E+oi@]EQaLLLD$<O^/$a6u=t:1X, ==P(DBBBt;{\_ukk+۷og̘1vJة3@.lcFc8#3d>-WE3w Jմ_s.24]5Y]D9iYӸVܱnC=N:ya ˘Ga^aoo^[W^ɬp8jsk1n%]W撙yT \?2NJJ:7&d߈X ngz!2ٽw12 @ʎΚmT0pVí#n5D`rrcev3 jFS-5[|.JRJxhC&v4Ծ u/2a yԒdl8{9&omz$1,}-XN4p'CUST`E -^Əeᐅ Jn7xj%KH qeٕX,T]/[B?.l_MrJEEQm}h7zKvQmmli?XruӈgVo=^ibcNV颟UUWh(L8ۈfKOk1,˸paZ0p 3 o/jDQdJͼ=n &qd˚'asf*j*jbk<7eDL\.A#YU>^Ʋq|K?m~-]o˒KtVȌʟ"5C"gp%K@׬Xty:]`]G4`$Q("I4:5\3d֬$dshw(74Z[1 ZuvFSڗ$I4wFo1U.(fl6#yu0DP dI2_ Cyͼ%LΟ(97ͧ[>Q^ʈɴ[\5[kcBʼ̑t/|}o}XMXmNz𨄍jO5.P/iO49^/ھ?U؈4WUI4qLUPu,&YUQ8 $ɄiojB \?c^@  2.&D,s$&fe2Ѭ(8M&BFDm8dbHE*AU`W @CGS>sh8uQ e9Urxg1ݼF?@o=3'yeczjųkAPRX AF#GHv4AEЁ"4L`av;UMQȲZiV\&fA`(D͆ڇ%JC KfOɄ42VQCj ) M$Z@$!H@0$ 'hvM 9dą<Ep{\VrkjpaV׬bbbD#=)n +3t~8Kk^;2xHO*g{?cٶe,,ZHqaq4c[5VU❍pw݌>֨!ݶ3.~`׋*L$It{P-˴tǀcKKԭF?Xr(R|AТ(x4#֮DOuB hChUU,(Ihx< 8@ath: $~^X=N+ν:)(?QpaU/#͒\7:1[Hzm5dn4>CG"+{yc׵GȊYοe0F腈]L>x.i>&dN7cNO?7B_%%)~v.r .JBq;mBOW|ɯA v7Tq b!+1~I=I yz7''AM>4aG6n !@n 8W g1_<[l `3*,)Q g)֊=u&G&?BT#On7͓fHN!Id EHfDI7a29s&'Gi{w!krYQlv;%: yE,508u18:qY8u!v(O|G&>BVb9?UWQTʺ:gb1u?`!!5K/Q,3}tf3~+/{9s6vZZO-Ϭyьv&'gV& j㷳~GU8]A_,L&$۵ZVl: "tzseTtA  SZOjB  9>g<ސ c, rg~ƳkщpvjFP obgN4a[6   k]$M"k6pl/ut6n‘ ,7&2m|Do"WOJH ѮX[xhqTtmXv1sRP6 m`)wRCJ",U9 XV\]v6<=~cbD$A:i؛TgX~ *k+qpk:: B5!f||o_G r$8c6捘o`كX̆'s;0N+F;$#aE<H_o~Q71Lt|AqHZ@kB?#Zhp@ U Oξ}|S}e$vĵ->92_EL.ŵ/I;9EZC5J jjj¥7)W0c+;^DJ"{#SIv*oM>! ,$/'EWxo{j}1bbb|d3ΕqW2s5a;⯴յ ?r1@C#ԁ?c755A`-tU'єH-${tbݱ.\63L4ukc=K 2^APAmWQUI@pMq 4UCE=fu?*"rkX&pM)Arg/++[-uB!@/01v'ab36>ažasp IB%곎#(UѥH%R2qG(-(\ܻ^~㷼]zuեA0pnZ ^?:7!IN[On{?NCךD~ ;'v265F&]Rh}~^]+gYien!ҋs:s}ν0˲AQj$uHX}o͕ߤ7F|Oó?˞#{F7XܷZ~߳YC=T29S9frϭ7݊S3k2-7CCPVh x` NjfҸ\.&6.hte?e)+e,ѪbY׶mrk9~_mw5JR$I׋ueYlʫ'^UoEpEky~/&9d23ɟwGޥO+B뺉G"1E*gYl={dLӼj3^ƶmz{{ Bg4L_5I=D.Q>9/~}_‡iߟxA,]Sգd?ևox:vllfxv8gJ=N_HH?xUN*s, 0(|ecAs,ZQ7!>~ĔUOKlɣOgQؚMiT>"nt#dKĂ'X޷0^'''q#"7 ,s[w띿fq 7TH21srva2d45J<;[|R˗Շґ"'J|>_ii <[G˅eYܟUġ>DE2eY>#O%"2b8heYV"(孼ϫ,~455ոJ9zjoʰC{n>4l'V>Ag[ggΟTXͭDYE,2d<5ΎFish ~)w-H(B0r]Vvpies og9 B9!p793na%b{3jDF0L$EBȘ&jAf+\wr-޹7Åa-)J\AjBR% OccUiWA v^7}ۻo\0cQJf^2dfv]3( =4Y_n~,.e˪'ƦF~|kL'Yi i:T#D0d2[l޼^zoo49z(vb add[n&_|>Ϻu똜dÆ i|>#M 122B4z Mx$h4JP8E&&&룡7x^EĉT/(MMMn^xzzzغu+8… ihh8#WK˲( 躎i5OB&yjS܍_b&|fA~/RӗXDq80u?Ŧ&tPSYv)9@r*։X9>oNjy@p(,۵d0-v(ZEFL(GGI%#&ň1EUT?>խqGP*^\j[6i5SES q ll|5K }`ʀ suc8s$ǣxuVn[й`)YD7ÙÜLdrvx }>ÍD{mmA~M4~l WeaBZ-|omWW|+rrD4M8C[[TP(DTT*ӟ~m۶8h6|>t|!x LOOpB,bppCww7b/| 22|o{D'z%q$|!F M7F=̈́}a(-64lvQH,B@Sذ]d #au2.Dž57dkc۶Q` uu<<ƨ BeU-$&pX` N-O}tuQE%Ub|v}=Q<''.Nj7Kc-+xTUEU^bZ-$!=M=(`oo6{泥ϲme9 |D"iFGGضM,oT*i&<0ccctuuO:fϞ=055aW˅m۴e6m@w}  d2"|>^/ƍ%͒H$u 6 X,|@CCD`0a455( nq8qǎ#2;;KKKKMBIe[4,*ϟ_d灝ղ^֟.{f*@{]D(O[KeBé8ZQcEh]u]lZH(?\H>T>&s'֦IH z=$ :#r~*v+8beJ[v/xmۙ@ G!*Eo|$Ǔ 6֓9 PbX)$G'^ 9d-&s[mGIb]wؤ49-^9;>Mk|>y#4$ j/(Iq?_飈_:ōchܡ_]tvvorXxq5r 5>ޣ:$y\СC,_Zr\1 ?n6f2GOMMqnfZZZD"iϞVxp4T.hV'[ _ుǸg=464l˱؞ow֚ܺ!Ke8lq˴)gli̒MJt^N$sތ6Cetv'2Sxi%S/Q>>]9,GƎƎ7hok-kn"g^#NJa8FgãsEhx<^=Ft6$!ȅb}g>DR%ִ;Mk" [slRzt1k42IhvFd]0MM^T&3;[wZ*ScZh  B橪Woʥxrw+:WT7-`fA5xIeYJ%Gm#y Plvw3|xWb{;ˡ'8uj?9E|'\di һim==5)}Ya4sc,9Ɨ~Ξr2##a?X1>wD"g_ow.jAt1᭱(fD>?9Bw?@&CB֘ehf(C1S$du0>VO"Fs禹XX$VaF^Kx2ϕrvzA"p@ Pʹ4 .NݶK:3f8:}d6ɨ3J<gmZ(u:Cxd.|r3g;)gH4KcJ"]L/)J8;xn$q"0A5HŸ%B"CU-0zA,Om}:oc킵vlzkISi OL/@sV19'Sww0Ⱥ+:sP}˱G?/y88D>o֮.bXurD-b~~W⻅;VA4vlrc#Lg$q4{Q;fFO xϰ M˯5wΛ$SJV 9>;&v0GOED.zV5CygCF Cʐ\;k͍2:q /s%{BЂz#2Pp(L[K(*JhFQ+2Sa";A&a0++LXO[O@ %E! E*ABJ_DSwW)b+]XRqj$:\i^?M4R-OK |yns N _$.k6a`:=^/ |>LAm,Fmt8%K.`i @R 2!zEAmS=St1sDz 0$1yJn7AYfDixH&$e|գToeQm|fC.X,ٹ3v5-Ȫ<2,.ܿV?P0'5bHVϲj?'gNwz/F`;@Gv~B= /'gr|,H,{# ޅ2"6?}\@x45wFɠd0 U"9ZbH1]x893Gʓra``cWOddIFq)(.Y?Vmr İ Lr‌*_.?[>~JգV=~n/.م[v4dbQ=*{?znz+ _eǑ_aA sjב$&-e1Z*0ch8lm#Km^/A5I9mDŐ$ld[46E"lGU9^*am<NECAKɶqI uMT!#;+Y=᭼zUVV/ɷy[0v /Gۣs;kʻ/; m_`5E+h fi yfr}K9Ü,!4Mh(ZA`9 Ejo @El'p7~kO\Ck':S3Z͝SwRv9QsLJRpyl<1hf=LK# u?, ]ryY~hGs~-F?>T9hp+k50|E+VZSB"q>6;`w) RT;.ɩ$k#kYٹD}7oe-+E?=N+YdY&3%BShCuե f3 ]NÛ\zcVؾp1P  ɾ>~w\&dH,.cAbRC47Tq^ѻ&B`SllgYձꌓe6Cosk:j@ G[D27J<3l11pb 1ZOufeVuh\.]2''l~nk> xޚ"Ҏ0p|8I+(.,nZLK} `e&?Á<m5ו$:.f0fg3wuEg['hp0\3q3b$5'ߦ.p,-ƺFᨘqDF*I?  g|ahӳiv;FV6,wKsöYrVP1C2J1n߳2̋&IDATh| ;ooxjzY娌\rTƜ6@W6lJC%zZ{=Kilj0c͔5s5 ~ ͡f<4F3 6-P(U7V㯳BY=}ՋB3rMe<=oKhv5(>D‘k$@ P \WɖC[x}uD[*?zIgdb#{2R&ّ`z% eY#d^Π(u . +e4)^M?Z({" |'{{ׇX0@u;yRyBCO"j;Kx$YBn?a5LLQ^wD# ba\nQ~4|nstwv3kβ^<"K%{;e61m$G0f6NO' %QO Pd y[vq`#]u]lϸv $I^Z0Mo-ӃH1m rvTz|h iH> Lpr1_!kE p@Rr䳡,p֬-G]^J;ꦤ0& \~֬//K;J4% k~\-36C/042Xi_+&1܌G\3[!Sf(=D]tXT;I8!3t=M ; ƿ慽/w\wU8$IBUU0u:I'&y䐌1c`XՔ=OI"~@JBnKYAH& kJYHDN)@uGiin\I9_p,rt(8kײj*N;[Vb3)ᖌ9=ASdGMj'CSU,/)(yF@ 9>~ @qj+?lF6e6{6|5Im|6 `|vc;ubS):^S*6ߧ.^뭾VurEH^s0s(Bii+Bnﻝh(Ux=b(W (W$I}m$rGtǻ%wu3yt {xZ=8%¾8^c1 ! h.Tя訽*cqyEHU?60G Gy+bno7}>"j'HrB%<:f3 _?H@C@pyI O_kYOs`#3#<.ڥ[fqHHTJB.ٸ#ӡq ̚hm6ܹt|K|d^-Lh]c@P2-AwȍÍyu%(/)dBx;Ox#$  _VÄaŭ( ^xkjDR@p(ydY& Xع]Iͦ|c]}TG`]юh( N4Q.ظ|.dE[賉`%5 `ً62e`LiQ S&GVrmDI[]bE_N s#ts BSگ#{h b$ SYptocڜ7>Nr%+x>_>BM$w% z Aush5$Y·܇1n}X<>^/v>'R 7V$Pq+{?3wͰorP\r)UpKRFyB Gkd.nY RCiD0g~'\K"'l0 t]'2>>λ%V{ڎ ȑT$ %߹Wl*sgʺilj"YEP \;$@pJ,x$ VY333heY5zJ.=- dYU]>wε*SCGgxx$PۦX,b9r|\ܧ}* @ (RP{Vq˲HbaP.x<+*WBq2 ^8333b1u,iB3rF @ xqlرcLLL`Y^(5mccgq,R=T mgD@ N&`Y,_\.GKKK@^NTUϚmrtkzz:zgϫld2ɇ~ǙaӦMUeL&K&arrp8̢Er:t~ ,r, I8x haB@ \|H$V$ Gjmb(nw^QQt:mf4>O8::J0\߁@:؎)UUn[ffesNNcffρ^AF}>v7prr[YC]\xJ[' .+Wł,?7ťuXX(%^Nd2DU`8 R (ʺ[\\\ Bc|岙;R4|V__088(.b\ ^jlllumllE 񇪪޼pEgvUTTԵFFF_677"x|]U[^7 &b\ H#VVVL&%M\>tWАD@ 0i=??_eYfuuH$IF)@KUUU I5f ؍bTꕝ=m煻M kϣ@@QI$Wgg7'A>Fc6oΛ eܼhД 8f2x i-˗OMFpAx`//M+VIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/itemPin.png0000644000000000000000000000013214532447230016600 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/itemPin.png0000644000175000001440000000175214532447230020035 0ustar00rncbcusers00000000000000PNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<wIDAT8]HdecFFR^l[P퀂3ꅔ݄IAF.Dݬ(8(x̀wҴ393.8∁T9O6k~ދs~W MӴYP558~W0~džatUS!"U=Jio"3}B;~U5M3M/u]x/^Ɔau0U-J855Ȉ?J}54;;뫫)Jض]B}}={{{v4-z<} 2pP(ԩ>77 mEd2 Wdց9>xu@9L:tZf#YZZ`0贴$U-)SW UOnw t,//ܜSBe YSSx.Jקq'/T+gT(-/;'l6+cccG'L&T*崷Uƅ+\ܶ,X,Vr\.SNT+6`-˲Zt{ޟއj\,-4:A oooK 8,kx.ojB:pvKӧ;e )hu/;;j/B++njjjq\w3&+NEDMtoc8Wkّ(iTò IENDB`qpwgraph-0.6.1/src/images/PaxHeaders/viewZoomOut.png0000644000000000000000000000013214532447230017502 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/viewZoomOut.png0000644000175000001440000000251614532447230020736 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8mLSgBQ2|a" ٤83!Dg6b6,q%3B2ݗ-!,QN0T"PLK}e! nNr|ssT* 0j@@pcK +GN:Us\>/ I< lwi)ik(tRٳ ^P(N#''Ahkk\SSS׀a`~%R ׿:x𠮷X__ߘuhh68zh4t%.\pmrr*0R _xqŋRM]ߚ ȵQQ-***n߶˕GE 3\j۶m6S>|KLǏWI$tu݋?d+Sx:<<7 mLi)ePr4~`jݺut:1He9) 2D"PmZ9͛24ͦ`H皜Y`l^FD"$Iay|||ĉFFFjZ0`iaatT*OR@$ ̔333LMMyS/&^oe{(>幼LAɓ' qCyyzrӃሙf֭[\Z-(F| lmm.6`AP/X,[Z 墻Ajv޽d2fei3DQ%<<>11p:ǭ@"p emUQQeSS|?P)vuJv"0Lϔ3kh,H8Nf<fI/,*Rr)lnr=Baox<(З $@t, z<@ `\6_[[].ePi˖6Z0*5U_PE3nIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/itemConnect.png0000644000000000000000000000013214532447230017443 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/itemConnect.png0000644000175000001440000000164114532447230020675 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8AL#U3vSZpvMp!L@=/pXLc$O$rd,'B8B%@]lȴ2,Б}{Tlty{OKifۅ x^Wˀ =('Η;|]0mYibC·CCC^˲B`YRppk.o"?Ͳ)Bz0M-7/FGGSt:-gff; ݑ*z}===&''B%d2?Q搦ދF{aTs\4MY(ryyY__!5:8.]!n׋a@pulUJ2'|_- Q.1MS Z0eE6}"ccc!r9|>p؏m2+br%ɢeY\.ffz/..~ԭ-DaeeQ"d2ū.x|@,ښn!.賻k K_4MR)955U BK66G]S)oE"7Ţxs%ɍǚOZo;@ pT*e{{;J~7y\駚y C @+@QӰ 1K ̨3õnnXHd)OIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/editRedo.png0000644000000000000000000000013214532447230016732 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/editRedo.png0000644000175000001440000000125614532447230020166 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<+IDAT8MAKFBxpAR^DuDzP:!QmOCrt뼉G13P" Ïw#.v~فaH!āɾah4 wvvPJ)eٔP赦iL&30MS!ZY;{{{I6llk{;RB_]]} \N1ciW؀y˥!vC3M]JT*Z"x^4Xpt]G Tj> MG|>+VU v*^ZA'Onpn!P~Ζ7Y=95$O8*XGIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/qpwgraph.png0000644000000000000000000000013214532447230017024 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/qpwgraph.png0000644000175000001440000000356114532447230020261 0ustar00rncbcusers00000000000000PNG  IHDR szz pHYsyytEXtSoftwarewww.inkscape.org<IDATXW{LT}FL-̴2"ٴ&M[- ..HX ( Q&4R!j( 1PVa <Œ3s=ÁhГL;wwra=%h]? |`|IDd-!/Iu:ܹsε1*S"CP?7Ij{rWEE1W"[(BƘnttqff&2$$$8<<عs'`lll"//wCCCe1V#EQhDZZfggADBhh=>>>DFEDtѣDDH vن t ~uuqfffw}""ΦL(>>>gjZ|dXf3ipT˃AAA/^ K===N555dZF0000 Ȓsqƍj>?߷o_bH#"==s @ݻwW@֭[\]]m:))SP./_n q433CNN'9r8DQii)9sFx۴io|??tfOOOngvXuXHcǎ+^AJuNVxAGq۶m$IfmmϟӉ'$cǾYzL&-I+@P]:66F^ E(44S^?5??Ofrssx]BTe߹sCR @]v}k׮j%HA,==]Xi%lTQQaJ XRRb_dm% \JSf]ǣ,J$%%022bϿb*ȇ1&8S^^~ZRm&"L&:t^ ġhQt+%''t.]966h&"JHl o$kRzOOmZ Wa`o+k]B`=dN? -lIENDB`qpwgraph-0.6.1/src/images/PaxHeaders/fileNew.png0000644000000000000000000000013214532447230016564 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/images/fileNew.png0000644000175000001440000000075714532447230020025 0ustar00rncbcusers00000000000000PNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<|IDAT8핱NP[Je eŁ fc):f^f ցBL{ν~H" } H1 P8\4Rt>y2b~'P(X*j2iԯ5 JYi]׋L气T 0Bnkj;<2| àRtjY\HGq'U6h`6vu\c^VUreY,K6 ^/UxXihF>!w* Q?z`8h0j~x<u  p gHL7/U!EI!Lڊߧw,~IENDB`qpwgraph-0.6.1/src/images/PaxHeaders/viewZoomIn.png0000644000000000000000000000013214532447230017301 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/images/viewZoomIn.png0000644000175000001440000000260214532447230020531 0ustar00rncbcusers00000000000000PNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8]LSg紥eW) ̯L[RΏɜ^x1љd.lf1a7$l l3FVB#ȂT - ؞삃!dOzCM@.`D ~  B+V>x඲,-@ Hܿ5R VʻZ OŽNȑ/s_~-~&&&0ddd MMMUU?622r g#p;wL줹9*IAY弼\V{'&Պ$=8q{N<(6mߪg?ptuu{(B &nt+[n+{{{׮]Z,o+W6Mܺժy`Po߾mDBuvvt j4bV$uuΠbf-VBIIett 0hѢ_dggoQE$''ۭVr rF.]7 K"tccc!xgKnfp&`ժUv]Fo4Ln1677 /[EEњG}˗\@0jYKӉ7n)..`0DK_^wThQQQ۳Gҽ{pΝyrr@mKܕ-z^:::hoov˄LF#ZH$B oZ[[~D"U1eAMpw:vY,9륭ބ 7lذ8l6EQ#džB$~ @ul~XZZ}mŽ99ַPIn=̜jv"U?v*I]N57) ގ$IƁ}`v=ި^[^. 7n|T+W!otcYEMD|>P(s^9Bf'eiUl@MA2IENDB`qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_config.h0000644000000000000000000000013214532447230016547 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_config.h0000644000175000001440000000754714532447230020014 0ustar00rncbcusers00000000000000// qpwgraph_config.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_config_h #define __qpwgraph_config_h #include "qpwgraph_node.h" #include // Forwards decls. class QSettings; class QMainWindow; //---------------------------------------------------------------------------- // qpwgraph_config -- Canvas state memento. class qpwgraph_config { public: // Constructor. qpwgraph_config(QSettings *settings, bool owner = false); qpwgraph_config(const QString& org_name, const QString& app_name); // Destructor. ~qpwgraph_config(); // Accessors. void setSettings(QSettings *settings, bool owner = false); QSettings *settings() const; void setMenubar(bool menubar); bool isMenubar() const; void setToolbar(bool toolbar); bool isToolbar() const; void setStatusbar(bool statusbar); bool isStatusbar() const; void setTextBesideIcons(bool texticons); bool isTextBesideIcons() const; void setZoomRange(bool zoomrange); bool isZoomRange() const; void setSortType(int sorttype); int sortType() const; void setSortOrder(int sortorder); int sortOrder() const; void setRepelOverlappingNodes(bool repelnodes); bool isRepelOverlappingNodes() const; void setConnectThroughNodes(bool cthrunodes); bool isConnectThroughNodes() const; void setPatchbayToolbar(bool toolbar); bool isPatchbayToolbar() const; void setPatchbayDir(const QString& dir); const QString& patchbayDir() const; void setPatchbayPath(const QString& path); const QString& patchbayPath() const; void setPatchbayActivated(bool activated); bool isPatchbayActivated() const; void setPatchbayExclusive(bool exclusive); bool isPatchbayExclusive() const; void setPatchbayAutoPin(bool autopin); bool isPatchbayAutoPin() const; void setPatchbayAutoDisconnect(bool autodisconnect); bool isPatchbayAutoDisconnect() const; void patchbayRecentFiles(const QString& path); const QStringList& patchbayRecentFiles() const; void setSystemTrayEnabled(bool enabled); bool isSystemTrayEnabled() const; void setAlsaMidiEnabled(bool enabled); bool isAlsaMidiEnabled() const; void setSessionStartMinimized(bool start_minimized); bool isSessionStartMinimized() const; // Graph main-widget state methods. bool restoreState(QMainWindow *widget); bool saveState(QMainWindow *widget) const; private: // Instance variables. QSettings *m_settings; bool m_owner; bool m_menubar; bool m_toolbar; bool m_statusbar; bool m_texticons; bool m_zoomrange; int m_sorttype; int m_sortorder; bool m_repelnodes; bool m_cthrunodes; bool m_patchbay_toolbar; QString m_patchbay_dir; QString m_patchbay_path; bool m_patchbay_activated; bool m_patchbay_exclusive; bool m_patchbay_autopin; bool m_patchbay_autodisconnect; QStringList m_patchbay_recentfiles; bool m_systray_enabled; bool m_alsaseq_enabled; }; #endif // __qpwgraph_config_h // end of qpwgraph_config.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_command.h0000644000000000000000000000013214532447230016720 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_command.h0000644000175000001440000001142514532447230020153 0ustar00rncbcusers00000000000000// qpwgraph_command.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_command_h #define __qpwgraph_command_h #include #include "qpwgraph_node.h" // Forward decls. class qpwgraph_canvas; //---------------------------------------------------------------------------- // qpwgraph_command -- Generic graph command pattern class qpwgraph_command : public QUndoCommand { public: // Constructor. qpwgraph_command(qpwgraph_canvas *canvas, QUndoCommand *parent = nullptr); // Accessors. qpwgraph_canvas *canvas() const { return m_canvas; } // Command methods. void undo(); void redo(); protected: // Command executive method. virtual bool execute(bool is_undo = false) = 0; private: // Command arguments. qpwgraph_canvas *m_canvas; }; //---------------------------------------------------------------------------- // qpwgraph_connect command -- Connect graph command class qpwgraph_connect_command : public qpwgraph_command { public: // Constructor. qpwgraph_connect_command(qpwgraph_canvas *canvas, qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect, qpwgraph_command *parent = nullptr); protected: // Command item address struct Addr { // Constructors. Addr(qpwgraph_port *port) { qpwgraph_node *node = port->portNode(); node_id = node->nodeId(); node_type = node->nodeType(); port_id = port->portId(); port_type = port->portType(); } // Copy constructor. Addr(const Addr& addr) { node_id = addr.node_id; node_type = addr.node_type; port_id = addr.port_id; port_type = addr.port_type; } // Member fields. uint node_id; uint node_type; uint port_id; uint port_type; }; // Command item descriptor struct Item { // Constructor. Item(qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect) : addr1(port1), addr2(port2), m_connect(is_connect) {} // Copy constructor. Item(const Item& item) : addr1(item.addr1), addr2(item.addr2), m_connect(item.is_connect()) {} // Accessors. bool is_connect() const { return m_connect; } // Public member fields. Addr addr1; Addr addr2; private: // Private member fields. bool m_connect; }; // Command executive method. bool execute(bool is_undo); private: // Command arguments. Item m_item; }; //---------------------------------------------------------------------------- // qpwgraph_move_command -- Move (node) graph command class qpwgraph_move_command : public qpwgraph_command { public: // Constructor. qpwgraph_move_command(qpwgraph_canvas *canvas, const QList& nodes, const QPointF& pos1, const QPointF& pos2, qpwgraph_command *parent = nullptr); // Destructor. ~qpwgraph_move_command(); // Add/replace (an already moved) node position for undo/redo... void addItem(qpwgraph_node *node, const QPointF& pos1, const QPointF& pos2); protected: // Command item descriptor struct Item { uint node_id; qpwgraph_item::Mode node_mode; uint node_type; QPointF node_pos1; QPointF node_pos2; }; // Command executive method. bool execute(bool is_undo); private: // Command arguments. QHash m_items; int m_nexec; }; //---------------------------------------------------------------------------- // qpwgraph_rename_command -- Rename (item) graph command class qpwgraph_rename_command : public qpwgraph_command { public: // Constructor. qpwgraph_rename_command(qpwgraph_canvas *canvas, qpwgraph_item *item, const QString& name, qpwgraph_command *parent = nullptr); protected: // Command item descriptor struct Item { int item_type; uint node_id; qpwgraph_item::Mode node_mode; uint node_type; uint port_id; qpwgraph_item::Mode port_mode; uint port_type; }; // Command executive method. bool execute(bool is_undo); private: // Command arguments. Item m_item; QString m_name; }; #endif // __qpwgraph_command_h // end of qpwgraph_command.h qpwgraph-0.6.1/src/PaxHeaders/appdata0000644000000000000000000000013214532447230014555 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/appdata/0000755000175000001440000000000014532447230016062 5ustar00rncbcusers00000000000000qpwgraph-0.6.1/src/appdata/PaxHeaders/org.rncbc.qpwgraph.metainfo.xml0000644000000000000000000000013214532447230022662 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/appdata/org.rncbc.qpwgraph.metainfo.xml0000644000175000001440000000314214532447230024112 0ustar00rncbcusers00000000000000 org.rncbc.qpwgraph FSFAP GPL-2.0+ qpwgraph

A PipeWire Graph Qt GUI Interface

qpwgraph is a graph manager dedicated to PipeWire (https\://pipewire.org), using the Qt C++ framework (https\://qt.io), based and pretty much like the same of QjackCtl (https\://qjackctl.sourceforge.io).

org.rncbc.qpwgraph.desktop qpwgraph https://gitlab.freedesktop.org/rncbc/qpwgraph/-/raw/main/src/images/qpwgraph_screenshot-1.png The main application window in action https://gitlab.freedesktop.org/rncbc/qpwgraph/-/raw/main/src/images/qpwgraph.svg The official system tray icon AudioVideo Audio Video MIDI JACK Qt https://gitlab.freedesktop.org/rncbc/qpwgraph rncbc aka. Rui Nuno Capela rncbc@rncbc.org qpwgraph-0.6.1/src/appdata/PaxHeaders/org.rncbc.qpwgraph.desktop0000644000000000000000000000013214532447230021732 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/appdata/org.rncbc.qpwgraph.desktop0000644000175000001440000000053114532447230023161 0ustar00rncbcusers00000000000000[Desktop Entry] Name=qpwgraph Version=1.0 GenericName=PipeWire Graph/Patchbay Comment=qpwgraph is a PipeWire graph Qt GUI interface Exec=qpwgraph %f Icon=org.rncbc.qpwgraph Categories=AudioVideo;Audio;Video;Midi;X-Alsa;X-PipeWire;Qt; MimeType=application/x-qpwgraph-patchbay; Keywords=PipeWire;MIDI;ALSA;JACK;Qt; Terminal=false Type=Application qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_alsamidi.h0000644000000000000000000000013214532447230017065 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_alsamidi.h0000644000175000001440000000506614532447230020324 0ustar00rncbcusers00000000000000// qpwgraph_alsamidi.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_alsamidi_h #define __qpwgraph_alsamidi_h #include "config.h" #include "qpwgraph_sect.h" #ifdef CONFIG_ALSA_MIDI #include #include // Forwards decls. class QSocketNotifier; //---------------------------------------------------------------------------- // qpwgraph_alsamidi -- ALSA graph driver class qpwgraph_alsamidi : public qpwgraph_sect { Q_OBJECT public: // Constructor. qpwgraph_alsamidi(qpwgraph_canvas *canvas); // Destructor. ~qpwgraph_alsamidi(); // Client methods. bool open(); void close(); // ALSA port (dis)connection. void connectPorts(qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect); // ALSA graph updaters. void updateItems(); void clearItems(); // Special port-type colors defaults (virtual). void resetPortTypeColors(); // ALSA node type inquirer. static bool isNodeType(uint node_type); // ALSA node type. static uint nodeType(); // ALSA port type inquirer. static bool isPortType(uint port_type); // ALSA port type. static uint midiPortType(); signals: void changed(); protected slots: // Callback notifiers. void changedNotify(); protected: // ALSA client:port finder and creator if not existing. bool findClientPort(snd_seq_client_info_t *client_info, snd_seq_port_info_t *port_info, qpwgraph_item::Mode port_mode, qpwgraph_node **node, qpwgraph_port **port, bool add_new); private: // Instance variables. snd_seq_t *m_seq; QSocketNotifier *m_notifier; // Notifier sanity mutex. QMutex m_mutex; }; #endif // CONFIG_ALSA_MIDI #endif // __qpwgraph_alsamidi_h // end of qpwgraph_alsamidi.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_systray.h0000644000000000000000000000013214532447230017020 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_systray.h0000644000175000001440000000345314532447230020255 0ustar00rncbcusers00000000000000// qpwgraph_systray.h // /**************************************************************************** Copyright (C) 2021-2022, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_systray_h #define __qpwgraph_systray_h #include "config.h" #ifdef CONFIG_SYSTEM_TRAY #include #include // Forward decls. class qpwgraph_form; class QAction; //---------------------------------------------------------------------------- // qpwgraph_systray -- Custom system tray icon. class qpwgraph_systray : public QSystemTrayIcon { Q_OBJECT public: // Constructor. qpwgraph_systray(qpwgraph_form *form); // Update context menu. void updateContextMenu(); protected slots: // Handle systeam tray activity. void activated(QSystemTrayIcon::ActivationReason reason); // Handle menu actions. void showHide(); private: qpwgraph_form *m_form; QAction *m_show; QAction *m_quit; QMenu m_menu; }; #endif // CONFIG_SYSTEM_TRAY #endif // __qpwgraph_systray_h // end of qpwgraph_systray.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_canvas.cpp0000644000000000000000000000013214532447230017110 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_canvas.cpp0000644000175000001440000013036114532447230020344 0ustar00rncbcusers00000000000000// qpwgraph_canvas.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_canvas.h" #include "qpwgraph_connect.h" #include "qpwgraph_patchbay.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Local constants. static const char *CanvasGroup = "/GraphCanvas"; static const char *CanvasRectKey = "/CanvasRect"; static const char *CanvasZoomKey = "/CanvasZoom"; static const char *NodePosGroup = "/GraphNodePos"; static const char *ColorsGroup = "/GraphColors"; static const char *NodeAliasesGroup = "/GraphNodeAliases"; static const char *PortAliasesGroup = "/GraphPortAliases"; //---------------------------------------------------------------------------- // qpwgraph_canvas -- Canvas graphics scene/view. // Constructor. qpwgraph_canvas::qpwgraph_canvas ( QWidget *parent ) : QGraphicsView(parent), m_state(DragNone), m_item(nullptr), m_connect(nullptr), m_rubberband(nullptr), m_zoom(1.0), m_zoomrange(false), m_gesture(false), m_commands(nullptr), m_settings(nullptr), m_patchbay(nullptr), m_patchbay_edit(false), m_patchbay_autopin(true), m_patchbay_autodisconnect(false), m_selected_nodes(0), m_repel_overlapping_nodes(false), m_edit_item(nullptr), m_editor(nullptr), m_edited(0) { m_scene = new QGraphicsScene(); m_commands = new QUndoStack(); m_patchbay = new qpwgraph_patchbay(this); QGraphicsView::setScene(m_scene); QGraphicsView::setRenderHint(QPainter::Antialiasing); QGraphicsView::setRenderHint(QPainter::SmoothPixmapTransform); QGraphicsView::setResizeAnchor(QGraphicsView::NoAnchor); QGraphicsView::setDragMode(QGraphicsView::NoDrag); m_editor = new QLineEdit(this); m_editor->setFrame(false); // m_editor->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); QObject::connect(m_editor, SIGNAL(textChanged(const QString&)), SLOT(textChanged(const QString&))); QObject::connect(m_editor, SIGNAL(editingFinished()), SLOT(editingFinished())); QGraphicsView::grabGesture(Qt::PinchGesture); m_editor->setEnabled(false); m_editor->hide(); } // Destructor. qpwgraph_canvas::~qpwgraph_canvas (void) { clear(); delete m_editor; delete m_patchbay; delete m_commands; delete m_scene; } // Accessors. QGraphicsScene *qpwgraph_canvas::scene (void) const { return m_scene; } QUndoStack *qpwgraph_canvas::commands (void) const { return m_commands; } void qpwgraph_canvas::setSettings ( QSettings *settings ) { m_settings = settings; } QSettings *qpwgraph_canvas::settings (void) const { return m_settings; } qpwgraph_patchbay *qpwgraph_canvas::patchbay (void) const { return m_patchbay; } // Patchbay auto-pin accessors. void qpwgraph_canvas::setPatchbayAutoPin ( bool on ) { m_patchbay_autopin = on; } bool qpwgraph_canvas::isPatchbayAutoPin (void) const { return m_patchbay_autopin; } // Patchbay auto-disconnect accessors. void qpwgraph_canvas::setPatchbayAutoDisconnect ( bool on ) { m_patchbay_autodisconnect = on; } bool qpwgraph_canvas::isPatchbayAutoDisconnect (void) const { return m_patchbay_autodisconnect; } // Patchbay edit-mode accessors. void qpwgraph_canvas::setPatchbayEdit ( bool on ) { if (m_patchbay == nullptr) return; if ((!on && !m_patchbay_edit) || ( on && m_patchbay_edit)) return; m_patchbay_edit = on; patchbayEdit(); } bool qpwgraph_canvas::isPatchbayEdit (void) const { return (m_patchbay && m_patchbay_edit); } void qpwgraph_canvas::patchbayEdit (void) { foreach (QGraphicsItem *item, m_scene->items()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect) { connect->setDimmed( m_patchbay_edit && !m_patchbay->findConnect(connect)); } } } } bool qpwgraph_canvas::canPatchbayPin (void) const { if (m_patchbay == nullptr || !m_patchbay_edit) return false; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect && !m_patchbay->findConnect(connect)) return true; } } return false; } bool qpwgraph_canvas::canPatchbayUnpin (void) const { if (m_patchbay == nullptr || !m_patchbay_edit) return false; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect && m_patchbay->findConnect(connect)) return true; } } return false; } void qpwgraph_canvas::patchbayPin (void) { if (m_patchbay == nullptr || !m_patchbay_edit) return; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect && m_patchbay->connect(connect, true)) connect->setDimmed(false); } } } void qpwgraph_canvas::patchbayUnpin (void) { if (m_patchbay == nullptr || !m_patchbay_edit) return; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect && m_patchbay->connect(connect, false)) connect->setDimmed(true); } } } // Canvas methods. void qpwgraph_canvas::addItem ( qpwgraph_item *item ) { if (item->type() != qpwgraph_port::Type) // ports are already in nodes m_scene->addItem(item); if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) { m_nodes.append(node); addNodeKeys(node); if (restoreNode(node)) emit updated(node); else emit added(node); } } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) restorePort(port); } else if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect) { connect->setDimmed(m_patchbay_edit && m_patchbay && !m_patchbay->findConnect(connect)); } } } void qpwgraph_canvas::removeItem ( qpwgraph_item *item ) { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node && saveNode(node)) { emit removed(node); node->removePorts(); removeNodeKeys(node); m_nodes.removeAll(node); } } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) savePort(port); } // Do not remove items from the scene // as they shall be removed upon delete... // // m_scene->removeItem(item); } // Current item accessor. qpwgraph_item *qpwgraph_canvas::currentItem (void) const { qpwgraph_item *item = m_item; if (item && item->type() == qpwgraph_connect::Type) item = nullptr; if (item == nullptr) { foreach (QGraphicsItem *item2, m_scene->selectedItems()) { if (item2->type() == qpwgraph_connect::Type) continue; item = static_cast (item2); if (item2->type() == qpwgraph_node::Type) break; } } return item; } // Connection predicates. bool qpwgraph_canvas::canConnect (void) const { int nins = 0; int nouts = 0; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) { if (node->nodeMode() & qpwgraph_item::Input) ++nins; else // if (node->nodeMode() & qpwgraph_item::Output) ++nouts; } } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) { if (port->isInput()) ++nins; else // if (port->isOutput()) ++nouts; } } if (nins > 0 && nouts > 0) return true; } return false; } bool qpwgraph_canvas::canDisconnect (void) const { foreach (QGraphicsItem *item, m_scene->selectedItems()) { switch (item->type()) { case qpwgraph_connect::Type: return true; case qpwgraph_node::Type: { qpwgraph_node *node = static_cast (item); foreach (qpwgraph_port *port, node->ports()) { if (!port->connects().isEmpty()) return true; } // Fall-thru... } default: break; } } return false; } // Edit predicates. bool qpwgraph_canvas::canRenameItem (void) const { qpwgraph_item *item = currentItem(); return (item && ( item->type() == qpwgraph_node::Type || item->type() == qpwgraph_port::Type)); } // Zooming methods. void qpwgraph_canvas::setZoom ( qreal zoom ) { if (zoom < 0.1) zoom = 0.1; else if (zoom > 1.9) zoom = 1.9; const qreal scale = zoom / m_zoom; QGraphicsView::scale(scale, scale); QFont font = m_editor->font(); font.setPointSizeF(scale * font.pointSizeF()); m_editor->setFont(font); updateEditorGeometry(); m_zoom = zoom; emit changed(); } qreal qpwgraph_canvas::zoom (void) const { return m_zoom; } void qpwgraph_canvas::setZoomRange ( bool zoomrange ) { m_zoomrange = zoomrange; } bool qpwgraph_canvas::isZoomRange (void) const { return m_zoomrange; } // Clean-up all un-marked nodes... void qpwgraph_canvas::resetNodes ( uint node_type ) { QList nodes; foreach (qpwgraph_node *node, m_nodes) { if (node->nodeType() == node_type) { if (node->isMarked()) { node->resetPorts(); node->setMarked(false); } else { removeItem(node); nodes.append(node); } } } qDeleteAll(nodes); } void qpwgraph_canvas::clearNodes ( uint node_type ) { QList nodes; foreach (qpwgraph_node *node, m_nodes) { if (node->nodeType() == node_type) { m_node_names.remove(qpwgraph_node::NodeNameKey(node), node); m_node_ids.remove(qpwgraph_node::NodeIdKey(node), node); m_nodes.removeAll(node); nodes.append(node); } } qDeleteAll(nodes); } // Special node finders. qpwgraph_node *qpwgraph_canvas::findNode ( uint id, qpwgraph_item::Mode mode, uint type ) const { return m_node_ids.value(qpwgraph_node::NodeIdKey(id, mode, type), nullptr); } QList qpwgraph_canvas::findNodes ( const qpwgraph_node::NodeNameKey& name_key ) const { struct CompareNodeId { bool operator()(qpwgraph_node *node1, qpwgraph_node *node2) const { return (node1->nodeId() < node2->nodeId()); } }; QList nodes = m_node_names.values(name_key); std::sort(nodes.begin(), nodes.end(), CompareNodeId()); return nodes; } QList qpwgraph_canvas::findNodes ( const QString& name, qpwgraph_item::Mode mode, uint type ) const { return findNodes(qpwgraph_node::NodeNameKey(name, mode, type)); } // Whether it's in the middle of something... bool qpwgraph_canvas::isBusy (void) const { return (m_state != DragNone || m_connect != nullptr || m_item != nullptr || m_edit_item != nullptr); } void qpwgraph_canvas::releaseNode ( qpwgraph_node *node ) { removeNodeKeys(node); node->setMarked(false); } // Port (dis)connections dispatcher. void qpwgraph_canvas::emitConnectPorts ( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect ) { if (m_patchbay_edit && !m_patchbay_autopin && is_connect) { qpwgraph_connect *connect = port1->findConnect(port2); if (connect) connect->setDimmed(true); } if (m_patchbay && (m_patchbay_autopin || (!is_connect && m_patchbay->isActivated()))) m_patchbay->connectPorts(port1, port2, is_connect); if (is_connect) emitConnected(port1, port2); else emitDisconnected(port1, port2); } // Port (dis)connections notifiers. void qpwgraph_canvas::emitConnected ( qpwgraph_port *port1, qpwgraph_port *port2 ) { emit connected(port1, port2); } void qpwgraph_canvas::emitDisconnected ( qpwgraph_port *port1, qpwgraph_port *port2 ) { emit disconnected(port1, port2); } // Rename notifiers. void qpwgraph_canvas::emitRenamed ( qpwgraph_item *item, const QString& name ) { emit renamed(item, name); } // Item finder (internal). qpwgraph_item *qpwgraph_canvas::itemAt ( const QPointF& pos ) const { const QList& items = m_scene->items(QRectF(pos - QPointF(2, 2), QSizeF(5, 5))); foreach (QGraphicsItem *item, items) { if (item->type() >= QGraphicsItem::UserType) return static_cast (item); } return nullptr; } // Port (dis)connection command. void qpwgraph_canvas::connectPorts ( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect ) { #if 0 // Sure the sect will check to this instead...? const bool is_connected // already connected? = (port1->findConnect(port2) != nullptr); if (( is_connect && is_connected) || (!is_connect && !is_connected)) return; #endif if (port1->isOutput()) { m_commands->push( new qpwgraph_connect_command(this, port1, port2, is_connect)); } else { m_commands->push( new qpwgraph_connect_command(this, port2, port1, is_connect)); } } // Mouse event handlers. void qpwgraph_canvas::mousePressEvent ( QMouseEvent *event ) { if (m_gesture) return; m_state = DragNone; m_item = nullptr; m_pos = QGraphicsView::mapToScene(event->pos()); if (event->button() == Qt::LeftButton) { m_item = itemAt(m_pos); m_state = DragStart; } if (m_item == nullptr && (((event->button() == Qt::LeftButton) && (event->modifiers() & Qt::ControlModifier)) || (event->button() == Qt::MiddleButton)) && m_scene->selectedItems().isEmpty()) { #if 1//NEW_DRAG_SCROLL_MODE // HACK: When about to drag-scroll, // always fake a left-button press... QGraphicsView::setDragMode(ScrollHandDrag); QMouseEvent event2(event->type(), #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) event->position(), event->globalPosition(), #else event->localPos(), event->globalPos(), #endif Qt::LeftButton, Qt::LeftButton, event->modifiers() | Qt::ControlModifier); QGraphicsView::mousePressEvent(&event2); #else QGraphicsView::setCursor(Qt::ClosedHandCursor) #endif m_state = DragScroll; } } void qpwgraph_canvas::mouseMoveEvent ( QMouseEvent *event ) { if (m_gesture) return; int nchanged = 0; QPointF pos = QGraphicsView::mapToScene(event->pos()); switch (m_state) { case DragStart: if ((pos - m_pos).manhattanLength() > 8.0) { m_state = DragMove; if (m_item) { // Start new connection line... if (m_item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (m_item); if (port) { QGraphicsView::setCursor(Qt::DragLinkCursor); m_selected_nodes = 0; m_scene->clearSelection(); m_connect = new qpwgraph_connect(); m_connect->setPort1(port); m_connect->setSelected(true); m_connect->raise(); m_scene->addItem(m_connect); m_item = nullptr; ++m_selected_nodes; ++nchanged; } } else // Start moving nodes around... if (m_item->type() == qpwgraph_node::Type) { QGraphicsView::setCursor(Qt::SizeAllCursor); if (!m_item->isSelected()) { if ((event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier)) == 0) { m_selected_nodes = 0; m_scene->clearSelection(); } m_item->setSelected(true); ++nchanged; } // Original node position (for move command)... m_pos1 = m_pos; snapPos(m_pos1); } else m_item = nullptr; } // Otherwise start lasso rubber-banding... if (m_rubberband == nullptr && m_item == nullptr && m_connect == nullptr) { QGraphicsView::setCursor(Qt::CrossCursor); m_rubberband = new QRubberBand(QRubberBand::Rectangle, this); } // Set allowed auto-scroll margins/limits... boundingRect(true); } break; case DragMove: // Allow auto-scroll only if within allowed margins/limits... boundingPos(pos); QGraphicsView::ensureVisible(QRectF(pos, QSizeF(2, 2)), 8, 8); // Move new connection line... if (m_connect) m_connect->updatePathTo(pos); // Move rubber-band lasso... if (m_rubberband) { const QRect rect( QGraphicsView::mapFromScene(m_pos), QGraphicsView::mapFromScene(pos)); m_rubberband->setGeometry(rect.normalized()); m_rubberband->show(); if (!m_zoomrange) { if (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)) { foreach (QGraphicsItem *item, m_selected) { item->setSelected(!item->isSelected()); ++nchanged; } m_selected.clear(); } else { m_selected_nodes = 0; m_scene->clearSelection(); ++nchanged; } const QRectF range_rect(m_pos, pos); foreach (QGraphicsItem *item, m_scene->items(range_rect.normalized())) { if (item->type() >= QGraphicsItem::UserType) { if (item->type() != qpwgraph_node::Type) ++m_selected_nodes; else if (m_selected_nodes > 0) continue; const bool is_selected = item->isSelected(); if (event->modifiers() & Qt::ControlModifier) { m_selected.append(item); item->setSelected(!is_selected); } else if (!is_selected) { if (event->modifiers() & Qt::ShiftModifier) m_selected.append(item); item->setSelected(true); } ++nchanged; } } } } // Move current selected nodes... if (m_item && m_item->type() == qpwgraph_node::Type) { snapPos(pos); const QPointF delta = (pos - m_pos); foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) node->setPos(node->pos() + delta); } } m_pos = pos; } else if (m_connect) { // Hovering ports high-lighting... const qreal zval = m_connect->zValue(); m_connect->setZValue(-1.0); QGraphicsItem *item = itemAt(pos); if (item && item->type() == qpwgraph_port::Type) { qpwgraph_port *port1 = m_connect->port1(); qpwgraph_port *port2 = static_cast (item); if (port1 && port2 && port1->portType() == port2->portType() && port1->portMode() != port2->portMode()) { port2->update(); } } m_connect->setZValue(zval); } break; case DragScroll: { #if 1//NEW_DRAG_SCROLL_MODE QGraphicsView::mouseMoveEvent(event); #else QScrollBar *hbar = QGraphicsView::horizontalScrollBar(); QScrollBar *vbar = QGraphicsView::verticalScrollBar(); const QPoint delta = (pos - m_pos).toPoint(); hbar->setValue(hbar->value() - delta.x()); vbar->setValue(vbar->value() - delta.y()); m_pos = pos; #endif break; } default: break; } if (nchanged > 0) emit changed(); } void qpwgraph_canvas::mouseReleaseEvent ( QMouseEvent *event ) { if (m_gesture) return; int nchanged = 0; switch (m_state) { case DragStart: // Make individual item (de)selections... if ((event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier)) == 0) { m_selected_nodes = 0; m_scene->clearSelection(); ++nchanged; } if (m_item) { bool is_selected = true; if (event->modifiers() & Qt::ControlModifier) is_selected = !m_item->isSelected(); m_item->setSelected(is_selected); if (m_item->type() != qpwgraph_node::Type && is_selected) ++m_selected_nodes; m_item = nullptr; // Not needed anymore! ++nchanged; } // Fall thru... case DragMove: // Close new connection line... if (m_connect) { m_connect->setZValue(-1.0); const QPointF& pos = QGraphicsView::mapToScene(event->pos()); qpwgraph_item *item = itemAt(pos); if (item && item->type() == qpwgraph_port::Type) { qpwgraph_port *port1 = m_connect->port1(); qpwgraph_port *port2 = static_cast (item); if (port1 && port2 // && port1->portNode() != port2->portNode() && port1->portMode() != port2->portMode() && port1->portType() == port2->portType() && port1->findConnect(port2) == nullptr) { port2->setSelected(true); #if 1 // Sure the sect will commit to this instead...? m_connect->setPort2(port2); m_connect->updatePortTypeColors(); m_connect->updatePathTo(port2->portPos()); emit connected(m_connect); m_connect = nullptr; ++m_selected_nodes; #else // m_selected_nodes = 0; // m_scene->clearSelection(); #endif // Submit command; notify eventual observers... m_commands->beginMacro(tr("Connect")); connectPorts(port1, port2, true); m_commands->endMacro(); ++nchanged; } } // Done with the hovering connection... if (m_connect) { m_connect->disconnect(); delete m_connect; m_connect = nullptr; } } // Maybe some node(s) were moved... if (m_item && m_item->type() == qpwgraph_node::Type) { const QPointF& pos = QGraphicsView::mapToScene(event->pos()); QList nodes; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) nodes.append(node); } } m_commands->push( new qpwgraph_move_command(this, nodes, m_pos1, pos)); } // Close rubber-band lasso... if (m_rubberband) { delete m_rubberband; m_rubberband = nullptr; m_selected.clear(); // Zooming in range?... if (m_zoomrange) { const QRectF range_rect(m_pos, QGraphicsView::mapToScene(event->pos())); zoomFitRange(range_rect); nchanged = 0; } } break; case DragScroll: default: break; } #if 1//NEW_DRAG_SCROLL_MODE if (QGraphicsView::dragMode() == ScrollHandDrag) { QGraphicsView::mouseReleaseEvent(event); QGraphicsView::setDragMode(NoDrag); } #endif m_state = DragNone; m_item = nullptr; // Reset cursor... QGraphicsView::setCursor(Qt::ArrowCursor); if (nchanged > 0) emit changed(); } void qpwgraph_canvas::mouseDoubleClickEvent ( QMouseEvent *event ) { m_pos = QGraphicsView::mapToScene(event->pos()); m_item = itemAt(m_pos); if (m_item && canRenameItem()) { renameItem(); } else { QGraphicsView::centerOn(m_pos); } } void qpwgraph_canvas::wheelEvent ( QWheelEvent *event ) { if (event->modifiers() & Qt::ControlModifier) { const int delta #if QT_VERSION < 0x050000 = event->delta(); #else = event->angleDelta().y(); #endif setZoom(zoom() + qreal(delta) / 1200.0); } else QGraphicsView::wheelEvent(event); } // Keyboard event handler. void qpwgraph_canvas::keyPressEvent ( QKeyEvent *event ) { if (event->key() == Qt::Key_Escape) { m_scene->clearSelection(); clear(); emit changed(); } } // Connect selected items. void qpwgraph_canvas::connectItems (void) { QList outs; QList ins; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) { if (port->isOutput()) outs.append(port); else ins.append(port); } } } if (outs.isEmpty() || ins.isEmpty()) return; // m_selected_nodes = 0; // m_scene->clearSelection(); std::sort(outs.begin(), outs.end(), qpwgraph_port::ComparePos()); std::sort(ins.begin(), ins.end(), qpwgraph_port::ComparePos()); QListIterator iter1(outs); QListIterator iter2(ins); m_commands->beginMacro(tr("Connect")); const int nports = qMax(outs.count(), ins.count()); for (int n = 0; n < nports; ++n) { // Wrap a'round... if (!iter1.hasNext()) iter1.toFront(); if (!iter2.hasNext()) iter2.toFront(); // Submit command; notify eventual observers... qpwgraph_port *port1 = iter1.next(); qpwgraph_port *port2 = iter2.next(); // Skip over non-matching port-types... bool wrapped = false; while (port1 && port2 && port1->portType() != port2->portType()) { if (!iter2.hasNext()) { if (wrapped) break; iter2.toFront(); wrapped = true; } port2 = iter2.next(); } // Submit command; notify eventual observers... if (!wrapped && port1 && port2 && port1->portNode() != port2->portNode()) connectPorts(port1, port2, true); } m_commands->endMacro(); } // Disconnect selected items. void qpwgraph_canvas::disconnectItems (void) { QList connects; QList nodes; foreach (QGraphicsItem *item, m_scene->selectedItems()) { switch (item->type()) { case qpwgraph_connect::Type: { qpwgraph_connect *connect = static_cast (item); if (!connects.contains(connect)) connects.append(connect); break; } case qpwgraph_node::Type: nodes.append(static_cast (item)); // Fall thru... default: break; } } if (connects.isEmpty()) { foreach (qpwgraph_node *node, nodes) { foreach (qpwgraph_port *port, node->ports()) { foreach (qpwgraph_connect *connect, port->connects()) { if (!connects.contains(connect)) connects.append(connect); } } } } if (connects.isEmpty()) return; // m_selected_nodes = 0; // m_scene->clearSelection(); m_item = nullptr; m_commands->beginMacro(tr("Disconnect")); foreach (qpwgraph_connect *connect, connects) { // Submit command; notify eventual observers... qpwgraph_port *port1 = connect->port1(); qpwgraph_port *port2 = connect->port2(); if (port1 && port2) connectPorts(port1, port2, false); } m_commands->endMacro(); } // Select actions. void qpwgraph_canvas::selectAll (void) { foreach (QGraphicsItem *item, m_scene->items()) { if (item->type() == qpwgraph_node::Type) item->setSelected(true); else ++m_selected_nodes; } emit changed(); } void qpwgraph_canvas::selectNone (void) { m_selected_nodes = 0; m_scene->clearSelection(); emit changed(); } void qpwgraph_canvas::selectInvert (void) { foreach (QGraphicsItem *item, m_scene->items()) { if (item->type() == qpwgraph_node::Type) item->setSelected(!item->isSelected()); else ++m_selected_nodes; } emit changed(); } // Edit actions. void qpwgraph_canvas::renameItem (void) { qpwgraph_item *item = currentItem(); if (item && item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) { QPalette pal; const QColor& foreground = node->foreground(); QColor background = node->background(); const bool is_dark = (background.value() < 192); pal.setColor(QPalette::Text, is_dark ? foreground.lighter() : foreground.darker()); background.setAlpha(255); pal.setColor(QPalette::Base, background); m_editor->setPalette(pal); QFont font = m_editor->font(); font.setBold(true); m_editor->setFont(font); m_editor->setPlaceholderText(node->nodeName()); m_editor->setText(node->nodeTitle()); } } else if (item && item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) { QPalette pal; const QColor& foreground = port->foreground(); const QColor& background = port->background(); const bool is_dark = (background.value() < 128); pal.setColor(QPalette::Text, is_dark ? foreground.lighter() : foreground.darker()); pal.setColor(QPalette::Base, background.lighter()); m_editor->setPalette(pal); QFont font = m_editor->font(); font.setBold(false); m_editor->setFont(font); m_editor->setPlaceholderText(port->portName()); m_editor->setText(port->portTitle()); } } else return; m_selected_nodes = 0; m_scene->clearSelection(); m_editor->show(); m_editor->setEnabled(true); m_editor->selectAll(); m_editor->setFocus(); m_edited = 0; m_edit_item = item; updateEditorGeometry(); } // Renaming editor position and size updater. void qpwgraph_canvas::updateEditorGeometry (void) { if (m_edit_item && m_editor->isEnabled() && m_editor->isVisible()) { const QRectF& rect = m_edit_item->editorRect().adjusted(+2.0, +2.0, -2.0, -2.0); const QPoint& pos1 = QGraphicsView::mapFromScene(rect.topLeft()); const QPoint& pos2 = QGraphicsView::mapFromScene(rect.bottomRight()); m_editor->setGeometry( pos1.x(), pos1.y(), pos2.x() - pos1.x(), pos2.y() - pos1.y()); } } // Discrete zooming actions. void qpwgraph_canvas::zoomIn (void) { setZoom(zoom() + 0.1); } void qpwgraph_canvas::zoomOut (void) { setZoom(zoom() - 0.1); } void qpwgraph_canvas::zoomFit (void) { zoomFitRange(m_scene->itemsBoundingRect()); } void qpwgraph_canvas::zoomReset (void) { setZoom(1.0); } // Update all nodes. void qpwgraph_canvas::updateNodes (void) { foreach (QGraphicsItem *item, m_scene->items()) { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) node->updatePath(); } } } // Update all connectors. void qpwgraph_canvas::updateConnects (void) { foreach (QGraphicsItem *item, m_scene->items()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect) connect->updatePath(); } } } // Zoom in rectangle range. void qpwgraph_canvas::zoomFitRange ( const QRectF& range_rect ) { QGraphicsView::fitInView( range_rect, Qt::KeepAspectRatio); const QTransform& transform = QGraphicsView::transform(); if (transform.isScaling()) { qreal zoom = transform.m11(); if (zoom < 0.1) { const qreal scale = 0.1 / zoom; QGraphicsView::scale(scale, scale); zoom = 0.1; } else if (zoom > 2.0) { const qreal scale = 2.0 / zoom; QGraphicsView::scale(scale, scale); zoom = 2.0; } m_zoom = zoom; } emit changed(); } // Graph node/port state methods. bool qpwgraph_canvas::restoreNode ( qpwgraph_node *node ) { if (m_settings == nullptr || node == nullptr) return false; // Assume node name-keys have been added before this... // const qpwgraph_node::NodeNameKey name_key(node); const int n = m_node_names.values(name_key).count(); const QString& node_key = nodeKey(node, n); m_settings->beginGroup(NodeAliasesGroup); const QString& node_title = m_settings->value('/' + node_key).toString(); m_settings->endGroup(); if (!node_title.isEmpty()) node->setNodeTitle(node_title); m_settings->beginGroup(NodePosGroup); QPointF node_pos = m_settings->value('/' + node_key).toPointF(); m_settings->endGroup(); if (node_pos.isNull()) return false; boundingPos(node_pos); node->setPos(node_pos); return true; } bool qpwgraph_canvas::saveNode ( qpwgraph_node *node ) const { if (m_settings == nullptr || node == nullptr) return false; // Assume node name-keys are to be removed after this... // const qpwgraph_node::NodeNameKey name_key(node); const int n = m_node_names.values(name_key).count(); if (n < 1) return false; const QString& node_key = nodeKey(node, n); m_settings->beginGroup(NodeAliasesGroup); if (node->nodeNameLabel() != node->nodeTitle()) { m_settings->setValue('/' + node_key, node->nodeTitle()); } else { m_settings->remove('/' + node_key); } m_settings->endGroup(); m_settings->beginGroup(NodePosGroup); m_settings->setValue('/' + node_key, node->pos()); m_settings->endGroup(); return true; } bool qpwgraph_canvas::restorePort ( qpwgraph_port *port ) { if (m_settings == nullptr || port == nullptr) return false; const QString& port_key = portKey(port); m_settings->beginGroup(PortAliasesGroup); const QString& port_title = m_settings->value('/' + port_key).toString(); m_settings->endGroup(); if (port_title.isEmpty()) return false; port->setPortTitle(port_title); return true; } bool qpwgraph_canvas::savePort ( qpwgraph_port *port ) const { if (m_settings == nullptr || port == nullptr) return false; const QString& port_key = portKey(port); m_settings->beginGroup(PortAliasesGroup); if (port->portNameLabel() != port->portTitle()) m_settings->setValue('/' + port_key, port->portTitle()); else m_settings->remove('/' + port_key); m_settings->endGroup(); return true; } bool qpwgraph_canvas::restoreState (void) { if (m_settings == nullptr) return false; #ifdef CONFIG_CLEANUP_NODE_NAMES cleanupNodeNames(NodePosGroup); cleanupNodeNames(NodeAliasesGroup); #endif m_settings->beginGroup(ColorsGroup); const QRegularExpression rx("^0x"); QStringListIterator key(m_settings->childKeys()); while (key.hasNext()) { const QString& sKey = key.next(); const QColor& color = QString(m_settings->value(sKey).toString()); if (color.isValid()) { QString sx(sKey); bool ok = false; const uint port_type = sx.remove(rx).toUInt(&ok, 16); if (ok) m_port_colors.insert(port_type, color); } } m_settings->endGroup(); m_settings->beginGroup(CanvasGroup); const QRectF& rect = m_settings->value(CanvasRectKey).toRectF(); const qreal zoom = m_settings->value(CanvasZoomKey, 1.0).toReal(); m_settings->endGroup(); if (rect.isValid()) m_rect1 = rect; // QGraphicsView::setSceneRect(rect); setZoom(zoom); return true; } bool qpwgraph_canvas::saveState (void) const { if (m_settings == nullptr) return false; QList nodes; const QList items(m_scene->items()); foreach (QGraphicsItem *item, items) { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node && !nodes.contains(node)) { int n = 0; const QList& nodes2 = findNodes(qpwgraph_node::NodeNameKey(node)); foreach (qpwgraph_node *node2, nodes2) { const QString& node2_key = nodeKey(node2, ++n); m_settings->beginGroup(NodePosGroup); m_settings->setValue('/' + node2_key, node2->pos()); m_settings->endGroup(); m_settings->beginGroup(NodeAliasesGroup); if (node2->nodeNameLabel() != node2->nodeTitle()) m_settings->setValue('/' + node2_key, node2->nodeTitle()); else m_settings->remove('/' + node2_key); m_settings->endGroup(); nodes.append(node); } } } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) { const QString& port_key = portKey(port); m_settings->beginGroup(PortAliasesGroup); if (port && port->portNameLabel() != port->portTitle()) m_settings->setValue('/' + port_key, port->portTitle()); else m_settings->remove('/' + port_key); m_settings->endGroup(); } } } m_settings->beginGroup(CanvasGroup); m_settings->setValue(CanvasZoomKey, zoom()); m_settings->setValue(CanvasRectKey, m_rect1); m_settings->endGroup(); m_settings->beginGroup(ColorsGroup); QStringListIterator key(m_settings->childKeys()); while (key.hasNext()) m_settings->remove(key.next()); QHash::ConstIterator iter = m_port_colors.constBegin(); const QHash::ConstIterator& iter_end = m_port_colors.constEnd(); for ( ; iter != iter_end; ++iter) { const uint port_type = iter.key(); const QColor& color = iter.value(); m_settings->setValue("0x" + QString::number(port_type, 16), color.name()); } m_settings->endGroup(); return true; } // Graph node/port key helpers. QString qpwgraph_canvas::nodeKey ( qpwgraph_node *node, int n ) const { QString node_key = node->nodeName(); if (n > 1) { node_key += '_'; node_key += QString::number(n - 1); } switch (node->nodeMode()) { case qpwgraph_item::Input: node_key += ":Input"; break; case qpwgraph_item::Output: node_key += ":Output"; break; default: break; } return node_key; } QString qpwgraph_canvas::portKey ( qpwgraph_port *port ) const { QString port_key; qpwgraph_node *node = port->portNode(); if (node == nullptr) return port_key; port_key += node->nodeName(); port_key += ':'; port_key += port->portName(); switch (port->portMode()) { case qpwgraph_item::Input: port_key += ":Input"; break; case qpwgraph_item::Output: port_key += ":Output"; break; default: break; } return port_key; } void qpwgraph_canvas::addNodeKeys ( qpwgraph_node *node ) { m_node_ids.insert(qpwgraph_node::NodeIdKey(node), node); m_node_names.insert(qpwgraph_node::NodeNameKey(node), node); } void qpwgraph_canvas::removeNodeKeys ( qpwgraph_node *node ) { m_node_names.remove(qpwgraph_node::NodeNameKey(node), node); m_node_ids.remove(qpwgraph_node::NodeIdKey(node), node); } // Graph port colors management. void qpwgraph_canvas::setPortTypeColor ( uint port_type, const QColor& port_color ) { m_port_colors.insert(port_type, port_color); } const QColor& qpwgraph_canvas::portTypeColor ( uint port_type ) { return m_port_colors[port_type]; } void qpwgraph_canvas::updatePortTypeColors ( uint port_type ) { foreach (QGraphicsItem *item, m_scene->items()) { if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port && (0 >= port_type || port->portType() == port_type)) { port->updatePortTypeColors(this); port->update(); } } } } void qpwgraph_canvas::clearPortTypeColors (void) { m_port_colors.clear(); } // Clear all selection. void qpwgraph_canvas::clearSelection (void) { m_item = nullptr; m_selected_nodes = 0; m_scene->clearSelection(); m_edit_item = nullptr; m_editor->setEnabled(false); m_editor->hide(); m_edited = 0; } // Clear all state. void qpwgraph_canvas::clear (void) { m_selected_nodes = 0; if (m_rubberband) { delete m_rubberband; m_rubberband = nullptr; m_selected.clear(); } if (m_connect) { m_connect->disconnect(); delete m_connect; m_connect = nullptr; } if (m_state == DragScroll) QGraphicsView::setDragMode(QGraphicsView::NoDrag); m_state = DragNone; m_item = nullptr; m_edit_item = nullptr; m_editor->setEnabled(false); m_editor->hide(); m_edited = 0; // Reset cursor... QGraphicsView::setCursor(Qt::ArrowCursor); } // Rename item slots. void qpwgraph_canvas::textChanged ( const QString& /* text */) { if (m_edit_item && m_editor->isEnabled() && m_editor->isVisible()) ++m_edited; } void qpwgraph_canvas::editingFinished (void) { if (m_edit_item && m_editor->isEnabled() && m_editor->isVisible()) { // If changed then notify... if (m_edited > 0) { m_commands->push( new qpwgraph_rename_command(this, m_edit_item, m_editor->text())); } // Reset all renaming stuff... m_edit_item = nullptr; m_editor->setEnabled(false); m_editor->hide(); m_edited = 0; } } // Repel overlapping nodes... void qpwgraph_canvas::setRepelOverlappingNodes ( bool on ) { m_repel_overlapping_nodes = on; } bool qpwgraph_canvas::isRepelOverlappingNodes (void) const { return m_repel_overlapping_nodes; } void qpwgraph_canvas::repelOverlappingNodes ( qpwgraph_node *node, qpwgraph_move_command *move_command, const QPointF& delta ) { const qreal MIN_NODE_GAP = 8.0f; node->setMarked(true); QRectF rect1 = node->sceneBoundingRect(); rect1.adjust( -2.0 * MIN_NODE_GAP, -MIN_NODE_GAP, +2.0 * MIN_NODE_GAP, +MIN_NODE_GAP); foreach (qpwgraph_node *node2, m_nodes) { if (node2->isMarked()) continue; const QPointF& pos1 = node2->pos(); QPointF pos2 = pos1; const QRectF& rect2 = node2->sceneBoundingRect(); const QRectF& recti = rect2.intersected(rect1); if (!recti.isNull()) { const QPointF delta2 = (delta.isNull() ? rect2.center() - rect1.center() : delta); if (recti.width() < (1.5 * recti.height())) { qreal dx = recti.width(); if ((delta2.x() < 0.0 && recti.width() >= rect1.width()) || (delta2.x() > 0.0 && recti.width() >= rect2.width())) { dx += qAbs(rect2.right() - rect1.right()); } else if ((delta2.x() > 0.0 && recti.width() >= rect1.width()) || (delta2.x() < 0.0 && recti.width() >= rect2.width())) { dx += qAbs(rect2.left() - rect1.left()); } if (delta2.x() < 0.0) pos2.setX(pos1.x() - dx); else pos2.setX(pos1.x() + dx); } else { qreal dy = recti.height(); if ((delta2.y() < 0.0 && recti.height() >= rect1.height()) || (delta2.y() > 0.0 && recti.height() >= rect2.height())) { dy += qAbs(rect2.bottom() - rect1.bottom()); } else if ((delta2.y() > 0.0 && recti.height() >= rect1.height()) || (delta2.y() < 0.0 && recti.height() >= rect2.height())) { dy += qAbs(rect2.top() - rect1.top()); } if (delta2.y() < 0.0) pos2.setY(pos1.y() - dy); else pos2.setY(pos1.y() + dy); } // Repel this node... node2->setPos(pos2); // Add this node for undo/redo... if (move_command) move_command->addItem(node2, pos1, pos2); // Repel this node neighbors, if any... repelOverlappingNodes(node2, move_command, delta2); } } node->setMarked(false); } void qpwgraph_canvas::repelOverlappingNodesAll ( qpwgraph_move_command *move_command ) { foreach (qpwgraph_node *node, m_nodes) repelOverlappingNodes(node, move_command); } // Gesture event handlers. // bool qpwgraph_canvas::event ( QEvent *event ) { if (event->type() == QEvent::Gesture) return gestureEvent(static_cast (event)); else return QGraphicsView::event(event); } bool qpwgraph_canvas::gestureEvent ( QGestureEvent *event ) { if (QGesture *pinch = event->gesture(Qt::PinchGesture)) pinchGesture(static_cast (pinch)); return true; } void qpwgraph_canvas::pinchGesture ( QPinchGesture *pinch ) { switch (pinch->state()) { case Qt::GestureStarted: { const qreal scale_factor = zoom(); pinch->setScaleFactor(scale_factor); pinch->setLastScaleFactor(scale_factor); pinch->setTotalScaleFactor(scale_factor); m_gesture = true; break; } case Qt::GestureFinished: m_gesture = false; // Fall thru... case Qt::GestureUpdated: if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) setZoom(pinch->totalScaleFactor()); // Fall thru... default: break; } } // Bounding margins/limits... // const QRectF& qpwgraph_canvas::boundingRect ( bool reset ) { if (!m_rect1.isValid() || reset) { const QRect& rect = QGraphicsView::rect(); const qreal mx = 0.33 * rect.width(); const qreal my = 0.33 * rect.height(); m_rect1 = m_scene->itemsBoundingRect() .marginsAdded(QMarginsF(mx, my, mx, my)); } return m_rect1; } void qpwgraph_canvas::boundingPos ( QPointF& pos ) { const QRectF& rect = boundingRect(); if (!rect.contains(pos)) { pos.setX(qBound(rect.left(), pos.x(), rect.right())); pos.setY(qBound(rect.top(), pos.y(), rect.bottom())); } } // Snap into position helpers. // void qpwgraph_canvas::snapPos ( QPointF& pos ) const { pos.setX(4.0 * ::round(0.25 * pos.x())); pos.setY(4.0 * ::round(0.25 * pos.y())); } QPointF qpwgraph_canvas::snapPos ( qreal x, qreal y ) const { QPointF pos(x, y); snapPos(pos); return pos; } #ifdef CONFIG_CLEANUP_NODE_NAMES void qpwgraph_canvas::cleanupNodeNames ( const char *group ) { bool cleanup = false; m_settings->beginGroup("/CleanupNodeNames"); cleanup = m_settings->value(group).toBool(); if (!cleanup) m_settings->setValue(group, true); m_settings->endGroup(); if (cleanup) return; m_settings->beginGroup(group); const QRegularExpression rx("\\-([0-9]+).*$"); QHash keys; QStringListIterator iter(m_settings->childKeys()); while (iter.hasNext()) { const QString& key = iter.next(); const QVariant& value = m_settings->value(key); QString key2 = key; if (cleanupNodeName(key2)) { int n = 0; if (keys.find(key2) != keys.end()) { const QRegularExpressionMatch mx = rx.match(key2); if (mx.hasMatch()) { n = mx.captured(1).toInt(); key2.remove(rx); } QString key3; do { key3 = key2 + '-' + QString::number(++n); } while (keys.find(key3) != keys.end()); key2 = key3; } if (n == 0) { keys.insert(key2, value); m_settings->setValue(key2, value); } m_settings->remove(key); } else { keys.insert(key, value); } } m_settings->endGroup(); } bool qpwgraph_canvas::cleanupNodeName ( QString& name ) { const QRegularExpression rx("^.+( \\[.+\\])[^ ]*$"); const QRegularExpressionMatch& mx = rx.match(name); if (mx.hasMatch()) { name.remove(mx.captured(1)); return true; } else { return false; } } #endif//CONFIG_CLEANUP_NODE_NAMES // end of qpwgraph_canvas.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph.qrc0000644000000000000000000000013214532447230015560 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph.qrc0000644000175000001440000000203714532447230017012 0ustar00rncbcusers00000000000000 images/qpwgraph.png images/qpwgraph.svg images/itemPipewire.png images/itemAlsamidi.png images/itemConnect.png images/itemDisconnect.png images/itemPatchbay.png images/itemActivate.png images/itemExclusive.png images/itemEdit.png images/itemPin.png images/itemUnpin.png images/itemJack.png images/itemPulse.png images/fileNew.png images/fileOpen.png images/fileSave.png images/editUndo.png images/editRedo.png images/viewCenter.png images/viewColors.png images/viewZoomIn.png images/viewZoomOut.png images/viewZoomFit.png images/viewZoomReset.png images/viewZoomRange.png images/viewZoomTool.png qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_patchbay.cpp0000644000000000000000000000013214532447230017430 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_patchbay.cpp0000644000175000001440000003156614532447230020673 0ustar00rncbcusers00000000000000// qpwgraph_patchbay.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "config.h" #include "qpwgraph_patchbay.h" #include "qpwgraph_canvas.h" #include "qpwgraph_connect.h" #include "qpwgraph_port.h" #include "qpwgraph_node.h" #include "qpwgraph_pipewire.h" #include "qpwgraph_alsamidi.h" #include #include #include // Deprecated QTextStreamFunctions/Qt namespaces workaround. #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #define endl Qt::endl #endif //---------------------------------------------------------------------------- // qpwgraph_patchbay -- Persistant connections patchbay impl. // Clear all patchbay rules and cache. void qpwgraph_patchbay::clear (void) { Items::ConstIterator iter = m_items.constBegin(); const Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) delete iter.value(); m_items.clear(); m_dirty = 0; } // Snapshot of all current graph connections... void qpwgraph_patchbay::snap (void) { // clear(); if (m_canvas == nullptr) return; QGraphicsScene *scene = m_canvas->scene(); if (scene == nullptr) return; foreach (QGraphicsItem *item, scene->items()) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect) { qpwgraph_port *port1 = connect->port1(); qpwgraph_port *port2 = connect->port2(); if (port1 && port2) { qpwgraph_node *node1 = port1->portNode(); qpwgraph_node *node2 = port2->portNode(); if (node1 && node2) { addItem(new Item( node1->nodeType(), port1->portType(), node1->nodeName(), port1->portName(), node2->nodeName(), port2->portName())); } } } } } } // Patchbay rules file I/O methods. bool qpwgraph_patchbay::load ( const QString& filename ) { // Open file... QFile file(filename); if (!file.open(QIODevice::ReadOnly)) return false; QDomDocument doc("patchbay"); if (!doc.setContent(&file)) { file.close(); return false; } file.close(); // clear(); QDomElement edoc = doc.documentElement(); for (QDomNode nroot = edoc.firstChild(); !nroot.isNull(); nroot = nroot.nextSibling()) { QDomElement eroot = nroot.toElement(); if (eroot.isNull()) continue; #ifdef CONFIG_CLEANUP_NODE_NAMES const bool cleanup = (eroot.attribute("version") < "0.5.0"); #endif if (eroot.tagName() == "items") { for (QDomNode nitem = eroot.firstChild(); !nitem.isNull(); nitem = nitem.nextSibling()) { QDomElement eitem = nitem.toElement(); if (eitem.isNull()) continue; if (eitem.tagName() == "item") { const uint node_type = nodeTypeFromText(eitem.attribute("node-type")); const uint port_type = portTypeFromText(eitem.attribute("port-type")); QString node1, port1, node2, port2; for (QDomNode nitem2 = eitem.firstChild(); !nitem2.isNull(); nitem2 = nitem2.nextSibling()) { QDomElement eitem2 = nitem2.toElement(); if (eitem2.isNull()) continue; if (eitem2.tagName() == "output") { node1 = eitem2.attribute("node"); port1 = eitem2.attribute("port"); } else if (eitem2.tagName() == "input") { node2 = eitem2.attribute("node"); port2 = eitem2.attribute("port"); } } #ifdef CONFIG_CLEANUP_NODE_NAMES if (cleanup) { // FIXME: Cleanup legacy node names... if (qpwgraph_canvas::cleanupNodeName(node1)) ++m_dirty; if (qpwgraph_canvas::cleanupNodeName(node2)) ++m_dirty; } #endif if (node_type > 0 && port_type > 0 && !node1.isEmpty() && !port1.isEmpty() && !node2.isEmpty() && !port2.isEmpty()) { addItem(new Item( node_type, port_type, node1, port1, node2, port2)); } } } } } return true; } bool qpwgraph_patchbay::save ( const QString& filename ) { if (m_canvas == nullptr) return false; QFileInfo fi(filename); const char *root_name = "patchbay"; QDomDocument doc(root_name); QDomElement eroot = doc.createElement(root_name); eroot.setAttribute("name", fi.baseName()); eroot.setAttribute("version", PROJECT_VERSION); doc.appendChild(eroot); QDomElement eitems = doc.createElement("items"); Items::ConstIterator iter = m_items.constBegin(); const Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) { Item *item = iter.value(); QDomElement eitem = doc.createElement("item"); eitem.setAttribute("node-type", textFromNodeType(item->node_type)); eitem.setAttribute("port-type", textFromPortType(item->port_type)); QDomElement eitem1 = doc.createElement("output"); eitem1.setAttribute("node", item->node1); eitem1.setAttribute("port", item->port1); eitem.appendChild(eitem1); QDomElement eitem2 = doc.createElement("input"); eitem2.setAttribute("node", item->node2); eitem2.setAttribute("port", item->port2); eitem.appendChild(eitem2); eitems.appendChild(eitem); } eroot.appendChild(eitems); QFile file(filename); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false; QTextStream ts(&file); ts << doc.toString() << endl; file.close(); m_dirty = 0; return true; } // Execute and apply rules to graph. bool qpwgraph_patchbay::scan (void) { if (m_canvas == nullptr) return false; QGraphicsScene *scene = m_canvas->scene(); if (scene == nullptr) return false; QHash disconnects; Items::ConstIterator iter = m_items.constBegin(); const Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) { Item *item = iter.value(); QList nodes1 = m_canvas->findNodes( item->node1, qpwgraph_item::Output, item->node_type); if (nodes1.isEmpty()) nodes1 = m_canvas->findNodes( item->node1, qpwgraph_item::Duplex, item->node_type); if (nodes1.isEmpty()) continue; foreach (qpwgraph_node *node1, nodes1) { qpwgraph_port *port1 = node1->findPort( item->port1, qpwgraph_item::Output, item->port_type); if (port1 == nullptr) continue; QList nodes2 = m_canvas->findNodes( item->node2, qpwgraph_item::Input, item->node_type); if (nodes2.isEmpty()) nodes2 = m_canvas->findNodes( item->node2, qpwgraph_item::Duplex, item->node_type); if (nodes2.isEmpty()) continue; foreach (qpwgraph_node *node2, nodes2) { qpwgraph_port *port2 = node2->findPort( item->port2, qpwgraph_item::Input, item->port_type); if (port2 == nullptr) continue; if (m_activated && m_exclusive) { foreach (qpwgraph_connect *connect12, port1->connects()) { qpwgraph_port *port12 = connect12->port2(); if (port12 == nullptr) continue; if (port12 != port2) { qpwgraph_node *node12 = port12->portNode(); if (node12 == nullptr) continue; const Item item12( node1->nodeType(), port1->portType(), node1->nodeName(), port1->portName(), node12->nodeName(), port12->portName()); if (m_items.constFind(item12) == iter_end) disconnects.insert(item12, connect12); } } foreach (qpwgraph_connect *connect21, port2->connects()) { qpwgraph_port *port21 = connect21->port1(); if (port21 == nullptr) continue; if (port21 != port1) { qpwgraph_node *node21 = port21->portNode(); if (node21 == nullptr) continue; const Item item21( node21->nodeType(), port21->portType(), node21->nodeName(), port21->portName(), node2->nodeName(), port2->portName()); if (m_items.constFind(item21) == iter_end) { disconnects.insert(item21, connect21); } } } } qpwgraph_connect *connect12 = port1->findConnect(port2); if (connect12 == nullptr && m_activated) m_canvas->emitConnected(port1, port2); else if (!m_activated && m_canvas->isPatchbayAutoDisconnect()) { const Item item12( node1->nodeType(), port1->portType(), node1->nodeName(), port1->portName(), node2->nodeName(), port2->portName()); disconnects.insert(item12, connect12); } } } } QHash::ConstIterator iter2 = disconnects.constBegin(); const QHash::ConstIterator& iter2_end = disconnects.constEnd(); for (; iter2 != iter2_end; ++iter2) { qpwgraph_connect *connect = iter2.value(); if (connect) m_canvas->emitDisconnected(connect->port1(), connect->port2()); } return true; } // Update rules on demand. bool qpwgraph_patchbay::connectPorts ( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect ) { if (port1 == nullptr || port2 == nullptr) return false; bool ret = false; qpwgraph_node *node1 = port1->portNode(); qpwgraph_node *node2 = port2->portNode(); if (node1 && node2) { const Item item( node1->nodeType(), port1->portType(), node1->nodeName(), port1->portName(), node2->nodeName(), port2->portName()); Items::ConstIterator iter = m_items.constFind(item); const Items::ConstIterator& iter_end = m_items.constEnd(); if (iter == iter_end && is_connect) { m_items.insert(item, new Item(item)); ret = true; } else if (iter != iter_end && !is_connect) { delete iter.value(); m_items.erase(iter); ret = true; } } if (ret) ++m_dirty; return ret; } bool qpwgraph_patchbay::connect ( qpwgraph_connect *connect, bool is_connect ) { return connectPorts(connect->port1(), connect->port2(), is_connect); } // Find a connection rule. qpwgraph_patchbay::Item *qpwgraph_patchbay::findConnectPorts ( qpwgraph_port *port1, qpwgraph_port *port2 ) const { Item *ret = nullptr; if (port1 && port2) { qpwgraph_node *node1 = port1->portNode(); qpwgraph_node *node2 = port2->portNode(); if (node1 && node2) { const Item item( node1->nodeType(), port1->portType(), node1->nodeName(), port1->portName(), node2->nodeName(), port2->portName()); ret = m_items.value(item, nullptr); } } return ret; } qpwgraph_patchbay::Item *qpwgraph_patchbay::findConnect ( qpwgraph_connect *connect ) const { return findConnectPorts(connect->port1(), connect->port2()); } // Add a new patchbay rule item. void qpwgraph_patchbay::addItem ( Item *item ) { Item *item2 = m_items.value(*item, nullptr); if (item2) delete item2; m_items.insert(*item, item); } // Node and port type to text helpers. uint qpwgraph_patchbay::nodeTypeFromText ( const QString& text ) { if (text == "pipewire") return qpwgraph_pipewire::nodeType(); else #ifdef CONFIG_ALSA_MIDI if (text == "alsa") return qpwgraph_alsamidi::nodeType(); else #endif return 0; } const char *qpwgraph_patchbay::textFromNodeType ( uint node_type ) { if (node_type == qpwgraph_pipewire::nodeType()) return "pipewire"; else #ifdef CONFIG_ALSA_MIDI if (node_type == qpwgraph_alsamidi::nodeType()) return "alsa"; else #endif return nullptr; } uint qpwgraph_patchbay::portTypeFromText ( const QString& text ) { if (text == "pipewire-audio") return qpwgraph_pipewire::audioPortType(); else if (text == "pipewire-midi") return qpwgraph_pipewire::midiPortType(); else if (text == "pipewire-video") return qpwgraph_pipewire::videoPortType(); else if (text == "pipewire-other") return qpwgraph_pipewire::otherPortType(); else #ifdef CONFIG_ALSA_MIDI if (text == "alsa-midi") return qpwgraph_alsamidi::midiPortType(); else #endif return 0; } const char *qpwgraph_patchbay::textFromPortType ( uint port_type ) { if (port_type == qpwgraph_pipewire::audioPortType()) return "pipewire-audio"; else if (port_type == qpwgraph_pipewire::midiPortType()) return "pipewire-midi"; else if (port_type == qpwgraph_pipewire::videoPortType()) return "pipewire-video"; else if (port_type == qpwgraph_pipewire::otherPortType()) return "pipewire-other"; else #ifdef CONFIG_ALSA_MIDI if (port_type == qpwgraph_alsamidi::midiPortType()) return "alsa-midi"; else #endif return nullptr; } // end of qpwgraph_patchbay.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_form.cpp0000644000000000000000000000013214532447230016600 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_form.cpp0000644000175000001440000012573114532447230020041 0ustar00rncbcusers00000000000000// qpwgraph_form.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph.h" #include "qpwgraph_form.h" #include "qpwgraph_config.h" #include "qpwgraph_pipewire.h" #include "qpwgraph_alsamidi.h" #include "qpwgraph_connect.h" #include "qpwgraph_patchbay.h" #include "qpwgraph_systray.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //---------------------------------------------------------------------------- // qpwgraph_zoom_slider -- Custom slider widget. #include class qpwgraph_zoom_slider : public QSlider { public: qpwgraph_zoom_slider() : QSlider(Qt::Horizontal) { QSlider::setMinimum(10); QSlider::setMaximum(190); QSlider::setTickInterval(90); QSlider::setTickPosition(QSlider::TicksBothSides); } protected: void mousePressEvent(QMouseEvent *ev) { QSlider::mousePressEvent(ev); if (ev->button() == Qt::MiddleButton) QSlider::setValue(100); } }; //---------------------------------------------------------------------------- // qpwgraph_form -- UI wrapper form. // Constructor. qpwgraph_form::qpwgraph_form ( QWidget *parent, Qt::WindowFlags wflags ) : QMainWindow(parent, wflags) { // Setup UI struct... m_ui.setupUi(this); #if QT_VERSION < QT_VERSION_CHECK(6, 1, 0) QMainWindow::setWindowIcon(QIcon(":/images/qpwgraph.png")); #endif m_config = new qpwgraph_config("rncbc.org", "qpwgraph"); m_ui.graphCanvas->setSettings(m_config->settings()); m_pipewire = new qpwgraph_pipewire(m_ui.graphCanvas); m_alsamidi = nullptr; m_pipewire_changed = 0; m_alsamidi_changed = 0; m_ins = m_mids = m_outs = 0; m_repel_overlapping_nodes = 0; m_patchbay_names = new QComboBox(m_ui.patchbayToolbar); m_patchbay_names->setEditable(false); m_patchbay_names->setMinimumWidth(120); m_patchbay_names->setMaximumWidth(240); m_patchbay_names_tool = m_ui.patchbayToolbar->insertWidget( m_ui.patchbaySaveAction, m_patchbay_names); m_systray = nullptr; m_systray_closed = false; QUndoStack *commands = m_ui.graphCanvas->commands(); QAction *undo_action = commands->createUndoAction(this, tr("&Undo")); undo_action->setIcon(QIcon(":/images/editUndo.png")); undo_action->setStatusTip(tr("Undo last edit action")); undo_action->setShortcuts(QKeySequence::Undo); QAction *redo_action = commands->createRedoAction(this, tr("&Redo")); redo_action->setIcon(QIcon(":/images/editRedo.png")); redo_action->setStatusTip(tr("Redo last edit action")); redo_action->setShortcuts(QKeySequence::Redo); QAction *before = m_ui.editSelectAllAction; m_ui.editMenu->insertAction(before, undo_action); m_ui.editMenu->insertAction(before, redo_action); m_ui.editMenu->insertSeparator(before); before = m_ui.viewCenterAction; m_ui.graphToolbar->insertAction(before, undo_action); m_ui.graphToolbar->insertAction(before, redo_action); m_ui.graphToolbar->insertSeparator(before); // Special zoom composite widget... QWidget *zoom_widget = new QWidget(); zoom_widget->setMaximumWidth(240); zoom_widget->setToolTip(tr("Zoom")); QHBoxLayout *zoom_layout = new QHBoxLayout(); zoom_layout->setContentsMargins(0, 0, 0, 0); zoom_layout->setSpacing(2); QToolButton *zoom_out = new QToolButton(); zoom_out->setDefaultAction(m_ui.viewZoomOutAction); zoom_out->setFixedSize(22, 22); zoom_layout->addWidget(zoom_out); m_zoom_slider = new qpwgraph_zoom_slider(); m_zoom_slider->setFixedHeight(22); zoom_layout->addWidget(m_zoom_slider); QToolButton *zoom_in = new QToolButton(); zoom_in->setDefaultAction(m_ui.viewZoomInAction); zoom_in->setFixedSize(22, 22); zoom_layout->addWidget(zoom_in); m_zoom_spinbox = new QSpinBox(); m_zoom_spinbox->setFixedHeight(22); m_zoom_spinbox->setAlignment(Qt::AlignCenter); m_zoom_spinbox->setMinimum(10); m_zoom_spinbox->setMaximum(200); m_zoom_spinbox->setSuffix(" %"); zoom_layout->addWidget(m_zoom_spinbox); zoom_widget->setLayout(zoom_layout); m_ui.StatusBar->addPermanentWidget(zoom_widget); QObject::connect(m_patchbay_names, SIGNAL(activated(int)), SLOT(patchbayNameChanged(int))); QObject::connect(m_zoom_spinbox, SIGNAL(valueChanged(int)), SLOT(zoomValueChanged(int))); QObject::connect(m_zoom_slider, SIGNAL(valueChanged(int)), SLOT(zoomValueChanged(int))); if (m_pipewire) { QObject::connect(m_pipewire, SIGNAL(changed()), SLOT(pipewire_changed())); } QObject::connect(m_ui.graphCanvas, SIGNAL(added(qpwgraph_node *)), SLOT(added(qpwgraph_node *))); QObject::connect(m_ui.graphCanvas, SIGNAL(updated(qpwgraph_node *)), SLOT(updated(qpwgraph_node *))); QObject::connect(m_ui.graphCanvas, SIGNAL(removed(qpwgraph_node *)), SLOT(removed(qpwgraph_node *))); QObject::connect(m_ui.graphCanvas, SIGNAL(connected(qpwgraph_port *, qpwgraph_port *)), SLOT(connected(qpwgraph_port *, qpwgraph_port *))); QObject::connect(m_ui.graphCanvas, SIGNAL(disconnected(qpwgraph_port *, qpwgraph_port *)), SLOT(disconnected(qpwgraph_port *, qpwgraph_port *))); QObject::connect(m_ui.graphCanvas, SIGNAL(connected(qpwgraph_connect *)), SLOT(connected(qpwgraph_connect *))); QObject::connect(m_ui.graphCanvas, SIGNAL(renamed(qpwgraph_item *, const QString&)), SLOT(renamed(qpwgraph_item *, const QString&))); QObject::connect(m_ui.graphCanvas, SIGNAL(changed()), SLOT(stabilize())); // Some actions surely need those // shortcuts firmly attached... addAction(m_ui.viewMenubarAction); // HACK: Make old Ins/Del standard shortcuts // for connect/disconnect available again... QList shortcuts; shortcuts.append(m_ui.graphConnectAction->shortcut()); shortcuts.append(QKeySequence("Ins")); m_ui.graphConnectAction->setShortcuts(shortcuts); shortcuts.clear(); shortcuts.append(m_ui.graphDisconnectAction->shortcut()); shortcuts.append(QKeySequence("Del")); m_ui.graphDisconnectAction->setShortcuts(shortcuts); QObject::connect(m_ui.graphConnectAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(connectItems())); QObject::connect(m_ui.graphDisconnectAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(disconnectItems())); QObject::connect(m_ui.patchbayMenu, SIGNAL(aboutToShow()), SLOT(updatePatchbayMenu())); QObject::connect(m_ui.patchbayNewAction, SIGNAL(triggered(bool)), SLOT(patchbayNew())); QObject::connect(m_ui.patchbayOpenAction, SIGNAL(triggered(bool)), SLOT(patchbayOpen())); QObject::connect(m_ui.patchbaySaveAction, SIGNAL(triggered(bool)), SLOT(patchbaySave())); QObject::connect(m_ui.patchbaySaveAsAction, SIGNAL(triggered(bool)), SLOT(patchbaySaveAs())); QObject::connect(m_ui.patchbayActivatedAction, SIGNAL(toggled(bool)), SLOT(patchbayActivated(bool))); QObject::connect(m_ui.patchbayExclusiveAction, SIGNAL(toggled(bool)), SLOT(patchbayExclusive(bool))); QObject::connect(m_ui.patchbayAutoPinAction, SIGNAL(toggled(bool)), SLOT(patchbayAutoPin(bool))); QObject::connect(m_ui.patchbayAutoDisconnectAction, SIGNAL(toggled(bool)), SLOT(patchbayAutoDisconnect(bool))); QObject::connect(m_ui.patchbayEditAction, SIGNAL(toggled(bool)), SLOT(patchbayEdit(bool))); QObject::connect(m_ui.patchbayPinAction, SIGNAL(triggered(bool)), SLOT(patchbayPin())); QObject::connect(m_ui.patchbayUnpinAction, SIGNAL(triggered(bool)), SLOT(patchbayUnpin())); QObject::connect(m_ui.graphQuitAction, SIGNAL(triggered(bool)), SLOT(closeQuit())); QObject::connect(m_ui.editSelectAllAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(selectAll())); QObject::connect(m_ui.editSelectNoneAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(selectNone())); QObject::connect(m_ui.editSelectInvertAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(selectInvert())); QObject::connect(m_ui.editRenameItemAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(renameItem())); QObject::connect(m_ui.viewMenubarAction, SIGNAL(triggered(bool)), SLOT(viewMenubar(bool))); QObject::connect(m_ui.viewStatusbarAction, SIGNAL(triggered(bool)), SLOT(viewStatusbar(bool))); QObject::connect(m_ui.viewGraphToolbarAction, SIGNAL(triggered(bool)), SLOT(viewGraphToolbar(bool))); QObject::connect(m_ui.viewPatchbayToolbarAction, SIGNAL(triggered(bool)), SLOT(viewPatchbayToolbar(bool))); QObject::connect(m_ui.viewTextBesideIconsAction, SIGNAL(triggered(bool)), SLOT(viewTextBesideIcons(bool))); QObject::connect(m_ui.viewCenterAction, SIGNAL(triggered(bool)), SLOT(viewCenter())); QObject::connect(m_ui.viewRefreshAction, SIGNAL(triggered(bool)), SLOT(viewRefresh())); QObject::connect(m_ui.viewZoomInAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(zoomIn())); QObject::connect(m_ui.viewZoomOutAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(zoomOut())); QObject::connect(m_ui.viewZoomFitAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(zoomFit())); QObject::connect(m_ui.viewZoomResetAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(zoomReset())); QObject::connect(m_ui.viewZoomRangeAction, SIGNAL(triggered(bool)), SLOT(viewZoomRange(bool))); QObject::connect(m_ui.viewRepelOverlappingNodesAction, SIGNAL(triggered(bool)), SLOT(viewRepelOverlappingNodes(bool))); QObject::connect(m_ui.viewConnectThroughNodesAction, SIGNAL(triggered(bool)), SLOT(viewConnectThroughNodes(bool))); m_ui.viewColorsPipewireAudioAction->setData(qpwgraph_pipewire::audioPortType()); m_ui.viewColorsPipewireMidiAction->setData(qpwgraph_pipewire::midiPortType()); m_ui.viewColorsPipewireVideoAction->setData(qpwgraph_pipewire::videoPortType()); m_ui.viewColorsPipewireOtherAction->setData(qpwgraph_pipewire::otherPortType()); #ifdef CONFIG_ALSA_MIDI m_ui.viewColorsAlsaMidiAction->setData(qpwgraph_alsamidi::midiPortType()); m_ui.viewColorsMenu->insertAction( m_ui.viewColorsResetAction, m_ui.viewColorsAlsaMidiAction); m_ui.viewColorsMenu->insertSeparator( m_ui.viewColorsResetAction); #endif QObject::connect(m_ui.viewColorsPipewireAudioAction, SIGNAL(triggered(bool)), SLOT(viewColorsAction())); QObject::connect(m_ui.viewColorsPipewireMidiAction, SIGNAL(triggered(bool)), SLOT(viewColorsAction())); QObject::connect(m_ui.viewColorsPipewireVideoAction, SIGNAL(triggered(bool)), SLOT(viewColorsAction())); QObject::connect(m_ui.viewColorsPipewireOtherAction, SIGNAL(triggered(bool)), SLOT(viewColorsAction())); #ifdef CONFIG_ALSA_MIDI QObject::connect(m_ui.viewColorsAlsaMidiAction, SIGNAL(triggered(bool)), SLOT(viewColorsAction())); #endif QObject::connect(m_ui.viewColorsResetAction, SIGNAL(triggered(bool)), SLOT(viewColorsReset())); m_sort_type = new QActionGroup(this); m_sort_type->setExclusive(true); m_sort_type->addAction(m_ui.viewSortPortNameAction); m_sort_type->addAction(m_ui.viewSortPortTitleAction); m_sort_type->addAction(m_ui.viewSortPortIndexAction); m_ui.viewSortPortNameAction->setData(qpwgraph_port::PortName); m_ui.viewSortPortTitleAction->setData(qpwgraph_port::PortTitle); m_ui.viewSortPortIndexAction->setData(qpwgraph_port::PortIndex); QObject::connect(m_ui.viewSortPortNameAction, SIGNAL(triggered(bool)), SLOT(viewSortTypeAction())); QObject::connect(m_ui.viewSortPortTitleAction, SIGNAL(triggered(bool)), SLOT(viewSortTypeAction())); QObject::connect(m_ui.viewSortPortIndexAction, SIGNAL(triggered(bool)), SLOT(viewSortTypeAction())); m_sort_order = new QActionGroup(this); m_sort_order->setExclusive(true); m_sort_order->addAction(m_ui.viewSortAscendingAction); m_sort_order->addAction(m_ui.viewSortDescendingAction); m_ui.viewSortAscendingAction->setData(qpwgraph_port::Ascending); m_ui.viewSortDescendingAction->setData(qpwgraph_port::Descending); QObject::connect(m_ui.viewSortAscendingAction, SIGNAL(triggered(bool)), SLOT(viewSortOrderAction())); QObject::connect(m_ui.viewSortDescendingAction, SIGNAL(triggered(bool)), SLOT(viewSortOrderAction())); #ifdef CONFIG_SYSTEM_TRAY m_ui.helpMenu->insertAction( m_ui.helpAboutAction, m_ui.helpSystemTrayAction); #ifndef CONFIG_ALSA_MIDI m_ui.helpMenu->insertSeparator( m_ui.helpAboutAction); #endif QObject::connect(m_ui.helpSystemTrayAction, SIGNAL(triggered(bool)), SLOT(helpSystemTray(bool))); #endif #ifdef CONFIG_ALSA_MIDI m_ui.helpMenu->insertAction( m_ui.helpAboutAction, m_ui.helpAlsaMidiAction); m_ui.helpMenu->insertSeparator( m_ui.helpAboutAction); QObject::connect(m_ui.helpAlsaMidiAction, SIGNAL(triggered(bool)), SLOT(helpAlsaMidi(bool))); #endif QObject::connect(m_ui.helpAboutAction, SIGNAL(triggered(bool)), SLOT(helpAbout())); QObject::connect(m_ui.helpAboutQtAction, SIGNAL(triggered(bool)), SLOT(helpAboutQt())); QObject::connect(m_ui.graphToolbar, SIGNAL(orientationChanged(Qt::Orientation)), SLOT(orientationChanged(Qt::Orientation))); QObject::connect(m_ui.patchbayToolbar, SIGNAL(orientationChanged(Qt::Orientation)), SLOT(orientationChanged(Qt::Orientation))); restoreState(); updatePatchbayMenu(); updateViewColors(); // Restore last open patchbay file... m_patchbay_untitled = 0; const QString path(m_patchbay_path); if (!path.isEmpty() && patchbayOpenFile(path)) { --m_patchbay_untitled; } else { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) patchbay->snap(); // Simulate patchbayNew()! } updatePatchbayNames(); // Make it ready :-) m_ui.StatusBar->showMessage(tr("Ready"), 3000); // Trigger refresh cycle... pipewire_changed(); alsamidi_changed(); QTimer::singleShot(300, this, SLOT(refresh())); } // Destructor. qpwgraph_form::~qpwgraph_form (void) { #ifdef CONFIG_SYSTEM_TRAY if (m_systray) delete m_systray; #endif // delete m_patchbay_names; delete m_sort_order; delete m_sort_type; if (m_pipewire) delete m_pipewire; #ifdef CONFIG_ALSA_MIDI if (m_alsamidi) delete m_alsamidi; #endif delete m_config; } // Take care of command line options and arguments... void qpwgraph_form::apply_args ( qpwgraph_application *app ) { if (app->isPatchbayActivatedSet()) m_ui.patchbayActivatedAction->setChecked(app->isPatchbayActivated()); if (app->isPatchbayExclusiveSet()) m_ui.patchbayExclusiveAction->setChecked(app->isPatchbayExclusive()); if (!app->patchbayPath().isEmpty()) patchbayOpenFile(app->patchbayPath()); updatePatchbayNames(); bool start_minimized = app->isStartMinimized(); if (!start_minimized && app->isSessionRestored()) start_minimized = m_config->isSessionStartMinimized(); if (start_minimized) { #ifdef CONFIG_SYSTEM_TRAY if (m_systray) { hide(); m_systray_closed = true; } else { showMinimized(); } #else showMinimized(); #endif } else { show(); } } // Patchbay menu slots. void qpwgraph_form::patchbayNew (void) { if (!patchbayQueryClose()) return; qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) { patchbay->clear(); patchbay->snap(); } m_patchbay_path.clear(); ++m_patchbay_untitled; m_ui.graphCanvas->patchbayEdit(); updatePatchbayNames(); } void qpwgraph_form::patchbayOpen (void) { if (!patchbayQueryClose()) return; const QString& path = QFileDialog::getOpenFileName(this, tr("Open Patchbay File"), patchbayFileDir(), patchbayFileFilter()); if (path.isEmpty()) return; patchbayOpenFile(path); updatePatchbayNames(); } void qpwgraph_form::patchbayOpenRecent (void) { // Retrive filename index from action data... QAction *action = qobject_cast (sender()); if (action) { const QString& path = action->data().toString(); // Check if we can safely close the current file... if (!path.isEmpty() && patchbayQueryClose()) patchbayOpenFile(path); } updatePatchbayNames(); } void qpwgraph_form::patchbaySave (void) { if (m_patchbay_path.isEmpty()) { patchbaySaveAs(); return; } patchbaySaveFile(m_patchbay_path); updatePatchbayNames(); } void qpwgraph_form::patchbaySaveAs (void) { const QString& path = QFileDialog::getSaveFileName(this, tr("Save Patchbay File"), patchbayFileDir(), patchbayFileFilter()); if (path.isEmpty()) return; if (QFileInfo(path).suffix().isEmpty()) patchbaySaveFile(path + '.' + patchbayFileExt()); else patchbaySaveFile(path); updatePatchbayNames(); } void qpwgraph_form::patchbayActivated ( bool on ) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) { patchbay->setActivated(on); patchbay->scan(); } stabilize(); } void qpwgraph_form::patchbayExclusive ( bool on ) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) { patchbay->setExclusive(on); if (patchbay->isActivated()) patchbay->scan(); } stabilize(); } void qpwgraph_form::patchbayEdit ( bool on ) { m_ui.graphCanvas->setPatchbayEdit(on); stabilize(); } void qpwgraph_form::patchbayPin (void) { m_ui.graphCanvas->patchbayPin(); stabilize(); } void qpwgraph_form::patchbayUnpin (void) { m_ui.graphCanvas->patchbayUnpin(); stabilize(); } void qpwgraph_form::patchbayAutoPin ( bool on ) { m_ui.graphCanvas->setPatchbayAutoPin(on); stabilize(); } void qpwgraph_form::patchbayAutoDisconnect ( bool on ) { m_ui.graphCanvas->setPatchbayAutoDisconnect(on); stabilize(); } // Main menu slots. void qpwgraph_form::viewMenubar ( bool on ) { m_ui.MenuBar->setVisible(on); } void qpwgraph_form::viewGraphToolbar ( bool on ) { m_ui.graphToolbar->setVisible(on); } void qpwgraph_form::viewPatchbayToolbar ( bool on ) { m_ui.patchbayToolbar->setVisible(on); } void qpwgraph_form::viewStatusbar ( bool on ) { m_ui.StatusBar->setVisible(on); } void qpwgraph_form::viewTextBesideIcons ( bool on ) { if (on) { m_ui.graphToolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); m_ui.patchbayToolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); } else { m_ui.graphToolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_ui.patchbayToolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); } } void qpwgraph_form::viewCenter (void) { const QRectF& scene_rect = m_ui.graphCanvas->scene()->itemsBoundingRect(); m_ui.graphCanvas->centerOn(scene_rect.center()); stabilize(); } void qpwgraph_form::viewRefresh (void) { pipewire_changed(); alsamidi_changed(); if (m_ui.graphCanvas->isRepelOverlappingNodes()) ++m_repel_overlapping_nodes; // fake nodes added! refresh(); } void qpwgraph_form::viewZoomRange ( bool on ) { m_ui.graphCanvas->setZoomRange(on); } void qpwgraph_form::viewColorsAction (void) { QAction *action = qobject_cast (sender()); if (action == nullptr) return; const uint port_type = action->data().toUInt(); if (0 >= port_type) return; const QColor& color = QColorDialog::getColor( m_ui.graphCanvas->portTypeColor(port_type), this, tr("Colors - %1").arg(action->text().remove('&'))); if (color.isValid()) { m_ui.graphCanvas->setPortTypeColor(port_type, color); m_ui.graphCanvas->updatePortTypeColors(port_type); updateViewColorsAction(action); } } void qpwgraph_form::viewColorsReset (void) { m_ui.graphCanvas->clearPortTypeColors(); if (m_pipewire) m_pipewire->resetPortTypeColors(); #ifdef CONFIG_ALSA_MIDI if (m_alsamidi) m_alsamidi->resetPortTypeColors(); #endif m_ui.graphCanvas->updatePortTypeColors(); updateViewColors(); } void qpwgraph_form::viewSortTypeAction (void) { QAction *action = qobject_cast (sender()); if (action == nullptr) return; const qpwgraph_port::SortType sort_type = qpwgraph_port::SortType(action->data().toInt()); qpwgraph_port::setSortType(sort_type); m_ui.graphCanvas->updateNodes(); } void qpwgraph_form::viewSortOrderAction (void) { QAction *action = qobject_cast (sender()); if (action == nullptr) return; const qpwgraph_port::SortOrder sort_order = qpwgraph_port::SortOrder(action->data().toInt()); qpwgraph_port::setSortOrder(sort_order); m_ui.graphCanvas->updateNodes(); } void qpwgraph_form::viewRepelOverlappingNodes ( bool on ) { m_ui.graphCanvas->setRepelOverlappingNodes(on); if (on) ++m_repel_overlapping_nodes; } void qpwgraph_form::viewConnectThroughNodes ( bool on ) { qpwgraph_connect::setConnectThroughNodes(on); m_ui.graphCanvas->updateConnects(); } void qpwgraph_form::helpSystemTray ( bool on ) { #ifdef CONFIG_SYSTEM_TRAY if (on && m_systray == nullptr) { m_systray = new qpwgraph_systray(this); m_systray->show(); } else if (!on && m_systray) { m_systray->hide(); delete m_systray; m_systray = nullptr; } #else (void) on; #endif m_systray_closed = false; } void qpwgraph_form::helpAlsaMidi ( bool on ) { #ifdef CONFIG_ALSA_MIDI if (on && m_alsamidi == nullptr) { m_alsamidi = new qpwgraph_alsamidi(m_ui.graphCanvas); QObject::connect( m_alsamidi, SIGNAL(changed()), this, SLOT(alsamidi_changed())); ++m_alsamidi_changed; } else if (!on && m_alsamidi) { m_alsamidi->clearItems(); QObject::disconnect( m_alsamidi, SIGNAL(changed()), this, SLOT(alsamidi_changed())); delete m_alsamidi; m_alsamidi = nullptr; } #else (void) on; #endif stabilize(); } void qpwgraph_form::helpAbout (void) { static const QString title = PROJECT_NAME; static const QString version = PROJECT_VERSION; static const QString subtitle = PROJECT_DESCRIPTION; static const QString website = PROJECT_HOMEPAGE_URL; static const QString copyright = "Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved."; QStringList list; #ifdef CONFIG_DEBUG list << tr("Debugging option enabled."); #endif #ifndef CONFIG_ALSA_MIDI list << tr("ALSA MIDI support disabled."); #endif #ifndef CONFIG_SYSTEM_TRAY list << tr("System-tray icon support disabled."); #endif QString text = "

" + title + "

\n"; text += "

" + subtitle + "
\n"; text += "
\n"; text += tr("Version") + ": " + version + "
\n"; if (!list.isEmpty()) { text += ""; text += list.join("
\n"); text += "
\n"; } text += "
\n"; text += tr("Using: Qt %1").arg(qVersion()); #if defined(QT_STATIC) text += "-static"; #endif text += ", "; text += tr("libpipewire %1 (headers: %2)") .arg(pw_get_library_version()) .arg(pw_get_headers_version()); text += "
\n"; text += "
\n"; text += tr("Website") + ": " + website + "
\n"; text += "
\n"; text += ""; text += copyright + "
\n"; text += "
\n"; text += tr("This program is free software; you can redistribute it and/or modify it") + "
\n"; text += tr("under the terms of the GNU General Public License version 2 or later."); text += "
"; text += "

\n"; QMessageBox::about(this, tr("About") + ' ' + title, text); } void qpwgraph_form::helpAboutQt (void) { QMessageBox::aboutQt(this); } void qpwgraph_form::zoomValueChanged ( int zoom_value ) { m_ui.graphCanvas->setZoom(0.01 * qreal(zoom_value)); } void qpwgraph_form::patchbayNameChanged ( int index ) { if (index > 0) { const QString& path = m_patchbay_names->itemData(index).toString(); if (!path.isEmpty() && patchbayQueryClose()) patchbayOpenFile(path); } updatePatchbayNames(); } // Node life-cycle slots. void qpwgraph_form::added ( qpwgraph_node *node ) { const qpwgraph_canvas *canvas = m_ui.graphCanvas; const QRectF& rect = canvas->mapToScene(canvas->viewport()->rect()).boundingRect(); const QPointF& pos = rect.center(); const qreal w = 0.33 * qMax(rect.width(), 800.0); const qreal h = 0.33 * qMax(rect.height(), 600.0); qreal x = pos.x(); qreal y = pos.y(); switch (node->nodeMode()) { case qpwgraph_item::Input: ++m_ins &= 0x0f; x += w; y += 0.33 * h * (m_ins & 1 ? +m_ins : -m_ins); break; case qpwgraph_item::Output: ++m_outs &= 0x0f; x -= w; y += 0.33 * h * (m_outs & 1 ? +m_outs : -m_outs); break; default: { int dx = 0; int dy = 0; for (int i = 0; i < m_mids; ++i) { if ((qAbs(dx) > qAbs(dy)) || (dx == dy && dx < 0)) dy += (dx < 0 ? +1 : -1); else dx += (dy < 0 ? -1 : +1); } x += 0.33 * w * qreal(dx); y += 0.33 * h * qreal(dy); ++m_mids &= 0x1f; break; }} x -= qreal(::rand() & 0x1f); y -= qreal(::rand() & 0x1f); node->setPos(canvas->snapPos(x, y)); updated(node); } void qpwgraph_form::updated ( qpwgraph_node */*node*/ ) { if (m_ui.graphCanvas->isRepelOverlappingNodes()) ++m_repel_overlapping_nodes; } void qpwgraph_form::removed ( qpwgraph_node */*node*/ ) { #if 0// FIXME: DANGEROUS! Node might have been deleted by now... if (node) { switch (node->nodeMode()) { case qpwgraph_item::Input: --m_ins; break; case qpwgraph_item::Output: --m_outs; break; default: --m_mids; break; } } #endif } // Port (dis)connection slots. void qpwgraph_form::connected ( qpwgraph_port *port1, qpwgraph_port *port2 ) { if (qpwgraph_pipewire::isPortType(port1->portType())) { if (m_pipewire) m_pipewire->connectPorts(port1, port2, true); pipewire_changed(); } #ifdef CONFIG_ALSA_MIDI else if (qpwgraph_alsamidi::isPortType(port1->portType())) { if (m_alsamidi) m_alsamidi->connectPorts(port1, port2, true); alsamidi_changed(); } #endif stabilize(); } void qpwgraph_form::disconnected ( qpwgraph_port *port1, qpwgraph_port *port2 ) { if (qpwgraph_pipewire::isPortType(port1->portType())) { if (m_pipewire) m_pipewire->connectPorts(port1, port2, false); pipewire_changed(); } #ifdef CONFIG_ALSA_MIDI else if (qpwgraph_alsamidi::isPortType(port1->portType())) { if (m_alsamidi) m_alsamidi->connectPorts(port1, port2, false); alsamidi_changed(); } #endif stabilize(); } void qpwgraph_form::connected ( qpwgraph_connect *connect ) { qpwgraph_port *port1 = connect->port1(); if (port1 == nullptr) return; if (qpwgraph_pipewire::isPortType(port1->portType())) { if (m_pipewire) m_pipewire->addItem(connect, false); } #ifdef CONFIG_ALSA_MIDI else if (qpwgraph_alsamidi::isPortType(port1->portType())) { if (m_alsamidi) m_alsamidi->addItem(connect, false); } #endif } // Item renaming slot. void qpwgraph_form::renamed ( qpwgraph_item *item, const QString& name ) { qpwgraph_sect *sect = item_sect(item); if (sect) sect->renameItem(item, name); } // Graph section slots. void qpwgraph_form::pipewire_changed (void) { ++m_pipewire_changed; } void qpwgraph_form::alsamidi_changed (void) { ++m_alsamidi_changed; } // Pseudo-asyncronous timed refreshner. void qpwgraph_form::refresh (void) { if (m_ui.graphCanvas->isBusy()) { QTimer::singleShot(1200, this, SLOT(refresh())); return; } int nchanged = 0; if (m_pipewire_changed > 0) { m_pipewire_changed = 0; if (m_pipewire) m_pipewire->updateItems(); ++nchanged; } #ifdef CONFIG_ALSA_MIDI if (m_alsamidi_changed > 0) { m_alsamidi_changed = 0; if (m_alsamidi) m_alsamidi->updateItems(); ++nchanged; } #endif if (nchanged > 0) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay && patchbay->isActivated()) patchbay->scan(); stabilize(); } else if (m_repel_overlapping_nodes > 0) { m_repel_overlapping_nodes = 0; m_ui.graphCanvas->repelOverlappingNodesAll(); stabilize(); } QTimer::singleShot(300, this, SLOT(refresh())); } // Graph selection change slot. void qpwgraph_form::stabilize (void) { const qpwgraph_canvas *canvas = m_ui.graphCanvas; const qpwgraph_patchbay *patchbay = canvas->patchbay(); const bool is_activated = (patchbay && patchbay->isActivated()); const bool is_dirty = (patchbay && patchbay->isDirty()); // Update window title. QString title = patchbayFileName(); if (is_dirty) title += ' ' + tr("[modified]"); setWindowTitle(title); #ifdef CONFIG_SYSTEM_TRAY if (m_systray) m_systray->setToolTip(title); #endif m_ui.patchbayExclusiveAction->setEnabled(is_activated); m_ui.patchbaySaveAction->setEnabled(is_dirty); m_ui.patchbayPinAction->setEnabled(canvas->canPatchbayPin()); m_ui.patchbayUnpinAction->setEnabled(canvas->canPatchbayUnpin()); m_ui.graphConnectAction->setEnabled(canvas->canConnect()); m_ui.graphDisconnectAction->setEnabled(canvas->canDisconnect()); m_ui.editSelectNoneAction->setEnabled( !canvas->scene()->selectedItems().isEmpty()); m_ui.editRenameItemAction->setEnabled( canvas->canRenameItem()); #if 0 const QRectF& outter_rect = canvas->scene()->sceneRect().adjusted(-2.0, -2.0, +2.0, +2.0); const QRectF& inner_rect = canvas->mapToScene(canvas->viewport()->rect()).boundingRect(); const bool is_contained = outter_rect.contains(inner_rect) || canvas->horizontalScrollBar()->isVisible() || canvas->verticalScrollBar()->isVisible(); #else const bool is_contained = true; #endif const qreal zoom = canvas->zoom(); m_ui.viewCenterAction->setEnabled(is_contained); m_ui.viewZoomInAction->setEnabled(zoom < 1.9); m_ui.viewZoomOutAction->setEnabled(zoom > 0.1); m_ui.viewZoomFitAction->setEnabled(is_contained); m_ui.viewZoomResetAction->setEnabled(zoom != 1.0); const int zoom_value = int(100.0f * zoom); const bool is_spinbox_blocked = m_zoom_spinbox->blockSignals(true); const bool is_slider_blocked = m_zoom_slider->blockSignals(true); m_zoom_spinbox->setValue(zoom_value); m_zoom_slider->setValue(zoom_value); m_zoom_spinbox->blockSignals(is_spinbox_blocked); m_zoom_slider->blockSignals(is_slider_blocked); #ifdef CONFIG_ALSA_MIDI m_ui.viewColorsAlsaMidiAction->setEnabled(m_alsamidi != nullptr); #endif } // Tool-bar orientation change slot. void qpwgraph_form::orientationChanged ( Qt::Orientation orientation ) { QToolBar *toolbar = qobject_cast (sender()); if (toolbar == nullptr) return; if (toolbar == m_ui.patchbayToolbar && m_patchbay_names_tool) m_patchbay_names_tool->setVisible(orientation == Qt::Horizontal); if (m_config->isTextBesideIcons() && orientation == Qt::Horizontal) { toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); } else { toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); } } // Open/save patchbay file. bool qpwgraph_form::patchbayOpenFile ( const QString& path, bool clear ) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay == nullptr) return false; if (clear) { patchbay->clear(); m_patchbay_path.clear(); } if (!patchbay->load(path)) { QMessageBox::critical(this, tr("Error"), tr("Could not open patchbay file:\n\n%1\n\nSorry.").arg(path), QMessageBox::Cancel); return false; } m_config->patchbayRecentFiles(path); m_patchbay_dir = QFileInfo(path).absolutePath(); m_patchbay_path = path; m_ui.graphCanvas->patchbayEdit(); if (patchbay->isActivated()) patchbay->scan(); return true; } bool qpwgraph_form::patchbaySaveFile ( const QString& path ) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay == nullptr) return false; if (!patchbay->save(path)) { QMessageBox::critical(this, tr("Error"), tr("Could not save patchbay file:\n\n%1\n\nSorry.").arg(path), QMessageBox::Cancel); return false; } m_config->patchbayRecentFiles(path); m_patchbay_dir = QFileInfo(path).absolutePath(); m_patchbay_path = path; return true; } // Get the current display file-name. QString qpwgraph_form::patchbayFileName (void) const { if (m_patchbay_path.isEmpty()) return tr("Untitled%1").arg(m_patchbay_untitled + 1); else return QFileInfo(m_patchbay_path).completeBaseName(); } // Get default patchbay file directory/extension/filter. QString qpwgraph_form::patchbayFileDir (void) const { if (m_patchbay_path.isEmpty()) return m_patchbay_dir; else return m_patchbay_path; } QString qpwgraph_form::patchbayFileExt (void) const { return QString(PROJECT_NAME).toLower(); } QString qpwgraph_form::patchbayFileFilter (void) const { return tr("Patchbay files (*.%1)").arg(patchbayFileExt()) + ";;" + tr("All files (*.*)"); } // Whether we can close/quit current patchbay. bool qpwgraph_form::patchbayQueryClose (void) { bool ret = true; const qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay && patchbay->isDirty()) { showNormal(); switch (QMessageBox::warning(this, tr("Warning"), tr("The current patchbay has been changed:\n\n\"%1\"\n\n" "Do you want to save the changes?").arg(patchbayFileName()), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel)) { case QMessageBox::Save: patchbaySave(); // Fall thru.... case QMessageBox::Discard: break; default: // Cancel. ret = false; break; } } return ret; } bool qpwgraph_form::patchbayQueryQuit (void) { if (!patchbayQueryClose()) return false; bool ret = true; const qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay && patchbay->isActivated()) { showNormal(); ret = (QMessageBox::warning(this, tr("Warning"), tr("A patchbay is currently activated:\n\n\"%1\"\n\n" "Are you sure you want to quit?").arg(patchbayFileName()), QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok); } return ret; } // Context-menu event handler. void qpwgraph_form::contextMenuEvent ( QContextMenuEvent *event ) { m_ui.graphCanvas->clear(); stabilize(); QMenu menu(this); if (m_ui.graphCanvas->isPatchbayEdit()) { menu.addAction(m_ui.patchbayPinAction); menu.addAction(m_ui.patchbayUnpinAction); menu.addSeparator(); } menu.addAction(m_ui.graphConnectAction); menu.addAction(m_ui.graphDisconnectAction); menu.addSeparator(); menu.addActions(m_ui.editMenu->actions()); menu.addSeparator(); menu.addMenu(m_ui.viewZoomMenu); menu.exec(event->globalPos()); stabilize(); } // Widget resize event handler. void qpwgraph_form::resizeEvent ( QResizeEvent *event ) { QMainWindow::resizeEvent(event); stabilize(); } // Widget event handlers. void qpwgraph_form::showEvent ( QShowEvent *event ) { QMainWindow::showEvent(event); #ifdef CONFIG_SYSTEM_TRAY if (m_systray) m_systray->updateContextMenu(); #endif } void qpwgraph_form::hideEvent ( QHideEvent *event ) { QMainWindow::hideEvent(event); #ifdef CONFIG_SYSTEM_TRAY if (m_systray) m_systray->updateContextMenu(); #endif saveState(); } void qpwgraph_form::closeEvent ( QCloseEvent *event ) { #ifdef CONFIG_SYSTEM_TRAY if (m_systray) { if (!m_systray_closed) { const QString& title = tr("Information"); const QString& text = tr("The program will keep running in the system tray.\n\n" "To terminate the program, please choose \"Quit\"\n" "in the main menu or the context menu of the system tray icon."); if (QSystemTrayIcon::supportsMessages()) m_systray->showMessage(title, text, QSystemTrayIcon::Information); else QMessageBox::information(this, title, text); } m_systray_closed = true; hide(); event->ignore(); } else #endif if (patchbayQueryQuit()) { hide(); QMainWindow::closeEvent(event); } else { event->ignore(); } } // Special port-type color methods. void qpwgraph_form::updateViewColorsAction ( QAction *action ) { const uint port_type = action->data().toUInt(); if (0 >= port_type) return; const QColor& color = m_ui.graphCanvas->portTypeColor(port_type); if (!color.isValid()) return; QPixmap pm(22, 22); QPainter(&pm).fillRect(0, 0, pm.width(), pm.height(), color); action->setIcon(QIcon(pm)); } void qpwgraph_form::updateViewColors (void) { updateViewColorsAction(m_ui.viewColorsPipewireAudioAction); updateViewColorsAction(m_ui.viewColorsPipewireMidiAction); updateViewColorsAction(m_ui.viewColorsPipewireVideoAction); updateViewColorsAction(m_ui.viewColorsPipewireOtherAction); #ifdef CONFIG_ALSA_MIDI updateViewColorsAction(m_ui.viewColorsAlsaMidiAction); #endif } // Update patchbay recent files menu. void qpwgraph_form::updatePatchbayMenu (void) { // Rebuild the recent files menu... const QIcon icon(":/images/itemPatchbay.png"); m_ui.patchbayOpenRecentMenu->clear(); QStringListIterator iter(m_config->patchbayRecentFiles()); for (int i = 0; iter.hasNext(); ++i) { const QFileInfo info(iter.next()); if (info.exists()) { QAction *action = m_ui.patchbayOpenRecentMenu->addAction(icon, QString("&%1 %2").arg(i + 1).arg(info.completeBaseName()), this, SLOT(patchbayOpenRecent())); action->setData(info.absoluteFilePath()); } } // Settle as enabled? m_ui.patchbayOpenRecentMenu->setEnabled( !m_ui.patchbayOpenRecentMenu->isEmpty()); } // Update patchbay names combo-box (toolbar). void qpwgraph_form::updatePatchbayNames (void) { const bool is_blocked = m_patchbay_names->blockSignals(true); const QIcon icon(":/images/itemPatchbay.png"); m_patchbay_names->clear(); m_patchbay_names->addItem(icon, patchbayFileName(), m_patchbay_path); const QStringList& paths = m_config->patchbayRecentFiles(); foreach (const QString& path, paths) { if (path == m_patchbay_path) continue; m_patchbay_names->addItem(icon, QFileInfo(path).completeBaseName(), path); } m_patchbay_names->setCurrentIndex(0); m_patchbay_names->blockSignals(is_blocked); stabilize(); } // Item sect predicate. qpwgraph_sect *qpwgraph_form::item_sect ( qpwgraph_item *item ) const { if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node && qpwgraph_pipewire::isNodeType(node->nodeType())) return m_pipewire; #ifdef CONFIG_ALSA_MIDI else if (node && qpwgraph_alsamidi::isNodeType(node->nodeType())) return m_alsamidi; #endif } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port && qpwgraph_pipewire::isPortType(port->portType())) return m_pipewire; #ifdef CONFIG_ALSA_MIDI else if (port && qpwgraph_alsamidi::isPortType(port->portType())) return m_alsamidi; #endif } return nullptr; // No deal! } // Restore whole form state... void qpwgraph_form::restoreState (void) { m_config->restoreState(this); qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) { const bool is_activated = m_config->isPatchbayActivated(); const bool is_exclusive = m_config->isPatchbayExclusive(); const bool is_autopin = m_config->isPatchbayAutoPin(); const bool is_autodisconnect = m_config->isPatchbayAutoDisconnect(); m_ui.patchbayActivatedAction->setChecked(is_activated); m_ui.patchbayExclusiveAction->setChecked(is_exclusive); m_ui.patchbayAutoPinAction->setChecked(is_autopin); m_ui.patchbayAutoDisconnectAction->setChecked(is_autodisconnect); patchbay->setActivated(is_activated); patchbay->setExclusive(is_exclusive); m_ui.graphCanvas->setPatchbayAutoPin(is_autopin); m_ui.graphCanvas->setPatchbayAutoDisconnect(is_autodisconnect); } m_ui.viewMenubarAction->setChecked(m_config->isMenubar()); m_ui.viewGraphToolbarAction->setChecked(m_config->isToolbar()); m_ui.viewPatchbayToolbarAction->setChecked(m_config->isPatchbayToolbar()); m_ui.viewStatusbarAction->setChecked(m_config->isStatusbar()); m_ui.viewTextBesideIconsAction->setChecked(m_config->isTextBesideIcons()); m_ui.viewZoomRangeAction->setChecked(m_config->isZoomRange()); m_ui.viewRepelOverlappingNodesAction->setChecked(m_config->isRepelOverlappingNodes()); m_ui.viewConnectThroughNodesAction->setChecked(m_config->isConnectThroughNodes()); const qpwgraph_port::SortType sort_type = qpwgraph_port::SortType(m_config->sortType()); qpwgraph_port::setSortType(sort_type); switch (sort_type) { case qpwgraph_port::PortIndex: m_ui.viewSortPortIndexAction->setChecked(true); break; case qpwgraph_port::PortTitle: m_ui.viewSortPortTitleAction->setChecked(true); break; case qpwgraph_port::PortName: default: m_ui.viewSortPortNameAction->setChecked(true); break; } const qpwgraph_port::SortOrder sort_order = qpwgraph_port::SortOrder(m_config->sortOrder()); qpwgraph_port::setSortOrder(sort_order); switch (sort_order) { case qpwgraph_port::Descending: m_ui.viewSortDescendingAction->setChecked(true); break; case qpwgraph_port::Ascending: default: m_ui.viewSortAscendingAction->setChecked(true); break; } viewMenubar(m_config->isMenubar()); viewGraphToolbar(m_config->isToolbar()); viewPatchbayToolbar(m_config->isPatchbayToolbar()); viewStatusbar(m_config->isStatusbar()); viewTextBesideIcons(m_config->isTextBesideIcons()); viewZoomRange(m_config->isZoomRange()); viewRepelOverlappingNodes(m_config->isRepelOverlappingNodes()); viewConnectThroughNodes(m_config->isConnectThroughNodes()); m_ui.graphCanvas->restoreState(); // Restore last open patchbay directory and file-path... m_patchbay_dir = m_config->patchbayDir(); m_patchbay_path = m_config->patchbayPath(); #ifdef CONFIG_SYSTEM_TRAY const bool is_systray_enabled = m_config->isSystemTrayEnabled(); m_ui.helpSystemTrayAction->setChecked(is_systray_enabled); helpSystemTray(is_systray_enabled); #endif #ifdef CONFIG_ALSA_MIDI const bool is_alsa_midi = m_config->isAlsaMidiEnabled(); m_ui.helpAlsaMidiAction->setChecked(is_alsa_midi); helpAlsaMidi(is_alsa_midi); #endif } // Forcibly save whole form state. void qpwgraph_form::saveState (void) { m_ui.graphCanvas->saveState(); m_config->setTextBesideIcons(m_ui.viewTextBesideIconsAction->isChecked()); m_config->setZoomRange(m_ui.viewZoomRangeAction->isChecked()); m_config->setSortType(int(qpwgraph_port::sortType())); m_config->setSortOrder(int(qpwgraph_port::sortOrder())); m_config->setRepelOverlappingNodes(m_ui.viewRepelOverlappingNodesAction->isChecked()); m_config->setConnectThroughNodes(m_ui.viewConnectThroughNodesAction->isChecked()); m_config->setStatusbar(m_ui.StatusBar->isVisible()); m_config->setToolbar(m_ui.graphToolbar->isVisible()); m_config->setPatchbayToolbar(m_ui.patchbayToolbar->isVisible()); m_config->setMenubar(m_ui.MenuBar->isVisible()); m_config->setPatchbayAutoPin(m_ui.patchbayAutoPinAction->isChecked()); m_config->setPatchbayAutoDisconnect(m_ui.patchbayAutoDisconnectAction->isChecked()); m_config->setPatchbayExclusive(m_ui.patchbayExclusiveAction->isChecked()); m_config->setPatchbayActivated(m_ui.patchbayActivatedAction->isChecked()); m_config->setPatchbayPath(m_patchbay_path); m_config->setPatchbayDir(m_patchbay_dir); #ifdef CONFIG_SYSTEM_TRAY m_config->setSystemTrayEnabled(m_ui.helpSystemTrayAction->isChecked()); #endif #ifdef CONFIG_ALSA_MIDI m_config->setAlsaMidiEnabled(m_ui.helpAlsaMidiAction->isChecked()); #endif m_config->saveState(this); } // Forcibly quit application. void qpwgraph_form::closeQuit (void) { if (!patchbayQueryQuit()) return; if (isVisible()) saveState(); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QApplication::exit(0); #else QApplication::quit(); #endif } // Session management handler (eg. logoff) void qpwgraph_form::commitData ( QSessionManager& sm ) { sm.release(); m_config->setSessionStartMinimized(!isVisible() && !isMinimized()); } // end of qpwgraph_form.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_item.cpp0000644000000000000000000000013214532447230016573 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_item.cpp0000644000175000001440000000627314532447230020033 0ustar00rncbcusers00000000000000// qpwgraph_item.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_item.h" #include "qpwgraph_node.h" #include "qpwgraph_port.h" #include "qpwgraph_connect.h" #include //---------------------------------------------------------------------------- // qpwgraph_item -- Base graphics item. // Constructor. qpwgraph_item::qpwgraph_item ( QGraphicsItem *parent ) : QGraphicsPathItem(parent), m_marked(false), m_hilite(false) { const QPalette pal; m_foreground = pal.buttonText().color(); m_background = pal.button().color(); } // Basic color accessors. void qpwgraph_item::setForeground ( const QColor& color ) { m_foreground = color; } const QColor& qpwgraph_item::foreground (void) const { return m_foreground; } void qpwgraph_item::setBackground ( const QColor& color ) { m_background = color; } const QColor& qpwgraph_item::background (void) const { return m_background; } // Marking methods. void qpwgraph_item::setMarked ( bool marked ) { m_marked = marked; } bool qpwgraph_item::isMarked (void) const { return m_marked; } // Highlighting methods. void qpwgraph_item::setHighlight ( bool hilite ) { m_hilite = hilite; if (m_hilite) raise(); QGraphicsPathItem::update(); } bool qpwgraph_item::isHighlight (void) const { return m_hilite; } // Raise item z-value (dynamic always-on-top). void qpwgraph_item::raise (void) { static qreal s_zvalue = 0.0; switch (type()) { case qpwgraph_port::Type: { QGraphicsPathItem::setZValue(s_zvalue += 0.003); qpwgraph_port *port = static_cast (this); if (port) { qpwgraph_node *node = port->portNode(); if (node) node->setZValue(s_zvalue += 0.002); } break; } case qpwgraph_connect::Type: default: QGraphicsPathItem::setZValue(s_zvalue += 0.001); break; } } // Item-type hash (static) uint qpwgraph_item::itemType ( const QByteArray& type_name ) { return qHash(type_name); } // Rectangular editor extents (virtual) QRectF qpwgraph_item::editorRect (void) const { return QRectF(); } // Path and bounding rectangle override. void qpwgraph_item::setPath ( const QPainterPath& path ) { m_rect = path.controlPointRect(); QGraphicsPathItem::setPath(path); } // Bounding rectangle accessor. const QRectF& qpwgraph_item::itemRect (void) const { return m_rect; } // end of qpwgraph_item.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_config.cpp0000644000000000000000000000013214532447230017102 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_config.cpp0000644000175000001440000003050514532447230020335 0ustar00rncbcusers00000000000000// qpwgraph_config.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "config.h" #include "qpwgraph_config.h" #include #include #include // Local constants. static const char *GeometryGroup = "/GraphGeometry"; static const char *LayoutGroup = "/GraphLayout"; static const char *ViewGroup = "/GraphView"; static const char *ViewMenubarKey = "/Menubar"; static const char *ViewToolbarKey = "/Toolbar"; static const char *ViewStatusbarKey = "/Statusbar"; static const char *ViewTextBesideIconsKey = "/TextBesideIcons"; static const char *ViewZoomRangeKey = "/ZoomRange"; static const char *ViewSortTypeKey = "/SortType"; static const char *ViewSortOrderKey = "/SortOrder"; static const char *ViewRepelOverlappingNodesKey = "/RepelOverlappingNodes"; static const char *ViewConnectThroughNodesKey = "/ConnectThroughNodes"; static const char *PatchbayGroup = "/Patchbay"; static const char *PatchbayDirKey = "/Dir"; static const char *PatchbayPathKey = "/Path"; static const char *PatchbayActivatedKey = "/Activated"; static const char *PatchbayExclusiveKey = "/Exclusive"; static const char *PatchbayAutoPinKey = "/AutoPin"; static const char *PatchbayAutoDisconnectKey = "/AutoDisconnect"; static const char *PatchbayRecentFilesKey = "/RecentFiles"; static const char *PatchbayToolbarKey = "/Toolbar"; #ifdef CONFIG_SYSTEM_TRAY static const char *SystemTrayGroup = "/SystemTray"; static const char *SystemTrayEnabledKey = "/Enabled"; #endif #ifdef CONFIG_ALSA_MIDI static const char *AlsaMidiGroup = "/AlsaMidi"; static const char *AlsaMidiEnabledKey = "/Enabled"; #endif static const char *SessionGroup = "/Session"; static const char *SessionStartMinimizedKey = "/StartMinimized"; //---------------------------------------------------------------------------- // qpwgraph_config -- Canvas state memento. // Constructors. qpwgraph_config::qpwgraph_config ( QSettings *settings, bool owner ) : m_settings(settings), m_owner(owner), m_menubar(false), m_toolbar(false), m_statusbar(false), m_texticons(false), m_zoomrange(false), m_sorttype(0), m_sortorder(0), m_repelnodes(false), m_cthrunodes(false), m_patchbay_toolbar(false), m_patchbay_activated(false), m_patchbay_exclusive(false), m_patchbay_autopin(true), m_patchbay_autodisconnect(false), m_systray_enabled(true), m_alsaseq_enabled(true) { } qpwgraph_config::qpwgraph_config ( const QString& org_name, const QString& app_name ) : qpwgraph_config(new QSettings(org_name, app_name), true) { } // Destructor. qpwgraph_config::~qpwgraph_config (void) { setSettings(nullptr); } // Accessors. void qpwgraph_config::setSettings ( QSettings *settings, bool owner ) { if (m_settings && m_owner) delete m_settings; m_settings = settings; m_owner = owner; } QSettings *qpwgraph_config::settings (void) const { return m_settings; } void qpwgraph_config::setMenubar ( bool menubar ) { m_menubar = menubar; } bool qpwgraph_config::isMenubar (void) const { return m_menubar; } void qpwgraph_config::setToolbar ( bool toolbar ) { m_toolbar = toolbar; } bool qpwgraph_config::isToolbar (void) const { return m_toolbar; } void qpwgraph_config::setStatusbar ( bool statusbar ) { m_statusbar = statusbar; } bool qpwgraph_config::isStatusbar (void) const { return m_statusbar; } void qpwgraph_config::setTextBesideIcons ( bool texticons ) { m_texticons = texticons; } bool qpwgraph_config::isTextBesideIcons (void) const { return m_texticons; } void qpwgraph_config::setZoomRange ( bool zoomrange ) { m_zoomrange = zoomrange; } bool qpwgraph_config::isZoomRange (void) const { return m_zoomrange; } void qpwgraph_config::setSortType ( int sorttype ) { m_sorttype = sorttype; } int qpwgraph_config::sortType (void) const { return m_sorttype; } void qpwgraph_config::setSortOrder ( int sortorder ) { m_sortorder = sortorder; } int qpwgraph_config::sortOrder (void) const { return m_sortorder; } void qpwgraph_config::setRepelOverlappingNodes ( bool repelnodes ) { m_repelnodes = repelnodes; } bool qpwgraph_config::isRepelOverlappingNodes (void) const { return m_repelnodes; } void qpwgraph_config::setConnectThroughNodes ( bool cthrunodes ) { m_cthrunodes = cthrunodes; } bool qpwgraph_config::isConnectThroughNodes (void) const { return m_cthrunodes; } void qpwgraph_config::setPatchbayToolbar ( bool toolbar ) { m_patchbay_toolbar = toolbar; } bool qpwgraph_config::isPatchbayToolbar (void) const { return m_patchbay_toolbar; } void qpwgraph_config::setPatchbayDir ( const QString& dir ) { m_patchbay_dir = dir; } const QString& qpwgraph_config::patchbayDir (void) const { return m_patchbay_dir; } void qpwgraph_config::setPatchbayPath ( const QString& path ) { m_patchbay_path = path; } const QString& qpwgraph_config::patchbayPath (void) const { return m_patchbay_path; } void qpwgraph_config::setPatchbayActivated ( bool activated ) { m_patchbay_activated = activated; } bool qpwgraph_config::isPatchbayActivated (void) const { return m_patchbay_activated; } void qpwgraph_config::setPatchbayExclusive ( bool exclusive ) { m_patchbay_exclusive = exclusive; } bool qpwgraph_config::isPatchbayExclusive (void) const { return m_patchbay_exclusive; } void qpwgraph_config::setPatchbayAutoPin ( bool autopin ) { m_patchbay_autopin = autopin; } bool qpwgraph_config::isPatchbayAutoPin (void) const { return m_patchbay_autopin; } void qpwgraph_config::setPatchbayAutoDisconnect ( bool autodisconnect ) { m_patchbay_autodisconnect = autodisconnect; } bool qpwgraph_config::isPatchbayAutoDisconnect (void) const { return m_patchbay_autodisconnect; } void qpwgraph_config::patchbayRecentFiles ( const QString& path ) { // Remove from list if already there (avoid duplicates) if (m_patchbay_recentfiles.contains(path)) m_patchbay_recentfiles.removeAll(path); // Put it to front... m_patchbay_recentfiles.push_front(path); // Time to keep the list under limits. int nfiles = m_patchbay_recentfiles.count(); while (nfiles > 8) { m_patchbay_recentfiles.pop_back(); --nfiles; } } const QStringList& qpwgraph_config::patchbayRecentFiles (void) const { return m_patchbay_recentfiles; } void qpwgraph_config::setSystemTrayEnabled ( bool enabled ) { m_systray_enabled = enabled; } bool qpwgraph_config::isSystemTrayEnabled (void) const { return m_systray_enabled; } void qpwgraph_config::setAlsaMidiEnabled ( bool enabled ) { m_alsaseq_enabled = enabled; } bool qpwgraph_config::isAlsaMidiEnabled (void) const { return m_alsaseq_enabled; } void qpwgraph_config::setSessionStartMinimized ( bool start_minimized ) { m_settings->beginGroup(SessionGroup); m_settings->setValue(SessionStartMinimizedKey, start_minimized); m_settings->endGroup(); m_settings->sync(); } bool qpwgraph_config::isSessionStartMinimized (void) const { m_settings->beginGroup(SessionGroup); const bool start_minimized = m_settings->value(SessionStartMinimizedKey, false).toBool(); m_settings->endGroup(); return start_minimized; } // Graph main-widget state methods. bool qpwgraph_config::restoreState ( QMainWindow *widget ) { if (m_settings == nullptr || widget == nullptr) return false; #ifdef CONFIG_SYSTEM_TRAY m_settings->beginGroup(SystemTrayGroup); m_systray_enabled = m_settings->value(SystemTrayEnabledKey, true).toBool(); m_settings->endGroup(); #endif #ifdef CONFIG_ALSA_MIDI m_settings->beginGroup(AlsaMidiGroup); m_alsaseq_enabled = m_settings->value(AlsaMidiEnabledKey, true).toBool(); m_settings->endGroup(); #endif m_settings->beginGroup(PatchbayGroup); m_patchbay_toolbar = m_settings->value(PatchbayToolbarKey, true).toBool(); m_patchbay_dir = m_settings->value(PatchbayDirKey).toString(); m_patchbay_path = m_settings->value(PatchbayPathKey).toString(); m_patchbay_activated = m_settings->value(PatchbayActivatedKey, false).toBool(); m_patchbay_exclusive = m_settings->value(PatchbayExclusiveKey, false).toBool(); m_patchbay_autopin = m_settings->value(PatchbayAutoPinKey, true).toBool(); m_patchbay_autodisconnect = m_settings->value(PatchbayAutoDisconnectKey, false).toBool(); m_patchbay_recentfiles = m_settings->value(PatchbayRecentFilesKey).toStringList(); m_settings->endGroup(); QMutableStringListIterator iter(m_patchbay_recentfiles); while (iter.hasNext()) { if (!QFileInfo(iter.next()).exists()) iter.remove(); } m_settings->beginGroup(ViewGroup); m_menubar = m_settings->value(ViewMenubarKey, true).toBool(); m_toolbar = m_settings->value(ViewToolbarKey, true).toBool(); m_statusbar = m_settings->value(ViewStatusbarKey, true).toBool(); m_texticons = m_settings->value(ViewTextBesideIconsKey, true).toBool(); m_zoomrange = m_settings->value(ViewZoomRangeKey, false).toBool(); m_sorttype = m_settings->value(ViewSortTypeKey, 0).toInt(); m_sortorder = m_settings->value(ViewSortOrderKey, 0).toInt(); m_repelnodes = m_settings->value(ViewRepelOverlappingNodesKey, false).toBool(); m_cthrunodes = m_settings->value(ViewConnectThroughNodesKey, false).toBool(); m_settings->endGroup(); m_settings->beginGroup(GeometryGroup); const QByteArray& geometry_state = m_settings->value('/' + widget->objectName()).toByteArray(); m_settings->endGroup(); if (geometry_state.isEmpty() || geometry_state.isNull()) return false; widget->restoreGeometry(geometry_state); m_settings->beginGroup(LayoutGroup); const QByteArray& layout_state = m_settings->value('/' + widget->objectName()).toByteArray(); m_settings->endGroup(); if (layout_state.isEmpty() || layout_state.isNull()) return false; widget->restoreState(layout_state); return true; } bool qpwgraph_config::saveState ( QMainWindow *widget ) const { if (m_settings == nullptr || widget == nullptr) return false; #ifdef CONFIG_SYSTEM_TRAY m_settings->beginGroup(SystemTrayGroup); m_settings->setValue(SystemTrayEnabledKey, m_systray_enabled); m_settings->endGroup(); #endif #ifdef CONFIG_ALSA_MIDI m_settings->beginGroup(AlsaMidiGroup); m_settings->setValue(AlsaMidiEnabledKey, m_alsaseq_enabled); m_settings->endGroup(); #endif m_settings->beginGroup(PatchbayGroup); m_settings->setValue(PatchbayToolbarKey, m_patchbay_toolbar); m_settings->setValue(PatchbayDirKey, m_patchbay_dir); m_settings->setValue(PatchbayPathKey, m_patchbay_path); m_settings->setValue(PatchbayActivatedKey, m_patchbay_activated); m_settings->setValue(PatchbayExclusiveKey, m_patchbay_exclusive); m_settings->setValue(PatchbayAutoPinKey, m_patchbay_autopin); m_settings->setValue(PatchbayAutoDisconnectKey, m_patchbay_autodisconnect); m_settings->setValue(PatchbayRecentFilesKey, m_patchbay_recentfiles); m_settings->endGroup(); m_settings->beginGroup(ViewGroup); m_settings->setValue(ViewMenubarKey, m_menubar); m_settings->setValue(ViewToolbarKey, m_toolbar); m_settings->setValue(ViewStatusbarKey, m_statusbar); m_settings->setValue(ViewTextBesideIconsKey, m_texticons); m_settings->setValue(ViewZoomRangeKey, m_zoomrange); m_settings->setValue(ViewSortTypeKey, m_sorttype); m_settings->setValue(ViewSortOrderKey, m_sortorder); m_settings->setValue(ViewRepelOverlappingNodesKey, m_repelnodes); m_settings->setValue(ViewConnectThroughNodesKey, m_cthrunodes); m_settings->endGroup(); m_settings->beginGroup(GeometryGroup); const QByteArray& geometry_state = widget->saveGeometry(); m_settings->setValue('/' + widget->objectName(), geometry_state); m_settings->endGroup(); m_settings->beginGroup(LayoutGroup); const QByteArray& layout_state = widget->saveState(); m_settings->setValue('/' + widget->objectName(), layout_state); m_settings->endGroup(); return true; } // end of qpwgraph_config.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_form.ui0000644000000000000000000000013214532447230016433 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_form.ui0000644000175000001440000007620114532447230017671 0ustar00rncbcusers00000000000000 rncbc aka Rui Nuno Capela qpwgraph - A PipeWire Graph Qt GUI Interface Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. qpwgraph_form 0 0 800 600 qpwgraph :/images/qpwgraph.svg 0 0 800 20 &Graph &Patchbay Open &Recent &Edit &View &Toolbar &Zoom :/images/viewZoomTool.png Co&lors :/images/viewColors.png S&ort &Help Qt::AllToolBarAreas Qt::Horizontal Qt::ToolButtonTextBesideIcon true TopToolBarArea false Qt::AllToolBarAreas Qt::Horizontal Qt::ToolButtonTextBesideIcon true TopToolBarArea true :/images/itemConnect.png &Connect Connect Connect Connect selected ports Ctrl+C :/images/itemDisconnect.png &Disconnect Disconnect Disconnect Disconnect selected ports Ctrl+D :/images/fileNew.png &New New New patchbay New patchbay Ctrl+N :/images/fileOpen.png &Open... Open Open patchbay Open patchbay Ctrl+O :/images/fileSave.png &Save Save Save patchbay Save current patchbay Ctrl+S Save &As... Save As Save as Save current patchbay with another name true :/images/itemActivate.png Act&ivated Activated Activated patchbay Activate current patchbay true :/images/itemExclusive.png E&xclusive Exclusive Exclusive patchbay Exclusive current patchbay true :/images/itemEdit.png &Edit Edit Edit patchbay Edit current patchbay :/images/itemPin.png &Pin Pin Pin connection Pin connection to current patchbay :/images/itemUnpin.png &Unpin Unpin Unpin connection Unpin connection from current patchbay true Au&to Pin Auto pin connections Auto pin connections to current patchbay true Auto &Disconnect Auto disconnect on deactivate Auto disconnect on deactivate current patchbay &Quit Quit Quit Quit this application program Ctrl+Q Select &All Select All Select All Select All Ctrl+A Select &None Select None Select None Select None Ctrl+Shift+A Select &Invert Select Invert Select Invert Select Invert Ctrl+I :/images/itemEdit.png &Rename... Rename item Rename Item Rename Item F2 true &Menubar Menubar Menubar Show/hide the main program window menubar Ctrl+M true &Graph Graph toolbar Graph toolbar Show/hide main program graph toolbar true &Patchbay Patchbay toolbar Patchbay toolbar Show/hide main program patchbay toolbar true &Statusbar Statusbar Statusbar Show/hide the main program window statusbar true Text Beside &Icons Text beside icons Text beside icons Show/hide text beside icons :/images/viewCenter.png &Center Center Center Center view &Refresh Refresh Refresh Refresh view F5 :/images/viewZoomIn.png Zoom &In Zoom In Zoom In Zoom In Ctrl++ :/images/viewZoomOut.png Zoom &Out Zoom Out Zoom Out Zoom Out Ctrl+- :/images/viewZoomFit.png Zoom &Fit Zoom Fit Zoom Fit Zoom Fit Ctrl+0 :/images/viewZoomReset.png Zoom &Reset Zoom Reset Zoom Reset Zoom Reset Ctrl+1 true :/images/viewZoomRange.png Zoom Rang&e Zoom Range Zoom Range Zoom Range PipeWire &Audio... PipeWire Audio color PipeWire Audio color PipeWire Audio color PipeWire &MIDI... PipeWire MIDI PipeWire MIDI color PipeWire MIDI color PipeWire &Video... PipeWire Video PipeWire Video color PipeWire Video color PipeWire &Other... PipeWire Other color PipeWire Other color PipeWire Other color ALSA M&IDI... ALSA MIDI ALSA MIDI color ALSA MIDI color &Reset Reset colors Reset colors Reset colors true Port &Name Port name Sort by port name true Port &Title Port title Sort by port title true Port &Index Port index Sort by port index true &Ascending Ascending Ascending sort order true &Descending Descending Descending sort order true Repel O&verlapping Nodes Repel nodes Repel overlapping nodes Repel overlapping nodes true Connect Thro&ugh Nodes Connect Through Nodes Connect through nodes Whether to draw connectors through or around nodes true &Enable System Tray Icon System tray System tray icon Enable system tray icon true &Enable ALSA MIDI ALSA MIDI ALSA MIDI Enable ALSA MIDI &About... About... About Show information about this application program About &Qt... About Qt... About Qt Show information about the Qt toolkit qpwgraph_canvas QGraphicsView
qpwgraph_canvas.h
qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_port.cpp0000644000000000000000000000013214532447230016621 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_port.cpp0000644000175000001440000002526414532447230020062 0ustar00rncbcusers00000000000000// qpwgraph_port.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_port.h" #include "qpwgraph_canvas.h" #include "qpwgraph_node.h" #include "qpwgraph_connect.h" #include #include #include #include #include //---------------------------------------------------------------------------- // qpwgraph_port -- Port graphics item. // Constructor. qpwgraph_port::qpwgraph_port ( qpwgraph_node *node, uint id, const QString& name, qpwgraph_item::Mode mode, uint type ) : qpwgraph_item(node), m_node(node), m_id(id), m_name(name), m_mode(mode), m_type(type), m_index(node->ports().count()), m_selectx(0), m_hilitex(0) { QGraphicsPathItem::setZValue(+1.0); const QPalette pal; setForeground(pal.buttonText().color()); setBackground(pal.button().color()); m_text = new QGraphicsTextItem(this); QGraphicsPathItem::setFlag(QGraphicsItem::ItemIsSelectable); QGraphicsPathItem::setFlag(QGraphicsItem::ItemSendsScenePositionChanges); QGraphicsPathItem::setAcceptHoverEvents(true); setPortTitle(QString()); } // Destructor. qpwgraph_port::~qpwgraph_port (void) { removeConnects(); // No actual need to destroy any children here... // //delete m_text; } // Accessors. qpwgraph_node *qpwgraph_port::portNode (void) const { return m_node; } uint qpwgraph_port::portId (void) const { return m_id; } void qpwgraph_port::setPortName ( const QString& name ) { m_name = name; QGraphicsPathItem::setToolTip(portNameLabel()); } const QString& qpwgraph_port::portName (void) const { return m_name; } void qpwgraph_port::setPortMode ( qpwgraph_item::Mode mode ) { m_mode = mode; } qpwgraph_item::Mode qpwgraph_port::portMode (void) const { return m_mode; } bool qpwgraph_port::isInput (void) const { return (m_mode & Input); } bool qpwgraph_port::isOutput (void) const { return (m_mode & Output); } void qpwgraph_port::setPortType ( uint type ) { m_type = type; } uint qpwgraph_port::portType (void) const { return m_type; } void qpwgraph_port::setPortLabel ( const QString& label ) { m_label = label; setPortTitle(QString()); // reset title. } const QString& qpwgraph_port::portLabel (void) const { return m_label; } QString qpwgraph_port::portNameLabel (void) const { QString label = m_name; if (!m_label.isEmpty()) { label += ' '; label += '['; label += m_label; label += ']'; } return label; } void qpwgraph_port::setPortTitle ( const QString& title ) { const QString& name_label = portNameLabel(); QGraphicsPathItem::setToolTip(name_label); m_title = (title.isEmpty() ? name_label : title); static const int MAX_TITLE_LENGTH = 29; static const QString ellipsis(3, '.'); QString text = m_title; const int nlength = text.indexOf(':'); if (nlength >= 0) text.remove(0, nlength + 1); if (text.length() >= MAX_TITLE_LENGTH + ellipsis.length()) text = ellipsis + text.right(MAX_TITLE_LENGTH); m_text->setPlainText(text); QPainterPath path; const QRectF& rect = m_text->boundingRect().adjusted(0, +2, 0, -2); path.addRoundedRect(rect, 5, 5); /*QGraphicsPathItem::*/setPath(path); } const QString& qpwgraph_port::portTitle (void) const { return m_title; } void qpwgraph_port::setPortIndex ( int index ) { m_index = index; } int qpwgraph_port::portIndex (void) const { return m_index; } QPointF qpwgraph_port::portPos (void) const { QPointF pos = QGraphicsPathItem::scenePos(); const QRectF& rect = itemRect(); if (m_mode == Output) pos.setX(pos.x() + rect.width()); pos.setY(pos.y() + rect.height() / 2); return pos; } // Connection-list methods. void qpwgraph_port::appendConnect ( qpwgraph_connect *connect ) { m_connects.append(connect); } void qpwgraph_port::removeConnect ( qpwgraph_connect *connect ) { m_connects.removeAll(connect); } void qpwgraph_port::removeConnects (void) { foreach (qpwgraph_connect *connect, m_connects) { if (connect->port1() != this) connect->setPort1(nullptr); if (connect->port2() != this) connect->setPort2(nullptr); } // Do not delete connects here as they are owned elsewhere... // // qDeleteAll(m_connects); m_connects.clear(); } qpwgraph_connect *qpwgraph_port::findConnect ( qpwgraph_port *port ) const { foreach (qpwgraph_connect *connect, m_connects) { if (connect->port1() == port || connect->port2() == port) return connect; } return nullptr; } // Connect-list accessor. const QList& qpwgraph_port::connects (void) const { return m_connects; } void qpwgraph_port::paint ( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget */*widget*/ ) { const QPalette& pal = option->palette; const QRectF& port_rect = itemRect(); QLinearGradient port_grad(0, port_rect.top(), 0, port_rect.bottom()); QColor port_color; if (QGraphicsPathItem::isSelected()) { m_text->setDefaultTextColor(pal.highlightedText().color()); painter->setPen(pal.highlightedText().color()); port_color = pal.highlight().color(); } else { const QColor& foreground = qpwgraph_item::foreground(); const QColor& background = qpwgraph_item::background(); const bool is_dark = (background.value() < 128); m_text->setDefaultTextColor(is_dark ? foreground.lighter() : foreground.darker()); if (qpwgraph_item::isHighlight() || QGraphicsPathItem::isUnderMouse()) { painter->setPen(foreground.lighter()); port_color = background.lighter(); } else { painter->setPen(foreground); port_color = background; } } port_grad.setColorAt(0.0, port_color); port_grad.setColorAt(1.0, port_color.darker(120)); painter->setBrush(port_grad); painter->drawPath(QGraphicsPathItem::path()); } QVariant qpwgraph_port::itemChange ( GraphicsItemChange change, const QVariant& value ) { if (change == QGraphicsItem::ItemScenePositionHasChanged) { foreach (qpwgraph_connect *connect, m_connects) { connect->updatePath(); } } else if (change == QGraphicsItem::ItemSelectedHasChanged && m_selectx < 1) { const bool is_selected = value.toBool(); setHighlightEx(is_selected); foreach (qpwgraph_connect *connect, m_connects) connect->setSelectedEx(this, is_selected); } return value; } // Selection propagation method... void qpwgraph_port::setSelectedEx ( bool is_selected ) { if (!is_selected) { foreach (qpwgraph_connect *connect, m_connects) { if (connect->isSelected()) { setHighlightEx(true); return; } } } ++m_selectx; setHighlightEx(is_selected); if (QGraphicsPathItem::isSelected() != is_selected) QGraphicsPathItem::setSelected(is_selected); --m_selectx; } // Highlighting propagation method... void qpwgraph_port::setHighlightEx ( bool is_highlight ) { if (m_hilitex > 0) return; ++m_hilitex; qpwgraph_item::setHighlight(is_highlight); foreach (qpwgraph_connect *connect, m_connects) connect->setHighlightEx(this, is_highlight); --m_hilitex; } // Special port-type color business. void qpwgraph_port::updatePortTypeColors ( qpwgraph_canvas *canvas ) { if (canvas) { const QColor& color = canvas->portTypeColor(m_type); if (color.isValid()) { const bool is_dark = (color.value() < 128); qpwgraph_item::setForeground(is_dark ? color.lighter(180) : color.darker()); qpwgraph_item::setBackground(color); if (m_mode & Output) { foreach (qpwgraph_connect *connect, m_connects) { connect->updatePortTypeColors(); connect->update(); } } } } } // Port sorting type. qpwgraph_port::SortType qpwgraph_port::g_sort_type = qpwgraph_port::PortName; void qpwgraph_port::setSortType ( SortType sort_type ) { g_sort_type = sort_type; } qpwgraph_port::SortType qpwgraph_port::sortType (void) { return g_sort_type; } // Port sorting order. qpwgraph_port::SortOrder qpwgraph_port::g_sort_order = qpwgraph_port::Ascending; void qpwgraph_port::setSortOrder( SortOrder sort_order ) { g_sort_order = sort_order; } qpwgraph_port::SortOrder qpwgraph_port::sortOrder (void) { return g_sort_order; } // Natural decimal sorting comparator (static) bool qpwgraph_port::lessThan ( qpwgraph_port *port1, qpwgraph_port *port2 ) { const int port_type_diff = int(port1->portType()) - int(port2->portType()); if (port_type_diff) return (port_type_diff > 0); if (g_sort_order == Descending) { qpwgraph_port *port = port1; port1 = port2; port2 = port; } if (g_sort_type == PortIndex) { const int port_index_diff = port1->portIndex() - port2->portIndex(); if (port_index_diff) return (port_index_diff < 0); } switch (g_sort_type) { case PortTitle: return qpwgraph_port::lessThan(port1->portTitle(), port2->portTitle()); case PortName: default: return qpwgraph_port::lessThan(port1->portName(), port2->portName()); } } bool qpwgraph_port::lessThan ( const QString& s1, const QString& s2 ) { const int n1 = s1.length(); const int n2 = s2.length(); int i1, i2; for (i1 = i2 = 0; i1 < n1 && i2 < n2; ++i1, ++i2) { // Skip (white)spaces... while (s1.at(i1).isSpace()) ++i1; while (s2.at(i2).isSpace()) ++i2; // Normalize (to uppercase) the next characters... QChar c1 = s1.at(i1).toUpper(); QChar c2 = s2.at(i2).toUpper(); if (c1.isDigit() && c2.isDigit()) { // Find the whole length numbers... int j1 = i1++; while (i1 < n1 && s1.at(i1).isDigit()) ++i1; int j2 = i2++; while (i2 < n2 && s2.at(i2).isDigit()) ++i2; // Compare as natural decimal-numbers... j1 = s1.mid(j1, i1 - j1).toInt(); j2 = s2.mid(j2, i2 - j2).toInt(); if (j1 != j2) return (j1 < j2); // Never go out of bounds... if (i1 >= n1 || i2 >= n2) break; // Go on with this next char... c1 = s1.at(i1).toUpper(); c2 = s2.at(i2).toUpper(); } // Compare this char... if (c1 != c2) return (c1 < c2); } // Probable exact match. return false; } // Rectangular editor extents. QRectF qpwgraph_port::editorRect (void) const { return QGraphicsPathItem::sceneBoundingRect(); } // end of qpwgraph_port.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph.h0000644000000000000000000000013214532447230015222 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph.h0000644000175000001440000000550314532447230016455 0ustar00rncbcusers00000000000000// qpwgraph.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_h #define __qpwgraph_h #include "config.h" #include // Foward decls. class QWidget; #ifdef CONFIG_SYSTEM_TRAY class QSharedMemory; class QLocalServer; #endif //------------------------------------------------------------------------- // Singleton application instance - decl. // class qpwgraph_application : public QApplication { Q_OBJECT public: // Constructor. qpwgraph_application(int& argc, char **argv); // Destructor. ~qpwgraph_application(); // Parse/help about command line arguments. bool parse_args(const QStringList& args); // Main application widget accessors. void setMainWidget(QWidget *widget) { m_widget = widget; } QWidget *mainWidget() const { return m_widget; } // Parsed command-line options and arguments accessors. const QString& patchbayPath() const { return m_patchbay_path; } bool isPatchbayActivatedSet() const { return m_patchbay_activated >= 0; } bool isPatchbayExclusiveSet() const { return m_patchbay_exclusive >= 0; } bool isPatchbayActivated() const { return m_patchbay_activated > 0; } bool isPatchbayExclusive() const { return m_patchbay_exclusive > 0; } bool isStartMinimized() const { return m_start_minimized; } #ifdef CONFIG_SYSTEM_TRAY // Check if another instance is running, // and raise its proper main widget... bool setupServer(); protected slots: // Local server slots. void newConnectionSlot(); void readyReadSlot(); protected: // Local server/shmem setup/cleanup. void clearServer(); #endif private: // Instance variables. QWidget *m_widget; #ifdef CONFIG_SYSTEM_TRAY QString m_unique; QSharedMemory *m_memory; QLocalServer *m_server; #endif // Parsed command-line options and arguments. QString m_patchbay_path; int m_patchbay_activated; int m_patchbay_exclusive; bool m_start_minimized; }; #endif // __qpwgraph_h // end of qpwgraph.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_item.h0000644000000000000000000000013214532447230016240 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_item.h0000644000175000001440000001001014532447230017460 0ustar00rncbcusers00000000000000// qpwgraph_item.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_item_h #define __qpwgraph_item_h #include #include #include //---------------------------------------------------------------------------- // qpwgraph_item -- Base graphics item. class qpwgraph_item : public QGraphicsPathItem { public: // Constructor. qpwgraph_item(QGraphicsItem *parent = nullptr); // Basic color accessors. void setForeground(const QColor& color); const QColor& foreground() const; void setBackground(const QColor& color); const QColor& background() const; // Marking methods. void setMarked(bool marked); bool isMarked() const; // Highlighting methods. void setHighlight(bool hilite); bool isHighlight() const; // Raise item z-value (dynamic always-on-top). void raise(); // Item modes. enum Mode { None = 0, Input = 1, Output = 2, Duplex = Input | Output }; // Item hash/map key (by id). class IdKey { public: // Constructors. IdKey (uint id, Mode mode, uint type = 0) : m_id(id), m_mode(mode), m_type(type) {} IdKey (const IdKey& key) : m_id(key.id()), m_mode(key.mode()), m_type(key.type()) {} // Key accessors. uint id() const { return m_id; } Mode mode() const { return m_mode; } uint type() const { return m_type; } // Hash/map key comparators. bool operator== (const IdKey& key) const { return IdKey::type() == key.type() && IdKey::mode() == key.mode() && IdKey::id() == key.id(); } private: // Key fields. uint m_id; Mode m_mode; uint m_type; }; typedef QHash IdKeys; // Item hash/map key (by name). class NameKey { public: // Constructors. NameKey (const QString& name, Mode mode, uint type = 0) : m_name(name), m_mode(mode), m_type(type) {} NameKey (const NameKey& key) : m_name(key.name()), m_mode(key.mode()), m_type(key.type()) {} // Key accessors. const QString& name() const { return m_name; } Mode mode() const { return m_mode; } uint type() const { return m_type; } // Hash/map key comparators. bool operator== (const NameKey& key) const { return NameKey::type() == key.type() && NameKey::mode() == key.mode() && NameKey::name() == key.name(); } private: // Key fields. QString m_name; Mode m_mode; uint m_type; }; typedef QHash NameKeys; // Item-type hash (static) static uint itemType(const QByteArray& type_name); // Rectangular editor extents. virtual QRectF editorRect() const; // Path and bounding rectangle override. void setPath(const QPainterPath& path); // Bounding rectangle accessor. const QRectF& itemRect() const; private: // Instance variables. QColor m_foreground; QColor m_background; bool m_marked; bool m_hilite; QRectF m_rect; }; // Item hash function. inline uint qHash ( const qpwgraph_item::IdKey& key ) { return qHash(key.id()) ^ qHash(uint(key.mode())) ^ qHash(key.type()); } inline uint qHash ( const qpwgraph_item::NameKey& key ) { return qHash(key.name()) ^ qHash(uint(key.mode())) ^ qHash(key.type()); } #endif // __qpwgraph_item_h // end of qpwgraph_item.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_port.h0000644000000000000000000000013214532447230016266 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_port.h0000644000175000001440000001140414532447230017516 0ustar00rncbcusers00000000000000// qpwgraph_port.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_port_h #define __qpwgraph_port_h #include "qpwgraph_item.h" // Forward decls. class qpwgraph_canvas; class qpwgraph_node; class qpwgraph_connect; class QStyleOptionGraphicsItem; //---------------------------------------------------------------------------- // qpwgraph_port -- Port graphics item. class qpwgraph_port : public qpwgraph_item { public: // Constructor. qpwgraph_port(qpwgraph_node *node, uint id, const QString& name, Mode mode, uint type = 0); // Destructor. ~qpwgraph_port(); // Graphics item type. enum { Type = QGraphicsItem::UserType + 2 }; int type() const { return Type; } // Accessors. qpwgraph_node *portNode() const; uint32_t portId() const; void setPortName(const QString& name); const QString& portName() const; void setPortMode(Mode mode); Mode portMode() const; bool isInput() const; bool isOutput() const; void setPortType(uint type); uint portType() const; void setPortLabel(const QString& label); const QString& portLabel() const; QString portNameLabel() const; void setPortTitle(const QString& title); const QString& portTitle() const; void setPortIndex(int index); int portIndex() const; QPointF portPos() const; // Connection-list methods. void appendConnect(qpwgraph_connect *connect); void removeConnect(qpwgraph_connect *connect); void removeConnects(); qpwgraph_connect *findConnect(qpwgraph_port *port) const; // Connect-list accessor. const QList& connects() const; // Selection propagation method... void setSelectedEx(bool is_selected); // Highlighting propagation method... void setHighlightEx(bool is_highlight); // Special port-type color business. void updatePortTypeColors(qpwgraph_canvas *canvas); // Port hash/map key (by id). class PortIdKey : public IdKey { public: // Constructor. PortIdKey (uint id, Mode mode, uint type = 0) : IdKey(id, mode, type) {} PortIdKey(qpwgraph_port *port) : IdKey(port->portId(), port->portMode(), port->portType()) {} }; typedef QHash PortIds; // Port hash/map key (by name). class PortNameKey : public NameKey { public: // Constructors. PortNameKey (const QString& name, Mode mode, uint type = 0) : NameKey(name, mode, type) {} PortNameKey(qpwgraph_port *port) : NameKey(port->portName(), port->portMode(), port->portType()) {} }; typedef QHash PortNames; // Port sorting type. enum SortType { PortName = 0, PortTitle, PortIndex }; static void setSortType(SortType sort_type); static SortType sortType(); // Port sorting order. enum SortOrder { Ascending = 0, Descending }; static void setSortOrder(SortOrder sort_order); static SortOrder sortOrder(); // Port sorting comparators. struct Compare { bool operator()(qpwgraph_port *port1, qpwgraph_port *port2) const { return qpwgraph_port::lessThan(port1, port2); } }; struct ComparePos { bool operator()(qpwgraph_port *port1, qpwgraph_port *port2) const { return (port1->scenePos().y() < port2->scenePos().y()); } }; // Rectangular editor extents. QRectF editorRect() const; protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); QVariant itemChange(GraphicsItemChange change, const QVariant& value); // Natural decimal sorting comparators. static bool lessThan(qpwgraph_port *port1, qpwgraph_port *port2); static bool lessThan(const QString& s1, const QString& s2); private: // instance variables. qpwgraph_node *m_node; uint m_id; QString m_name; Mode m_mode; uint m_type; QString m_label; QString m_title; int m_index; QGraphicsTextItem *m_text; QList m_connects; int m_selectx; int m_hilitex; static SortType g_sort_type; static SortOrder g_sort_order; }; #endif // __qpwgraph_port_h // end of qpwgraph_port.h qpwgraph-0.6.1/src/PaxHeaders/CMakeLists.txt0000644000000000000000000000013214532447230015760 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/CMakeLists.txt0000644000175000001440000000653514532447230017221 0ustar00rncbcusers00000000000000# project (qpwgraph) set (CMAKE_INCLUDE_CURRENT_DIR ON) set (CMAKE_AUTOUIC ON) set (CMAKE_AUTOMOC ON) set (CMAKE_AUTORCC ON) if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/config.h) file (REMOVE ${CMAKE_CURRENT_SOURCE_DIR}/config.h) endif () configure_file (config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) set (HEADERS qpwgraph.h qpwgraph_config.h qpwgraph_canvas.h qpwgraph_command.h qpwgraph_connect.h qpwgraph_port.h qpwgraph_node.h qpwgraph_item.h qpwgraph_sect.h qpwgraph_pipewire.h qpwgraph_alsamidi.h qpwgraph_alsamidi.h qpwgraph_patchbay.h qpwgraph_systray.h qpwgraph_form.h ) set (SOURCES qpwgraph.cpp qpwgraph_config.cpp qpwgraph_canvas.cpp qpwgraph_command.cpp qpwgraph_connect.cpp qpwgraph_port.cpp qpwgraph_node.cpp qpwgraph_item.cpp qpwgraph_sect.cpp qpwgraph_pipewire.cpp qpwgraph_alsamidi.cpp qpwgraph_patchbay.cpp qpwgraph_systray.cpp qpwgraph_form.cpp ) set (FORMS qpwgraph_form.ui ) set (RESOURCES qpwgraph.qrc ) add_executable (${PROJECT_NAME} ${HEADERS} ${SOURCES} ${FORMS} ${RESOURCES} ) # Add some debugger flags. if (CONFIG_DEBUG AND UNIX AND NOT APPLE) set (CONFIG_DEBUG_OPTIONS -g -fsanitize=address -fno-omit-frame-pointer) target_compile_options (${PROJECT_NAME} PRIVATE ${CONFIG_DEBUG_OPTIONS}) target_link_options (${PROJECT_NAME} PRIVATE ${CONFIG_DEBUG_OPTIONS}) endif () set_target_properties (${PROJECT_NAME} PROPERTIES C_STANDARD 99) set_target_properties (${PROJECT_NAME} PROPERTIES CXX_STANDARD 17) include(FindPkgConfig) pkg_check_modules (PIPEWIRE REQUIRED IMPORTED_TARGET libpipewire-0.3) if (PIPEWIRE_FOUND) target_link_libraries (${PROJECT_NAME} PRIVATE PkgConfig::PIPEWIRE) else () message (WARNING "*** PipeWire library not found.") endif () if (CONFIG_ALSA_MIDI) pkg_check_modules (ALSA REQUIRED IMPORTED_TARGET alsa) if (ALSA_FOUND) target_link_libraries (${PROJECT_NAME} PRIVATE PkgConfig::ALSA) endif () endif () target_link_libraries (${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) target_link_libraries (${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Xml) target_link_libraries (${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Svg) if (CONFIG_SYSTEM_TRAY) target_link_libraries (${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Network) endif () install (TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install (FILES images/${PROJECT_NAME}.png RENAME org.rncbc.${PROJECT_NAME}.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/32x32/apps) install (FILES images/${PROJECT_NAME}.svg RENAME org.rncbc.${PROJECT_NAME}.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) install (FILES appdata/org.rncbc.${PROJECT_NAME}.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install (FILES appdata/org.rncbc.${PROJECT_NAME}.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install (FILES mimetypes/org.rncbc.${PROJECT_NAME}.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages) install (FILES mimetypes/org.rncbc.${PROJECT_NAME}.application-x-${PROJECT_NAME}-patchbay.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/32x32/mimetypes) install (FILES mimetypes/org.rncbc.${PROJECT_NAME}.application-x-${PROJECT_NAME}-patchbay.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/mimetypes) install (FILES man1/qpwgraph.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_node.cpp0000644000000000000000000000013214532447230016562 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_node.cpp0000644000175000001440000002247214532447230020021 0ustar00rncbcusers00000000000000// qpwgraph_node.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_node.h" #include #include #include #include #include #include #include //---------------------------------------------------------------------------- // qpwgraph_node -- Node graphics item. // Constructor. qpwgraph_node::qpwgraph_node ( uint id, const QString& name, qpwgraph_item::Mode mode, uint type ) : qpwgraph_item(nullptr), m_id(id), m_name(name), m_mode(mode), m_type(type) { QGraphicsPathItem::setZValue(0.0); const QPalette pal; const int base_value = pal.base().color().value(); const bool is_dark = (base_value < 128); const QColor& text_color = pal.text().color(); QColor foreground_color(is_dark ? text_color.darker() : text_color); qpwgraph_item::setForeground(foreground_color); const QColor& window_color = pal.window().color(); QColor background_color(is_dark ? window_color.lighter() : window_color); background_color.setAlpha(160); qpwgraph_item::setBackground(background_color); m_pixmap = new QGraphicsPixmapItem(this); m_text = new QGraphicsTextItem(this); QGraphicsPathItem::setFlag(QGraphicsItem::ItemIsMovable); QGraphicsPathItem::setFlag(QGraphicsItem::ItemIsSelectable); setNodeTitle(QString()); const bool is_darkest = (base_value < 24); QColor shadow_color = (is_darkest ? Qt::white : Qt::black); shadow_color.setAlpha(180); QGraphicsDropShadowEffect *effect = new QGraphicsDropShadowEffect(); effect->setColor(shadow_color); effect->setBlurRadius(is_darkest ? 8 : 16); effect->setOffset(is_darkest ? 0 : 2); QGraphicsPathItem::setGraphicsEffect(effect); qpwgraph_item::raise(); } // Destructor. qpwgraph_node::~qpwgraph_node (void) { removePorts(); // No actual need to destroy any children here... // // QGraphicsPathItem::setGraphicsEffect(nullptr); // delete m_text; // delete m_pixmap; } // accessors. uint qpwgraph_node::nodeId (void) const { return m_id; } void qpwgraph_node::setNodeName ( const QString& name ) { m_name = name; QGraphicsPathItem::setToolTip(nodeNameLabel()); } const QString& qpwgraph_node::nodeName (void) const { return m_name; } void qpwgraph_node::setNodeMode ( qpwgraph_item::Mode mode ) { m_mode = mode; } qpwgraph_item::Mode qpwgraph_node::nodeMode (void) const { return m_mode; } void qpwgraph_node::setNodeType ( uint type ) { m_type = type; } uint qpwgraph_node::nodeType (void) const { return m_type; } void qpwgraph_node::setNodeIcon ( const QIcon& icon ) { m_icon = icon; m_pixmap->setPixmap(m_icon.pixmap(24, 24)); } const QIcon& qpwgraph_node::nodeIcon (void) const { return m_icon; } void qpwgraph_node::setNodeLabel ( const QString& label ) { m_label = label; setNodeTitle(QString()); // reset title. } const QString& qpwgraph_node::nodeLabel (void) const { return m_label; } QString qpwgraph_node::nodeNameLabel (void) const { QString label = m_name; if (!m_label.isEmpty()) { label += ' '; label += '['; label += m_label; label += ']'; } return label; } void qpwgraph_node::setNodeTitle ( const QString& title ) { const QString& name_label = nodeNameLabel(); QGraphicsPathItem::setToolTip(name_label); const QFont& font = m_text->font(); m_text->setFont(QFont(font.family(), font.pointSize(), QFont::Bold)); m_title = (title.isEmpty() ? name_label : title); static const int MAX_TITLE_LENGTH = 29; static const QString ellipsis(3, '.'); QString text = m_title; if (text.length() >= MAX_TITLE_LENGTH + ellipsis.length()) text = text.left(MAX_TITLE_LENGTH) + ellipsis; m_text->setPlainText(text); } const QString& qpwgraph_node::nodeTitle (void) const { return m_title; } // Port-list methods. qpwgraph_port *qpwgraph_node::addPort ( uint id, const QString& name, qpwgraph_item::Mode mode, int type ) { qpwgraph_port *port = new qpwgraph_port(this, id, name, mode, type); m_ports.append(port); m_port_ids.insert(qpwgraph_port::PortIdKey(port), port); m_port_names.insert(qpwgraph_port::PortNameKey(port), port); updatePath(); return port; } qpwgraph_port *qpwgraph_node::addInputPort ( uint id, const QString& name, int type ) { return addPort(id, name, qpwgraph_item::Input, type); } qpwgraph_port *qpwgraph_node::addOutputPort ( uint id, const QString& name, int type ) { return addPort(id, name, qpwgraph_item::Output, type); } void qpwgraph_node::removePort ( qpwgraph_port *port ) { m_port_names.remove(qpwgraph_port::PortNameKey(port)); m_port_ids.remove(qpwgraph_port::PortIdKey(port)); m_ports.removeAll(port); updatePath(); } void qpwgraph_node::removePorts (void) { foreach (qpwgraph_port *port, m_ports) port->removeConnects(); // Do not delete ports here as they are node's child items... // //qDeleteAll(m_ports); m_ports.clear(); m_port_ids.clear(); m_port_names.clear(); } // Port finder (by id/name, mode and type) qpwgraph_port *qpwgraph_node::findPort ( uint id, qpwgraph_item::Mode mode, uint type ) { return m_port_ids.value(qpwgraph_port::PortIdKey(id, mode, type), nullptr); } qpwgraph_port *qpwgraph_node::findPort ( const QString& name, qpwgraph_item::Mode mode, uint type ) { return m_port_names.value(qpwgraph_port::PortNameKey(name, mode, type), nullptr); } // Port-list accessor. const QList& qpwgraph_node::ports (void) const { return m_ports; } // Reset port markings, destroy if unmarked. void qpwgraph_node::resetPorts (void) { QList ports; foreach (qpwgraph_port *port, m_ports) { if (port->isMarked()) { port->setMarked(false); } else { ports.append(port); } } foreach (qpwgraph_port *port, ports) { port->removeConnects(); removePort(port); delete port; } } // Path/shape updater. void qpwgraph_node::updatePath (void) { const QRectF& rect = m_text->boundingRect(); int width = rect.width() / 2 + 24; int wi, wo; wi = wo = width; foreach (qpwgraph_port *port, m_ports) { const int w = port->itemRect().width(); if (port->isOutput()) { if (wo < w) wo = w; } else { if (wi < w) wi = w; } } width = wi + wo; std::sort(m_ports.begin(), m_ports.end(), qpwgraph_port::Compare()); int height = rect.height() + 2; int type = 0; int yi, yo; yi = yo = height; foreach (qpwgraph_port *port, m_ports) { const QRectF& port_rect = port->itemRect(); const int w = port_rect.width(); const int h = port_rect.height() + 1; if (type - port->portType()) { type = port->portType(); height += 2; yi = yo = height; } if (port->isOutput()) { port->setPos(+width / 2 + 6 - w, yo); yo += h; if (height < yo) height = yo; } else { port->setPos(-width / 2 - 6, yi); yi += h; if (height < yi) height = yi; } } QPainterPath path; path.addRoundedRect(-width / 2, 0, width, height + 6, 5, 5); /*QGraphicsPathItem::*/setPath(path); } void qpwgraph_node::paint ( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget */*widget*/ ) { const QPalette& pal = option->palette; const QRectF& node_rect = itemRect(); QLinearGradient node_grad(0, node_rect.top(), 0, node_rect.bottom()); QColor node_color; if (QGraphicsPathItem::isSelected()) { const QColor& hilitetext_color = pal.highlightedText().color(); m_text->setDefaultTextColor(hilitetext_color); painter->setPen(hilitetext_color); node_color = pal.highlight().color(); } else { const QColor& foreground = qpwgraph_item::foreground(); const QColor& background = qpwgraph_item::background(); const bool is_dark = (background.value() < 192); m_text->setDefaultTextColor(is_dark ? foreground.lighter() : foreground.darker()); painter->setPen(foreground); node_color = background; } node_color.setAlpha(180); node_grad.setColorAt(0.6, node_color); node_grad.setColorAt(1.0, node_color.darker(120)); painter->setBrush(node_grad); painter->drawPath(QGraphicsPathItem::path()); m_pixmap->setPos(node_rect.x() + 4, node_rect.y() + 4); const QRectF& text_rect = m_text->boundingRect(); m_text->setPos(- text_rect.width() / 2, text_rect.y() + 2); } QVariant qpwgraph_node::itemChange ( GraphicsItemChange change, const QVariant& value ) { if (change == QGraphicsItem::ItemSelectedHasChanged) { const bool is_selected = value.toBool(); foreach (qpwgraph_port *port, m_ports) port->setSelected(is_selected); } return value; } // Rectangular editor extents. QRectF qpwgraph_node::editorRect (void) const { return m_text->sceneBoundingRect(); } // end of qpwgraph_node.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_sect.h0000644000000000000000000000013214532447230016240 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_sect.h0000644000175000001440000000375414532447230017501 0ustar00rncbcusers00000000000000// qpwgraph_sect.h // /**************************************************************************** Copyright (C) 2021-2022, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_sect_h #define __qpwgraph_sect_h #include "qpwgraph_node.h" #include #include // Forwards decls. class qpwgraph_canvas; //---------------------------------------------------------------------------- // qpwgraph_sect -- Generic graph driver class qpwgraph_sect : public QObject { Q_OBJECT public: // Constructor. qpwgraph_sect(qpwgraph_canvas *canvas); // Accessors. qpwgraph_canvas *canvas() const; // Generic sect/graph methods. void addItem(qpwgraph_item *item, bool is_new = true); void removeItem(qpwgraph_item *item); // Clean-up all un-marked items... void resetItems(uint node_type); void clearItems(uint node_type); // Special node finder. qpwgraph_node *findNode(uint id, qpwgraph_item::Mode mode, uint type = 0) const; // Client/port renaming method. virtual void renameItem(qpwgraph_item *item, const QString& name); private: // Instance variables. qpwgraph_canvas *m_canvas; QList m_connects; }; #endif // __qpwgraph_sect_h // end of qpwgraph_sect.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_connect.h0000644000000000000000000000013214532447230016733 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_connect.h0000644000175000001440000000525414532447230020171 0ustar00rncbcusers00000000000000// qpwgraph_connect.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_connect_h #define __qpwgraph_connect_h #include "qpwgraph_item.h" // Forward decls. class qpwgraph_port; //---------------------------------------------------------------------------- // qpwgraph_connect -- Connection-line graphics item. class qpwgraph_connect : public qpwgraph_item { public: // Constructor. qpwgraph_connect(); // Destructor.. ~qpwgraph_connect(); // Graphics item type. enum { Type = QGraphicsItem::UserType + 3 }; int type() const { return Type; } // Accessors. void setPort1(qpwgraph_port *port); qpwgraph_port *port1() const; void setPort2(qpwgraph_port *port); qpwgraph_port *port2() const; // Active disconnection. void disconnect(); // Path/shaper updaters. void updatePathTo(const QPointF& pos); void updatePath(); // Selection propagation method... void setSelectedEx(qpwgraph_port *port, bool is_selected); // Highlighting propagation method... void setHighlightEx(qpwgraph_port *port, bool is_highlight); // Special port-type color business. void updatePortTypeColors(); // Dim/transparency option. void setDimmed(bool dimmed); int isDimmed() const; // Connector curve draw style (through vs. around nodes) static void setConnectThroughNodes(bool on); static bool isConnectThroughNodes(); protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); QVariant itemChange(GraphicsItemChange change, const QVariant& value); QPainterPath shape() const; private: // Instance variables. qpwgraph_port *m_port1; qpwgraph_port *m_port2; bool m_dimmed; // Connector curve draw style (through vs. around nodes) static bool g_connect_through_nodes; }; #endif // __qpwgraph_connect_h // end of qpwgraph_connect.h qpwgraph-0.6.1/src/PaxHeaders/config.h.cmake0000644000000000000000000000013214532447230015715 xustar0030 mtime=1701465752.091364752 30 atime=1701465752.091364752 30 ctime=1701465752.091364752 qpwgraph-0.6.1/src/config.h.cmake0000644000175000001440000000147614532447230017155 0ustar00rncbcusers00000000000000#ifndef __CONFIG_H #define __CONFIG_H /* Define to the name of this package. */ #cmakedefine PROJECT_NAME "@PROJECT_NAME@" /* Define to the version of this package. */ #cmakedefine PROJECT_VERSION "@PROJECT_VERSION@" /* Define to the description of this package. */ #cmakedefine PROJECT_DESCRIPTION "@PROJECT_DESCRIPTION@" /* Define to the homepage of this package. */ #cmakedefine PROJECT_HOMEPAGE_URL "@PROJECT_HOMEPAGE_URL@" /* Define if debugging is enabled. */ #cmakedefine CONFIG_DEBUG @CONFIG_DEBUG@ /* Define if ALSA MIDI support is available. */ #cmakedefine CONFIG_ALSA_MIDI @CONFIG_ALSA_MIDI@ /* Define if system-tray icon support is available. */ #cmakedefine CONFIG_SYSTEM_TRAY @CONFIG_SYSTEM_TRAY@ /* Define if Wayland is supported */ #cmakedefine CONFIG_WAYLAND @CONFIG_WAYLAND@ #endif // __CONFIG_H qpwgraph-0.6.1/src/PaxHeaders/qpwgraph.cpp0000644000000000000000000000013214532447230015555 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph.cpp0000644000175000001440000001754214532447230017016 0ustar00rncbcusers00000000000000// qpwgraph.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph.h" #include "qpwgraph_form.h" #include #include #include #include #ifdef CONFIG_SYSTEM_TRAY #include #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) #include #endif #include #include #include #endif //------------------------------------------------------------------------- // Singleton application instance - impl. // // Constructor. qpwgraph_application::qpwgraph_application ( int& argc, char **argv ) : QApplication(argc, argv), m_widget(nullptr) #ifdef CONFIG_SYSTEM_TRAY , m_memory(nullptr), m_server(nullptr) #endif , m_patchbay_activated(-1) , m_patchbay_exclusive(-1) , m_start_minimized(false) { QApplication::setApplicationName(PROJECT_NAME); QApplication::setApplicationDisplayName(PROJECT_DESCRIPTION); QApplication::setDesktopFileName( QString("org.rncbc.%1").arg(PROJECT_NAME)); QString version(PROJECT_VERSION); version += '\n'; version += QString("Qt: %1").arg(qVersion()); #if defined(QT_STATIC) version += "-static"; #endif version += '\n'; version += QString("libpipewire: %1 (headers: %2)") .arg(pw_get_library_version()) .arg(pw_get_headers_version()); QApplication::setApplicationVersion(version); } // Destructor. qpwgraph_application::~qpwgraph_application (void) { #ifdef CONFIG_SYSTEM_TRAY clearServer(); #endif } // Parse command line arguments. bool qpwgraph_application::parse_args ( const QStringList& args ) { QCommandLineParser parser; parser.setApplicationDescription( PROJECT_NAME " - " + QObject::tr(PROJECT_DESCRIPTION)); const QString s_activated = "activated"; const QString s_deactivated = "de" + s_activated; const QString s_exclusive = "exclusive"; const QString s_nonexclusive = "non" + s_exclusive; const QString s_minimized = "minimized"; parser.addOption({{"a", s_activated}, QObject::tr("Activated patchbay.")}); parser.addOption({{"d", s_deactivated}, QObject::tr("Deactivated patchbay.")}); parser.addOption({{"x", s_exclusive}, QObject::tr("Exclusive patchbay.")}); parser.addOption({{"n", s_nonexclusive}, QObject::tr("Non-exclusive patchbay.")}); parser.addOption({{"m", s_minimized}, QObject::tr("Start minimized.")}); parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument("patchbay-file", QObject::tr("Patchbay file (.%1)") .arg(QString(PROJECT_NAME).toLower()), QObject::tr("[patchbay-file]")); parser.process(args); if (parser.isSet(s_activated)) m_patchbay_activated = 1; else if (parser.isSet(s_deactivated)) m_patchbay_activated = 0; if (parser.isSet(s_exclusive)) m_patchbay_exclusive = 1; else if (parser.isSet(s_nonexclusive)) m_patchbay_exclusive = 0; m_start_minimized = parser.isSet(s_minimized); int nargs = 0; m_patchbay_path.clear(); foreach (const QString& arg, parser.positionalArguments()) { if (nargs > 0) m_patchbay_path += ' '; m_patchbay_path += arg; ++nargs; } // Alright with argument parsing. return true; } #ifdef CONFIG_SYSTEM_TRAY // Check if another instance is running, // and raise its proper main widget... bool qpwgraph_application::setupServer (void) { clearServer(); m_unique = QCoreApplication::applicationName(); QString uname = QString::fromUtf8(::getenv("USER")); if (uname.isEmpty()) uname = QString::fromUtf8(::getenv("USERNAME")); if (!uname.isEmpty()) { m_unique += ':'; m_unique += uname; } m_unique += '@'; m_unique += QHostInfo::localHostName(); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) const QNativeIpcKey native_key = QSharedMemory::legacyNativeKey(m_unique); m_memory = new QSharedMemory(native_key); #else #if defined(Q_OS_UNIX) m_memory = new QSharedMemory(m_unique); m_memory->attach(); delete m_memory; #endif m_memory = new QSharedMemory(m_unique); #endif bool is_server = false; const qint64 pid = QCoreApplication::applicationPid(); struct Data { qint64 pid; }; if (m_memory->create(sizeof(Data))) { m_memory->lock(); Data *data = static_cast (m_memory->data()); if (data) { data->pid = pid; is_server = true; } m_memory->unlock(); } else if (m_memory->attach()) { m_memory->lock(); // maybe not necessary? Data *data = static_cast (m_memory->data()); if (data) is_server = (data->pid == pid); m_memory->unlock(); } if (is_server) { QLocalServer::removeServer(m_unique); m_server = new QLocalServer(); m_server->setSocketOptions(QLocalServer::UserAccessOption); m_server->listen(m_unique); QObject::connect(m_server, SIGNAL(newConnection()), SLOT(newConnectionSlot())); } else { QLocalSocket socket; socket.connectToServer(m_unique); if (socket.state() == QLocalSocket::ConnectingState) socket.waitForConnected(200); if (socket.state() == QLocalSocket::ConnectedState) { socket.write(QCoreApplication::arguments().join(' ').toUtf8()); socket.flush(); socket.waitForBytesWritten(200); } } return is_server; } // Local server/shmem cleanup. void qpwgraph_application::clearServer (void) { if (m_server) { m_server->close(); delete m_server; m_server = nullptr; } if (m_memory) { delete m_memory; m_memory = nullptr; } m_unique.clear(); } // Local server connection slot. void qpwgraph_application::newConnectionSlot (void) { QLocalSocket *socket = m_server->nextPendingConnection(); QObject::connect(socket, SIGNAL(readyRead()), SLOT(readyReadSlot())); } // Local server data-ready slot. void qpwgraph_application::readyReadSlot (void) { QLocalSocket *socket = qobject_cast (sender()); if (socket) { const qint64 nread = socket->bytesAvailable(); if (nread > 0) { const QByteArray data = socket->read(nread); // Parse and apply passed command-line arguments... qpwgraph_form *form = static_cast (m_widget); if (form && parse_args(QString(data).split(' '))) form->apply_args(this); // Just make it always shows up fine... if (m_widget && !m_start_minimized) { m_widget->showNormal(); m_widget->raise(); m_widget->activateWindow(); } // Reset server... setupServer(); } } } #endif // CONFIG_SYSTEM_TRAY //---------------------------------------------------------------------------- // main. int main ( int argc, char *argv[] ) { Q_INIT_RESOURCE(qpwgraph); #if defined(Q_OS_LINUX) && !defined(CONFIG_WAYLAND) ::setenv("QT_QPA_PLATFORM", "xcb", 0); #endif qpwgraph_application app(argc, argv); if (!app.parse_args(app.arguments())) { app.quit(); return 1; } #ifdef CONFIG_SYSTEM_TRAY // Have another instance running? if (!app.setupServer()) { app.quit(); return 2; } #endif qpwgraph_form form; app.setMainWidget(&form); form.apply_args(&app); // Setup session manager shutdown (eg. logoff)... QObject::connect( &app, SIGNAL(commitDataRequest(QSessionManager&)), &form, SLOT(commitData(QSessionManager&)), Qt::DirectConnection); return app.exec(); } // end of qpwgraph.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_form.h0000644000000000000000000000013214532447230016245 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_form.h0000644000175000001440000001265514532447230017506 0ustar00rncbcusers00000000000000// qpwgraph_form.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_form_h #define __qpwgraph_form_h #include "ui_qpwgraph_form.h" // Forward decls. class qpwgraph_application; class qpwgraph_config; class qpwgraph_sect; class qpwgraph_pipewire; class qpwgraph_alsamidi; class qpwgraph_item; class qpwgraph_port; class qpwgraph_connect; class qpwgraph_systray; class QResizeEvent; class QCloseEvent; class QSlider; class QSpinBox; class QComboBox; class QActionGroup; class QSessionManager; //---------------------------------------------------------------------------- // qpwgraph_form -- UI wrapper form. class qpwgraph_form : public QMainWindow { Q_OBJECT public: // Constructor. qpwgraph_form(QWidget *parent = nullptr, Qt::WindowFlags wflags = Qt::WindowFlags()); // Destructor. ~qpwgraph_form(); // Take care of command line options and arguments... void apply_args(qpwgraph_application *app); protected slots: // Node life-cycle slots void added(qpwgraph_node *node); void updated(qpwgraph_node *node); void removed(qpwgraph_node *node); // Port (dis)connection slots. void connected(qpwgraph_port *port1, qpwgraph_port *port2); void disconnected(qpwgraph_port *port1, qpwgraph_port *port2); void connected(qpwgraph_connect *connect); // Item renaming slot. void renamed(qpwgraph_item *item, const QString& name); // Graph section slots. void pipewire_changed(); void alsamidi_changed(); // Pseudo-asynchronous timed refreshner. void refresh(); // Graph selection change slot. void stabilize(); // Tool-bar orientation change slot. void orientationChanged(Qt::Orientation orientation); // Patchbay menu slots. void patchbayNew(); void patchbayOpen(); void patchbayOpenRecent(); void patchbaySave(); void patchbaySaveAs(); void patchbayActivated(bool on); void patchbayExclusive(bool on); void patchbayEdit(bool on); void patchbayPin(); void patchbayUnpin(); void patchbayAutoPin(bool on); void patchbayAutoDisconnect(bool on); // Main menu slots. void viewMenubar(bool on); void viewGraphToolbar(bool on); void viewPatchbayToolbar(bool on); void viewStatusbar(bool on); void viewTextBesideIcons(bool on); void viewCenter(); void viewRefresh(); void viewZoomRange(bool on); void viewSortTypeAction(); void viewSortOrderAction(); void viewColorsAction(); void viewColorsReset(); void viewRepelOverlappingNodes(bool on); void viewConnectThroughNodes(bool on); void helpSystemTray(bool on); void helpAlsaMidi(bool on); void helpAbout(); void helpAboutQt(); void zoomValueChanged(int zoom_value); void patchbayNameChanged(int index); // Update patchbay recent files menu. void updatePatchbayMenu(); public slots: void closeQuit(); void commitData(QSessionManager& sm); protected: // Open/save patchbay file. bool patchbayOpenFile(const QString& path, bool clear = true); bool patchbaySaveFile(const QString& path); // Get the current display file-name. QString patchbayFileName() const; // Get the current default directory/path. QString patchbayFileDir() const; // Get default patchbay file extension/filter. QString patchbayFileExt() const; QString patchbayFileFilter() const; // Whether we can close/quit current patchbay. bool patchbayQueryClose(); bool patchbayQueryQuit(); // Context-menu event handler. void contextMenuEvent(QContextMenuEvent *event); // Widget resize event handler. void resizeEvent(QResizeEvent *event); // Widget event handlers. void showEvent(QShowEvent *event); void hideEvent(QHideEvent *event); void closeEvent(QCloseEvent *event); // Special port-type color method. void updateViewColorsAction(QAction *action); void updateViewColors(); // Update patchbay names combo-box (toolbar). void updatePatchbayNames(); // Item sect predicate. qpwgraph_sect *item_sect(qpwgraph_item *item) const; // Restore/save whole form state... void restoreState(); void saveState(); private: // The Qt-designer UI struct... Ui::qpwgraph_form m_ui; // Instance variables. qpwgraph_config *m_config; qpwgraph_pipewire *m_pipewire; qpwgraph_alsamidi *m_alsamidi; int m_pipewire_changed; int m_alsamidi_changed; int m_ins, m_mids, m_outs; int m_repel_overlapping_nodes; QSlider *m_zoom_slider; QSpinBox *m_zoom_spinbox; QActionGroup *m_sort_type; QActionGroup *m_sort_order; QString m_patchbay_dir; QString m_patchbay_path; int m_patchbay_untitled; QComboBox *m_patchbay_names; QAction *m_patchbay_names_tool; qpwgraph_systray *m_systray; bool m_systray_closed; }; #endif // __qpwgraph_form_h // end of qpwgraph_form.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_command.cpp0000644000000000000000000000013214532447230017253 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_command.cpp0000644000175000001440000001565514532447230020517 0ustar00rncbcusers00000000000000// qpwgraph_command.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_command.h" #include "qpwgraph_canvas.h" //---------------------------------------------------------------------------- // qpwgraph_command -- Generic graph command pattern // Constructor. qpwgraph_command::qpwgraph_command ( qpwgraph_canvas *canvas, QUndoCommand *parent ) : QUndoCommand(parent), m_canvas(canvas) { } // Command methods. void qpwgraph_command::undo (void) { execute(true); } void qpwgraph_command::redo (void) { execute(false); } //---------------------------------------------------------------------------- // qpwgraph_connect_command -- Connect graph command pattern // Constructor. qpwgraph_connect_command::qpwgraph_connect_command ( qpwgraph_canvas *canvas, qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect, qpwgraph_command *parent ) : qpwgraph_command(canvas, parent), m_item(port1, port2, is_connect) { } // Command executive bool qpwgraph_connect_command::execute ( bool is_undo ) { qpwgraph_canvas *canvas = qpwgraph_command::canvas(); if (canvas == nullptr) return false; qpwgraph_node *node1 = canvas->findNode( m_item.addr1.node_id, qpwgraph_item::Output, m_item.addr1.node_type); if (node1 == nullptr) node1 = canvas->findNode( m_item.addr1.node_id, qpwgraph_item::Duplex, m_item.addr1.node_type); if (node1 == nullptr) return false; qpwgraph_port *port1 = node1->findPort( m_item.addr1.port_id, qpwgraph_item::Output, m_item.addr1.port_type); if (port1 == nullptr) return false; qpwgraph_node *node2 = canvas->findNode( m_item.addr2.node_id, qpwgraph_item::Input, m_item.addr2.node_type); if (node2 == nullptr) node2 = canvas->findNode( m_item.addr2.node_id, qpwgraph_item::Duplex, m_item.addr2.node_type); if (node2 == nullptr) return false; qpwgraph_port *port2 = node2->findPort( m_item.addr2.port_id, qpwgraph_item::Input, m_item.addr2.port_type); if (port2 == nullptr) return false; const bool is_connect = (m_item.is_connect() && !is_undo) || (!m_item.is_connect() && is_undo); canvas->emitConnectPorts(port1, port2, is_connect); return true; } //---------------------------------------------------------------------------- // qpwgraph_move_command -- Move (node) graph command // Constructor. qpwgraph_move_command::qpwgraph_move_command ( qpwgraph_canvas *canvas, const QList& nodes, const QPointF& pos1, const QPointF& pos2, qpwgraph_command *parent ) : qpwgraph_command(canvas, parent), m_nexec(0) { qpwgraph_command::setText(QObject::tr("Move")); const QPointF delta = (pos1 - pos2); foreach (qpwgraph_node *node, nodes) { Item *item = new Item; item->node_id = node->nodeId(); item->node_mode = node->nodeMode(); item->node_type = node->nodeType(); const QPointF& pos = node->pos(); item->node_pos1 = pos + delta; item->node_pos2 = pos; m_items.insert(node, item); } if (canvas && canvas->isRepelOverlappingNodes()) { foreach (qpwgraph_node *node, nodes) canvas->repelOverlappingNodes(node, this); } } // Destructor. qpwgraph_move_command::~qpwgraph_move_command (void) { qDeleteAll(m_items); m_items.clear(); } // Add/replace (an already moved) node position for undo/redo... void qpwgraph_move_command::addItem ( qpwgraph_node *node, const QPointF& pos1, const QPointF& pos2 ) { Item *item = m_items.value(node, nullptr); if (item) { // item->node_pos1 = pos1; item->node_pos2 = pos2;//node->pos(); } else { item = new Item; item->node_id = node->nodeId(); item->node_mode = node->nodeMode(); item->node_type = node->nodeType(); item->node_pos1 = pos1; item->node_pos2 = pos2;//node->pos(); m_items.insert(node, item); } } // Command executive method. bool qpwgraph_move_command::execute ( bool /* is_undo */ ) { qpwgraph_canvas *canvas = qpwgraph_command::canvas(); if (canvas == nullptr) return false; if (++m_nexec > 1) { foreach (qpwgraph_node *key, m_items.keys()) { Item *item = m_items.value(key, nullptr); if (item) { qpwgraph_node *node = canvas->findNode( item->node_id, item->node_mode, item->node_type); if (node) { const QPointF pos1 = item->node_pos1; node->setPos(pos1); item->node_pos1 = item->node_pos2; item->node_pos2 = pos1; } } } } return true; } //---------------------------------------------------------------------------- // qpwgraph_rename_command -- Rename (item) graph command // Constructor. qpwgraph_rename_command::qpwgraph_rename_command ( qpwgraph_canvas *canvas, qpwgraph_item *item, const QString& name, qpwgraph_command *parent ) : qpwgraph_command(canvas, parent), m_name(name) { qpwgraph_command::setText(QObject::tr("Rename")); m_item.item_type = item->type(); qpwgraph_node *node = nullptr; qpwgraph_port *port = nullptr; if (m_item.item_type == qpwgraph_node::Type) node = static_cast (item); else if (m_item.item_type == qpwgraph_port::Type) port = static_cast (item); if (port) node = port->portNode(); if (node) { m_item.node_id = node->nodeId(); m_item.node_mode = node->nodeMode(); m_item.node_type = node->nodeType(); } if (port) { m_item.port_id = port->portId(); m_item.port_mode = port->portMode(); m_item.port_type = port->portType(); } } // Command executive method. bool qpwgraph_rename_command::execute ( bool /*is_undo*/ ) { qpwgraph_canvas *canvas = qpwgraph_command::canvas(); if (canvas == nullptr) return false; QString name = m_name; qpwgraph_item *item = nullptr; qpwgraph_node *node = canvas->findNode( m_item.node_id, m_item.node_mode, m_item.node_type); if (m_item.item_type == qpwgraph_node::Type && node) { m_name = node->nodeTitle(); item = node; } else if (m_item.item_type == qpwgraph_port::Type && node) { qpwgraph_port *port = node->findPort( m_item.port_id, m_item.port_mode, m_item.port_type); if (port) { m_name = port->portTitle(); item = port; } } if (item == nullptr) return false; canvas->emitRenamed(item, name); return true; } // end of qpwgraph_command.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_alsamidi.cpp0000644000000000000000000000013214532447230017420 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/qpwgraph_alsamidi.cpp0000644000175000001440000002726114532447230020660 0ustar00rncbcusers00000000000000// qpwgraph_alsamidi.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_alsamidi.h" #ifdef CONFIG_ALSA_MIDI #include "qpwgraph_canvas.h" #include "qpwgraph_connect.h" #include #include //---------------------------------------------------------------------------- // qpwgraph_alsamidi -- ALSA graph driver // Constructor. qpwgraph_alsamidi::qpwgraph_alsamidi ( qpwgraph_canvas *canvas ) : qpwgraph_sect(canvas), m_seq(nullptr), m_notifier(nullptr) { resetPortTypeColors(); open(); } // Destructor. qpwgraph_alsamidi::~qpwgraph_alsamidi (void) { close(); } // Client methods. bool qpwgraph_alsamidi::open (void) { QMutexLocker locker(&m_mutex); if (m_seq) return true; if (snd_seq_open(&m_seq, "hw", SND_SEQ_OPEN_DUPLEX, 0) < 0) { m_seq = nullptr; return false; } const int port_id = snd_seq_create_simple_port(m_seq, "qpwgraph_alsamidi", SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE | SND_SEQ_PORT_CAP_NO_EXPORT, SND_SEQ_PORT_TYPE_APPLICATION ); if (port_id < 0) { snd_seq_close(m_seq); m_seq = nullptr; return false; } snd_seq_port_subscribe_t *seq_subs; snd_seq_addr_t seq_addr; struct pollfd seq_fds[1]; snd_seq_port_subscribe_alloca(&seq_subs); seq_addr.client = SND_SEQ_CLIENT_SYSTEM; seq_addr.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE; snd_seq_port_subscribe_set_sender(seq_subs, &seq_addr); seq_addr.client = snd_seq_client_id(m_seq); seq_addr.port = port_id; snd_seq_port_subscribe_set_dest(seq_subs, &seq_addr); snd_seq_subscribe_port(m_seq, seq_subs); snd_seq_poll_descriptors(m_seq, seq_fds, 1, POLLIN); m_notifier = new QSocketNotifier(seq_fds[0].fd, QSocketNotifier::Read); QObject::connect(m_notifier, SIGNAL(activated(int)), SLOT(changedNotify())); return true; } void qpwgraph_alsamidi::close (void) { QMutexLocker locker(&m_mutex); if (m_seq == nullptr) return; if (m_notifier) { delete m_notifier; m_notifier = nullptr; } snd_seq_close(m_seq); m_seq = nullptr; } // Callback notifiers. void qpwgraph_alsamidi::changedNotify (void) { if (m_seq == nullptr) return; do { snd_seq_event_t *seq_event; snd_seq_event_input(m_seq, &seq_event); snd_seq_free_event(seq_event); } while (snd_seq_event_input_pending(m_seq, 0) > 0); emit changed(); } // ALSA port (dis)connection. void qpwgraph_alsamidi::connectPorts ( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect ) { if (m_seq == nullptr) return; if (port1 == nullptr || port2 == nullptr) return; const qpwgraph_node *node1 = port1->portNode(); const qpwgraph_node *node2 = port2->portNode(); if (node1 == nullptr || node2 == nullptr) return; QMutexLocker locker(&m_mutex); const int client_id1 = node1->nodeName().section(':', 0, 0).toInt(); const int port_id1 = port1->portName().section(':', 0, 0).toInt(); const int client_id2 = node2->nodeName().section(':', 0, 0).toInt(); const int port_id2 = port2->portName().section(':', 0, 0).toInt(); #ifdef CONFIG_DEBUG qDebug("qpwgraph_alsamidi::connectPorts(%d:%d, %d:%d, %d)", client_id1, port_id1, client_id2, port_id2, is_connect); #endif snd_seq_port_subscribe_t *seq_subs; snd_seq_addr_t seq_addr; snd_seq_port_subscribe_alloca(&seq_subs); seq_addr.client = client_id1; seq_addr.port = port_id1; snd_seq_port_subscribe_set_sender(seq_subs, &seq_addr); seq_addr.client = client_id2; seq_addr.port = port_id2; snd_seq_port_subscribe_set_dest(seq_subs, &seq_addr); if (is_connect) { snd_seq_subscribe_port(m_seq, seq_subs); } else { snd_seq_unsubscribe_port(m_seq, seq_subs); } } // ALSA node type inquirer. (static) bool qpwgraph_alsamidi::isNodeType ( uint node_type ) { return (node_type == qpwgraph_alsamidi::nodeType()); } // ALSA node type. uint qpwgraph_alsamidi::nodeType (void) { static const uint AlsaNodeType = qpwgraph_item::itemType("ALSA_NODE_TYPE"); return AlsaNodeType; } // ALSA port type inquirer. (static) bool qpwgraph_alsamidi::isPortType ( uint port_type ) { return (port_type == qpwgraph_alsamidi::midiPortType()); } // ALSA port type. uint qpwgraph_alsamidi::midiPortType (void) { static const uint AlsaMidiPortType = qpwgraph_item::itemType("ALSA_PORT_TYPE"); return AlsaMidiPortType; } // ALSA client:port finder and creator if not existing. bool qpwgraph_alsamidi::findClientPort ( snd_seq_client_info_t *client_info, snd_seq_port_info_t *port_info, qpwgraph_item::Mode port_mode, qpwgraph_node **node, qpwgraph_port **port, bool add_new ) { const int client_id = snd_seq_client_info_get_client(client_info); const int client_port_id = snd_seq_port_info_get_port(port_info); const QString& node_name = QString::number(client_id) + ':' + QString::fromUtf8(snd_seq_client_info_get_name(client_info)); const QString& port_name = QString::number(client_port_id) + ':' + QString::fromUtf8(snd_seq_port_info_get_name(port_info)); const uint node_type = qpwgraph_alsamidi::nodeType(); const uint port_type = qpwgraph_alsamidi::midiPortType(); qpwgraph_item::Mode node_mode = port_mode; const uint node_id = qHash(node_name); const uint port_id = qHash(port_name) ^ node_id; *node = qpwgraph_sect::findNode(node_id, node_mode, node_type); *port = nullptr; if (*node == nullptr && client_id >= 128) { node_mode = qpwgraph_item::Duplex; *node = qpwgraph_sect::findNode(node_id, node_mode, node_type); } if (*node) *port = (*node)->findPort(port_id, port_mode, port_type); if (add_new && *node == nullptr) { *node = new qpwgraph_node(node_id, node_name, node_mode, node_type); (*node)->setNodeIcon(QIcon(":/images/itemAlsamidi.png")); qpwgraph_sect::addItem(*node); } if (add_new && *port == nullptr && *node) { *port = (*node)->addPort(port_id, port_name, port_mode, port_type); (*port)->updatePortTypeColors(qpwgraph_sect::canvas()); qpwgraph_sect::addItem(*port); } return (*node && *port); } // ALSA graph updater. void qpwgraph_alsamidi::updateItems (void) { QMutexLocker locker(&m_mutex); if (m_seq == nullptr) return; #ifdef CONFIG_DEBUG qDebug("qpwgraph_alsamidi::updateItems()"); #endif // 1. Client/ports inventory... // snd_seq_client_info_t *client_info1; snd_seq_port_info_t *port_info1; snd_seq_client_info_alloca(&client_info1); snd_seq_port_info_alloca(&port_info1); snd_seq_client_info_set_client(client_info1, -1); while (snd_seq_query_next_client(m_seq, client_info1) >= 0) { const int client_id = snd_seq_client_info_get_client(client_info1); if (0 >= client_id) // Skip 0:System client... continue; snd_seq_port_info_set_client(port_info1, client_id); snd_seq_port_info_set_port(port_info1, -1); while (snd_seq_query_next_port(m_seq, port_info1) >= 0) { const unsigned int port_caps1 = snd_seq_port_info_get_capability(port_info1); if (port_caps1 & SND_SEQ_PORT_CAP_NO_EXPORT) continue; qpwgraph_item::Mode port_mode1 = qpwgraph_item::None; const unsigned int port_is_input = (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE); if ((port_caps1 & port_is_input) == port_is_input) { port_mode1 = qpwgraph_item::Input; qpwgraph_node *node1 = nullptr; qpwgraph_port *port1 = nullptr; if (findClientPort(client_info1, port_info1, port_mode1, &node1, &port1, true)) { node1->setMarked(true); port1->setMarked(true); } } const unsigned int port_is_output = (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ); if ((port_caps1 & port_is_output) == port_is_output) { port_mode1 = qpwgraph_item::Output; qpwgraph_node *node1 = nullptr; qpwgraph_port *port1 = nullptr; if (findClientPort(client_info1, port_info1, port_mode1, &node1, &port1, true)) { node1->setMarked(true); port1->setMarked(true); } } } } // 2. Connections inventory... // snd_seq_client_info_t *client_info2; snd_seq_port_info_t *port_info2; snd_seq_client_info_alloca(&client_info2); snd_seq_port_info_alloca(&port_info2); snd_seq_query_subscribe_t *seq_subs; snd_seq_addr_t seq_addr; snd_seq_query_subscribe_alloca(&seq_subs); snd_seq_client_info_set_client(client_info1, -1); while (snd_seq_query_next_client(m_seq, client_info1) >= 0) { const int client_id = snd_seq_client_info_get_client(client_info1); if (0 >= client_id) // Skip 0:system client... continue; snd_seq_port_info_set_client(port_info1, client_id); snd_seq_port_info_set_port(port_info1, -1); while (snd_seq_query_next_port(m_seq, port_info1) >= 0) { const unsigned int port_caps1 = snd_seq_port_info_get_capability(port_info1); if (port_caps1 & SND_SEQ_PORT_CAP_NO_EXPORT) continue; if (port_caps1 & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) { const qpwgraph_item::Mode port_mode1 = qpwgraph_item::Output; qpwgraph_node *node1 = nullptr; qpwgraph_port *port1 = nullptr; if (!findClientPort(client_info1, port_info1, port_mode1, &node1, &port1, false)) continue; snd_seq_query_subscribe_set_type(seq_subs, SND_SEQ_QUERY_SUBS_READ); snd_seq_query_subscribe_set_index(seq_subs, 0); seq_addr.client = client_id; seq_addr.port = snd_seq_port_info_get_port(port_info1); snd_seq_query_subscribe_set_root(seq_subs, &seq_addr); while (snd_seq_query_port_subscribers(m_seq, seq_subs) >= 0) { seq_addr = *snd_seq_query_subscribe_get_addr(seq_subs); if (snd_seq_get_any_client_info(m_seq, seq_addr.client, client_info2) >= 0 && snd_seq_get_any_port_info(m_seq, seq_addr.client, seq_addr.port, port_info2) >= 0) { const qpwgraph_item::Mode port_mode2 = qpwgraph_item::Input; qpwgraph_node *node2 = nullptr; qpwgraph_port *port2 = nullptr; if (findClientPort(client_info2, port_info2, port_mode2, &node2, &port2, false)) { qpwgraph_connect *connect = port1->findConnect(port2); if (connect == nullptr) { connect = new qpwgraph_connect(); connect->setPort1(port1); connect->setPort2(port2); connect->updatePortTypeColors(); connect->updatePath(); qpwgraph_sect::addItem(connect); } if (connect) connect->setMarked(true); } } snd_seq_query_subscribe_set_index(seq_subs, snd_seq_query_subscribe_get_index(seq_subs) + 1); } } } } // 3. Clean-up all un-marked items... // qpwgraph_sect::resetItems(qpwgraph_alsamidi::nodeType()); } void qpwgraph_alsamidi::clearItems (void) { QMutexLocker locker(&m_mutex); #ifdef CONFIG_DEBUG qDebug("qpwgraph_alsamidi::clearItems()"); #endif qpwgraph_sect::clearItems(qpwgraph_alsamidi::nodeType()); } // Special port-type colors defaults (virtual). void qpwgraph_alsamidi::resetPortTypeColors (void) { qpwgraph_canvas *canvas = qpwgraph_sect::canvas(); if (canvas) { canvas->setPortTypeColor( qpwgraph_alsamidi::midiPortType(), QColor(Qt::darkMagenta).darker(120)); } } #endif // CONFIG_ALSA_MIDI // end of qpwgraph_alsamidi.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_pipewire.cpp0000644000000000000000000000013214532447230017461 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_pipewire.cpp0000644000175000001440000010317114532447230020714 0ustar00rncbcusers00000000000000// qpwgraph_pipewire.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_pipewire.h" #include "qpwgraph_canvas.h" #include "qpwgraph_connect.h" #include #include #include #include // Default port types... #define DEFAULT_AUDIO_TYPE "32 bit float mono audio" #define DEFAULT_MIDI_TYPE "8 bit raw midi" #define DEFAULT_VIDEO_TYPE "32 bit float RGBA video" //---------------------------------------------------------------------------- // qpwgraph_pipewire icon cache. static QIcon qpwgraph_icon ( const QString& name ) { static QHash icon_cache; QIcon icon = icon_cache.value(name); if (icon.isNull()) { if (name.at(0) == ':') icon = QIcon(name); else icon = QIcon::fromTheme(name); if (!icon.isNull()) icon_cache.insert(name, icon); } return icon; } //---------------------------------------------------------------------------- // qpwgraph_pipewire::Data -- PipeWire graph data structs. struct qpwgraph_pipewire::Proxy { qpwgraph_pipewire *pw; struct pw_proxy *proxy; void *info; pw_destroy_t destroy; struct spa_hook proxy_listener; struct spa_hook object_listener; int pending_seq; struct spa_list pending_link; }; struct qpwgraph_pipewire::Object { enum Type { Node, Port, Link }; Object(uint oid, Type otype) : id(oid), type(otype), p(nullptr) {} virtual ~Object() { destroy_proxy(); } void create_proxy(qpwgraph_pipewire *pw); void destroy_proxy (); uint id; Type type; Proxy *p; }; struct qpwgraph_pipewire::Node : public qpwgraph_pipewire::Object { Node (uint node_id) : Object(node_id, Type::Node) {} enum NodeType { None = 0, Audio = 1, Video = 2, Midi = 4 }; struct NameKey { NameKey (Node *node) : node_name(node->node_name), node_mode(node->node_mode), node_type(node->node_type) {} NameKey (const NameKey& key) : node_name(key.node_name), node_mode(key.node_mode), node_type(key.node_type) {} bool operator== (const NameKey& key) const { return node_type == key.node_type && node_mode == key.node_mode && node_name == key.node_name; } QString node_name; qpwgraph_item::Mode node_mode; uint node_type; }; QString node_name; qpwgraph_item::Mode node_mode; NodeType node_type; QList node_ports; QIcon node_icon; QString media_name; bool node_changed; bool node_ready; uint name_num; }; struct qpwgraph_pipewire::Port : public qpwgraph_pipewire::Object { Port (uint port_id) : Object(port_id, Type::Port) {} enum Flags { None = 0, Physical = 1, Terminal = 2, Monitor = 4, Control = 8 }; uint node_id; QString port_name; qpwgraph_item::Mode port_mode; uint port_type; Flags port_flags; QList port_links; }; struct qpwgraph_pipewire::Link : public qpwgraph_pipewire::Object { Link (uint link_id) : Object(link_id, Type::Link) {} uint port1_id; uint port2_id; }; struct qpwgraph_pipewire::Data { struct pw_thread_loop *loop; struct pw_context *context; struct pw_core *core; struct spa_hook core_listener; struct pw_registry *registry; struct spa_hook registry_listener; int pending_seq; struct spa_list pending; int last_seq; int last_res; bool error; typedef QMultiHash NodeNames; NodeNames *node_names; }; inline uint qHash ( const qpwgraph_pipewire::Node::NameKey& key ) { return qHash(key.node_name) ^ qHash(uint(key.node_mode)) ^ qHash(key.node_type); } // sync-methods... static void qpwgraph_add_pending ( qpwgraph_pipewire::Proxy *p ) { qpwgraph_pipewire *pw = p->pw; qpwgraph_pipewire::Data *pd = pw->data(); if (p->pending_seq == 0) spa_list_append(&pd->pending, &p->pending_link); p->pending_seq = pw_core_sync(pd->core, 0, p->pending_seq); pd->pending_seq = p->pending_seq; } static void qpwgraph_remove_pending ( qpwgraph_pipewire::Proxy *p ) { if (p->pending_seq != 0) { spa_list_remove(&p->pending_link); p->pending_seq = 0; } } // sync-methods. // node-events... static void qpwgraph_node_event_info ( void *data, const struct pw_node_info *info ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p) { info = pw_node_info_update((struct pw_node_info *)object->p->info, info); object->p->info = (void *)info; // Get node icon and media.name, if any... if (info && (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)) { qpwgraph_pipewire::Node *node = static_cast (object); if (node) { QIcon node_icon; const char *icon_name = spa_dict_lookup(info->props, PW_KEY_APP_ICON_NAME); if (icon_name && ::strlen(icon_name) > 0) node_icon = qpwgraph_icon(icon_name); if (node_icon.isNull()) node_icon = qpwgraph_icon(node->node_name.toLower()); if (node_icon.isNull()) { const char *client_api = spa_dict_lookup(info->props, PW_KEY_CLIENT_API); if (client_api && ::strlen(client_api) > 0) { if (::strcmp(client_api, "jack") == 0 || ::strcmp(client_api, "pipewire-jack") == 0) { node_icon = qpwgraph_icon(":images/itemJack.png"); } else if (::strcmp(client_api, "pulse") == 0 || ::strcmp(client_api, "pipewire-pulse") == 0) { node_icon = qpwgraph_icon(":images/itemPulse.png"); } } } if (!node_icon.isNull()) node->node_icon = node_icon; const char *media_name = spa_dict_lookup(info->props, PW_KEY_MEDIA_NAME); if (media_name && ::strlen(media_name) > 0) node->media_name = media_name; node->node_changed = true; node->node_ready = true; if (object->p->pw) object->p->pw->changedNotify(); } } } } static const struct pw_node_events qpwgraph_node_events = { .version = PW_VERSION_NODE_EVENTS, .info = qpwgraph_node_event_info, }; // node-events. // port-events... static void qpwgraph_port_event_info ( void *data, const struct pw_port_info *info ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p) { info = pw_port_info_update((struct pw_port_info *)object->p->info, info); object->p->info = (void *)info; } } static const struct pw_port_events qpwgraph_port_events = { .version = PW_VERSION_PORT_EVENTS, .info = qpwgraph_port_event_info, }; // port-events. // link-events... static void qpwgraph_link_event_info ( void *data, const struct pw_link_info *info ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p) { info = pw_link_info_update((struct pw_link_info *)object->p->info, info); object->p->info = (void *)info; } } static const struct pw_link_events qpwgraph_link_events = { .version = PW_VERSION_LINK_EVENTS, .info = qpwgraph_link_event_info, }; // link-events. // proxy-events... static void qpwgraph_proxy_removed ( void *data ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p && object->p->proxy) { struct pw_proxy *proxy = object->p->proxy; object->p->proxy = nullptr; pw_proxy_destroy(proxy); } } static void qpwgraph_proxy_destroy ( void *data ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object) object->destroy_proxy(); } static const struct pw_proxy_events qpwgraph_proxy_events = { .version = PW_VERSION_PROXY_EVENTS, .destroy = qpwgraph_proxy_destroy, .removed = qpwgraph_proxy_removed, }; // proxy-events. // proxy-methods... void qpwgraph_pipewire::Object::create_proxy ( qpwgraph_pipewire *pw ) { if (p) return; const char *proxy_type = nullptr; uint32_t version = 0; pw_destroy_t destroy = nullptr; const void *events = nullptr; switch (type) { case Node: proxy_type = PW_TYPE_INTERFACE_Node; version = PW_VERSION_NODE; destroy = (pw_destroy_t) pw_node_info_free; events = &qpwgraph_node_events; break; case Port: proxy_type = PW_TYPE_INTERFACE_Port; version = PW_VERSION_PORT; destroy = (pw_destroy_t) pw_port_info_free; events = &qpwgraph_port_events; break; case Link: proxy_type = PW_TYPE_INTERFACE_Link; version = PW_VERSION_LINK; destroy = (pw_destroy_t) pw_link_info_free; events = &qpwgraph_link_events; break; } struct pw_proxy *proxy = (struct pw_proxy *)pw_registry_bind( pw->data()->registry, id, proxy_type, version, sizeof(Proxy)); if (proxy) p = (Proxy *)pw_proxy_get_user_data(proxy); if (p) { p->pw = pw; p->proxy = proxy; p->destroy = destroy; p->pending_seq = 0; pw_proxy_add_object_listener(proxy, &p->object_listener, events, this); pw_proxy_add_listener(proxy, &p->proxy_listener, &qpwgraph_proxy_events, this); } } void qpwgraph_pipewire::Object::destroy_proxy (void) { if (p == nullptr) return; spa_hook_remove(&p->object_listener); spa_hook_remove(&p->proxy_listener); qpwgraph_remove_pending(p); if (p->info && p->destroy) { p->destroy(p->info); p->info = nullptr; } if (p->proxy) { pw_proxy_destroy(p->proxy); p->proxy = nullptr; } p = nullptr; } // proxy-methods. // registry-events... static void qpwgraph_registry_event_global ( void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props ) { if (props == nullptr) return; qpwgraph_pipewire *pw = static_cast (data); #ifdef CONFIG_DEBUG qDebug("qpwgraph_registry_event_global[%p]: id:%u type:%s/%u", pw, id, type, version); #endif int nchanged = 0; if (::strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { QString node_name; const char *str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); if (str == nullptr) str = spa_dict_lookup(props, PW_KEY_NODE_NICK); if (str == nullptr) str = spa_dict_lookup(props, PW_KEY_NODE_NAME); if (str == nullptr) str = "node"; const char *app = spa_dict_lookup(props, PW_KEY_APP_NAME); if (app && ::strcmp(app, str) != 0) { node_name += app; node_name += '/'; } node_name += str; qpwgraph_item::Mode node_mode = qpwgraph_item::None; uint node_types = qpwgraph_pipewire::Node::None; str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); if (str) { const QString media_class(str); if (media_class.contains("Source") || media_class.contains("Output")) node_mode = qpwgraph_item::Output; else if (media_class.contains("Sink") || media_class.contains("Input")) node_mode = qpwgraph_item::Input; if (media_class.contains("Audio")) node_types |= qpwgraph_pipewire::Node::Audio; if (media_class.contains("Video")) node_types |= qpwgraph_pipewire::Node::Video; if (media_class.contains("Midi")) node_types |= qpwgraph_pipewire::Node::Midi; } if (node_mode == qpwgraph_item::None) { str = spa_dict_lookup(props, PW_KEY_MEDIA_CATEGORY); if (str) { const QString media_category(str); if (media_category.contains("Duplex")) node_mode = qpwgraph_item::Duplex; } } if (pw->createNode(id, node_name, node_mode, node_types)) ++nchanged; } else if (::strcmp(type, PW_TYPE_INTERFACE_Port) == 0) { const char *str = spa_dict_lookup(props, PW_KEY_NODE_ID); const uint node_id = (str ? uint(::atoi(str)) : 0); QString port_name; str = spa_dict_lookup(props, PW_KEY_PORT_ALIAS); if (str == nullptr) str = spa_dict_lookup(props, PW_KEY_PORT_NAME); if (str == nullptr) str = "port"; port_name += str; qpwgraph_pipewire::Node *n = pw->findNode(node_id); uint port_type = qpwgraph_pipewire::otherPortType(); str = spa_dict_lookup(props, PW_KEY_FORMAT_DSP); if (str) port_type = qpwgraph_item::itemType(str); else if (n && n->node_type == qpwgraph_pipewire::Node::Video) port_type = qpwgraph_pipewire::videoPortType(); qpwgraph_item::Mode port_mode = qpwgraph_item::None; str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION); if (str) { if (::strcmp(str, "in") == 0) port_mode = qpwgraph_item::Input; else if (::strcmp(str, "out") == 0) port_mode = qpwgraph_item::Output; } uint port_flags = qpwgraph_pipewire::Port::None; if (n && (n->node_mode != qpwgraph_item::Duplex)) port_flags |= qpwgraph_pipewire::Port::Terminal; str = spa_dict_lookup(props, PW_KEY_PORT_PHYSICAL); if (str && pw_properties_parse_bool(str)) port_flags |= qpwgraph_pipewire::Port::Physical; str = spa_dict_lookup(props, PW_KEY_PORT_TERMINAL); if (str && pw_properties_parse_bool(str)) port_flags |= qpwgraph_pipewire::Port::Terminal; str = spa_dict_lookup(props, PW_KEY_PORT_MONITOR); if (str && pw_properties_parse_bool(str)) port_flags |= qpwgraph_pipewire::Port::Monitor; str = spa_dict_lookup(props, PW_KEY_PORT_CONTROL); if (str && pw_properties_parse_bool(str)) port_flags |= qpwgraph_pipewire::Port::Control; if (pw->createPort(id, node_id, port_name, port_mode, port_type, port_flags)) ++nchanged; } else if (::strcmp(type, PW_TYPE_INTERFACE_Link) == 0) { const char *str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT); const uint port1_id = (str ? uint(pw_properties_parse_int(str)) : 0); str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT); const uint port2_id = (str ? uint(pw_properties_parse_int(str)) : 0); if (pw->createLink(id, port1_id, port2_id)) ++nchanged; } if (nchanged > 0) pw->changedNotify(); } static void qpwgraph_registry_event_global_remove ( void *data, uint32_t id ) { qpwgraph_pipewire *pw = static_cast (data); #ifdef CONFIG_DEBUG qDebug("qpwgraph_registry_event_global_remove[%p]: id:%u", pw, id); #endif pw->removeObjectEx(id); pw->changedNotify(); } static const struct pw_registry_events qpwgraph_registry_events = { .version = PW_VERSION_REGISTRY_EVENTS, .global = qpwgraph_registry_event_global, .global_remove = qpwgraph_registry_event_global_remove, }; // registry-events. // core-events... static void qpwgraph_core_event_done ( void *data, uint32_t id, int seq ) { qpwgraph_pipewire *pw = static_cast (data); qpwgraph_pipewire::Data *pd = pw->data(); #ifdef CONFIG_DEBUG qDebug("qpwgraph_core_event_done[%p]: id:%u seq:%d", pd, id, seq); #endif struct qpwgraph_pipewire::Proxy *p, *q; spa_list_for_each_safe(p, q, &pd->pending, pending_link) { if (p->pending_seq == seq) qpwgraph_remove_pending(p); } if (id == PW_ID_CORE) { pd->last_seq = seq; if (pd->pending_seq == seq) pw_thread_loop_signal(pd->loop, false); } } static void qpwgraph_core_event_error ( void *data, uint32_t id, int seq, int res, const char *message ) { qpwgraph_pipewire *pw = static_cast (data); qpwgraph_pipewire::Data *pd = pw->data(); #ifdef CONFIG_DEBUG qDebug("qpwgraph_core_event_error[%p]: id:%u seq:%d res:%d : %s", pd, id, seq, res, message); #endif if (id == PW_ID_CORE) { pd->last_res = res; if (res == -EPIPE) pd->error = true; } pw_thread_loop_signal(pd->loop, false); } static const struct pw_core_events qpwgraph_core_events = { .version = PW_VERSION_CORE_EVENTS, .info = nullptr, .done = qpwgraph_core_event_done, .error = qpwgraph_core_event_error, }; // core-events. // link-events... static int qpwgraph_link_proxy_sync ( qpwgraph_pipewire *pw ) { qpwgraph_pipewire::Data *pd = pw->data(); if (pw_thread_loop_in_thread(pd->loop)) return 0; pd->pending_seq = pw_proxy_sync((struct pw_proxy *)pd->core, pd->pending_seq); while (true) { pw_thread_loop_wait(pd->loop); if (pd->error) return pd->last_res; if (pd->pending_seq == pd->last_seq) break; } return 0; } static void qpwgraph_link_proxy_error ( void *data, int seq, int res, const char *message ) { #ifdef CONFIG_DEBUG qDebug("qpwgraph_link_proxy_error: seq:%d res:%d : %s", seq, res, message); #endif int *link_res = (int *)data; *link_res = res; } static const struct pw_proxy_events qpwgraph_link_proxy_events = { .version = PW_VERSION_PROXY_EVENTS, .error = qpwgraph_link_proxy_error, }; // link-events. //---------------------------------------------------------------------------- // qpwgraph_pipewire -- PipeWire graph driver // Constructor. qpwgraph_pipewire::qpwgraph_pipewire ( qpwgraph_canvas *canvas ) : qpwgraph_sect(canvas), m_data(nullptr) { resetPortTypeColors(); if (!open()) QTimer::singleShot(3000, this, SLOT(reset())); } // Destructor. qpwgraph_pipewire::~qpwgraph_pipewire (void) { close(); } // Client methods. bool qpwgraph_pipewire::open (void) { QMutexLocker locker1(&m_mutex1); pw_init(nullptr, nullptr); m_data = new Data; spa_zero(*m_data); spa_list_init(&m_data->pending); m_data->pending_seq = 0; m_data->node_names = new Data::NodeNames; m_data->loop = pw_thread_loop_new("qpwgraph_thread_loop", nullptr); if (m_data->loop == nullptr) { qDebug("pw_thread_loop_new: Can't create thread loop."); delete m_data; pw_deinit(); return false; } struct pw_loop *loop = pw_thread_loop_get_loop(m_data->loop); m_data->context = pw_context_new(loop, nullptr /*properties*/, 0 /*user_data size*/); if (m_data->context == nullptr) { qDebug("pw_context_new: Can't create context."); pw_thread_loop_destroy(m_data->loop); delete m_data; m_data = nullptr; pw_deinit(); return false; } m_data->core = pw_context_connect(m_data->context, nullptr /*properties*/, 0 /*user_data size*/); if (m_data->core == nullptr) { qDebug("pw_context_connect: Can't connect context."); pw_context_destroy(m_data->context); pw_thread_loop_destroy(m_data->loop); delete m_data; m_data = nullptr; pw_deinit(); return false; } pw_core_add_listener(m_data->core, &m_data->core_listener, &qpwgraph_core_events, this); m_data->registry = pw_core_get_registry(m_data->core, PW_VERSION_REGISTRY, 0 /*user_data size*/); pw_registry_add_listener(m_data->registry, &m_data->registry_listener, &qpwgraph_registry_events, this); m_data->pending_seq = 0; m_data->last_seq = 0; m_data->error = false; pw_thread_loop_start(m_data->loop); return true; } void qpwgraph_pipewire::close (void) { if (m_data == nullptr) return; QMutexLocker locker1(&m_mutex1); clearObjects(); if (m_data->loop) pw_thread_loop_stop(m_data->loop); if (m_data->registry) { spa_hook_remove(&m_data->registry_listener); pw_proxy_destroy((struct pw_proxy*)m_data->registry); } if (m_data->core) { spa_hook_remove(&m_data->core_listener); pw_core_disconnect(m_data->core); } if (m_data->context) pw_context_destroy(m_data->context); if (m_data->loop) pw_thread_loop_destroy(m_data->loop); if (m_data->node_names) delete m_data->node_names; delete m_data; m_data = nullptr; pw_deinit(); } // Get a brand new core and context... void qpwgraph_pipewire::reset (void) { clearItems(); close(); if (!open()) QTimer::singleShot(3000, this, SLOT(reset())); } // Callback notifiers. void qpwgraph_pipewire::changedNotify (void) { emit changed(); } // PipeWire port (dis)connection. void qpwgraph_pipewire::connectPorts ( qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect ) { if (m_data == nullptr) return; if (port1 == nullptr || port2 == nullptr) return; const qpwgraph_node *node1 = port1->portNode(); const qpwgraph_node *node2 = port2->portNode(); if (node1 == nullptr || node2 == nullptr) return; QMutexLocker locker1(&m_mutex1); pw_thread_loop_lock(m_data->loop); Port *p1 = findPort(port1->portId()); Port *p2 = findPort(port2->portId()); if ((p1 == nullptr || p2 == nullptr) || (p1->port_mode & qpwgraph_item::Output) == 0 || (p2->port_mode & qpwgraph_item::Input) == 0 || (p1->port_type != p2->port_type)) { pw_thread_loop_unlock(m_data->loop); return; } if (!is_connect) { // Disconnect ports... foreach (Link *link, p1->port_links) { if ((link->port1_id == p1->id) && (link->port2_id == p2->id)) { pw_registry_destroy(m_data->registry, link->id); qpwgraph_link_proxy_sync(this); break; } } pw_thread_loop_unlock(m_data->loop); return; } // Connect ports... char val[4][16]; ::snprintf(val[0], sizeof(val[0]), "%u", p1->node_id); ::snprintf(val[1], sizeof(val[1]), "%u", p1->id); ::snprintf(val[2], sizeof(val[2]), "%u", p2->node_id); ::snprintf(val[3], sizeof(val[3]), "%u", p2->id); struct spa_dict props; struct spa_dict_item items[6]; props = SPA_DICT_INIT(items, 0); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_OUTPUT_NODE, val[0]); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_OUTPUT_PORT, val[1]); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_NODE, val[2]); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_PORT, val[3]); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_LINGER, "true"); const char *str = ::getenv("PIPEWIRE_LINK_PASSIVE"); if (str && pw_properties_parse_bool(str)) items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_PASSIVE, "true"); struct pw_proxy *proxy = (struct pw_proxy *)pw_core_create_object(m_data->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props, 0); if (proxy) { int link_res = 0; struct spa_hook listener; spa_zero(listener); pw_proxy_add_listener(proxy, &listener, &qpwgraph_link_proxy_events, &link_res); qpwgraph_link_proxy_sync(this); spa_hook_remove(&listener); pw_proxy_destroy(proxy); } pw_thread_loop_unlock(m_data->loop); } // PipeWire node type inquirer. (static) bool qpwgraph_pipewire::isNodeType ( uint node_type ) { return (node_type == qpwgraph_pipewire::nodeType()); } // PipeWire node type. uint qpwgraph_pipewire::nodeType (void) { static const uint PipeWireNodeType = qpwgraph_item::itemType("PIPEWIRE_NODE_TYPE"); return PipeWireNodeType; } // PipeWire port type(s) inquirer. (static) bool qpwgraph_pipewire::isPortType ( uint port_type ) { return port_type == audioPortType() || port_type == midiPortType() || port_type == videoPortType() || port_type == otherPortType(); } uint qpwgraph_pipewire::audioPortType (void) { return qpwgraph_item::itemType(DEFAULT_AUDIO_TYPE); } uint qpwgraph_pipewire::midiPortType (void) { return qpwgraph_item::itemType(DEFAULT_MIDI_TYPE); } uint qpwgraph_pipewire::videoPortType (void) { return qpwgraph_item::itemType(DEFAULT_VIDEO_TYPE); } uint qpwgraph_pipewire::otherPortType (void) { return qpwgraph_item::itemType("PIPEWIRE_PORT_TYPE"); } // PipeWire node:port finder and creator if not existing. bool qpwgraph_pipewire::findNodePort ( uint node_id, uint port_id, qpwgraph_item::Mode port_mode, qpwgraph_node **node, qpwgraph_port **port, bool add_new ) { Node *n = findNode(node_id); if (n == nullptr) return false; if (!n->node_ready) return false; Port *p = findPort(port_id); if (p == nullptr) return false; const uint node_type = qpwgraph_pipewire::nodeType(); qpwgraph_item::Mode node_mode = port_mode; const uint port_type = p->port_type; *node = qpwgraph_sect::findNode(node_id, node_mode, node_type); *port = nullptr; if (*node == nullptr) { const uint port_flags = p->port_flags; const uint port_flags_mask = (Port::Physical | Port::Terminal); if ((port_flags & port_flags_mask) != port_flags_mask) { node_mode = qpwgraph_item::Duplex; *node = qpwgraph_sect::findNode(node_id, node_mode, node_type); } } if (*node && m_recycled_nodes.value(qpwgraph_node::NodeIdKey(*node), nullptr)) return false; if (*node && n->node_changed) { canvas()->releaseNode(*node); *node = nullptr; } if (*node) *port = (*node)->findPort(port_id, port_mode, port_type); if (*port && m_recycled_ports.value(qpwgraph_port::PortIdKey(*port), nullptr)) return false; if (add_new && *node == nullptr) { QString node_name = n->node_name; if ((p->port_flags & Port::Physical) == Port::None) { if (n->name_num > 0) { node_name += '-'; node_name += QString::number(n->name_num); } if (p->port_flags & Port::Monitor) { node_name += ' '; node_name += "[Monitor]"; } if (p->port_flags & Port::Control) { node_name += ' '; node_name += "[Control]"; } } *node = new qpwgraph_node(node_id, node_name, node_mode, node_type); (*node)->setNodeIcon(n->node_icon); (*node)->setNodeLabel(n->media_name); n->node_changed = false; qpwgraph_sect::addItem(*node); } if (add_new && *port == nullptr && *node) { *port = (*node)->addPort(port_id, p->port_name, port_mode, port_type); (*port)->updatePortTypeColors(qpwgraph_sect::canvas()); qpwgraph_sect::addItem(*port); } return (*node && *port); } // PipeWire graph updaters. void qpwgraph_pipewire::updateItems (void) { if (m_data == nullptr) return; #ifdef CONFIG_DEBUG qDebug("qpwgraph_pipewire::updateItems()"); #endif QMutexLocker locker1(&m_mutex1); QMutexLocker locker2(&m_mutex2); // 0. Check for core errors... // if (m_data->error) { QTimer::singleShot(3000, this, SLOT(reset())); return; } // 1. Nodes/ports inventory... // QList ports; foreach (Object *object, m_objects) { if (object->type != Object::Node) continue; Node *n1 = static_cast (object); if (!n1->node_ready) continue; foreach (const Port *p1, n1->node_ports) { const qpwgraph_item::Mode port_mode1 = p1->port_mode; qpwgraph_node *node1 = nullptr; qpwgraph_port *port1 = nullptr; if (findNodePort(n1->id, p1->id, port_mode1, &node1, &port1, true)) { node1->setMarked(true); port1->setMarked(true); if ((port_mode1 & qpwgraph_item::Output) && (!p1->port_links.isEmpty())) { ports.append(port1); } } } } // 2. Links inventory... // foreach (qpwgraph_port *port1, ports) { Port *p1 = findPort(port1->portId()); if (p1 == nullptr) continue; foreach (const Link *link, p1->port_links) { Port *p2 = findPort(link->port2_id); if (p2 == nullptr) continue; const qpwgraph_item::Mode port_mode2 = qpwgraph_item::Input; qpwgraph_node *node2 = nullptr; qpwgraph_port *port2 = nullptr; if (findNodePort(p2->node_id, link->port2_id, port_mode2, &node2, &port2, false)) { qpwgraph_connect *connect = port1->findConnect(port2); if (connect == nullptr) { connect = new qpwgraph_connect(); connect->setPort1(port1); connect->setPort2(port2); connect->updatePortTypeColors(); connect->updatePath(); qpwgraph_sect::addItem(connect); } if (connect) connect->setMarked(true); } } } // 3. Clean-up all un-marked items... // qpwgraph_sect::resetItems(qpwgraph_pipewire::nodeType()); m_recycled_nodes.clear(); m_recycled_ports.clear(); } void qpwgraph_pipewire::clearItems (void) { if (m_data == nullptr) return; #ifdef CONFIG_DEBUG qDebug("qpwgraph_pipewire::clearItems()"); #endif QMutexLocker locker1(&m_mutex1); // Clean-up all items... // qpwgraph_sect::clearItems(qpwgraph_pipewire::nodeType()); m_recycled_nodes.clear(); m_recycled_ports.clear(); } // Special port-type colors defaults (virtual). void qpwgraph_pipewire::resetPortTypeColors (void) { qpwgraph_canvas *canvas = qpwgraph_sect::canvas(); if (canvas) { canvas->setPortTypeColor( qpwgraph_pipewire::audioPortType(), QColor(Qt::darkGreen).darker(120)); canvas->setPortTypeColor( qpwgraph_pipewire::midiPortType(), QColor(Qt::darkRed).darker(120)); canvas->setPortTypeColor( qpwgraph_pipewire::videoPortType(), QColor(Qt::darkCyan).darker(120)); canvas->setPortTypeColor( qpwgraph_pipewire::otherPortType(), QColor(Qt::darkYellow).darker(120)); } } // Node/port renaming method (virtual override). void qpwgraph_pipewire::renameItem ( qpwgraph_item *item, const QString& name ) { // TODO: ?... // qpwgraph_sect::renameItem(item, name); } // PipeWire client data struct access. // qpwgraph_pipewire::Data *qpwgraph_pipewire::data (void) const { return m_data; } // Object methods. // qpwgraph_pipewire::Object *qpwgraph_pipewire::findObject ( uint id ) const { return m_objectids.value(id, nullptr); } void qpwgraph_pipewire::addObject ( uint id, Object *object ) { object->create_proxy(this); m_objectids.insert(id, object); m_objects.append(object); } void qpwgraph_pipewire::removeObject ( uint id ) { Object *object = findObject(id); if (object == nullptr) return; m_objectids.remove(id); m_objects.removeAll(object); if (object->type == Object::Node) destroyNode(static_cast (object)); else if (object->type == Object::Port) destroyPort(static_cast (object)); else if (object->type == Object::Link) destroyLink(static_cast (object)); } void qpwgraph_pipewire::clearObjects (void) { qDeleteAll(m_objects); m_objects.clear(); m_objectids.clear(); } void qpwgraph_pipewire::removeObjectEx ( uint id ) { QMutexLocker locker2(&m_mutex2); removeObject(id); } void qpwgraph_pipewire::addObjectEx ( uint id, Object *object ) { QMutexLocker locker2(&m_mutex2); addObject(id, object); } // Node methods. // qpwgraph_pipewire::Node *qpwgraph_pipewire::findNode ( uint node_id ) const { Node *node = static_cast (findObject(node_id)); return (node && node->type == Object::Node ? node : nullptr); } qpwgraph_pipewire::Node *qpwgraph_pipewire::createNode ( uint node_id, const QString& node_name, qpwgraph_item::Mode node_mode, uint node_type ) { recycleNode(node_id, node_mode); Node *node = new Node(node_id); node->node_name = node_name; node->node_mode = node_mode; node->node_type = Node::NodeType(node_type); node->node_icon = qpwgraph_icon(":/images/itemPipewire.png"); node->node_changed = false; node->node_ready = false; node->name_num = 0; Data::NodeNames *node_names = nullptr; if (m_data) node_names = m_data->node_names; if (node_names) { const Node::NameKey name_key(node); Data::NodeNames::Iterator name_iter = node_names->find(name_key, node->name_num); while (name_iter != node_names->end()) name_iter = node_names->find(name_key, ++(node->name_num)); node_names->insert(name_key, node->name_num); } addObjectEx(node_id, node); return node; } void qpwgraph_pipewire::destroyNode ( Node *node ) { Data::NodeNames *node_names = nullptr; if (m_data) node_names = m_data->node_names; if (node_names) node_names->remove(Node::NameKey(node), node->name_num); foreach (const Port *port, node->node_ports) removeObject(port->id); node->node_ports.clear(); delete node; } // Port methods. // qpwgraph_pipewire::Port *qpwgraph_pipewire::findPort ( uint port_id ) const { Port *port = static_cast (findObject(port_id)); return (port && port->type == Object::Port ? port : nullptr); } qpwgraph_pipewire::Port *qpwgraph_pipewire::createPort ( uint port_id, uint node_id, const QString& port_name, qpwgraph_item::Mode port_mode, uint port_type, uint port_flags ) { recyclePort(port_id, node_id, port_mode, port_type); Node *node = findNode(node_id); if (node == nullptr) return nullptr; Port *port = new Port(port_id); port->node_id = node_id; port->port_name = port_name; port->port_mode = port_mode; port->port_type = port_type; port->port_flags = Port::Flags(port_flags); node->node_ports.append(port); addObjectEx(port_id, port); return port; } void qpwgraph_pipewire::destroyPort ( Port *port ) { Node *node = findNode(port->node_id); if (node == nullptr) return; foreach (const Link *link, port->port_links) removeObject(link->id); port->port_links.clear(); node->node_ports.removeAll(port); delete port; } // Link methods. // qpwgraph_pipewire::Link *qpwgraph_pipewire::findLink ( uint link_id ) const { Link *link = static_cast (findObject(link_id)); return (link && link->type == Object::Link ? link : nullptr); } qpwgraph_pipewire::Link *qpwgraph_pipewire::createLink ( uint link_id, uint port1_id, uint port2_id ) { Port *port1 = findPort(port1_id); if (port1 == nullptr) return nullptr; if ((port1->port_mode & qpwgraph_item::Output) == 0) return nullptr; Port *port2 = findPort(port2_id); if (port2 == nullptr) return nullptr; if ((port2->port_mode & qpwgraph_item::Input) == 0) return nullptr; Link *link = new Link(link_id); link->port1_id = port1_id; link->port2_id = port2_id; port1->port_links.append(link); addObjectEx(link_id, link); return link; } void qpwgraph_pipewire::destroyLink ( Link *link ) { Port *port = findPort(link->port1_id); if (port == nullptr) return; port->port_links.removeAll(link); delete link; } // Special node finder... qpwgraph_node *qpwgraph_pipewire::findNode ( uint node_id, qpwgraph_item::Mode node_mode ) const { const uint node_type = qpwgraph_pipewire::nodeType(); qpwgraph_node *node = qpwgraph_sect::findNode(node_id, node_mode, node_type); if (node == nullptr) node = qpwgraph_sect::findNode(node_id, qpwgraph_item::Duplex, node_type); return node; } // Special node recycler... void qpwgraph_pipewire::recycleNode ( uint node_id, qpwgraph_item::Mode node_mode ) { qpwgraph_node *node = findNode(node_id, node_mode); if (node) m_recycled_nodes.insert(qpwgraph_node::NodeIdKey(node), node); } // Special port recycler... void qpwgraph_pipewire::recyclePort ( uint port_id, uint node_id, qpwgraph_item::Mode port_mode, uint port_type ) { qpwgraph_node *node = findNode(node_id, port_mode); if (node) { qpwgraph_port *port = node->findPort(port_id, port_mode, port_type); if (port) m_recycled_ports.insert(qpwgraph_port::PortIdKey(port), port); } } // end of qpwgraph_pipewire.cpp qpwgraph-0.6.1/src/PaxHeaders/man10000644000000000000000000000013214532447230013777 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/man1/0000755000175000001440000000000014532447230015304 5ustar00rncbcusers00000000000000qpwgraph-0.6.1/src/man1/PaxHeaders/qpwgraph.10000644000000000000000000000013214532447230015767 xustar0030 mtime=1701465752.092364752 30 atime=1701465752.092364752 30 ctime=1701465752.092364752 qpwgraph-0.6.1/src/man1/qpwgraph.10000644000175000001440000000170514532447230017222 0ustar00rncbcusers00000000000000.TH QPWGRAPH "1" "March 15, 2022" .SH NAME qpwgraph \- A PipeWire Graph Qt GUI Interface .SH SYNOPSIS .B qpwgraph [\fIoptions\fR] [\fIpatchbay-file\fR] .SH DESCRIPTION This manual page documents briefly the .B qpwgraph command. .PP \fBqpwgraph\fP is a graph manager dedicated to PipeWire (https://pipewire.org), using the Qt C++ framework (https://qt.io), based and pretty much like the same of QjackCtl (https://qjackctl.sourceforge.io). .PP Source code repository: https://gitlab.freedesktop.org/rncbc/qpwgraph .SH OPTIONS .HP \fB\-a\fR, \fB\-\-activated\fR .IP Activated patchbay. .HP \fB\-x\fR, \fB\-\-exclusive\fR .IP Exclusive patchbay. .HP \fB\-m\fR, \fB\-\-minimized\fR .IP Start minimized. .HP \fB\-h\fR, \fB\-\-help\fR .IP Show help about command line options .HP \fB\-v\fR, \fB\-\-version\fR .IP Show version information .SH FILES Configuration settings are stored in ~/.config/rncbc.org/qpwgraph.conf .SH AUTHOR qpwgraph was written by Rui Nuno Capela. qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_systray.cpp0000644000000000000000000000013214532447230017353 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_systray.cpp0000644000175000001440000000542414532447230020610 0ustar00rncbcusers00000000000000// qpwgraph_systray.cpp // /**************************************************************************** Copyright (C) 2021-2022, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_systray.h" #ifdef CONFIG_SYSTEM_TRAY #include "qpwgraph_form.h" #include #include #include //---------------------------------------------------------------------------- // qpwgraph_systray -- Custom system tray icon. // Constructor. qpwgraph_systray::qpwgraph_systray ( qpwgraph_form *form ) : QSystemTrayIcon(form), m_form(form) { // Set things as inherited... #if QT_VERSION < QT_VERSION_CHECK(6, 1, 0) QSystemTrayIcon::setIcon(QIcon(":/images/qpwgraph.png")); #else QSystemTrayIcon::setIcon(m_form->windowIcon()); #endif QSystemTrayIcon::setToolTip(m_form->windowTitle()); m_show = m_menu.addAction(tr("Show/Hide"), this, SLOT(showHide())); m_quit = m_menu.addAction(tr("Quit"), m_form, SLOT(closeQuit())); QSystemTrayIcon::setContextMenu(&m_menu); QObject::connect(this, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(activated(QSystemTrayIcon::ActivationReason))); QSystemTrayIcon::show(); } // Update context menu. void qpwgraph_systray::updateContextMenu (void) { if (m_form->isVisible() && !m_form->isMinimized()) m_show->setText(tr("Hide")); else m_show->setText(tr("Show")); } // Handle systeam tray activity. void qpwgraph_systray::activated ( QSystemTrayIcon::ActivationReason reason ) { switch (reason) { case QSystemTrayIcon::Trigger: showHide(); // Fall trhu... case QSystemTrayIcon::MiddleClick: case QSystemTrayIcon::DoubleClick: case QSystemTrayIcon::Unknown: default: break; } } // Handle menu actions. void qpwgraph_systray::showHide (void) { if (m_form->isVisible() && !m_form->isMinimized()) { // Hide away from sight, totally... m_form->hide(); } else { // Show normally. m_form->showNormal(); m_form->raise(); m_form->activateWindow(); } } #endif // CONFIG_SYSTEM_TRAY // end of qpwgraph_systray.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_connect.cpp0000644000000000000000000000013214532447230017266 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.092364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_connect.cpp0000644000175000001440000002074414532447230020525 0ustar00rncbcusers00000000000000// qpwgraph_connect.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_connect.h" #include "qpwgraph_node.h" #include #include #include #include #if 0//Disable drop-shadow effect... #include #endif #include //---------------------------------------------------------------------------- // qpwgraph_connect -- Connection-line graphics item. // Constructor. qpwgraph_connect::qpwgraph_connect (void) : qpwgraph_item(nullptr), m_port1(nullptr), m_port2(nullptr), m_dimmed(false) { QGraphicsPathItem::setZValue(-1.0); QGraphicsPathItem::setFlag(QGraphicsItem::ItemIsSelectable); qpwgraph_item::setBackground(qpwgraph_item::foreground()); #if 0//Disable drop-shadow effect... const QPalette pal; const bool is_darkest = (pal.base().color().value() < 24); QColor shadow_color = (is_darkest ? Qt::white : Qt::black); shadow_color.setAlpha(220); QGraphicsDropShadowEffect *effect = new QGraphicsDropShadowEffect(); effect->setColor(shadow_color); effect->setBlurRadius(is_darkest ? 4 : 8); effect->setOffset(is_darkest ? 0 : 1); QGraphicsPathItem::setGraphicsEffect(effect); #endif QGraphicsPathItem::setAcceptHoverEvents(true); qpwgraph_item::raise(); } // Destructor. qpwgraph_connect::~qpwgraph_connect (void) { // No actual need to destroy any children here... // //QGraphicsPathItem::setGraphicsEffect(nullptr); } // Accessors. void qpwgraph_connect::setPort1 ( qpwgraph_port *port ) { if (m_port1) m_port1->removeConnect(this); m_port1 = port; if (m_port1) m_port1->appendConnect(this); if (m_port1 && m_port1->isSelected()) setSelectedEx(m_port1, true); } qpwgraph_port *qpwgraph_connect::port1 (void) const { return m_port1; } void qpwgraph_connect::setPort2 ( qpwgraph_port *port ) { if (m_port2) m_port2->removeConnect(this); m_port2 = port; if (m_port2) m_port2->appendConnect(this); if (m_port2 && m_port2->isSelected()) setSelectedEx(m_port2, true); } qpwgraph_port *qpwgraph_connect::port2 (void) const { return m_port2; } // Active disconnection. void qpwgraph_connect::disconnect (void) { if (m_port1) { m_port1->removeConnect(this); m_port1 = nullptr; } if (m_port2) { m_port2->removeConnect(this); m_port2 = nullptr; } } // Path/shaper updaters. void qpwgraph_connect::updatePathTo ( const QPointF& pos ) { const bool is_out0 = m_port1->isOutput(); const QPointF pos0 = m_port1->portPos(); const QPointF d1(1.0, 0.0); const QPointF pos1 = (is_out0 ? pos0 + d1 : pos - d1); const QPointF pos4 = (is_out0 ? pos - d1 : pos0 + d1); const QPointF d2(2.0, 0.0); const QPointF pos1_2(is_out0 ? pos1 + d2 : pos1 - d2); const QPointF pos3_4(is_out0 ? pos4 - d2 : pos4 + d2); qpwgraph_node *node1 = m_port1->portNode(); const QRectF& rect1 = node1->itemRect(); const qreal h1 = 0.5 * rect1.height(); const qreal dh = pos0.y() - node1->scenePos().y() - h1; const qreal dx = pos3_4.x() - pos1_2.x(); const qreal x_max = rect1.width() + h1; const qreal x_min = qMin(x_max, qAbs(dx)); const qreal x_offset = (dx > 0.0 ? 0.5 : 1.0) * x_min; qreal y_offset = 0.0; if (g_connect_through_nodes) { // New "normal" connection line curves (inside/through nodes)... const qreal h2 = m_port1->itemRect().height(); const qreal dy = qAbs(pos3_4.y() - pos1_2.y()); y_offset = (dx > -h2 || dy > h2 ? 0.0 : (dh > 0.0 ? +h2 : -h2)); } else { // Old "weird" connection line curves (outside/around nodes)... y_offset = (dx > 0.0 ? 0.0 : (dh > 0.0 ? +x_min : -x_min)); } const QPointF pos2(pos1.x() + x_offset, pos1.y() + y_offset); const QPointF pos3(pos4.x() - x_offset, pos4.y() + y_offset); QPainterPath path; path.moveTo(pos1); path.lineTo(pos1_2); path.cubicTo(pos2, pos3, pos3_4); path.lineTo(pos4); const qreal arrow_angle = path.angleAtPercent(0.5) * M_PI / 180.0; const QPointF arrow_pos0 = path.pointAtPercent(0.5); const qreal arrow_size = 8.0; QVector arrow; arrow.append(arrow_pos0); arrow.append(arrow_pos0 - QPointF( ::sin(arrow_angle + M_PI / 2.25) * arrow_size, ::cos(arrow_angle + M_PI / 2.25) * arrow_size)); arrow.append(arrow_pos0 - QPointF( ::sin(arrow_angle + M_PI - M_PI / 2.25) * arrow_size, ::cos(arrow_angle + M_PI - M_PI / 2.25) * arrow_size)); arrow.append(arrow_pos0); path.addPolygon(QPolygonF(arrow)); /*QGraphicsPathItem::*/setPath(path); } void qpwgraph_connect::updatePath (void) { if (m_port2) updatePathTo(m_port2->portPos()); } void qpwgraph_connect::paint ( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget */*widget*/ ) { QColor color; if (QGraphicsPathItem::isSelected()) color = option->palette.highlight().color(); else if (qpwgraph_item::isHighlight() || QGraphicsPathItem::isUnderMouse()) color = qpwgraph_item::foreground().lighter(); else color = qpwgraph_item::foreground(); color.setAlpha(m_dimmed ? 128 : 255); const QPalette pal; const bool is_darkest = (pal.base().color().value() < 24); QColor shadow_color = (is_darkest ? Qt::white : Qt::black); shadow_color.setAlpha(m_dimmed ? 40 : 80); const QPainterPath& path = QGraphicsPathItem::path(); painter->setBrush(Qt::NoBrush); painter->setPen(QPen(shadow_color, 3)); painter->drawPath(path.translated(+1.0, +1.0)); painter->setPen(QPen(color, 2)); painter->drawPath(path); } QVariant qpwgraph_connect::itemChange ( GraphicsItemChange change, const QVariant& value ) { if (change == QGraphicsItem::ItemSelectedHasChanged) { const bool is_selected = value.toBool(); qpwgraph_item::setHighlight(is_selected); if (m_port1) m_port1->setSelectedEx(is_selected); if (m_port2) m_port2->setSelectedEx(is_selected); } return value; } QPainterPath qpwgraph_connect::shape (void) const { #if (QT_VERSION < QT_VERSION_CHECK(6, 1, 0)) && (__cplusplus < 201703L) return QGraphicsPathItem::shape(); #else const QPainterPathStroker stroker = QPainterPathStroker(QPen(QColor(), 2)); return stroker.createStroke(path()); #endif } // Selection propagation method... void qpwgraph_connect::setSelectedEx ( qpwgraph_port *port, bool is_selected ) { setHighlightEx(port, is_selected); if (QGraphicsPathItem::isSelected() != is_selected) { #if 0//OLD_SELECT_BEHAVIOR QGraphicsPathItem::setSelected(is_selected); if (is_selected) { if (m_port1 && m_port1 != port) m_port1->setSelectedEx(is_selected); if (m_port2 && m_port2 != port) m_port2->setSelectedEx(is_selected); } #else if (!is_selected || (m_port1 && m_port2 && m_port1->isSelected() && m_port2->isSelected())) { QGraphicsPathItem::setSelected(is_selected); } #endif } } // Highlighting propagation method... void qpwgraph_connect::setHighlightEx ( qpwgraph_port *port, bool is_highlight ) { qpwgraph_item::setHighlight(is_highlight); if (m_port1 && m_port1 != port) m_port1->setHighlight(is_highlight); if (m_port2 && m_port2 != port) m_port2->setHighlight(is_highlight); } // Special port-type color business. void qpwgraph_connect::updatePortTypeColors (void) { if (m_port1) { const QColor& color = m_port1->background().lighter(); qpwgraph_item::setForeground(color); qpwgraph_item::setBackground(color); } } // Dim/transparency option. void qpwgraph_connect::setDimmed ( bool dimmed ) { m_dimmed = dimmed; update(); } int qpwgraph_connect::isDimmed (void) const { return m_dimmed; } // Connector curve draw style (through vs. around nodes) // bool qpwgraph_connect::g_connect_through_nodes = false; void qpwgraph_connect::setConnectThroughNodes ( bool on ) { g_connect_through_nodes = on; } bool qpwgraph_connect::isConnectThroughNodes (void) { return g_connect_through_nodes; } // end of qpwgraph_connect.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_patchbay.h0000644000000000000000000000013214532447230017075 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_patchbay.h0000644000175000001440000000771414532447230020336 0ustar00rncbcusers00000000000000// qpraph1_patchbay.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_patchbay_h #define __qpwgraph_patchbay_h #include "qpwgraph_item.h" #include #include #include // Forward decls. class qpwgraph_canvas; class qpwgraph_connect; class qpwgraph_port; class qpwgraph_node; //---------------------------------------------------------------------------- // qpwgraph_patchbay -- Persistant connections patchbay decl. class qpwgraph_patchbay { public: // Constructor. qpwgraph_patchbay(qpwgraph_canvas *canvas) : m_canvas(canvas), m_activated(false), m_exclusive(false), m_dirty(0) {} // Destructor. ~qpwgraph_patchbay() { clear(); } // Mode/properties accessors. void setActivated(bool activated) { m_activated = activated; } bool isActivated() const { return m_activated; } void setExclusive(bool exclusive) { m_exclusive = exclusive; } bool isExclusive() const { return m_exclusive; } // Clear all patchbay rules and cache. void clear(); // Snapshot of all current graph connections... void snap(); // Patchbay rules file I/O methods. bool load(const QString& filename); bool save(const QString& filename); // Execute and apply rules to graph. bool scan(); // Update rules on demand. bool connectPorts(qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect); bool connect(qpwgraph_connect *connect, bool is_connect); // Patchbay rule item. struct Item { Item(uint nt, uint pt, const QString& n1, const QString& p1, const QString& n2, const QString& p2) : node_type(nt), port_type(pt), node1(n1), port1(p1), node2(n2), port2(p2) {} Item(const Item& item) : node_type(item.node_type), port_type(item.port_type), node1(item.node1), port1(item.port1), node2(item.node2), port2(item.port2) {} bool operator== (const Item& item) const { return node_type == item.node_type && port_type == item.port_type && node1 == item.node1 && port1 == item.port1 && node2 == item.node2 && port2 == item.port2; } uint node_type; uint port_type; QString node1; QString port1; QString node2; QString port2; }; typedef QHash Items; // Find a connection rule. Item *findConnectPorts(qpwgraph_port *port1, qpwgraph_port *port2) const; Item *findConnect(qpwgraph_connect *connect) const; // Dirty status flag. bool isDirty() const { return (m_dirty > 0); } protected: // Add a new patchbay rule item. void addItem(Item *item); // Node and port type to text helpers. static uint nodeTypeFromText(const QString& text); static const char *textFromNodeType(uint node_type); static uint portTypeFromText(const QString& text); static const char *textFromPortType(uint port_type); private: // Instance variables. qpwgraph_canvas *m_canvas; bool m_activated; bool m_exclusive; Items m_items; int m_dirty; }; inline uint qHash ( const qpwgraph_patchbay::Item& item ) { return qHash(item.node_type) ^ qHash(item.port_type) ^ qHash(item.node1 + item.port1) ^ qHash(item.node2 + item.port2); } #endif // __qpwgraph_patchbay_h // end of qpwgraph_patchbay.h qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_sect.cpp0000644000000000000000000000013214532447230016573 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_sect.cpp0000644000175000001440000000626314532447230020032 0ustar00rncbcusers00000000000000// qpwgraph_sect.cpp // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #include "qpwgraph_sect.h" #include "qpwgraph_canvas.h" #include "qpwgraph_connect.h" //---------------------------------------------------------------------------- // qpwgraph_sect -- Generic graph driver // Constructor. qpwgraph_sect::qpwgraph_sect ( qpwgraph_canvas *canvas ) : QObject(canvas), m_canvas(canvas) { } // Accessors. qpwgraph_canvas *qpwgraph_sect::canvas (void) const { return m_canvas; } // Generic sect/graph methods. void qpwgraph_sect::addItem ( qpwgraph_item *item, bool is_new ) { if (is_new) m_canvas->addItem(item); if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect) m_connects.append(connect); } } void qpwgraph_sect::removeItem ( qpwgraph_item *item ) { if (item->type() == qpwgraph_connect::Type) { qpwgraph_connect *connect = static_cast (item); if (connect) { connect->disconnect(); m_connects.removeAll(connect); } } m_canvas->removeItem(item); } // Clean-up all un-marked items... void qpwgraph_sect::resetItems ( uint node_type ) { const QList connects(m_connects); foreach (qpwgraph_connect *connect, connects) { if (connect->isMarked()) { connect->setMarked(false); } else { removeItem(connect); delete connect; } } m_canvas->resetNodes(node_type); } void qpwgraph_sect::clearItems ( uint node_type ) { qpwgraph_sect::resetItems(node_type); // qDeleteAll(m_connects); m_connects.clear(); m_canvas->clearNodes(node_type); } // Special node finder. qpwgraph_node *qpwgraph_sect::findNode ( uint id, qpwgraph_item::Mode mode, uint type ) const { return m_canvas->findNode(id, mode, type); } // Client/port renaming method. void qpwgraph_sect::renameItem ( qpwgraph_item *item, const QString& name ) { qpwgraph_node *node = nullptr; if (item->type() == qpwgraph_node::Type) { qpwgraph_node *node = static_cast (item); if (node) node->setNodeTitle(name); } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) node = port->portNode(); if (port && node) port->setPortTitle(name); } if (node) node->updatePath(); } // end of qpwgraph_sect.cpp qpwgraph-0.6.1/src/PaxHeaders/qpwgraph_pipewire.h0000644000000000000000000000013214532447230017126 xustar0030 mtime=1701465752.093364752 30 atime=1701465752.093364752 30 ctime=1701465752.093364752 qpwgraph-0.6.1/src/qpwgraph_pipewire.h0000644000175000001440000001010514532447230020353 0ustar00rncbcusers00000000000000// qpwgraph_pipewire.h // /**************************************************************************** Copyright (C) 2021-2023, rncbc aka Rui Nuno Capela. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *****************************************************************************/ #ifndef __qpwgraph_pipewire_h #define __qpwgraph_pipewire_h #include "config.h" #include "qpwgraph_sect.h" #include #include //---------------------------------------------------------------------------- // qpwgraph_pipewire -- PipeWire graph driver class qpwgraph_pipewire : public qpwgraph_sect { Q_OBJECT public: // Constructor. qpwgraph_pipewire(qpwgraph_canvas *canvas); // Destructor. ~qpwgraph_pipewire(); // Client methods. bool open(); void close(); // Callback notifiers. void changedNotify(); // PipeWire port (dis)connection. void connectPorts(qpwgraph_port *port1, qpwgraph_port *port2, bool is_connect); // PipeWire graph updaters. void updateItems(); void clearItems(); // Special port-type colors defaults (virtual). void resetPortTypeColors(); // PipeWire node type inquirer. static bool isNodeType(uint node_type); // PipeWire node type. static uint nodeType(); // PipeWire port type(s) inquirer. static bool isPortType(uint port_type); // PipeWire port types. static uint audioPortType(); static uint midiPortType(); static uint videoPortType(); static uint otherPortType(); // Node/port renaming method (virtual override). void renameItem(qpwgraph_item *item, const QString& name); // PipeWire client data struct access. // struct Data; struct Proxy; struct Object; struct Node; struct Port; struct Link; Data *data() const; // Object methods... Object *findObject(uint id) const; void addObject(uint id, Object *object); void removeObject(uint id); void clearObjects(); void addObjectEx(uint id, Object *object); void removeObjectEx(uint id); // Node methods.... Node *findNode(uint node_id) const; Node *createNode( uint node_id, const QString& node_name, qpwgraph_item::Mode node_mode, uint node_type); void destroyNode(Node *node); // Port methods.... Port *findPort(uint port_id) const; Port *createPort( uint port_id, uint node_id, const QString& port_name, qpwgraph_item::Mode port_mode, uint port_type, uint port_flags); void destroyPort(Port *port); // Link methods.... Link *findLink(uint link_id) const; Link *createLink(uint link_id, uint port1_id, uint port2_id); void destroyLink(Link *link); signals: void changed(); protected slots: void reset(); protected: // PipeWire node:port finder and creator if not existing. bool findNodePort( uint node_id, uint port_id, qpwgraph_item::Mode port_mode, qpwgraph_node **node, qpwgraph_port **port, bool add_new); // Special node finder... qpwgraph_node *findNode(uint node_id, qpwgraph_item::Mode node_mode) const; // Special node/port recycler... void recycleNode( uint node_id, qpwgraph_item::Mode node_mode); void recyclePort( uint port_id, uint node_id, qpwgraph_item::Mode port_mode, uint port_type); private: // PipeWire client impl. Data *m_data; // PipeWire object database. QHash m_objectids; QList m_objects; qpwgraph_node::NodeIds m_recycled_nodes; qpwgraph_port::PortIds m_recycled_ports; // Callback sanity mutex. QMutex m_mutex1; QMutex m_mutex2; }; #endif // __qpwgraph_pipewire_h // end of qpwgraph_pipewire.h