qpwgraph-0.8.2/PaxHeaders/CMakeLists.txt0000644000000000000000000000013214762602507015201 xustar0030 mtime=1741358407.415941632 30 atime=1741358407.415941632 30 ctime=1741358407.415941632 qpwgraph-0.8.2/CMakeLists.txt0000644000175000001440000000563314762602507015200 0ustar00rncbcuserscmake_minimum_required (VERSION 3.15) project(qpwgraph VERSION 0.8.2 DESCRIPTION "A PipeWire Graph Qt GUI Interface" HOMEPAGE_URL "https://gitlab.freedesktop.org/rncbc/qpwgraph" LANGUAGES C CXX) set (PROJECT_COPYRIGHT "Copyright (C) 2021-2025, rncbc aka Rui Nuno Capela. All rights reserved.") set (PROJECT_DOMAIN "rncbc.org") 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 "-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 () 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 () # 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) 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.8.2/PaxHeaders/ChangeLog0000644000000000000000000000013214762602507014213 xustar0030 mtime=1741358407.415941632 30 atime=1741358407.415941632 30 ctime=1741358407.415941632 qpwgraph-0.8.2/ChangeLog0000644000175000001440000002601314762602507014205 0ustar00rncbcusersqpwgraph - A PipeWire Graph Qt GUI Interface -------------------------------------------- ChangeLog 0.8.2 2025-03-07 An End-of-Winter'25 Beta Release. - Port colors: new PipeWire MIDI 2 (UMP) port type is now aliased to the same as (now old) MIDI port type. (pipewire >= 1.3.81) - Fixed command line parsing (QCommandLineParser/Option) to not exiting the application with a segfault when showing help and version information. 0.8.1 2024-12-27 An End-of-Year'24 Beta Release. - Patchbay 'presets' may now be selected from the system-tray icon context-menu. 0.8.0 2024-11-14 A Mid-Autumn'24 Beta Release. - A new Patchbay/Manage... feature dialog is introduced, targeting the currently loaded patchbay, allowing the removal and cleanup of connection rules that are no longer used, obsolete or simply not applicable anymore. - Force an actual complete graph refresh on main View/Refresh... - Introducing the new Graph/Options... dialog, to where the most general option settings have been (re)moved from the main Help menu. - Introducing Graph/Options.../Filter to hide nodes by node name pattern (blacklist). - Fix default PipeWire node nickname if given empty. 0.7.9 2024-10-28 An Autumn'24 Beta Release. - Thumb-view: Fixed flicker when dragging widget from top to bottom corners (and vice-versa); inner thumb-viewport is drag+moved only if the Ctrl keyboard modifier is pressed. - Node reference positioning changed to the top-left corner, improving the base snapping-to-grid perception. - Prepping up next development cycle (Qt >= 6.8) 0.7.8 2024-09-19 An End-of-Summer'24 Beta Release. - When visible the thumb-view may now be drag-moved to a different corner position anytime. 0.7.7 2024-08-21 A Mid-Summer'24 Beta Release. - Thumb-view repositions and resizes immediately when visible. 0.7.5 2024-07-12 A Summer'24 Beta Release. - Implement patchbay node search functionality: adds a floating text-entry field, which appears when typing in the canvas, selecting nodes that contains the typed text (maybe regular expression). 0.7.4 2024-06-28 An Early-Summer'24 Hot-fix Release. - Force a default PipeWire node name if given empty. - Fixed a potential use-after-free issue on canvas nodes refresh and recycle. 0.7.3 2024-06-22 An Early-Summer'24 Beta Release. - Cancel button option added to close to system-tray icon message. - Introducing thumbview context-menu. 0.7.2 2024-05-12 A Mid-Spring'24 Beta Release. - Remove margins and spacing from the top-level canvas layout. - Possible fix to pipewire backend thread loop locking. - Updated to latest framework level (Qt >= 6.7) 0.7.1 2024-04-25 A Spring'24 Beta Release Hot-Fix. - Hot-fix: disable thumbview interactivity: prevent dragging nodes over to extremely severe off-limits by accident. 0.7.0 2024-04-22 A Spring'24 Beta Release. - Whether to show the System-tray informational and/or active Patchbay warning messages, respectively on Close and/or Quit, is now optional (cf. Help > Enable System Tray Message and/or Help > Enable Patchbay Message, resp.) - Introducing the View/Thumbview option as a whole graph thumbnail overview helper. 0.6.3 2024-03-29 A Good-Friday'24 Release. - Node and port title ellipsis shortning trimmed and simplified. - Fixed system-tray icon to a 32x32 pixmap. - Fixed renamed ports positioned incorrectly after relaunch. 0.6.2 2024-01-22 A Winter'24 Release. - Make the main canvas background to mid-gray, when on non-dark color themes. - Updated copyright headers into the New Year (2024). 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.8.2/PaxHeaders/LICENSE.md0000644000000000000000000000013214762602507014045 xustar0030 mtime=1741358407.415941632 30 atime=1741358407.415941632 30 ctime=1741358407.415941632 qpwgraph-0.8.2/LICENSE.md0000644000175000001440000004302514762602507014041 0ustar00rncbcusersGNU 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.8.2/PaxHeaders/README.md0000644000000000000000000000013214762602507013720 xustar0030 mtime=1741358407.415941632 30 atime=1741358407.415941632 30 ctime=1741358407.415941632 qpwgraph-0.8.2/README.md0000644000175000001440000000360214762602507013711 0ustar00rncbcusers# qpwgraph - A PipeWire Graph Qt GUI Interface ![Screenshot](src/images/qpwgraph_screenshot-4.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++20 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 immediately 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-2024, rncbc aka Rui Nuno Capela. All rights reserved. qpwgraph-0.8.2/PaxHeaders/docs0000644000000000000000000000013014762602507013312 xustar0029 mtime=1741358407.41670067 30 atime=1741358407.416536229 29 ctime=1741358407.41670067 qpwgraph-0.8.2/docs/0000755000175000001440000000000014762602507013361 5ustar00rncbcusersqpwgraph-0.8.2/docs/PaxHeaders/qpwgraph-user_manual.md0000644000000000000000000000013214762602507020055 xustar0030 mtime=1741358407.416536229 30 atime=1741358407.416536229 30 ctime=1741358407.416536229 qpwgraph-0.8.2/docs/qpwgraph-user_manual.md0000644000175000001440000000730414762602507020051 0ustar00rncbcusers# 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.8.2/docs/PaxHeaders/qpwgraph_patchbay-user_manual.md0000644000000000000000000000013014762602507021726 xustar0029 mtime=1741358407.41670067 30 atime=1741358407.416536229 29 ctime=1741358407.41670067 qpwgraph-0.8.2/docs/qpwgraph_patchbay-user_manual.md0000644000175000001440000000421114762602507021716 0ustar00rncbcusers# 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. ## Manage... dialog Manages the currently loaded patchbay, allowing the removal and cleanup of connection rules that are no longer used, obsolete or simply not applicable anymore. (EXPERIMENTAL) --- Credits: @Lootre (a.k.a. Thomas Lachat). qpwgraph-0.8.2/PaxHeaders/src0000644000000000000000000000013114762602507013152 xustar0030 mtime=1741358407.419265969 29 atime=1741358407.41670067 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/0000755000175000001440000000000014762602507013220 5ustar00rncbcusersqpwgraph-0.8.2/src/PaxHeaders/appdata0000644000000000000000000000012714762602507014571 xustar0029 mtime=1741358407.41690441 29 atime=1741358407.41670067 29 ctime=1741358407.41690441 qpwgraph-0.8.2/src/appdata/0000755000175000001440000000000014762602507014632 5ustar00rncbcusersqpwgraph-0.8.2/src/appdata/PaxHeaders/org.rncbc.qpwgraph.desktop0000644000000000000000000000013214762602507021742 xustar0030 mtime=1741358407.416867616 30 atime=1741358407.416867616 30 ctime=1741358407.416867616 qpwgraph-0.8.2/src/appdata/org.rncbc.qpwgraph.desktop0000644000175000001440000000053114762602507021731 0ustar00rncbcusers[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.8.2/src/appdata/PaxHeaders/org.rncbc.qpwgraph.metainfo.xml0000644000000000000000000000013014762602507022670 xustar0029 mtime=1741358407.41690441 30 atime=1741358407.416867616 29 ctime=1741358407.41690441 qpwgraph-0.8.2/src/appdata/org.rncbc.qpwgraph.metainfo.xml0000644000175000001440000000305014762602507022660 0ustar00rncbcusers 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-4.png The main application window in action AudioVideo Audio Video MIDI JACK Qt https://gitlab.freedesktop.org/rncbc/qpwgraph rncbc aka. Rui Nuno Capela rncbc@rncbc.org
qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_patchbay.h0000644000000000000000000000013214762602507017105 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_patchbay.h0000644000175000001440000001035514762602507017101 0ustar00rncbcusers// qpraph1_patchbay.h // /**************************************************************************** Copyright (C) 2021-2024, 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(); } // Canvas accessor. qpwgraph_canvas *canvas() const { return m_canvas; } // 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 items. // 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; }; struct Items : public QHash { bool addItem(const Item& item); bool removeItem(const Item& item); void copyItems(const Items& items); void clearItems(); }; // Find a connection rule. Item *findConnectPorts(qpwgraph_port *port1, qpwgraph_port *port2) const; Item *findConnect(qpwgraph_connect *connect) const; // Patchbay rule items accessors. void setItems(const Items& items); const Items& items() const { return m_items; } // Dirty status flag. bool isDirty() const { return (m_dirty > 0); } protected: // 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.8.2/src/PaxHeaders/qpwgraph_patchman.cpp0000644000000000000000000000013214762602507017440 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.418265975 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_patchman.cpp0000644000175000001440000007574114762602507017446 0ustar00rncbcusers// qpwgraph_patchman.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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_patchman.h" #include "qpwgraph_canvas.h" #include "qpwgraph_pipewire.h" #include "qpwgraph_alsamidi.h" #include #include #include #include #include #include #include #include #include #include #include #include //---------------------------------------------------------------------------- // qpwgraph_patchman::TreeWidget -- side-view tree widget decl. class qpwgraph_patchman::TreeWidget : public QTreeWidget { public: // Side mode type. enum Mode { Outputs, Inputs }; // Constructor. TreeWidget(MainWidget *parent, Mode mode); // Destructor. ~TreeWidget(); // Side mode accessors. bool isOutputs() const; bool isInputs() const; // Node/port finders. QTreeWidgetItem *findNodeItem( const QString& node_name, int node_type) const; QTreeWidgetItem *findPortItem(QTreeWidgetItem *node_item, const QString& port_name, int port_type) const; // Brainless override necessary to make this public (Qt5)... QTreeWidgetItem *itemFromIndex(const QModelIndex &index) const { return QTreeWidget::itemFromIndex(index); } protected: // Initial size hints. QSize sizeHint() const; private: // Instance members. MainWidget *m_main; Mode m_mode; }; //---------------------------------------------------------------------------- // qpwgraph_patchman::LineWidget -- middle-view line widget decl. class qpwgraph_patchman::LineWidget : public QWidget { public: // Constructor. LineWidget(MainWidget *parent); // Destructor. ~LineWidget(); // Connect line managers. void addLine(QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item); void removeLine(QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item); // Connect line finder. bool findLine(QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item) const; // Connect-lines cleaner. void clear(); // Connect-line empty status. bool isEmpty() const; protected: // Legal client/port item position helper. int itemY(QTreeWidgetItem *item) const; // Draw one connection line. void drawLine(QPainter *painter, int x1, int y1, int x2, int y2, int h1, int h2, const QPen& pen) const; // Draw connection lines. void paintEvent(QPaintEvent *); // Initial size hints. QSize sizeHint() const; private: // Instance members. MainWidget *m_main; typedef QMultiHash Lines; Lines m_lines; // Connector line color map/persistence. static QHash g_colors; }; //---------------------------------------------------------------------------- // qpwgraph_patchman::MainWidget -- main-view composite widget decl. class qpwgraph_patchman::MainWidget : public QSplitter { public: // Constructor. MainWidget(qpwgraph_patchman *parent); // Destructor. ~MainWidget(); // Child widget accessors. TreeWidget *outputs() const { return m_outputs; } LineWidget *connects() const { return m_connects; } TreeWidget *inputs() const { return m_inputs; } // Patchbay items accessors. void setItems(const qpwgraph_patchbay::Items& items); const qpwgraph_patchbay::Items& items() const; // Patchbay view refresh. void refresh(); // Patchbay management actions. bool canRemove() const; void remove(); bool canRemoveAll() const; void removeAll(); bool canCleanup() const; void cleanup(); // Stabilize item highlights. void stabilize(); protected: // Patchbay item connection finder. bool findConnect(qpwgraph_patchbay::Item *item) const; // Patchbay connect line finder/removal. bool findLine( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item) const; bool removeLine( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item); void clearItems(); // Initial size hints. QSize sizeHint() const; private: // Instance memebers qpwgraph_patchman *m_patchman; TreeWidget *m_outputs; LineWidget *m_connects; TreeWidget *m_inputs; qpwgraph_patchbay::Items m_items; }; //---------------------------------------------------------------------------- // qpwgraph_patchman::ItemDelegate -- side-view item delegate decl. class qpwgraph_patchman::ItemDelegate : public QItemDelegate { public: // Constructor. ItemDelegate(TreeWidget *parent) : QItemDelegate(parent), m_tree(parent) {} protected: // Overridden paint method. void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; QTreeWidgetItem *item = m_tree->itemFromIndex(index); if (item) { const int data = item->data(0, Qt::UserRole).toInt(); if (data & 1) { const QColor& color = opt.palette.base().color().value() < 0x7f ? Qt::cyan : Qt::blue; if (opt.state & QStyle::State_Selected) opt.palette.setColor(QPalette::HighlightedText, color); else opt.palette.setColor(QPalette::Text, color); } if (data & 2) opt.font.setBold(item->parent() || !item->isExpanded()); } QItemDelegate::paint(painter, opt, index); } private: TreeWidget *m_tree; }; //---------------------------------------------------------------------------- // qpwgraph_patchman::TreeWidget -- side-view tree widget impl. // Constructor. qpwgraph_patchman::TreeWidget::TreeWidget ( MainWidget *parent, Mode mode ) : QTreeWidget(parent), m_main(parent), m_mode(mode) { QHeaderView *header = QTreeWidget::header(); header->setDefaultAlignment(Qt::AlignLeft); header->setSectionsMovable(false); header->setSectionsClickable(true); header->setSortIndicatorShown(true); header->setStretchLastSection(true); QTreeWidget::setRootIsDecorated(true); QTreeWidget::setUniformRowHeights(true); QTreeWidget::setAutoScroll(true); QTreeWidget::setSelectionMode( QAbstractItemView::ExtendedSelection); QTreeWidget::setSizePolicy( QSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding)); QTreeWidget::setSortingEnabled(true); QTreeWidget::setMinimumWidth(120); QTreeWidget::setColumnCount(1); QTreeWidget::setItemDelegate(new ItemDelegate(this)); QString text; if (isOutputs()) text = tr("Nodes / Output Ports"); else text = tr("Nodes / Input Ports"); QTreeWidget::headerItem()->setText(0, text); QTreeWidget::sortItems(0, Qt::AscendingOrder); QTreeWidget::setToolTip(text); } // Destructor. qpwgraph_patchman::TreeWidget::~TreeWidget (void) { } // Side mode accessors. bool qpwgraph_patchman::TreeWidget::isOutputs (void) const { return (m_mode == Outputs); } bool qpwgraph_patchman::TreeWidget::isInputs (void) const { return (m_mode == Inputs); } // Node/port item finders. QTreeWidgetItem *qpwgraph_patchman::TreeWidget::findNodeItem ( const QString& node_name, int node_type ) const { const int nitems = QTreeWidget::topLevelItemCount(); for (int i = 0; i < nitems; ++i) { QTreeWidgetItem *node_item = QTreeWidget::topLevelItem(i); if (node_item->text(0) == node_name && node_item->type() == node_type) { return node_item; } } return nullptr; } QTreeWidgetItem *qpwgraph_patchman::TreeWidget::findPortItem ( QTreeWidgetItem *node_item, const QString& port_name, int port_type ) const { const int nitems = node_item->childCount(); for (int i = 0; i < nitems; ++i) { QTreeWidgetItem *port_item = node_item->child(i); if (port_item->text(0) == port_name && port_item->type() == port_type) { return port_item; } } return nullptr; } // Initial size hints. QSize qpwgraph_patchman::TreeWidget::sizeHint (void) const { return QSize(290, 260); } //---------------------------------------------------------------------------- // qpwgraph_patchman::LineWidget -- middle-view line widget impl. // Connector line color map/persistence. QHash qpwgraph_patchman::LineWidget::g_colors; // Constructor. qpwgraph_patchman::LineWidget::LineWidget ( MainWidget *parent ) : QWidget(parent), m_main(parent) { QWidget::setSizePolicy( QSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding)); QWidget::setMinimumWidth(20); } // Destructor. qpwgraph_patchman::LineWidget::~LineWidget (void) { } // Connect-line managers. void qpwgraph_patchman::LineWidget::addLine ( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item ) { m_lines.insert(port1_item, port2_item); } void qpwgraph_patchman::LineWidget::removeLine ( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item ) { m_lines.remove(port1_item, port2_item); } // Connect line finder. bool qpwgraph_patchman::LineWidget::findLine ( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item ) const { const QList& port2_items = m_lines.values(port1_item); return port2_items.contains(port2_item); } // Connect-lines cleaner. void qpwgraph_patchman::LineWidget::clear (void) { m_lines.clear(); QWidget::update(); } // Connect-line empty status. bool qpwgraph_patchman::LineWidget::isEmpty (void) const { return m_lines.isEmpty(); } // Legal client/port item position helper. int qpwgraph_patchman::LineWidget::itemY ( QTreeWidgetItem *item ) const { QRect rect; QTreeWidget *tree_widget = item->treeWidget(); QTreeWidgetItem *parent_item = item->parent(); if (parent_item && !parent_item->isExpanded()) rect = tree_widget->visualItemRect(parent_item); else rect = tree_widget->visualItemRect(item); return rect.top() + rect.height() / 2; } // Draw one connection line. void qpwgraph_patchman::LineWidget::drawLine ( QPainter *painter, int x1, int y1, int x2, int y2, int h1, int h2, const QPen& pen ) const { // Set apropriate pen... painter->setPen(pen); // Account for list view headers. y1 += h1; y2 += h2; // Invisible output ports don't get a connecting dot. if (y1 > h1) painter->drawLine(x1, y1, x1 + 4, y1); // Setup control points QPolygon spline(4); const int cp = int(float(x2 - x1 - 8) * 0.4f); spline.putPoints(0, 4, x1 + 4, y1, x1 + 4 + cp, y1, x2 - 4 - cp, y2, x2 - 4, y2); // The connection line, it self. QPainterPath path; path.moveTo(spline.at(0)); path.cubicTo(spline.at(1), spline.at(2), spline.at(3)); painter->strokePath(path, pen); // Invisible input ports don't get a connecting dot. if (y2 > h2) painter->drawLine(x2 - 4, y2, x2, y2); } // Draw connection lines. void qpwgraph_patchman::LineWidget::paintEvent ( QPaintEvent * ) { TreeWidget *outputs = m_main->outputs(); TreeWidget *inputs = m_main->inputs(); const int yc = QWidget::pos().y(); const int yo = outputs->pos().y(); const int yi = inputs->pos().y(); QPainter painter(this); int x1, y1, h1; int x2, y2, h2; int rgb[3] = { 0x33, 0x66, 0x99 }; // Draw all lines anti-aliased... painter.setRenderHint(QPainter::Antialiasing); // Inline adaptive to darker background themes... if (QWidget::palette().window().color().value() < 0x7f) for (int i = 0; i < 3; ++i) rgb[i] += 0x33; // Almost constants. x1 = 0; x2 = QWidget::width(); h1 = (outputs->header())->sizeHint().height(); h2 = (inputs->header())->sizeHint().height(); Lines::ConstIterator iter = m_lines.constBegin(); const Lines::ConstIterator& iter_end = m_lines.constEnd(); for ( ; iter != iter_end; ++iter) { QTreeWidgetItem *port1_item = iter.key(); QTreeWidgetItem *node1_item = port1_item->parent(); if (node1_item == nullptr) continue; // Set new connector color. const QString& node1_name = node1_item->text(0); int k = g_colors.value(node1_name, -1); if (k < 0) { k = g_colors.size() + 1; g_colors.insert(node1_name, k); } QPen pen(QColor(rgb[k % 3], rgb[(k / 3) % 3], rgb[(k / 9) % 3])); // Get starting connector line coordinates. y1 = itemY(port1_item) + (yo - yc); const QList& port2_items = m_lines.values(port1_item); for (QTreeWidgetItem *port2_item : port2_items) { QTreeWidgetItem *node2_item = port2_item->parent(); if (node2_item == nullptr) continue; // Actual currently active connections lines are thicker: pen.setWidth(( port1_item->data(0, Qt::UserRole).toInt() & port2_item->data(0, Qt::UserRole).toInt() & 2) ? 2 : 1); // Obviously, should be a connection // from port1 to port2 items: y2 = itemY(port2_item) + (yi - yc); drawLine(&painter, x1, y1, x2, y2, h1, h2, pen); } } } // Initial size hints. QSize qpwgraph_patchman::LineWidget::sizeHint (void) const { return QSize(60, 260); } //---------------------------------------------------------------------------- // qpwgraph_patchman::MainWidget -- main-view composite widget impl. // Constructor. qpwgraph_patchman::MainWidget::MainWidget ( qpwgraph_patchman *parent ) : QSplitter(parent), m_patchman(parent) { m_outputs = new TreeWidget(this, TreeWidget::Outputs); m_connects = new LineWidget(this); m_inputs = new TreeWidget(this, TreeWidget::Inputs); QSplitter::setHandleWidth(2); QObject::connect(m_outputs, SIGNAL(itemExpanded(QTreeWidgetItem *)), m_connects, SLOT(update())); QObject::connect(m_outputs, SIGNAL(itemCollapsed(QTreeWidgetItem *)), m_connects, SLOT(update())); QObject::connect(m_outputs->verticalScrollBar(), SIGNAL(valueChanged(int)), m_connects, SLOT(update())); QObject::connect(m_outputs->header(), SIGNAL(sectionClicked(int)), m_connects, SLOT(update())); QObject::connect(m_inputs, SIGNAL(itemExpanded(QTreeWidgetItem *)), m_connects, SLOT(update())); QObject::connect(m_inputs, SIGNAL(itemCollapsed(QTreeWidgetItem *)), m_connects, SLOT(update())); QObject::connect(m_inputs->verticalScrollBar(), SIGNAL(valueChanged(int)), m_connects, SLOT(update())); QObject::connect(m_inputs->header(), SIGNAL(sectionClicked(int)), m_connects, SLOT(update())); } // Destructor. qpwgraph_patchman::MainWidget::~MainWidget (void) { m_items.clearItems(); } // Patchbay items accessors. void qpwgraph_patchman::MainWidget::setItems ( const qpwgraph_patchbay::Items& items ) { m_items.copyItems(items); refresh(); } const qpwgraph_patchbay::Items& qpwgraph_patchman::MainWidget::items (void) const { return m_items; } // Patchbay view refresh. void qpwgraph_patchman::MainWidget::refresh (void) { qpwgraph_patchbay *patchbay = m_patchman->patchbay(); if (patchbay == nullptr) return; m_outputs->clear(); m_connects->clear(); m_inputs->clear(); QFont font; font.setBold(true); qpwgraph_patchbay::Items::ConstIterator iter = m_items.constBegin(); const qpwgraph_patchbay::Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) { qpwgraph_patchbay::Item *item = iter.value(); const int data = (findConnect(item) ? 2 : 0); const int node_type = item->node_type; QIcon node_icon; if (node_type == qpwgraph_pipewire::nodeType()) node_icon = QIcon(":images/itemPipewire.png"); #ifdef CONFIG_ALSA_MIDI else if (node_type == qpwgraph_alsamidi::nodeType()) node_icon = QIcon(":images/itemAlsamidi.png"); #endif const int port_type = item->port_type; QIcon port_icon; const QColor& color = (patchbay->canvas())->portTypeColor(port_type); if (color.isValid()) { QPixmap pm(8, 8); QPainter(&pm).fillRect(0, 0, pm.width(), pm.height(), color); port_icon = QIcon(pm); } // Output node/port... const QString& node1_name = item->node1; QTreeWidgetItem *node1_item = m_outputs->findNodeItem(node1_name, node_type); if (node1_item == nullptr) { node1_item = new QTreeWidgetItem(m_outputs, node_type); node1_item->setIcon(0, node_icon); node1_item->setText(0, node1_name); node1_item->setData(0, Qt::UserRole, data); } else { node1_item->setData(0, Qt::UserRole, data | node1_item->data(0, Qt::UserRole).toInt()); } const QString& port1_name = item->port1; QTreeWidgetItem *port1_item = m_outputs->findPortItem(node1_item, port1_name, port_type); if (port1_item == nullptr) { port1_item = new QTreeWidgetItem(node1_item, port_type); port1_item->setIcon(0, port_icon); port1_item->setText(0, port1_name); port1_item->setData(0, Qt::UserRole, data); } else { port1_item->setData(0, Qt::UserRole, data | port1_item->data(0, Qt::UserRole).toInt()); } // Input node/port... const QString& node2_name = item->node2; QTreeWidgetItem *node2_item = m_inputs->findNodeItem(node2_name, node_type); if (node2_item == nullptr) { node2_item = new QTreeWidgetItem(m_inputs, node_type); node2_item->setIcon(0, node_icon); node2_item->setText(0, node2_name); node2_item->setData(0, Qt::UserRole, data); } else { node2_item->setData(0, Qt::UserRole, data | node2_item->data(0, Qt::UserRole).toInt()); } const QString& port2_name = item->port2; QTreeWidgetItem *port2_item = m_inputs->findPortItem(node2_item, port2_name, port_type); if (port2_item == nullptr) { port2_item = new QTreeWidgetItem(node2_item, port_type); port2_item->setIcon(0, port_icon); port2_item->setText(0, port2_name); port2_item->setData(0, Qt::UserRole, data); } else { port2_item->setData(0, Qt::UserRole, data | port2_item->data(0, Qt::UserRole).toInt()); } // Connect line... m_connects->addLine(port1_item, port2_item); } m_inputs->expandAll(); m_outputs->expandAll(); m_connects->update(); } // Patchbay management actions. bool qpwgraph_patchman::MainWidget::canRemove (void) const { const QList& items1 = m_outputs->selectedItems(); const QList& items2 = m_inputs->selectedItems(); if (items1.isEmpty() || items2.isEmpty()) return false; QListIterator iter1(items1); QListIterator iter2(items2); const int nitems = qMax(items1.count(),items2.count()); for (int i = 0; i < nitems; ++i) { if (!iter1.hasNext()) iter1.toFront(); if (!iter2.hasNext()) iter2.toFront(); QTreeWidgetItem *item1 = iter1.next(); QTreeWidgetItem *item2 = iter2.next(); if (item2->parent() == nullptr) { if (item1->parent() == nullptr) { // Each-to-each connections... const int nchilds = qMin(item1->childCount(), item2->childCount()); for (int j = 0; j < nchilds; ++j) { QTreeWidgetItem *port1_item = item1->child(j); QTreeWidgetItem *port2_item = item2->child(j); if (findLine(port1_item, port2_item)) return true; } } else { // Many(all)-to-one/many connection... const int nchilds = item2->childCount(); for (int j = 0; j < nchilds; ++j) { QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2->child(j); if (findLine(port1_item, port2_item)) return true; } } } else { if (item1->parent() == nullptr) { // Many(all)-to-one/many connection... const int nchilds = item1->childCount(); for (int j = 0; j < nchilds; ++j) { QTreeWidgetItem *port1_item = item1->child(j); QTreeWidgetItem *port2_item = item2; if (findLine(port1_item, port2_item)) return true; } } else { // One-to-many(all) connection... QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2; if (findLine(port1_item, port2_item)) return true; } } } return false; } void qpwgraph_patchman::MainWidget::remove (void) { int nremoved = 0; const QList& items1 = m_outputs->selectedItems(); const QList& items2 = m_inputs->selectedItems(); if (items1.isEmpty() || items2.isEmpty()) return; QListIterator iter1(items1); QListIterator iter2(items2); const int nitems = qMax(items1.count(),items2.count()); for (int i = 0; i < nitems; ++i) { if (!iter1.hasNext()) iter1.toFront(); if (!iter2.hasNext()) iter2.toFront(); QTreeWidgetItem *item1 = iter1.next(); QTreeWidgetItem *item2 = iter2.next(); if (item2->parent() == nullptr) { if (item1->parent() == nullptr) { // Each-to-each connections... const int nchilds = qMin(item1->childCount(), item2->childCount()); for (int j = 0; j < nchilds; ++j) { QTreeWidgetItem *port1_item = item1->child(j); QTreeWidgetItem *port2_item = item2->child(j); if (removeLine(port1_item, port2_item)) ++nremoved; } } else { // Many(all)-to-one/many connection... const int nchilds = item2->childCount(); for (int j = 0; j < nchilds; ++j) { QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2->child(j); if (removeLine(port1_item, port2_item)) ++nremoved; } } } else { if (item1->parent() == nullptr) { // Many(all)-to-one/many connection... const int nchilds = item1->childCount(); for (int j = 0; j < nchilds; ++j) { QTreeWidgetItem *port1_item = item1->child(j); QTreeWidgetItem *port2_item = item2; if (removeLine(port1_item, port2_item)) ++nremoved; } } else { // One-to-many(all) connection... QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2; if (removeLine(port1_item, port2_item)) ++nremoved; } } } // Refresh if anything has been removed... if (nremoved > 0) refresh(); } bool qpwgraph_patchman::MainWidget::canRemoveAll (void) const { return !m_connects->isEmpty(); } void qpwgraph_patchman::MainWidget::removeAll (void) { m_items.clearItems(); refresh(); } bool qpwgraph_patchman::MainWidget::canCleanup (void) const { qpwgraph_patchbay::Items::ConstIterator iter = m_items.constBegin(); const qpwgraph_patchbay::Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) { qpwgraph_patchbay::Item *item = iter.value(); if (!findConnect(iter.value())) return true; } return false; } void qpwgraph_patchman::MainWidget::cleanup (void) { QList items; qpwgraph_patchbay::Items::ConstIterator iter = m_items.constBegin(); const qpwgraph_patchbay::Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) { qpwgraph_patchbay::Item *item = iter.value(); if (!findConnect(item)) items.append(item); } if (!items.isEmpty()) { QListIterator iter2(items); while (iter2.hasNext()) m_items.removeItem(*iter2.next()); refresh(); } } // Patchbay item connection finder. bool qpwgraph_patchman::MainWidget::findConnect ( qpwgraph_patchbay::Item *item ) const { qpwgraph_patchbay *patchbay = m_patchman->patchbay(); if (patchbay == nullptr) return false; qpwgraph_canvas *canvas = patchbay->canvas(); if (canvas == nullptr) return false; QList nodes1 = canvas->findNodes( item->node1, qpwgraph_item::Output, item->node_type); if (nodes1.isEmpty()) nodes1 = canvas->findNodes( item->node1, qpwgraph_item::Duplex, item->node_type); if (nodes1.isEmpty()) return false; foreach (qpwgraph_node *node1, nodes1) { qpwgraph_port *port1 = node1->findPort( item->port1, qpwgraph_item::Output, item->port_type); if (port1 == nullptr) continue; QList nodes2 = canvas->findNodes( item->node2, qpwgraph_item::Input, item->node_type); if (nodes2.isEmpty()) nodes2 = 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; qpwgraph_connect *connect12 = port1->findConnect(port2); if (connect12) return true; qpwgraph_connect *connect21 = port2->findConnect(port1); if (connect21) return true; } } return false; } // Patchbay connect line finder/removal. bool qpwgraph_patchman::MainWidget::findLine ( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item ) const { return m_connects->findLine(port1_item, port2_item); } bool qpwgraph_patchman::MainWidget::removeLine ( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item ) { if (!findLine(port1_item, port2_item)) return false; QTreeWidgetItem *node1_item = port1_item->parent(); QTreeWidgetItem *node2_item = port2_item->parent(); if (node1_item == nullptr || node2_item == nullptr) return false; m_connects->removeLine(port1_item, port2_item); return m_items.removeItem( qpwgraph_patchbay::Item( node1_item->type(), port1_item->type(), node1_item->text(0), port1_item->text(0), node2_item->text(0), port2_item->text(0))); } // Stabilize item highlights. void qpwgraph_patchman::MainWidget::stabilize (void) { QHash items; qpwgraph_patchbay::Items::ConstIterator iter = m_items.constBegin(); const qpwgraph_patchbay::Items::ConstIterator& iter_end = m_items.constEnd(); for ( ; iter != iter_end; ++iter) { qpwgraph_patchbay::Item *item = iter.value(); QTreeWidgetItem *node1_item = m_outputs->findNodeItem(item->node1, item->node_type); if (node1_item == nullptr) continue; QTreeWidgetItem *node2_item = m_inputs->findNodeItem(item->node2, item->node_type); if (node2_item == nullptr) continue; QTreeWidgetItem *port1_item = m_outputs->findPortItem(node1_item, item->port1, item->port_type); if (port1_item == nullptr) continue; QTreeWidgetItem *port2_item = m_inputs->findPortItem(node2_item, item->port2, item->port_type); if (port2_item == nullptr) continue; const bool hilite1 = (node2_item->isSelected() || port2_item->isSelected()); int n = items.value(node1_item, 0); if (hilite1) ++n; items.insert(node1_item, n); n = items.value(port1_item, 0); if (hilite1) ++n; items.insert(port1_item, n); const bool hilite2 = (node1_item->isSelected() || port1_item->isSelected()); n = items.value(node2_item, 0); if (hilite2) ++n; items.insert(node2_item, n); n = items.value(port2_item, 0); if (hilite2) ++n; items.insert(port2_item, n); } QHash::ConstIterator items_iter = items.constBegin(); const QHash::ConstIterator& items_end = items.constEnd(); for ( ; items_iter != items_end; ++items_iter) { QTreeWidgetItem *item = items_iter.key(); const bool hilite = (items_iter.value() > 0); int data = item->data(0, Qt::UserRole).toInt(); if ((item->parent() || !item->isExpanded()) && hilite) data |= 1; else data &= 2; item->setData(0, Qt::UserRole, data); } } // Initial size hints. QSize qpwgraph_patchman::MainWidget::sizeHint (void) const { return QSize(640, 260); } //---------------------------------------------------------------------------- // qpwgraph_patchman -- main dialog impl. // Constructor. qpwgraph_patchman::qpwgraph_patchman ( QWidget *parent ) : QDialog(parent), m_patchbay(nullptr), m_dirty(0) { QDialog::setWindowTitle(tr("Manage Patchbay")); m_remove_button = new QPushButton("&Remove"); m_remove_all_button = new QPushButton("Remove &All"); m_cleanup_button = new QPushButton("Clean&up"); m_reset_button = new QPushButton("Re&set"); m_button_box = new QDialogButtonBox(); m_button_box->setStandardButtons( QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_main = new MainWidget(this); QHBoxLayout *hbox = new QHBoxLayout(); hbox->setContentsMargins(4, 8, 4, 4); hbox->setSpacing(8); hbox->addWidget(m_remove_button); hbox->addWidget(m_remove_all_button); hbox->addStretch(20); hbox->addWidget(m_cleanup_button); hbox->addStretch(8); hbox->addWidget(m_reset_button); hbox->addStretch(20); hbox->addWidget(m_button_box); QVBoxLayout *vbox = new QVBoxLayout(); vbox->setContentsMargins(4, 8, 4, 4); vbox->setSpacing(4); vbox->addWidget(m_main); vbox->addLayout(hbox); QDialog::setLayout(vbox); QObject::connect(m_main->outputs(), SIGNAL(itemSelectionChanged()), SLOT(stabilize())); QObject::connect(m_main->outputs(), SIGNAL(itemExpanded(QTreeWidgetItem *)), SLOT(stabilize())); QObject::connect(m_main->outputs(), SIGNAL(itemCollapsed(QTreeWidgetItem *)), SLOT(stabilize())); QObject::connect(m_main->inputs(), SIGNAL(itemSelectionChanged()), SLOT(stabilize())); QObject::connect(m_main->inputs(), SIGNAL(itemExpanded(QTreeWidgetItem *)), SLOT(stabilize())); QObject::connect(m_main->inputs(), SIGNAL(itemCollapsed(QTreeWidgetItem *)), SLOT(stabilize())); QObject::connect(m_remove_button, SIGNAL(clicked()), SLOT(removeClicked())); QObject::connect(m_remove_all_button, SIGNAL(clicked()), SLOT(removeAllClicked())); QObject::connect(m_cleanup_button, SIGNAL(clicked()), SLOT(cleanupClicked())); QObject::connect(m_reset_button, SIGNAL(clicked()), SLOT(resetClicked())); QObject::connect(m_button_box, SIGNAL(accepted()), SLOT(accept())); QObject::connect(m_button_box, SIGNAL(rejected()), SLOT(reject())); // Ready? stabilize(); } // Patchbay accessors. void qpwgraph_patchman::setPatchbay ( qpwgraph_patchbay *patchbay ) { m_patchbay = patchbay; resetClicked(); } qpwgraph_patchbay *qpwgraph_patchman::patchbay (void) const { return m_patchbay; } // Patchbay view refresh. void qpwgraph_patchman::refresh (void) { m_main->refresh(); stabilize(); } // Destructor. qpwgraph_patchman::~qpwgraph_patchman (void) { } void qpwgraph_patchman::removeClicked (void) { m_main->remove(); ++m_dirty; stabilize(); } void qpwgraph_patchman::removeAllClicked (void) { m_main->removeAll(); ++m_dirty; stabilize(); } void qpwgraph_patchman::cleanupClicked (void) { m_main->cleanup(); ++m_dirty; stabilize(); } void qpwgraph_patchman::resetClicked (void) { if (m_patchbay) m_main->setItems(m_patchbay->items()); m_dirty = 0; stabilize(); } void qpwgraph_patchman::accept (void) { if (m_patchbay) m_patchbay->setItems(m_main->items()); QDialog::accept(); } void qpwgraph_patchman::reject (void) { bool ret = true; // Check if there's any pending changes... if (m_dirty > 0) { switch (QMessageBox::warning(this, tr("Warning"), tr("The current patchbay have been changed.") + "\n\n" + tr("Do you want to apply the changes?"), QMessageBox::Apply | QMessageBox::Discard | QMessageBox::Cancel)) { case QMessageBox::Apply: accept(); return; case QMessageBox::Discard: break; default: // Cancel. ret = false; } } if (ret) QDialog::reject(); } // Stabilize current form state. void qpwgraph_patchman::stabilize (void) { m_main->stabilize(); m_remove_button->setEnabled(m_main->canRemove()); m_remove_all_button->setEnabled(m_main->canRemoveAll()); m_cleanup_button->setEnabled(m_main->canCleanup()); m_reset_button->setEnabled(m_dirty > 0); m_button_box->button(QDialogButtonBox::Ok)->setEnabled(m_dirty > 0); } // end of qpwgraph_patchman.cpp qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_sect.h0000644000000000000000000000013214762602507016250 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_sect.h0000644000175000001440000000375414762602507016251 0ustar00rncbcusers// 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.8.2/src/PaxHeaders/qpwgraph_alsamidi.cpp0000644000000000000000000000013214762602507017430 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_alsamidi.cpp0000644000175000001440000002744214762602507017431 0ustar00rncbcusers// qpwgraph_alsamidi.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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 ) { qpwgraph_canvas *canvas = qpwgraph_sect::canvas(); if (canvas == nullptr) return false; 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 && !canvas->isFilterNodes(node_name)) { *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(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.8.2/src/PaxHeaders/qpwgraph_canvas.h0000644000000000000000000000013214762602507016565 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_canvas.h0000644000175000001440000002171714762602507016565 0ustar00rncbcusers// qpwgraph_canvas.h // /**************************************************************************** Copyright (C) 2021-2024, 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(); bool isPatchbayEmpty() const; // 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; bool canSearchItem() 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 notifier. void emitRenamed(qpwgraph_item *item, const QString& name); // Other generic notifier. void emitChanged(); // 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(const QPointF& pos) const; #ifdef CONFIG_CLEANUP_NODE_NAMES static bool cleanupNodeName(QString& name); #endif // Search placeholder text accessors. void setSearchPlaceholderText(const QString& text); QString searchPlaceholderText() const; // Filter/hide list management accessors. void setFilterNodesEnabled(bool enabled); bool isFilterNodesEnabled() const; void setFilterNodesList(const QStringList& nodes); const QStringList& filterNodesList() const; bool isFilterNodes(const QString& node_name) const; 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(); void searchItem(); // 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 renameTextChanged(const QString&); void renameEditingFinished(); // Search item slots. void searchTextChanged(const QString&); void searchEditingFinished(); 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; // Update editors position and size. void updateRenameEditor(); void updateSearchEditor(); // Bounding margins/limits... const QRectF& boundingRect(bool reset = false); void boundingPos(QPointF& pos); // Start search editor... void startSearchEditor(const QString& text = QString()); void resizeEvent(QResizeEvent *event) override; #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_rename_item; QLineEdit *m_rename_editor; int m_renamed; // Original node position (for move command). QPointF m_pos1; // Allowed auto-scroll margins/limits (for move command). QRectF m_rect1; // Item search stuff. QLineEdit *m_search_editor; // Filter/hide list management. bool m_filter_enabled; QStringList m_filter_nodes; }; #endif // __qpwgraph_canvas_h // end of qpwgraph_canvas.h qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_node.cpp0000644000000000000000000000013214762602507016572 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_node.cpp0000644000175000001440000002304514762602507016566 0ustar00rncbcusers// qpwgraph_node.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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.simplified(); if (text.length() >= MAX_TITLE_LENGTH + ellipsis.length()) text = text.left(MAX_TITLE_LENGTH).trimmed() + ellipsis; m_text->setPlainText(text); } const QString& qpwgraph_node::nodeTitle (void) const { return m_title; } void qpwgraph_node::setNodePrefix ( const QString& prefix ) { m_prefix = prefix; } const QString& qpwgraph_node::nodePrefix (void) const { return m_prefix; } // 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 = 4 * ((wi + wo) / 4); 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 + 6 - w, yo); yo += h; if (height < yo) height = yo; } else { port->setPos(-6, yi); yi += h; if (height < yi) height = yi; } } QPainterPath path; path.addRoundedRect(0, 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(); const qreal w2 = (node_rect.width() - text_rect.width()) / 2; m_text->setPos(node_rect.x() + w2 + 4, node_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.8.2/src/PaxHeaders/qpwgraph_item.h0000644000000000000000000000013214762602507016250 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_item.h0000644000175000001440000001001014762602507016230 0ustar00rncbcusers// 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.8.2/src/PaxHeaders/qpwgraph_node.h0000644000000000000000000000013214762602507016237 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_node.h0000644000175000001440000001004214762602507016224 0ustar00rncbcusers// qpwgraph_node.h // /**************************************************************************** Copyright (C) 2021-2024, 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; void setNodePrefix(const QString& prefix); const QString& nodePrefix() 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; QString m_prefix; 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.8.2/src/PaxHeaders/qpwgraph_item.cpp0000644000000000000000000000013214762602507016603 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_item.cpp0000644000175000001440000000627314762602507016603 0ustar00rncbcusers// 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.8.2/src/PaxHeaders/qpwgraph_alsamidi.h0000644000000000000000000000013214762602507017075 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_alsamidi.h0000644000175000001440000000506614762602507017074 0ustar00rncbcusers// qpwgraph_alsamidi.h // /**************************************************************************** Copyright (C) 2021-2024, 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.8.2/src/PaxHeaders/qpwgraph_patchbay.cpp0000644000000000000000000000013214762602507017440 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_patchbay.cpp0000644000175000001440000003263714762602507017443 0ustar00rncbcusers// qpwgraph_patchbay.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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. // Manage connection rules. bool qpwgraph_patchbay::Items::addItem ( const Item& item ) { bool ret = false; ConstIterator iter = constFind(item); if (iter == constEnd()) { insert(item, new Item(item)); ret = true; } return ret; } bool qpwgraph_patchbay::Items::removeItem ( const Item& item ) { bool ret = false; ConstIterator iter = constFind(item); if (iter != constEnd()) { delete iter.value(); erase(iter); ret = true; } return ret; } // Copy all patchbay rules and cache. void qpwgraph_patchbay::Items::copyItems ( const Items& items ) { clearItems(); Items::ConstIterator iter = items.constBegin(); const Items::ConstIterator& iter_end = items.constEnd(); for ( ; iter != iter_end; ++iter) addItem(iter.key()); } // Clear all patchbay rules and cache. void qpwgraph_patchbay::Items::clearItems (void) { ConstIterator iter = constBegin(); const ConstIterator& iter_end = constEnd(); for ( ; iter != iter_end; ++iter) delete iter.value(); clear(); } void qpwgraph_patchbay::clear (void) { m_items.clearItems(); 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) { m_items.addItem(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()) { m_items.addItem(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()); if (is_connect) ret = m_items.addItem(item); else ret = m_items.removeItem(item); } 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()); } // Dirty status flag. void qpwgraph_patchbay::setItems ( const Items& items ) { m_items.copyItems(items); ++m_dirty; m_canvas->patchbayEdit(); emit m_canvas->changed(); } // 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.8.2/src/PaxHeaders/mimetypes0000644000000000000000000000013214762602507015167 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.417265981 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/mimetypes/0000755000175000001440000000000014762602507015234 5ustar00rncbcusersqpwgraph-0.8.2/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.png0000644000000000000000000000013214762602507027646 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/mimetypes/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.png0000644000175000001440000000303114762602507027633 0ustar00rncbcusersPNG  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.8.2/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.xml0000644000000000000000000000013214762602507021473 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/mimetypes/org.rncbc.qpwgraph.xml0000644000175000001440000000046014762602507021463 0ustar00rncbcusers qpwgraph patchbay qpwgraph-0.8.2/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.svg0000644000000000000000000000013214762602507027661 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.417265981 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/mimetypes/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.svg0000644000175000001440000003270214762602507027655 0ustar00rncbcusers qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_sect.cpp0000644000000000000000000000013214762602507016603 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_sect.cpp0000644000175000001440000000626314762602507016602 0ustar00rncbcusers// 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.8.2/src/PaxHeaders/qpwgraph_port.h0000644000000000000000000000013214762602507016276 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_port.h0000644000175000001440000001140414762602507016266 0ustar00rncbcusers// qpwgraph_port.h // /**************************************************************************** Copyright (C) 2021-2024, 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.8.2/src/PaxHeaders/qpwgraph.qrc0000644000000000000000000000013214762602507015570 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph.qrc0000644000175000001440000000215214762602507015560 0ustar00rncbcusers 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/itemFind.png images/fileNew.png images/fileOpen.png images/fileSave.png images/editUndo.png images/editRedo.png images/viewCenter.png images/viewColors.png images/viewThumbview.png images/viewZoomIn.png images/viewZoomOut.png images/viewZoomFit.png images/viewZoomReset.png images/viewZoomRange.png images/viewZoomTool.png qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_pipewire.cpp0000644000000000000000000000013214762602507017471 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_pipewire.cpp0000644000175000001440000010613714762602507017471 0ustar00rncbcusers// qpwgraph_pipewire.cpp // /**************************************************************************** Copyright (C) 2021-2025, 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_MIDI2_TYPE "32 bit raw UMP" #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() && !name.isEmpty() && name != "node") { 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; QString node_nick; 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; if (node->node_nick.isEmpty()) { qpwgraph_pipewire *pw = nullptr; if (node->p) pw = (node->p)->pw; qpwgraph_pipewire::Data::NodeNames *node_names = nullptr; if (pw && pw->data()) node_names = (pw->data())->node_names; if (node_names) { node_names->remove( qpwgraph_pipewire::Node::NameKey(node), node->name_num); node->node_name = node->media_name; node->media_name.clear(); node_names->insert( qpwgraph_pipewire::Node::NameKey(node), node->name_num); } } } 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); const char *nick = spa_dict_lookup(props, PW_KEY_NODE_NICK); if (str == nullptr || ::strlen(str) < 1) str = nick; if (str == nullptr || ::strlen(str) < 1) str = nick = spa_dict_lookup(props, PW_KEY_NODE_NAME); if (str == nullptr || ::strlen(str) < 1) str = "node"; const char *app = spa_dict_lookup(props, PW_KEY_APP_NAME); if (app && ::strlen(app) > 0 && ::strcmp(app, str) != 0) { node_name += app; node_name += '/'; } node_name += str; const QString node_nick(nick ? nick : 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_nick, 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->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; } pw_thread_loop_lock(m_data->loop); 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_unlock(m_data->loop); 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_thread_loop_unlock(m_data->loop); 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; m_data->node_names = new Data::NodeNames; pw_thread_loop_start(m_data->loop); pw_thread_loop_unlock(m_data->loop); return true; } void qpwgraph_pipewire::close (void) { if (m_data == nullptr) return; QMutexLocker locker1(&m_mutex1); pw_thread_loop_lock(m_data->loop); clearObjects(); pw_thread_loop_unlock(m_data->loop); 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::midi2PortType (void) { return qpwgraph_item::itemType(DEFAULT_MIDI2_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 ) { qpwgraph_canvas *canvas = qpwgraph_sect::canvas(); if (canvas == nullptr) return false; 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 && !canvas->isFilterNodes(n->node_name)) { 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); (*node)->setNodePrefix(n->node_nick); 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(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, const QString& node_nick, 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_nick = node_nick; 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); if (port->port_type == midi2PortType()) port->port_type = midiPortType(); // FIXME: legacy aliasing!... 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.8.2/src/PaxHeaders/qpwgraph_main.ui0000644000000000000000000000013214762602507016424 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_main.ui0000644000175000001440000010567714762602507016434 0ustar00rncbcusers rncbc aka Rui Nuno Capela qpwgraph - A PipeWire Graph Qt GUI Interface Copyright (C) 2021-2025, 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_main 0 0 800 600 qpwgraph :/images/qpwgraph.svg 0 0 0 0 800 20 &Graph &Patchbay Open &Recent &Edit &View &Toolbar T&humbview :/images/viewThumbview.png &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 &Options... Options Options Change general application program options &Quit Quit Quit Quit this application program Ctrl+Q :/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 &Manage... Manage Manage current patchbay 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 :/images/itemFind.png &Find... Find Find nodes Find nodes Ctrl+F 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 &Top Left Top left Top left Show the thumbnail overview on the top-left true Top &Right Top Right Top right Show the thumbnail overview on the top-right true Bottom &Left Bottom Left Bottom left Show the thumbnail overview on the bottom-left true &Bottom Right Bottom Right Bottom right Show the thumbnail overview on the bottom-right true &None None Hide thumbview Hide the thumbnail overview 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 Color PipeWire MIDI color PipeWire MIDI color PipeWire &Video... PipeWire Video Color PipeWire Video color PipeWire Video color PipeWire &Other... PipeWire Other Color PipeWire Other color PipeWire Other color ALSA M&IDI... ALSA MIDI Color 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 &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.8.2/src/PaxHeaders/qpwgraph_port.cpp0000644000000000000000000000013214762602507016631 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_port.cpp0000644000175000001440000002551514762602507016631 0ustar00rncbcusers// qpwgraph_port.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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.simplified(); if (m_node) text.remove(QRegularExpression('^' + m_node->nodePrefix() + ':')); if (text.length() >= MAX_TITLE_LENGTH) { 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).trimmed(); 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.8.2/src/PaxHeaders/qpwgraph_connect.cpp0000644000000000000000000000013214762602507017276 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418115141 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_connect.cpp0000644000175000001440000002074414762602507017275 0ustar00rncbcusers// 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.8.2/src/PaxHeaders/qpwgraph_systray.h0000644000000000000000000000013214762602507017030 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_systray.h0000644000175000001440000000424614762602507017026 0ustar00rncbcusers// qpwgraph_systray.h // /**************************************************************************** Copyright (C) 2021-2024, 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_main; class QAction; //---------------------------------------------------------------------------- // qpwgraph_systray -- Custom system tray icon. class qpwgraph_systray : public QSystemTrayIcon { Q_OBJECT public: // Constructor. qpwgraph_systray(qpwgraph_main *main); // Destructor. ~qpwgraph_systray(); // Update context menu. void updateContextMenu(); protected slots: // Handle systeam tray activity. void activated(QSystemTrayIcon::ActivationReason reason); // Handle menu actions. void showHide(); // Handle presets menu actions. void patchbayPresetTriggered(QAction *action); // Rebuild the patchbay presets menu. void updatePatchbayPresets(); signals: // Notify about preset changes. void patchbayPresetChanged(int index); protected: // Destroy the patchbay presets menu. void clearPatchbayPresets(); private: qpwgraph_main *m_main; QAction *m_show; QAction *m_quit; QMenu m_menu; QMenu *m_presets; }; #endif // CONFIG_SYSTEM_TRAY #endif // __qpwgraph_systray_h // end of qpwgraph_systray.h qpwgraph-0.8.2/src/PaxHeaders/qpwgraph.cpp0000644000000000000000000000013214762602507015565 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph.cpp0000644000175000001440000002013514762602507015556 0ustar00rncbcusers// qpwgraph.cpp // /**************************************************************************** Copyright (C) 2021-2025, 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_main.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)); QApplication::setApplicationVersion(PROJECT_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.")}); const QCommandLineOption& helpOption = parser.addHelpOption(); const QCommandLineOption& versionOption = parser.addVersionOption(); parser.addPositionalArgument("patchbay-file", QObject::tr("Patchbay file (.%1)") .arg(QString(PROJECT_NAME).toLower()), QObject::tr("[patchbay-file]")); QTextStream out(stderr); if (!parser.parse(args)) { out << parser.errorText() << '\n'; return false; } if (parser.isSet(helpOption)) { out << parser.helpText() << '\n'; return false; } if (parser.isSet(versionOption)) { out << QString("%1 %2\n") .arg(PROJECT_NAME) .arg(QCoreApplication::applicationVersion()); out << QString("Qt: %1").arg(qVersion()); #if defined(QT_STATIC) out << "-static"; #endif out << '\n' << '\n';; return false; } 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_main *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_main 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.8.2/src/PaxHeaders/qpwgraph_pipewire.h0000644000000000000000000000013214762602507017136 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_pipewire.h0000644000175000001440000001017714762602507017134 0ustar00rncbcusers// qpwgraph_pipewire.h // /**************************************************************************** Copyright (C) 2021-2025, 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 midi2PortType(); 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, const QString& node_nick, 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 qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_canvas.cpp0000644000000000000000000000013214762602507017120 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_canvas.cpp0000644000175000001440000014016714762602507017121 0ustar00rncbcusers// qpwgraph_canvas.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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_rename_item(nullptr), m_rename_editor(nullptr), m_renamed(0), m_search_editor(nullptr), m_filter_enabled(false) { 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); QPalette pal = QGraphicsView::palette(); const QPalette::ColorRole role = QPalette::Window; const QColor& color = pal.color(role); pal.setColor(role, color.darker(120)); QGraphicsView::setPalette(pal); QGraphicsView::setBackgroundRole(role); m_rename_editor = new QLineEdit(this); m_rename_editor->setFrame(false); m_rename_editor->setEnabled(false); m_rename_editor->hide(); QObject::connect(m_rename_editor, SIGNAL(textChanged(const QString&)), SLOT(renameTextChanged(const QString&))); QObject::connect(m_rename_editor, SIGNAL(editingFinished()), SLOT(renameEditingFinished())); QGraphicsView::grabGesture(Qt::PinchGesture); m_search_editor = new QLineEdit(this); m_search_editor->setClearButtonEnabled(true); m_search_editor->setEnabled(false); m_search_editor->hide(); QObject::connect(m_search_editor, SIGNAL(textChanged(const QString&)), SLOT(searchTextChanged(const QString&))); QObject::connect(m_search_editor, SIGNAL(editingFinished()), SLOT(searchEditingFinished())); } // Destructor. qpwgraph_canvas::~qpwgraph_canvas (void) { clear(); delete m_search_editor; delete m_rename_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); } } } bool qpwgraph_canvas::isPatchbayEmpty (void) const { return (m_patchbay ? m_patchbay->items().isEmpty() : 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)); } bool qpwgraph_canvas::canSearchItem (void) const { return !m_nodes.isEmpty(); } // 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_rename_editor->font(); font.setPointSizeF(scale * font.pointSizeF()); m_rename_editor->setFont(font); updateRenameEditor(); 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 { nodes.append(node); } } } foreach (qpwgraph_node *node, nodes) removeItem(node); qDeleteAll(nodes); } void qpwgraph_canvas::clearNodes ( uint node_type ) { QList nodes; foreach (qpwgraph_node *node, m_nodes) { if (node->nodeType() == node_type) nodes.append(node); } foreach (qpwgraph_node *node, nodes) { removeNodeKeys(node); m_nodes.removeAll(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_rename_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 notifier. void qpwgraph_canvas::emitRenamed ( qpwgraph_item *item, const QString& name ) { emit renamed(item, name); } // Other generic notifier. void qpwgraph_canvas::emitChanged (void) { emit changed(); } // 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))) { #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 = snapPos(m_pos); } 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) { pos = 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(snapPos(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, m_pos)); ++nchanged; } // 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(); m_search_editor->editingFinished(); clear(); emit changed(); } else if (!m_search_editor->isEnabled() && (event->modifiers() & Qt::ControlModifier) == 0 && !event->text().trimmed().isEmpty()) { startSearchEditor(event->text()); } } // 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_rename_editor->setPalette(pal); QFont font = m_rename_editor->font(); font.setBold(true); m_rename_editor->setFont(font); m_rename_editor->setPlaceholderText(node->nodeName()); m_rename_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_rename_editor->setPalette(pal); QFont font = m_rename_editor->font(); font.setBold(false); m_rename_editor->setFont(font); m_rename_editor->setPlaceholderText(port->portName()); m_rename_editor->setText(port->portTitle()); } } else return; m_selected_nodes = 0; m_scene->clearSelection(); m_rename_editor->show(); m_rename_editor->setEnabled(true); m_rename_editor->selectAll(); m_rename_editor->setFocus(); m_renamed = 0; m_rename_item = item; updateRenameEditor(); } void qpwgraph_canvas::searchItem (void) { if (!m_search_editor->isEnabled()) startSearchEditor(); } // Update editors position and size. void qpwgraph_canvas::updateRenameEditor (void) { if (m_rename_item && m_rename_editor->isEnabled() && m_rename_editor->isVisible()) { const QRectF& rect = m_rename_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_rename_editor->setGeometry( pos1.x(), pos1.y(), pos2.x() - pos1.x(), pos2.y() - pos1.y()); } } void qpwgraph_canvas::updateSearchEditor (void) { // Position the search editor to the bottom-right of the canvas... const QSize& size = QGraphicsView::viewport()->size(); const QSize& hint = m_search_editor->sizeHint(); const int w = hint.width() * 2; const int h = hint.height(); m_search_editor->setGeometry(size.width() - w, size.height() - h, w, h); } // 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 true; 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); qpwgraph_node *node = port->portNode(); if (node) node->updatePath(); 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_rename_item = nullptr; m_rename_editor->setEnabled(false); m_rename_editor->hide(); m_renamed = 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_rename_item = nullptr; m_rename_editor->setEnabled(false); m_rename_editor->hide(); m_renamed = 0; // Reset cursor... QGraphicsView::setCursor(Qt::ArrowCursor); } // Rename item slots. void qpwgraph_canvas::renameTextChanged ( const QString& /* text */) { if (m_rename_item && m_rename_editor->isEnabled() && m_rename_editor->isVisible()) ++m_renamed; } void qpwgraph_canvas::renameEditingFinished (void) { if (m_rename_item && m_rename_editor->isEnabled() && m_rename_editor->isVisible()) { // If changed then notify... if (m_renamed > 0) { m_commands->push( new qpwgraph_rename_command(this, m_rename_item, m_rename_editor->text())); } // Reset all renaming stuff... m_rename_item = nullptr; m_rename_editor->setEnabled(false); m_rename_editor->hide(); m_renamed = 0; } } // Search item slots. void qpwgraph_canvas::searchTextChanged ( const QString& text ) { clearSelection(); if (text.isEmpty()) return; const QRegularExpression& rx = QRegularExpression(text, QRegularExpression::CaseInsensitiveOption); if (!rx.isValid()) return; for (qpwgraph_node *node : m_nodes) { if (rx.match(node->nodeTitle()).hasMatch()) { node->setSelected(true); QGraphicsView::ensureVisible(node); } } } void qpwgraph_canvas::searchEditingFinished (void) { m_search_editor->setEnabled(false); m_search_editor->hide(); m_search_editor->clearFocus(); QGraphicsView::setFocus(); } // Start search editor... void qpwgraph_canvas::startSearchEditor ( const QString& text ) { m_search_editor->setEnabled(true); m_search_editor->setText(text); m_search_editor->raise(); m_search_editor->show(); m_search_editor->setFocus(); } void qpwgraph_canvas::resizeEvent( QResizeEvent *event ) { QGraphicsView::resizeEvent(event); updateSearchEditor(); } // 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. // QPointF qpwgraph_canvas::snapPos ( const QPointF& pos ) const { return QPointF( 4.0 * ::round(0.25 * pos.x()), 4.0 * ::round(0.25 * pos.y())); } #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 // Search placeholder text accessors. void qpwgraph_canvas::setSearchPlaceholderText ( const QString& text ) { m_search_editor->setPlaceholderText(text); } QString qpwgraph_canvas::searchPlaceholderText (void) const { return m_search_editor->placeholderText(); } // Filter/hide list management accessors. void qpwgraph_canvas::setFilterNodesEnabled ( bool enabled ) { m_filter_enabled = enabled; } bool qpwgraph_canvas::isFilterNodesEnabled (void) const { return m_filter_enabled; } void qpwgraph_canvas::setFilterNodesList ( const QStringList& nodes ) { m_filter_nodes = nodes; } const QStringList& qpwgraph_canvas::filterNodesList (void) const { return m_filter_nodes; } bool qpwgraph_canvas::isFilterNodes ( const QString& node_name ) const { if (!m_filter_enabled) return false; QStringListIterator iter(m_filter_nodes); while (iter.hasNext()) { const QString& node_pattern = iter.next(); const QRegularExpression& rx = QRegularExpression(node_pattern, QRegularExpression::CaseInsensitiveOption); if (rx.isValid()) { if (rx.match(node_name).hasMatch()) return true; } } return false; } // end of qpwgraph_canvas.cpp qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_options.cpp0000644000000000000000000000013214762602507017340 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_options.cpp0000644000175000001440000002147714762602507017343 0ustar00rncbcusers// qpwgraph_options.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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_options.h" #include "qpwgraph_main.h" #include "qpwgraph_config.h" #include #include #include #ifdef CONFIG_SYSTEM_TRAY #include #endif //---------------------------------------------------------------------------- // qpwgraph_options -- UI wrapper form. // Constructor. qpwgraph_options::qpwgraph_options ( qpwgraph_main *parent ) : QDialog(parent) { // Setup UI struct... m_ui.setupUi(this); // Initialize dirty control state. m_dirty = 0; m_dirty_filter = 0; // Setup current options... qpwgraph_config *config = parent->config(); if (config) { config->loadComboBoxHistory(m_ui.FilterNodesNameComboBox); #ifdef CONFIG_SYSTEM_TRAY m_ui.SystemTrayEnabledCheckBox->setChecked( config->isSystemTrayEnabled()); m_ui.SystemTrayQueryCloseCheckBox->setChecked( config->isSystemTrayQueryClose()); m_ui.SystemTrayStartMinimizedCheckBox->setChecked( config->isStartMinimized()); #endif m_ui.PatchbayQueryQuitCheckBox->setChecked( config->isPatchbayQueryQuit()); #ifdef CONFIG_ALSA_MIDI m_ui.AlsaMidiEnabledCheckBox->setChecked( config->isAlsaMidiEnabled()); #endif } #ifdef CONFIG_SYSTEM_TRAY if (!QSystemTrayIcon::isSystemTrayAvailable()) { m_ui.SystemTrayEnabledCheckBox->setEnabled(false); m_ui.SystemTrayQueryCloseCheckBox->setEnabled(false); m_ui.SystemTrayStartMinimizedCheckBox->setChecked(false); } #else m_ui.SystemTrayEnabledCheckBox->hide(); m_ui.SystemTrayQueryCloseCheckBox->hide(); m_ui.SystemTrayStartMinimizedCheckBox->hide(); #endif #ifndef CONFIG_ALSA_MIDI m_ui.AlsaMidiEnabledCheckBox->hide(); #endif // Filter/hide list management... // m_ui.FilterNodesEnabledCheckBox->setChecked( config->isFilterNodesEnabled()); m_ui.FilterNodesNameComboBox->lineEdit()->setClearButtonEnabled(true); m_ui.FilterNodesNameComboBox->lineEdit()->setPlaceholderText( m_ui.FilterNodesNameComboBox->toolTip()); m_ui.FilterNodesNameComboBox->setCurrentText(QString()); m_ui.FilterNodesListWidget->clear(); m_ui.FilterNodesListWidget->addItems( config->filterNodesList()); // Try to restore old window positioning. adjustSize(); // UI connections... #ifdef CONFIG_SYSTEM_TRAY QObject::connect(m_ui.SystemTrayEnabledCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); QObject::connect(m_ui.SystemTrayQueryCloseCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); QObject::connect(m_ui.SystemTrayStartMinimizedCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); #endif QObject::connect(m_ui.PatchbayQueryQuitCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); #ifdef CONFIG_ALSA_MIDI QObject::connect(m_ui.AlsaMidiEnabledCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); #endif QObject::connect(m_ui.FilterNodesEnabledCheckBox, SIGNAL(stateChanged(int)), SLOT(changedFilterNodes())); QObject::connect(m_ui.FilterNodesNameComboBox, SIGNAL(editTextChanged(const QString&)), SLOT(selectFilterNodes())); QObject::connect(m_ui.FilterNodesAddToolButton, SIGNAL(clicked()), SLOT(addFilterNodes())); QObject::connect(m_ui.FilterNodesListWidget, SIGNAL(itemSelectionChanged()), SLOT(selectFilterNodes())); QObject::connect(m_ui.FilterNodesRemoveToolButton, SIGNAL(clicked()), SLOT(removeFilterNodes())); QObject::connect(m_ui.FilterNodesClearToolButton, SIGNAL(clicked()), SLOT(clearFilterNodes())); QObject::connect(m_ui.DialogButtonBox, SIGNAL(accepted()), SLOT(accept())); QObject::connect(m_ui.DialogButtonBox, SIGNAL(rejected()), SLOT(reject())); // Ready? stabilize(); } // Destructor. qpwgraph_options::~qpwgraph_options (void) { } // Reject options (Cancel button slot). void qpwgraph_options::reject (void) { bool ret = true; // Check if there's any pending changes... if (m_dirty > 0) { switch (QMessageBox::warning(this, tr("Warning"), tr("Some options have been changed.") + "\n\n" + tr("Do you want to apply the changes?"), QMessageBox::Apply | QMessageBox::Discard | QMessageBox::Cancel)) { case QMessageBox::Apply: accept(); return; case QMessageBox::Discard: break; default: // Cancel. ret = false; } } if (ret) QDialog::reject(); } // Accept options (OK button slot). void qpwgraph_options::accept (void) { qpwgraph_config *config = nullptr; qpwgraph_main *parent = qobject_cast (parentWidget()); if (parent) config = parent->config(); if (config) { #ifdef CONFIG_SYSTEM_TRAY config->setSystemTrayEnabled( m_ui.SystemTrayEnabledCheckBox->isChecked()); config->setSystemTrayQueryClose( m_ui.SystemTrayQueryCloseCheckBox->isChecked()); config->setStartMinimized( m_ui.SystemTrayStartMinimizedCheckBox->isChecked()); #endif config->setPatchbayQueryQuit( m_ui.PatchbayQueryQuitCheckBox->isChecked()); #ifdef CONFIG_ALSA_MIDI config->setAlsaMidiEnabled( m_ui.AlsaMidiEnabledCheckBox->isChecked()); #endif if (m_dirty_filter > 0) { config->setFilterNodesEnabled( m_ui.FilterNodesEnabledCheckBox->isChecked()); QStringList nodes; const int n = m_ui.FilterNodesListWidget->count(); for (int i = 0; i < n; ++i) { QListWidgetItem *item = m_ui.FilterNodesListWidget->item(i); if (item) nodes.append(item->text()); } config->setFilterNodesList(nodes); config->setFilterNodesDirty(true); m_dirty_filter = 0; } config->saveComboBoxHistory(m_ui.FilterNodesNameComboBox); parent->updateOptions(); } QDialog::accept(); } // Dirty up options. void qpwgraph_options::changed (void) { ++m_dirty; stabilize(); } // Filter/hide list management... // void qpwgraph_options::changedFilterNodes (void) { ++m_dirty_filter; changed(); } void qpwgraph_options::selectFilterNodes (void) { stabilize(); } void qpwgraph_options::addFilterNodes (void) { const QString& node_name = m_ui.FilterNodesNameComboBox->currentText(); if (node_name.isEmpty()) return; m_ui.FilterNodesListWidget->addItem(node_name); m_ui.FilterNodesListWidget->setCurrentRow( m_ui.FilterNodesListWidget->count() - 1); const int i = m_ui.FilterNodesNameComboBox->findText(node_name); if (i >= 0) m_ui.FilterNodesNameComboBox->removeItem(i); m_ui.FilterNodesNameComboBox->insertItem(0, node_name); m_ui.FilterNodesNameComboBox->setEditText(QString()); m_ui.FilterNodesListWidget->setFocus(); changedFilterNodes(); } void qpwgraph_options::removeFilterNodes (void) { const int i = m_ui.FilterNodesListWidget->currentRow(); if (i < 0) return; QListWidgetItem *item = m_ui.FilterNodesListWidget->takeItem(i); if (item) delete item; changedFilterNodes(); } void qpwgraph_options::clearFilterNodes (void) { m_ui.FilterNodesListWidget->clear(); changedFilterNodes(); } // Stabilize current form state. void qpwgraph_options::stabilize (void) { #ifdef CONFIG_SYSTEM_TRAY const bool systray = m_ui.SystemTrayEnabledCheckBox->isChecked(); m_ui.SystemTrayQueryCloseCheckBox->setEnabled(systray); m_ui.SystemTrayStartMinimizedCheckBox->setEnabled(systray); #endif if (m_ui.FilterNodesEnabledCheckBox->isChecked()) { m_ui.FilterNodesNameComboBox->setEnabled(true); m_ui.FilterNodesListWidget->setEnabled(true); const QString& node_name = m_ui.FilterNodesNameComboBox->currentText(); m_ui.FilterNodesAddToolButton->setEnabled(!node_name.isEmpty() && m_ui.FilterNodesListWidget->findItems(node_name, Qt::MatchFixedString).isEmpty()); const int i = m_ui.FilterNodesListWidget->currentRow(); m_ui.FilterNodesRemoveToolButton->setEnabled(i >= 0); m_ui.FilterNodesClearToolButton->setEnabled( m_ui.FilterNodesListWidget->count() > 0); } else { m_ui.FilterNodesNameComboBox->setEnabled(false); m_ui.FilterNodesListWidget->setEnabled(false); m_ui.FilterNodesAddToolButton->setEnabled(false); m_ui.FilterNodesRemoveToolButton->setEnabled(false); m_ui.FilterNodesClearToolButton->setEnabled(false); } m_ui.DialogButtonBox->button(QDialogButtonBox::Ok)->setEnabled(m_dirty > 0); } // end of qpwgraph_options.cpp qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_config.h0000644000000000000000000000013214762602507016557 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_config.h0000644000175000001440000001145114762602507016551 0ustar00rncbcusers// qpwgraph_config.h // /**************************************************************************** Copyright (C) 2021-2024, 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; class QComboBox; //---------------------------------------------------------------------------- // 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 setThumbview(int thumbview); int thumbview() 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 setPatchbayQueryQuit(bool query_quit); bool isPatchbayQueryQuit() const; void setSystemTrayQueryClose(bool query_close); bool isSystemTrayQueryClose() const; void setSystemTrayEnabled(bool enabled); bool isSystemTrayEnabled() const; void setAlsaMidiEnabled(bool enabled); bool isAlsaMidiEnabled() const; void setFilterNodesEnabled(bool enabled); bool isFilterNodesEnabled() const; void setFilterNodesList(const QStringList& nodes); const QStringList& filterNodesList() const; void setFilterNodesDirty(bool dirty); bool isFilterNodesDirty() const; void setStartMinimized(bool start_minimized); bool isStartMinimized() const; void setSessionStartMinimized(bool start_minimized); bool isSessionStartMinimized() const; // Graph main-widget state methods. bool restoreState(QMainWindow *widget); bool saveState(QMainWindow *widget) const; // Combo box history persistence helpers. void loadComboBoxHistory(QComboBox *cbox, int nlimit = 8); void saveComboBoxHistory(QComboBox *cbox, int nlimit = 8); private: // Instance variables. QSettings *m_settings; bool m_owner; bool m_menubar; bool m_toolbar; bool m_statusbar; int m_thumbview; 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_patchbay_queryquit; bool m_systray_queryclose; bool m_systray_enabled; bool m_alsaseq_enabled; bool m_filter_enabled; QStringList m_filter_nodes; bool m_filter_dirty; bool m_start_minimized; }; #endif // __qpwgraph_config_h // end of qpwgraph_config.h qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_command.h0000644000000000000000000000013214762602507016730 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_command.h0000644000175000001440000001142514762602507016723 0ustar00rncbcusers// qpwgraph_command.h // /**************************************************************************** Copyright (C) 2021-2024, 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.8.2/src/PaxHeaders/qpwgraph_connect.h0000644000000000000000000000013214762602507016743 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_connect.h0000644000175000001440000000525414762602507016741 0ustar00rncbcusers// 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.8.2/src/PaxHeaders/qpwgraph_patchman.h0000644000000000000000000000013214762602507017105 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_patchman.h0000644000175000001440000000425014762602507017076 0ustar00rncbcusers// qpwgraph_patchman.h // /**************************************************************************** Copyright (C) 2021-2024, 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_patchman_h #define __qpwgraph_patchman_h #include "qpwgraph_patchbay.h" #include #include // Forward decls. class QPushButton; class QDialogButtonBox; //---------------------------------------------------------------------------- // qpwgraph_patchman -- main dialog decl. class qpwgraph_patchman : public QDialog { Q_OBJECT public: // Constructor. qpwgraph_patchman(QWidget *parent); // Destructor. ~qpwgraph_patchman(); // Patchbay accessors. void setPatchbay(qpwgraph_patchbay *patchbay); qpwgraph_patchbay *patchbay() const; // Patchbay view refresh. void refresh(); protected slots: void removeClicked(); void removeAllClicked(); void cleanupClicked(); void resetClicked(); void accept(); void reject(); void stabilize(); protected: // Forward decls. class MainWidget; class TreeWidget; class LineWidget; class ItemDelegate; private: // Instance members. qpwgraph_patchbay *m_patchbay; int m_dirty; MainWidget *m_main; QPushButton *m_remove_button; QPushButton *m_remove_all_button; QPushButton *m_cleanup_button; QPushButton *m_reset_button; QDialogButtonBox *m_button_box; }; #endif // __qpwgraph_patchman_h // end of qpwgraph_patchman.h qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_main.cpp0000644000000000000000000000013214762602507016571 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_main.cpp0000644000175000001440000014057414762602507016574 0ustar00rncbcusers// qpwgraph_main.cpp // /**************************************************************************** Copyright (C) 2021-2025, 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_main.h" #include "qpwgraph_config.h" #include "qpwgraph_pipewire.h" #include "qpwgraph_alsamidi.h" #include "qpwgraph_connect.h" #include "qpwgraph_patchbay.h" #include "qpwgraph_patchman.h" #include "qpwgraph_systray.h" #include "qpwgraph_thumb.h" #include #include "qpwgraph_options.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_main -- UI wrapper form. // Constructor. qpwgraph_main::qpwgraph_main ( 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(PROJECT_DOMAIN, PROJECT_NAME); 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; m_thumb = nullptr; m_thumb_update = 0; 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(changed())); // Some actions surely need those // shortcuts firmly attached... addAction(m_ui.viewMenubarAction); addAction(m_ui.editSearchItemAction); // 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.graphOptionsAction, SIGNAL(triggered(bool)), SLOT(graphOptions())); 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.patchbayManageAction, SIGNAL(triggered(bool)), SLOT(patchbayManage())); 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.editSearchItemAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(searchItem())); 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))); m_thumb_mode = new QActionGroup(this); m_thumb_mode->setExclusive(true); m_thumb_mode->addAction(m_ui.viewThumbviewTopLeftAction); m_thumb_mode->addAction(m_ui.viewThumbviewTopRightAction); m_thumb_mode->addAction(m_ui.viewThumbviewBottomLeftAction); m_thumb_mode->addAction(m_ui.viewThumbviewBottomRightAction); m_thumb_mode->addAction(m_ui.viewThumbviewNoneAction); m_ui.viewThumbviewTopLeftAction->setData(qpwgraph_thumb::TopLeft); m_ui.viewThumbviewTopRightAction->setData(qpwgraph_thumb::TopRight); m_ui.viewThumbviewBottomLeftAction->setData(qpwgraph_thumb::BottomLeft); m_ui.viewThumbviewBottomRightAction->setData(qpwgraph_thumb::BottomRight); m_ui.viewThumbviewNoneAction->setData(qpwgraph_thumb::None); QObject::connect(m_ui.viewThumbviewTopLeftAction, SIGNAL(triggered(bool)), SLOT(viewThumbviewAction())); QObject::connect(m_ui.viewThumbviewTopRightAction, SIGNAL(triggered(bool)), SLOT(viewThumbviewAction())); QObject::connect(m_ui.viewThumbviewBottomLeftAction, SIGNAL(triggered(bool)), SLOT(viewThumbviewAction())); QObject::connect(m_ui.viewThumbviewBottomRightAction, SIGNAL(triggered(bool)), SLOT(viewThumbviewAction())); QObject::connect(m_ui.viewThumbviewNoneAction, SIGNAL(triggered(bool)), SLOT(viewThumbviewAction())); 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())); 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))); m_ui.graphCanvas->setSearchPlaceholderText( m_ui.editSearchItemAction->statusTip() + QString(3, '.')); restoreState(); // 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()! } updateViewColors(); updatePatchbayMenu(); updatePatchbayNames(); updateOptions(); // 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_main::~qpwgraph_main (void) { if (m_thumb) delete m_thumb; delete m_thumb_mode; #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; } // Configuration accessor. qpwgraph_config *qpwgraph_main::config (void) const { return m_config; } // Take care of command line options and arguments... void qpwgraph_main::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()) m_patchbay_path = app->patchbayPath(); bool start_minimized = app->isStartMinimized(); if (!start_minimized) start_minimized = m_config->isStartMinimized(); if (!start_minimized) start_minimized = app->isSessionRestored(); if (start_minimized) { #ifdef CONFIG_SYSTEM_TRAY if (m_systray) { hide(); m_systray_closed = true; } else { showMinimized(); } #else showMinimized(); #endif } else { show(); } } // Update configure options. void qpwgraph_main::updateOptions (void) { #ifdef CONFIG_SYSTEM_TRAY const bool systray_enabled = m_config->isSystemTrayEnabled(); if (systray_enabled && m_systray == nullptr) { m_systray = new qpwgraph_systray(this); m_systray->updateContextMenu(); m_systray->show(); m_systray_closed = false; QObject::connect(m_systray, SIGNAL(patchbayPresetChanged(int)), SLOT(patchbayNameChanged(int))); } else if (!systray_enabled && m_systray) { m_systray->hide(); delete m_systray; m_systray = nullptr; } #endif #ifdef CONFIG_ALSA_MIDI const bool alsamidi_enabled = m_config->isAlsaMidiEnabled(); if (alsamidi_enabled && 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 (!alsamidi_enabled && m_alsamidi) { m_alsamidi->clearItems(); QObject::disconnect( m_alsamidi, SIGNAL(changed()), this, SLOT(alsa_changed())); delete m_alsamidi; m_alsamidi = nullptr; } #endif m_ui.graphCanvas->setFilterNodesEnabled(m_config->isFilterNodesEnabled()); m_ui.graphCanvas->setFilterNodesList(m_config->filterNodesList()); if (m_config->isFilterNodesDirty()) { m_config->setFilterNodesDirty(false); viewRefresh(); } stabilize(); } // Current selected patchbay path accessor. const QString& qpwgraph_main::patchbayPath (void) const { return m_patchbay_path; } // Patchbay menu slots. void qpwgraph_main::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_main::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_main::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_main::patchbaySave (void) { if (m_patchbay_path.isEmpty()) { patchbaySaveAs(); return; } patchbaySaveFile(m_patchbay_path); updatePatchbayNames(); } void qpwgraph_main::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_main::patchbayActivated ( bool on ) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) { patchbay->setActivated(on); patchbay->scan(); } stabilize(); } void qpwgraph_main::patchbayExclusive ( bool on ) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) { patchbay->setExclusive(on); if (patchbay->isActivated()) patchbay->scan(); } stabilize(); } void qpwgraph_main::patchbayEdit ( bool on ) { m_ui.graphCanvas->setPatchbayEdit(on); stabilize(); } void qpwgraph_main::patchbayPin (void) { m_ui.graphCanvas->patchbayPin(); stabilize(); } void qpwgraph_main::patchbayUnpin (void) { m_ui.graphCanvas->patchbayUnpin(); stabilize(); } void qpwgraph_main::patchbayAutoPin ( bool on ) { m_ui.graphCanvas->setPatchbayAutoPin(on); stabilize(); } void qpwgraph_main::patchbayAutoDisconnect ( bool on ) { m_ui.graphCanvas->setPatchbayAutoDisconnect(on); stabilize(); } void qpwgraph_main::patchbayManage (void) { qpwgraph_patchman patchman(this); patchman.setPatchbay(m_ui.graphCanvas->patchbay()); patchman.exec(); } // Main menu slots. void qpwgraph_main::viewMenubar ( bool on ) { m_ui.MenuBar->setVisible(on); ++m_thumb_update; } void qpwgraph_main::viewGraphToolbar ( bool on ) { m_ui.graphToolbar->setVisible(on); ++m_thumb_update; } void qpwgraph_main::viewPatchbayToolbar ( bool on ) { m_ui.patchbayToolbar->setVisible(on); ++m_thumb_update; } void qpwgraph_main::viewStatusbar ( bool on ) { m_ui.StatusBar->setVisible(on); ++m_thumb_update; } void qpwgraph_main::viewThumbviewAction (void) { QAction *action = qobject_cast (sender()); if (action) viewThumbview(action->data().toInt()); } void qpwgraph_main::viewThumbview ( int thumbview ) { const qpwgraph_thumb::Position position = qpwgraph_thumb::Position(thumbview); if (position == qpwgraph_thumb::None) { if (m_thumb) { m_thumb->hide(); delete m_thumb; m_thumb = nullptr; m_thumb_update = 0; } } else { if (m_thumb) { m_thumb->setPosition(position); } else { m_thumb = new qpwgraph_thumb(m_ui.graphCanvas, position); QObject::connect(m_thumb, SIGNAL(contextMenuRequested(const QPoint&)), SLOT(thumbviewContextMenu(const QPoint&)), Qt::QueuedConnection); QObject::connect(m_thumb, SIGNAL(positionRequested(int)), SLOT(viewThumbview(int)), Qt::QueuedConnection); ++m_thumb_update; } } switch (position) { case qpwgraph_thumb::TopLeft: m_ui.viewThumbviewTopLeftAction->setChecked(true); break; case qpwgraph_thumb::TopRight: m_ui.viewThumbviewTopRightAction->setChecked(true); break; case qpwgraph_thumb::BottomLeft: m_ui.viewThumbviewBottomLeftAction->setChecked(true); break; case qpwgraph_thumb::BottomRight: m_ui.viewThumbviewBottomRightAction->setChecked(true); break; case qpwgraph_thumb::None: default: m_ui.viewThumbviewNoneAction->setChecked(true); break; } } void qpwgraph_main::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); } ++m_thumb_update; } void qpwgraph_main::viewCenter (void) { const QRectF& scene_rect = m_ui.graphCanvas->scene()->itemsBoundingRect(); m_ui.graphCanvas->centerOn(scene_rect.center()); stabilize(); } void qpwgraph_main::viewRefresh (void) { if (m_pipewire) m_pipewire->clearItems(); #ifdef CONFIG_ALSA_MIDI if (m_alsamidi) m_alsamidi->clearItems(); #endif pipewire_changed(); alsamidi_changed(); if (m_ui.graphCanvas->isRepelOverlappingNodes()) ++m_repel_overlapping_nodes; // fake nodes added! ++m_thumb_update; refresh(); } void qpwgraph_main::viewZoomRange ( bool on ) { m_ui.graphCanvas->setZoomRange(on); } void qpwgraph_main::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_main::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_main::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_main::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_main::viewRepelOverlappingNodes ( bool on ) { m_ui.graphCanvas->setRepelOverlappingNodes(on); if (on) ++m_repel_overlapping_nodes; } void qpwgraph_main::viewConnectThroughNodes ( bool on ) { qpwgraph_connect::setConnectThroughNodes(on); m_ui.graphCanvas->updateConnects(); } void qpwgraph_main::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 = PROJECT_COPYRIGHT; 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"; text += "

\n"; QMessageBox::about(this, tr("About") + ' ' + title, text); } void qpwgraph_main::helpAboutQt (void) { QMessageBox::aboutQt(this); } void qpwgraph_main::thumbviewContextMenu ( const QPoint& pos ) { stabilize(); QMenu menu(this); menu.addMenu(m_ui.viewThumbviewMenu); menu.addSeparator(); menu.addAction(m_ui.viewCenterAction); menu.addMenu(m_ui.viewZoomMenu); menu.exec(pos); stabilize(); } void qpwgraph_main::zoomValueChanged ( int zoom_value ) { m_ui.graphCanvas->setZoom(0.01 * qreal(zoom_value)); } void qpwgraph_main::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_main::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(QPointF(x, y))); updated(node); } void qpwgraph_main::updated ( qpwgraph_node */*node*/ ) { if (m_ui.graphCanvas->isRepelOverlappingNodes()) ++m_repel_overlapping_nodes; } void qpwgraph_main::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_main::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_main::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_main::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_main::renamed ( qpwgraph_item *item, const QString& name ) { qpwgraph_sect *sect = item_sect(item); if (sect) sect->renameItem(item, name); } // Graph view change slot. void qpwgraph_main::changed (void) { ++m_thumb_update; stabilize(); } // Graph section slots. void qpwgraph_main::pipewire_changed (void) { ++m_pipewire_changed; } void qpwgraph_main::alsamidi_changed (void) { ++m_alsamidi_changed; } // Pseudo-asyncronous timed refreshner. void qpwgraph_main::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(); ++nchanged; } if (m_thumb_update > 0 || nchanged > 0) { m_thumb_update = 0; if (m_thumb) m_thumb->updateView(); } QTimer::singleShot(300, this, SLOT(refresh())); } // Graph selection change slot. void qpwgraph_main::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.patchbayManageAction->setEnabled(!canvas->isPatchbayEmpty()); 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()); m_ui.editSearchItemAction->setEnabled( canvas->canSearchItem()); #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_main::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); } } // Options/settings dialog accessor. void qpwgraph_main::graphOptions (void) { qpwgraph_options(this).exec(); } // Open/save patchbay file. bool qpwgraph_main::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_main::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_main::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_main::patchbayFileDir (void) const { if (m_patchbay_path.isEmpty()) return m_patchbay_dir; else return m_patchbay_path; } QString qpwgraph_main::patchbayFileExt (void) const { return QString(PROJECT_NAME).toLower(); } QString qpwgraph_main::patchbayFileFilter (void) const { return tr("Patchbay files (*.%1)").arg(patchbayFileExt()) + ";;" + tr("All files (*.*)"); } // Whether we can close/quit current patchbay. bool qpwgraph_main::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_main::patchbayQueryQuit (void) { if (!patchbayQueryClose()) return false; bool ret = true; const qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay && patchbay->isActivated()) { showNormal(); if (m_config->isPatchbayQueryQuit()) { const QString& title = tr("Warning"); const QString& text = tr("A patchbay is currently activated:\n\n\"%1\"\n\n" "Are you sure you want to quit?").arg(patchbayFileName()); #if 0// Old no dont-ask-again message-box... ret = (QMessageBox::warning(this, title, text, QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok); #else QMessageBox mbox(this); mbox.setIcon(QMessageBox::Warning); mbox.setWindowTitle(title); mbox.setText(text); mbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); QCheckBox cbox(tr("Don't ask this again")); cbox.setChecked(false); cbox.blockSignals(true); mbox.addButton(&cbox, QMessageBox::ActionRole); ret = (mbox.exec() == QMessageBox::Ok); if (ret && cbox.isChecked()) { m_config->setPatchbayQueryQuit(false); } #endif } } return ret; } // Context-menu event handler. void qpwgraph_main::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_main::resizeEvent ( QResizeEvent *event ) { QMainWindow::resizeEvent(event); if (m_thumb) { m_thumb_update = 0; m_thumb->updateView(); } stabilize(); } // Widget event handlers. void qpwgraph_main::showEvent ( QShowEvent *event ) { ++m_thumb_update; QMainWindow::showEvent(event); #ifdef CONFIG_SYSTEM_TRAY if (m_systray) m_systray->updateContextMenu(); #endif } void qpwgraph_main::hideEvent ( QHideEvent *event ) { QMainWindow::hideEvent(event); #ifdef CONFIG_SYSTEM_TRAY if (m_systray) m_systray->updateContextMenu(); #endif saveState(); } void qpwgraph_main::closeEvent ( QCloseEvent *event ) { #ifdef CONFIG_SYSTEM_TRAY if (m_systray) { if (!m_systray_closed && m_config->isSystemTrayQueryClose()) { 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 context menu of the system tray icon."); #if 0//--Old no dont-ask-again message-box... if (QSystemTrayIcon::supportsMessages()) m_systray->showMessage(title, text, QSystemTrayIcon::Information); else QMessageBox::information(this, title, text); #else QMessageBox mbox(this); mbox.setIcon(QMessageBox::Information); mbox.setWindowTitle(title); mbox.setText(text); mbox.setStandardButtons(QMessageBox::Ok|QMessageBox::Cancel); QCheckBox cbox(tr("Don't show this message again")); cbox.setChecked(false); cbox.blockSignals(true); mbox.addButton(&cbox, QMessageBox::ActionRole); m_systray_closed = (mbox.exec() == QMessageBox::Ok); if (cbox.isChecked()) { m_config->setSystemTrayQueryClose(false); } #endif } if (m_systray_closed || !m_config->isSystemTrayQueryClose()) hide(); event->ignore(); } else #endif if (patchbayQueryQuit()) { hide(); QMainWindow::closeEvent(event); } else { event->ignore(); } } // Special port-type color methods. void qpwgraph_main::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_main::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_main::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_main::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_main::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_main::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()); viewThumbview(m_config->thumbview()); 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(); } // Forcibly save whole form state. void qpwgraph_main::saveState (void) { m_ui.graphCanvas->saveState(); m_config->setThumbview(m_thumb ? m_thumb->position() : qpwgraph_thumb::None); 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); m_config->saveState(this); } // Forcibly quit application. void qpwgraph_main::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_main::commitData ( QSessionManager& sm ) { sm.release(); m_config->setSessionStartMinimized(!isVisible() && !isMinimized()); } // end of qpwgraph_main.cpp qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_thumb.h0000644000000000000000000000013214762602507016431 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_thumb.h0000644000175000001440000000426114762602507016424 0ustar00rncbcusers// qpwgraph_thumb.h // /**************************************************************************** Copyright (C) 2018-2024, 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_thumb_h #define __qpwgraph_thumb_h #include // Forward decls. class qpwgraph_canvas; //---------------------------------------------------------------------------- // qpwgraph_thumb -- Thumb graphics scene/view. class qpwgraph_thumb : public QFrame { Q_OBJECT public: // Corner position. enum Position { None = 0, TopLeft, TopRight, BottomLeft, BottomRight }; // Constructor. qpwgraph_thumb(qpwgraph_canvas *canvas, Position position = BottomLeft); // Destructor. ~qpwgraph_thumb(); // Accessors. qpwgraph_canvas *canvas() const; void setPosition(Position position); Position position() const; // Emit context-menu request. void requestContextMenu(const QPoint& pos); // Request re-positioning. void requestPosition(Position position); signals: // Context-menu request. void contextMenuRequested(const QPoint& pos); // Re-positioning request. void positionRequested(int position); public slots: // Update view slot. void updateView(); protected: // Update position. void updatePosition(); // Forward decl. class View; private: // Inatance members. qpwgraph_canvas *m_canvas; Position m_position; View *m_view; }; #endif // __qpwgraph_thumb_h // end of qpwgraph_thumb.h qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_thumb.cpp0000644000000000000000000000013214762602507016764 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_thumb.cpp0000644000175000001440000002047614762602507016765 0ustar00rncbcusers// qpwgraph_thumb.cpp // /**************************************************************************** Copyright (C) 2018-2024, 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_thumb.h" #include "qpwgraph_canvas.h" #include #include #include #include #include //---------------------------------------------------------------------------- // qpwgraph_thumb::View -- Thumb graphics scene/view. class qpwgraph_thumb::View : public QGraphicsView { public: // Constructor. View(qpwgraph_thumb *thumb) : QGraphicsView(thumb->canvas()->viewport()), m_thumb(thumb), m_drag_state(DragNone) { QGraphicsView::setInteractive(false); QGraphicsView::setRenderHints(QPainter::Antialiasing); QGraphicsView::setRenderHint(QPainter::SmoothPixmapTransform); QGraphicsView::setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); QGraphicsView::setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); qpwgraph_canvas *canvas = m_thumb->canvas(); QPalette pal = canvas->palette(); const QPalette::ColorRole role = canvas->backgroundRole(); const QColor& color = pal.color(role); pal.setColor(role, color.darker(120)); QGraphicsView::setPalette(pal); QGraphicsView::setBackgroundRole(role); QGraphicsView::setScene(canvas->scene()); } protected: // Compute the view(port) rectangle. QRect viewRect() const { qpwgraph_canvas *canvas = m_thumb->canvas(); const QRect& vrect = canvas->viewport()->rect(); const QRectF srect( canvas->mapToScene(vrect.topLeft()), canvas->mapToScene(vrect.bottomRight())); return QGraphicsView::viewport()->rect().intersected(QRect( QGraphicsView::mapFromScene(srect.topLeft()), QGraphicsView::mapFromScene(srect.bottomRight()))) .adjusted(0, 0, -1, -1); } // View paint method. void paintEvent(QPaintEvent *event) { QGraphicsView::paintEvent(event); QPainter painter(QGraphicsView::viewport()); // const QPalette& pal = QGraphicsView::palette(); // painter.setPen(pal.midlight().color()); const QRect& vrect = QGraphicsView::viewport()->rect(); const QRect& vrect2 = viewRect(); const QColor shade(0, 0, 0, 64); QRect rect; // top shade... rect.setTopLeft(vrect.topLeft()); rect.setBottomRight(QPoint(vrect.right(), vrect2.top() - 1)); if (rect.isValid()) painter.fillRect(rect, shade); // left shade... rect.setTopLeft(QPoint(vrect.left(), vrect2.top())); rect.setBottomRight(vrect2.bottomLeft()); if (rect.isValid()) painter.fillRect(rect, shade); // right shade... rect.setTopLeft(vrect2.topRight()); rect.setBottomRight(QPoint(vrect.right(), vrect2.bottom())); if (rect.isValid()) painter.fillRect(rect, shade); // bottom shade... rect.setTopLeft(QPoint(vrect.left(), vrect2.bottom() + 1)); rect.setBottomRight(vrect.bottomRight()); if (rect.isValid()) painter.fillRect(rect, shade); } // Handle mouse events. // void mousePressEvent(QMouseEvent *event) { QGraphicsView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { m_drag_pos = event->pos(); m_drag_state = DragStart; QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor)); } } void mouseMoveEvent(QMouseEvent *event) { QGraphicsView::mouseMoveEvent(event); if (m_drag_state == DragStart && (event->pos() - m_drag_pos).manhattanLength() > QApplication::startDragDistance()) { m_drag_state = DragMove; QApplication::changeOverrideCursor(QCursor(Qt::DragMoveCursor)); } if (m_drag_state == DragMove) { const QRect& rect = QGraphicsView::rect(); if (!rect.contains(event->pos())) { const int mx = rect.width() + 4; const int my = rect.height() + 4; const Position position = m_thumb->position(); if (event->pos().x() < rect.left() - mx) { if (position == TopRight) m_thumb->requestPosition(TopLeft); else if (position == BottomRight) m_thumb->requestPosition(BottomLeft); } else if (event->pos().x() > rect.right() + mx) { if (position == TopLeft) m_thumb->requestPosition(TopRight); else if (position == BottomLeft) m_thumb->requestPosition(BottomRight); } else if (event->pos().y() < rect.top() - my) { if (position == BottomLeft) m_thumb->requestPosition(TopLeft); else if (position == BottomRight) m_thumb->requestPosition(TopRight); } else if (event->pos().y() > rect.bottom() + my) { if (position == TopLeft) m_thumb->requestPosition(BottomLeft); else if (position == TopRight) m_thumb->requestPosition(BottomRight); } } else if (event->modifiers() & Qt::ControlModifier) { m_thumb->canvas()->centerOn( QGraphicsView::mapToScene(event->pos())); } } } void mouseReleaseEvent(QMouseEvent *event) { QGraphicsView::mouseReleaseEvent(event); if (m_drag_state != DragNone) { if ((m_drag_state == DragStart) || (event->modifiers() & Qt::ControlModifier)) { m_thumb->canvas()->centerOn( QGraphicsView::mapToScene(event->pos())); } m_drag_state = DragNone; QApplication::restoreOverrideCursor(); } } void wheelEvent(QWheelEvent *) {} // Ignore wheel events. void contextMenuEvent(QContextMenuEvent *event) { m_thumb->requestContextMenu(event->globalPos()); } private: // Instance members. qpwgraph_thumb *m_thumb; enum { DragNone = 0, DragStart, DragMove } m_drag_state; QPoint m_drag_pos; }; //---------------------------------------------------------------------------- // qpwgraph_thumb -- Thumb graphics scene/view. // Constructor. qpwgraph_thumb::qpwgraph_thumb ( qpwgraph_canvas *canvas, Position position ) : QFrame(canvas), m_canvas(canvas), m_position(position), m_view(nullptr) { m_view = new View(this); QVBoxLayout *layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_view); QFrame::setLayout(layout); QFrame::setFrameStyle(QFrame::Panel); QFrame::setForegroundRole(QPalette::Window); QObject::connect(m_canvas->horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(updateView())); QObject::connect(m_canvas->verticalScrollBar(), SIGNAL(valueChanged(int)), SLOT(updateView())); } // Destructor. qpwgraph_thumb::~qpwgraph_thumb (void) { } // Accessors. qpwgraph_canvas *qpwgraph_thumb::canvas (void) const { return m_canvas; } void qpwgraph_thumb::setPosition ( Position position ) { m_position = position; updatePosition(); } qpwgraph_thumb::Position qpwgraph_thumb::position (void) const { return m_position; } // Request re-positioning. void qpwgraph_thumb::requestPosition ( Position position ) { emit positionRequested(int(position)); } // Emit context-menu request. void qpwgraph_thumb::requestContextMenu ( const QPoint& pos ) { emit contextMenuRequested(pos); } // Update view. void qpwgraph_thumb::updatePosition (void) { const QRect& rect = m_canvas->viewport()->rect(); const int w = rect.width() / 4; const int h = rect.height() / 4; QFrame:setFixedSize(w + 1, h + 1); // 1px slack. switch (m_position) { case TopLeft: QFrame::move(0, 0); break; case TopRight: QFrame::move(rect.width() - w, 0); break; case BottomLeft: QFrame::move(0, rect.height() - h); break; case BottomRight: QFrame::move(rect.width() - w, rect.height() - h); break; case None: default: break; } QFrame::show(); } // Update view. void qpwgraph_thumb::updateView (void) { updatePosition(); const qreal m = 24.0; m_view->fitInView( m_canvas->scene()->itemsBoundingRect() .marginsAdded(QMarginsF(m, m, m, m)), Qt::KeepAspectRatio); } // end of qpwgraph_thumb.cpp qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_options.h0000644000000000000000000000013214762602507017005 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_options.h0000644000175000001440000000345414762602507017003 0ustar00rncbcusers// qpwgraph_options.h // /**************************************************************************** Copyright (C) 2021-2024, 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_options_h #define __qpwgraph_options_h #include "ui_qpwgraph_options.h" // Forward decls. class qpwgraph_main; //---------------------------------------------------------------------------- // qpwgraph_options -- UI wrapper form. class qpwgraph_options : public QDialog { Q_OBJECT public: // Constructor. qpwgraph_options(qpwgraph_main *parent); // Destructor. ~qpwgraph_options(); protected slots: void changed(); void accept(); void reject(); // Filter/hide list management slots. void selectFilterNodes(); void addFilterNodes(); void removeFilterNodes(); void clearFilterNodes(); void changedFilterNodes(); protected: void stabilize(); private: // The Qt-designer UI struct... Ui::qpwgraph_options m_ui; int m_dirty; int m_dirty_filter; }; #endif // __qpwgraph_options_h // end of qpwgraph_options.h qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_main.h0000644000000000000000000000013214762602507016236 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_main.h0000644000175000001440000001362214762602507016232 0ustar00rncbcusers// qpwgraph_main.h // /**************************************************************************** Copyright (C) 2021-2025, 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_main_h #define __qpwgraph_main_h #include "ui_qpwgraph_main.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 qpwgraph_thumb; class QResizeEvent; class QCloseEvent; class QSlider; class QSpinBox; class QComboBox; class QActionGroup; class QSessionManager; //---------------------------------------------------------------------------- // qpwgraph_main -- UI wrapper form. class qpwgraph_main : public QMainWindow { Q_OBJECT public: // Constructor. qpwgraph_main(QWidget *parent = nullptr, Qt::WindowFlags wflags = Qt::WindowFlags()); // Destructor. ~qpwgraph_main(); // Configuration accessor. qpwgraph_config *config() const; // Take care of command line options and arguments... void apply_args(qpwgraph_application *app); // Update configure options. void updateOptions(); // Current selected patchbay path accessor. const QString& patchbayPath() const; 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 view change slot. void changed(); // 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); // Options/settings dialog accessor. void graphOptions(); // 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); void patchbayManage(); // Main menu slots. void viewMenubar(bool on); void viewGraphToolbar(bool on); void viewPatchbayToolbar(bool on); void viewStatusbar(bool on); void viewThumbviewAction(); void viewThumbview(int thumbview); 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 helpAbout(); void helpAboutQt(); void thumbviewContextMenu(const QPoint& pos); 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_main 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; QActionGroup *m_thumb_mode; qpwgraph_thumb *m_thumb; int m_thumb_update; }; #endif // __qpwgraph_main_h // end of qpwgraph_main.h qpwgraph-0.8.2/src/PaxHeaders/man10000644000000000000000000000013214762602507014007 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/man1/0000755000175000001440000000000014762602507014054 5ustar00rncbcusersqpwgraph-0.8.2/src/man1/PaxHeaders/qpwgraph.10000644000000000000000000000013214762602507015777 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/man1/qpwgraph.10000644000175000001440000000170514762602507015772 0ustar00rncbcusers.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.8.2/src/PaxHeaders/images0000644000000000000000000000013114762602507014417 xustar0030 mtime=1741358407.417265981 29 atime=1741358407.41690441 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/0000755000175000001440000000000014762602507014465 5ustar00rncbcusersqpwgraph-0.8.2/src/images/PaxHeaders/itemExclusive.png0000644000000000000000000000013214762602507020031 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemExclusive.png0000644000175000001440000000245514762602507020027 0ustar00rncbcusersPNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8umLSW8Qhyɜ^h%heDȈX c񋋉ee 7;T`F$c`+{>p ܜ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("ђ*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.8.2/src/images/PaxHeaders/viewZoomIn.png0000644000000000000000000000013214762602507017311 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewZoomIn.png0000644000175000001440000000260214762602507017301 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/itemConnect.png0000644000000000000000000000013214762602507017453 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemConnect.png0000644000175000001440000000164114762602507017445 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/itemPulse.png0000644000000000000000000000013214762602507017152 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemPulse.png0000644000175000001440000000263214762602507017145 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/qpwgraph_screenshot-4.png0000644000000000000000000000013214762602507021432 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.416986867 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/qpwgraph_screenshot-4.png0000644000175000001440000035445614762602507021443 0ustar00rncbcusersPNG  IHDR5 IDATxYlٙ{.ݸ]RDE{Δ0v FQ0<UkۙY̔S.BI}6dD^RD)N) ^ĉ/wQAA>O8{6AASmAՊ""PA`W<333~em㨎(H5  (BX|`X ivLӴ4i|gzBǴ, UUQg렴, EQד`kZUOZų6  |QUJ"U* Zm%jx'r8_yp``grTBoR8dii+f||wdYO`G?btt9FFF)Ǚ֭[ܻwɧ.*wΝ;NAAx1 # $|އq~??u;>ccckB!E7ߤT*1550 \tiyw) *ؙ3g8r/hmmehheTU?w%qy @Q&''YZZP(:a6r9VVVH$[V۷I$=z]9vA0m?t:;;~GUUpRDuu5pY\t:M&!P]]- :_8t>+++ß~R}m۶)ʇ~ m uy#+>}0 oZ4,²m4xqWJss3̿~]v=L}}}|_e޽J4Ç{۬dĺAAq4 ߚ!`z+78ݻwI$twwF!¾}(,--QSSþ}D" \(!( d2Ccc#vbtt:8uuu0HPtwwdؽ{7MMMXEmm-{ٶb `jjI B3LH$|n;l&UUUގ'JKK g׮]=L@H$ڵjD"$I(w! rqt]+AalvrH)&Mq\w* ަ-hhhx:nСg65No}m[8ܸqÛPE8~ߚgvv? 7!sܸq'Nu %sss>0 s@ݻw/" AAx`___ Q`ȸpCp8T0AA! = AA^`6@wr  8=*a&Bp8\/8aP,qO0Ԗ()$`̗y}O\FUUB>>5p)b6``0maۥ+3hth4 #; topq( r9t]'9Ygqr\|X,J:8m;󐨳m1д;PSSKsЕer9?%O{{s+Z-=,\vrL($R*饹|EWUS,]CB벸KKKʱcǘgaa\.iB!lĉ2%|xMN|7i4M|nn\.47n N8tI2o~a*m?N?2aiǥRi۶(}\w211{gfmcC055$\+WpE2 L[)yj/_-811 20piS(apEt \ŋ|~.\sy]t Ocbb댌Ӹt333/sJ%X]]e׮]?1 Q!Ξ=aAxXy3l,Jyܹs4{X<^{'M|M:;;#p "[t[|>=͹lV\K.hjj"L7xs4[Mec޾}oaX:Ƴe133㕷e9jkS*PU=ѕ6???Ͻ{#,--g,/^ Ӷm,BQG֑mNoaYR)Wr s;{)ضǎ_vD"իWimmVw, ۶QU!/lj'M>&O<࠷d'=fff! =("p5?,b6Dk׮L8iy!pjLɓ,..k|:tyvaBי~ya%???>$ʩW.4Mjjj O4ֆfgg)˘^| pƕK)i@r=m3;;K8Mu1AQgϞ=躎(brErm݆-[fHPN*"W\ĉ222"zC}+++?"R χidY~_s|>OXDQ} 555;mFT*1>>(H: `zzbDmm uu122{!ˡ( f={sm6B{&9~C6SU[155 T@ @&e޽\~1o{ S~,Q*,kC"Ooٻw/e) \xwe8mJCC555IWWbBHGG^Ejw-,"cuqw?OO0Mwww,˦l6KPѣ,..Ndhh1s!TUݶ̅Brae2 ǽu"@kcmB7l灭"`YhF0ٳ:ues֭[455122 op:;;hii֭[,..F/|w+p9TU%H0;;믿ƨP(J__!p]=qiA(Z_*RSSٜm︷`׮]LOP[[˭[kBܹs|K_b~~l6ӧqQ0McǎӧO3;;KX>'ž={*sM"sVVVbbMٳ:׾[oE,c~~ÇslW^?1Ǐ9rp8+u⦧y뭷줣/>\{q5=r!xG444(xp Ν;޽=ꫯ288י{?|>q~4MT*399IT" X\\%ǎܹs|P(D__/_:::>td22wmmm Y^^&N+lY6a`6rnVu=ʅ x7?%~ijjbrr}1<OOOKKKR'zWwGMZG~^>|;|>~_ ҥK033ݻytF6\0Bq4?ȑ#$ ޽K2g??'z-jjjhnn^OnY0MӋaжM${j{F,׳΢,`0e4{nY]]Ŷ-~| 򆉷^Lz{.of||a6S尐;}' O;Cmmר=i=a0 4)˼P.IRz[ 755TWWc6>|)gQUUEPnTU}*PBȞ=0O(T*ӟ7xq>}IFGG9z(Nbvv^^{59z(ccc=D/o>/wp8'N׷zxBfll/|98vofzz'NǑ#G룫gpp42߻wE?G<77ɓ'F ^{5吝 38P _իض⵽.|>pFT*04Muuu1B s>|\.??xe\pwt:y;捎$ }{brr+*tf}TqL&C6pHdóн.a !P__׾5>C7{166Fkkmb~/ kONNnX ؍1r277 ;w~}oW[ۛ˱q޽K]]^0 ܹMA[YʲYVo:p4؍myrLNNR]]MSSwlYYYʉ A.An i&'\`CN7_R=7Ot2ԍrv![y=ěoIgg'|۶b=zF+ɽnknݘViz۸sùsɋmkkVvvrl\q͓@IM; b1/cyy:#gd2coaow`0ȫ $InܸAooлw[ե[?_Ռ|>/_&voJ[i] O[9s^kK|=m~l6_ /8˿ a}g“>6{iwq "pG7G,ˢK.Q__Ϸ-=[^^իy/٧u[={ gsss|\r pbbbQg>y6߼O<I)鳝Wͱw޽z? {d2ܺud2IkksA8_!\/g~~za, 9x HsqV^w"ib1{&Nπ.H$ oyݻw[8>>X,6AA}l  _6@7  8=AOu/AA]kG0zKn  /)K  @hF<v[ T*FAA^ ,zzH&rAA)AAD  "AA  @AAA   PAA(  AAD  "AA  @AAA  @AAA  w IDAT PAA(  AAD  "AAT |m/_/P~ @Ax q)l0 ybq W3;_EQPq' t:M[[uuueY8h{,8EAUU.]D0$ ! R@\&8'\X9TZ.f0in+w9)H&Rт P_h6'OdxxH$޽{󌏏ƾ}Pex Ο?  266F6%L2==Muu5Bp8̡Cu]ihh`Ϟ=,--/A?޽{筷_*uuuLLLoͿ0 )A^ujjjP]v穩AUU8b.~Cysx_NXETb`` ryFFF}}}(Q__Ϯ]hjjz(s!PD sK vC'}zp|zE҆ǘ(* <@@^B\c``D"a)J$H:&l9\ "m?Iw#9 ?|9iX^ub[a’楳{o߾Moo/zhu 6t@*%o/ ; P^L^*- v(8KK8@31M@^2~],Ru0j @mebp&C}m-*3EAUUE44UůiUG@|F;f.ÉTUEUU4EASU4ƫՃi@ioo'lưe lp-OD K'?qLm1?Niz]*v/4>V]MXU i% ;zr*x+:|l0'MѶ)6`:&l|) ֺt* WU|q( X(؊ih_|~~?~?aMdp,kM`A"$j||o~ERGh\5 1- KUc~?-f>3"OD s(MpӢh[Vqn##jjE bEol$w25546ʩImۀ},8+^mcZ֚/03ʶ"P^k0F1M@}4kØ_.SLEVUXp=!uYJ%rcQ'S4O`>G@Q) N{*M) ׇ*\p {|x ^*G=A(/mk]NndzXg4 dVTO454Tũ?דβmc"` U5/ Xմm}lƴmLʖQ]nڪ6^1 ڰ&U* m3p]Bh$vLaxT"r8"NC7zc#8֔~RUU @Axbvvd2af.q~>J[͛7ii#4}[x9?aʜm'0l0= dtaYJ}}=em~?@@Vyhuܸ6 A4`.,PWP- 緬/EQ,kC AD {S|1(AMCƶ!nm"c*;C;IR*b1\@A> D [4^ٿ7o2z6RUp\1݉}3ikkeA <:}}}ݻb!+EyPMu[Z sa0yA( |'/AO@AA   PAA 1 c2SSS6.[bY`V("² ҞG*T]墽I:ȑ#ض#}4H*0 nܸAWWLH|4  "ZUUU,͒%#ˡ:mXz'n*eyd6`۶7Wb9%~\f. ib P>{fff8t@ࡄO$ޏJJ>Ν;tttE|p^\fddrLuu5l۶<48||'OW_}Z TWW377ǯ~+z{{ihh菶>sdTT*S,74NnkYتa;wSNJ>}}}A^JGGL]יȑ#\~e:w嶺kˆRLV?wHmyyvz 뵾z*pR__O]]?d;3;;$L4uL&˗ B 8ݴRUUE__ׯ_'NS]]mgfe`i\DA(e0 EJUUUqػw'\4McnnT*Ct]'Ld8s UUULOON}6A2ҥKqEoרY*i2>>N("HP.4|lq1Z9vA(ycj6TT*ifgg͞R ccc4551==M&T*Q__שݻs 0 ly͞uuכbY dضi8pt:ux FGG7K%8Cssolao8ΆpJ{-VD 8E._L,#ra׮]] @oo/aq2s TUq4M;V"}Ų i@^csss q{AӴ'ZB{ثn'q"A <A)q B6{ ;3һ&SϷaȭ͍+eH%{ަ\/2B覎mی xxd%57(ۉV D @AxPF8inhPtrxzúUC3c<~zCʚGl);/0,_x]@Ys?wf*߼Y__Hݫu[ ]D Y^U7q3'^`\`N&J. 3o藹AO4 _, _4pSDFQyx'*?A(n#z\8,~ȿoBH.1kΒzvf\ ǽ=ZV v4(;e,@;q,"ceU6IhLsm` &f3$QTҪڋVXhv& aSgƜ!p |'Q#~/Eɤ1n,,Q6?UO ˹2N*XX411 C}qT6U?Eʔ6vФ7qp d Ifc ŴMC"-SQ "Z/Ò~%,,TE%؂-Bp t?hi'11Y5W3ka4UhQy|u4ף**ZXj8b;8"@A+a=p[Y|(, G)QAE-FWE&W3\&oiPX2bҪ0AtE¢X,bYF8k>-Ϣz:sqpX1Vl gg7ʍ!BL'OUvG);e4CvG @w@A(/ܩtP̝LħYIΥ%llvFK25rCF...KAǽ=4E:pܣ@W_}45oTaOa^6}JEoaKޓ@7wr-A <jנnHIo7o qOrm^.Di.Sz,I 5$$Y+KĈP֣GuY eU®c( O cZȂcL @ ԕ5 ;r P>{4=Ofqp4ob;^uUs[3;=VԈlje˂ituuH6T*m\?>c4mmHdA(hL&WeɒWn#yg ~D"|>W%.BUU(HdKčJ{;$ P>re=gyٳR((˞f a͛d6mmmd"<߸62K>AA(L&ɓ')0'js +v"6CQ] ,󞽾hu;ŝH.^EA(OAVz7te6 dYt]4M$KKKE(~ !PSSC$yܼ(FQYj]2mmmҠLNN~Y;[D=tm\.su A <9mS,T*y ծ. mܸqݻwۿ[dY._ ͛7y׽###$Iz{{I&ܿl6Kuu5;vt]'JQWWD"2 333'dvv|+<\mcYGE?pxÌ'94Mò,σw{_NNr>zMeX\\dϞ= "qRD$>CYZZbyyd2HZ4eyyyXZZ" *tuuQ(HR,..*R ]iii!N8{ҥKAI\rTUe||F9|0ܼy]v8'qwW@btZ,bqqt]ǶmJPWWG4fwA4N>s)jjjann]ש"kq-ioo~sq!NurL4E4t:M&em E^}UBa|D"A.<(B˲TWWy:::PUD"A0G*üD"9&T6s,?`,a5̪OWkKsD9;Gҷ6`HY)UP|5@.Q#Β[WB\3TUԴH#kI)LӤڬxZy%"itЁ@5hNJGm>A |"@rOOgL+:H@ pp?I[!ke)E,,vv3Q`0L#ALj`,E=s_SUhv6 +: c].MK\?sљQR)Q>(+ ?~˃<<1Q){e(]ԅotw}(+@A0 / VyО۰l |k<5E)ԂY9 WG^GWt:X0h %mZYLǤ_p~4{{_OH 4L&N%Dgޘ'am/RGz$ͭܢ z+1-v9؎'ŅE*kt>A@7e߫HZvӴdlb6mmm<#tvv6Zi{KK| `Z&2{:gyvWkLuDžnj+ͮp]GEBL{*Y6oY qRhLq7r9W7Wܢ$on۩ػ؅Z˲8zNVhh񦱬MքH2~iKnqi Z[[y')TF_x/. ;w[?뺍F @殉㘅<ϻ-y7M۶|]mmmXEFPTFFFj5߭ioU;v*xDNE~y:;;h4-5/^T*EXq75 G?O?4SSSE~~ 4ַV*jӧOR.ikk?!O=F:&J??ͳ>LJB:]pY8tЮa]FQDT"ceh4Zj4RJ1 x7˗/IROgΜa||Iaaa~ӟ>TY{1>xҗĵkX_et k|=z7n믓dX^^gvvrܰv)←&٭"qk펵Ux݉ݶwżU*4⥵M~lq=X4_Hh M?Vd5o{a*Q7O~;ޓN.b(DllPtN?zG4Q] 4 8o*|cmnΛ;ۤi׃EFQxN;vꓦ7>kk{"e}_)E\YKVLJevuu q* J):::hkk}x뭷-~s=G\KKKtww,jGR.m~_|,..ׇ뺼\zC1<g$Sو7w)%JbHEVXV1 ~^9o' z2}D`R!c|e0dee'O6,n}nJ)FGGtKKKpE_ވf?#r"O>_(J_5---[*?ׯ366Fqyl矧N0M>* ===H)W|fhhARJe46gVfxN {5i*f]s9'=B33 )S )3@` EB…RȘI5thtbb,aQ tZDfԈ<8 N7\e!,..ޔ1N:ئos9r#Glz^tM =:s7ewj>#+ĜbW0M֨x ¯}waܩ-;7Dgnk0N߭,r9pBǝ@U*D &nH?]K eR%zKgg>@p=9|p,݈=m}p7νhh0 FGG)JTU{? E:K!U<[9'sO22L)G8w3LPe$=gFN׳۶M6%ꁢP"#ƍYѽh?KW{{{z}JKww>֟v)\^~&ZՅfwF @ ̍78x lvSJ꼷z>+R7xG}TLǬ74OkLMvzf!eY`۶l-53j!STYP5/w{n]\\U\m \^^&":::,e|ߧ۶T*DQDKK +++=5 pG(nq7w;ς uC}ҌJ*333H)iooRS=x}a Ðb||}{벶ƹs_&oI~w~5"CCCsyyطoJRēO> FDQZG8&B,ٯj^eL^g0hkkceeBF @,˥KA2:SBY>΁( ۖĸV(@JIaEJTUr\#ж~~ΝŋF>G壏>4M*/W\6_<>#N:ŋIՊIr۰}h;JE @]Emd'"pR/H__sssX> Jˑ#Gvr P(0 w /A6EJɊ ͽ{kRR.YYY\.ӧaOOO}Oj^666/~A__]]]|ǤiRlll011eYd2fffp===yx "׿n[& [)F @sH|AL$" uݛ^ppp!8nBR 7IPhf ^bKO[?Ō.LdccGJ<8r\|4) J%ZZZؿ?<8q˲lRJ666dbbU~i׾StjjFihLdppt: ImܫRIݦ-P0R)n`jIq|!M'V݆ ؾ|2ì}}}rrL(8p@㘽\?>w{-5d-ǵ^'?Cb4I,vRd WwnU/LQ V +J!* JAPa+EԴA,!s[RZ;WQ`w7i"}>jH;57osws#HGy;W@\v5~CmRxR)tO i5M)ݲp e\Rt>5P:ͤEl,!ʴ6M|@JL!p (K=L>ׂ6P:!ӾF1 M1y< ˟|BauEt*D%[fr"#(ML =qOe?r'NpOT!v;˟-5X 6>$:pq vb}RҖ$tfY"rIPc.aHed\FKQJ:m.f9-ɵ0d%}f2 kT+ o}ˡtJ0yJq b! Y"Ne !KqkHl)à4O_f80=MJqu"e) Yc>2p,!(KX;JqHIir$a1 ~f5YcӤ ~:c߬Y6?@F @!{䱭81 .QDmE,EPV 3JRb .JёG\(b&$kO&l"*qL ,!U \!b59J1yt6)Jɐ2\C2AYJ>("I&8&Pv-yϣ˲ubqa AԬ‰ u 06M]GR),!"mJQc*qLֲaPNSOm2M2Amӓ`#j0 ɛ&A",(6aJ+q `Ta|p*#R0D-R2,yC䗭@-5-5O]++3gPە66H7%cl{$ WWw8ޭ^J 9l\){J$nRTTݬwp- aȥkw6 7}vidy,D3ad{4F$Ѩ>v^__G1e'BLiqmNDlbᮏMO"?hzےͧ{?_Jb>}0++KI%ւeQJb1`<2 >TcTܩe ~\.GYJ.Vәw0bLKJR%"i\V$)ZӆAmsy&|x&BҙX>%cжi,riGtvrz͊ѯ}>Z>_mm/ׇk?0 x FMܩ-ꭴ=SJDBct8U)i5M<)وcaYmd{C.U*J&QH*Us)EeQc/%R$UA!O~i"FS)R؆A.qB|y6SOmsqK2g|b1Iș&}A$3 ~\,|>!<8uk1wYdazySg~B0$#Eb[/!$rj7|a!5n Qe1 YrL8+9ooZ28)*EeEMu>OJ ~` ~WccI@F @AOm{&8&3uZd&%U) ZLD(M4$PcR&rq9͒3 㘕(òD!Iɻ28,x}à7Mq-Fb-b)&ʑTZILjYB4bɖd3FW0> IDATVp &-ck&tt.%) zntD NpZ%m1aqʱtbS~mԎHIPI4.ύ0d0'b9XCvL N[- ș&W}n&mDmIh1hZjr{;U 8۶&4n"~X,Jucsk".u>m,6/.IVb6d_*JRL>^LVmHH*ZiRCoiaXC]QӤ"%dEPVJKRz(Nm$cd,G$ɤ(%rLx8qmfRs$ -ɸ]":m$&Q+qLgm%ը/Rqv=u8CdBlKW' 6^%ܮS(HIpT%}l`1 Ԭ̫qL*)RK%ӜTX CROJ U{DQ*"Ð#4WUlKRW(jX7OJm$D:g&"2,!RFCB*2XdaY]*ѕ,41Z]CøIދ}5Zj4xuO?MO[}QثOw԰֥|, A-۶k0l '"%К_["mA@t&qLi6&~#8BPU6Ӭ9svM]:zo|ҏ_`[D ReaP+/&WiRVF- GV7(UK05M6e&y-JQJ,u\"@N2E2)]T6/☏c kJ ՊIcc f$";E4%hPyX'V!]} GO`T'')-/s(a#qP:Yc|p:M9ZmYt68,%DzY.U*L> R %}}%nژVN 03ak .onl u`f䇇٘utT]:8pg'Q4V4iyھZ==<-^7s9̖}XnB44-5lsӜI${KXcNd$16 QD`ݦIiդ3/J%:-# TXNz ]RCiV}JJJ扸qf}ZM&0q,dwpL kBa A0P@QJ6W#YFQG*aܿ b*bd HwxW{߾{v=iw{̭1 FF @sLs,iϦIE)6^gة^Zl>O%i5MŹJ$*k,FgUmKϱtX),\J\V C8Rl}JaS+uӘmcBf.G.[]%٩0",h,҆p*Ō?bX$Ttit&$8ͲNUJ>}&15 nia-ZI$p$b6 iRȩSL˿4\d)`-hml49L[]J-Vk:y}-偍 lKqL;>CCRm==> ^\'}R,!G2><֪URN,-QĉLIg,3ӧ)tu\R"ړ²(W)fh捍 FfoT*JQaYV{U(JAcU!nR, CeYO: " J'_t)%aYV)= ^`Hޛ:tmbgnީS::adyҗX,6b~{>5b,fk؞grrgCyId2ommm8pq[mon1a<Cntx}h4-ضRmI@m'nz]qWMi*nݯ1v$àTO\<?sdtFhCCCo6{;᧔r2. *h.a0_ǒA2|P툵5 mh4?{5]51LCpp 䆄." pG]UT}&J\As TYAx傑1%y}>x!Fd*RWGiX"͙1Zj4Oo߂[owR KHX-ғIOaX6+Oϒz$EcfMnv-6zD`#e!QRBPEV$nK!"삍bbXvtpkCG(U*hZy^ifu:;;, l-d6\R?lF󩡗 6"3>fޤ~Y-b{%\ 1&NCtx%FnȚBy n UUfSah M\YT*S&dJ X]PA̜I~uDIe|`X|ˢ}~*o]qTbvv8ImqLWWa&bR)(R1~|ߧ>1Nj"8ܸq[;Kn걫hR/n\Ս°~(?#Z6^qc(վR,B:|, c[ۏxP gq6Ffo}v-9%֓ד͚`d\c S Cn`eqYX^^ŋ/ۿNJ>)|4M:;;YZZ>|az-^x~322ٳg Ð* mu~m7J&5 ]o|gh#FRJ)1&*`݈YK Rp ؖ>ɚvg'8ZO `xxv LwmԌ}4oO!.+++T*2\t ۶T*9r?'OsssLOO300@qL6emmaիg7 kJ)()+++^[9y/p'q܈Aly,nd' Æf/E޽ǸU ^_̍7I @Mp/`%!Z;blUBfBrV-Y1Ȏg !q9FFX-Չ*#,`+ZGaLb/Y@"A/ͬE- X֎]r\&#=R̞㭬ڦ@z]@ϑ#G,aah즚mۿۛnr1d2|_iL튶owf|QoAr(0 !c1EcYVclR)|oč۰zKضBT_A8jp "Ray^\ۅ,*n^MuwXfDV%T֞-pom233>q)F.$r;cTP`vVկ'R6)+_Q=WRS!.ƈA\K1m lRBҝ&!Bs,ob4LQ E¤AvֱQF̖Kw3X,277GXĶm2qAgg'tzG̙3?_RO{o>>^y\N2=mmm_˲طo7M(JXUMN__Q*L:8@6ݵq|\zEUݾJ)F6YK5 =`SPy YD+SYȲiTV*&rV# *N\ l3#b#o ިKwܐ1;LQ;~] vr{hg?W~as?)cǤR o2;z}QJ5J.Z[[' C*haHWWl˲8y$}J HH)ۿ[GR!ˡT*Jtj{9immu)ܸq!(1??$|+_ʮVrVuP /ÇY]]% CLNN裏n?xmN~I[|2kkk0c' XP:Vd~fdp[cRd,Y|>Îtc@{[L{Y豮"p``߽vo~>y|Ice bfjZ1 uk}oR\GK?b[R o_nӧ6Ѷ6i^vܵP DWL?룽7F|azfWW#wkAD ) 'Fi̓B7 _BF@KnF 88D FH8җj;n\y|FNs׾=Lo0Wb)J$QJd'Uvq/yH*JnWU~˕(+K4-YHb,`<ܙ`0X AԭwsWHVM@@ Iwq.R%w 5ޒ`ͱSddgvo Vۦ595N۱6n9Cpl~ p¬Ɏ"ܠ].i;'3">xLԴ뺸Ubxn ^s[%V"X @gtl,,?\&|PL!(&}]݇/ 9eO(BEEE:Ya$#!#K.ʟ@t,"d8CWkWT95fY<<$zY"MNJYRJ^7Zk f Dmeo IDAT 轡KlG oi_j.O;,86} I 5"4el{!+*ɐP!)t\QA 9%M$rBmؖ"(] ~M8$ $|>!&ܫD`"l dYP:n]MIժGpfԼ:jNŚЊBεP jN Ep̄n b%\_q3h#ZMP5"g>>66ʟ88T.t"NR$IKiR%#gd EQPe]C5e ]k䬬RHԔ;II)r yUK%[1ZeL]982)R y( ! j0n䲊o-?'V(J|cAt ! 6jVQ*a9f;xKB=*^C)UGߦ>F ))XIE}jB VaOh}3-$]B* u;(aѐVREh/p9HE0BZ-͠^lyVMc@9.1\VP VE~8$@ >${}JƖS_+! UH{^>u=si-A30EV}#PLUR|.#=a`"*&T$OP KV7rr$J1WcP_'ٛ=/K:1-iڻAo/^!p.?YX^@S5a#Mz&h[OwZfUXEWiJB35Xݬ!J$ I&hFx xK^G*(Đʵ4ۇ~cL¾>bHF!}0k]::y=Oʡ-+#bS.Jκxr^F tnq.ɰbNž؁RD:&N?JXc9OcV $N2VYq*Sy>ǀ>@"]nWvAUQkqZX7)¯$ KG:wtiM^ )CR9rꀊi2gJ6Wvm5BT:mνupZ+l[]YnIH#(.,$^Yp\F0""DpJUUx'"*d7G %odViAneY9n^W))F kKgЗXC3qV߷:UKddX)]7Lǹtq.hm* S D^;$IvK%@hI/IRM X#"1o񄊬**HJ'P¸ñJ֫c LMGXrEҰ,"08N' }dH(y%O#  ).eNJQoa&JxVjλZWbEբtwXr&40fq$F mq<mmqۑ{%5ÈF!!166֩-ꭓ_+`ϛovX݄=&*{/i!PEhv%s>t4 U*L82*::-E?ꗿB4& [!P8ϕ,`'k8C~I.|KW ]G&0R8KJ<߶0\Ypz5?@IX-ԝj!ZQ^Z=k#)r}&p c=cL9$$hmzO+(i! tϋ D`"i[lannSum]{%kz1ivL&Ȋ  6lsպJZ%n>BIi307OR&c؈PeXc2rq$Y/KA*KW䯐s4݉nNUOmf90pEQ1S S<L:ep vHWȸ.!zRd!dڧ"0IͫHq) 6EB_:)e_mI&X#"Dob1oL+Fn!(Ȳ Z,,ie>0(QOcOfq)Xj.q=\c.VFx:4i-|.0oc[6s}ZƉY1AJEH_ MPTSn- 'FW1L.)S6I( ~'/NLO P%y]p325t@7 0vUm `!Ԑ4>/tIBw/$pyZ-!" `aar뺟KY~_:XfYz{{Ycbu@JgA,Y.` QkFANH( Z!i芎"B2P\] ڬFA ݅0.N,XYκg6{ U^-][xJy|&OWc3 n/q=΂$zZ A7I2#PCo}i،+o8r?~} 3M6^'k7Ex{3Wq#!"a޽7 h|ƽDkW\add$j<"l<3oSUi .T.0=JWWI & :0Lsz9}lIwW×}iK88s~<^# ٚʑVnTEXT>>N|XOw 01ɏNiE52=Dᖒ9r;/WY:Y2NǝBPVپ}{4QvM:ƶm,]CX U۶1 kaYhZȲ(c A !p]0:yxf(9X,$XjAv|rXu]jZ8vfQYڬRԋv;xm/6aq6C`{z;G G Ó=N@ XX3=&I.l,Wͳg) Hw?S|gX-t19%i$oD<'199ƞz6$A0+fH6AQvM&<:|<T>??LMMQ,iZ@*Jw0Adصk.\X,ili^{5N8A^'}={^>gΜ菐e3g{kgg|穧⣏>׿5===8pMpEQضm}m$Iu]ZmŢ!-?P*d0**BO'MIeKq~wϧWfҙd2FjSatW|jnTL..Ӭ7T]'/-Y׈Y1ؑAo%҃#gg9:LW9sζmn!ڬI/;i&|ga&OfhhUUjJ. ;ջxWc6<#GpiE!1\zC@<Dz,TU4M6i:}z!rtڵk=zufpp_WƥK(B'' }2w?Lʲ3#D0g#Ǐ?9$뻁 zfffǏw\:B8lݺ>^ʕ+Wصkǎczz!Ok/* hq r9#l|ϝO9(sz33G GطcMZ-NOsajK2C$IT935ky&EȈ1®]p\ݍW𘘟+=__/A'7J,iFw`ttǏ3>>Ε+WF$lFUUdw'p;zF[mQǝ֯Yy/~ :D.P/<ᅢwrLKkqv.>B2LHi)qc2}ťޥQoU C4UJ+QP rrDOAT*/1jnّ`|T"{q< 4"[SZ\nqMIUzOӼPz9>qMYiBg*g*oeqB@w. #oljUqgVƴ5Ͱ7̶6gc|7]J敨FR#" J7~E*Nu~熺l"1t qEB4F𰓽h6hMF-OP|^KoLf%!; 6BT*VKԺ< v' m? 'TǗaYbڙfl~ ˴ةԓ2M# 2@Sko1Fx#D0<@sJ)YcY +n?L _o0<kjJUr|Pd %q8TMh4W>szRa6nDWg:|k'cOZ5cMy[\(_ d١ԋ$sI$nO#KKDUf_ hOػ$'ZHI WYpX=*>s$~ N&δC׳]s6JVl8Fb3* ,eHzxm{+{=JcK֑2rF k P pe~:S&Fڠ7KN!(/#`1AVeI[RY;K^SwٛKCbJ^\o_n1'qe y7ONQ"pt(pCī=k X M-ѣPLl嫥v*Rl3 ,\ϥk1;͓ĺc7jd _T3Agޱr@KgkgmRp J_Xe{Rxs!" V?*ȯ%A0W_䓟5+W@LP뒉y[;Zc&5nu2nHY,u'AګL;7$ApC!ruP5np Y-l@_[If/|Aye} & Α#2Nd w:%=~]-7Tݗľ>}7Kߤ[|@ STkUZ $)%E..lUVaj-*Sɕ K%"OBIPJwpp]ґ%0VDF`7o`hj@beFk>$ti4E{d)03HU"a=\/uJ-Q 14q]WЇtڣmXΌ6U<. g6]쯬BB'$A}j32~ͿaݪcDyHZPl~Y%_9欞[o[N`H×q=ow2cUu,wv̀ o)E^T^$1+t|Ffyo(ccZ5҅;"-9W9Ǚr8s]LSB푪@Fx@ Զ]]jUbͮQmV?}t-%@ͫYkB%bC1I¤cܚ1%'m!$.p䔌3`_ц4䄌o]2-%`2JT+&WT )kѢ7pۗKi)]6?&G* <\%lJ zZaDCC)Yb7q@+>-,\_Pc1TSe@,үF޸/B\WZWB9s4&W*5&fM=AEay'N; z73!du_A\ѣ"Bq%*Ikal5h{mJd5.5<ҥ~$Q!QSܺf BҨ hh=Μ%\<.h% !dCrTBؾ^{gs?=XkРn2*W#f҆觟Q`$5BBJؔcK]r-idU&dzֵ3Ma2sr$eqP?ox)~sIs?eأst wmZʗ (KUC"v1MUTtYn9-7|Hi-vi|x^hŻ,ٜ$4 !PP%t n@l8y 3'lfH圌Pv֙+vuB$^[a͐w5`Ƙ|<{2{F>]0oSkԨ5/Gi+hFEAxF~>tߦ-Md6\ 2W#-!(*Erq9*(θ3\0/04Ve+ϧ'Q`9fN7OSoy.Y#i1_F"jguJݫf변7; du?C=nB ?򚾋Wdo6aAT (n\SW9}eé2K߃lYlכש֪,kˌGɸ2RQ`v׷c(BX z4m% IDATjCFPԊVۅ*as'QN,8:ȳφ EU>!8<@%2-L%WثnHFxp(JLMML&q]ϧӛjޫFw,[%>TRKrڗNa+\\ō\m\^-qz[I߼?, |$|{c;#;\i_l,^`A~P1=%0|F6=fIn |ǶΘQ_ݴۍN_V}k[-ڠi?sD`dlzzf8n &E !V&ྃd2>{LSA)`iQs%N-ژ\n\lԔv>RpbA~c\'{42#0mFIӍJApmn{7Yz1w!?'E"Ȳl?$S3}1iPX֗]Ű f)I%1!J ^X@@Kk1[tL.R?7ء` O &PhWjg:ʠsw|}RjP*x15rn*1=.7.3//!omp]xN^cQ]r29'G^SLm` #!mZRTy O z>/ҡy^yW_@H" 顝mLX>p9:w%ݞ,K˜Y:Xu}>f(Y>6EztǢJAӊghaa)lr ۷ogΝ7!Hww4 <2?O^`qqIx8~8'ODez-oo2228;vɓ\v Mp]&Z"nD#Dx \1vw=6V@G8؎ٌ5h6,)K5u{{($ lc/"+fm 5<e)|RJ['":t Ζr~k-w omڌYcZ:EJq(sCU,c1rz1{ C[]ץP(>daTUu]dYP(:fzz388Ȗ-[$8㥗^ܹse Ht^4Z 4L&iݻ˗/:\vy|&0>>NWWxvi;v^x>T*޽{ٺu+rx<ݻf 244!}_bȉ':W{I:\y$gH6>>T*=԰k~5n4iM.U.4Jv (Ɗ9.bzlS,Ld٥fp\)iidYfޚ'wɨJM{t+LӜ^8mNfk~{K'<E>Zll'KF.wM7ss|cEÀe qZOP(077޽{X3ZXJyxm6\nx﮺W3@ZENfZkYMLя~)Jo.۷ognnݻu᱅.t%1ٜd09ȑ=OX4CǢeb~ HH zDݤ)b"^WLx2WYX^ .xx%8zVē4.\dX dt:cυ ( @@Y0J4fffx'b߾}$vגzċFI]{+M tZW˹ގ>z5ֵ^{@EmLc۳_cbyn(cP6-hu`kuf*3XŨ;JZ H*IJj}>R=)4Hʆ%A.8,@̌qߕX; F z<}p [¢/E7FyG4D |VmX-H-'Y{Wz+mO`fǢAȰ^$o|qB¤PU\_\/SWºXP DV6U< `Y]fjy Wu98pxV7I %B:Cؾ(~e?f^%UJ! 鶿eou{lr,{p.*5eRkk~9!+h^_pUM#!D}lQ{&o,eqw߽>?%,K#F0c +r/r7_~Ud!XJ‚"g8iL$COv~"E#kcggxk* T 9EZNM$™kHnFG*8j{,k+i*K5&V?3ZR*UJwAJIU嫛v˵uf˳J;vRR<>-eFۣ\YC<c$(e62٘89Jn(iz>KuηγU 0x2g5< Iys; &NSU-FV=, pM\T 7. -*-ϣ(BS>m0(+dye]gR1?Oш`O.d$I GR@*;vpyOXh4T +1%F\S lӶΥ%UV%ySj"ryrLJJ! \؝MLa5=ݓ<)[xT88!bۼ/\i\G(~c?GrG0?ʳڳK_/!k5 K3m|%VR< x.>Mm$LG$\xh{󒄠hDR}!eq0]X=$IE!(4}sR8eU cD U۶mCUJmf!Z! !< .'/nKpVulf\3U"I+i %NboYẂ.1βPY-9:(I%,SS$aB$o\l1%M|RN^NLm^@@Kmqj/[˩3X#,=κg|ӵs' 3CYT>#ˍ jFgW$7_ )ҲY$Uk5`XA@R|+zY$iR}f]$}+WGL摫d۶;j}ݻi4ZOl>NCUUr }|v||9su3 7[-j" !p:τ3AR4ܸLN'QHR׵`|26N0,NQoy2$?}S''qGdߓw |0;;۱bqNOc,=;Vxa6odY'ȻƝ%kneYN>}_R,O_Jd[C]\8?@ߚmtAz#%6lF@ʼn ;9!#%$NELq0pxވFyG&a˖-?#{%iM}^}Ul;C"СC>}blw}YI$LOOw,;l*? M|}˶QfVFIc4>m]ŮPXV>UGp=I3 hi-jsu:S)^Mky ϑH'PzK^D>r c@_5mzP\C<~lcɻ:YmZeY4NWL&GMA+pT>W{*xr>M&ҥKLOOS(D" ɹƍEt[?M&!m.$''4sZ67mׂUUupY^g&5!*}Po\%4O^uzIߝ`TB @k +&B:ZF"W &PUVRJ4VXQ%bvB+Rq+U)/s2Q"zڝ&U_z\%EP X+\blf0`Z&u^j#hX$ōbQ2KEO''s`2 dȐiU[5{)Cg o{a<‰ & l nu? dtn49z(;wxGOOPB)e5 sΪ*_nںuk(lذaAF<6*p4@Ƿ(G.MmvyJ(%42Rz, fuqa7MM xEcER@ DX0<1eKN8nMMpeuqxp5'la_y0^mIAvv1 6Sϗ nE:`[8Y䁋_eKœ,Pws URZ¼`].0d8/QRx~hXU C!&KȊ$I&i1blHn Ъ9W'T$Y%9g|fzYODPgPn))"-h?LSƽ>HP7˫ 6noeK$ 7B y!N”&MƓz-ѓR2NR,EPrJL+Ӕ*%2>MȈ5Bkݠ=ȇ#%[ǝNԽ .P(QM EQPU[#6GvR nL/( hMk6JH$]DF#!\Ȋ–Pq*Tpwgh5liIԢ^#mn4T 4@iJz w%,t$ғH[%5sj\.\͏$a%,8Ë/*%V+1 tM} '<Cbȕ+%i.^$fhVi4P 6禢kg2]ՆWcwMd2mض{U0F24g[ 셺v4 ]Q+s%yua ESmԈ;ujx3pU|='-aۨ1%`OUůnOU<iI"=Y?V-Yб,s9Z>Jo{TEŮy3 g/cmF0;Bh>\OKjm~+ & v˃5x1_0f%tG>IoSS\^bSp?Hsr Q"_Pu!*+xy@[kb8?̋]/dݓ }pN]9xuI&V"k%5PA(/UDP;|< `h3r:%֒L G&D v,F(a[|W|+NDpn%%hF<kY~'VϬGy= "{ƾd_]M7-Q?&oY@sw3V={F#AGSl,f.,^ P@\u]FQh)L*vLѤ4i'VB4U빂].u_i}SYf89~7ݷ%AA`^b^D/;vֺ,_S e!+yQ/m#]캼=$력4k|/g_cJ@-T+S.!8huZF0e'ZBq0]\>GsC)MΨ2ʖ薇dߌV7ߨ1P#2~oP{;"yK"KصcG+G a楷6CWd!IK@ 2B[O͉r<~j4ib&L禉x4E#uԫtD:!_Y[( hI]uryI5f ZЯ8kBKhc.B" (,PS*ZUQ#~:=ii})9EZC /Iv'wnL| 5c9 \hR2?@[fw<3I#lӶ]tU%W+hƪU0 px% ZBc8@#Ii)"I=IJ01W^z2 c HyKRw31PS4>ۿ#G؛ًiln/>`;^ͻEdBt&:i ,,k`M܇5q5J(>$ ƺhEiy%=ڒ_"}+{XK8]Fg.bhWH,~!6yjqw"9+LJ#[,%-QB ҒĶɡy/NUY

T*@-Q`m0/ik>~2vu[ZwL (zE.985y ٘ȶmZMnt4ݡ*z}<{Zðެt%jJ?9-Qr_2׷x089x+nQtBHOi8SHPc.ǻY߼hYe CtF;gxPh6pk sE spE }\ְo bO Aױm_ƥ^OchI kg&aaO잴Q*8 MIB{嬃Pi4i&-Y#]S<^8P$~90yssiH0R%Ǚ6i[j's'K6P`Y"uF9?DnU*K=G,y2<#[ɲ5a{>+o z9Y{BKBC&M;QpKӗPpDeA˂^wEOXW碬=#"WA*~?M1ܜK-q:؋΃HW}ˇ{ @laE f|yMމ=.ʿgVajK<r̷܅t(7졠xRqBPT%uO!=u"~ȧ^7{]\_&!P |OiIZH, _y7ptIZ5ێe٣ N,$(Ueʜ/!d^[k޾otyJ{cGlb(ЊL_T@x. \@U IxU'h `>4Z #Q#*Nr}Q{R-p=?H |8;7Jv}_j`I^S837]Iio^ȫ! UCQ*++I& zdzbƜბh ) ]W?^n'Fٕ\s4{2ReޗTUGuqpD9AocnǨ3ʱ1ƧYY? jXʡ2݁ntid297?uDN.W)*zA%@@ T&*j-Q>[F:% T@s{ *mz(,a!/9 dWXX@ߛYuShml5P#4:::8vbqAEۑɿ̗v\]緺t:]m;$$os9&|:ZwQZ;X1D<$ bSrJ#=ml&$ǧ3Xeo|/s;"g*g8]8MCd/MKI;Nx ^x?c`r#` ,Ah[a_Y-+Ku]棕Ǎcƚ! @ܬVo=A8cW}F4躎@SCl?kkrŻG? k6 }jKH{$p$>G܄/fEbV~=kݬNz "Z7㱏zYz zo׆..!sg*gΫ'sGfGu)pp4|HSK~YgUSJ*b\'dK>P}/wuj$,XQ#<9G+瓽7?|׎}uSJ h+bUq^ox_S?T'J+$"8Fm:6[ _a-kq<{}#xЬta|:)O?*LoPcf?c\Ȅ)MR`6CwUf|<4)t[pidw88p5#+HD@u\r¿^Wv6c \~'EmW%xJTA$ZP jDx=v]w ):̐I@k=4s]pmP#5|3~@*6Ria>N?MT.94ucOӞ&$}>67 gc\ì2VwHwx@y3PJ =^KF(= d,7FKc׈Ry %Z /"1M8~/qUe|qd&zNhU'}E=T'm˯.JXYzJG(P%YM t@Kl Y(A%jT:B3.A|GXZ8QoG$001z>?S=dvWJ)cŊץ3=۶9wDĢFkgkahyO>,ˬAEE I`ݖ+E{pl-[_=ld_dž<'ɒW}>̊ɦ&J ~oz:"g!7 ΈC';2MVPAoXx h `^UFKuTUpe_A@KW 1⎐Q22;h9dz e#a(I4%Ty?5!`]שT*D"\BWtI?áC迳q/$xk-^ɼR +HCd\VʌF8S:CegUw RF8Q:Ta.Ů.M!A.#"#4Fϵ>ǧO~ʉ ~mQ-7.{oOgcprߤhygg0tcq<|sѕf)d? ]He#8yI.4MTUR~w49x dzXbms|IB`ƍ|ܹQΞ=˪U`ݺuϖ-[8z(Tk.a\⽱u]JRP79?/ɫW1T@{^a>^ͼJ@}0H`PYXI>\P=K9U>Ev&R})[b[HeRs)9Ξo:kb#ELƈ붟̝P!*V&4#r?~Jz~['tc !q$ucA'H2^L3ˬefz~_s|G466 HWWirv{P(F`ͨÇItfx8z^O^+xa$GgR# ^7V>5t[t*`Q:R"d %(᣼*:׏'`4㨺 95c*^ë^5"+*D\᷻4: GXwx"x{Cϧ>SL)Rn꾇Ma[vVy# ͅzaٲe388޽{1MϳvZ=eYR):;;_dSs\q.B <ӣy-at!졄#1/8٫P*J@l^ܠ$eq<ȺQR,~=k8kXݴ49sh4ҥK9s RJ-[F2u]ϼ (A{-~ ^CԨJB Ľ%3П1np7%#/*D0`p,w d*ݱb# Iɛ^kҹhSB z~ÂlehUA:?-'}ODu?sr/@+(Rb iD긭 NZҚl/ؓs<^䷓ȓ##ӡb4HyD绞0%5` BZW[^孑Ӽ:8ᓩOx&Bj %8R9B}|Т\2}>gYcag|'Է^|aU,^Hzi w ] .=fchi ggn'\tzJ.:{\@&U~u wE +`&;ǝM؞My,+}!{$I?H?W+:! ;"-T?gYx-7yIx9Ff^푷ٓSZNƛ88y- [ ԅNĤ9IS."/e8_>ObU|3BТCI+q|8ggiwYZOehXdD|)J BeACTJq`GMpID4ɻչP(UoCM4lP|iA#"[vGl&`huZ\4Yy}4Ʀ|b)VZKTJrBa{qm۾NVyxwD9 󼪥0~r۶ 8( ZPs嚦a۾[|3moiiVGoK&9Q Z/na ZBU + خn]$3 osHER^WB 5iřj.t{˓beϱcXv-x18,_ԘEQbll8~)~)?mI.\>˗sIfff >3TU{kضi?#,_ 6l؀e>S, 300__c&__a&RJN< B>)J ,UVa6+WP(ؽ{wj򺞠\ŶO fO}Z_#фƳg/؛K2_e eLJl\#} NO2S!%XYfB5:"w.~a=G:k",w *۱kw[!%0]@hn \[8ӎwm(E m%- _!.m_K[W_x1` gCIƜ1,lndkf74jdm~uD~;#%j\Hx'V1;C*" 4L^z&0 }v1 feƒ%Khoog||ݻwNYnT{b6]]]g?#2>>ƍ B,_xW^%l0jY3ΖeSd5d؈:htuud\}1,Ewyyy.˙QW<|tJb$; d8;LFdh 6'3O:S&9}fyH)R?Ngwk닮Aǭ;w ҰoQ>X2/Uw iڝk1<ܙJ) 07rb WiJ, lR9_ `Td,z\?eo~KHESHq>P9ē'I7kD{CT*VXQ%Lsƍ]񭎽s S>s===oaҥKo"uxH77] ZE~;[m~E(4Y:~Z^- B`cc;6%DY.,Qo bKbfr֨4rɼ9)pոE}ڭi_ 8&$mz-Q`{z;w|<_wIKn㼒~u@W<B dCdKdB;g[% lY߅"ȉO~9ct4jį{KZ[[illh1"7C@-Rp .٘oD8Y|s]bFbV]"4M~Ɠ'1ӉOy)PmQ+deJBm4Y^VuI0\fI| BTPp +c8xeb++y<8@6pO˳IR佱8p( Y T*Kvk.2r_8wJX~uʷ%hTRΔGMvOśoD }/zE209Zֈ_ _NS?Oi#$oAIA4 Ӽ0];ykGK /'_c~̱c0-F_~@S$B&h aWR'9;t%1/Fɚ"{M6vAxz4J@Aoi{ԗl IPpw"Y&Idxq9RJ^ *BeBheg!I =_vƔ{T.T-*ߟg4F+ܻ*|j/}ﵫ=DdyZ ?OoΝsdz=q۟ʞ_@5"=; =_͹ !-yDMBk}ґ1Nj3.1!:μlA˷MbN? 5XB;D| =ϐkϟ)qwl'b(HOV(-h `#6BD6D&,ܬK75d!J_5W'd.{qg\Nx]kUm%Ր'sH17hyl)c9Nh0Ĕ/^dn?++XZ&BĤL0Ta>Liݍ"^Yjx`P)VG.Vb(֠Ehy{FM~Z_  ;.n締,@(\`wƭ!^u ]y7c>)% B3?"wHJlϷzհ E4 !+ ~WP  !=((d԰?g"!IC(ՠ4U+yD+h1 % {˜Lܒ/\+fU"Ah0n%=Wp.F-~NAKjK¯}Xm\|b(H{FoђKnEo s9/%f\bOp.̔3>IJ(ov#}Gh+KQU]D@>@[ƨ74 6nZ-5oCƒu_zLxWctz"33Ei$ў;fu]WorE hJeKlGc;-I:U4P]T/ T ~. nnQ$vId;qQ[5XDw:LEQ%Q,sAs=k_hzI,ɭ 4?/`Y2Zʧt;:NM{;,R@(y +ϧS?J\(rsCxaE0P:SBKk KWP3*_O8s_~,m-BH& vOƘ6YD44(hDa8}1|(+/IPF=d*16.)%~wore7%C"9vP @ESPdQ펐ճ= ԨJvOԣ)J=O$~.]iXg w a!%Cbq"A>P3xC~sQdxvpUODM(FHT<72t%)bXwUT5>}/i'i=MDB ӍOQ/ͧ'§edb!1D>gd+.ZT᝞wHw,M% #Kg=ˇ=rv,F@Խ)|3^>O2d$XNܝ n7)Z w Z!AB9h ^'{9c0AsAp\&0WaQF K2ϛHP7]]!]Sjb*GqV((%+\D7pZJ;ln Q.Pt5OI*eM:Z#*sEkx.+|'cų<&IӔBLhΞq}~TU*GS1L)yKH*7dmfmG$\ zYܞJD SV<{UBϢ' P A!@M jQ*ZR [-M :UQA+ZqMҕ[ /@:(jZ$K@;b4z_NXQY>c#԰O:%M;I >mc֙(qw/y:2S %Z15FČ0D(u^Y<l.J])*jQPTd̒fY[C SR1$LuoNbc(XxX(bHqa|+( !>a%Uj*a61~?^27Z[oW8M[49M\HzI=e8;[UԤAHU _Pj8Z;My&JL!?N #+#Ŭ@InOOH[b8+C$A. :~.NRФ5bB('p* zeXgmoOw<²:_MLVVq?JnO.̹8X~5u>O9t{֪D6Ep\u 47P xk-ZV:ob._!N=LcmR%f . J 0""B00i0WOCBs.ts0 )ɣ)5V?JaFE!PIo4 OC\@PЛuJgK_Pj]ɰZWnH2T^uS.(O(1W%z1*P "7Jawh6<# ]WkKHGh!,-7PQP *PkTC>/6LX.) ST"B@َUVV#H#*o IDAT>1m7El3. uǒ\C$KW m.́ 8@4j12V)eu%-hfEx>MBJtBT@8NbGbUGp,`o~/b n 5L;4;ͼa- %@!D%;zۍO5B/w"E>adx5Mhp`ǎpakq^\.d-Y8SUt:M<V>ڣ(SSShh5ZVd7_%YTEȅ(Ãf9յھP {WνZ~DTmb kyEZ^χo^4`$KrbmZ㡏iA]Uا0w'YE;D&ZF<wFGrO1roSUh,JUUբ-s||c^l~C^(F,bӋNP,R.fǏez{{mBٞo}s&ҹjgOi){wƧPš}a`jGI>I1t!ǹc<4vysUI7qi#>nbT3zky$%nAT5逅W^ ,/xv켩!V)$''=Rѐj444( _.p rD_6,t|_A*bxxe˖-EQhoo .R. \HX}۴]?W+ayU?Vd, !x!~=kRZXˢg[z&qݞEeݺut, ͊+阥RA{K[aN牔 _y+y\/ `Ps3Mh2(L˶[LQUuQtD)(_D U7=dOeJ? j}k|P```+㶜MP`͚5;v4X3gpw2>>YlT'Om6>ʾ===tuuH$Xr%Ǐ;DQTUԩS,_ՖeCb( .K:a=[Yh\,Y_M_/dmjmڪ!^>2;`6!y7O\PR)Luʸ Y^ʊ^{MRLNN^?00-[*[=dtHXĉtt\R+4PPxQ~{Ft@$N* K>mSx@CMV݀6{^'<^y.tynvfkd++ZV!0>>Α#GmEQhjjqro6tzvE}}=wy'MMM|tvv222G}8O=w鈴{b&4O9y$e{rJ>̎;8p͸K{{;۶mwA- @ʒ WC"'i6SclɻL3)Wcau앶ml6K.Cu,a`YyLMMl6]wůk֮]iPz.+m\>isg>id2N:E4 0s555xiFd21>>NP ˱i&˦M8sLоӧOsi}ݼ^I$r466ribLbʕ9sfN>ͺu8{,uuutuu]3mZt[GhA˾_rlf=e\3N)fsJ*@:xw?D ]4ˋ}(MIcD޲}Qn%ihh 399mmm9RJTU!zjt]GQVZʕ++@QU̇QU^xUUٴiB6mڄirw(J%4|Z%$KrJLB =65<謌d8ΡCtUu]=ا8ݨ(GaÆ ߿|>mTWW_P,inn{ACCN"L{m6 4MEg8|0ΝcttZذa_}gϞEJ /P AީS}۶+裏Xz5\t]T*fٸqc͗-n 2C2PvvD˓'UqAKj it%n@:jAz99vgEUОDZ1 {Uߣ.Uw[Fڊx/Erv>\Bt]GJYof;{.->,m*3CW]x9]s7wqbSV-1R6R{] `xx)++^z!Z* siV^֊fKR+)˲gZ/૯ )%R+Wj*|ߧ),":MMM!8{,B Cnذ>j:::PUGy)%sw( :tQpvttTys_u"}vΝ;eYرuH)+R]]]yeQUUE,X,*d>rG}*4 j^rTZ U ,^KߗL>'nD ,'/u1йZߴ:g?gߜwukao:̍ȉ \JhM$`r<?d1M}o[\o`8Ɂll >+^:RMQ}VP^~?{r9:B,ˢ|oG%HT@\z)o\L^]>y}Ǭ0.UUH$rA>!i,[rO?4dB _{e.,VZuEDJY {.m/ּ ;deD;4K ()zdػJ{!G!6;4uݣ\J "rl9!/wQ"J8g~hId)$K#[MV--%Z`xog}\ҋ2.kdw˿P^Ҫo qW]>} Jyi+WY.3,{t]y~gui_覀3>ҏTYgݠbAOĽ${Y EauQ (@)Hk.`> qh4 E/\"l+D*_Hѭ r޻w/tMysM^lyMh[ʞbUyRZ;w'5;q&=<~9d(%Rit|>yDU]'@J}}5e9ϓ))OAױ;1E!y Ni5MΖJ9!$UKU9cی<ˡg2"${,^nLhLW,!(IIBQ0GJ\)u]}&]'}1gm$&s9&ԪU,@$=ΑHy^E\*o.b>ly(s9ԜʓMOb7U\yDۿ<a>f+(9ם9T,VDκ.-+VӋo]f)(W0ؖLʕyTEb3+bH?uuur9"MMM8LMMUߚ>,ˢq!ZZ.ng6<<\Ib R ͷ(WJ o<hҕbnS_#g=hmj-˧f埯B`* :] 4JI,|!de[*a( y-/eQ!eu$€` a Ҧ@!XaYm"3 , M}ؒH}&<3 HjT* iX> Wo}j]=f XY 0i1五 F đMFJ%M3O[*bR4/Gƫ\Ж7Zqh/X7 ü3[-tt߾e54*3&hb@h$Mq8PVaT56k>Ɨ>Y! /m;@ LOO399l6*LJߥ䗿%ͼ(ٳgygY~=|?~7r>ccBS4}o1k=#^yBPf y|b005-%}:.=bJɰWmMMDXJ֯g0c}'%K%z@_.&GJ&s9t!px4 HL. pXD^airT4t] @:à+!8R}|ZγRQF<3#KFr]Қ|g&[L,P t.ͦIƘY =8[oUy v o8 8c~ydfϗe4 C bٚފ"."0 N-XlK?'~'7VKnS ;Әy۾%x C3뺤R)&UBPUUEX$|rx뭷hllt:UBwc||0ضmֲg8t2g}Fkk+{A4*$g:"|;9&F" 4O6=;TSEv`592}{kRQ-F@8ҙТE9])(3*K!67cͰՔC&|[ `Qȸ:O?ZێR]ޙfs3cX ),$r40=eY{ zud93qqnrr3GFiں;+d[E>o'37\jہ)F~ok0w[p?=S9Gqφ=˅%K6wwI%9o˭39^#spUj庽ak_:ӝtvܶh ^ h4Z NNNVB1BtA,GE 555Ybd\.Y|y 㰹۶Yz5tt]gddfryx~k֬PI\oe7;Q*7=Oz~ /44 4Dt4, oWA3qmX_6g:vUsNPq5EQt屋s$SSϓs9'TZ0TQ2?z ZVBZrs5PHISHBoLJƭuu~.W_T˩c d;Y7[(( 򋳿`L @ 5Xum'i~!o瑚c`rB@ w|tOGqf~jREk)8 yͦfYu| ۷;yo4h{J9Ƨ27:%b .Nww7--->a\ .2JH& LB֭nYVkCCBA"RT&B&켌UО7A,,}ߍgC"2w=:~{h=Gs`VZVLD*Q %@Iw&sscw2d IDATȎgmSM(A1x$j\Epz"D5hu:$$E4|02{/ [[+'WH.2n<1 d{[/3.#4 K&BnX*>`yL ;O@N bgNUQwW}B7ž=+eyj9MfZZ 1߼7P4wEQUi|F\c;!(M/Rz7@o m/H*Ct6\ե2xR^\%ÀĂ" poRgP}5ޟ6d 5ESJFfʞb*?W_ VSp]e&7zNc;ؾM *1gWxGbN0 [6fP⎹ tjDJ}%>a{T<(}}TŪjQL)#!NP i`S'wUr.m.H u=ˉǁfSÜscnRf]kۍK>>V:t5w0oڼ?>Vgvr:6 ܇Y0I #y_-ZHDz.S=KJR!dVf0}0k*qD\41iJ4qGPUJ'JC 0TD>Y$ ϸ`<4M#L\+Y;_L'ßHCh.^ąf3j._bm|-NeOёR,v6ٸCޞfҜl9zԀӇ9;qH)EWPL@ &UQ- !/̗<(7Y ~Zag}TT!PLIA>(ö6[t _LARRK6RK xaY!PtD< 9宕f+-&!`C!dL'6R) q'<$ >%2<1Lrv&w:Nh{d4yU`ڔ&-HG<h"! yvL$'pw~P *8Qu%xX<@9} ?К)к m%./uHG.D -l~JDADA>@1RwŽ=?%i6RtoxE͵B*M&~B b?Ww ,ra!(@,I'n%;$% 錄石=I3PI\puX,Bvz I@A!R[w^˽Mj_X-眃֢xPHh5DEW_ަ{Ht ˺:V|oLhκda0GbߨoI:NۋX[w!ў@QAϦ>t7~m1P>S=G5Yu* }SLBx.aC \Sl7ViMKpd:nj ?'qgoC]&Td P!Gn?xE IQ; [ A) i> A(CԤDGYb@8ޔ.K>'&Nfo; O?RKXEG$')i'he ?@2,<˝;i/pijk卯v:S N"DAᮚx]&LVKN1MD ?J?ǽ5i5}ۂcW.zd4/*E>-|Nla3jE FQzV%VqW."u)qlispӣwZ3o,b;6%0P`8NP`4͛_ItCSh0Ӝȟh \UGA.}jH7 q"w(.*ۢP٫qGYRB"*7zD:"ŀȪ2MP)a-*h:}Zcz:cxjFthDWG^X"ɳ!G}d?mC3WzGytE;yU2FXۢ/́1||'&o) Ti!)M2P i_&JԨzsT;9g+_SI6 `[x^fc)aT"%$ e̦0 'm).`[!a%O@E+M2?8 sżqYN9ŤeZGwE0\ 5Ov>s(,aR_۾}NQzPQyٰS!'|VĢ.+y TV-Wb0è5*ЛJֲue<~'X޸%a;6jDEpSy&߆^\`35?oIYG]ni,pZ"ΪxsM66&u;A>皞[h#̢?# 醛|s9# MMln܌YتMϾ}odɝB,|~'e xYiq8Ei #D %z^ivjـT4,"bm-Qr+Lu$ mIXmc=:ԡfDR1]yw䱕JI@ENRB. `FG*(6712F/(d}@GQ] o]GľaE $AbUdo_y :ŃE.E8$CZWC^x}bҐ8;iiL.+&&IɜL* ?f*Ot4zJgk2/8_YdY&CovKȶm"pDQvv*?&cK*mxp+h!0ORh/SHw(!Exd`8wp^_}H^{@rd5(='ȝ̍z\ 89gj}]!j(ʏ΁k&vq1}Fw#^ƚ| 8熔N#xh[]p,uT.E 5<`YInNƳ^7*sGG`}x=AO'K(2e4L|>?ʙt|=X[W19Ep\?jbE v啪WdՉ,;o= "φ8ĹA"SSxdz9;z<8 =zȧ,p/`Qd A`GggD*"Dݵ?2_ 5/ KW%bcQYXXE)&:~Vx4\Qpz4pN6E7QGlڂ][p>}(<y]]4-Z 4/VxOU?uPeKŖ嫙j+~O0)SDj#2LLbssJPllD`mOFcm>SSG]K%/Q+e KOdu0YYPT+~uk^xvxɸ2wVN$VeW..:<˙լ仃 Y$/bV7ɜ&Y__hJˉtX\7qw4.(n.xֳ.4'ikaSd>`a @ +D;ᔜ͠_yA+I\ω%AbU*mU"KKG4~'8?3JLf(3x#ƨ` QP8;åK X\uWUuloJJեsf={lDW#[vJ+>j OwpVqDd1ᘘ\3pw9󜂓#/ysV}nwgSŦqgkUMX[os,y`lچvi3iMrq(Y3t5*:gx]ˋV=͊ţ@ao."Y\/Ͷmu,&|&zEj=<΂r{=-{X^?\13,s,cr*LLbZQ*KKyRIHp"u"M9sBq44/lgUp)[YYErGţLjYXL]āc8+l}Li)m{eKSb''hmfCd}Mu :].ǖT)KNG]mX|XRuۑSr>/_"⻺kni˧|7l5| KZXDym5۰)r M{yB1ehQ[8;x\˃ 7'Ҽ/_%}ll$X*6ї󓚟4ympO/Z7B<ǩ9òG0-83g5!cgs?۝rQȷ˖o,g3gihެgMh p1q)?L<g}.p'.LZON;|rUKINNbjx~^({/c_j N_75{oCܺHL.#hٵ`g&{&&OhʂX`gN>q4Un.Xf?pal\ѫ)qd!-<<ʫoy`moY\KQ./rC)ega @ qR逸Y^B/&{b{xudwVbN`qdĜb'lTL+|p0'IJLq0,,fat!Ł) CO91q2nA,tËV>=]]Rp,>+E#R>WqEwTx*&DT9>\}.6}leߗjQ9 ZVlWLhk,0yYA4 mK#?gOU?u| TIޏyJzp0==f j5rG&gR8 Қo^Afkm68ieKs*ܱ˒eO|Δޚ0WNE||+WSP$J$sǹ0tp>Zm5ƝSO>d 5RRVT +e9?ΥKD  >CYcCOboO=j%,,hacxXZƮ]lٌ` \2wOo[ADLtx!jE-߇2IĬYg3n?̋VDuަ`rE#EQYWI\'R)8>roR&awn^ ^L2ݧQGll*|XXGC!u,/&XSwu?&ڬdqK4FEC^{QJLe&SSo)A}Aڲiɵpp Q5e9=p{Q Oh{Gh| ;_˾ yBVCŏپ| jݵ k*y笩\c ) Hb:PWtvubmÓoz C08[8ڴr&}nNgkVDq73>׃vQ#f0bgC1#^p@4}ۙitttibf+B$I"  -haa; .wWW|8kıc,-.MwPޗcKp^BL}7K4n:|3 o]^;Ҥ9=If3Vy}}*|XTEQs2u_HL.|y>eE顩@ ivQ  p1v;.׏vʲŶmHMe*Wu,Dj|y=h׭w}/_:8vr]92tY]NMu[(bZcc%gǫҪIbBu\.z-$a }FQKSL&q8 TU0`2$ @$ 8\0Jxv"MBN'7\.?nŁńkHӌlnG1\{/J+D- àǏ( KɩSuFۇFQDQd4773{lt]CD8y$UUUߗ:ܹ;v "^ӧO#I_|3fvc8&qY|Bw' b owM@qF(S`AO P(Э|TU-Y($IBQҽr8+,|fCӴ^/2$SUQE|>$IB$LDUUEP(`FۈiȲL2D1')0*ɹbf˦M%˝`pp,nm'T S͜bm6hWs{V[8:*;{wYʼet=k; MӤ\.nEQ5kgpp[ىiAhjj4Mڰ9a ===b ^/AfϞM__twwSQQE//1 z Q2e iC.cٲeϸ )S-2K,)mMd( @A8v$Khȥ%"?cRaU%k=Ekmϣ,Gڥi̘1MJ~$ARv[VVV:… BG}V8\t-׿zq:Xa[ r9^oIv֛D"(dYt]rUXX,(]uL&fCe Ad2x7xTL&)AnJ*Wk7gC 9BlOf00u!zzzpy<. ʼn'PUz4D"N'hơCx<|\j--- f͚<3۷p8L[[M]]1m7|ѣGټy3NѣG9}4-sϲ}v~_|r===9s!ZZZ^ftJ1@sW39r_E[/~Ͽ/ԏRYZI8FonK)'sv,*E\;ssY^{,sPkFy]^pP>4M˱cjeiH$r𺕅w<[+[*KZۍ(  K.Ӽ[UUd2% P;z<Z[[9s&3gz#BwRNz(U*~E;[Al܂S~ ƏXE>iooG$&MDP`$IˤI&1k,0 Җִi`͚5R)>sy $I!x$+W2sL8nZl6TTTm6Ǐ3eBhh4Z]UugD ʥr&XXp1w]_XXzJ4NNRRVԮieS4,trht GTjm, .! ܟ3=gRtEy<Տ4(\đp`J&Oc ;IQ8E; DAhVtݍK=,KZXܦA*,[!Ο?O]]|>ώ;XnDDQX,ӧp6T*E ȑ#,XGx2e 1uj3g4n7|C{{;vOP@4vq\444cO_ vZLD4PUK.K~\n:Ć "ɠpKKKٽdz\z!P/\6.e0g|+N TyCr 3S\(DM^7*(߿{f :+>:p7 Bճsnw{-sD X/p䧺A,uuudYb~??v3m4D" an7o(ىIp~vEcc#x{UU6mHuuod)=; *mל]4I aQ"tD 罹fLv;+7y< ,,iell3XE~sd0֍)TQX\ڌ6Y]{ռ~8!AD.AC-t;~z;ZZ)\) vdLh/`Eي;c$I Y:>swk%g`08fI&a&>z\{}L#F&x^*++o+#b0RTxqI.^z:76f{gstzwшr9̞mm 첌:MȈx+^x$leeE"q>Ic=?7IDFgl]s7Qz{ɇBee`,PO!wΩ)taux5.#mGн:;b;P2 /nae|/-L A4Ϥ$<-ttveJqW[6Tl' S2I ,R7QL"pnl `Ce*m(aAP;T̂ڮ"M@UW1L-8 t:ŋ+d@lbc:߭ur:߉Z c=Me2u׼X" ju_rs cμohض ջ;6s6ϡ[:w۬ N{*+YNN 5 ,̂ 'Χ]RWa~+ )b8<ʁ"yobo2gǑWM(&vL d,{ηh |d =ϑJ "R&,-);QY؆U*f,*( ( %+##{<" pfW#{zp-t%j~KVWxi;h*1=ƱcGVGValm;tws pJyD3M#I :~Qm6.D>ML1LQ,8s'W# >D"ݻEE(&a$lO(PA]GDJ(29z?u*nw17`z@ݸğ|VWG޽tu/K*B%a$Z89$ (KCi"4IJ gβzcU}}PVP+cbҜO|YZ:  $cQTvZv7. {-#q)g%cTyhдNyQzbV-^Qy ;vͯ~} `N|R2.\glO;)r5v}KU/=A!`&+dAOP ]J2 4d$& e² h MüI4L`U TaP(E\,# |>Uŭd(ENep:1MȧRi\.uww . ..ŕ/X\35ʮ]r!?"TAVNfN{ױ)6LL 죪PTY=yN&Odf{gM\{\>!S" 0=iiHF^{>(ZВJǬe҆A/^D WњF'd: ]=l1LEz\(ҥd EAT$gLw:4| 6 ,ʬZQCGYTliF0*n.rx%Bu؊EzT)N''YDA@4 $aM}PF4aK_c~Y.t TlȂ@@i0Pm8 @HOy}w֮E뾯}lD`32GÖ\Цki4YXZo-^GU0 loޗ9zmHcnr}ttbztv21x+wn$i|z_#E `c%^7(EkNtc9:VڍMtUeIF\Ӱ &P(tEH:NIB7MNL`L=k x$nUE4RM+: 1IX^{>=Nk>ߗ,iWjE?pi.vPRPjO.vtS>|ԩ|y\Goak|| 9͆}gSJsS tj|= r/.||!'?gF+aWi T#bBrKfH3 Y#lr˼-ǫ{ϪUH-*L$IT6\)ڒm$IPA6d$S4LZh w{.*օ׈߂T9̱c̔g6w+#JGTd-~ slsp\HpBgSt4[H. =#BQj{чY\@(m>#QTZq ohF:pDH(%e_o IDATgrtҨ$TLkrmDwIRO05 +Vp&oDW_9f2 I|σCP!\(ڏi`-]OhN'iMm-Ğ6VmD9r|P*W[~1Oipr?V9RQAd7k}Zp5k'l(BP >ճG:NNq!y|^DHPjʢe|4GU'y5$IBEeGw=ʼqa}!oV溹i%Y!G jVV Ea1$Jkj->Q ~|xds4Zp|C$ 7چif#"C$a㡤itt:]3:"fTv?G1 +9>AddZMMh\:sXptSg@A5G$ ZΞI˰ݥ_2!r5owWR,'g=j̈́&$,1# " olo˵=+>LL6,7c` =ݝǏ'뇏PjLL#oSl$zڻ9:N` bQ"DkhNO:?tijeE۔ y0 >}ʯVSa&2m[J@(X^:Nll:ŴihkNڇi3^"HRd  Z]1uL F^ȓshjhV#$)M60TMvTU]9ɉɞ͒VhV+ }h*y49DNJR(e C gǾ9F}U==2 = &^hto~D郈}" ;!'ǎUϫ=Eizc];ΔLrm|uF"nO0a$8?Dž vf{go)J(271%7v*\P "-o^kXb̜9GH}AX/1͆GQacrbk~7i0Mr9>D4\."n7>~G!dM$i&݅ߑW)nbaM%KS26V3u6i7+okyz';_ ^{EUN'Nch!!Dg)86rEVP SFĢDBDdqFL 7m&-:qnGQ^ ̛†7şAgWBC}Ns5nS29:7yiK݂:T-}f?žc[ Mx=` ysi;Bzc[Ew EQd\|vr#}M*Ǔn3yRK> ֤{7 Dl/pi 8ߓ'ib.HTֹ^hI=Qckdx dy3de)t8s -#+EQ*B6OBBJ /^bpr,"Sbg5{O+XU ޹C/Kr* "'Z2ŊKq17 G.SPLjju#'?=M>`tDlP0MՄCane8<}z%né)b6lMM eq[8wfoVr3$I4MS>hx;,˖ڎ$ 7uw+p pwna"ڐs3gb&F@PCaqh14<{r-Yʾ>LO!;x%wm獚7vlңY-R?8HyYYP*檋Ǔ"]r?O1q9[nގ@2RK݆B/_1EO㟲¹|++e0j*^fj[k&6m=>(J),`2 QcJL&C*z()躎%˕EeTU%5x'^= 9o% SaY2?s];AQ]WW+%(,o|2x"=;;ʪ$ZG!3D 4-Xngeېs`Sd!Oo<[{Gy3W`sd3!oqQi͵e4MQ>Ϻ9B.) *vR[[n/Zyb !2 bU1;^vM2*źOPGvg 3|6Y-;깾>CI-Wv-pvΖ-l֪8e'S}S>=Ë](TUUv%͎ʛBu)"~`0tKh  +yvAjs(mF5Y*/olbb||ïsYwp ". 6.~ut+22`g"9|L[K\BFFTSE)pJEȬ "\%vBNR@6[\"ZLib&iL:/ma^LK4MDDlؐ=2x Iz^>,SSSض,8#(\.^/n޷Fw#)-<y.jvG?b`$b|_ }RetZӭ{Y[A^ῴ_uy}.;?f53n(W |7.BuQk0 IӔ!".]P(:. UUK%  <`#oooh%&B[A$^x]; *Ae*ֱ/kr:} 3blF]Y\h(a-̊F'{VüY i3ll! X7ʈ(P459[;هXL0BDž 0 tٰfLɤ3Hi QiTW;]r}/biBm#$@ ZZ1p1vp㍷혘/v8n{^;pt;ؘHWs׌Erl3?®EpUx P(.Baze`0(b KXUS]Czam/ߌ3F15oշDe{r2GJK*-mh3t2XE"D7Z6 eervq8%%՝<>7R \&(H'χiei$O8/6HMrfT޿L)O~RJfme/7[_ZvY %I 5H`m7k[[H-y=my!4=k}R'۫;%g=.quq{~ze뙭]RW}>ɐV |oe]f%iWN¦&fǽA/oɃփ,X\MVQ% aU%m51Zp#5}Ok9o L\agpU0 w?FCc]:^;u:N WDp>gZϚ~V%~W}^qG#\-;/|YC}?a,{{>gh^;UT uiXԜ~E_iX ufGN&\X۶gE&''+e&0Hb~bb^EqwC<4q%%ɳϲo+75AJ8:zb!=[{1FѷZ_6m==}z;}CxnF*W>K0 ;QUT FE͑"KTK5@!FMN2t0gy|H6{465K?phR"9 Ð|.G4- 1K`RXӴ6 ka>kz,b]:^}_$F_N~+ln\]ݾw٘X!ֽj;+'o`!wv9C7M֙8q}-Цg.К?.ᥛNfǯl(uusML9W?W{{qOFJLP)JR / )NNa_Fj D[)4\Ѕ  ͟OeHTWq尅lO;~t$:ǻﲾy}Bst={hpX]j7k9nZ4F^7y#+=BBfߙ}"~_NNN@,#Jfi\{Õ'sruW8o|$aR*fTnC fM\ m(iH@4Ч32DqVJ)t}I@N=_в*A9>X'ܵ`R͇xGphB;M_F>cMÚj$r7ʾ>NN666l&ݒ뀃ŃK~?7NZsE4<<>^j{i/am^e !ϭanCVYccch"f_uIC}}=˖-# u] twwc68a DJi˾}_|G4t]X,`ժUx_|%KoYfM`<ò,|߯|**sv [3<& `WVIԅ T #xūFR) 4 @hRH`Tb?dϿ>u<jW*nFQ>G}eQjQu'}\TL|CX +u5wːoKlj45.Ew}^8?X*̼gf8;vG,\FL̸E믿u]8t/?0 9|0l2"Xhwu|>}#EXd<Xů~+Z[[I& `N8AOO+Wd||{˗G}m,]ǏpB X,_G}{>~)˗/g<ڵG}O?G} 石j*>#6nH]#a~^lբ:((ÐX,VߓRV~7M<08A'''g5ڟ> i+U!~Mˊ G!%\L*_Il[,~q<ϣP(P,9~8c8p۶+#;dbb֬YC2d||e˖G~zG"L{{;CCC!$NA%+a%yU[A8i_ I)+Jmd똦Y!aiZe>)G4M#n4 qmv˲*A'!Ǐ0 ,Q@M>+WjEw-b```ҥK81u$kyBCiJfkb=mb (;L ]K$mo~jLW|:$rsyBҪU\o~{oyz,al˳?%ldS)M"?gEr?x/,ϓg_A+AY(:N]/ajkysöT*UZWbIjIDe?UUT ,׺1׃mCS,EzX C):o}biN s#".0ccAaxxx.|MX;ݙmEU[ =`v~l _4gίPp1;WRJ(lJoba[Z"b_ɓhS^L5Sd:-63K&uc|F3^+_1g\v6lKKN?b|>PJ`탬 ׀eԒŽĴOb($LL4u_x]-JSSU!~rIhUQ Oz;L0)lSʗPmZk[фVrT٩*`5f9u1-FĊ {Z~7.'s'E%]V&略AC,ei vvy/'Xr9$W57 oyK8z OYи:JJqNh6 <K0 S )4̷m1ݲ,tIBߟ[sRKa,W\ Am,FRR XHP.ce(E }:,>#P8RJ/xp 4 C Acc.Bf14Ȉ Xvf_BTDYBi JiEHQ+" JK'm|WSnR2|*P@!(Վ"ep's.Nq3-擡O?5X^`4`}h!.. ǡ!z^mX^O!&U?ߒ rwe<ԭ7A.ftlJ_.sڿB0:>ܟNsT4u]  L!p)%caHj¹үJ1< GJgrBh >-ɾR i!m0$QFpp.D"q^oz1'ɹ !ĬiN5 ugT5giIHe_LPNJM "%,,Z pC34#Vbi9#.u,Z󧸞E恳dA06:;_U.ƽ{?m5n%_}Gʺs!d`o&!I{i'"D}b[VW(LJd3lxFm.;fۢU3Ӥ~J䓈!SA5͋`T3jk[[ֵi$c<x@){MSDžBo_r.ANpz a(H$bmxGKs Tr*sBv+kP" Se͚-@35VB [utw~<1_|wL bJ.ɕe]9~(M~XӾyY4o|5W7oa\Y]`OJAW"JEJ$>#1<H.Z4w\sP=@ss(ͬUBSʐETss xdTH^){޼y(86rY~mph1УDDz%>>RHB"t*@ahrc.@LFQ4q@uLjºup0qMpUWE:Z6Zk$$Mf(XSwe^.0`89ra Zb-,/醧Ijf p\j{qb[ZEp/D:qֳR"93|󷬹>,aTԵ*&b<=)~\c4Mt?EЦ5hTȧSjJ|'9Q<)zHU YgsܘrBu].kaχrN;җ~W<-GI(ڍ)QAtt/a%K$0[FHLwcy<ãƫXU\sغlƦd Y׸Bwq4*8Z<ʘ7މx-,H-aa22hBFunH$L|P~{Za\\%TLݹEHZ+L!CjPEP[BZɓ$Ƶ[Y~(Bvb %԰B⩨2C*,PQS:: Q>::;" ث~$pS&mɦ'IibZ'Z`{v4q[M@Šy$*B099Y)8wG/-nVEbQB HF+ dmt)HH,<Dqӄ=xC9(4Hu EHQD!cKYX!.W%z>R*jg5Ť;H7}5B40=G\͇gC^{-[H)bZ'[O0EȾ}NHB XYFQsK溕;#q3i]S!mLoӼŒBNNV[7H놄)T)^lzq֖psZ.8%Qk%A:Ljb5dYc,,ÊYĈJ+RGfڄ6l,Ͽ M:(ionTfE#>= 8cb1AZVA l箝 |5L%(\X)uHnH00*E2ߔNO YY}$>Ut /JМOBli_3bIHۯѣH)I$8d[Z[[NuQM `)L!=x*>Z5㍾7ܾSĴ67.J-쎔č9FN:5bVyy<~Dc]WRߏy3&*f.ZZZ}7|b6,lkq #:;3hQ:gF-`뙭d,v>;*Y ^1VTE!Z+#C@hB$m땐`b7%*m ^RV^{3\oSB v?&.zHM~S "s+M!NGP쓧He"'h*iD>eJ@0UҮEHdTq9Z<jƖ|V +] ߊgD X_2o828ށoOIIl=F/.[;<1C"y!Wax:e'm6Uwv}UCCC,]˲*}oux-?޿?as#>8{s_>fJ[Oo>>ky{mԤ⥶H3 @0or~w,JS1p?hnỵ%B[G̍" )$_|S^4B5O}{XX\<ތYg3='#LAX+V0B!Gj^7g+4 hhhm' C8ϟqTګ]lu#.!a"`Xl f PB(d KEE+8GLDHLP.H$dJzqlۮbBЌ3*/F!u.U vf<]@bſ$h?1 ,:Nu\A.~Md 34< .MȖ ETx,K@A{"̇/(3́\up()' ±a 6a>$V9|0]svy] iR+^W{vpCQʗ5k+OE[,\}k(Sf"66IB2~' _ES66h KZՕ|38:eZAT)hYLތz=ER'ԙ tf)_*AtIGz92&$eg&%|Na8 06>GGrp /|Zݱ]Cl3kNeG.0}fݬXnR sQ. `bb> C^y8WfhhE!sI `ɒ%A@2?d``?EqwymHӼ;gKqm7Oj~/ޣq3\{b3 9|AFQ'@<}KKUIV|焢d,M.JW iCΝ|X QJaCE01GL,,b"Օ&5]%`$S:d!O]\PC'J)0D35#)(+IUP5 R9{] mۘYY?dO"t0WHGl<ıUi0jo6Um+ƦV%@D=yCFSxW>qކ%Qbmtnn9~ A)Șޥkx|KZj7BᲾJX,V]jgp|RXPpDd,Ir-YLaV@mN|Ȇ hBC:k?>4=r& )%saN<ŋq`ow.رcXӧO3<<ڵkcddub1o[z!N<$w^N>ͦMxy裏7R,9}4K.e߾}KG?b͚5۷ 6P*8u ,ȑ#ZjƾӒR.R U{<"ay+$u~#GXй|# S!Q B/4$h2x|dW]zvY/ѷ[ԯɯT_ˆYb10 李'OhnnÇ{n6oL" ?4199I}}=aiRJlۮ,r0d׮],[1\ץP(0|2 g||L&eUwV⮻W_eHT"͒*Jǎc޽ASS $322k?83"/)ee|+V IDATog{_<WU:}L;?±c<[,M٦OPEdAԧ p=I1?=--$k—Bʐ 019A(m[BHFYCcneeuv?vmlaȼGݯ(y%ʳڳ,/d.&%mK8v>~ 9c>wEM<" 111O~J:~v &?㭯M?g睗J(?6+0wClj V{3(5+"6kƙ? 1U=vd3ksjLִ· JGQA 4 W$d⚶+$K$H`f%+$QrBQEEջ8ԇ'zDȥRbMj0pOr Bf[b]B٥L[_lB 8H$> pwn*ZCClJpEC}y xw]:k~L1$u:Ur0+aXt)ͣX, es#twnK[Mu۶I$ZU @x[/G p'JD T,u!(]!b҆Gh϶ nCYQ?d PE[]8%Pb?%YAbzK1t4VYA]FPb26X!"xJ*<Ǚ(L4&eiY`- "ڜbh^nɖ'IjQ7cdx3V׬3-T^i۸yf7ds9RS۶τG *4 A'įy=j~l< "*9X<Ȏ<ȟ=+9ϴY/&`o^լҶ컮1)$ 2s7Ҥ7ѕbt]CWd"$oOqx0oa1 +4T*E*껯bA+enABDQ08r*wAMyM#}%JR>~J|wSaH$1#[Bŏfj8C+'! GDɟ#YSy/Ɋ,Hi iBpY$HSOUJ9C#021^  & zKoQKmd )-EƝ;k숈öXhX_WϼʦMQR:Z6w|/y-iuɓy}RSW 95nu0F 4"K00Sc_{DMuɸƕKy_%yxu&h(JIQJiÐ.u|O)c1|}t[OϯMb/Ztί##==y?aI݅r*Pt$v CyMy.RCg+z[+RWTP*rP8?¨?Aڃv]i1oYmel6K jDΜ9S$|0,RD&axxZ&&&*ax0$4&xpֹ~G)qYA@IJl])cI:A@RӰ4 JQ5e A1 q$2ML߻zQty}:^j~g,ɯ92q;;=~;Ӽwu^}wN$U$%2eI(;ԲI4Mjg2m&IfɤԙL2N_gIգqfK6KDJ₍p/Ë{  p 8w}{0I8wY.LF8:J!˓,bv=)lm,HE},B-kyޮ][*@")"!7[dZbX҈I miD"BGPEnSQ/bJBV?` |][~ό}g#L'OrjOןß`xl|oҷGzWX6+fOU(P:|x[ Īn0\`Z$ɚcR쭬&.-y3XG-6]QWu>/.c Cmҟs>1v a[6|mkdK?:2m:-m<~?#C#%]}dKXu822bN.%x #; ;cg~niVNsͭ(3ve+{dS9ws*G$Pq*6)Q)hEkn]po5YSbų3ϲo?Uʁhr/{S_;)Ҧ^J鞧4Mg0w͎h_*<̈7p|8G#һ(OUW}$S٦;@&DwD|~6ކ$̈y1;Ʒ9=w}>~S+vSUűtAs>:υ |}E?!9YhQ~Q*nxR(x'{yފb `ҟm9zf`*HW_IYUl8J_eIK066A }yKO98,--zRj5\EkM7uջƩ8 _azeR;o~~>_Ez9G8ͅFJ M.R=N腫pP"VZ"OJB$IɬeY[LH{)a'j0\qoGo. |䐒Scyk-_|2ZeҚ|奯`X,[[Mb#>S$,&=FٗC<4]mⴻf?e֟wbҙ|r&2p1tw +wξ;)4 X3rÜ3ǟMYbA\ [Ԋ5.Kn&nĸ{LӜ' ⅋/p}; C'RDIΰ^brRRb##ayF#X~T*4M(¶,ٳ۲z.IP}$L2FSE$&M5 JlWm훈ʺe/ha!??ø!}VD.L`*?"OL?ѓ ~)qlY,jUZqSSsßщKr!p;悂d&xH^DknV]nZ|p* )-%jDh.5ɚp~>G=ֲ 7` }}TU˃g{ۃMigylKPi:M^lH9+;uQ5áHPݲb'b":# rr;^,ޔx8^nrjJqX*_/"jmdcҦM|5i4ޟ)Rԧh̰+ A|ݻ}Aք4^yuflޱk+p}}SO# {.jfi߈ೣɲ )8+TX;_b>շn 骮nWN7^HT8o E4ܜ=p1W@a!~?~?寷~; 4 $}"V"kdX8d3]L(@E*5VB$i=Ezl1vTΦȂD$>~mG ~Ϧkǯ3 K/Оjs_>Fry'f \.ͱc&u5kηl59gY.oRa0|h8aavvRDO2,mݻ7bR MΛ#%%##$Mɷst׷*BJ4o?gr`O>[y: wpa 300d¥˥ ^ {yg/^~(h}z)Wc4 j^odvW o`?<˅DWWt:o2ꌞyT9KEʅ?-x`ڣm^j7g '2J+:nt:n7d8ff<ν^>1  xy^/W22$U)/O\:ob"S^KCWozUĒX"!l=3#/;ʱȫmȉ& H/.ձGŭnoR{K ֚͊HF%%"^kC{!E]ė~hI._sZH|u"`RNWa i -5ː7m>ϗ ك,ɇw`@kG5~v:Oɘ)~s<9k닯ҳT <9$C<`i ^ssZ-9:9qAvwpPIg0|eYض:FAr+|3&*THO@'}>~_Bl.MںM ?J߳:˯~F\˲H'Qs"i,hxhC_dmrw-x֠5.:m![r֎έ*57ռ2 ǝyJMOY Nc.pӧƥ78`X*bl&{Ѷڼ~ XbT2(zU&Je!# ZAoJm% T{B*OiHJD-OzU`_t/_ I5; 7\ VBєM̦$J~]$<#Z Ί|b?$DA-W1OL]:ӍiMbS% c>MHKd .d.AvsMIDATM+f:<=4<>~o d;<.K/tG$ %ThԦ/o4ָpM2h~3g( =*0Ϝ/v6b~π=8#С:ӧ{tл쓤lcvR31:Zڧ(%}k0! RN&M6'!Y'-x媜Fӱ;ӼRޔnYm*f;cul0l>_rc[6.9BDR5\~ ,F>~wIT{]Ww9iIN#3m180,``G")*0 )Z&%]ByEGv@CU J(j.+pC}^Y8ҤI5j[<0 2??Ϟ={XZZh`YVO$&IB\F)E\fxx\ЩHYJD@AEyjZͫxM[Y[T@DBJI$8Nԭ4 XԋT19ẹ抪a,\FF"b̓|xlc}%knuL6`0 FIɅG)HfdσFѥ&ŭ9?FdY.:Igqj5\\b-D4@KRr/ӭ@vidI܃ ~qyb@@iV3> -wDޚ&166HZi||<hS٠`0.+ng[F0inXi}T5<"r&q.f!C&3S8LaYZ,om.e"G~FO-W/|a{oGd4Ű7\{~۱/I`ikMG(Bb\2o{64;xǩE57e mr `0|p`(Bfu_?&zn_kw{/hM;i^:OaXDXi^j11EūW4NKBm)f񧄢[ڥ/W63-i/飤J,kno$XZZBk8q t]RN뺸3. ?hC2`0~@`&rz#U]A8"))(-J(%hB RRQ>Yn4H0{j4MWn۶b/Gn%QDĝdI6 U"F|vhB-ڴ)SޔJEJ*R ʿۼ̦hb G;en3nHp6_x3,!SIGԆ5_gE;rB>ρeW`0 F^\Jmur,, EHKPY3@,| qs{&$ 1P(dF3 s{oף WGAJj};Ҋ`]bf`0 #W>,ք,DIHD. .b,F (um!ʭ],P=uu)W^K^PJ2^XSx,q3b0 Wወ(QzJNKMGwuLJJ CXںt G$dYm(zY =SnMmۤiydYV +mH_ !!eU^ի C~_rOD^2hAf[A$""hNAP,m)io W ě`0 #8 Kl,tM.=98XڢƦX&Ik ,9_;͛[T#.x6HH̙K,e G;G`0 J$!tTه$giηO_y"4Ii^G:~ #dmkS3 ` Ph(>in2`'A \\Y` ={OR5 `0U`ޤV'FFQ[/h `0`p`0+&`0 @`0 `0 # `0 F `0>;8bÇ3d;!I86'`0 qTUs& `0>d-``0 0`0 ZeنB RJ91`0 ÆBҥKJ v*H-}~ib۷V\yY@P0k9i4[)_4 ,ؑAKܹPJaYVOZ- NC$EmS*6 veǿq$IBZKZ-{=(`rr!Ξ=K$8áCnB;p5c~~@`0 [GkR 5Qq)<0 9wwߚ֚-=VѠP(P.WUVǡ݂ ԩSLMMԠX,qy|6mkLMM111A\tr޽{{ܹs8pv8 a!Yeo&Ķmij`0 +dxqrQjٳp̹vK #'{U.*N}陸R244JOMMqe8F"Ν;GV[q̗e>O3p *J>O?4jSN1;;ˣ>JӔ'xG}{lS@`0 ;qYqy0D)E=7K#\n yl>,BYPJe9 "IZJ$I6af7|AO=B~~ ]v/.j_~cǎÇ9x B(ꀊ`06%|یj ٷozY BOpLTw~`nZ#4%]ZVE4zCkETPѣG9y$O>$?c?]wFw$O=FqclllB+4 p\xzE^}FG8p@oν;8ocff8}(عs';w*ݾ?i("c ```$drruGT<8q_|zΑ#GV3gxyx}YSOG~l `1[33q#|,preYGNCX 3;;ۛZXOl1vfCk X#({СCضoxFjkn0 K"k.attBF o-3)%###[z~z qB޽R/,0 j}> [qw3F `0-2벰p2Bk}Mbp]ER˥K='m8Io#h=r+ȸ\ UW 4M4}92G)(lZ @`0 ?XJkiiZk>l6?(˛:'ݯkZV5 @B^+n5`0 e(4 `ضh[ 0句ZY~A4kF `0l-ֲ dY*,,ZV6`0 -5z*uݷ,,Voh0 }_"&˷lcz[) w3"`0 a{ ARqqw3`0 '5^Og0 =?cci6mIENDB`qpwgraph-0.8.2/src/images/PaxHeaders/viewZoomRange.png0000644000000000000000000000013214762602507017777 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewZoomRange.png0000644000175000001440000000254314762602507017773 0ustar00rncbcusersPNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8_LgZ HYD((a2()n. ܲ?nsQ?Lي&&ɶ /0: *uln&U.YI:pX&-(HKK}׌9Mv}: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Ś Axف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.8.2/src/images/PaxHeaders/viewColors.png0000644000000000000000000000013214762602507017337 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewColors.png0000644000175000001440000000256114762602507017333 0ustar00rncbcusersPNG  IHDRĴl;8IDAT8ˍKl\g;sOq&NBT4"  RJn`- "bX TX!RHӤ-A}ФMMcG<3;޹ JZHG,;g.qy1:S7Lu)v'$L'|m'|4 w*N9?x\=JƮKi2[υ%)L./S  CShK0GO#Lۓ_މ ꥎ1#ϲkNLQ 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.8.2/src/images/PaxHeaders/itemAlsamidi.png0000644000000000000000000000013214762602507017605 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemAlsamidi.png0000644000175000001440000000177714762602507017611 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/qpwgraph.svg0000644000000000000000000000013214762602507017047 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/qpwgraph.svg0000644000175000001440000002407014762602507017042 0ustar00rncbcusers qpwgraph-0.8.2/src/images/PaxHeaders/viewZoomOut.png0000644000000000000000000000013214762602507017512 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewZoomOut.png0000644000175000001440000000251614762602507017506 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/viewCenter.png0000644000000000000000000000013214762602507017316 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewCenter.png0000644000175000001440000000234714762602507017314 0ustar00rncbcusersPNG  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;8D|?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.8.2/src/images/PaxHeaders/itemActivate.png0000644000000000000000000000013214762602507017622 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemActivate.png0000644000175000001440000000112114762602507017605 0ustar00rncbcusersPNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8Ք˫RQڃBäE  5n,i\3h"5> 8nAXhM⌚ ۃǽjn`OZwYǂㆵFtF'~|~f-Z&yhe@ wmXT,|T*L)edjt/Vf خT*ևZkv? 8 <3""(wBމ&o(kIENDB`qpwgraph-0.8.2/src/images/PaxHeaders/itemPin.png0000644000000000000000000000013214762602507016610 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemPin.png0000644000175000001440000000175214762602507016605 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/viewZoomFit.png0000644000000000000000000000013214762602507017465 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewZoomFit.png0000644000175000001440000000265314762602507017463 0ustar00rncbcusersPNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<(IDAT8]lSe=l~lNd`|Š^8nt*w!`&jØfv.ng c[ctkO{NeNM|7{<: @ 8F1`Hi?uJ`5k^ݵkW͖-[V\a}?u\w8'X%hϞEQtpX,F2DErsshoo}g_OMM~?T>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|,Sk4bxj5uttd2ZT~ͦ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^;ky2b~'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.8.2/src/images/PaxHeaders/itemFind.png0000644000000000000000000000013214762602507016742 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemFind.png0000644000175000001440000000252314762602507016734 0ustar00rncbcusersPNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8]LSgƟ=ZJiRbF4t8,N 7\#1Kpɼ.[xX51).jYF0ActإJs9ywa1| \'?`!" 9"PT&iaR P[ZZ~e!D`<zJL48`9)(( ˲}E]I,?zqJ)3 0ƄQQYn9 !RSd2l6 3w2,,))LF[ZZ`$!D`2 "(L>??ò`0X;=4 X,@So1q&LDBzA_v!ڵk+ȕ555G庎P(!$97VY,Cu'<(grfݮt:&$_93P]QQڐLŊ9u~?YXXڹsDwwϣd2IPB!EQr|I$reKY,=''QJj5vء,,,LG"7583efEQċ/R6MaMӄeQuAE.I@}vjo04Meee^]s94`zz$)1&b1$I0 @$Iuؤ[*Jr[[իWt: \VVWW+VUU7?nଯ4rr*kFL__O6\4W:޳wÇ[hwwu:jii*:͛7M744gmx.pfr|bqH  jllfhTu\hPUpγ}PD8_x022ff,~b߿=ǽe9wp >ɓ'c##QmjjOL<6.]N>q8#pX,s-&dbʦXlmx )1;vݻwjbxxukk~[xCzSVzBldmmm hb1H$4uRBY!jDU@`}Gq3/ oNEb6y{ggԮ]~@ަofMlqa6uIENDB`qpwgraph-0.8.2/src/images/PaxHeaders/itemDisconnect.png0000644000000000000000000000013214762602507020153 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemDisconnect.png0000644000175000001440000000221214762602507020140 0ustar00rncbcusersPNG  IHDRĴl;sBIT|d pHYs & &Q3tEXtSoftwarewww.inkscape.org<IDAT8MHgVt76kG@@i$Kk*ٖ^ZD"9xh7 n)E]BaխZҭhcT4L4S9/ 7ϼýzK̚P^25iO4a&C`dc$x(DxEjm BGڇL0L-``~~^ؘܼ XLqqE`"׀ꮮ/r999t:}DnޱvIk(F ` xX{MMM~cmm!5dRڈb?hGZ" 555J"Pߟ>|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.8.2/src/images/PaxHeaders/itemPatchbay.png0000644000000000000000000000013214762602507017615 xustar0030 mtime=1741358407.416986867 30 atime=1741358407.416986867 30 ctime=1741358407.416986867 qpwgraph-0.8.2/src/images/itemPatchbay.png0000644000175000001440000000346314762602507017613 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/viewZoomTool.png0000644000000000000000000000013214762602507017660 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewZoomTool.png0000644000175000001440000000237014762602507017652 0ustar00rncbcusersPNG  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.8.2/src/images/PaxHeaders/viewZoomReset.png0000644000000000000000000000013214762602507020025 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewZoomReset.png0000644000175000001440000000250414762602507020016 0ustar00rncbcusersPNG  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_^{"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^>>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.8.2/src/images/PaxHeaders/viewThumbview.png0000644000000000000000000000013214762602507020050 xustar0030 mtime=1741358407.417265981 30 atime=1741358407.417265981 30 ctime=1741358407.417265981 qpwgraph-0.8.2/src/images/viewThumbview.png0000644000175000001440000000130414762602507020036 0ustar00rncbcusersPNG  IHDRĴl; pHYs & &Q3tEXtSoftwarewww.inkscape.org<QIDAT8NQƿs쬗XI;FPPvXPhHOOOKT4M 7vM7Uh4:*Jd2NNN8::boo/ "odZrl6+aH"Uy<uu~^] ۨQ: -~nmmҀEZPdttp{2D8(J< UUl5{ 9cuN |aUY \] 1y@6wP؋TIENDB`qpwgraph-0.8.2/src/PaxHeaders/qpwgraph_options.ui0000644000000000000000000000013214762602507017173 xustar0030 mtime=1741358407.418265975 30 atime=1741358407.418265975 30 ctime=1741358407.418265975 qpwgraph-0.8.2/src/qpwgraph_options.ui0000644000175000001440000003132714762602507017171 0ustar00rncbcusers rncbc aka Rui Nuno Capela qpwgraph - A PipeWire Graph Qt GUI Interface Copyright (C) 2021-2024, 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_options 0 0 240 120 Options :/images/qpwgraph.svg true 480 240 0 General 50 false Whether to enable the system tray icon &Enable system tray icon Qt::Horizontal QSizePolicy::Minimum 20 20 Qt::Horizontal QSizePolicy::Expanding 20 20 50 false Whether to show system tray message on main window close Show &system tray message on close 50 false Whether to start minimized to system tray Start &minimized to system tray Qt::Vertical QSizePolicy::Expanding 20 8 50 false Whether to show activated patchbay message on application quit Show activated &patchbay message on quit Qt::Vertical QSizePolicy::Expanding 20 8 Qt::Vertical QSizePolicy::Expanding 20 20 50 false Whether to enable ALSA MIDI support Enable &ALSA MIDI support Filter 50 false Whether to filter / hide nodes from the graph Enable filter / hide nodes 50 false Node name (regex) true 72 24 50 false Add node name (regex) to filter / hide list &Add Qt::ToolButtonTextBesideIcon 320 80 50 false Node name (regex) filter / hide list QAbstractItemView::SelectRows true 72 24 50 false Remove node name (regex) from filter / hide list &Remove Qt::ToolButtonTextBesideIcon Qt::Vertical 20 8 72 24 50 false Clear filter / hide list &Clear Qt::ToolButtonTextBesideIcon Qt::Vertical QSizePolicy::Expanding 20 8 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok OptionsTabWidget SystemTrayEnabledCheckBox SystemTrayQueryCloseCheckBox SystemTrayStartMinimizedCheckBox PatchbayQueryQuitCheckBox AlsaMidiEnabledCheckBox FilterNodesEnabledCheckBox FilterNodesNameComboBox FilterNodesAddToolButton FilterNodesListWidget FilterNodesRemoveToolButton FilterNodesClearToolButton DialogButtonBox qpwgraph-0.8.2/src/PaxHeaders/config.h.cmake0000644000000000000000000000012714762602507015731 xustar0029 mtime=1741358407.41690441 29 atime=1741358407.41690441 29 ctime=1741358407.41690441 qpwgraph-0.8.2/src/config.h.cmake0000644000175000001440000000177614762602507015730 0ustar00rncbcusers#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 to the copyright of this package. */ #cmakedefine PROJECT_COPYRIGHT "@PROJECT_COPYRIGHT@" /* Define to the domain of this package. */ #cmakedefine PROJECT_DOMAIN "@PROJECT_DOMAIN@" /* 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.8.2/src/PaxHeaders/qpwgraph_command.cpp0000644000000000000000000000013214762602507017263 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_command.cpp0000644000175000001440000001570514762602507017263 0ustar00rncbcusers// qpwgraph_command.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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; } } } } canvas->emitChanged(); 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.8.2/src/PaxHeaders/qpwgraph_systray.cpp0000644000000000000000000000013214762602507017363 xustar0030 mtime=1741358407.419265969 30 atime=1741358407.419265969 30 ctime=1741358407.419265969 qpwgraph-0.8.2/src/qpwgraph_systray.cpp0000644000175000001440000001031514762602507017353 0ustar00rncbcusers// qpwgraph_systray.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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_main.h" #include "qpwgraph_config.h" #include #include #include #include //---------------------------------------------------------------------------- // qpwgraph_systray -- Custom system tray icon. // Constructor. qpwgraph_systray::qpwgraph_systray ( qpwgraph_main *main ) : QSystemTrayIcon(main), m_main(main) { // Set things as inherited... #if QT_VERSION < QT_VERSION_CHECK(6, 1, 0) QSystemTrayIcon::setIcon(QIcon(":/images/qpwgraph.png")); #else QSystemTrayIcon::setIcon(m_main->windowIcon().pixmap(32, 32)); #endif QSystemTrayIcon::setToolTip(m_main->windowTitle()); m_presets = m_menu.addMenu(tr("Presets")); m_menu.addSeparator(); m_show = m_menu.addAction(tr("Show/Hide"), this, SLOT(showHide())); m_quit = m_menu.addAction(tr("Quit"), m_main, SLOT(closeQuit())); QSystemTrayIcon::setContextMenu(&m_menu); QObject::connect(this, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(activated(QSystemTrayIcon::ActivationReason))); QObject::connect(m_presets, SIGNAL(aboutToShow()), SLOT(updatePatchbayPresets())); QObject::connect(m_presets, SIGNAL(triggered(QAction *)), SLOT(patchbayPresetTriggered(QAction *))); QSystemTrayIcon::show(); } // Destructor. qpwgraph_systray::~qpwgraph_systray (void) { clearPatchbayPresets(); } // Update context menu. void qpwgraph_systray::updateContextMenu (void) { if (m_main->isVisible() && !m_main->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_main->isVisible() && !m_main->isMinimized()) { // Hide away from sight, totally... m_main->hide(); } else { // Show normally. m_main->showNormal(); m_main->raise(); m_main->activateWindow(); } } // Handle patchbay presets menu actions. void qpwgraph_systray::patchbayPresetTriggered ( QAction *action ) { const int index = m_presets->actions().indexOf(action); if (index >= 0) emit patchbayPresetChanged(index); } // Rebuild the patchbay presets menu. void qpwgraph_systray::updatePatchbayPresets (void) { clearPatchbayPresets(); const QString& patchbay_path = m_main->patchbayPath(); const QStringList& paths = m_main->config()->patchbayRecentFiles(); foreach (const QString& path, paths) { const QString& name = QFileInfo(path).completeBaseName(); const bool is_selected = (path == patchbay_path); QAction *action = new QAction(name); action->setCheckable(true); action->setChecked(is_selected); m_presets->addAction(action); } } // Destroy the patchbay presets menu. void qpwgraph_systray::clearPatchbayPresets (void) { QListIterator iter(m_presets->actions()); while (iter.hasNext()) { QAction *action = iter.next(); m_presets->removeAction(action); delete action; } m_presets->clear(); } #endif // CONFIG_SYSTEM_TRAY // end of qpwgraph_systray.cpp qpwgraph-0.8.2/src/PaxHeaders/CMakeLists.txt0000644000000000000000000000012714762602507015774 xustar0029 mtime=1741358407.41670067 29 atime=1741358407.41670067 29 ctime=1741358407.41670067 qpwgraph-0.8.2/src/CMakeLists.txt0000644000175000001440000000702514762602507015764 0ustar00rncbcusers# 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_patchman.h qpwgraph_systray.h qpwgraph_thumb.h qpwgraph_main.h qpwgraph_options.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_patchman.cpp qpwgraph_systray.cpp qpwgraph_thumb.cpp qpwgraph_main.cpp qpwgraph_options.cpp ) set (FORMS qpwgraph_main.ui qpwgraph_options.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 20 CXX_STANDARD_REQUIRED TRUE ) 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.8.2/src/PaxHeaders/qpwgraph_config.cpp0000644000000000000000000000013214762602507017112 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph_config.cpp0000644000175000001440000004220114762602507017101 0ustar00rncbcusers// qpwgraph_config.cpp // /**************************************************************************** Copyright (C) 2021-2024, 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 #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 *ViewThumbviewKey = "/Thumbview"; 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"; static const char *PatchbayQueryQuitKey = "/QueryQuit"; #ifdef CONFIG_SYSTEM_TRAY static const char *SystemTrayGroup = "/SystemTray"; static const char *SystemTrayEnabledKey = "/Enabled"; static const char *SystemTrayQueryCloseKey = "/QueryClose"; static const char *SystemTrayStartMinimizedKey = "/StartMinimized"; #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"; static const char *FilterNodesGroup = "/FilterNodes"; static const char *FilterNodesEnabledKey = "/Enabled"; static const char *FilterNodesListKey = "/List"; static const char *HistoryGroup = "/History"; // Legacy main-form class renaming support (> v0.7.7) #define LEGACY_MAIN_FORM 1 #ifdef LEGACY_MAIN_FORM static const char *LegacyName = "form"; static const char *ModernName = "main"; #endif //---------------------------------------------------------------------------- // 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_thumbview(0), 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_patchbay_queryquit(false), m_systray_queryclose(false), m_systray_enabled(true), m_alsaseq_enabled(true), m_start_minimized(false), m_filter_enabled(false), m_filter_dirty(false) { } 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::setThumbview ( int thumbview ) { m_thumbview = thumbview; } int qpwgraph_config::thumbview (void) const { return m_thumbview; } 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::setPatchbayQueryQuit ( bool query_quit ) { m_patchbay_queryquit = query_quit; } bool qpwgraph_config::isPatchbayQueryQuit (void) const { return m_patchbay_queryquit; } void qpwgraph_config::setSystemTrayQueryClose ( bool query_close ) { m_systray_queryclose = query_close; } bool qpwgraph_config::isSystemTrayQueryClose (void) const { return m_systray_queryclose; } 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::setStartMinimized ( bool start_minimized ) { m_start_minimized = start_minimized; } bool qpwgraph_config::isStartMinimized (void) const { return m_start_minimized; } void qpwgraph_config::setFilterNodesEnabled ( bool enabled ) { m_filter_enabled = enabled; } bool qpwgraph_config::isFilterNodesEnabled (void) const { return m_filter_enabled; } void qpwgraph_config::setFilterNodesList ( const QStringList& nodes ) { m_filter_nodes = nodes; } const QStringList& qpwgraph_config::filterNodesList (void) const { return m_filter_nodes; } void qpwgraph_config::setFilterNodesDirty ( bool dirty ) { m_filter_dirty = dirty; } bool qpwgraph_config::isFilterNodesDirty (void) const { return m_filter_dirty; } 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; m_settings->beginGroup(FilterNodesGroup); m_filter_enabled = m_settings->value(FilterNodesEnabledKey, false).toBool(); m_filter_nodes = m_settings->value(FilterNodesListKey).toStringList(); m_settings->endGroup(); #ifdef CONFIG_SYSTEM_TRAY m_settings->beginGroup(SystemTrayGroup); m_systray_enabled = m_settings->value(SystemTrayEnabledKey, true).toBool(); m_systray_queryclose = m_settings->value(SystemTrayQueryCloseKey, true).toBool(); m_start_minimized = m_settings->value(SystemTrayStartMinimizedKey, false).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_patchbay_queryquit = m_settings->value(PatchbayQueryQuitKey, true).toBool(); 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_thumbview = m_settings->value(ViewThumbviewKey, 0).toInt(); 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); #ifdef LEGACY_MAIN_FORM QString sGeometryKey = '/' + widget->objectName(); QByteArray geometry_state = m_settings->value(sGeometryKey).toByteArray(); if (geometry_state.isEmpty() || geometry_state.isNull()) { sGeometryKey.replace(ModernName, LegacyName); geometry_state = m_settings->value(sGeometryKey).toByteArray(); if (!geometry_state.isEmpty() && !geometry_state.isNull()) m_settings->remove(sGeometryKey); } #else const QByteArray& geometry_state = m_settings->value('/' + widget->objectName()).toByteArray(); #endif m_settings->endGroup(); if (geometry_state.isEmpty() || geometry_state.isNull()) return false; widget->restoreGeometry(geometry_state); m_settings->beginGroup(LayoutGroup); #ifdef LEGACY_MAIN_FORM QString sLayoutKey = '/' + widget->objectName(); QByteArray layout_state = m_settings->value(sLayoutKey).toByteArray(); if (layout_state.isEmpty() || layout_state.isNull()) { sLayoutKey.replace(ModernName, LegacyName); layout_state = m_settings->value(sLayoutKey).toByteArray(); if (!layout_state.isEmpty() && !layout_state.isNull()) m_settings->remove(sLayoutKey); } #else const QByteArray& layout_state = m_settings->value('/' + widget->objectName()).toByteArray(); #endif 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; m_settings->beginGroup(FilterNodesGroup); m_settings->setValue(FilterNodesEnabledKey, m_filter_enabled); m_settings->setValue(FilterNodesListKey, m_filter_nodes); m_settings->endGroup(); #ifdef CONFIG_SYSTEM_TRAY m_settings->beginGroup(SystemTrayGroup); m_settings->setValue(SystemTrayEnabledKey, m_systray_enabled); m_settings->setValue(SystemTrayQueryCloseKey, m_systray_queryclose); m_settings->setValue(SystemTrayStartMinimizedKey, m_start_minimized); 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->setValue(PatchbayQueryQuitKey, m_patchbay_queryquit); 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(ViewThumbviewKey, m_thumbview); 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; } // Combo box history persistence helpers. // void qpwgraph_config::loadComboBoxHistory ( QComboBox *cbox, int nlimit ) { m_settings->beginGroup(HistoryGroup); if (m_settings->childKeys().count() > 0) { const QStringList& items = m_settings->value('/' + cbox->objectName()).toStringList(); if (!items.isEmpty()) { const bool block_signals = cbox->blockSignals(true); cbox->setUpdatesEnabled(false); cbox->setDuplicatesEnabled(false); cbox->clear(); cbox->addItems(items); cbox->setUpdatesEnabled(true); cbox->blockSignals(block_signals); } } m_settings->endGroup(); } void qpwgraph_config::saveComboBoxHistory ( QComboBox *cbox, int nlimit ) { int ncount = cbox->count(); if (ncount > nlimit) ncount = nlimit; QStringList items; for (int i = 0; i < ncount; ++i) items.append(cbox->itemText(i)); m_settings->beginGroup(HistoryGroup); m_settings->setValue('/' + cbox->objectName(), items); m_settings->endGroup(); } // end of qpwgraph_config.cpp qpwgraph-0.8.2/src/PaxHeaders/qpwgraph.h0000644000000000000000000000013214762602507015232 xustar0030 mtime=1741358407.418115141 30 atime=1741358407.418115141 30 ctime=1741358407.418115141 qpwgraph-0.8.2/src/qpwgraph.h0000644000175000001440000000550314762602507015225 0ustar00rncbcusers// qpwgraph.h // /**************************************************************************** Copyright (C) 2021-2025, 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