qpwgraph-1.0.3/PaxHeaders/CMakeLists.txt0000644000000000000000000000013215225434775015200 xustar0030 mtime=1784035837.872147003 30 atime=1784035837.872147003 30 ctime=1784035837.872147003 qpwgraph-1.0.3/CMakeLists.txt0000644000175000001440000000577215225434775015203 0ustar00rncbcuserscmake_minimum_required (VERSION 3.15) project(qpwgraph VERSION 1.0.3 DESCRIPTION "A PipeWire Graph Qt GUI Interface" HOMEPAGE_URL "https://gitlab.freedesktop.org/rncbc/qpwgraph" LANGUAGES C CXX) set (PROJECT_COPYRIGHT "Copyright (C) 2021-2026, 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 unique/single instance. option (CONFIG_XUNIQUE "Enable unique/single instance (default=yes)" 1) # 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 ("") show_option (" Unique/Single instance support . . . . . . . . . ." CONFIG_XUNIQUE) message ("\n Install prefix . . . . . . . . . . . . . . . . . .: ${CMAKE_INSTALL_PREFIX}\n") qpwgraph-1.0.3/PaxHeaders/ChangeLog0000644000000000000000000000013215225434775014212 xustar0030 mtime=1784035837.872147003 30 atime=1784035837.872147003 30 ctime=1784035837.872147003 qpwgraph-1.0.3/ChangeLog0000644000175000001440000003506615225434775014214 0ustar00rncbcusersqpwgraph - A PipeWire Graph Qt GUI Interface -------------------------------------------- ChangeLog 1.0.3 2026-07-14 A Summer'26 Release. - Fixed crash when selecting the "Don't ask this again" option on some message-box prompts. - Added Ctrl+R keyboard shortcut for View/Arrange Nodes. 1.0.2 2026-05-23 A Mid-Spring'26 Release. - PipeWire node and port IDs are now shown on hovering tooltips. - Make sure thumb-view widget background is filled/opaque (ie. not transparent) 1.0.1 2026-04-21 A Spring'26 Release. - Added remote daemon name and modified state labels to the main status-bar. - Added command line option to connect on a remote PipeWire daemon context, by name (-r, --remote); nb. PIPEWIRE_REMOTE environment variable takes precedence though. - Separate the singleton/unique application instance setup logic from system-tray icon build option; add PipeWire remote-name, if any, to singleton/unique application instance identifier. - Settling on all boards to next development cycle (Qt >= 6.11) 1.0.0 2026-03-17 An Early-Spring'26 Release. - Configured ALSA MIDI port colors now reset/restored properly. 0.9.9 2026-02-10 A Mid-Winter'26 Release. - Most probably the first and last release candidate ever ;) 0.9.8 2025-12-29 An End-of-Year'25 Beta Release. - Fixed 'mime-info' file to the correct MIME-type icon names. 0.9.7 2025-11-14 An Autumn'25 Beta Release. - Add topological sort node arrangement (cf. View/Arrange Nodes; by Mike Bourgeous) 0.9.6 2025-10-06 An Early-Fall'25 Beta Release. - Allow the complete node name to get the same treatment in the Graph/Options.../Filter as same for Merger. - Get rid of CONFIG_WAYLAND build config option; add underlying platform name (eg. xcb, wayland) to Qt version string. 0.9.5 2025-08-15 A Mid-Summer'25 Beta Release. - Make all node-names in merger list (Graph/Options.../Merger) implicitly exclusive for Patchbay persistence. - Cope with in-flight node/port count changing. 0.9.4 2025-06-21 An Early-Summer'25 Beta Release. - Indulged on a new 'Add' (pinned connection) button into the Patchbay/Manage... dialog also make dialog size and position persistent. - Introducing Graph/Options.../Merger to unify node-names for Patchbay persistence, especially useful to PipeWire clients that spawn more than one node, having the very same name (eg. web browsers). 0.9.3 2025-06-02 An End-of-Spring'25 Beta Release. - Corrected (again) salvage of node positions that possible have the very same and exact name. 0.9.2 2025-05-17 A Mid-Spring'25 Hot-Fix Beta Release. - Save node positions as soon as they are taken or moved around, avoiding accidental re-positioning to the previous or original state. - Regression to the late uniform and generic patchbay node-names which was a terrible mistake: while it sort of fixed some issues regarding multimedia players, it sent all other cases bonkers. So sorry for all the foul play or inconvenience. 0.9.1 2025-05-15 A Mid-Spring'25 Beta Release. - Patchbay node-name persistence is now made uniform and generic for most if not all multimedia players (eg. web browsers) that usually tend to power-cycle their PipeWire client/driver on a whim (eg. pause, skip,... whatever) 0.9.0 2025-04-25 A Spring'25 Beta Release. - Mitigate an alternate re-positioning to some application nodes (eg. Discord) on each screen refresh (F5). - Stepping up next development cycle (Qt >= 6.9) 0.8.3 2025-03-28 An Early Spring'25 Beta Release. - Loading a patchbay profile (*.qpwgraph) from the command line, is now properly shown on the main window title and toolbar. - Added PipeWire command line version information. 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-1.0.3/PaxHeaders/LICENSE.md0000644000000000000000000000013215225434775014044 xustar0030 mtime=1784035837.873146989 30 atime=1784035837.872147003 30 ctime=1784035837.873146989 qpwgraph-1.0.3/LICENSE.md0000644000175000001440000004302515225434775014040 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-1.0.3/PaxHeaders/README.md0000644000000000000000000000013215225434775013717 xustar0030 mtime=1784035837.873146989 30 atime=1784035837.873146989 30 ctime=1784035837.873146989 qpwgraph-1.0.3/README.md0000644000175000001440000000360215225434775013710 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-2026, rncbc aka Rui Nuno Capela. All rights reserved. qpwgraph-1.0.3/PaxHeaders/docs0000644000000000000000000000013115225434775013312 xustar0030 mtime=1784035837.875350384 29 atime=1784035837.87459068 30 ctime=1784035837.875350384 qpwgraph-1.0.3/docs/0000755000175000001440000000000015225434775013360 5ustar00rncbcusersqpwgraph-1.0.3/docs/PaxHeaders/qpwgraph-user_manual.md0000644000000000000000000000012715225434775020060 xustar0029 mtime=1784035837.87459068 29 atime=1784035837.87459068 29 ctime=1784035837.87459068 qpwgraph-1.0.3/docs/qpwgraph-user_manual.md0000644000175000001440000001051615225434775020047 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) | | Ctrl + R | Arrange selected nodes using topological sort | ## 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) ## Options Main menu Graph > Options... ### General > Enable system tray icon >> Show system tray message on close >> Start minimized to system tray > Show activated patchbay message on exit > Enable ALSA MIDI support ### Filter > Enable filter / hide nodes > List of node names or titles that shall never appear on the graph; case insensitive; regular expressions allowed. ### Merger > Enable merge / unify nodes > List of node name or title prefixes that will match as same by the Patchbay scan; case insensitive; regular expressions allowed. --- Credits: @denilsonsa (a.k.a. Denilson Sá Maia). qpwgraph-1.0.3/docs/PaxHeaders/qpwgraph_patchbay-user_manual.md0000644000000000000000000000013115225434775021726 xustar0030 mtime=1784035837.875350384 29 atime=1784035837.87459068 30 ctime=1784035837.875350384 qpwgraph-1.0.3/docs/qpwgraph_patchbay-user_manual.md0000644000175000001440000000422015225434775021715 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, at user's discretion. --- Credits: @Lootre (a.k.a. Thomas Lachat). qpwgraph-1.0.3/PaxHeaders/src0000644000000000000000000000013215225434775013152 xustar0030 mtime=1784035837.891146741 30 atime=1784035837.875350384 30 ctime=1784035837.891146741 qpwgraph-1.0.3/src/0000755000175000001440000000000015225434775013217 5ustar00rncbcusersqpwgraph-1.0.3/src/PaxHeaders/appdata0000644000000000000000000000013215225434775014564 xustar0030 mtime=1784035837.876250108 30 atime=1784035837.875350384 30 ctime=1784035837.876250108 qpwgraph-1.0.3/src/appdata/0000755000175000001440000000000015225434775014631 5ustar00rncbcusersqpwgraph-1.0.3/src/appdata/PaxHeaders/org.rncbc.qpwgraph.desktop0000644000000000000000000000013215225434775021741 xustar0030 mtime=1784035837.876087728 30 atime=1784035837.876087728 30 ctime=1784035837.876087728 qpwgraph-1.0.3/src/appdata/org.rncbc.qpwgraph.desktop0000644000175000001440000000053115225434775021730 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-1.0.3/src/appdata/PaxHeaders/org.rncbc.qpwgraph.metainfo.xml0000644000000000000000000000013215225434775022671 xustar0030 mtime=1784035837.876250108 30 atime=1784035837.876087728 30 ctime=1784035837.876250108 qpwgraph-1.0.3/src/appdata/org.rncbc.qpwgraph.metainfo.xml0000644000175000001440000000335115225434775022663 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 PipeWire AudioVideo Audio Video MIDI JACK Qt https://gitlab.freedesktop.org/rncbc/qpwgraph https://gitlab.freedesktop.org/rncbc/qpwgraph rncbc aka. Rui Nuno Capela rncbc@rncbc.org
qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_patchbay.h0000644000000000000000000000013215225434775017104 xustar0030 mtime=1784035837.888146782 30 atime=1784035837.887146797 30 ctime=1784035837.888146782 qpwgraph-1.0.3/src/qpwgraph_patchbay.h0000644000175000001440000001035515225434775017100 0ustar00rncbcusers// qpraph1_patchbay.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_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-1.0.3/src/PaxHeaders/qpwgraph_patchman.cpp0000644000000000000000000000013215225434775017437 xustar0030 mtime=1784035837.888146782 30 atime=1784035837.888146782 30 ctime=1784035837.888146782 qpwgraph-1.0.3/src/qpwgraph_patchman.cpp0000644000175000001440000011100315225434775017423 0ustar00rncbcusers// qpwgraph_patchman.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 "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 canAdd() const; void add(); 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 addLine( QTreeWidgetItem *port1_item, QTreeWidgetItem *port2_item); 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::canAdd (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 (port1_item->type() == port2_item->type() && !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 (port1_item->type() == port2_item->type() && !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 (port1_item->type() == port2_item->type() && !findLine(port1_item, port2_item)) return true; } } else { // One-to-many(all) connection... QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2; if (port1_item->type() == port2_item->type() && !findLine(port1_item, port2_item)) return true; } } } return false; } void qpwgraph_patchman::MainWidget::add (void) { int nadded = 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 (port1_item->type() == port2_item->type() && addLine(port1_item, port2_item)) ++nadded; } } 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 (port1_item->type() == port2_item->type() && addLine(port1_item, port2_item)) ++nadded; } } } 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 (port1_item->type() == port2_item->type() && addLine(port1_item, port2_item)) ++nadded; } } else { // One-to-many(all) connection... QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2; if (port1_item->type() == port2_item->type() && addLine(port1_item, port2_item)) ++nadded; } } } // Refresh if anything has been added... if (nadded > 0) refresh(); } 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 (port1_item->type() == port2_item->type() && 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 (port1_item->type() == port2_item->type() && 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 (port1_item->type() == port2_item->type() && findLine(port1_item, port2_item)) return true; } } else { // One-to-many(all) connection... QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2; if (port1_item->type() == port2_item->type() && 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 (port1_item->type() == port2_item->type() && 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 (port1_item->type() == port2_item->type() && 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 (port1_item->type() == port2_item->type() && removeLine(port1_item, port2_item)) ++nremoved; } } else { // One-to-many(all) connection... QTreeWidgetItem *port1_item = item1; QTreeWidgetItem *port2_item = item2; if (port1_item->type() == port2_item->type() && 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::addLine ( 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->addLine(port1_item, port2_item); return m_items.addItem( 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))); } 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::setObjectName("qpwgraph_patchman"); QDialog::setWindowTitle(tr("Manage Patchbay")); m_add_button = new QPushButton("&Add"); m_remove_button = new QPushButton("&Remove"); m_remove_all_button = new QPushButton("Remo&ve 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_add_button); 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_add_button, SIGNAL(clicked()), SLOT(addClicked())); 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::addClicked (void) { m_main->add(); ++m_dirty; stabilize(); } 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_add_button->setEnabled(m_main->canAdd()); 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-1.0.3/src/PaxHeaders/qpwgraph_sect.h0000644000000000000000000000013215225434775016247 xustar0030 mtime=1784035837.890146755 30 atime=1784035837.890146755 30 ctime=1784035837.890146755 qpwgraph-1.0.3/src/qpwgraph_sect.h0000644000175000001440000000375415225434775016250 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-1.0.3/src/PaxHeaders/qpwgraph_alsamidi.cpp0000644000000000000000000000013215225434775017427 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.881713957 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/qpwgraph_alsamidi.cpp0000644000175000001440000002735115225434775017427 0ustar00rncbcusers// qpwgraph_alsamidi.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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) { 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 (static). void qpwgraph_alsamidi::resetPortTypeColors ( qpwgraph_canvas *canvas ) { if (canvas) { canvas->setPortTypeColor( qpwgraph_alsamidi::midiPortType(), QColor(Qt::darkMagenta).darker(120)); } } #endif // CONFIG_ALSA_MIDI // end of qpwgraph_alsamidi.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_canvas.h0000644000000000000000000000013215225434775016564 xustar0030 mtime=1784035837.883146851 30 atime=1784035837.883146851 30 ctime=1784035837.883146851 qpwgraph-1.0.3/src/qpwgraph_canvas.h0000644000175000001440000002313415225434775016557 0ustar00rncbcusers// qpwgraph_canvas.h // /**************************************************************************** Copyright (C) 2021-2026, 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 canSearchItem() const; bool canRenameItem() const; bool canArrangeNodes() const; // Zooming methods. void setZoom(qreal zoom); qreal zoom() const; void setZoomRange(bool zoomrange); bool isZoomRange() const; void centerView(bool showSelected); // Clean-up all un-marked nodes... void resetNodes(uint node_type); void clearNodes(uint node_type); // Special node finders. qpwgraph_node *findNode( const qpwgraph_node::NodeIdKey& node_key) const; 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; // 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; // 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; // Merger/unify list management accessors. void setMergerNodesEnabled(bool enabled); bool isMergerNodesEnabled() const; void setMergerNodesList(const QStringList& nodes); const QStringList& mergerNodesList() const; bool isMergerNodes(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 searchItem(); void renameItem(); void arrangeNodes(); // 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); // Node number if multiple (else count). int nodeNum(qpwgraph_node *node) const; // 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); // 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; qpwgraph_port *m_port2; 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; // Merger/unify list management. bool m_merger_enabled; QStringList m_merger_nodes; }; #endif // __qpwgraph_canvas_h // end of qpwgraph_canvas.h qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_node.cpp0000644000000000000000000000012715225434775016575 xustar0029 mtime=1784035837.88614681 29 atime=1784035837.88614681 29 ctime=1784035837.88614681 qpwgraph-1.0.3/src/qpwgraph_node.cpp0000644000175000001440000002501215225434775016561 0ustar00rncbcusers// qpwgraph_node.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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), m_num(0), m_name_ex(false), m_label_ex(false) { 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(nodeNameLabelEx()); } QString qpwgraph_node::nodeName (void) const { QString name = m_name; if (m_num > 0) { name += '-'; name += QString::number(m_num); } return 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::setNodeNum ( uint num ) { m_num = num; setNodeTitle(QString()); // reset title. } uint qpwgraph_node::nodeNum (void) const { return m_num; } 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 name_label = nodeName(); if (!m_label.isEmpty()) { name_label += ' '; name_label += '['; name_label += m_label; name_label += ']'; } return name_label; } void qpwgraph_node::setNodeTitle ( const QString& title ) { const QString& name_label = nodeNameLabel(); 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); QGraphicsPathItem::setToolTip(nodeNameLabelEx()); } 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; } void qpwgraph_node::setNodeNameEx ( bool name_ex ) { m_name_ex = name_ex; } bool qpwgraph_node::isNodeNameEx (void) const { return m_name_ex; } QString qpwgraph_node::nodeNameEx (void) const { return (m_name_ex ? m_name : nodeName()); } void qpwgraph_node::setNodeLabelEx ( bool label_ex ) { m_label_ex = label_ex; setNodeTitle(QString()); // reset title. } bool qpwgraph_node::isNodeLabelEx (void) const { return m_label_ex; } QString qpwgraph_node::nodeNameLabelEx (void) const { QString name_label; if (m_label_ex) { name_label += QString::number(nodeId()); name_label += ':'; } return name_label + nodeNameLabel(); } // 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-1.0.3/src/PaxHeaders/qpwgraph_item.h0000644000000000000000000000013215225434775016247 xustar0030 mtime=1784035837.884146838 30 atime=1784035837.884146838 30 ctime=1784035837.884146838 qpwgraph-1.0.3/src/qpwgraph_item.h0000644000175000001440000001001015225434775016227 0ustar00rncbcusers// qpwgraph_item.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_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-1.0.3/src/PaxHeaders/qpwgraph_node.h0000644000000000000000000000012715225434775016242 xustar0029 mtime=1784035837.88614681 29 atime=1784035837.88614681 29 ctime=1784035837.88614681 qpwgraph-1.0.3/src/qpwgraph_node.h0000644000175000001440000001050715225434775016231 0ustar00rncbcusers// qpwgraph_node.h // /**************************************************************************** Copyright (C) 2021-2026, 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); 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 setNodeNum(uint num); uint nodeNum() 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; void setNodeNameEx(bool name_ex); bool isNodeNameEx() const; QString nodeNameEx() const; void setNodeLabelEx(bool label_ex); bool isNodeLabelEx() const; QString nodeNameLabelEx() 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->nodeNameEx(), 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; uint m_num; QString m_label; QString m_title; QString m_prefix; bool m_name_ex; bool m_label_ex; 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-1.0.3/src/PaxHeaders/qpwgraph_item.cpp0000644000000000000000000000013215225434775016602 xustar0030 mtime=1784035837.884146838 30 atime=1784035837.884146838 30 ctime=1784035837.884146838 qpwgraph-1.0.3/src/qpwgraph_item.cpp0000644000175000001440000000624015225434775016574 0ustar00rncbcusers// qpwgraph_item.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_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; 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-1.0.3/src/PaxHeaders/qpwgraph_alsamidi.h0000644000000000000000000000013215225434775017074 xustar0030 mtime=1784035837.882146865 30 atime=1784035837.881713957 30 ctime=1784035837.882146865 qpwgraph-1.0.3/src/qpwgraph_alsamidi.h0000644000175000001440000000512415225434775017066 0ustar00rncbcusers// qpwgraph_alsamidi.h // /**************************************************************************** Copyright (C) 2021-2026, 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(); // 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(); // Special port-type colors defaults (virtual). static void resetPortTypeColors(qpwgraph_canvas *canvas); 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-1.0.3/src/PaxHeaders/qpwgraph_patchbay.cpp0000644000000000000000000000013215225434775017437 xustar0030 mtime=1784035837.887146797 30 atime=1784035837.887146797 30 ctime=1784035837.887146797 qpwgraph-1.0.3/src/qpwgraph_patchbay.cpp0000644000175000001440000003303415225434775017432 0ustar00rncbcusers// qpwgraph_patchbay.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 "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->nodeNameEx(), port1->portName(), node2->nodeNameEx(), 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; const bool node1_exclusive = m_canvas->isMergerNodes(node1->nodeName()); 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 || node1_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->nodeNameEx(), port1->portName(), node12->nodeNameEx(), 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->nodeNameEx(), port21->portName(), node2->nodeNameEx(), 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->nodeNameEx(), port1->portName(), node2->nodeNameEx(), 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->nodeNameEx(), port1->portName(), node2->nodeNameEx(), 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->nodeNameEx(), port1->portName(), node2->nodeNameEx(), 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-1.0.3/src/PaxHeaders/mimetypes0000644000000000000000000000013215225434775015166 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.880146893 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/mimetypes/0000755000175000001440000000000015225434775015233 5ustar00rncbcusersqpwgraph-1.0.3/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.png0000644000000000000000000000013215225434775027645 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/mimetypes/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.png0000644000175000001440000000303115225434775027632 0ustar00rncbcusers‰PNG  IHDR szzô pHYsÃÃÇo¨dtEXtSoftwarewww.inkscape.org›î<¦IDATX…½—[l\W†¿}f—L&/ÜÍ;…ÒZôË‹©²ªŠP¸¥ñ�7qÊï÷?ñÿÜ“lqñܳû+ú³_žh…úJÙo*Á½ªú¡®¥xésÊy7‰<›œ|`U–Ÿ¥åU</øŒWDއ·o*:1qê�TTTp5µÀµTŸUÅÁϾ`Ú¶ý^°¹¹?>==ô‘˜ÞrÆ£Sœ=”ä|Ci‚�zóÛßúÎß”R�"ûH,Ÿ�;;ÙñH�Óô"",¦®Zm¯^)uxý…ußO�Û]†åócY>LÓ €R M½ÑÙÕÌõö÷ ˜ª|¾2ÀU@ký@¡ »›u¿T° µv0 7†`a¥¯%YIÇH§6o'Ÿ³±jñz½ìfcS$ç®�YY¤Ïã_êÞùZP)õY…­z@­�¢™XÍÌ3zz�H¸�æ}=“š�@ëÛ9�JâÒ~ú“ôìj'5ööö~! u¯ÙÐZJBØRF÷£�3¶“ñ‹gp‡ÄôEºw=Ì�þ™�‘N§—GFF.Çb±±5?K ¢ÑÚÀ0¶.G8ÒF."ô±^”RLNŽá·*i ocv6ÊüLœgú{854JM°ƒî=-üà»/þýo~þ#IÉ€(ÇqnBÏ„ÇcÞ G:<}ŽÖH˜tb†¦€�¥åâsBá–Á\†öz[6!€ËÅ–™È^_%>=Nf)C}Cv>ÏjNˆÎÄéòQü>GÞ:J™YÅ™ÑSضƒ“_í;tèÐ¥ÔqŠÜn¤­ ’‰(KÉ(ím­X–›êê:tG–eQQQÀ³û÷²´´„ˆ�Fyùë_Û§”Ú=44t8S@¥çÖZ)u3øFˆÕÕ,áp˜±K—Ø“¸š"ŸËÓÞ¹ðŸ±q‰ylÇæê|‚�wu:Éd2?99¹P´ŽvÖ­lÀ½ "Òñ±éIšÚ{±, -š±‹Òª5#£ç…I$樬‹P[WK+ðê7¾ü«�ãÇ^‘‰5ï�¡c;8ÚA;ZkÇFk½¡1AhŽ´R·mfy9†áB² ”{MNÈÛz‡½}OpöôûXV5~ Á`xˆ¯�UÀ¶í’¢5‰+1R×’d¯_gøä žÞßÏ_ÿ>@WÏÊ*ë¹<1Eïc»x÷/d2‹,,¦ª�Ú’Žc—„˜™ÇÔiâÿfæÒŸèífìÒe²Ú¤¾>Àî={9ñ�hiÓPmòþ;àûß{å«ÇŽ;_^^ÞRÀ) ‘Í.“N¥ñz½ø¬J2™E>¾@ß¾§�Íû‹_ä·¿{‹ÚÚ‡h Ôãv»•ÇãñÖÔÔÔ¬Å*Ø„ù\<ž�›,mê-üëÜ0 ©+4†šP®Uö?pÃ�YYeñÔ§^âÂ…³dsyÞxåÕ“©TòÍx<~¾(€íب|�I¸ÎÚ4Mv?¹Ã0PÊÀ0Ê0‘ �`ˆ@0ÀàÀñã'Žˆˆ]Àåm9¶]h»`&6QôÍ¢ÅFù†;}@.·´|îÌðÓ4³ p¹Ýâ2 0 1”�Ëp ÆÚå”aÈ�€‚ajÓà�º|åÎýMÇJ©zÀ³éÁ½K³².èÿ‹¿ÑNzüÀIIEND®B`‚qpwgraph-1.0.3/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.xml0000644000000000000000000000013215225434775021472 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.881713957 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/mimetypes/org.rncbc.qpwgraph.xml0000644000175000001440000000056615225434775021471 0ustar00rncbcusers qpwgraph patchbay qpwgraph-1.0.3/src/mimetypes/PaxHeaders/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.svg0000644000000000000000000000013215225434775027660 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.880146893 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/mimetypes/org.rncbc.qpwgraph.application-x-qpwgraph-patchbay.svg0000644000175000001440000003270215225434775027654 0ustar00rncbcusers qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_sect.cpp0000644000000000000000000000013215225434775016602 xustar0030 mtime=1784035837.890146755 30 atime=1784035837.890146755 30 ctime=1784035837.890146755 qpwgraph-1.0.3/src/qpwgraph_sect.cpp0000644000175000001440000000637115225434775016601 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); m_canvas->saveNode(node); } } else if (item->type() == qpwgraph_port::Type) { qpwgraph_port *port = static_cast (item); if (port) node = port->portNode(); if (port && node) { port->setPortTitle(name); m_canvas->savePort(port); } } if (node) node->updatePath(); } // end of qpwgraph_sect.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_port.h0000644000000000000000000000013215225434775016275 xustar0030 mtime=1784035837.890146755 30 atime=1784035837.890146755 30 ctime=1784035837.890146755 qpwgraph-1.0.3/src/qpwgraph_port.h0000644000175000001440000001157415225434775016275 0ustar00rncbcusers// qpwgraph_port.h // /**************************************************************************** Copyright (C) 2021-2026, 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; void setPortLabelEx(bool label_ex); bool isPortLabelEx() const; QString portNameLabelEx() 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; bool m_label_ex; static SortType g_sort_type; static SortOrder g_sort_order; }; #endif // __qpwgraph_port_h // end of qpwgraph_port.h qpwgraph-1.0.3/src/PaxHeaders/qpwgraph.qrc0000644000000000000000000000013215225434775015567 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.881713957 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/qpwgraph.qrc0000644000175000001440000000215215225434775015557 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-1.0.3/src/PaxHeaders/qpwgraph_pipewire.cpp0000644000000000000000000000013215225434775017470 xustar0030 mtime=1784035837.889146769 30 atime=1784035837.889146769 30 ctime=1784035837.889146769 qpwgraph-1.0.3/src/qpwgraph_pipewire.cpp0000644000175000001440000010503715225434775017466 0ustar00rncbcusers// qpwgraph_pipewire.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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; qpwgraph_item::Mode node_mode2; QIcon node_icon; QString media_name; bool node_changed; bool node_ready; uint name_num; }; struct qpwgraph_pipewire::Port : public qpwgraph_pipewire::Object { Port (uint port_id) : Object(port_id, Type::Port) {} enum Flags { None = 0, Physical = 1, Terminal = 2, Monitor = 4, Control = 8 }; uint node_id; QString port_name; qpwgraph_item::Mode port_mode; uint port_type; Flags port_flags; QList port_links; }; struct qpwgraph_pipewire::Link : public qpwgraph_pipewire::Object { Link (uint link_id) : Object(link_id, Type::Link) {} uint port1_id; uint port2_id; }; struct qpwgraph_pipewire::Data { struct pw_thread_loop *loop; struct pw_context *context; struct pw_core *core; struct spa_hook core_listener; struct pw_registry *registry; struct spa_hook registry_listener; int pending_seq; struct spa_list pending; int last_seq; int last_res; bool error; typedef QMultiHash NodeNames; NodeNames *node_names; }; inline uint qHash ( const qpwgraph_pipewire::Node::NameKey& key ) { return qHash(key.node_name) ^ qHash(uint(key.node_mode)) ^ qHash(key.node_type); } // sync-methods... static void qpwgraph_add_pending ( qpwgraph_pipewire::Proxy *p ) { qpwgraph_pipewire *pw = p->pw; qpwgraph_pipewire::Data *pd = pw->data(); if (p->pending_seq == 0) spa_list_append(&pd->pending, &p->pending_link); p->pending_seq = pw_core_sync(pd->core, 0, p->pending_seq); pd->pending_seq = p->pending_seq; } static void qpwgraph_remove_pending ( qpwgraph_pipewire::Proxy *p ) { if (p->pending_seq != 0) { spa_list_remove(&p->pending_link); p->pending_seq = 0; } } // sync-methods. // node-events... static void qpwgraph_node_event_info ( void *data, const struct pw_node_info *info ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p) { info = pw_node_info_update((struct pw_node_info *)object->p->info, info); object->p->info = (void *)info; // Get node icon and media.name, if any... if (info && (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)) { qpwgraph_pipewire::Node *node = static_cast (object); if (node) { QIcon node_icon; const char *icon_name = spa_dict_lookup(info->props, PW_KEY_APP_ICON_NAME); if (icon_name && ::strlen(icon_name) > 0) node_icon = qpwgraph_icon(icon_name); if (node_icon.isNull()) node_icon = qpwgraph_icon(node->node_name.toLower()); if (node_icon.isNull()) { const char *client_api = spa_dict_lookup(info->props, PW_KEY_CLIENT_API); if (client_api && ::strlen(client_api) > 0) { if (::strcmp(client_api, "jack") == 0 || ::strcmp(client_api, "pipewire-jack") == 0) { node_icon = qpwgraph_icon(":images/itemJack.png"); } else if (::strcmp(client_api, "pulse") == 0 || ::strcmp(client_api, "pipewire-pulse") == 0) { node_icon = qpwgraph_icon(":images/itemPulse.png"); } } } if (!node_icon.isNull()) node->node_icon = node_icon; const char *media_name = spa_dict_lookup(info->props, PW_KEY_MEDIA_NAME); if (media_name && ::strlen(media_name) > 0) node->media_name = media_name; node->node_changed = true; node->node_ready = true; if (object->p->pw) object->p->pw->changedNotify(); } } } } static const struct pw_node_events qpwgraph_node_events = { .version = PW_VERSION_NODE_EVENTS, .info = qpwgraph_node_event_info, }; // node-events. // port-events... static void qpwgraph_port_event_info ( void *data, const struct pw_port_info *info ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p) { info = pw_port_info_update((struct pw_port_info *)object->p->info, info); object->p->info = (void *)info; } } static const struct pw_port_events qpwgraph_port_events = { .version = PW_VERSION_PORT_EVENTS, .info = qpwgraph_port_event_info, }; // port-events. // link-events... static void qpwgraph_link_event_info ( void *data, const struct pw_link_info *info ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p) { info = pw_link_info_update((struct pw_link_info *)object->p->info, info); object->p->info = (void *)info; } } static const struct pw_link_events qpwgraph_link_events = { .version = PW_VERSION_LINK_EVENTS, .info = qpwgraph_link_event_info, }; // link-events. // proxy-events... static void qpwgraph_proxy_removed ( void *data ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object && object->p && object->p->proxy) { struct pw_proxy *proxy = object->p->proxy; object->p->proxy = nullptr; pw_proxy_destroy(proxy); } } static void qpwgraph_proxy_destroy ( void *data ) { qpwgraph_pipewire::Object *object = static_cast (data); if (object) object->destroy_proxy(); } static const struct pw_proxy_events qpwgraph_proxy_events = { .version = PW_VERSION_PROXY_EVENTS, .destroy = qpwgraph_proxy_destroy, .removed = qpwgraph_proxy_removed, }; // proxy-events. // proxy-methods... void qpwgraph_pipewire::Object::create_proxy ( qpwgraph_pipewire *pw ) { if (p) return; const char *proxy_type = nullptr; uint32_t version = 0; pw_destroy_t destroy = nullptr; const void *events = nullptr; switch (type) { case Node: proxy_type = PW_TYPE_INTERFACE_Node; version = PW_VERSION_NODE; destroy = (pw_destroy_t) pw_node_info_free; events = &qpwgraph_node_events; break; case Port: proxy_type = PW_TYPE_INTERFACE_Port; version = PW_VERSION_PORT; destroy = (pw_destroy_t) pw_port_info_free; events = &qpwgraph_port_events; break; case Link: proxy_type = PW_TYPE_INTERFACE_Link; version = PW_VERSION_LINK; destroy = (pw_destroy_t) pw_link_info_free; events = &qpwgraph_link_events; break; } struct pw_proxy *proxy = (struct pw_proxy *)pw_registry_bind( pw->data()->registry, id, proxy_type, version, sizeof(Proxy)); if (proxy) p = (Proxy *)pw_proxy_get_user_data(proxy); if (p) { p->pw = pw; p->proxy = proxy; p->destroy = destroy; p->pending_seq = 0; pw_proxy_add_object_listener(proxy, &p->object_listener, events, this); pw_proxy_add_listener(proxy, &p->proxy_listener, &qpwgraph_proxy_events, this); } } void qpwgraph_pipewire::Object::destroy_proxy (void) { if (p == nullptr) return; spa_hook_remove(&p->object_listener); spa_hook_remove(&p->proxy_listener); qpwgraph_remove_pending(p); if (p->info && p->destroy) { p->destroy(p->info); p->info = nullptr; } if (p->proxy) { pw_proxy_destroy(p->proxy); p->proxy = nullptr; } p = nullptr; } // proxy-methods. // registry-events... static void qpwgraph_registry_event_global ( void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props ) { if (props == nullptr) return; qpwgraph_pipewire *pw = static_cast (data); #ifdef CONFIG_DEBUG qDebug("qpwgraph_registry_event_global[%p]: id:%u type:%s/%u", pw, id, type, version); #endif int nchanged = 0; if (::strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { QString node_name; const char *str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); 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_info ( void *data, const struct pw_core_info *info ) { qpwgraph_pipewire *pw = static_cast (data); qpwgraph_pipewire::Data *pd = pw->data(); #ifdef CONFIG_DEBUG qDebug("qpwgraph_core_event_info[%p]: name:%s", pd, info->name); #endif pw->setRemoteName(info->name); pw->changedNotify(); } 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 = qpwgraph_core_event_info, .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) { 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; } struct pw_properties *props = nullptr; if (!m_remote_name.isEmpty()) { props = pw_properties_new(PW_KEY_REMOTE_NAME, m_remote_name.toUtf8().constData(), nullptr); } m_data->core = pw_context_connect(m_data->context, props, 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;//n->node_mode2; 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 = n->node_mode2;//qpwgraph_item::Duplex; *node = qpwgraph_sect::findNode(node_id, node_mode, node_type); } } if (*node && n->node_changed) { canvas->releaseNode(*node); *node = nullptr; } if (*node) *port = (*node)->findPort(port_id, port_mode, port_type); if (add_new && *node == nullptr) { QString node_name = n->node_name; if ((p->port_flags & Port::Physical) == Port::None) { if (p->port_flags & Port::Monitor) { node_name += ' '; node_name += "[Monitor]"; } if (p->port_flags & Port::Control) { node_name += ' '; node_name += "[Control]"; } } if (!canvas->isFilterNodes(node_name)) { *node = new qpwgraph_node(node_id, node_name, node_mode, node_type); (*node)->setNodeIcon(n->node_icon); (*node)->setNodeNum(n->name_num); (*node)->setNodeLabel(n->media_name); (*node)->setNodePrefix(n->node_nick); (*node)->setNodeNameEx(canvas->isMergerNodes(node_name)); (*node)->setNodeLabelEx(true); 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); (*port)->setPortLabelEx(true); 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()); } 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()); } // Special port-type colors defaults (static). void qpwgraph_pipewire::resetPortTypeColors ( qpwgraph_canvas *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 ) { 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_mode2 = node_mode; 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 ) { 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!... if ((node->node_mode2 & port_mode) == qpwgraph_item::None) node->node_mode2 = qpwgraph_item::Duplex; node->node_ports.append(port); node->node_changed = true; 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); node->node_changed = true; 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; } // Remote name accessors. void qpwgraph_pipewire::setRemoteName ( const QString& remote_name ) { m_remote_name = remote_name; } const QString& qpwgraph_pipewire::remoteName (void) const { return m_remote_name; } // end of qpwgraph_pipewire.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_main.ui0000644000000000000000000000012715225434775016427 xustar0029 mtime=1784035837.88614681 29 atime=1784035837.88614681 29 ctime=1784035837.88614681 qpwgraph-1.0.3/src/qpwgraph_main.ui0000644000175000001440000010705515225434775016423 0ustar00rncbcusers rncbc aka Rui Nuno Capela qpwgraph - A PipeWire Graph Qt GUI Interface Copyright (C) 2021-2026, 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/itemFind.png &Find... Find Find nodes Find nodes Ctrl+F :/images/itemEdit.png &Rename... Rename Item Rename item Rename item F2 true &Menubar Menubar Menubar Show/hide the main program window menubar Ctrl+M true &Graph Graph Toolbar Graph toolbar Show/hide main program graph toolbar true &Patchbay Patchbay Toolbar Patchbay toolbar Show/hide main program patchbay toolbar true &Statusbar Statusbar Statusbar Show/hide the main program window statusbar true &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 &Arrange Nodes Arrange Nodes Arrange nodes (topological sort) Use topological sort to arrange selected nodes following signal path from left to right. Ctrl+R &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-1.0.3/src/PaxHeaders/qpwgraph_port.cpp0000644000000000000000000000013215225434775016630 xustar0030 mtime=1784035837.890146755 30 atime=1784035837.889146769 30 ctime=1784035837.890146755 qpwgraph-1.0.3/src/qpwgraph_port.cpp0000644000175000001440000002633315225434775016627 0ustar00rncbcusers// qpwgraph_port.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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), m_label_ex(false) { 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(portNameLabelEx()); } 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(); 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); QGraphicsPathItem::setToolTip(portNameLabelEx()); } 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; } void qpwgraph_port::setPortLabelEx ( bool label_ex ) { m_label_ex = label_ex; setPortTitle(QString()); // reset title. } bool qpwgraph_port::isPortLabelEx (void) const { return m_label_ex; } QString qpwgraph_port::portNameLabelEx (void) const { QString name_label; if (m_label_ex) { name_label += QString::number(portId()); name_label += ':'; } return name_label + portNameLabel(); } 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()) { 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-1.0.3/src/PaxHeaders/qpwgraph_connect.cpp0000644000000000000000000000013215225434775017275 xustar0030 mtime=1784035837.884146838 30 atime=1784035837.884146838 30 ctime=1784035837.884146838 qpwgraph-1.0.3/src/qpwgraph_connect.cpp0000644000175000001440000002067715225434775017301 0ustar00rncbcusers// qpwgraph_connect.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_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()) 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-1.0.3/src/PaxHeaders/qpwgraph_toposort.cpp0000644000000000000000000000013215225434775017535 xustar0030 mtime=1784035837.891146741 30 atime=1784035837.891146741 30 ctime=1784035837.891146741 qpwgraph-1.0.3/src/qpwgraph_toposort.cpp0000644000175000001440000003135115225434775017530 0ustar00rncbcusers// qpwgraph_toposort.cpp // /**************************************************************************** Copyright (C) 2025, Mike Bourgeous. 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_toposort.h" #include "qpwgraph_node.h" #include "qpwgraph_port.h" #include "qpwgraph_connect.h" #include #include //---------------------------------------------------------------------------- // qpwgraph_toposort -- Topological sort and related code for graph nodes impl. // qpwgraph_toposort::qpwgraph_toposort ( const QList& nodes ) : m_inputNodes(nodes) { } // Rearrange nodes by type and connection using a topological ordering. // Nodes with no incoming connections go first, nodes with no outgoing // connections go last. Nodes are arranged in columns based on their distance // in the graph from a source node. // // Returns a Map from node pointer to position, without modifying the nodes. const QHash& qpwgraph_toposort::arrange ( const QRect& viewportRect ) { // Sort nodes topologically, using heuristics to break cycles. rankAndSort(); // Precompute and store information used during arrangement. QMap> rankNodes; QMap rankMaxWidth; qreal xmin = std::numeric_limits::infinity(); qreal ymin = std::numeric_limits::infinity(); int maxRank = 0; foreach (qpwgraph_node *n, m_inputNodes) { int rank = m_nodeRanks[n]; if (!rankNodes.contains(rank)) { rankNodes[rank] = QList(); } rankNodes[rank] << n; if (!rankMaxWidth.contains(rank)) { rankMaxWidth[rank] = 0; } rankMaxWidth[rank] = qMax(rankMaxWidth[rank], n->boundingRect().width()); maxRank = qMax(maxRank, rank); xmin = qMin(xmin, n->pos().x()); ymin = qMin(ymin, n->pos().y()); } // Calculate total space required for all columns plus margins, for // expanding to fill viewport width. qreal totalColumnWidths = 40.0; foreach (qreal w, rankMaxWidth) { totalColumnWidths += w; } // Place nodes based on topological sort const qreal xpad = qMax(40.0, (viewportRect.width() - totalColumnWidths) / qMax(2, rankMaxWidth.size() - 1)); const qreal ypad = 20.0; qreal x = xmin, y = ymin; int current_rank = m_nodeRanks[m_inputNodes.first()]; foreach (qpwgraph_node *node, m_inputNodes) { if (m_nodeRanks[node] != current_rank) { // End of a rank; reset Y and move to the next column y = ymin; x += rankMaxWidth[current_rank] + xpad; current_rank = m_nodeRanks[node]; } const qreal w = node->boundingRect().width(); if (current_rank == 0) { // Right-align sources m_newPositions[node] = QPointF(x + (rankMaxWidth[current_rank] - w), y); } else if (current_rank == maxRank) { // Left-align sinks m_newPositions[node] = QPointF(x, y); } else { // Center-align everything else m_newPositions[node] = QPointF(x + (rankMaxWidth[current_rank] - w) / 2, y); } y += node->boundingRect().height() + ypad; } // Move columns vertically for shorter and neater wires for (int i = 1; i <= maxRank; i++) { if (!rankNodes.contains(i)) { continue; } // Sort each column by average connection source port Y value // to place nodes vertically closer to their upstream nodes. sortNodesByConnectionY(rankNodes[i]); qreal minY = m_newPositions[rankNodes[i].first()].y(); foreach (qpwgraph_node *n, rankNodes[i]) { minY = qMin(minY, m_newPositions[n].y()); } qreal y = minY; foreach (qpwgraph_node *n, rankNodes[i]) { m_newPositions[n] = QPointF(m_newPositions[n].x(), y); y += n->boundingRect().height() + ypad; } // Center each column's average input port Y value on the source ports' average Y value qreal parentY = meanParentPortY(rankNodes[i], m_newPositions); qreal inputY = meanInputPortY(rankNodes[i], m_newPositions); qreal delta = inputY - parentY; if (std::isfinite(delta)) { foreach (qpwgraph_node *n, rankNodes[i]) { QPointF &pos = m_newPositions[n]; m_newPositions[n] = QPointF(pos.x(), pos.y() - delta); } } } return m_newPositions; } // Assigns ranks to and sorts the nodes given to the constructor. Sort occurs // in-place in the inputNodes member variable. void qpwgraph_toposort::rankAndSort (void) { foreach (qpwgraph_node *n, m_inputNodes) { if (nodeIsTrueSource(n)) { m_nodeRanks[n] = 0; } else if (nodeIsTrueSink(n)) { m_nodeRanks[n] = 2; } else { m_nodeRanks[n] = 1; } } // Sort before for best cycle-breaking heuristics sortNodesByRank(m_inputNodes); m_unvisitedNodes.clear(); m_unvisitedNodes << m_inputNodes; // Visit sources foreach (qpwgraph_node *n, m_inputNodes) { if (nodeIsEffectiveSource(n)) { visitNode(QSet(), n); } } // Break cycles while (!m_unvisitedNodes.empty()) { qsizetype initialCount = m_unvisitedNodes.size(); qpwgraph_node *n = m_unvisitedNodes.first(); visitNode(QSet(), n); if (initialCount == m_unvisitedNodes.size()) { // This is a bug but we don't want an infinite loop break; } } // Place sink nodes at final rank int sinkDepth = 2; foreach (qpwgraph_node *n, m_inputNodes) { if (nodeIsTrueSink(n)) { sinkDepth = qMax(sinkDepth, m_nodeRanks[n]); } else { sinkDepth = qMax(sinkDepth, m_nodeRanks[n] + 1); } } foreach (qpwgraph_node *n, m_inputNodes) { if (nodeIsTrueSink(n)) { m_nodeRanks[n] = qMax(m_nodeRanks[n], sinkDepth); } } // Sort again with computed ranks sortNodesByRank(m_inputNodes); } // Depth-first iteration of all children of the given node to assign ranks. void qpwgraph_toposort::visitNode ( const QSet& path, qpwgraph_node *n ) { m_unvisitedNodes.removeOne(n); auto newPath = QSet(path); newPath += n; foreach (qpwgraph_node *next, childNodes(n)) { if (newPath.contains(next)) { // Cycle detected; skip this child node continue; } int newDepth = qMax(m_nodeRanks[n] + 1, m_nodeRanks[next]); m_nodeRanks[next] = newDepth; visitNode(newPath, next); } } void qpwgraph_toposort::sortNodesByRank ( QList& nodes ) { std::sort(nodes.begin(), nodes.end(), [this](qpwgraph_node *n1, qpwgraph_node *n2) { return compareNodes(n1, n2); }); } void qpwgraph_toposort::sortNodesByConnectionY ( QList& nodes ) { std::sort(nodes.begin(), nodes.end(), [this](qpwgraph_node *n1, qpwgraph_node *n2) { return compareNodesByConnectionLocation(n1, n2); }); } qsizetype qpwgraph_toposort::countInputPorts ( qpwgraph_node *n ) { return std::count_if(n->ports().begin(), n->ports().end(), [](qpwgraph_port *p) { return p->isInput(); }); } qsizetype qpwgraph_toposort::countOutputPorts ( qpwgraph_node *n ) { return std::count_if(n->ports().begin(), n->ports().end(), [](qpwgraph_port *p) { return p->isOutput(); }); } // Counts connections that do not have the same source and destination node qsizetype qpwgraph_toposort::countExternalConnections ( qpwgraph_node *n, bool is_input ) { qsizetype count = 0; foreach (qpwgraph_port *p, n->ports()) { if (p->isInput() && is_input) { count += std::count_if(p->connects().begin(), p->connects().end(), [](qpwgraph_connect *c) { return c->port1()->portNode() != c->port2()->portNode(); }); } } return count; } qsizetype qpwgraph_toposort::countInputConnections ( qpwgraph_node *n ) { return countExternalConnections(n, true); } bool qpwgraph_toposort::nodeIsTrueSource ( qpwgraph_node *n ) { return countInputPorts(n) == 0; } bool qpwgraph_toposort::nodeIsEffectiveSource ( qpwgraph_node *n ) { return countInputConnections(n) == 0; } bool qpwgraph_toposort::nodeIsTrueSink ( qpwgraph_node *n ) { return countOutputPorts(n) == 0; } // Returns all nodes directly connected to output ports of the given node QSet qpwgraph_toposort::childNodes ( qpwgraph_node *n ) { auto children = QSet(); foreach (qpwgraph_port *p, n->ports()) { if (p->isInput()) { continue; } foreach (qpwgraph_connect *c, p->connects()) { qpwgraph_node *n1 = c->port1()->portNode(); qpwgraph_node *n2 = c->port2()->portNode(); if (n1 == n2) { continue; } if (n1 == n) { children << n2; } else { children << n1; } } } return children; } QSet qpwgraph_toposort::connectedParentPorts ( qpwgraph_node *n ) { QSet portList; foreach (qpwgraph_port *p, n->ports()) { if (!p->isInput()) { continue; } foreach (qpwgraph_connect *c, p->connects()) { qpwgraph_node *n1 = c->port1()->portNode(); qpwgraph_node *n2 = c->port2()->portNode(); if (n1 == n2) { // self feedback continue; } if (m_nodeRanks[n1] > m_nodeRanks[n] || m_nodeRanks[n2] > m_nodeRanks[n]) { // left-pointing edge in a cycle continue; } if (c->port1()->portNode() == n) { portList << c->port2(); } else { portList << c->port1(); } } } return portList; } QSet qpwgraph_toposort::connectedInputPorts ( qpwgraph_node *n ) { QSet portList; foreach (qpwgraph_port *p, n->ports()) { if (!p->isInput()) { continue; } foreach (qpwgraph_connect *c, p->connects()) { qpwgraph_node *n1 = c->port1()->portNode(); qpwgraph_node *n2 = c->port2()->portNode(); if (n1 == n2) { // self feedback continue; } if (m_nodeRanks[n1] > m_nodeRanks[n] || m_nodeRanks[n2] > m_nodeRanks[n]) { // left-pointing edge in a cycle continue; } portList << p; } } return portList; } bool qpwgraph_toposort::compareNodes(qpwgraph_node *n1, qpwgraph_node *n2) { if (m_nodeRanks[n1] < m_nodeRanks[n2]) { return true; } if (m_nodeRanks[n2] < m_nodeRanks[n1]) { return false; } if (n1->ports().empty() && !n2->ports().empty()) { return true; } if (n2->ports().empty() && !n1->ports().empty()) { return false; } if (!(n1->ports().empty() || n2->ports().empty())) { if (n1->ports().first()->portType() < n2->ports().first()->portType()) { return true; } if (n2->ports().first()->portType() < n1->ports().first()->portType()) { return false; } } if (countInputPorts(n1) < countInputPorts(n2)) { return true; } if (countInputPorts(n2) < countInputPorts(n1)) { return false; } if (countOutputPorts(n1) < countOutputPorts(n2)) { return true; } if (countOutputPorts(n2) < countOutputPorts(n1)) { return false; } if (n1->nodeName() < n2->nodeName()) { return true; } if (n2->nodeName() < n1->nodeName()) { return false; } return false; } bool qpwgraph_toposort::compareNodesByConnectionLocation ( qpwgraph_node *n1, qpwgraph_node *n2 ) { // Compare port types first so that e.g. MIDI-only nodes still appear above audio nodes if (n1->ports().empty() && !n2->ports().empty()) { return true; } if (n2->ports().empty() && !n1->ports().empty()) { return false; } if (!(n1->ports().empty() || n2->ports().empty())) { if (n1->ports().first()->portType() < n2->ports().first()->portType()) { return true; } if (n2->ports().first()->portType() < n1->ports().first()->portType()) { return false; } } // Compare source port locations return meanParentPortY({n1}, m_newPositions) < meanParentPortY({n2}, m_newPositions); } qreal qpwgraph_toposort::meanPortY ( const QSet& ports, const QHash& positions ) { if (ports.empty()) { return -std::numeric_limits::infinity(); } qreal y = 0; foreach (qpwgraph_port *p, ports) { y += p->pos().y() + positions[p->portNode()].y(); } return y / ports.size(); } qreal qpwgraph_toposort::meanParentPortY ( const QList& nodes, const QHash& positions ) { auto ports = QSet(); foreach (qpwgraph_node *n, nodes) { ports |= connectedParentPorts(n); } return meanPortY(ports, positions); } qreal qpwgraph_toposort::meanInputPortY ( const QList& nodes, const QHash& positions ) { auto ports = QSet(); foreach (qpwgraph_node *n, nodes) { ports |= connectedInputPorts(n); } return meanPortY(ports, positions); } // end of qpwgraph_toposort.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_toposort.h0000644000000000000000000000013215225434775017202 xustar0030 mtime=1784035837.891146741 30 atime=1784035837.891146741 30 ctime=1784035837.891146741 qpwgraph-1.0.3/src/qpwgraph_toposort.h0000644000175000001440000000643315225434775017200 0ustar00rncbcusers// qpwgraph_toposort.h // /**************************************************************************** Copyright (C) 2025, Mike Bourgeous. 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_toposort_h #define __qpwgraph_toposort_h #include "qpwgraph_node.h" #include //---------------------------------------------------------------------------- // qpwgraph_toposort -- Topological sort and related code for graph nodes decl. // // Contains code for topologically sorting nodes, as well as arranging those // nodes based on the sort. // // This class is non-reentrant, so use each instance just once from a single // function. class qpwgraph_toposort { public: //Constructor. qpwgraph_toposort(const QList& nodes); // Call this to perform ranking and node arrangement. Returns the map // of new positions without applying those positions to the nodes. const QHash& arrange(const QRect& viewportRect); protected: void rankAndSort(); void visitNode(const QSet& path, qpwgraph_node *n); void sortNodesByRank(QList& nodes); void sortNodesByConnectionY(QList& nodes); static qsizetype countInputPorts(qpwgraph_node *n); static qsizetype countOutputPorts(qpwgraph_node *n); static qsizetype countExternalConnections(qpwgraph_node *n, bool input); static qsizetype countInputConnections(qpwgraph_node *n); static bool nodeIsTrueSource(qpwgraph_node *n); static bool nodeIsEffectiveSource(qpwgraph_node *n); static bool nodeIsTrueSink(qpwgraph_node *n); static QSet childNodes(qpwgraph_node *n); QSet connectedParentPorts(qpwgraph_node *n); QSet connectedInputPorts(qpwgraph_node *n); QSet connectedOutputPorts(qpwgraph_node *n); qreal meanPortY(const QSet& ports, const QHash& positions); qreal meanParentPortY(const QList& nodes, const QHash& positions); qreal meanInputPortY(const QList& nodes, const QHash& positions); bool compareNodes(qpwgraph_node *n1, qpwgraph_node *n2); bool compareNodesByConnectionLocation(qpwgraph_node *n1, qpwgraph_node *n2); private: // Instance variables QList m_inputNodes; QList m_unvisitedNodes; QHash m_newPositions; QHash m_nodeRanks; }; #endif /* __qpwgraph_toposort_h */ // end of qpwgraph_toposort.h qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_systray.h0000644000000000000000000000013215225434775017027 xustar0030 mtime=1784035837.891146741 30 atime=1784035837.890146755 30 ctime=1784035837.891146741 qpwgraph-1.0.3/src/qpwgraph_systray.h0000644000175000001440000000424615225434775017025 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-1.0.3/src/PaxHeaders/qpwgraph.cpp0000644000000000000000000000013215225434775015564 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.881713957 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/qpwgraph.cpp0000644000175000001440000002140115225434775015552 0ustar00rncbcusers// qpwgraph.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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_XUNIQUE #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_XUNIQUE , m_memory(nullptr), m_server(nullptr) #endif , m_patchbay_activated(-1) , m_patchbay_exclusive(-1) , m_start_minimized(false) , m_remote_name("pipewire-0") { 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_XUNIQUE 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"; const QString s_remote_name = "remote"; const QString s_help = "help"; parser.addOption({{"a", s_activated}, QObject::tr("Activated patchbay.")}); parser.addOption({{"d", s_deactivated}, QObject::tr("Deactivated patchbay.")}); parser.addOption({{"x", s_exclusive}, QObject::tr("Exclusive patchbay.")}); parser.addOption({{"n", s_nonexclusive}, QObject::tr("Non-exclusive patchbay.")}); parser.addOption({{"m", s_minimized}, QObject::tr("Start minimized.")}); parser.addOption({{"r", s_remote_name}, QObject::tr("Remote daemon name."), QObject::tr("name")}); parser.addOption({{"?", s_help}, QObject::tr("Displays help on command-line options.")}); 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(s_help)) { 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 #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) out << ' ' << '('; out << QApplication::platformName(); out << ')'; #endif out << '\n'; out << QString("PipeWire: %1 (headers: %2)") .arg(pw_get_library_version()) .arg(pw_get_headers_version()); 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); if (parser.isSet(s_remote_name)) m_remote_name = parser.value(s_remote_name); 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_XUNIQUE // 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; } QString uremote = QString::fromUtf8(::getenv("PIPEWIRE_REMOTE")); if (uremote.isEmpty()) uremote = m_remote_name; if (!uremote.isEmpty()) { m_unique += ':'; m_unique += uremote; m_remote_name = uremote; } 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_XUNIQUE //---------------------------------------------------------------------------- // main. int main ( int argc, char *argv[] ) { Q_INIT_RESOURCE(qpwgraph); qpwgraph_application app(argc, argv); if (!app.parse_args(app.arguments())) { app.quit(); return 1; } #ifdef CONFIG_XUNIQUE // 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-1.0.3/src/PaxHeaders/qpwgraph_pipewire.h0000644000000000000000000000013215225434775017135 xustar0030 mtime=1784035837.889146769 30 atime=1784035837.889146769 30 ctime=1784035837.889146769 qpwgraph-1.0.3/src/qpwgraph_pipewire.h0000644000175000001440000001016415225434775017127 0ustar00rncbcusers// qpwgraph_pipewire.h // /**************************************************************************** Copyright (C) 2021-2026, 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. static void resetPortTypeColors(qpwgraph_canvas *canvas); // 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); // Remote name accessors. void setRemoteName(const QString& remote_name); const QString& remoteName() const; signals: void changed(); public 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; 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; // PipeWire remote daemon name. QString m_remote_name; }; #endif // __qpwgraph_pipewire_h // end of qpwgraph_pipewire.h qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_canvas.cpp0000644000000000000000000000013215225434775017117 xustar0030 mtime=1784035837.882146865 30 atime=1784035837.882146865 30 ctime=1784035837.882146865 qpwgraph-1.0.3/src/qpwgraph_canvas.cpp0000644000175000001440000014647015225434775017123 0ustar00rncbcusers// qpwgraph_canvas.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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 "qpwgraph_toposort.h" #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_port2(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_merger_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); } if (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::canSearchItem (void) const { return !m_nodes.isEmpty(); } 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::canArrangeNodes (void) const { int n = 0; foreach (QGraphicsItem *item, m_scene->selectedItems()) { if (item->type() == qpwgraph_node::Type && ++n > 1) return true; } return false; } // 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 ( const qpwgraph_node::NodeIdKey& node_key ) const { return m_node_ids.value(node_key, nullptr); } qpwgraph_node *qpwgraph_canvas::findNode ( uint id, qpwgraph_item::Mode mode, uint type ) const { return findNode(qpwgraph_node::NodeIdKey(id, mode, type)); } 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); if (m_port2) { m_port2->setHighlight(false); m_port2 = nullptr; } 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 && !port2->isHighlight() && port1->portType() == port2->portType() && port1->portMode() != port2->portMode()) { m_port2 = port2; m_port2->setHighlight(true); } } 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_port2) { m_port2->setHighlight(false); m_port2 = nullptr; } 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::searchItem (void) { if (!m_search_editor->isEnabled()) startSearchEditor(); } 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(); } // 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); } // Centers the view on the middle of the graph, then makes sure each selected // item is visible if showSelected is true. void qpwgraph_canvas::centerView (bool showSelected) { m_scene->setSceneRect(boundingRect(true)); QGraphicsView::centerOn(m_scene->itemsBoundingRect().center()); if (showSelected) { foreach (QGraphicsItem *item, m_scene->selectedItems()) { ensureVisible(item); } } } // 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 int n = nodeNum(node); 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; // Sure node keys hasn't been released before?... if (findNode(qpwgraph_node::NodeIdKey(node)) != node) return false; // Assume node name-keys are to be removed after this... // const int n = nodeNum(node); 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; } // Node number if multiple (else count). int qpwgraph_canvas::nodeNum ( qpwgraph_node *node ) const { int n = 0; const QList& nodes = findNodes(qpwgraph_node::NodeNameKey(node)); const QList::ConstIterator& iter_end = nodes.constEnd(); QList::ConstIterator iter = nodes.constBegin(); while (iter != iter_end) { ++n; if ((*iter)->nodeId() == node->nodeId()) break; ++iter; } return n; } // 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_port2) { m_port2->setHighlight(false); m_port2 = nullptr; } 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); } // Node rearrangement by topological sort. // void qpwgraph_canvas::arrangeNodes (void) { 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); } } if (nodes.size() < 2) return; qpwgraph_toposort topo(nodes); auto newPositions = topo.arrange(QGraphicsView::viewport()->rect()); qpwgraph_move_command *mc = new qpwgraph_move_command(this, newPositions); foreach (qpwgraph_node *n, newPositions.keys()) { n->setPos(newPositions.value(n)); } if (isRepelOverlappingNodes()) { foreach (qpwgraph_node *node, nodes) repelOverlappingNodes(node, mc); } m_commands->push(mc); centerView(true); // Repaint to avoid glitch trails m_scene->update(); } // 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; } // Merger/unify list management accessors. void qpwgraph_canvas::setMergerNodesEnabled ( bool enabled ) { m_merger_enabled = enabled; } bool qpwgraph_canvas::isMergerNodesEnabled (void) const { return m_merger_enabled; } void qpwgraph_canvas::setMergerNodesList ( const QStringList& nodes ) { m_merger_nodes = nodes; } const QStringList& qpwgraph_canvas::mergerNodesList (void) const { return m_merger_nodes; } bool qpwgraph_canvas::isMergerNodes ( const QString& node_name ) const { if (!m_merger_enabled) return false; QStringListIterator iter(m_merger_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-1.0.3/src/PaxHeaders/qpwgraph_options.cpp0000644000000000000000000000013215225434775017337 xustar0030 mtime=1784035837.887146797 30 atime=1784035837.887146797 30 ctime=1784035837.887146797 qpwgraph-1.0.3/src/qpwgraph_options.cpp0000644000175000001440000003140415225434775017331 0ustar00rncbcusers// qpwgraph_options.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 "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; m_dirty_merger = 0; // Setup current options... qpwgraph_config *config = parent->config(); if (config) { config->loadComboBoxHistory(m_ui.MergerNodesNameComboBox); 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... // if (config) { 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()); } // Merger/unify list management... // if (config) { m_ui.MergerNodesEnabledCheckBox->setChecked( config->isMergerNodesEnabled()); m_ui.MergerNodesNameComboBox->lineEdit()->setClearButtonEnabled(true); m_ui.MergerNodesNameComboBox->lineEdit()->setPlaceholderText( m_ui.MergerNodesNameComboBox->toolTip()); m_ui.MergerNodesNameComboBox->setCurrentText(QString()); m_ui.MergerNodesListWidget->clear(); m_ui.MergerNodesListWidget->addItems( config->mergerNodesList()); } // 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(stabilize())); 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.MergerNodesEnabledCheckBox, SIGNAL(stateChanged(int)), SLOT(changedMergerNodes())); QObject::connect(m_ui.MergerNodesNameComboBox, SIGNAL(editTextChanged(const QString&)), SLOT(stabilize())); QObject::connect(m_ui.MergerNodesAddToolButton, SIGNAL(clicked()), SLOT(addMergerNodes())); QObject::connect(m_ui.MergerNodesListWidget, SIGNAL(itemSelectionChanged()), SLOT(selectMergerNodes())); QObject::connect(m_ui.MergerNodesRemoveToolButton, SIGNAL(clicked()), SLOT(removeMergerNodes())); QObject::connect(m_ui.MergerNodesClearToolButton, SIGNAL(clicked()), SLOT(clearMergerNodes())); 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); if (m_dirty_merger > 0) { config->setMergerNodesEnabled( m_ui.MergerNodesEnabledCheckBox->isChecked()); QStringList nodes; const int n = m_ui.MergerNodesListWidget->count(); for (int i = 0; i < n; ++i) { QListWidgetItem *item = m_ui.MergerNodesListWidget->item(i); if (item) nodes.append(item->text()); } config->setMergerNodesList(nodes); config->setMergerNodesDirty(true); m_dirty_merger = 0; } config->saveComboBoxHistory(m_ui.MergerNodesNameComboBox); 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) { QListWidgetItem *item = m_ui.FilterNodesListWidget->currentItem(); if (item) m_ui.FilterNodesNameComboBox->setEditText(item->text()); 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(); } // Merger/unify list management... // void qpwgraph_options::changedMergerNodes (void) { ++m_dirty_merger; changed(); } void qpwgraph_options::selectMergerNodes (void) { QListWidgetItem *item = m_ui.MergerNodesListWidget->currentItem(); if (item) m_ui.MergerNodesNameComboBox->setEditText(item->text()); stabilize(); } void qpwgraph_options::addMergerNodes (void) { const QString& node_name = m_ui.MergerNodesNameComboBox->currentText(); if (node_name.isEmpty()) return; m_ui.MergerNodesListWidget->addItem(node_name); m_ui.MergerNodesListWidget->setCurrentRow( m_ui.MergerNodesListWidget->count() - 1); const int i = m_ui.MergerNodesNameComboBox->findText(node_name); if (i >= 0) m_ui.MergerNodesNameComboBox->removeItem(i); m_ui.MergerNodesNameComboBox->insertItem(0, node_name); m_ui.MergerNodesNameComboBox->setEditText(QString()); m_ui.MergerNodesListWidget->setFocus(); changedMergerNodes(); } void qpwgraph_options::removeMergerNodes (void) { const int i = m_ui.MergerNodesListWidget->currentRow(); if (i < 0) return; QListWidgetItem *item = m_ui.MergerNodesListWidget->takeItem(i); if (item) delete item; changedMergerNodes(); } void qpwgraph_options::clearMergerNodes (void) { m_ui.MergerNodesListWidget->clear(); changedMergerNodes(); } // 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); } if (m_ui.MergerNodesEnabledCheckBox->isChecked()) { m_ui.MergerNodesNameComboBox->setEnabled(true); m_ui.MergerNodesListWidget->setEnabled(true); const QString& node_name = m_ui.MergerNodesNameComboBox->currentText(); m_ui.MergerNodesAddToolButton->setEnabled(!node_name.isEmpty() && m_ui.MergerNodesListWidget->findItems(node_name, Qt::MatchFixedString).isEmpty()); const int i = m_ui.MergerNodesListWidget->currentRow(); m_ui.MergerNodesRemoveToolButton->setEnabled(i >= 0); m_ui.MergerNodesClearToolButton->setEnabled( m_ui.MergerNodesListWidget->count() > 0); } else { m_ui.MergerNodesNameComboBox->setEnabled(false); m_ui.MergerNodesListWidget->setEnabled(false); m_ui.MergerNodesAddToolButton->setEnabled(false); m_ui.MergerNodesRemoveToolButton->setEnabled(false); m_ui.MergerNodesClearToolButton->setEnabled(false); } m_ui.DialogButtonBox->button(QDialogButtonBox::Ok)->setEnabled(m_dirty > 0); } // end of qpwgraph_options.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_config.h0000644000000000000000000000013215225434775016556 xustar0030 mtime=1784035837.884146838 30 atime=1784035837.884146838 30 ctime=1784035837.884146838 qpwgraph-1.0.3/src/qpwgraph_config.h0000644000175000001440000001237715225434775016560 0ustar00rncbcusers// qpwgraph_config.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_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 setMergerNodesEnabled(bool enabled); bool isMergerNodesEnabled() const; void setMergerNodesList(const QStringList& nodes); const QStringList& mergerNodesList() const; void setMergerNodesDirty(bool dirty); bool isMergerNodesDirty() 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); // Widget geometry persistence helpers. void loadWidgetGeometry(QWidget *widget); void saveWidgetGeometry(QWidget *widget); 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_merger_enabled; QStringList m_merger_nodes; bool m_merger_dirty; bool m_start_minimized; }; #endif // __qpwgraph_config_h // end of qpwgraph_config.h qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_command.h0000644000000000000000000000013215225434775016727 xustar0030 mtime=1784035837.883146851 30 atime=1784035837.883146851 30 ctime=1784035837.883146851 qpwgraph-1.0.3/src/qpwgraph_command.h0000644000175000001440000001163515225434775016725 0ustar00rncbcusers// qpwgraph_command.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_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: // Constructors. qpwgraph_move_command(qpwgraph_canvas *canvas, const QList& nodes, const QPointF& pos1, const QPointF& pos2, qpwgraph_command *parent = nullptr); qpwgraph_move_command(qpwgraph_canvas *canvas, const QHash& positions); // 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. virtual bool execute(bool is_undo) override; private: // Command arguments. QHash m_items; int m_nexec; bool m_center; }; //---------------------------------------------------------------------------- // 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-1.0.3/src/PaxHeaders/qpwgraph_connect.h0000644000000000000000000000013215225434775016742 xustar0030 mtime=1784035837.884146838 30 atime=1784035837.884146838 30 ctime=1784035837.884146838 qpwgraph-1.0.3/src/qpwgraph_connect.h0000644000175000001440000000525415225434775016740 0ustar00rncbcusers// qpwgraph_connect.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_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-1.0.3/src/PaxHeaders/qpwgraph_patchman.h0000644000000000000000000000013215225434775017104 xustar0030 mtime=1784035837.888146782 30 atime=1784035837.888146782 30 ctime=1784035837.888146782 qpwgraph-1.0.3/src/qpwgraph_patchman.h0000644000175000001440000000433015225434775017074 0ustar00rncbcusers// qpwgraph_patchman.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_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 addClicked(); 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_add_button; 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-1.0.3/src/PaxHeaders/qpwgraph_main.cpp0000644000000000000000000000013215225434775016570 xustar0030 mtime=1784035837.885146824 30 atime=1784035837.885146824 30 ctime=1784035837.885146824 qpwgraph-1.0.3/src/qpwgraph_main.cpp0000644000175000001440000014473215225434775016573 0ustar00rncbcusers// qpwgraph_main.cpp // /**************************************************************************** Copyright (C) 2021-2026, 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 #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); // Status-bar labels... m_remote_label = new QLabel(); m_remote_label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); m_remote_label->setFixedHeight(22); m_remote_label->setMinimumWidth(66); m_ui.StatusBar->addPermanentWidget(m_remote_label); m_status_label = new QLabel(); m_status_label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); m_status_label->setFixedHeight(22); m_status_label->setMinimumWidth(33); m_ui.StatusBar->addPermanentWidget(m_status_label); // 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.editSearchItemAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(searchItem())); QObject::connect(m_ui.editRenameItemAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(renameItem())); QObject::connect(m_ui.viewMenubarAction, SIGNAL(triggered(bool)), SLOT(viewMenubar(bool))); QObject::connect(m_ui.viewStatusbarAction, SIGNAL(triggered(bool)), SLOT(viewStatusbar(bool))); QObject::connect(m_ui.viewGraphToolbarAction, SIGNAL(triggered(bool)), SLOT(viewGraphToolbar(bool))); QObject::connect(m_ui.viewPatchbayToolbarAction, SIGNAL(triggered(bool)), SLOT(viewPatchbayToolbar(bool))); 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))); QObject::connect(m_ui.viewArrangeNodesAction, SIGNAL(triggered(bool)), m_ui.graphCanvas, SLOT(arrangeNodes())); 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, '.')); qpwgraph_pipewire::resetPortTypeColors(m_ui.graphCanvas); #ifdef CONFIG_ALSA_MIDI qpwgraph_alsamidi::resetPortTypeColors(m_ui.graphCanvas); #endif restoreState(); m_patchbay_untitled = 0; 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 (m_pipewire) { const QString& remote_name = app->remoteName(); if (remote_name != m_pipewire->remoteName()) { m_pipewire->setRemoteName(remote_name); m_pipewire->reset(); } } 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(); const QString path(m_patchbay_path); if (path.isEmpty() || !patchbayOpenFile(path)) { qpwgraph_patchbay *patchbay = m_ui.graphCanvas->patchbay(); if (patchbay) patchbay->snap(); // Simulate patchbayNew()! ++m_patchbay_untitled; } updatePatchbayNames(); 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()); m_ui.graphCanvas->setMergerNodesEnabled(m_config->isMergerNodesEnabled()); m_ui.graphCanvas->setMergerNodesList(m_config->mergerNodesList()); int nrefresh = 0; if (m_config->isFilterNodesDirty()) { m_config->setFilterNodesDirty(false); ++nrefresh; } if (m_config->isMergerNodesDirty()) { m_config->setMergerNodesDirty(false); ++nrefresh; } if (nrefresh > 0) 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()); if (m_config) m_config->loadWidgetGeometry(&patchman); patchman.exec(); if (m_config) m_config->saveWidgetGeometry(&patchman); } // View 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) { m_ui.graphCanvas->centerView(false); 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(); qpwgraph_pipewire::resetPortTypeColors(m_ui.graphCanvas); #ifdef CONFIG_ALSA_MIDI qpwgraph_alsamidi::resetPortTypeColors(m_ui.graphCanvas); #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(); } // Help menu slots. 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 #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) text += ' '; text += '('; text += QApplication::platformName(); text += ')'; #endif text += ", "; text += tr("PipeWire %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.graphConnectAction->setEnabled(canvas->canConnect()); m_ui.graphDisconnectAction->setEnabled(canvas->canDisconnect()); 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.editSelectNoneAction->setEnabled( !canvas->scene()->selectedItems().isEmpty()); m_ui.editSearchItemAction->setEnabled( canvas->canSearchItem()); m_ui.editRenameItemAction->setEnabled( canvas->canRenameItem()); m_ui.viewArrangeNodesAction->setEnabled(canvas->canArrangeNodes()); #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 if (m_pipewire) m_remote_label->setText(m_pipewire->remoteName()); else m_remote_label->clear(); if (is_dirty) m_status_label->setText(tr("MOD")); else m_status_label->clear(); } // 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); 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); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) mbox.setOptions(QMessageBox::Option::DontUseNativeDialog); #endif 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); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) mbox.setOptions(QMessageBox::Option::DontUseNativeDialog); #endif 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 ) { #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) QStringList args; args << QApplication::applicationName(); args << "-platform" << QApplication::platformName(); args << "-session" << sm.sessionId(); sm.setRestartCommand(args); #endif sm.release(); m_config->setSessionStartMinimized(!isVisible() && !isMinimized()); } // end of qpwgraph_main.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_thumb.h0000644000000000000000000000013215225434775016430 xustar0030 mtime=1784035837.891146741 30 atime=1784035837.891146741 30 ctime=1784035837.891146741 qpwgraph-1.0.3/src/qpwgraph_thumb.h0000644000175000001440000000426115225434775016423 0ustar00rncbcusers// qpwgraph_thumb.h // /**************************************************************************** Copyright (C) 2018-2026, 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-1.0.3/src/PaxHeaders/qpwgraph_thumb.cpp0000644000000000000000000000013215225434775016763 xustar0030 mtime=1784035837.891146741 30 atime=1784035837.891146741 30 ctime=1784035837.891146741 qpwgraph-1.0.3/src/qpwgraph_thumb.cpp0000644000175000001440000002055415225434775016761 0ustar00rncbcusers// qpwgraph_thumb.cpp // /**************************************************************************** Copyright (C) 2018-2026, 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::setAutoFillBackground(true); 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-1.0.3/src/PaxHeaders/qpwgraph_options.h0000644000000000000000000000013215225434775017004 xustar0030 mtime=1784035837.887146797 30 atime=1784035837.887146797 30 ctime=1784035837.887146797 qpwgraph-1.0.3/src/qpwgraph_options.h0000644000175000001440000000374215225434775017002 0ustar00rncbcusers// qpwgraph_options.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_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(); // Merger/unify list management slots. void selectMergerNodes(); void addMergerNodes(); void removeMergerNodes(); void clearMergerNodes(); void changedMergerNodes(); void stabilize(); private: // The Qt-designer UI struct... Ui::qpwgraph_options m_ui; int m_dirty; int m_dirty_filter; int m_dirty_merger; }; #endif // __qpwgraph_options_h // end of qpwgraph_options.h qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_main.h0000644000000000000000000000013015225434775016233 xustar0029 mtime=1784035837.88614681 30 atime=1784035837.885146824 29 ctime=1784035837.88614681 qpwgraph-1.0.3/src/qpwgraph_main.h0000644000175000001440000001375015225434775016233 0ustar00rncbcusers// qpwgraph_main.h // /**************************************************************************** Copyright (C) 2021-2026, 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 QLabel; 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(); // View 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); // Help menu slots. 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; QLabel *m_status_label; QLabel *m_remote_label; 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-1.0.3/src/PaxHeaders/man10000644000000000000000000000013215225434775014006 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/man1/0000755000175000001440000000000015225434775014053 5ustar00rncbcusersqpwgraph-1.0.3/src/man1/PaxHeaders/qpwgraph.10000644000000000000000000000013215225434775015776 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/man1/qpwgraph.10000644000175000001440000000223315225434775015766 0ustar00rncbcusers.TH qpwgraph "1" "April 16, 2026" .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-line options. .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\-d\fR, \fB\-\-deactivated\fR .IP Deactivated patchbay. .HP \fB\-x\fR, \fB\-\-exclusive\fR .IP Exclusive patchbay. .HP \fB\-n\fR, \fB\-\-nonexclusive\fR .IP Non-exclusive patchbay. .HP \fB\-m\fR, \fB\-\-minimized\fR .IP Start minimized. .HP \fB\-r\fR, \fB\-\-remote\fR <\fIname\fR> .IP Remote daemon name. .HP \fB\-?\fR, \fB\-\-help\fR .IP Displays help on 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-1.0.3/src/PaxHeaders/images0000644000000000000000000000013215225434775014417 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.876250108 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/0000755000175000001440000000000015225434775014464 5ustar00rncbcusersqpwgraph-1.0.3/src/images/PaxHeaders/itemExclusive.png0000644000000000000000000000013215225434775020030 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemExclusive.png0000644000175000001440000000245515225434775020026 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<ºIDAT8�u•mLSWǽ•¶8ŒQhyÉœ^âh%hÑe�¡D¶ÁøÈˆÙXŒ cñ‹‹‰”e‹›ÄeÆ 7;ÅT¿°�À`F¦‹·$€cš‘¶È`+°¥ÞÞ{Ï>pŠ ÇóåÜœsîïœó?Ïÿ9&Ö fÙšd¿ @—­Xëçµ€ë�$ Ø Xåø3à ð˜þù¿VƒM€p®ŠŠŠ·+++_ßµkWªÃá°<~ü82<<<ÝÙÙyûúõë]À0ð ®„¯+€ ÈÈËË«:sæÌ‘’’’Ô©©)µ¯¯oÎï÷ÿmÉÉÉ ûöíKLOO�˜>vìØ·£££�€ˆÈÝÿl²öïß_wéÒ¥EQNŸ>}÷ìÙ³¿äçço³Ûí› ÃX'„P4M[çp8[ZZrm6›Q[[ë¾yóæyà¡”j9ÌÀ–œœœÆ©©©ÈÄÄÄÂÎ�;¿Þr:�ŸkšfˆUÑÐÐðsVVV›×ë]ƒ‘íÛ·7[$kYŽàà�7‚333ªÓéü p{›››Óu}¨ëºJJú(r¹\­³³³jOOO8¼SB¶–••�Óu]466ö{å¾ÛßßÖ4M!D4³³³šÓéüÈ6{›ššt]øØ*™X€Â .Ü…BªÕj­—Д´´´OUU5„Â0 ¡ªª¨¨¨µZ­/q€#>>þ“P(mkk^“Ll@ù½{÷ÂÝÝÝ�äq€mÇ�ÿqµ¶BqëÖ-�/Áñ@yww÷ôÐÐP(l1gÅ'''Ûü~ÿS`ЀˆÏç{Ø××÷tå-ëºN{{ûïr^Ì�ó�@ ìp8âåBf%–ËÅ�FWÚt±££ã×ÞÞÞ)!ž›ª¥¥eÄív·Ærªªê6›-æZÖÉ#sssªÝnO`ÉÊ&¹ë¸C‡¥†ÁÈÈÈ³ŽŽŽAÀÏ’„­w8fffTÙ¯+rpÞëõ>)((°)Rw%%%%.;;;Þd2¡( .—ËÚÚÚúÏë†I~§ìÞ½Û>>>><•Lâ€õõõ? !Dee¥È•˜›˜˜xÎçóE…" i{öìùBf„YBs«ªª® !D]]]'°CŽa6Y,–š,†­Vë–\´£´´ô!„Ð4MTWWwò‚â€4«Õzdhh(ìõzÌfó{2·—K…È©©©¹�F �Çã3›Íï\¹reB!šššîo²TB7ÛÌfóûW¯^õ©ªjTWW{X2�ee™ää’“'OÞÑu]tuuM§¦¦~wùòå ·Û}¨œ@P–��ýYooﴮ뢹¹ùP,å{¡Æ+@"PzôèÑžP( Ú©S§‚ååå· ¿/..>øðá.�ÇóÇüü¼‡£õõõÝ@©<‰² 3°(ÈÌÌí©ÝÃ"¾�ÒÑ’*¡ N¦rý“ÑVKb»Æhï�³æÞ{î9{ðÄe{Ú‹ƒýàpáwïý}ç÷}çû�1†£ ñH«ÿðoÂöÏ„ "€ ¾ß`ñÏJ9Z–£ee „1FÿÀ‹×ð8 Àp‚ç 9„çìÞPùFvøó€ëÞ�1v¸T8wíÚµŸS©”ÑÖÖ¶¨(ŠþêÕ+šH$¬ÁÁÁÍ7n¼I$dee…ÎÌÌ�Ë—/§âñ¸ÙÝÝ¿råÊÊøø¸~þüùïÔ1Æþ¢H�SvB–e]]]Åõë×Ï444Ø¢Ñ(õûýB(òNLLÐÝÝ]qzzÚjoo—:;;ë|>Ÿ- ¶ž>}Zzþü¹677÷†w| 2§¦€€Ã4ÍŠb±J©”N§ …ÞÅb1¦ë:vvv„x<Îf‡‡‡GGGßjš†@ E£Qóþýû?Y–¥Ø;pÀŸ8E)­ „À0  ܺuë½ .ˆš¦Á4M˜¦ k�?þ%—˽ÝÛÛcªªâáÇ/u]Ÿ�áZ4\ºt黥¥¥Ýn÷EBÈqBt]G6›Å½{÷6#‘E¢(ÂãñÀ‡}}}_Õ×××»\.A–eܼy³¥²²²€·tBKollüÈçóU�ŒŒ|æt:ŵµ5 ªªª´¹¹™ÜÚÚªQUµZ×u8áÁƒŸvuuIcccÆúú:ž={f�Š¡¡¡oîÞ½«X–õ;½Ô�6;;»<55E!b&“Áüü�p8ìUå�$I† TÅb:�Þnjjª1M²,áp8VÒ·`ïééùâÎ�;߯b1fš&·@)cÌ–Íf‘N§a·Û«nß¾ý±Ýn¥„<}út»··÷D$ÑZ[[kR©Ôüää¤RNQMuuõÙªªª¯ƒÁàE¿ßß�ËåBDQdÅb‘ÖÖÖÚö÷÷±ººšÝØØHJ’¤ 6›MÏårÛ^¯×Ma²,›�=úÀoŒ±B @â&óø€ƒwÇØš››O¹\.Çòòò¤®ë/¸‘(?ï†#¸fo¨Œ1K(]™ÜÍ6 «ceô1þ³ƒóªâ`È‘²÷åÁÀ'é!EGÿýíÈþZA DÅî IEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewZoomIn.png0000644000000000000000000000013215225434775017310 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/viewZoomIn.png0000644000175000001440000000260215225434775017300 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<ÿIDAT8��•]LSgÇç´¥e¥ÇW) ̯ÄL[R‰Î�øÉœ^�x1Ñ™¸dÆçÌ.–lf1a7$ÓÝl ºl¡3F¨ˆVB”#È‚±¥ T -‚ ØžÚö삃!·dOòæ¹zïó¼ÿçCàßM¼äÅ@.`D „€~à ð ˆ €ð  B³�Ò+Vì>xðà¶²²²Ììì,-@ HÜ¿ß5ÙØØØÕÒÒR ÜVˆÊ»ÀZ OާN�úúÈ‘/s_¿~-øý~&&&0›Íddd MMMÓUU?þ622rè �g#­¸páü;wîLëì줹¹9*IžúýþAY–å¼¼¼\›ÍV´{÷'&«ÕŠ$=ˆŸ8qâìðððÀ³Ð¤{Nž<ùäåËÊùó(›6mê¾ߪg?ptõêÕÍuuµñ‡{”êêêˆ(ŠÇËBÑþ Ž&nÞt+[·n+�Ï{{{Ã×®]‹Z,–o€åÀö•+W6¶·ßMܺժ”——÷ëÅy`P¸oß¾m‰DB¸uëvÌívÿt Àj4“b±˜V¯×$ž¾¾¾‹uuΠÅbÁf³-Šç‚ßVBII‰ett”žžž 0²hÑ¢õ_dggoQEÐëõ$''Û­Vëáôôôr är¹î�F–.]¢7 K´ó"�”ŒŒtÝÔÔccc!ÀÒØØx°ÿgïKÒºº¤nà±fžp&`ÕªU«v¤¥¥¬]»F“ŸŸo4™Löîîn1�‡ëëë677ß ‡Ã/[EEÅÑššû£G}ÂåË—§\®«Õ@›0jì‡ã˜ÓY·K§Ó‰7n¸)..Â`0ðüùóDKË_ã^¯wT–åhQQQæŽÛ³GÒ½{÷p¹®ÊçÎ�ûyrr²æ@m‡ã«K—þÜ•••-z½^:::hoo�ÚívÝòåË„ÌÌLŒF#Z­–H$B  ·÷oZ[[Ç~‰D"U1eAM¿Äápw:ëvY,9¢×륭­�ÞÞÞ„Ëåº 7lذ¾¸¸8ßl6éEQ#Œ��dž††B’$Ý~úô© è@ul~XZZú}míŽ99Ö·PI’n·ûº×ë=Í̜ͬjv"ðU?­ŠÿvÐ*I]N«57) ÒÞÞŽ$I‰¦¦¦Æ���ÓÀ}`Š™vŸ=³”Þ¨^™[^Àìóù–¥¥¥.éï÷ ÝÝ7n¸¯û|¾ŸTè+W!oÔtcÌYEóMD|>ßP(²¦¦šsëë¼^ï™9ÐÄBÿËf'š™™eiUÿl@MÿAþÜØ2ý¡ãIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemConnect.png0000644000000000000000000000013215225434775017452 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemConnect.png0000644000175000001440000000164115225434775017444 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<IDAT8�¥•AL#UÆ3­ívSZpév¡MÄpáâ!LŒñ@=/ÄpXL¼c$á¢O$r¢—õdÂ�,'B8B‚ô%š–@]Òl´âÈ´Ì2,Бöµ}{èTl—t÷yïÍá÷Þÿ›ï{OáÕKi˜ËfÛ…©€ xÍ^W”€êË€Ø =€(¿'Η;€°Óé|°¾¾þ]0¼mY¦i�Çÿš��ýø¹°bCƒÀÈ·CCC^˲B`Y¥RéppÞ®k©ÚÐ.àÃéééo"‘ˆ?›Í²¹¹)óù¼B ëz0€òM-û�7�·�€/FGGSÇÇÇÕt:-gffª;À ðð‘Ý‘ÚêÄ*Ðíõzßñûý}���Ý===÷&''B%‹�ÇÿÈd2?¿çÔQ¢æ�¦åÞ‹F£{†aTs¹\Õ4MY(¤®ëryyYŽ��__!ÀÉ5ë¶:±8….]×!n·¯×‹aœœœ°¿¿ì§@…†pÔulUÕÃÃÃJ2™¼'¥|½¿¿_- œ��Q.—1MSÝÛÛû ø�Z0þ§e³’ÀE6›}"¥ccc!är9|>ápØ�m­ë2´+Ôbêí홟Ÿ¿ïr¹”��–––ž%“É¢eY¸\.‡ªªžfÐfàzî/..~êóùÔ­­-‰DaeeåQ"‘ØÈd2Å«««ººº.x�êÜ|@ø,�þ�ËåäÚÚšœ˜˜n·û!ðð.ðøè³»k ½KÍÜ_ß§4M«¦R)955U …BK6ð6à¶G�Ý]S)œöŽoÍÍÍÍE"‘7ŠÅ¢âñx”óós%‹É���Çš¦ÍO€«›Zo;�î@ ìèèpT*ŽŽŽØÝÝe{{;ŸJ¥~7êy\ž¦Óé§š¦y Ãàòò’ÓÓÓÂêêêC ˜í@á¿Û+@íÇôQÓ° èÀ¯À1µK¼­ª ßøÌ¨Ôòï3õ¸¶n¶nÖXÏóíHÖd)OëIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemPulse.png0000644000000000000000000000013215225434775017151 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemPulse.png0000644000175000001440000000263215225434775017144 0ustar00rncbcusers‰PNG  IHDRàw=ø pHYs  ÒÝ~ütEXtSoftwarewww.inkscape.org›î<'IDATH‰µ–MLTWÇïcf€ˆEèàˆ"¤¡$Zc)¶DÛn0)Ii* cŒ¤¤.L””…±FmH ‰�HÒB­ „fA‰©†¨ƒ ü*±„O'µ#¨�êÀÈ8μw»à�Ajwõ&wóî¹çœÿ9÷üÿOBð*—üJ½êÊ’$IF`“q.€  ®a£f@"@ÐÅŠ’¼@’$Ù¸´X¬üÀ 0m8‰Þ{x ˆ÷€YI’BBý¹S!FPx Ø|Y^^~­««ëñéÓ§ÿÞb�l%À¼ß××ç½téÒã²²²> Ø $Ês¿†sÙp¾Ýn·Ÿéëë['Nœ˜‰��?lÌË’1y©©©µ­­­{½^ÑÝÝ\¿~}ð�ÈÑjE3Úb·ÛϸÝîð­[·ôŠŠŠIà;àS£\1ÑöÙ@Pøðaïèè¨~ñâÅHNNN'PĶ(@&P}óæÍàØØ˜¨ªªòß˲\{òäÉ+¥BÅØ‰ÅÅÅŸÕÖÖþ | 4=zÔ722"Ξ=VUõ$àÔhS7îÛ·owFFFÌõë×õ¦¦& 9�ÎÏcccía£™©@ `ííí :ŽÜŽŽŽJ RSSsÁëõj‡C---Ý ä128öîÝ›¤þþþ@0ôUUUíŒ���;räH7ðxÏn·‘žž^ ¼ öìÙó‹Ãá°íß¿ÿCMÓ¦:;;½IIIäççÛ;`’ ÈI‰‰‰¦ññq†‡‡ZaaaúíÛ·ŸD"‘ÀºæææºÞÞÞ#­­­_@³Ùl¦ÙÙÙg@$333gçÎ�ékÖ¬‘-‹’–––e6›ßž�ˆÅb1šÇã™0™LÂl6KªªÆ²  ªª`2™�eY$!²,K€‡õññq¦¦¦eff]×£s�\ �Tuiv£ç2 ÍÏÏErr²”–––¨>Ÿ/¼jÕ*3`šœœ¼ÓÕÕ5îr¹´¡¡!mbbâ^$™LV«U]XXˆ¦M›69E‘‚Á Ðu=è* þG�iÙÙÙʶmÛ2zzz—Ëu¿¤¤dƒÅbÉ ƒ Õ6›m ùý~70§ªj�Ãá°¶µµyÓ®]»Þ”eYòù|4™%"ó8�λ�@€¼¼¼˜¸¸¸”æææ+^¯×üøñ��'zÍï÷ÿä÷ûú€ØÆÆÆ�dzÐÒÒrAQ”Õ¥¥¥«ÇÆÆ¼Lax¸Ï�;׿v»Cv»]®®®.Ôü0==í.**²A`ÆØ‹¹¹¹ŠßïPQQÑèÇŽû@×u¥¿¿?rùòåàŽ‘üóÑ;55µ¥½½=|õêU½²²ò!PgPE¶a#-ã¢uÀ'@meeåÝááaýÔ©S‘ììì.`»�ZZIvE)))Í���O\.—¨¯¯ŸµÙlß¹¼œì¾mkk›uuuO³²²~>bÙIÏO’À lŠwìØQZRR’/ËràСCåÀoQÈ,Q÷ÖóçÏ·{½^«Óé¼ÓÓÓÓôn`^¡± r4H”›’�dO€ß�)!Dذ3±$8F)þþf�GZ)úË$SeI6…ñ"ÿ!™2KJ÷RÉüW€ÿ{½ò¿ŠdZN¸gÒž’IEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/qpwgraph_screenshot-4.png0000644000000000000000000000013215225434775021431 xustar0030 mtime=1784035837.879146906 30 atime=1784035837.877146934 30 ctime=1784035837.879146906 qpwgraph-1.0.3/src/images/qpwgraph_screenshot-4.png0000644000175000001440000035445615225434775021442 0ustar00rncbcusers‰PNG  IHDR€à5ÑÜä IDATxÚì½YlÙ™çù‹ˆ{ã.äݸ]RÜD‘ÔÊE{ÊΔÇå­0žÒvÃîê æ¥ �F¦Q¨§žÌÃó0À<ôú¡ÊU¨†kÁ¼Œ—Û™YéÌ”SÎÔ.ŠÔBI”Ä}ßî¾Ä6dD^R¤D)•N)õý„ ^Ý÷ĉ/âüÏw¾ó…‡QAA>O8�{Ê6AA„Sômþ»Aà¹ïÕŠ—""PAá…`W¼<è«333~¿ÿë–e©¶m«Žã¨Žã(ŽãH5 ‚ ‚ ¼(ŠB±X|ÐÙÙù`¬¿X� àø¨ðüišvLÓ´¿Ô4íÿ¸išø|¾gzB•Ç´, UUQ”gë ´, EQ¼×“`ÛkâZUÕOå‚Z–ų¸6‚ ‚ |¾QUõÿ†€�J•"ÐU*  Z–¥m%jùàƒ¸xñ¢'r…ã8ü×ÿú_y”çp``€³gÏrÿþý‡TëÐÐ…Bá¡Ïÿöoÿ–R©„ã8üò—¿diié©+f||œ÷ߟßýîwd³YO`ýèG?btt”¹¹9FFF)ÇÇÇ™ŸŸàÖ­[Ü»w�ÉÉɧ.Óêê*wïÞàÎ�;†±¡Nðƒì¨þAAx¹1 # $�ü|Þ‡ÆÇq~¾?û³?ëÑuý;•¸ÿ>ccc¼öÚk„B!EáÍ7ߤT*155Åðð0 \ºt‰ééiy÷Ýw)—Ë *Ø™3g8rä¿øÅ/hmmehhˆååeTUå?øŽãÐÔÔÄØØwïÞ%‹qþüy Ã@Q&''YZZ¢P( ë:ÌÍÍaÛ6ÓÓÓär9VVVH$[VÌíÛ·I$=z]×9þ<·oß&•JÑÒÒB±XäW¿ú‰D‚ššš-… eYœ;wŽ={öðÞ{ïÑÚÚŠã8ÌÏÏsíÚ5 Ã`zzšªª*Þ|óM𛛥¶¶vË2-,,p÷î]ººº¸xñ"---\¾|™Û·o‹Å¸yó&ä׿þ5333„ÃaªªªÄÊAAØ@.—›ý‹¿ø‹ÀdÍûgë±�•@Åqœ‡TÎØØGŽaee…÷Þ{�¹¹9éïï§¾¾žºº:~ýë_3??OGGuuuܹs¿ßÏ+¯¼ÂÀÀÀ–â©\.sõêUþàþ€X,FCC¿ûÝï‡ÃìÛ·�“'O¢ë:¿üå/9zô(Á`MÓhkkcpp€ýû÷sñâE’É$/^IJ,._¾Ì|@$Ù¶bÇáÃ?ä�wÞáÞ½{,--qêÔ)EaaaÇq8tèûöíÛÖ ‹ÅÈf³Œ��‘L&1 ƒ……~ô£‘L&9wîŽãpæÌ.\¸ÀoûÛÇ ðÖ[oqëÖ-VVV"‰påÊ`mÜ0 Ž9ÂÕ«WÅÂAAx˲t T>vúAÅ0¶m?¤t:;;ùÝï~GUUáp˜R©Duu5Š¢pöìY‰¹\€t:M&“!P]]�®ë�- ¦ë:_øÂ8tèï¿ÿ>+++†áÅù¹ÃŸ~¿ŸR©ä}îóù°mÛ¶)‹¨ªÊ‡~ÈÌÌ ¶m …Ðu�ùùyâñø#+çøñãœ>}ÚÛÎ0 oØZÓ4,˲¬m÷×4�ð—ù—ôôôxûÆãqêêêøêW¿Jss3ÿüÏÿÌ¿ý·ÿ–·ß~›]»v=²L}}}|õ«_eïÞ½¨ªJ4¥³³“Ç{Û¬¬¬�ÉdĺAAØÇq4 è›Äßš†áã!`íÏþìÏz�À·+7‡Ã8ŽÃÝ»wI$tww�F©©©!°²²Â¾}û(—Ë,--QSSþ}ûˆD"Ô×× …¨©©Ù\(Ç!‹¡( d2öìÙCcc#»víbtt”ºº:öîÝËøø8uuuž§0�ÒØØH¡PààÁƒtww“Édؽ{7MMMX–Emm-{öìÙ¶b Ã`jjŠÉÉIš›› …Bž3™L’H$ðù|¬®®n;lš&UUUÔÕÕÑÞÞŽßï'‹ÑÓÓÃØØš¦ÑÚÚJKK û÷ïg×®]´µµ=²L�@€H$‚ªªìÚµ‹êêj¦§§‰D"$“I¢Ñ(wîÜ! rüøqt]+AaÙlvþ¿ü—ÿrH)Ö&‚¬Mq\ñ§úØØØw£Ñè*ðàÁƒ Þ¦ÖÖÖ-ãêhhhx¬‡Ë:·nÝòþèСg6»5�NóöÛoóµ¯}m[ ã8ܸqÛPÑÕÕE8~âßšŸŸgvvÖûÿþýû?‘ »ÿ¾7! ­­í¡sÈçóܸqƒ'Nˆu ‚ ‚°%sssƒû÷ïÿ>0 Œs@Ö�àÝ»w€/"Š¢ ¹ AAx™`__ß_¯ ÀQ`ȸpC¢¾p8üTž0AAáù! =òû Ðçóá÷û¥ÖAA^`§ç6@wr† ‚ ‚ðâò8=ç“*„�aš&…B�p8ü\/Éç8†aP,q¿ßO0üÔ–(ü¤¸)�Ü$ïÁ`¿ßÿÌ—yü}ŸO¹\FUUB¡>Ÿï¹>ŸÏÛ5pÏ)›ÍbÛ6Á`�`0èݶm“Ëå¼ÔaÛ¥+„Ï3ÏÄhét€h4úÜ #;Žãå topÇq( är9t]'�¾9Yø³«gÇqÈår\¾|™X,J:�áøñãÞÊ8Ÿ•m;Žó�¨³m›±±1ÆÇÇдµÛ;›ÍPSSKÿs×ЕËeÉårë9?‹‹%öïßO{{ûs+Z-ËÂçó=ô¼,‹\»v�r¹L($“ÉR*éé饹¹ù¹|Ö‹E×WUòS,®]ƒC‡ÑÚÚúB‰ÀÊ벸¸ÈÂÂKKK¨ªÊ±cǘŸŸgaa�\.‡¦i„B!lÛæÄ‰2ú%|îxœM«�Nù|þ‘7¿ëiÁ4M¦§§™œœ|nn¤\.Çôô47nÜ �Nã8étšÉÉIÊå2·oß~a*—Ëåmë?�N?2aõ‹ˆiš‹ÅßûÚÇ¥RiƒÍÛ¶Íèè(}ô¹\–»wï211Î{ï½ÇÔÔÔgfë¶mcšæCö055ÅÔÔ$¹\ž+W®pñâE2™ ™LÚ[)çyj¬/_¾Œª®-í811Áôô š¦20põ­«ýiS(§†apñâEtÝÏíÛà \ãâÅ‹ø|~.\¸ÀÜÜÜsyŸ]ºt ŸOcbbœ¡¡ëŒŒÜÃçÓ¸té333/Ôs£òº”J%–––X]]e×®]œ?ž±±1²Ù †Qæúõ!Ξ=»aAx™ØÒXy3lç,•Jäóy¦§§‰ÇãܹsÓ4©««{èXŠ¢<“^¤{¬'ÁMä|óæM:;;™››#‰pãÆ "‘ˆ×[t…†[Ö|>ÿ©›=͹lVô¹\ŽK—.hjj"™Lâ÷û7x°ÜsÓ4Í[Me«cÞ¾}›ýû÷où›†aà÷û½X¶:Ƴ–e133ã•·¥¥eÃ9¹×j»kS*•PUõ©=Ñ•6???Ͻ{#,--±gϺ»÷²°°Àìì,/^ þ²—Ó¶m,ËBQ”GÖ‘mÛÏôž¨¼N·o߯ï÷aY½½½¤R)˜��Á¶WÉår„Ãá öþ´×s;{Ú)ËËËØ¶ÍÀÀÇŽ÷’¸_»v�D"ÁÕ«WimmõV‚µÕwžöž´, Û¶QUõ!/ïãlj'õ¿°°€ÏçãƒÎÐÝÝM>¿&ÄOž<Áàà ·dä'=×Öfff¨©©! =ñ(Áââ"Š¢píÚ5Ž?Áìì,ªªbÛ6‰D‚k×®ÑÔÔôLÊì8Ž×iy’!ñ§ýp‡êêjLÓääÉ“,..¢ªkË|:tˆùùyvïîàöíaB¡�×™~–×åyöĸa%�?ÿó??ä®â>$·Ê©W.—Å4Mjjj˜˜˜ ‹ÑßßO4Ö†fgg)—˘¦‰®ë^|‰û pÆ•K®)Š‚išž@r=¶m3;;K8ÞñMå÷ûÑu�±±1êëëAQæææèïïgÏž=躎¢(ŒŽŽb«««žˆrEÓærªªºmù݆ÑýÎ-¿[f÷ÜÖHêŽP·N*‡œŠÅ"W®\áĉÔÖÖ222²îý™"�zC}+++œ?žŽŽþê¯þŠžžŠÅ"¥R ŸÏ‡išd³Y~ýë_säÈòù<¥RÉO…B�ÿïÿ=_úÒ—¸yó&?ûÙÏ8zô(ÙlÖ;†a i…B€l6‹¢(OüÐ^^^f||œ\.‡eYìÝ»Çq˜ŸŸ÷ìªP(0==M.—óêÏ}p»õä&Óv¿Û©½¸ûûý~Ï ~ùò%VVV8vìñxœT*å­“N§Y\\¤¥¥˲ø§ú'æææ¸qã{öì¡P(`>Ÿ�|>O±XDQÞÿ} …555ê;—Ëm¨û�ˆÔÊFªT*1>>ÎØØ(‘H„ºº: Ã`zzšb±Dmm uuõÌÍÍ122Âûï¿ÏÔÔ{öì!—Ë¡( ªª’Íf={sËmÛ6…BÁ{&üçÿüŸ9~üøC6³SÖÊUàæÍ[ÔÕÕ155ÅÌÌ ©TŠ@ @&“eïÞ½\¿~�±±1þæoþ†½{÷â÷¯ Súý~,Ëò®·ÞQ*•°,kC™ŠÅ"ÿôOÿÄ¿ýíoÙ»w/–eá÷û) \¼x‘……êëëwÔð—Ëe�Àõøø8¶m±ººJCC555˜¦IWWÅb‘B¡HGGƒƒƒŒ��óýïŸÞÞ^EñÊjš¦w-,Ë"ŸÏc†÷ÒuÇq¸wï?ýéO½OÃ0øõ¯Mww÷Ýwããã‹,˦££ƒl6K¡PàèÑ£,..�N§éììdhhˆ±±1¾ÿýïsèÐ!TUݶ̅B�r¹Œa”Ëeï2™ Žã�ǽu‡‹Å"�@Àkèc±«««ÞÉm¸B¡�7ÄÛàlç�­ªª"‰`YýýýhšF0äìÙ³œ:uÊŒîþ–e±°°ÀÏþsúûû©««ãÖ­[455122ÂÊÊ o½õáp˜žž:;;hiiáÖ­[,..�FæÁƒ¬¬¬ð…/|�»wïòÊ+¯pîÜ9TU%‘H0;;Ë믿¾ãƨP(°ººJ__Ÿ·¶´»´Þââ¢×!p]×=q¾¸¸ˆ¦i†A(Z�_*RSSãÙœmÛÔ××︷äþ�˜˜`×®]LOÏP[[Ë­[kBåܹs|éK_b~~žl6ËÄÄŠ¢ÐÞÞÎéÓ§q‡ÑÑQ†††0M“cÇŽñ³ŸýŒÓ§O3;;K±Xô®Õõë×èééáûßÿ>ò'ž={ž*ÞsMŒ—©ªª"ŸÏsçÎVVVˆÅbëbM°ïÙ³‡ºº:†‡‡ùÚ×¾Æ[o½E,c~~žÃ‡såÊlÛæ•W^áÇ?þ1Ç�çòåË9r„p8Ì+¯¼âu⦧§yë­·èì줣£ã‘Ë/>Ôû\¿†º®{¢Óqœõ5¿ýž=räüáþ!ñxœýèG444ÐÚÚÊèè(ñxœpâÄ Î�;‡®ëÞ½ûúë¯�Ç=¡ûꫯ288Èõë×™˜˜à{ßû?þñ�‰Çãø|>¯q~4M£T*399I©T" ‘ËåX\\¤««Ó%ÇŽãܹs|ûÛ߯çóñóŸÿœP(D__—/_¦¹¹™™™:::¸ÿ>étšd2Éòò2ßùÎw¨ªªòž�mmm �ÇY^^&�NïÈÖ+ËlY6†a`Û6årÙûnçV×uŽ=Ê… xã�7ƒüô§?%�²ÿ~ijjbrr’}ûö1<OOOKKK¤R©'zîWžŸÛwGMÜÎÁZG~­à^—�>úˆï|ç;ø|>~ñ‹_ éëëãÒ¥K´´´033ÃîÝ»yðàétšÆÆF–––6\—ššÂá0…B�ññqÒé4÷ïßçôéÓüð‡?äÈ‘#$ îÞ½K2™äg?ûÿé?ý'Þzë-jjjhnnæàÁƒ¢^„OݨnçY0MÓ‹�Øüªa¨ªú曇�MÓ$�{ž²êêjÂá°×{ŠF£ÞÌ,׳ä΢‹ÇãÞ,®`0¸e™÷Ò4�û÷ï³{÷n¦§§Y]]Ŷí-Ëê~æ÷ûñù| òù<‘H„h4J©T"‹‹ÅÖ§ub±˜w¼Êž_±Xô†žr¹µµµTWWã÷û©®®öDÍã^›Ë‰D¨­­åý÷ßçêÕ« ñÎ;ï`š¦'BÝmK¥’'êN�:Åþýû%pâÄ Z[[™œœäرc=z”‘‘oÿ={ö000@2™$ 277ÇéÓ§éêêB×u–––¸páà£�>ò†‰·³Ÿ­^™L†úúz¸{÷.·oßf||œááaššš6ÔSå°�;ËÏ}¸' O;ŽCmm­×¨=i=‡Ãa†††0 Ó4)—˼ÿþûÔÔÔP.—I¥RÌÍÍz�[å 755…ªªTWWcÛ6ÝÝÝ>|˜©©)‰„g‡š¦QUUE¡P ££ƒîînTU}*›P…B¡Èž=�žÚí¤´··±°0O(¢T*ñÓŸþ”7ÞxÇqÈçóœ>}šÉÉIFGG9zô(§N�bvv–ÞÞ^^{í5ÚÛÛ9zô(ccc=D¾ð…/°oß>/þw§¯p8Ìâ⇢ªªŠ'NÐ××·z¡xBfllŒð…/|�¹¹9úúú8vì·oßfzzš'NÐÐÐà�Ç‘#Gèë룫«ËëŸgppÓ4½2ß»w�ÅÅEŽ?ÎÔÔG�õÊ<77ÇÉ“'‰F£ ^{í5úûûéïï§­­�••• õæÆ3ã8¡PÈ _™ššâêÕ«Ø¶í­‰î†·<íu)—ˬ®®’Íf¹ÿo¿ý|ð[êêj½Î‚{]R©GŽabbbópaaÁ».Åb‘Ó§OÓ××ÇáÇٵk—w]Üß.—Ë^Ç&•JqãÆ Ãà•W^ñÄ®Ïçãĉ´µµ155å=7ËåòSµqò’×v÷ÃŽàæ6�™¦Éìì,wïÞ%‰0::J?ííí­ìz`Ö†ü~¿çYÛ*ÎýíêêjoøÎu«/..R.—7 ÷ìÐýÉøø8³³³Äb1ª««™ŸŸ§¡¡�Û·o“J¥¼2†ÁÂÂóóóÔÔÔx"Ã�­ …B¤R)–—— ¬¬¬°²²òÈ•SÜôŽã¼ß_]]E×u/êiP…ææfzzzˆF£ÞƒhyyyCú†p8Ìôô4o¿ý6­­­¾s‡óΟ?Ïìì,ííí|ôÑG|øá‡8pÀû­P(Äw¿û]úûûQU•––Þzë-îܹC<§··—+W®ÐÒÒ—¿üeÏ÷$žw¸?—Ë‘L&éììô¼U›{þ�@€††VWW‰F£žW°ººš••²Ù,ªª¢ª*sss�§ŠÅill¤³³‹úú:®_¿Noo/ßþö·éééáÆ�8ŽÍ¿ø*µµµ´´´0::Ê›o¾ÉOúSÉf³„B¡ u®( ‰D‚ÁÁAš››Éf³ƒAoöüÓ£›¦ÉÐÐÐú�D"N,e÷îv"‘jîܹË+¯œ" òî»ïbY·nÝâÞ½{Äb1~õ«_­Ç8vóá‡ræÌš››=ω{-+½æ®—²òÜvJ&“áÆ�ë;v”R©´”�‡çøñã þñÿ‘p8ÌÅ‹Ñ4�«W¯òÁÐÓÓCGGï½÷KKKÂCTUÝPžææfFFFxûí·¹xñ"$óþûï³¼¼L"‘àÆ�Þ3áIY]]åÎ�;ôõõ¡( ÇŽe~~Ž®®.š›[X^^¡¯¯�L&Ã?þã?¸páÕÕÕÞ=wðàAZ[[ùÍo~óÐH‡{>•âµ½½�®®.|>ápØóFï”T*Åðð0‡Æ4MÂáuuuÄã1B¡ ³³s>|˜\.Ç?üÃ?xe‡Ã\¸p�ßýîwŸ§¶¶–t:íyû�;æ�Ž$ }ª{brrÒ+çÊÊ*étf}TÀÁq ³³“L&C6›ÝpŽ‘Hdóн.†a ½!ÝÍçP__Ï×¾ö5>üðC¦§§7¬¿ùÙûÞ{ï166Fkk«çm�Åbâº~/À kONNnX Ø�1r277ç –;wî°ÿ~¶}ðožâ¿Wö¾·û[ùۛ˱“†ÆqîÞ½K]]«««^�Î0 îܹãMðèééñÊæ»òA[YþíʲYÄV–o»:pßï4ÐØ�myÔÍårLNNR]]MSS“wl×YÓYY–ʉ •AÚ.•AØn ¤»�¦i&š¸ÞÞ'�\`𦗮¡¦¦æ‘C ›í¥²N7_—­R¥í=à7O©Òtë§2†Ô�rÅÔv![±yÒÂõë×=ïÄ›o¾Igg'ù|Û¶‰Åb=z”ÆÆFï·+ëɽžnà»kn™ÜïݘV·Œ¦izÛ¸õ½Óø¹sçÎÑÓÓùsç§©©É‹mkk£¿¿ßó¬VÎvv¯²®Ürl÷\qïÇÍ“@ÜIM•ÃÑ; þÏçóÄb1/ßâùóçèëëç½÷Þcyy™ºº:¯#²gÏúúú<ïâæóqËàÚIåý¶ÝùTÚöæûx'6T*•8wî}}}œ9sÆ‹tãÕ:::èëëó†5Ÿu™+ïƒJt¯Må½ó$çUy]fggyðà Äb1,Ëâĉâ©/\¸@2™¤··÷¡I ÏâÝ{µòYén·²²‚išüüç?çOÿôO={ü$�¡’¹¹¹Áîîî�­¼9в,/y¦ëMºÿ¾7\U__ÿH1¶y¦ãf£Þì9¨ü»Ýû'¡ªªŠû÷ïã8ýýýTUU°ÿ~/¾«²·ú¨²=ª,�*ߣêàYßäUUUìÛ·oË8˜Ê‡ge/ts9¶m•ûºï+·«|8+ŠòTAÌ>Ÿ�d2¹coaåßÊòow½ž–`0È«¯¾ÊÐЋ‹ $“InܸAooÜлw½Ž[Õ¥[?î_·ŽžÕŒê|>ÇÒÒ—/_æ›ßü&ÝÝÝ„Òã®ñvßo¶•J[ØìÜi]¯ O[œ9s†••þõ¿þ×^ˆkK•Ç|œ=mþ~»òl¾6_¯§ ¾/•Ê8üË¿ü †að½ï}�ªªªŸÏæg“>‹6{iwÚÑq‡ßüæ …"ßûÞ÷¼p…ßG™7ß�²Á§½G,Ë¢ªªŠK—.Q__Ï·¾õ-¯=[^^æêÕ«äóy/ÝÙ§uŽ[={Ý gsss|÷»ßõê\r½ Ÿ™pbbbƒðQž‡g•ÆâÓ>y·6ß¼O›†å³<I)ðé³�WÖͱwïÞ½Çz¾? {Èd2ܺu‹d2IkkësíAø8_áû÷ï!‡¼\/°ëµg~~žƒza,ÏûóÄ �9xð ‘HäsqÿV^w"”išÄb1ººº¼{&�N¯Ï€.’H$¼ o‚ðyóîÝ»w[à8>>þÝX,ö©6AA„—ÙÙÙÁ}ûöílØí ‚ ‚ Ÿ_6@7©¥ ‚ ‚ðâò8=·AæóùOu™/AAáÓÇ]�kG0�zKn ‚ ‚ /�¬Ü)ŸÏKí ‚ ‚ ¼@hšF<ìv[ ÀT*ååFAA^ ,ËzzH&rAA„Ï)¢òAAD ‚ ‚ "AA€‚ ‚ ‚@AAA  ‚ ‚ PAA(‚ ‚ ˆAAD ‚ ‚ "AA€‚ ‚ ‚@AAA  ‚ ‚@AAA  ‚ дw IDAT‚ PAA(‚ ‚ ˆAAD ‚ ‚ "AA„ÏŸT� |ŒmÛ/_/P•~  ‚@Ax q‡©©)lÛÆ0 ÇyæbÐqœ ÿWå3;_EQPÇq¨¯¯'�‹‚ ˆ„—‹t:M[[uuu˜¦‰eY8Žó�h{‘Ù,8EAUU.]ºD0$ Š!‚ ˆ„—‡R©@¹\&—Ëá8Ž'˜\X9TZ.—Éf³ÔÔÔÂá0¦in+wÚ9¨´±©©)æææH&“RÑ‚ Pž_hÛ6'Oždxx˜H$ÂÞ½{ÉçóŒ��ÓÖÖÆ¾}ûP…ååeÞxã Ο?ÏÜÜ  266F6›%™L2==Muu5…B�p8Ì¡C‡Ðu]×ihh`Ïž=,--ñå/™ÁÁAŽ?ÎÞ½{™ŸŸç­·Þâ«_ý*uuuLLLðoþÍ¿Á0 êëë)‹ƒA^ýujjjP…]»v‘Ïç©©©AUUâñ8Åb‘®®.ü~ÿC�y¥øs x_Nòù<¡PÃ0(•JÄßæð‡r¹Œªªø|¾G ?÷o0drr’††±-A(ϯÔu�+W®pòäI�årEQhšæ5l---8ŽÃ«¯¾ÊüÁ Q…Ý»w{Ç«¯¯ßö·hmmÅ0 víÚE$ ¦¦†o}ë[D£Qïÿ•ìß¿Ëc†Ãaººº¶=·JáçŠ?wèÛõüHJ˜—ÓîmÛfaaÓ4™šš¢¡¡�B¡Àââ"‘HÓ4Ñ4�……VVV8räËËËD£Qoˆw³TU˲ÈçóضíÝ?‚ ˆ„ç EQH&“œÿX–E©Tb``€††‚Á år™ùùyFFF¼÷}}}(ŠÂßýÝßQ__Ï®]»hjjz(¼À¥²s!PD ÂsK  ¶¶vC˜'�}zñ·•p|Üç�ûÞzËâ£ÅEÒ†±õþëǘ(•ˆª* ù<�@@†ê^B\›éëëc``€D"�a´´´�Ïç)•J´··ÓÐÐÀää$ø‡H:�&‰lé9®ì\¸÷’ "á¹m?Iòçßw#·Ýï9Žƒ ü?÷ïó×|ðð9iX–÷^Õuœb[×ùïÃaÂ’楳{×öoß¾Moo/ªªz¡íííhšæu ÚÚÚ6t@*Ó%¹Âoóç/ã ;‚ P^LÓô^Ï*ô§- ·v(8KK8@ ¥3•Â1MÔ@€ªÞ^2~ˆ],Ruø0j @áömþeb‚p&C}m-š®ƒÏŠâ½÷·*³ò3EAUUEÓ44UůiøU¿ÏG@Óø|èšFð;f.›ëÉÊúñTUEUU4EASU4ÀÙÆ«éÕƒ˜óÛi@×ioo'‰l¼Æ°e ¡ílp³-Š÷OD ÂK'?‹†ÜqLÇÁ±mªúû1‰?Niz]¨*v±–…/§4>ŽV]MXUé ‡i«­% ‚;¬çzr*…•ãxï+ÿ:|ìñ±lË0°Ö'š¶MѶ)Û6`:&l|) ÖºØt�§* š¢àWU|ªŠÏq( X€¥(ØŠªŠªihš†_Óú|„ü~Â~?Õ~?aMÃÉdp,kM`£†A"$jšèë“||ªŠoý~E�RɨŸGÁ¹hÛ\5 ¦§1- KU±——½ïc~?½‘�-fø>®3"âOD Âs�¢(˜¦éM†pÓ¢¼h�˜[VÛqЇâÈÁînŠ##øjj°×E� b¥ÓïÞEol$wù2‰Ž’554®§’ù½Öÿ6¢Ê©ÈIçmÛ€‹},8+^¶mcZÖšˆ/03ʶÍ"P^œÆúk0�F1MÂ@}4ЦªkØ¿_.—×öSLEÁVUX÷pú×=š!Ÿ�êu¡Y¥ªøJ%¬rùcQ¤ªž'SÓ4O`ê>ºÏG@Qð) N±ˆó{*M) ÿׇòÖê*\¸°ñËpµ© {|œÿùµ×x£­ ­â^Ù*G ˆ=A(/¤´mk]<¸éQ^T¨:_kme"�&51±æÅšžö¼Yjo/J(„=>Nnd„Úúzþ›XŒðg4 d»VT•O«4¯54 T¤Å©¬?ד¹•—ÓÝ⬵—mc•ËÅ"†ã`¬ U“5/§ XëÂÕ´mŒõ}lÛÆ´mLÇÁÊ–…Qé]÷nÚªº6ü^1î ­«êÚ°øº—Ô&÷Uþ¿â¥* Šã€m3‘Épæî]BhÉ$v¹Lax§T"rü8Æâ"NC7ççùzc#¡õ8À­Ö”Þê~RUUŒ ‚@Ax¾©ªªbvv–d2‰af.¾ˆÐqºü~þ—¾>JëÃÁ[´ÒÐÓÃÍ›7iŒÇi­«#‹½4³€}[x9ŸèÜ?aÊœm'ñÏðð0ÙlÃ0ž‰ð÷= d³tÏa«Y˜ªªâ÷ûÙ×ÚJ}}=µµµeðï�mÓÿü~?�@@VyÉhÖu¾×ßÏßܸ±6Ì A4Š`.,P𙡩¹™WP- ‹Çç·¬´/EQ°,kC AD Âó{Sø|Ÿ»1¶¯Ò(¿ÜèŠÂÿÐ×ÇAMC‡©®®Æ¶¬µ!ãîn”õm"ë“c*;C;IR®ª*«««Äb1é\‚@A> Dì [Ô4^Ù¿Ÿ›7o2zû6¥Ré¡Up\1çþ݉}¹3ž›››ikk“eàA  ‚ð<¡ë:}}}ìÝ»—b±¸!¦+æEyìPîæðMÓÐuÝ[çZ€‚ Âs„ªª„ÃaÂá0À–yþA(‚ |ŽÁ'/AÇOª@AA  ‚ ‚ PAAø¼ 1€‚ ŸcÊå2SSSÞê6.Ï[bóÊYÄÁ`�ÖÖVÉ("áù²¬ ùÒžG*T]×墽„˜¦I:�æÈ‘#ض½#›}öü4“H*íÕ0 nܸAWW—LH€‚ð|4¨ � �éó"·Z§UUU²Ù,ÍÍÍ’“í%#—Ë¡ë:š¦mX×z'öên¯ª*–ey¶dÛ6Š¢`Û¶÷7W »bˆû9à%�~œ�«\f. ²²²‚išb³‚ P>{fff8tè�@à¡„¹OÃãöý$Þ�J�J>ŸçÎ�;tttÈE|‰pÅ^¹\fdd„r¹Luu5ÙlÛ¶ñûý´´´<4Ôê8|ðù|ž“'Oòãÿ˜W_}•ÚÚZ¦¦¦ˆÇã ÑÛÛËßÿýßóçþçTWW377ǯ~õ+z{{ùðÃihhà�þè�¶´ãí>s…d©T¢T*‰€‚ðÙS,74NnkY–çíØªaËçóœ;wŽS§N‘J¥¸ÿ>}}}ƒA®^½JGG™L]×™ŸŸçÈ‘#\¿~�ååe:ÄÀÀ»w嶺«kˆ³RLVŠ?wHmyy™ööv‰«z  ëµ¾zõ*áp˜ýû÷óá‡R__O]]º®?d·ýýý¼óÎ;Œ��3;;Ëää$™LÓ4Ñu�L&ÃåË— …B â8ÝÝÝ´¶¶RUUE__ׯ_'�NS]]½mgf«eä`Íiš¦\DA(Ï–e‘Ëå0 ƒÅÅEJ¥UUUÄãqÆÆÆØ»w¯'¼\4McnnŽT*ÅøCt]'™L’Éd8sæ UUULOO“N§¹}û6���†A2™äÒ¥KÔÕÕqåÊE¡½½ý¡ãoרºÛY–å­ó*ðå�¦i2>>N("‘HP.—™��¥¦¦Ó4ñù|lÊqŸï±1‡•öZ9„ìv¬A(ŸycjÛ6©TŠT*…išfgg½¸¥ÍžÀR©ÄÊÊ ccc4551==M&“¡T*Q__Ïõëש¯¯çîÝ»´··sãÆ š››À0 ‚Á ýýýÌÌÌlyüÍž·Áuu×›bY– ©½dض�¦i8p€t:íuÞxã FGG7ØK%‡âàÁƒ8ŽCssó†ï¾ñ�ol‹­­­„Ãa¾õ­oá8ΆpƒJ{Ü´ãç-¶VD ‚€ã8‹E._¾L,#›ÍræÌÇa×®]á]�üÇ @oo/†aà÷ûq‡žžÊå2º®sâÄ TUÅq4M£­­Í;V"‘ð†ê¶î}œ—Ų¬ i@„—Ç^c±sss”Ëå q«{öìAÓ´'Z•B®Ò{·�Ø«ün'q­"üA  <—ƒAš››)‹Äãq¯Ñ …BÛ6˜º®{ ›;3Ò»·&SŸÏ·a›­È­ËÍ�¬+eHí%Â{Þ¦¼\¦�/2B覎mÛÛŒ¥Åª¢‚ Ûxèxüd%5¬¢7é(š²¥øÛ‰½V Dñ ‚@Ax®P…ÆÆFâñ8¦inhPÝt�órx�ÜzúUC·á3¥òíc<~ÊzCŠ‚ƒƒÊšG±l”)—Ë;öÂ/0,�_âÌÿx†‚]@Yÿ·•�s?wøØf*·ß¼ŸûY御ú_ü_¿HÝ«u[ ¿]DŸ ˆ„çYº^¼­†U7ãÑqì²ÍÌÛ3¤'Ò^£½ó`Á\`—�ÁN&J´Ú. 3oÎóåè—¹‘¿AO¸‡éò4 _‡Ùò, _‚�ü»õÝ4ø¸°p�©‹SD�FQýª±yˆx'ö*Þ?A(Ï•ên#µ¹±z\¬�ã8,~´È¿üoÿB£ÞHÉ.1kΒФ¬õ¾z²v–°fÁ\ ¤„ØÚǽâ=ZõV v�å4ø(;e¾«æêÚ,á@;ã¥q,Ç"ceÐU�6½�‘âæ‚IôÿŒ’hLˆðsm¤`ä šô&fÊ3ìíÆÄ$æ‹Q¶ËTûª©ÒªØÚ‹VÔX°hvš×& aSï¯gÆœ!â‹p |¿ê'î�Q#¼›~—/E¿„††‰É¤1Éîàn,,¬²…Q6ÐÍó„?‰�UæOµ ˆ„ç˹²ž›¬2ôNãð*ßççóXX4š11 •CÄ}qóƒôT÷�6ÓøU?‹™EÊ”™6¦évФ7q§p ‹d I�¯†f½™¡ü–c‘¶Òì ïÅ´M†òC�"-Á®¯S¸Q µ”"Z/àçŸÃ’¹ÄþÐ~–Ì%,,TE¥%Ø‚íØä¬-BÆÎp t€¤?Éhi”¼“'¬…11Y5W™3æka4UãhÕQæ�yê|u˜˜4ù›¨÷×£**ÖZX„jï8b;è®8"‚@Aøì+Žƒa†áÅÿ=Ép•ã¬Å[Y¦…‰¹|ï(,˜ „µðšGÐ)Qã¯AE¥-ÐFÞÊÓWÕÇÅìE&ÊôW÷3\&oçiPX2—Öb±¨Òª0°±±°ªAtEÇ¢X,bY–ÀÏù“½ñ•F®ý¿×8kœõ>ž-Ï¢°æå«äzî:×s×qpX1VÖl† gÓg7éÊ�±«!BL–'ùÉÒOU‡ˆvG);e4Cóàv¢G @w²’@A(/„Ü©t‚P¦Ì�âLÇħø¸Y¼I»ÞÎ¥ì%llòvž�FK£”�25¾†rC˜˜Œ–F.×âŽ�ŠÊ.ÿ.æKóŽAÆÉàÃǽâ=4Eƒ:p‰­úÜ£@óWšùÆ_}ƒéÛÓ4·5oœ°TaOa^ª²6}µJEoÕÁaKÞ“@7w¡®ër-A  <íêú²j€× nH«ÁöIo7oê qò�O²rmÇ^û.êD±±i á¡ß.S¦žzª¨â¶,£‰I 5$•$Y+KĈPÿßÖ£GuY äe°UŸÂ®c»(Ä O cÆZÈ‚½ÑcýL @ Ô•5¯ßæ ;€ðñrŠ���¹�‚ P>{êëë§¶¶Ó4=OÇf�÷ØöÓqp4‡úÿ®žÚoÔb;öÃ^™§uÔUæs[ÿ3;=‹VÔˆ·Ç‰ÅeðË‚¦ituuÑØØH6›¥T*mð\?©Ý>ªcä¾4mmø·²“±“´H•ûªªªdÕA(ÏÑh”L&Ãõë×ÉçóžWe³ÜÉ’Wn#¼yÿgÝ ûý~‰õ»ê‰D"ø|>ÊåòÇW¹‰©%.ëÅBUU¢Ñ(‘HdK�õ³�îÄ�J»{;ù$û ‚ P>–——Éår”Ëe=áö´�g¥æÓÊy¶Ù³R((—Ëž‡f§ «aÍ›dÛ6mmm­d"<߸6ð2ýK>AA(ŸˆL&ÃÉ“')—ËÞ0Ú'€¿�js –+úv"þ6CQ™œœ¤½½] âÀ²,¦§§Éçóž½¾hÂu«÷;Å�HÒÞÞ.^EA(O‡®ë†±AVzï7teÛ6£££Ô×ׇ™œœ¤©© ŸÏÇÌÌ ÕÕÕd³Yt]Ç4M’É$KKK‹E¢Ñ(~¿Ÿ‰‰ Ç!PSSC$y¨Áܼžª¢(ÌÍÍ�F©ªªÚQY·j„]ïÑòò2mmmÒ ¾LNN²ÿ~‚Áà†YÀ;É[ùûè ìDø=­tm¼\.sýúuº»»Å A  <9¶mS,¢T*y ¡ÝáÕ®®.�À¶ •mÛܸqƒÝ»wãóùøÛ¿ý[þÃød³Y._¾ ÀÍ›7yýõ×½÷###$“Iz{{I&“Ü¿Ÿl6Kuu5“““;vŒ••t]'•JQWW‡®ëD"2™ 333ž'dvv–¯|å+´´´<Õò\¶mcY’GðEÁ?ápxÃŒõ'õº9ù4Mò,σ¼ÕìwŸÏçý†{_¸“¥ÜNÄNÃÜr>‰·z³M»eX\\dÏž=º "áÉq‡R©D$¡¶¶–�>úˆC‡‡YZZbyy™d2ùHïZ4eyy™ùùyªªªXZZ" ¡ª*ÕÕÕtuuQ(H¥R,..ÒÐÐÀêê*¥R ]×iii!�Nã8{÷îåÒ¥KƒAúûûI§Ó\¹r…––TUe||œÆÆF†‡‡9|ø0ÝÝÝܼy“]»ví8'›û×q¯ÑwW@øbtZ,Ëbqq‘™™t]ǶmJ¥Š¢PWWG4ÝÖfÇá�wÞAÓ4NŸ>ÍßÿýßsêÔ)jjjann]שªª"›ÍòÚk¯qëÖ-æççioo§§§‡·ß~›sçÎqèÐ!�§N�Âï÷“ËåÐu�r¹L4EÓ4òù<š¦‘ÍfI¥RLNNròäIžÈf]\¯µ;ûY  ˆ„§fee…h4JOO>Ÿ�t:M&“¡¦¦æ‘Ãe¶mÓÚÚ À¿øE^}õUB¡†að•¯|…D"A.—ó<(…B˲ƒTWWÐÒÒây:::PU•D"A0¤¯¯ŸÏG*•âðáüýöÛD"Ž9‚Ïçó&®T6ø�ús‡‘·šµ,<ÿ˜¦‰a„B!ÆÆÆ¼™àš¦qÿþ}z{{Ùi9xð  ±°°Àìì,år™t:Íôô4©TŠ£G�òàÁŠÅ¢ç�N§Ó(ŠÂ¿øEVVV8uê}ô÷wÇáÇñù|ŒŽŽ¢ë:�@€ööv®^½JUU–e‰D°,‹ø‡à?þÇÿ¸ã°…­¶±,kÃ}#‚@Ax"4M#‰P.—±mÛK íæ,{ÔöªªzÐqêêêÚ&‹=$À*qcøÜï¢Ñ¨÷ÝîÝ»½÷555ضÍ7¿ùM‚Á Á`pC¹6ÝítˆÍ€î° ð|ãæ©t;)uuu¤Ói‚Á ³³³,ËÚà«´Û¶YXX@UUZZZøú׿ŽßïDz,’É$IJ,2™ ¡PˆH$ÂÂÂÝÝÝÄãqoE�®®.‚Á ½½½444ÐÐÐ@]]¡PˆT*ÅîÝ»yðà}}}¤R)Ξ=Ë7¿ù͵ôEõõض½ãÌ›c`Ýó�N‹ ˆ„§F×uššš(•Jk«*TLyTüߣ¼[5`°–Ìy«FvŠ¢(Äcñõ�×’K+(ÿÝÉjÊÇK}¡€c;˜†I¹\~"á(|vÀçóy! º®“Íf ^§åQ¶uèÐ!<ˆã8:(ÛýÖf[…B;v Çq¨©©aïÞ½Þ6µµµÞ¶���(ŠÂØØMMMìÝ»ŸÏçMÞx\ŽÍ­D`¥[  ˆ„§Æzn@üV�ÞÓSU ¿Ü½KW–pŒO°"È&q·yu�'=o(Kèc:ÙD–Èш¬Ôð‚‹Å0LÛZO]„ƒª¨ž û$�Çun¶ë\lÞÞýÿæCO:ai³A  |boJåëQÛøß?`öÆ,a5̪µŠOñáWükKÆ𢑳sDÔ9;GÒ·6¥`HY)¯ñU¶P|5@Á.Q#èªÎ’¹´Á[èWü˜Ž‰‚B\‹“±3äòTýßUÔ´×H#û¼ÚkÊÁ±I©)LÓ¤Ú¬þxæîºZóy%ÿ±Èâ"îit¡òÐ�@5 h µÇŠ»G­©½“mÅ>A  |"Ü@r÷µÝÀOO·Õg¥L‰•ë+¬:H@ p§p‡¤?I[ �Œ•!ke)ÚE,,vv3Qž`¸0Lܧ#ÔAÊL±j®²`,õE=ñ–³s˜Ž‰_ñS­U£¢Òhåvá6å ö…÷¡+: æ–c‘·ò„´]Á.ÞM½K\‹³?¼Ÿs…Ñ™QR“)¢ÍQñ>‡(+ ¿ý?~˃ß<à—<1¿Q›)‹»õÿÛØÞ{e®âÍŒ‡¼ÍÛìãÃÇÑÿé(]Ô…ª«�¼ožtèwó}(+�‚@AøÄÐ0 / ÊVyОÄÛ°�´l ‹µ|kóÆ<5€…EÉ)Ô‚¤ì»»ÈY9êýõ †¸W¾G�^GÞÎãWýt„:X0h ¬%mÎZYLǤÞ_Ïp~˜á¤­4{‚{¸_¾OH ‘·ò4ëÍL”&è©êáNá%§D­¿–¸gÞ˜'æ‹a—m/RàóGz$Í­ßÜ¢¿ª€Áü ­z+1-†®êäívïý9êãØŽ'ÓÅ…EÖ*kt<ÒAWw—îò€’=�åÀ ø—7¶�ÿ»Ì寢 þ·,Y.Wj¯¯…k›¶_ –6%µÑF:Nó±÷1eÊœ Ï‘'ÏÛÅ·É‘ã½�÷6ûÃÒ‡\‰¯pÕ»Š�Á±—Ž¡2Š(ŠöLnm×ÜÇZ/V4-5š]1>>ÎÄÄ«««A@†7eß«HÛZ¸v«•ñÓ´dlíbÛ6mmm<òÈ#tvv6ZˆiÀ{»ÅKÿ÷K|ðú ô`Z&2–{:†ŠgyvWûŽ�Õk¡LuÇ…Ônãjëã·§ª+�€Í®p]—£G�EµBÈõLà{±ÐÝ*°Y6o÷Y ÀºÅqR©Žãhñ÷€“êLqô7ŽrîÜ9ÖW×7�W…úÌÜ¢‚$Öo®ö½nýÛ©Ø»ÓØ…Zç˲8zô¨NVÒh´Ôhîñ¦±¬MÖ„½žH·Š½Ï2~i«KínÛqi Z[[yòÉ')—ËT«ÕFüêý_ØÉxª/.ê ‹;¹w“ý[?¶ëº�âí�F @�殉㘅…<Ï»©¥Ô-¸¼y7M“öööÛ¶Ó|öÔ]÷mmmŸ™ð»ÓXºÛEˆF£ÑP£ùTˆ¢ˆ••FFFj5ú’ìß­ð‹’iØŸo”€¹U;¸�v*xÐDÝNE„‚þþ~Þyç:;;µ°Ðh4-5š‡ƒºÕ/®^½Š”’T*E±XÀqº»»7ÅÆ5 ¡ºà›žžæG?úO?ý4SSS‹E~í×~ Ó4ùÖ·¾À«¯¾Š”’ÖÖVªÕ*Õj•Ó§Oóì³ÏR.—ikkã‡?ü!O=õ³³³´´´ÐÖÖF:�&•J±¸¸È?ýÓ?ÑÝÝͳÏ>Û«™L†J¥B:]ëêpöìYÚÚÚ8tèЮa]FQD©T"Žc�e©Ñh4Zj4RJ–——1 ƒ xóÍ7¡µµ•Ë—/“ÏçI¥R·O½½½ôõõgΜa||œÉÉIÇaaa�ŽŽ~úÓŸâû>©TŠÙÙY{ì1>øàºººxíµ×øÒ—¾Äµk×X_¯et ñÚk¯ñøã�ó»¿û»|üñÇ=z”7nðú믓ÉdX^^æüùó<ûì³ÌÍÍQ(XZZÂqŽ?NE;ÊÀÜÄ_ÇZj4Íç �Ò§ÑÜ¥¾ïsãÆ Òé4¶m“ÏçÙØØ¸©Ì­¾¦§§¹ví©Tб±1|ß§¥¥…0 ã÷~ï÷xôÑG1M“ÎÎN8zô(ûöícyy™ññqÇ¡«« Û¶ ¥¥…“'O2<<ŒRŠ¡¡!®\¹‚çy¤Ói{ì1â8æèÑ£†Áã�?NKK ===�R.·»æí¾¶ûÖ ck4�æóƒH¾ À™ššú7---=77‡çyúÝÑ|áY]]m”’�R6jB-AäÊ•+?~œt:}Ë^¦[ E׿š·mÎÌl~­ž\Òüúv}M·¶¼Ún¿m[Š=ßñ�Ñ´�a„aÈ~ðž}öYòù¼0+++¬¬¬ÜTúå =é%EÍ{{{Éf³z�h:¢(âÀ�ÌÏÏŸ9|øðÿ\M¾€ ¤öÙh4wA>Ÿgvv–r¹Ü°v)¥èïï¿©Àì�&Ù­"qk÷‚펵Ux݉íö»Ý¶w›Å¼Uˆ*¥µå4Þ⥵µ•ÑÑÑM½«¿¨¥‹¶þ~áÂlÛÆq=X4_H´ÔhŠÃMÝ?îVd5o{§ìa¥²*Q7OÖõ~¬;¥Þ“õNûÞíÿ¥.bÃ(Dl¼ª‡l‘ºPôÄÚÚƒƒƒ†±©tÑN?ózGÓ4‰ã¸Q¨°]�ð­ �ºð4 ƒ8Žo*Ê|»cm¥¾ÿn²Î›;Û¤Ói–––èïï׃E£ F£¹ýäQŸx¶N¨;©¯v«íꓦ7ãñþÿó>kk{"þîeÿæ}¶Û_)E\�Y’K´ýV©LJ”„ºevuu•™™ ÃÀq*• J):::hkk»ã¸}ï½÷xë­·ø­ßú-~üãÓÞÞÎsÏ=G¹\æïÿþïéííåßøKKKtww³²²Âìì,Õj•£G�R.—±m›·ß~›_|‘þð‡¼úê«,..Ò×ׇ뺼ùæ›\½z•C‡1<g“þ$SÙˆ7èwú)Å%ª²J¿ÓÏb¸HEVX�V1„Á‰Ì ~^ú9âo…' ¸�¸z2}ÀD`¥R!Žc¾ó�ïð•¯|…ååeÂ0dee…“'O6,nÛ}nJ)FGG¹téKKKŒŒŒpñâE®_¿Þˆ�Íf³üã?þ#­­­¼ÿþû¬®®ràÀŠÅ"§OŸæûßÿ>_ûÚ×(•Jüõ_ÿ5---¼õÖ[¼ñÆüê¯þ*Ï?ÿúˆëׯ366F†ôõõqþüylÛæù矧½½�ÎÎN†††0M“¾¾>*• ===H)ùêW¿ÊÔÔù|žžžúúúfhhˆÁÁAR©•J…ååeòùü¦§ÝÄ46g°ëÄ�€�æ–V”fx«N {5iÇ*f]®sÐ9ˆ'=B¨€3•3Œº£¸† ¦ü)†Sà ¸)3…@` E¨BÂ…ÆþRȘ<åÑauÐn·s¥z…w€P…H$11YÁW>¾òI5·î¡t­ht bb,aQ° tZ�DfÔˆ�Ô<8 �N7\«–e†!,..Þ”•¾Ý1N�:ÕØ¦¯¯oÛs9r„#GŽlzí¥—^Út�MÏ =ä:s7ewj>#ñç+Äœ bW0M“Ö¨µ¶x‘ª–®€ øÂ¯}–w£‡îaÜ©-;7’‰Díg³ÃÄn·k¿ï0¦ïN÷ß­,›�€�æ–är9¹páB£ãÇ�’@îU*¥�®D &î¹úä©nžH?]í÷K¡ ¨eR–Ê%zŽ÷ÐÛ×Kgg§¶>ˆ@pîÿ=Çù¿9Ï|pÛ,î݈¸»=ÆÖóm·}›Âžù¿žÁp7‰¶½°ÔÝν­Ñh¨Ñh¶Å0 FGG)•JT«U<ÏÛÔúm7É�ÜÀõؤ­ñ†Ÿõ„Õì–3 Ó4¢µµ•|>¯{? ¨EÅÙ:K!U‹Ï<[9‹‚'sO2ëÏ2Lã)G8ìw÷3LP–eË<ÆÙÊY§s¾zž´‘¦I‹4'²'ˆUÌ™Êá`ƒª¬62Ê=é±ßÝOUV™ gy¥íΖϲÏÝÇb¸ÈHj„b\äŠw…‚Uà‚w�çòÏ1Q�`âý _8Lgwmq·e•v[ °^�P£ÑP£ÑÜV¶´´ÐÒÒ²IôÝ‹ »Ó¾Û‰¾Anƒ¦©ãþ4â°³ºÏÝÇŒ?Ãhj”Õh•´‘¦àè°;ˆUL‡ÝA¤"£E¤ ‘ì¶ac ‹Z^À—«—i1[˜ fp8‘=Áj´J›ÕFÁ*P‘,aQŒ‹d�, áqÓau0ž§(‹´Z­œ¯œçÉü“„2$ò"6â ,êÅÙúµD¢­p/ãõêÇÕP£ÑP£Ù•ÒAäšwű³¬Ðít3åO2éM2Ìp*w ?öY¨Ê*íV;9+G).«S˜* "+D*b.šc9^æHúY#ËR´Äb´HŸÓ‡’µx )$Žáª�ˆˆé`šÕhµ‘`’³rËãïzì""ºì.\Ãe*œ"ŒC‚ h[ßIqõ� @!QéûX£ F£ÑhýW0xä©G8ýÝÓ’Zæï<óüdý'7Å’^ª^B"7Å Ö´¤�DR¥Ê;ïlúÛÏŠ?k»!°êãØ¼Q|cÓßÎUÎ5~žò§pqùáú(ì/àô;DQÔˆÙ»�8»áÖlÔÖk�€�F£y(iÁSö™Ç2d�,©L Ë7¤@€]°±ú¬M=�÷Úõ»õgÏóèêêÒ"P£ F£Ñhœ6‡C¿qˆ3gÎP*—ˆd„Œ%R5YìÉåwξÙ盲Êg↸ï õýLÓ¤µµ•ŽŽíÖh¨Ñ|ÑyP²mVê®7ͧC.—ãÔ©S¬­­Q©T‚ Qƨ>Þëßï$‚î×=Ñ“gFãëN׳ÛÓõãÛ¶M6›%›Íê�¢ÑP£ù"#¥äÆ�·ìöñYˆÑ½žh?KáW�{{{ÉçózÀ}J¸®Kww÷¦>ÖŸvû³»)ÒÜ\^è~&ZÕ…fów�F @�æ Ì�78xð ÙlvS�¿ÏJÞê¼·ë�z»×>+Ñ×ü»RŠ7ÞxƒG}T¸ûLǬ¯¯7ºØ4�çOkLßM¼ávîÞûéþÝzÞf!¸«£eY´´´`Û¶l-5šÏ3Õj!¾ïS©T‚¥YP5»/wÚ{ôn·]\\¤­­�ÕÕU\×mÔ \^^&Š":::°,‹ååe|ß§»»Û¶©T*DQDKK +++äóù=›œî5ã²ù»”’ pGºû(¼nôÿÝêöý¢q7Åw;ÎëÏ‚‰‰ †‡‡uûC�€Íç}ÒŒã˜J¥Âêê*333H)ioo§R©õS=xðà}�a Ã�¿ú«¿b||œï}ï{üÁü®ë²¶¶Æ¹sçèëëãå—_&ŸÏóío›ÉÉI~çw~‡µµ5ŠÅ"CCC¬¯¯sþüyžyæ–––Ø·o•J…R©Ä“O>ù© ¯íŠFDQ„ïûZÞGâ8&ŸÏÓÒÒB†›,€»ù»Ž»Ù¯¾øj^„íeLà^ŒgÃ0hkkcee…B¡ �F @�æó,¥”¸®Ë¥K—˜››£­­�ÁÁAÊå2ëëë›:ìõ„S …B�ÙÙY>Î�( ۖĸïçV(„@JI†„›úaEJÙÚÓÓÓT«Ur¹\#жí†ûò~~þçÎ�ãâÅ‹ôõõ±¶¶F>ŸçÑGå£�>Â4MªÕ*/½ôW®\áÛßþ6àÅ_Äó<>úè#N�:ÅÅ‹I§ÓôõõÕŠIçr¬®®ÒÛÛÛ°¼}ãh;•JE @�€Íç]ÖEËÈȶm“Éd˜ŸŸ'Š"úûû“ëý˜p”R†Á‹/¾H__sssX–…‚¾¾> à •JÑÛÛË‘#G¸víår™žž‚ “ÉP(èïïÇ0 ÆÇÇùîw¿Ë /¼Àýѱ±±A6›EJ¹Éй ͽþ¿›{kRR.—YYY¡\.³´´ÄéÓ§aÿþýø¾OOOÏ}OÕj•ù—¡³³“ÞÞ^666øÅ/~A__]]]|üñǤÓi”Rlll011�eYd2fff¸pá===äóyÞxã à Š"øú׿ÎñãÇï©Ìn÷«[&ëá [ï)�F @�æsHÝ|ðàALÓ$Š" ÃÀuÝ›ÄÌ^ºÍêppp !8ºººnš¸†‡‡·½ŽB¡€R Ïóøæ7¿I¡PhÉf�°› ð^Äb³KO[?�ÅŒ”’®®.LÓdccƒ£G�’J¥ð<¯±¸Öàú8ÉårüÞïý­­­\¾|Ó4) ”J%ZZZØ¿?�<ò«««œ8q˲èììlÜRJ666èììdbb‚ÕÕUž~úiÈçó›®§×¾S×tóñë÷jÝj¯ËFi´ÔhLÓdpp�t: °IømÍÜ«¿RI™Ý¦¤“-³ìP0¥R)úúún`ÔjïÞïI«q|!M×'V݆ëþ ÀºØ¾|ù2ÃÃ쯯}}}är¹›âëörL(¥8pà@㘽½½·\<år9öíÛ·éüõ…L]dA@.—ãàÁƒ V¿þû½ ØúÞ4‹l�F @�æ! žy»]˜û1ÁD¥‹o¾‰¿²Ò€{=ßÊ’ñi"„!P‡‡iÿÊWhÕ±S÷UZ–Åêê*§N�"ŸÏ322²©ÏîN Aß«XºÓ¼Õ>õ®�?þø¶ûïö>Ùíÿwë{¥­-5š‡€æd…û-”êǵ×^ã'ÿù?Cb4€ˆ•ÂI,v‘Rd •ÂWwnÌU/LQ�¶³… V +ù…J!³É*˜ JÞÅÄæAPŸ¨�Œaà+EÔ´¯A­ã˜,!°’s[R’þÝßÅÙâZ×Ü;±çW«QÄ`w7†i"}¿>ðjŸ‡Hš¿‰;5�»7ñ·éoÛÝswsÌÛì#„ÃÀH²ÊïGy¦;W‹@�€Íç\üÝêõ½v�5~CÖçæèµmö§RxR¢€)ϱtšO<�kAÀ�tšvË"Tй `> i5M¢ä˜)¯Ý²p„ e\ªRòt>Ï5ßçP:ͤç±El,…!ž”¼ÐÚÊ´ç‘6M¦|Ÿ@JL!p ƒõ(ú¥ˆKŽ=ìºLû>ׂ€6ÓäP:�!Ó¾ÏF“1 ¶M1޹ìy<–Íâ Á»¥ËŸ|Bau•®ûœˆðE£tõ*þ§ÿDõÒ%Î[Öf«r"“�½#¶�(MâLÜâõ� =q›ßë÷OÃe�ü?r'NpüOÿ”T’µ!¸“ûv;ËŸ-5š‡X î…Ü6†¨>±$¯™‰°:”ÉpÝ÷q ƒvËbÂ÷™ò}ÊRÒ–$tÏfY‹"r¦I¨PŒcö¹.óaH‡e±†Îdø°\Fƒ®KQJ:m›.Ûf9ŠÈ-¦Éµ0d%Š˜ð}Žf2<’Já%Ç�ÏåX�"2¦‰‘Ä$º†�%ƒŽCYJ2¦ÉRÑeÛ ¸.R)JqL¬£‰¥o> k×ìûT+� ²·—‰o}‹ëï¾Ë¡tš�JÇ0¸âyÄJq •b! YŠ"Ne³¤ £!ÎKqŒkH¥�€l§)àÃ4‰“ÏO_ÊfùÄó8œÉ0Ì=¶MJ–’qu"›e) Y�c®ú>2™p,!(K‰‘X�ä;œJqÙó¨HI»ir$“a1 ™¥°€~Ûf5ŽY�c§Ó¤ ƒ~ð:�cô›ß¬Y6?…˜@�F @�æ!�{™ä±­á8¬Ç1”Ë ¹.QD‡m³E,EPV 3Ž©JÉRb�» ».†øJÑ‘ˆÅG\—õ(b&¤$kšÌ“¾OÁ¶É&îäl"Ø*qL ,„!U¥˜ \!•b5Ž9”J1éytÚ6)©JÉ�ë2ãû\C2†AYJ>(—™�"žI&þ�8&PŠvËâ‚çá-–Åyϣ˲uåžbqa A¿ãÔ¬Â‰à ¤¤ßu €é0äªï“6M†]—GR),!ˆ”"mšÄJQ‰c*qLֲȶaPNS¾O—m“2M2†A·mÓ“¸`#¥ð”âj0¦<�SÙ,ÝŽC¬±RxR’3MV¢ˆVÓÄ6 R‰åNAcܘB0躇P)RõŒß mdM“J“7M¢JLj¦Ú€w[DZ£ÑP£Ñì9J)TÕb°ên£ÚPJ1üÌ3,½óÕë×A)V‡ùú‡GšºfÄ@»ãPZ,‹Õ¦s•€”ãà[ R’O§©&7y(XµÛ½œl¿`ÛØ¶Í#©e Ç²P€—lÓÜØrM×eEÖM“öL€z$ß00 `YËÂ*Àp.GX öõ÷#F˜¦žœ÷rÑ’Œ©¸ØB�7M�6M$5—j”Œ£˜šÕøZp(�fªZå`:�)9Ë¢`Û,F)à ¦|Ÿ×%JRr#ÈY™$ŽÔHD$Ô,ˆqò}> É›&A"þÚ,‹(Ø6ŽaJ‰+ëqÌõ  `ÛTâ˜ëaÈ|p*—#RŠ…0D�-óR2,í¶ÍyÏCòËä—­@-5-5šO]ü…++œýÿƒµ3gPÛ•Œ�66H7%cl{¬ú$ç¼ WÛí¶Ww8Þ­þ^J 9§šËÊló�íþ–\“)–€{J$Ùîn£ˆ…R©‘TT�ݬ»wp- aÈ¥ä÷‰kw6 7}viÃÀÍd¸äy,D3aÀõd{Ñ4FŒ$Ѩ>v^__G«Õê¦1e'BÑL®iÔqè°mN—ËDÀlbá®�™‹ÄM‰O"–?ÝØhüÿÒ¨z�æºøÛ’Á¯ÝÂ�€ͧÆÒ{ïñÁ?ü_Jbè>ñ}¥0„ +‰ß+KIŒ%®Ö‚eQ‘’Jb1±…`<•Â2 >ªT“cTŸÜ©eîæ ƒ~Çá¢çñ\.GYJ.V«¨ÄÓ™¸ÇÖw0•bÂóèL’KJRâ%"õ€ëÒiÛ\ªV©$É)‚Z¯“¸œÓ†AŸmsÙ÷y&—ã|¥Âx&ÃBÒ™X¯ú>�–Å%Ïcжiµ,ÎÏÎrðôiªG�tvâærz ìÍŠƒÑ¯}�§>ü�àòåZ¿å»È¿£°ö·¶²ÿ>_þmm»Ú/Ýׇ«k?ûÂ0‹ ’xÀôà éááFâ‹��€MÓÜ©-�ê­´Ô=ÔåSJ¡¤DB²¦‰ct8U)i5M<)ÙˆcÐaYä’mÜd{C.U*Já˜&QÄH*UsÉ)E—eQŒc²¦‰/%¾R´$±UÇA„!Oåó�€~™”�i‰"FS)”R؆A.qÚB°†|ìy´Û6S¾O»msÄq˜K2‘g|ŸËb1IæÈ™&}ŸA×Å¢‘$’3 ~\,ò|>�!<�¥8¦Ûuk1�““œûîwYîìdøðazŸyS—„¹g~ò®¾÷B0”“$#EÍb[/!$’ÁrÓj¬7‰|€aÇ!�’‰5n¶ìæ £Q–è×e1 Y“rÛóL“Å8¦+9o³•oÀ¶±„àZÜ2³8›”)ª*E¯e±EäMïúuÖ>øOJæ� µð†~`° ~õ¿þW²cc�÷I‹@�F @�¦A½OmÇ{&£8&¢3u¾Zåd&ƒ%U)™ ZL“ÁDø(ÀM4$PŽcŠRÖêš&�–ÅrqÅ÷9™Í’3 –㘕(¢Ã²ˆ¤D!àIÉ»å2Ïæó8Ô,„Ÿx}ŽÃ ë’7MŠqŒ-Fb-ñ•b)Ѝ&Ê‘TŠˆZ²I¯ã ·ÝLÐjY˜B4bÉ–£ˆ’”øÉdíÇ3™F©W0> IDATVp ´™&ýŽƒ-îÐc¯¾Êðþý”§§¹úÚkŒüë�Ùï¨Ù&°t馔´¤Ót¹.%)™ z‡nÛæt©DÁ¶™ Nµ´p¹Z%m´Ä1ŽaðqµÊ±tšbS‚~×mÔŽ¬HI¯ãPI’4ö¹.³¾Ï�0d0�¦'•b9ŠXC‡vÛæL¹Œ›„ N²Û[- ÛóÈ™&W}ŸnÛ&m¬DmI†º—Éú‚h1Éhÿ¤Zåjðr{;U«¤ ƒ�8¦Û¶™ò<ÆM[Þ.•ȤÓ()¹¶´ÄÆä$é6uŸ¹×bÐ:�I£ Fó�XãÄ5Z·6·yÚ•T ·½,‹776ˆ€·’ï uûÎW« +]ÝÍ*„ÀI„¢~Z,b$BN«I|UsÑeG¢ÄÒ÷½õu¥øçb‘ˆ´H)f£“_º�­¦bÎu×n¬o•J�ß•RLú>&4½n"~X,ÖJuÔcü’søÉk˜‰"×Ö”Â.°ÇuÉ>Œm,¾ý6½/½¤ã.Iü›‚VËb6ð¤d_*ÅJRL>‡^ÇÁLÆV‡mHÉH*ÅZá®iRCžoiaÂóXC]—QÓ¤"%©d‘²E�P…VËâ£JKö§RÄÀz²(N§é°m¼$c׎d³,Gé$ɤ(%�årLxå8¦Ûqè²mfƒ€Rs$ -�ɸ]Œ":m›¹$&Q+qLg’m%‚Õ¨/R¢¨qï¦Ðûv=“u8�€ÍCdBl²ÞKÁW¥¹“'ù�¿ø 6®^­%�Ü®SÂý°(HIpþ<áôt-áä3¦R© GG‰}Ëq™ö±1ægf(ÍÌ�Ôƒq7‹!P†Ájóf±HU)ÆS)>ªT%}lÃ`1 ¹ÄԬ̫qL*)­R”’·K%Ž§Óœ©TX CªR’OJ U“{DQË*÷óÅ"×Ã�#é4W«UlÀKâRW¢ˆÅ(jX¶7âOJºm›å$Û÷D:Íßg&ˆ•"•„2,…!�R¬FC®ËB*Åå2³õXÓdÁÔaY¼]*Ñ•Ô,4…À1ŒZ]CøIÞ‹Ô}€5Zj4ÍÀx›u»²íO?Më“OÞÔ[¸±Ý}QµØ«à“Oøùw¾ƒ¿´Ô°Ö¥… ˜Ä|, A-³·Û¶k0¿l '•"›¸‹%КÔ_[�"ºm›A@t˜&«qL›i6&~#±Ô8BPUŠ6Ó¬•˜9s†‘çžÃvœM]:ŸzŠëßù™o|ÃÒ�¦�_`ðŹüÖ[D‹‹¤ ƒ«R‚eaP+ǵÒ/˜&W“ñŽiR¢VF¨- G¸V7–…Ì(UK¢0ÍÆù¦¤ÓÄ5M6€×e&y-J¶ÙŠQ„J,uÍ\�"Ò@˜N2…E2)]«T6/â˜�“c´Å øÉk—Jµ± ”¥¤¸áÕŠíòI²cc Áf$‰";E4%‘h¨ÑP£yX¬'ÉýVÀݯ!›Ž³]ìÐ} GOÊ`T'')-/s(“a#Žq„àP:Í××Y�c|àp:M¨9ÓäZÑmYtÙ6Õ8¦,%DzY.U*Lø>¡ ¦R %}}¯%n¹§ÛÚ˜¬VN¥¸¤ ƒå0¤3éòa¥‚kš ¹.onl �u’`f³ä‡‡Ù˜ž¦utTÊ]Ð:8Èpg'Q¹Œ•ˆµÏ4VÍ4iþyÚ¾úÕZ¿Þ=î=Ü<ÞÅ-^7s9Ì––Æ}Xn»�B4’Å4-5š‡€ºlξ—És«ûøÓœˆëç’I‰‘${ìK¥X‹cNd³Ìå$ù¢ç1âºôÙ6 QD�ã`ݦI¤ûÓi‚Õ¤3É/J%:-‹#™ ¿¨TXNz ×]„¡RÜCÆÒiüäVã˜}‰¥JJ‰JÞãæ‰¸åøqf¿ÿ}Z†‡Mûš»&“ÿçÿ0qö,¦dwpÁ²¸†˜ÀÑL†ù kš¸Bð±ça AÞ0P@QJöÙ6Wƒ#Y¬FQ£¨ôGÕ*®aÔÜ¿‰ •b*Åb²šd —¤äHÒwÚýÞ÷xáW{ß¾{v½îæ=i¾¿w{Ì­1€Í F�F @�æsLsÈÖ,à½îüi¿ÆÏ¦IE)6’^¯g“Ø©^ÛæZðl>O%Ži5Mò–ŹJ…Ž$þ*kš,Fg«Uúm›K¾Ï±tšX)Ú,‹Çá\µJ\¬V™ C8žRl›}®ËÙJ…œaS+uÓ˜‚mc›BÐf.G.—Ã[]%ÝÙ©æ0•",•hµ,Ò†Áp*ÅŒï³?•b©X$TŠ«¾ÏátšŒi²t&½¥§Ö$Š8šÍ²¸¾NUJ>ñ}žÊç©&™¿1�5 žnia-Š˜ÃZI˜$p$•b6øØó° ƒŒiRñ<âr™d‘ß«¨ÛéýÐlußÎj·1X·îFÐj4Zj4˜õ¤Ù]»Õuû  ¿[�;3>ÎÈ©SLÿË¿4\d)`-hÎml4ö9ŸLæ«[]³À¼ïÓJ-Vk:y}-±å�� Ÿl°�ýKqL;°èûÌù>�CC¸ãã¤R©m¯==>Îú… ¤Ÿ^ÌÒ\'Ï‚}®‹ö§R,‡!G2>ñ<ÖªUÍ帆„R’N,½-©ëQĉL†Ißg,�æšï3†<“Ïc'b~=Ž ©ey»B�J,ˆ’_¶�³„ —Ô¯”{p?ÝË}Ý,ïõx:P£ æ¾?¸4ŸžÐÊårT*òùüC%híÞ^Ný‡ÿ@\©lž€?­‰¸ÉâGçOŸ¦0:JÇþýäóùmÇyjp�ùï}ùÌ3MIšÛn6Ë|Õâ9ƒ€Š”¬Æ1í¦IQJÚ‚€bRóïrµJE)ªR2éy„J(Å\ÑcY¬Ä1YÃ`=Éþ¨Ze#)8þÏÅ"U)u+KI(ÁÇIâE,$ĦˆtºÑÿú³x6lí |/÷T³ÐÐa -5{Éõë×÷Ì õ° d)%®ëÒ××wOÇ`rr’‰‰ <ÏÛs7ðg!Š›Ý^Ûu7ù4?+Ã0p]—þ/}‰îîn:;;oiDrÇ�Sš™¡exXö»ýÌ�ƒÿößR‰cV§¦hki©ÅY6 ~¥h¾SÚ·9N½ÏPò½»éo�ßæü�ÉצÏÑ49tü8Ö¾}Ÿé³l/¬Û¹“]×Õ"P£ foX^^f``€îîn¢(º© ±fs9€ .P,iiiÙõ1 ÃààÁƒŒŽŽa6DàVkÂN­Ÿ¥¬OTõ²6ÛMÂws�÷Z6Ã0 Ç!�N“N§1ï`Ùk=x�¹Ó§µÜ!©¾>Žýñó‹Ó§)tuÕ\¯R"•Ú“¾À»²(W«¾¿)fîóèåh¾æ�� †††´·F£ fo¨T*ôôô•J¥‘˜ÐQaYVã{ýçºU(•JAcU†!nR, CŒ¤�ªeYø¾O:� Š"‚ •Já'ñ_ét)%žçaYV­)ý=Š‰Û ‹úÃø^`ã†HÞ›‡Î:tmíöb²Üégn¶µá¿óÞ©S¤::ôƒadóyûÒ—˜˜˜ X,6îÿúbæ~�…­Ÿ{ý>5“¤ŸÁb¶Û,àfköØØØžŸgrr’ŸýìgœüðCúûûyûí·I§Ód2oºî�mmm�ã8p q[¯m§Âoëÿµn1�ã¸aÍÔ<ŽCnt”x}´Ôh4-¿ض�RŠŽmà·ÆÒÜI@m'šnz]qWMi…Ü*•ðnݯ‚Ú1v•“¨Ù$àëTO\Ñ<´?Îü¹sdúút F£ÑhøÅ€CCCÛoï6{;á§”ârù2Ö. ‘›ÄÝý*ÑhÛ.îý†a0_œÇ’ƒÕA2™ŒŽ|˜Pí툵5â À´mý†h4�€?{�5·]¬Ý5ÿöúŸ1LCpÁp 䆄Œ.«Ý"˜ pG]‚™UT˜}&ñJ\Aå¦s´ TYAx—å‚‘1�%yË}Ì>“x!Fd*RW <…pH°:-Âë!™|†ßÊÿ)7¥ÏÂëâd2økkd’0„ÛáyËËË7•Oz�1 ƒÞÞ^¾ Ñh´ÔÜ�x»g,ú‹L§ÉËá_óqœ^‡Ê… þ6Àê¶°ÛØ6ÑTD,cZo%¸ «LP¾B¡H ¦ð®yXy ïª&dÆ2H_"I´áô8„‹!V›E\ޱÚ,oÄŽATŒ¦ÀûØZN´à]ñ02q±6±§IÎ…˜9“p%„6øhå#^î~™^·W˜‡¥È?Îú… w€Aàû>GŽiXÎä"êÍ™ìü1Zj4šOoñ©ß‚[ôÝow¦R ¢šKÕHX-Ò“¤¦IíOaµXØí6•+”O—ÉÏ’z$E¼cfMìnå©Úv-6ÑzDæ`#e€¹!QR¡B…P·ÇEV$n¿K¸â„!ˆ–"ì‚�ÙbbµX׈Áv‘¾Ät¦ÀpkC¶G(UÍ*hüÀ§Z©êÖy™Þ^‚éiâðöfåõõu:;;±,«‘ þ †l-ádÛ6¹\ŽR©¤?l�Fó©¡—›´ñãþ …Â6"ø3>fÞ¤ü~Y–-ñbŒ{È%\ 1³&N�Cùt™x%FnÈšõ®ÕB¤þ”˜y“�‰ ¬n UU«fÉSah ì‚Må\Y–Tý*ÊS˜&þdíñJ X]ÄPüA»ßÌœI´±~u³ÕDI…Ýe|`·ˆ£X×|ØîË¢}ÿ~*óóäo¹]½èq©Tbvv–8ŽI§Óø¾�mÛÄqLWW†a&bR)Õ(RÇ1¬®®²ÿ~|ß§¿¿¥Žãàû>ŽãðþûïÇ1§N�j"¯³¸¸ˆã8ܸqƒýû÷“Ëå¶‹[;KÔën¶¶¶ê±«Ñh´ü¢R/n\ïáºÕ�µ°~ŒÑÔ(ÿî±Ç?Îü#áZˆ™6¡^qc(Õ¾R©,B:Ÿ†|ò÷,Éï° ¦c¤[ÒÛ�´ÕäxõP½ àÕgq°Ó6F‡�ÙfoÄØ}v-9%Ö“ãד£Íš`ìd\Œc S ‡�ÔØ ï¼Cn`à¶e�¤”ÌÍÍqöìY†††X^^æâÅ‹ŒŽŽ²¸¸ÈË/¿Œ‚¿ýÛ¿ÅN’J>Œ”’©©)|ßÇ4M:;;YZZâûßÿ>ù|žááaÞzë-^xá~þóŸ322ÂÙ³g Ã�™™*• žç‘Ïç±m›õõuÞ~ûm¾þõ¯7J&5 ¿æŸë×]o¥¨ù|¢…ûƒgäÐhø¹#ŽãFû¦í²€÷RJ)1³&*ÿ`݈YK üRpÞò Ø–½ã>ÉšÏvg'Ž”„8·è¿Z¿O‚ `xx˜ööv–––ø�ßø LÓäÝwßmÔŒô}ŸŽŽÒé4ŽãðÆoðÄO†!ï¾û.+++T*Êå2£££\ºt Û¶©T*9r„?ü�“'Oò¿þ×ÿâßøsssLOO300@ÄqL6›emm­aÕ«gâ7 ¿úkJ)¢(ÒÖëÏ)+++�Ïý^[îíô9þy/Í×Üp'Çq܈AlîyÝ,ønµÓ‘Ïžç‘Éd' Æ÷f/EÞ½¬­Ç¸U Ùí^¿_â²ú°¼¼Ì�7èëëÓI @M³¼•p/`%ªÆ!¹‘Z°;èblª“U‚«ÑB„Ýf×BrV-YÄ1ÈŽg !q9FŒŠFŒŸÓëÌX-Õ‰*™#µ,`å+¢õZ²G´aæLb/ÆÊYµ„‘@"A´�ïâ/³ˆÍ¬‰°E- XÖŽé]õ¦œ‡à“€r\&Š#=€RÌžÄûïã­¬�Ú¦@z]@µ¶¶ÒßßÏ‘#G°,«aahžÌ즚‚¶móÛ¿ýÛ›¶ÙnâËårµ1žôÖÍd2|õ«_½iÛæLÞ튶owìfñªù|QoAår™(Š0 £!…˜¦¹c1E¾ïcYVcl§R)|ßoÄ�º®Û°z»®KÇØ¶�¢BT_ˆA€ã8�ö™ÕjµÑp Š"R©†aày^ã\Û…,ì…ø«†Áêê*½½½Úò­ ænà^Muw˜ˆÂXfÖDV%©ÑT­Öž-púŠo±»m2‡3ø3>q)F˜§à.$±ƒârŒ;àcTPËþ•‰¬Ô`v‡�òV‡…Õ¯�'RÄÅ»·6)+_Q=W…R‡SÄå§×!.ƈA\ЉK1†m l�ÕRÆBþÒ�¦&!BÐùÜs,þøÇôþæob4—LQ ŠE¤õÑÁAÄÆñv÷ÊÖ±QßFÌ–Ä·KËÝÞw·�šÏï3»X,277G±XĶmÊå2qÓÑÑAgg'étzGãåÌ™3üÃ?ü_ûÚ×øàƒ�RòÄOðÞ{ï±oß>>øà^yå®\¹‚‚ÎÎNÊå2Ï=÷mmmÌÏÏó‹_ü˲طoÿ÷Ï7¿ùMÆÆÆ(•J¼þúëX–UóMNò‡ø‡¸®Ë_ýÕ_Q*•øò—¿L:�¦½½�……8@6›ÝµèÛÎÇq£�|³µ\£àzE¹UÞë±Ý¾J)òFž6»��óYƒ�K5á—È ‰=`Sù¨�Púy Y–D+ÊSY‘Ȳ¬•iÉTV*˜&r­V#°ò‹ *N\ ¦Àl3‰–#”¯îb#oܦ Þ¨‰Kw¿‹Ü�Ï1;L„Q;~¼ã]¨ÕÄïŠ vÇr´ø{ˆ±ÛÚhgáÇ?¦çW~aš s?ý)çþçÿ„µµ†…cǤR ÿæo2ö;¿ƒ¹ÃÞÃzÌ}±QJ5¬•J…®®.Z[[™ŸŸ' CªÕ*®ëîhœ„aHWWÙl˲8yò$}ô•J… H§ÓH)™™™á÷ÿ÷ùÛ¿ý[öíÛG¥R!—Ë¡”¢T*‘J¥¸téÕj•÷Þ{�¹¹9†‡‡imm¥££×u)—Ëܸqƒ¡¡!¢(âÀ�¬­­1??Ïää$ù|žþçæ+_ùÊ®ÆüV—róœVuÒP À/<¦iEQÃä¿õk/XCÖÿñùÿÈ¥â%$µ/¹1·¶�J¾÷ßÅÁ÷mù~»mnùFPëø¨›¶ÛÝ‹«‹¤ÇÒ<×òù\^OÆ9¹C‡ð«U¦_{�ŽG%mYœùïÿ�ë““´) •Â×0ð¤Ä1 ¤RŒº.S¾�k¬D­‰kN*EUJVÿÛcè™gÈ<؈ãÛ:ám½ïu¼íÅOóÙ @¥®ërùòe666‚€J¥‚eYôõõ!¥¼©äíê…B�cÇŽ¡”âСC,//óâ‹/ríÚ5òù<Ç�gmm�_ù•_áìÙ³¼úꫬ¯¯399IKK ííítuuaš&©TŠ?ù“?ajjŠãÇ�†!abYqÓÓÓC¡P@Á³Ï>ËáÇY]]% C„LNNòè£�nú?ÜËxmN~Úë˜I�€Ÿ[º»»¹|ù2kkk�0õ›c¯'ˆú XP:èØVd~f“’dÛÖp[cRd,Yôè ··—|>¯ÑÃŽtžc�@‘ŒÃ{±‹[LÜñµ{YÈè±® æ"p``à–îß½vo~ò�Ùì>©›¦©…ß™ÉÐyäï|òIcžý»åe àÅbãµfŒjµñZ½êÀ¾±1¬¦ ã­uûöâþk}Íâo»¢ïš›R\âGK?bÝ[§¥ÒRû ço_ÎçnÓ§ý6…ÉѶ£ÈØ6iã^¯óvܵÔP³Í £ÝDÍWLœúÓ?Åè룽½7�Fî¦|’aÐzèfWW#wkÈA¤¢½›¬D’ ) üÈ'ƒFi̓�Bñ7Ÿü ùÎ_BFÖ@®KÌn“øF 8�8D FÖÀH“8µ–™Ò—¨j­;n÷\¼y|íF­Nª×<î“s®Ý¼h0:j×Ò0i[`¶›Ä•B0rjÉ{†À*XDkûÓûù/¿ö_èsûv$V5Zj4ÍgBª»›Ö—_¦;‰­'RI¥e^n%ܶмºE¼ùu‰ä­¥·x}æubß“;n“Ll�åj™œ•ãß—þ=�dþöÎ,H®ë>ï¿s×¾Ý=½Lo³0ØWb¡î)J$‘òQJd'Uv•ËqåÑ/yH*©JònW¥òà‡ÄU~ˆË•(+ŠK4-Y¤Hb,Ì`ö­÷î»ß<Ü™æ`0X A€¸ßÔ­î¾ÓwísÏùÎùþÛ­�WÜHŠVïM@@� ñé÷îò¾ÉI×wq„¾úŽ.®¿R%É—ðwö ü5Þ’`ͱÖSÖdäd¨‹gËv¸oßÁ ¼°ÑÊþV—¶Û¦é5©95ÚNÛ±±Û6nËåýÚûè9ßòC‰¬‹ØpŒêl<ðf=ºux¡ªíÛh9�ä±$Ö¸E`Hš„5cal3ðš¡Î©gz¡4V^ ó {:ÔWõš� wÉh½ö´~· b/Ø(I).á|Ì‹&� ë» ¬I gÖ!q0AûR›Ä±Í3MÔŒŠ^йòÉ–íezÔ„‘¿ˆFˆ!£@W„lo twÁ9Ö´¯­Ê±óÎ<ÿéýÿD­RCߢc^4?%>´~�À pæÂ¬ÉŽì‘"Ü ó]¥ à.¸iå;ëÖ'‡ù牎3¸é">ËxÜLÒÔ´Šëº¸ÂÅÃëŸUbåx®ç†Ë÷nˆ ^�s[%V"X Å@ gtlßÆÆÆò,,?\&ë“|Pû€L�!§æ(&‹á}]݇/� 9�‘eõO(¨BEEEá:YÈa¸‡$#‰•°!#KŸ.ÒÊŸ@t,«"dÙ8¾CàÔäWkW©ÌT¨95fÝYü<<’$ÉzYÒ"MNäÈJYRJŠ^­—¤š¤á7øáØùZìk f ÁDm‚Ëîe´o¿� IDATä¸ 轡ÔKlG ¯ê¡oÑi_jã.¹¤žOá””´‚;ëŒá,8ØÓ6‰} ¬I 5§"4e°Çlâ{ã!ñ+*ùÉ�P‹!)t\¼ª‡œ�Qº´A 9%ÓøMƒä±$rB¦m·Á‡Ø–"(] ~ÍM8�$¬ $|>!™&øÜ«�Dˆ`„"l dYÞP:én]Áë¼µ±M·IÕªßGëÕpfÔ¼Š¤‡:˜jNÅš°ÐŠB´ÎµPŠ jN EÐÝp€�íŒáÌ„éínÕ b%\_ùq…ÿ3÷øhù#ZMÑPä5µ­"g’ÇÕ÷>>66öÊŸ88�ƒ·âT„‚.tâ"NR$IKi²R–¬œ%#gÐd EQPe]ÕÑC5Ðe �À ]âk‰–„„䬬RHÔ”�„;šÃëé×II)âr¼“ ûyŠUKª%[´Ìžî1Ze¹²L]Ô9Ù8‰Ù2)R¤ ÈËyöéû(ä ª�! âjÃ0n¨ä²Š†Úào¯þ-ú•?¥'V­(ÕJœ¿|c�Aët ! ìë6j¿VQ*©a�òœŠ9fâŒ;xKB¨=*^ÍCÉ)ÔUGߦÓ>ßFÎÊø ))X�Ðü¨Ià¨E¯âÛ£}¾j¥¶B V¯áaOÚh}­3-$]BÉ*  u¦…;ã¢ô(a¥§Ñ�VREhÑ/p®9H¾„ŠÅäE0B„¬–¿Z«-¶–üÝÍ ¶^ßlõ½çyà�VÒðMcÄ@ê’ŠÅÌí9.‡1\V€—P’ �„VEà›~8°Ç$¼º‡–×@…À ð>¸ð¤ò$¿×û{ôõõ}JþÄÆ–½àS_+! ð…ÿéçUÂH€ç{¸^è>u=×siù-ÚA30±„E‹Vèµ}„#PLUR‘|‰¶ÕÆ.¾ì#Éšª‘Фô=ƒa`¯¹�" «¢Ú*� &T$OÚPÞé†ë¹ ¢ÈKîíV7îrrá$õJ�š¨1W�cP¤_ï'¯æÙ›ÚËË=/£K:1-†¦i�ìÚ»AÛoóÇÿ/¯¤^!ŸÊßp.?ØþÆçÇYX^@S5Œa#üMz˜™•µ&h[´OwÜZ�« fUXEWÂõiJB35Xݬš¢!ÍJ$ I¨¯¬÷”„&hFx�ò  xK^Gã•*(„Ä�Âʵ4š¦ñÛ‡~›¥§cßL¾>ÔbµH„ˆFˆ!Â}0k]–›¥¡::y=OùÊ¡Û-+#b¢S.‘ü–J¯åκxór^¦½ÔFî– Ì tnÕqË.­É°bNОØ�R¼D:“&�N?øû·J®Xc9õƒOcãVþ‚ $”®ëâ¸NÃÁ¬˜”ƒ2VÇYq*¼Sy‡>ÑÇ€>@"–Àõ]ÚnWvAUQ‰kqÒZšîX7)‘¯û�$ ŒïKŒÖG¹:w×tiÚMò^ží©í )CäR9réꀊ¦i�2g÷J6Wñvåmž5ž½�ü­BÕTü„�ã:â½éˆm°Î½ÅúupZ+l[]YnIH#œ(¸.²,ß$^½Yp•\F0"€"Dˆpÿ�ÙJUƒUâ·Þx'"¸‘ÆÙ*²dù7Gþ ¿œû%¦oÞùdÖV¯¸‰iAneY9nµ^¥ÔWâ)ã)âFü ¹kÉKgЗÖX°ÖC»3qþVß·:®âUKd‡d®X)]7Lö·Ç¹týãíq.¸hÛm*í º¥Sð ÛÃD�‚^À‰;$“IÝá’v‰Kò%ä@Æh¤´I/IRM’ �ßX#Ë"…1o«ñ„Ь **ª¢¢HJ'žP‘¸ñê­J‹Âî¾Ö«õ¾çc “¿¼ø—LÌM€GXârÁEÒ°Æ,„"0ö8óN' ¸}¶�dH(y%ÜO#  ).…“‡e÷ÆNJQÁoøa&ðJxªÐVÊjλŸZ‰WbEÕ¢–ÛtƒÎwÕ¯êXr·&›4½0fq$Få“ ÿmñ¿q<œm±mqëÛ‘¿{%†«“5Ïó¢ÈFˆ!Âý!—Ë166Ö©-ê­“‚¹_+`ÐÏ›Ýovˆå¦XÝ„Àó=&*ôä{è/õiªÙ!PEhvò%Çsð<�z¬ÎDy»m³¬,3ÝœFµT ßàª}ÕVÙìf»¾�áÞaú»úIÇÒhªv[òº¶®¬çy8¶ƒ¸¸ª‹„‰/>>žìÑt›4¬ §ÁUë*—­ËL8”ƒ2¾ï£ú*::-·E?ýüê—¿Bó4&Ì [!çåP…÷•÷Ã8Ï•,`'ë õk8×Cñ~ó¢I×ñ.|ÛïÔKW ]G»°&­0®R8KúÀJ<ß¶0\ÀYpÐz5?@ŠIX×-Ô�j˜ì†!ZQÃ^°ñªZ¿†=k#)r—Æú}Ò&p´ c‡�=cãL9$ö$hžm’ùz†ÊO+(i! ÛÁtÍž�Ï‹® ¸ŸýDˆ`„" i[¶lann®S‹um]í{%�ëk–z÷¢1¸Á¸¥i»v»›L&óÈŠš �Ð š´­6¶lsÕºJµZ–ì%n‚>©�B¦€œ‘I»i3 ¦Ô0„Á©7èOõ“ÒR&cÜîØˆPâeõ³¯úÔí°øªÏXcŒåú2®êr¾qžÉÆ$Y/KA*°KÞÅWä¯�“s¤õ4݉nNUO‘mf90pEQø¸õ1S S<“†L<ƒ¤Hüé‰?åTùj!$ºZO˜ ÛÃ�w‰ŒÓ¾ÔÆ«zt=Õ…=i#2ö¤�> ãÌ:¸e—Øp ·âvâH½¦‡Wñ�÷ȸ‹.’!¡•´Ðz˜RÂd“¯å!Çd”¤‚Ú§"¤0IÍ«Hq)Œ ô 6”¸EB™¤Œ_ó‘´Ð:)§eü²ßÉ_mû›IÎÖ&X­º€#òÀ"D¸oÄb1†‡‡oˆÜL+àFnåû!€«¡¢(ȲüÈ †�¥Z,µ–°,‹ieš‰òº£ãË>†0(Q¢OïcOfq)ŽóXj.qÞ=Ï\c.VFx:õ4i-�®é÷|.Žä0oÍc[6sêŸÌ}‚åZŒ·Æ‰Y1ÕAJ±E¹HŽ_Í­¤¡ MÕPT¥Sn- À'¼×FíW1—L¤.)ŒõÀ¯ùØS6æ¨Ià(Ý ~Ó'¾/NûL¥O ãP%�yÕÄ]p‘32�5tû¢@ã7 ¼Å0vUÎÊØ×m‚ Àº`!Ô�4í€>£/t›¯I¹Bw/$põyZ-ë!"€�‚ `aa�r¹ŒëºŸKYœ»Í~¼Ýö_ôÀ´:Xf³Yz{{£Yc„Ûbu@‰JÞgßA˜¸²,–Y¨.` “Qk”F­ANäH( Z�!iˆýÙý芎"B2ÐP\]ºÊó Ú¬F¿ÖÏAã Ý…î0»ø.þNì °,²XYÄñκg¹6{ ÅUÀ†^µ—-][xJyŠ|&OW²cýîÔÖö“3î ½n/q=΂²ÀäÒ$¯ö¾zƒZ ø»ÿÝA7“õI2#™PCo}Üiÿ§âØŒÜâàÃ+¯ƒëÖËÀ¶›o8r?³Á~†Ö}Ö 3ŠM6ÄŸ^‹Ü'³»k7E©xƒÕ{3ÊÀ­íÇWÇß÷q§#!"€�¦§§aïÞ½7¹«ñ» ÜhÛÛý½|ƽÎïDkµW®\add$j<"l<±3oŒS­Uiè .T.�0ôÈ=òJžWµWI &ÂÄ :…Žç0Ls½z�ÓõÓô9}lïÚÎë‰×Iæ“wèWÏ×}¦½i–ªK88œ·Ïs~î<^Ã#¤ ÙšÞÊ‘ä¾VøÝÙnTEíXTï>>§—Nó|÷óX’ÅOÇÊw ßÙ01­¦ù­í¿ÅÉ�N’ô“¸žûiE’52=÷D¼á–’9«r;·ì/W™�óY:Y�Ë2³•ÙNòÇ�úðû‰BP­VÙ¾}{4Q‹àã‰v»M:�ƶm,ËÚÐ]µ¶C»›ÙX³Ù ™UÛ¶1 £kaYŠ¢‹ÅhµZȲŒ¢(ø¾êc A»Ýîˆê !p]Ã0:çëyñxœf³‰¢(†Ñ9ÿX,†ªª·í$îµÓX�—ªÕj×A„îv|ørXÚÌu]jZ�ñÊ8vÛfQY¤Ú¬Òë÷RÔ‹’v;x¶øìm“/� á6˜a†ñåq®6¯²Cì`{z;G Gˆé± ÏÓ=ÚN›@ X’–˜XžÀ3=&åIÎ.ŸÅl˜,·—�èWúÙÛͳ¹g) Hw¥?S|àg…©šX-‹t1Íûõ÷9ª%i$oùýD<Á±'�199Æžz6žëÝ$Aô0Š+¯×æó„fHËò碸6ôAQvïÞM&“‰<:|<±T>??�ëºLMMQ,iµZ@èÂ*•Jwý0AÀŸÿùŸ“Édصk.\ X,¢iÙl–Ÿþô§¤Ói^{í5Nœ8A½^çÉ'ŸÄ÷}Ž=Š‚ÿõ¿þ{÷î¥^¯ãû>gΜá�þè��e™3gÎðÞ{ïñÚk¯ñgög|ÿûßç©§žâ£�>â׿þ5===8pMÓpEQضmÛ}‘¿Õm$IÂu]Z­¶m‹Å¢!Â-¬?ŽìP±*¸�Ëd0ÉÔò**¯BÂOÐ'úèMô’IeˆKqôœ~wϧ€²WfÒ™d¬2FÓj²SÞÉáÌa¾–ùŠ¢tÎÁW|jn�¶ÙÆTL.·.Ó¬7‘T‰º]'/ç¾Àò-®Y׈Y1ÈØ‘ßAo¦—¬ž%®ÅÒƒ#gËg9”:„©šLW¦9ÒsäŽÛèºÎ¶mÛn�!Ú¬ØÓI×/÷;i¿Œ&ñ|¬±gaš&§OŸfhhUU©Õj¨ªJ.— ;Õ»xWc6úúúð<�#GŽpúôiE!Ë1\½z•C‡ññdz°°@<Dz,TUÅ4MÚí6¦iòî»ï¢ë:}ôõz�¡¡!r¹étšßþíßæÚµk=z×uéîîfpp�_ýêW<õÔS\¼x‘‰‰ þäOþ¤3c¿¸z}«R"D YSi²P_À ,.;—Y®,ÓE²,S� å"#é Ù@“4d峻ܿ½y&Û“œ¯œ'ÄÙ¡íàk©¯‘Ô’ÈŠLM®1YŸÄ¶m®ùט­Ì’ ’HB"%§(ˆOŸBÉ+˜Âd¬>Æ¥¥K(žBŸÚÇÉ'È'òèŠþ…Ÿ Í ¼™}““å“2¡Èw?Lʲ¹3#D0Âg#�Ç�çç?ÿ9$éëë»�ØÝ �zî¹çèîîfff†ãÇ�w\½¯¿þ:Bâñ8lݺ•¾¾>®^½Ê•+WصkÇŽczzš¡¡!òù<™LÓ4Ù»w/çÎ�£¿¿ŸjµŠ‚ÐÛÛËÐÐétšD"ÁÖ­[©×ë Þpþ÷zÖ—ùr]7j8Ÿ>‚Ok/ˆ* ´hqÞ ãâŠr‘¬ž¥¨9¦£«·«#l|¯Ï��O9(s¹z™3õ3”üG GØ·cMš´Z-N·OsaþjK¥ ÈÄ2䔇äC$‹IT9ŒË35“kË×øyíç˜&EµÈˆ1®î]ÄÔ²ôp�¦º\§»Ý�W𘘟à+=_‰_„/óÜA˜'¤���7•Jýåìì,¦iFwç`tt”ãÇ�3>>Ε+WF’$lÛFUU’ÉdÇúw'¸ÖÝp;—Ãz·ÄF¦þ[m¿Q²Ç�Ö¯ÿY¤µÛyžÇ/~ñ :D.—‹P„/<á…¢ÉÂãºw�r¥LKkq±v‘.«‹>¥�B2LÈHi)Œ˜qc2Æ}ÀÅ¥„Éï¶Þ¥Qo�U² ©C4ŒU³JÉ+QP rrޤžD×ôOÏA€íÛTÕ*—æ/1j�’n¥Ù‘ÜÁ`|�T"–{qÚ¾Ê< ò4"ÜÛÌ[ˆS˜˜–‰¥Z\n„ñqMµI¹U¦ä—èÑzèOö“–Ó¼Pzᦫû9>„q‚M«‰åYœiœáBãg*gØ*oeÀàˆq„B¢@w¬›.© #ol螊 jU™qg¸V»Æ´5Ͱ7̶ô6gcô�Äïòáò‡|7÷]ÎÔÏðJ敨¡FøR#"€ J¥ÝÝÝ7°¿¡»«~�Eîù*N…ëæu¼À묰Áõ…Í×~熺¡¬l"1t¬ žçqE»B¶�¥ÛéîÄ4Fˆð°“½ª¨Òh6hªMF«£¸-OöP|…^ÑKo¬—Lf%!£;Œƒ 6åøBT½*õVK±¸ÔºÄüâ< îËìv°'¹‡× ¯ñƒm? ¡'ÐTíöÇ—aÉYbÚ™fl~ Ë´Ø©îäÉÔ“¤2©M#« ËÁ2»@SkoÆ1òFÔx#D0Âç<@¬s�J’´)YcY +n…ÿðëÿÀ?Lý�è óáÕ_ðoãÿ–ýÅý‘0ÂÃõ<ë‚k†j½JU«r¾|­­‘PdÔ %©Äqã8ÉTMh·”4¹Wòç«>söµz�ŠRa´6ŠnëÄDŒºWg¦:Ã|kž'cOòZþ5úôcèÆMç±ÑñæƒyÆ[ã\(_ dÙ¡ïàÅÔ‹$sI$ùÑÍêœnO³#¹ƒKÕKìÏD®ßŒð�ÀU׿fœ_� œhOðîØ»$ö'°®ZHI ­WÃYp°®X¨=*Æ>sÔ$~ Nóã&δC׳]Øs6JVÁ¼l8á¹ÅFbØ3¡*©·ì� ,eäH†„ßðzxmú{ÊÆ+{=´Jà‡¦»ä†ïc‚ÌKêÖ‘ã2rF ¤k‚À PÒ pùÄe~:þSšõ&FÚ 7ÓKNÉ!‹(/˜ƒˆ©#`¢1AµVeI[âRåY;K^ÉSŒéwûÙ›ÙK¬CbóJ^ùøø��­Ø\o_§Õn1'Ïqeù y7ONÉQŒÙ"¶ptø(×Ú×øpéC¤ªÄ«=¯²¥k ‰Xâ®ÎÇ MÉ-Ñ£÷PLÙlå«¥¯v*Rl3Á ,\Ï¥k1¶<†mÛL3Mµ^¥?è§ Â㳕¯÷}EQ°›9oŽOªŸ0qq‚½Ê^^I¿B:Ÿ¾+ÂfcSñ+œ¯œçRëÃÞ0»3»9Ö{ US77Äã!€)™h¦†Ùc2²8òȹ¯#Dˆà#ŠUëÜçQx=$$„'ðª±í1ü¦�5e¡dÜY}»Ž’Sà V@à8“²!ã.¹è[t' °/@ŠIØ36B83rFF PºÃÌe¿¹blƒ;ï‚€ØÖXHg�@Nȸs.¸ ¤¬‰ð|ÔnP‹*Õ·«¨9)&�A3 ?ÞÏ¡áCôôôtîãLs†·Æßb89ÌW²_A•¢Ž<ÂÝY€Ð-Zf S6¹Ø¸ˆÕ´h*M,Ó¢—^J±;â;è’»0ÒÆ¦Yô_ó©™5lÇfA]`bi\X –Ð\�~ÑO>ž§;ÖÍ“ò“ĺc7jd ŸŠ_áTå3­½AögöóÕþ¯Þ±r†@àÈKögkg™mÏRp ìJíâéâÓ_úXÛe{™R¬Äxsœ�©�Ñ!"€ V?Ö*ȯ%‡›A0W_ô¾±ûü䓟„5+W@LÌP¯ë’‰yÑ­�[¡;Z˜c&�5nu’2nHYÉ,öêÎu'”‚˜°A€Ú«âL;7$ˆApC!ruPí¸—ý–�5nÝp �ÀY-l@_¾�‚[¸If¦/ÙÇ÷ßã|åA€çy�e} ¡Í&�ñ Α‘#œ2Náî¦dÞ w:Áð%¾=~Ç]µš-¶ûÛéº7TÝ—„ľì>úâ}üï©ÿÍ7Kߤ[íŽÛãø|@ ¦ÚSTkUÊZ™ å $Ì)%E.–£Ø.ò’ññlU¨›VÉaõÙjé-¦*S˜¶É•à K•%ò"OBIPÒJì�wp´p]Ò‘%ù®¬ŠŽì0VãDãFÛ`·±›7Šo`hƉj@Àb°ÈåêeFk£ô‰>ö$öðtéi4E{ü’ªd˜)Ï0Ø3H¥U‰¼"áa=\/³™ðŠu…ÿúëÿJ-Q “14�œ–q—]ü†�’WЇtÚ£mâûãXãÎŒƒ6¤áU<¤. gÖé6¥ à×ýÐ]쯬”BB'$Aà}jé“32~Í¿aݪcí¶±‘ö”�ÐDèò•Á«yH†„ZP±—l~Yÿ%_9ôˆ·¼æ¬žå[ÅoñÖì[üNÿï`H‘¬Ã—¾ðq=ßðoŒw’2ÆÊcäÝU­Êõòu,Çâ’w‰v½Í€ oä)êE^T^$1˜+t|Fñf�yožÑÖ(c‹cìÓ÷ñZê5Ò…;Çõ" -µ9W9Ç™æríÒ8Üs]ÕëLúšSÃõBìíñ푪@„ˆFx@Š èÔ¶]]î‡ðÝjÛU«bÍ®QmV‰?§}¾�œ’Ñût¼�ÖÇ-¼%@Í«¨YkÜBÒ%bC1‚�I“ð¤cöÜš‹—1¯™%Œÿ³'mÔ!‡$.p䔌3ï`_·Ñ†4䄌oúÈ]2­³-Ü%¯`ì2J˜õ«TÌ+&ÙW²Tß ËÏ)kÑ¢á7p½Û—„Ki)¾]ú6?šþ¯÷¾Ž&Gº��*ÉÁ <\×%lÝæJí Íz“ŠZa©¹D�×C�ÞC)Yb—ØÅ7ú¾qÇø·»…@à+>-»…ë¹,ª‹\_ºŽPcÍ1TSe@ ˜,Ò¯õó„ñFÞ¸/Bˆ€†Ûà\ëWZWBâ–9ÀsƒÏÝùº4½&W­«Œ›ãÔ*5&òfúM=‰¨A­ E‹ay˜“ó'ùNö;Ñ ‰ÀÐzáûº€73!du_¾ï¸A˜\Ñ£"ÉB¨q%£„ïó*æ¨IËkal5h{mü†J¼d¥ð5.á5<ü¶�Ò¥àÛ~˜œá…$QÈ!QSºܺf ×BÒ¨ hh=Μƒœ–Á%\<ˆï‰ã.¹h% ¯î!d�œ”Cr˜T�âBؾ^‹ç{”ÖÒÏçgs?ãŸôý“¨Á=X�ÓkРÙn2ïÎóÿ*ÿ�šW#ãf҆觟‚Q`$5BBJËÆ’Ø”c¸ªK¥]Ár-¦¥i¦–§Ðd�²U&dè•zÙÖµ�´šæ™Ü3¨Úæ¹Ma2ïÏsrî$–eqP?Èo¥‹x)~Çs·›Is’�?¦e·Ø£îá¹ôst wmZÒÊ— çêç(ÅKUC‹¼"áâv1€›MUTtY§ñn9-ã7|üš�Hüš�œ•iÙ-‚v€ßöi|ÐÀ¯úx‹^hÅ»,‡åÙœ$�4 ¯æ!§åPP%t ·ÚçÚn@l8†yÍ 3‚�””°'lüfH圌Púv�Ö™”–�+¿v€uÎB$^Å[aÍ��w¯5`Ƙá|å<{2{¢F÷°>]0oÍSkÔ¨é5Î/ŸGi+hŠF—ÜEAxF~†>½�¾tߦ»-MÝd¾6�é˜\ ®2Wž#-Ò!(*EŠr‘‘ôq9Ž*«(²ò¹ÄÏθ3\0/0³4ÃVe+ϧŸ'›ÏÞQ`9�f¬N7OSoÔé—úy.õY#»i1�_F" Ýj£guJí²Ý«Œð ¾•òfë³€7; du?CÒÿê©ÅßÏþ=nàB‚þ ö?²ò𾋝æWdoñÿ6aÈíAT² Ð(nðÿþ\àS­W9²çû´}ÆÝÏØeñé2� KíŠ߃lY±lכש֪,kËŒ–Gɸ2R†‚Q `øv×·‰åc(BÙ«•XÑÀ êz�éò4m§Í%ÿ‹ùû IDATÕj•ÑCFËPÔŠVÓÕÛ…*©a¬ž¸s™Äû¹'£íQN,Ÿ ä–8˜:ȳýφá âöÛÖE�«æU>˜ÿ€!†8�<@±§%2Ü-ñ÷L–¼%®˜WØ«ï�nH„ˆFxp(•JLMM‘L&q]÷˜ÏÃϧžçéÄÓ›âjþ¬ÛÞ«ÕF–—–™wç,Ò[ê%�ßõ>T¡òRÏKürî—¼ÖÿÚ—NÌöa‚+\\×Å�¹\m\¥^«³¤-q½z�’[¢¨…I½ƒß¼„�»½?¸,¸ |Òø„Éò$‡�Ã|¯û{Äcñ;ž‡#;\i_álý,^Õã`×A~Pø1=%0|F´å6=¢‡f«I¾��nÈ |ßǶíΘ´Q_¿öóÝ´»Û�N²‚Ï_¡âVç}§k[-Ú iº®?²¿sD¿`d³¡Ùlzzšf³‰ã8·´nù ‚à&ÝÁEþî• !�¤°ŒV&“áྃd2ºº>{LSA)`¨ÓÍiúýQ¼ŸÎs%N¯-Ú˜¦‰©š\n\Æl˜Ô”õv�>¿�R¬Äpb˜ƒòAŒ~cÓ\’�'{4œ®ë2#Ï0½<�P“íIâvœy€B²@7ÝÉA/é_ä £á5¸ÐºÀ¥Æ%º­ndðÒÐKw¼'®pY –8µxŠÙÖ,{å½|=óuÒÝéˆôÝ.Õ.—ãTÝ*º¬G7d…ü---‘Éd:ŸD‘‚G¢Ï[ó¬I’D£Ñ Z­R,#áÞI`6›½É¼™3¡õVÅÕ‡ú~àƒzàVI’�eMÓî™DËã­é·ø§Æ?E‘¢GànÉ2”ý2ÍV“º^g´mFIÓ�J¤ApÛÉÏÚmÖn{7ÇYÿz»1é�wÞ!ŸÏ?’í'êE"Ȳl?$äÛSÛ«�±3•}Ú1˜iÏP«×XÖ—]Ű Ù «f)I%ž1ž!™J¢ õ–„ä^X@@Kk1[�ÅtL.—¹R¾ÂÏë?çò7Ø¡íà`ö OäŸ &ÅPä�ã¿hòW“jœ¯œg´:Ê äéÔÓäºsw|Æ}ÍçRõ§j§PÛ*ûâûx£ô15rñn*1÷=.7.3¨²/±/º!«ío…ðµÛm¦¦¦p]—x<Þ!]’$ññÇsúôivîÜɱcÇÂ.#ë(ZH’D<§V«ñÃþ�D"Á·¾õ-LÓ$•JQ©TH$œ:uŠ‘‘R©†a°´´Ô‰ë¶m›d2I³Ùäí·ßæõ×_§ÙlòWõWìܹ“þþ~0 Ïó°m›ÙÙYÞxã dYæ¯þê¯øú׿ÎÛo¿ÍïþîïbY±XŒ÷Þ{�gŸ}–V«E*•¢Õj!IÉd’'N�Ç9yò$Ï=÷¿ùÍoxóÍ7o �k_eY¦^¯cÛ6±Ø£W9'"€KìOïçNüOFR#�­Ð+ÖfÃg¼>N­^cQ]ärå29'G^ÉSLÙâmá`ö º¦#!mZR†„š—µÂTy Oñø¤ñ íz›>©/¬Ò¡y^yžW_åÚ@ÒH" é¡�m»¸Lº“œX>�Ûp9œ:Ìw‹ß%¦Ýž¼ùø,KËœY:ÃXuŒ}ê>¾™þf(òY¦>´å6E·ˆ§ztÇ¢JA�‰ÓŠghaa�©©)�Ùl–r¹ÌÌÌ Û·ogçÎ�üæ7¿!‘Hðw÷w4 <Èòò2?ùÉOä…^Àó`qq‘ÉÉIºººxï½÷8~ü8'OžD–eÞzë-¾ñ�oðá‡òæ›o222Âøø8;vìàäÉ“\»v MÓp]÷&ËßZ"èºnD#Dx´ \1v¥w=6V@G8ØŽ�­ÙŒ5Æh6š,)KÌ5æèu{éÑ{($ lc/õ¾„"+›fm äÓ5ñ<�e�©å)|ÕçRãJ[éˆ'ƒ"»»:âÉë“tôäãåãÓ ΖÏr¶~–ìàk™¯…ò-w omÚŒYcœZ:EÊJq(sˆçúŸCU£,ÞÏcõ1rzŽ1{ C�ôÿÖ[]×¥P(àû>Éd’ááaTUÅu]dY¦P( ë:‡fzzšóçÏ388È–-[$›Íâ8ûöí㥗^âܹs”Ëe‚ ‘Ht^Óé4µZ ÏóÐ4�L&ƒ¦iìÝ»—Ë—/£ë:¹\®®ôõ¯�v»�çy|ûÛß&“É0>>NWWñxœv»�¦i;vŒ^x��>úˆT*ÅÞ½{Ùºu+år™x<ÎîÝ»Éf³ 244„ïû!ð}Ÿ×_�b±È‰':ëWÉñê{I’:¥\Çy$g±²H€6>>þÝT*õ—³³³˜¦=¾Ô°‹¿žøk~è÷¿®5�ênÓ4iêM.U.á4êJ×vé ú(ÆŠä9º¤.bzlS,LñdÙ¥f×p\‡)iŠéåidYfÞš'í¦é—ûÉwåɨâJM{t+³˜˜L»Óœ^8�mÙNf°k�˜~{K€'<ýE>Züˆ…öûÕýlÏl'KF.ÞˆwêïÐM7sËs|cøE¼èÀååe ÃÀqZ­§OŸ¦P(077ÀÞ½{éíí½©ßX3·ÖZ¶–XJ’ÔyÝÈúx«m6œ\nx«ï®º¦W÷¿þ3@µZE×õNfïZkßúëYMLüÑ�~ÄÁƒ)•JÍoèº.Û·ognnîôîÝ»ÿ¸¶²ÌuÀüÈá±….t¶%·1Ùœd09øÈ‘=OóX4C—Ç¢ºÈåòeb~ Ï÷HH zD»�ݤò)b"†¦Þ^Wî³ßÔL‹¡x2WYX^ .âxx¤%¥Äñ®ãÄå8zVÿÜÄ“4¼.˜˜\œdXæ…Ì dŒÌ‰t�:cÖçæÏ… ©”²¥(¡ã @@ÀÙÊYöÄ÷0˜ŒÈßÄJÓ4fffxòÉ'éêêbß¾}·$v‘¥õë×’³õÏÊz·êÚÄ‹Õï®ÝçFI‘´µß]{+ËòM étú¦ã¯ý¼– ®ZW˹ގ¨>¬ˆzž�5óÖµ·ø^ò{í@Em­ÍLcÛ³¹æ_cby‚n¿›¤œ¤ (ȶ§·c¨ŠP6-™hu`¬kuf*3XžÅ¨;JµZ¥ H*IJj‰}Ê>R=)4¡¡Hʆ%ÙåAÖ.ãÖ8,@ÌŒq¨ëÇû�ß•Xó„;Á™æË öÇ÷óFá âz<²ö}�p ‡¢[Ä¢/E7äF¶ƒïy¤ººèïGÓ4D |‚ƒ‚•§ùVmXÄ-HÞ-'•¸YïôÝ{ÙW°zþ+¯þmû¿O÷¬Û¦žã`šfÇ¢À¾AÆÈ°Ø^$o|qB°�¤PUª\_¾ŽØ\¶/S­W麷…X�ãòñP DV6U<Ùñ‚ `Y]fjy Wu9×8‡Ûpéý¡x²Vä›ê7I %BñäÍ:þCˆÕؾÑÖ(ÍÄ~e?¯f^%UJ! é¶¿eËouª{lõ¶r,{ŒÜp.*5ö�àòÂeâRœk­k¼œ~9º!+h^¹ÂÅÿò_pªUM#ðý�ð­!DâŽ}™ ðä“lùþ÷Q‰{&o›��,оeqí¯ÿšùwß½áÚ>ãŽ?%ºóó,ìÝKö�þˆìàà#ÕF0Âc �à‰Ôœ+Ÿãã…r¯/rÁ¯7×åü_ü§Þ~U¼ë˜d!ð×XÓJŠÂ‚ë"gÅ8¨iL¯$CÈï¿OvϺŸ~ú"öEõ#kcëgÎðögxkÈá* T„À ”•ÿ…9×EZ¹NM싨$½ŠÂ™k×HnÛFüŸý³G*8jù{ä´³þ,–k¡+›“i*K×5¥&õV?æ3Z¥R« ª*U«JwŠïèÚAJIñUå«›–ý¹š”ᨕv˵¸Îuf˳¨’JÕùôø;»v’RR<þ±Î>-eFÛ£\YºÂC<×ý™âícû„$(»eÎ6Ï2Ù˜¤ä–8š9Jn(iz>¤¨KuηγUÞÊÁ®ƒÑ é0À€ÖÔšÆîxœŠë2gÛ5�¬¢Ðð< I¢êy…s­; ƒ“Í&N�SU¶-ÏãýF»VëÄÅ=,È p›M\à…T 7¨º.Á É-¨*-Ï£éû(B�S¶ù>×m›²ã0 ë(+d¸æyÄe™]gºR¡1?O£Ñˆ`„Oáº.óóó�dš¸$I …GRßè³@*;ºvp½y�íéí÷´OõX°h4T´ +‘Ú1%F\ŠS� lÓ¶‘Î¥Ñ%UV‘%ySãâjƒÅÚ"®êr¡y�rµLJJ! ™¢\¤¤”Ø�ÝMLŠ¡Éa5•Ç=øÝ“<¦Ü)Þ[xÅT8�8À¡Ò!bÚíÛ¼/û\i\á£úG(¦Â~c?GrG0´ÈÚ÷Ð?ï’ʳڳ¸’K_¼/º!kÛ5 K¶ï3mÛ|%™ä“V‹¢ªRñ<¬  éûx„.á>Mãºm—$LßG‘$\xh{–Õó’„ âºh’DR–±}Ÿ†ç!„àšeq0§íû”]—‘XŒ�=�¸$áIE!£(4}sÅRê8–eÝU cD‹ UÀÂÂÛ¶mCUÕJ¼mf!íÛZ‚î±! !ð<� .ÐÓÓó¥'�ÛâÛøÅì/nKý•páV¬ÅõòulËf\Œ3U�"ïåI+i ±%§ÄÎÔNbù²�o÷õYÉWÀŠÌ�.1ëβPY -µ9×:‡×ð(I%²±,µÀSÊS$û’aBˆ$o\lý1%M©ÉÙå³|Rý„�ÊN^N½Lº�¾mŒ^@@Kmqjé–/°[ÝÍË©—Éä3‘Xó#„ÙÚ,=±κgÉè™è†|ÚéÓµs'ò 3ŽCË÷Y¨T¨û>ã¶’º²#­ŒË� ßÇj�FgWº$¡çó7Œ_ )Ò²Y$Uåïk5`ºXA@R’°|+ø‡zY$à¼iR÷}f]—¤$Ñð}’+„WG¤Læ‘«—ÀÏ«‚”ÉdÛ¶;Äj}ýÝ»i4÷ZOñ³l³>NCUUr¹óóó }©«”œÂ|êv�¸Çñ||æ¤9æÊs¸Âå¢u¿é3 �7òô[¤-Äûã�j"›ÑÉ !p…‹ã:ø²Ï„3A¹R¦¡4¸Ü¸LÊNÑ'÷QHR†Ø×µ£`|©26Nà0Ë,§NQoÔy2õ$?èýÁ­}SÖ'ë'qêG»Žòdß“wŒ |äî�ã0;;Û±bã�qN×Ocµ,žˆ=ÁÑÜÑ;V÷xÔaÛ6ªª�Ço²ÞdýY'È»‘ÔÆ�ú›»%kËne³YNŸ>}_ÐR,Oá—Í_òŠñJdõ[C€]\þûÅÿÎÿ8û?@‚ ð©øßšmtAàœÜz’÷ä¿#%¥6l«F�‡…üÝ@×è®Å‰Ú þý;ÿ9!#%$œëNçúEL˜«¸q½0px€½ÁÞˆF¸±±yžG&“aË–-üã?þ#{÷î%ŸÏóÁ iÚMÉúŽÔ÷}^}õU¶lÙÂ;ï¼C"‘àСCœ>}šb±ÈÐÐÙl–wß}Y–I$LOOw,�ëÀ;ÍlÖ*�¯ªœ? ± MµÉ|}˶˜Qf¸V¾FÒI“c¤Õ4©À>m]Å®P´XV>UšGpÆ=ƒ¯øIÜ3ñ hi-æjs˜¶Éué:Så)Ò^MÖÈky �ÏÏ‘H'P…zK«^Dþ>rƒ å œ«œc@à_§ÿ5ƒýƒ¤ŒÔm¡zPç\óËC<“~†îl÷c“É»:Yµm›Z­†eY4›ÍNíW€L&ÓÉ÷<�r¹Œ,Ë\¸p×uÙ½{7©T ]×ñ}ŸZ­Ö©¸ ª*’$aÛ6ü1ªªrüøñ;’ÆÕɨëº�°÷ú»”[e¡ Ápjøñ�X––Yª-ÑTš\n^¦Ò¨X¾éó‹™_ õih% ¯éá–ÝÎ{¯é˜ñqÚÛ,Õ–hx ’$;}èú ÁÃl-_[Ýcåæ0o΃‰½ ¬i‹Ä‘ö¤�œ’1v´/µªÀoúx-Ù�‰ï�Sùi…s&J‰°qC›ŸŸgvv–ÇsñâEFGG)•J7Ä´¬o8« sdd„-[¶ Iø‡þpŠÂÿñw:V![·~jŽñÅ;Ū7ƒÀzž÷…K„ø"L¢‘‰Š\aººÕ«Z•KµK(-…¹‡l,K^Ës 8ÀÓ…§Q éŽÌ€€#]G¸X»ÈÑüÑÛWx¾2Ìú³,T°d‹ó­ó8 ‡>ÑG·ÑMA+pT>ÊW{¾Š*«¡xr§·¹Ö>M&“áÒ¥KLOOS(ˆD" ¿óçɹˆÆ�Et‹ž[œ?M&�!m¤¿Õ÷Û.“Î$'ò'¸4s‰µúZ6Ä7°«mׂÑUUéupöÊYÊ^gÜÁ&¡5!*}Püðoå\%¢4OÃ^u�zÐIß�ˆ`T�B ¬‹@k€üïòË Ì+&Bèõ:Z�Fñ‹"�–ö˜�Wñª ¡&PUµVRÃõ¤JÓ4V¬XQ%b·³vBÑ+Rq+U)�/sü¯2Q"üzÚ�&çåªUÌ_ûz\%EŽîP° X–…+\†blfŒ€0ãÍ`Z&u^�j#™h†X$æçÅÅ�»–›û¾–bQ2KEöOï''s`Á¬2 dÈ�¦i�´U¢[‚5±ß{½°)¾Þâ¡ÙCÌgè öò£º†o{Ía¤<Â‰Ü & “l näu? dÔtûn4ÇÇÇ9zô(;wîäðáÃxžGOO¡PèÚB�òãÿ)e5ô °sçΪ±*„àå—_¾n¾Úºuk•(lذaA£÷FïßÜ<6�ø*°Ïõp4‡Ž@Ç·ÒËë(öG§Ž.‡éMö²«mªvçyJÙó—(%…éò4¡Ž�Ÿ‚2R½z�, †f°«uq¯Ž›ùÄüa¹¶7޽M±Müçíÿ™ Ù xE–€´®îcƒ˜õóER@ DX0<1ÌÒeKÙÝN8®ÀnM¿’MÂpe˜ÿuôqxêp5'l±¹a_y0^múè¸I’³AvÄvÜ1 þ6©S”Ï—ñ nÎE:ˆ¬‹`�[8Y¯ä�‹_ÅeK”ˆÂœ,»”’PwsÀ žéùU²RZ¼`âæ]Œ.£ÍÀ0‘šdüÒ8ÿ/üÿˆªQR©Š®àááx~hXÕU �C¥!&K“ÈŠ$I’°&¨‰i1b¥ËÕålHn  ÐЪ¡í…9èW'‰T$Y%ËÄì¶´9ïžg|fœzYOD�PgÔÑPnà)ã)"-‘ªh²‹Ë?»ÿLS´©ƽŸ²�Œº£œ>H¡P 7ÜË« ¯6noeK$ƒö ‡‹‡Ù7¸�çƒÏóBúâ µüË�y†¼!N«Þ”&ÓÎôMÆ“‚zµž°¾-Ñ“R2éNR–×Ò,²E¯Š¢PrJL+Ó”*%2ñ¥ïå´>M£ÞȈ5BØkîÝ =ȇ#Ò%»Ø[·÷ŽÏÇ��N¦ÙÔ½‰ .P(øQšùM æßEQPUõ[#6GvçR nLŸš/‘¦( Ñh”Mk6‘J¥H$]D¨Fï#!\ÈŠ¾Î–Pq*à‚TÁ‚pwgÖÁh5ðliIÔ¢Š^¯#m‰Ñn�û4‡šT 4ª@ºiJ´z wÆ%¸,ˆt$Ò“H[¢%5Ìs¦œ°Š´ýíj\.Î\äÍ�ß$–Ša%,¦ƒÓ8†Ã‹õ/²*¼Š%ÎVª+1ê tM¯æÀ}“ ®'<¿C�b È•™+”Š%¦�i.Î^$fÆhV›iˆ4P¯×óŒú ‘¶ˆßã6禢Òkg¶§2]áÕ†W‰…cwMˆ¾®®Žd2‰mÛØ¶�{U0ùF24gÄ[ «ùáì…ºvÍÿÕ4 ]×ÚQ�Þâ÷¥+s%à€yÙĵÑu¬a ES°§mÔˆŠ;í¢ujx¶‡3í p¯êàU|='-©a�Û¨1%¤`OØU Å¯nOúŸãU<ìiI"=ÜY—¥…?éýV-Y…б,‹±ò§s§9Z>Jo¼—¥á¥èÊÝ{T°¤EÅ®ày3ú —g/c—mÊF™áâ0;B³h¦>\OK°…ÕÊjžm~ö+ & í‘v˃5x�à‡1Ã_0œfƒ±�%tGÝ>—Io’SùS\Ê^bSp?HþàŽï«é�srüß ÿ—Óã§Q"Š_Ýéø†§P¡u!*+xy�@[kÈb8?Ì‹]/òdÝ“ Ž}pùN]9…Ðx×uéI„&®åV"àkËÍý¯%5PA«×(/£ÆUDPàŒ;ü|èç<Óð ¡`hÑÄÃ3r†:£Ž%Ö’‡š°¸ÂåLé G&�ðDè –v,ýF¾�¢(†�aµ‡å[мÅ|Wò|+êNDpnß%‘%hºFþý<ÈÿÖÿkõY~Åó'V³Ï¬æîÍGó¤y=‘ "{ÌÆ¾d_·]M«„7„±-œQ?Ï&ûoY@sw3ª§V={FÀ #ÐAG¢ƒ²Sælî,ûf÷±.¾Žå±å¨,Î^ ±ªP @¡\Àu]FµQ³ƒhž†)L*v…”LѤ4Ñi'�V‹B4U»ë¹‚]‘.þuü_é©ë©iñ}SÆ‚Yf°89~’¤›¤7ÑËî¶Ý·%ïAA¸`^àÌäb^ŒÞD/;ÛvÖº¤,–üáð«á_Á¬ïS ¡e!ðÀ­¸”—�®Ä+y¨Q/ëȰm¤#±]ûº¹ìº¼=$Žë ¥4Âk|/�g_c�J@Á-¸Tú+¸S.¡µ!´˜æëÎÍ8huZ½Fá³áî0e×'€ZBÃq0]“\>GsCó¢ï·)MôŠÎ¨2Ê–è–‡ö¾ådŽßŒý†V»•7ߨ1ÕP#€2æ—ç~o•P{;"èyK"KصcG+G¯�Ü a楷ö6†ºCˆÕW«î—d!I¯ÛK@ Ü2”ÒB¬¯[O·Û͉™ürä—<•~Š­á“Šj‘éâ4¦ibê&­‹L禉x4E#¤†¨uÔ«õtD:é!_°Y[¸(ä› hºÔI„äÍ]¾urü¼yIñü° 5f¡¨ Z½†Ð¯¦½8kÐBKh¸c.B÷£"Š¡ì ú(,PS*Z½†UQ#~:‹=iƒ–i})9˜‹æEZC­ »ÃÄñ‡òž/çääIv'w“nLׯ| 5ø cŽô9޳ \h•R2ëÎò“Ã?á£þ�@Ü[´fºšãw×<3ª¸æI”�#Çå®ËlÓ¶]›tÄU%W+¥�hªÆªÖU0 ÿpúx%ý ZBc 8@®˜#îÄIi)"�I=IÒJò„þá¦0š¢Ý1Wë^zâ2¡ cå±¼ �HŠ¢ÈùÊyޤKéâ‰ø¤ÚRwÌí31¹P¾ÀçSŸÓì4³>±žÆöÆÛ¿¯†á‡#“G؛ً¦ilnØÌ/>ûùó`ƒ;í^ÆÍ»«Ed…ÉBt&:i ¶,,k…`Mýö܇5q5J¡øŸ(>”¶$Ð ÜƺhùEiy%¦à•=ŸÚ’Ù_Ï"‹óœ‰}ÅÆ+{¬êX…�âK�½ñÂ8]Fg.bhWH³,Ëü~ö÷„ò!ö6í­yýj¨À‡ÆÛq•üÍÀÃÀw"�9+LJ#[óóá†,¿%�-QB ҒĶÇÈž÷É¡ãçûy¦/N©ÆUÜYT²*á@-¨Q¶Ëõ`m0/–ø©‚ikš>³�þÙ~2v†Íu›©[ZwÛL (zE.9—85y ÝÒÙ˜ØÈ¶ömèZM‡ñnátå4Ý¡î*�zô}<à{õËòZ±ððެ‡t%jJ?Ÿù9Ñ-QþrÍ_Þ2×·¿ÒÏÿxÿ089x£+nQºtBHO¢Õi8SHPëüc»Ó.Ç»Ž³Yß¼h˜Y–Ée ™CtF;šgèxÎù¾Pÿ©hª6pk¨À‡ sE sp±E Õ}\‰ðÖ°…Ùoì bOØ A¯×±‡m_Æ¥^ƒ’ïù“žŸOc�ÚhI kÈòóg&üŠa‚ „¬aËOìž´Q£*8 MIåB{ÐöÉ嬃šPi4³iù&–-Yæ—ý#°]›‰ò‡§SôŠ<^÷8™Pæºï‘$ø~Û÷90y€sùsìiÜóÐH0¨R%®Ç™6§iÑ[jƒù°›�ò's'±K6ëÃëÙP¿�`àöäY"uF9ž?ÎDn‚Uú*žK=G,«yûî2<Å#[ɲ5µ•a{˜Ï>çí+o£è zÆ÷ª9Y{ÔBKB˜C&ö°M¤;‚Q˜ýp–KÓ—¨PÁàæpêDe‚ÁÙA‚Ë‚Õê^wÖEOëX£W碬�=ä#¸"ˆWñ�¦‚�A*ý~«ù?äñò±M1ÜœKÉ-qÞ:�íØ‹Îƒ¾˜»HW}‡Ë‡Ù{ð @laóEñ ÓöföÞñù©¡†|­¸yMÌçÿÞ‰üÍ=.âüÅÊ¿àçgŽVa”jK™•þ<rÌ·Ü…ô«t(€7ì¡ à�x¨RÅq�‘¨BÅõP¤‚T%uOÔ!=‰uÁ"Ò�‹~ȧ^«§7Ö{]’þ\Î_&š!ÍP° |‘ý‚O³Ÿ²§iIõZˆHÛÓÛ, òë±_³»y7ñpÈt¬¯æäÔIZ¢5¸€ÛŽe†£Ù£ N²,°Œ�‰�$ë’(êíÃUeÊœ/ŸçÐä!šd½±^[kÞ¾o»ÂtyšJªÂ{—ÞcGlÐÿ€b(í֋Њö ¯àL_íºá�Tš@xâº. ·œË\ß@U •þ ‘Þæ€IxU'ïh `Øè­>é4Z ¤#Q#*NÞÁ�r}Q{é–Rüÿ-p=?ª²¨H ’ã…ã|¿ùû8¡†;®ä–Ø7¹�•îJvµîº}_ëj¨ÀÁ`ðºü¿¯’¨I�×Ú^ã©ÌS8ž37«]ËIûª©iËo^È«!š U¹—ËC—QË*+“+I&“ zd¢z”§ŸbƜბh ·²)µ ]èW?^Ðn'¢FøÙ•Ÿñ\ãs4›ø{˜2RäeÞ—µ¨TUGuèËõq¸p˜D9Ao¼—ÇÛ¿cn–‡Ç¨3ʱÂ1ƧÇYYÏÒ?øÚÝ jXÊ¡2Ý�nŽ•�ñtòiÚÓíd¦2¸9ó’é7·¿š?ŒðuøDN .W)�*ùz£õA%è™·@@ øT&Âðµû*j-¥Q>[F:%  Tÿµ@s{Ìö ¦*°m´´†Ðz£Žñ(�,a�úº¥!/´¨9 «dÉØ®ØWXXþ@ß›Yu–·¯¼ÍS‘§hml­ú5ÔP#€ãÅÕ4:::8vìÅbqAEõÛ‘À•É¿Œ„Ì—væ\]€ç·º©¯«§½³�t:]m;$�$o´½Á¹Ùs¼9ô&ßÉ|‡:õZ–w�QÇ÷Z¾Ç;£ïðXò1–D—<Ø$ž ¶bSrJÄÕø#=žml&å$ǧŽ3X¤×èeo|/±¦Øís;�"g*g8]8MC¹�Þd/MKšºÖI;ÎNœ†¦‡x¢í ^éx…Ïú?c`r#` ,Ah[¯èa_´«œŠŠÑY¦-ã»+¿K˜ðu]ææ£•‘•üÇ�ÿ‘÷úßcÆš! @ܬ‹†V­oã•=ìAÛ¬Ÿ•8cŽŸWè€}ÒF¹úÃ4躎†ÆúèÊâŠ@®”®ÐSßCÿl?kkØûrÅ»ÂG—?â…Ô ÔÇêkµ†ü6 �²}ûöjK�¹ÐÅíHàüíó{ÎÁ÷¹$pþ$>GþæÚÜ„Ãá/ÝêfEb­‘V~=òkº£Ý¬N®®z "Z„7ÚÞàã±�ÉzYzã½ zo׆ø.æ.²!½á‘¿sg*g˜ Î«£'ÙsG±fGu)�p¼pœéÂ4ë�õ¼ž|�HS­KÇý¹™~‰Yg–UúªêýS…J*žb\' üùdÖK >P}/ÒßwuãjÖ$×,XÐQ#<·â9Ž˜G°+öÂ瓽á7?|׎}užSJ h éÚ+bUáøôq^ox�_ÏüšÇS�?�·åTéÇ'ŽóJÃ+$"‰Ú8­¡F¿mø:ýï6Ù[ ¼Õÿ_a-Ìk­¯q<{œ}#ûx¡é‚ŠŸÐ¬¢òtãÓüaò|:ù)OÔ?�*LoPc¸‘Ïf?c½\ÿÈ„)MÕäRþÇóÇÁ„ž`š6ÒCw¬äœUfé›éã|î<Í4³)¾‰t[ºæí»Ïp¤Ãéâiù�·ùdÈÃã�áw88p5¦’Ï#+éHD@ÙÁ™u¨œ©\¤r¾Â¿^þWv6íä±Äc Îï\~‡ã'�£„°¯¥¬EøŸm¼‚W%xJTA–®îè$ZöPâ jDŶx¯ó=vÉ]wœ §Ü)ê¼:Ì�I»Ýþ@ök=4sˆ‘Ù¾Ûô]ÂÁpm�ÖP#€5|3„ì~@*ê6ÐRiaßð>žN?M“ÑTý.ÛÓÛ94uˆ÷ÆÞcOÓž’&•$}¥>6”7� g¾µãÌÃcÈâ\åÃì2Vñ�ØwH¤wô¶xªÇ@y€3PJ =‘^K¿F(ªåö= �žd,7FÚKÓc׈Ry %¨Zò /€Âþ²"1ûM™8~/qáùU½e§|ÛqdÚ&zƒNhUÈ'}®ôE =‰T±'m¬˯.Þ©JXY“zJG‰(þPð%¯‰Y M üÏtüÏš@Kù­àÕïl Y(Aů·%jTÅÉ:„ºB¸3.A|G¿X�©Z8³ÒóõQoG$á001ÀúÌz>ÿ�?«ÿ³ç™S=öç÷£dv´îøÖWúJ)cŊץ3=ˆŽÛ¶9wî����DÄ¢Fk¸gkaÞhyƒO¦>¡,ˬ‰¯AEE èIö`¨û†÷ñÝ–ï¢+ŽEìÙ{öplèØ-[_=lª à¸d_âÌô¼²Ç†Øþ<ýç�à'É’Wâ’}‰¾©>̊ɦØ&þ¼éÎï­áþâJé ý•~žozþ:²¶"¶gÌ!7” ΈC¨'„;ë2ùÿMV÷­œªP—ª£AoXxŒ h ·`�^Õú»öÂUFàÿêKuÂëÃTÎUüp°‡ßîòÆ¿ÊeÊ_”A@KW м½�1âŽ�Q2”�2;óÀhé9ŠÃdzž ³µeë#a(™¦I4%ßTy?‰à�‘5!Á`]שT*D"ßþ\å¬ážBWtžI?á©Cüû迳§q�/¬Œ­$¤…xkä-^ɼRÝþ  +ÒÅÚHäCÕÍdþ¢\VÊŒ”F8S:C¶˜eµ¶šg“ÏúU¼w µR“Œ”F8Q:ÁTaŠ.ÑÅ®Ô.âMñš·ï!A.Ÿ#"#4F¯ϵ>ǧO~ʉò ô€~mQŽ-7.{êöÐî¸í±oœýOígÀ¸cÅpÏârߤ”hy�gœg0tcÁqçáñÅø<�|šs„•Ñ•„Ñf)d? ]H³±eã#óÜ8Žƒçy˜¦I.—Ã4MTU¥R© ª~ÿwÓ49xð Éd’úúzúúúX±b¶msåÊž|òIB¡`ãÆ�|øá‡ìܹ“ÑÑQΞ=˪U«`ݺu†Áûï¿Ï–-[8zô(©Tеk×âº.†a\â½±øÑu]J¥R�ÖPÃ7…Íõ›9Ÿ?Ï/‡É«™W1T¿£@{¨¥^aßð>^ͼJ@}0H`PY™XI¶œ¥>\ÿP=á—K—9U>Ev&ËR})[b[HeR‹ÊsÉ)9ÎΞ¥o¶�ÙÀºè:›kbÍ#¬ ELƈ©±ë¶ŸÌ�äPß!*Vå&¢4ßà™#r?Õ~Jýæz~Ôù£[¶‚²†ø‡Ïþ�£'¾–±t«c !¸Òq…ÇôÇ$ucöA'H2œä­á·ø^Ã÷î¿L˜¼3ñˬeôfzª~Ä_sêÓÓÓ|ôÑG466 ùãÿHWW¦iráÂvïÞÍïÿ{‰‡¢P(�F`óæÍ¨ªÊáÇI§Ó¶ž†¶tõÎÄÍÆfÀàxî8ö¬ÍúØz^O¿^+èx˜a$Gg�R¨# ^7V>ûÓ5Ñët¼’‡[t‘æµ*`¯âQ:R"öd %¤�û(ÇÑ᣼Úù*:×�'�`¨4Äñ¡ã¨º š¿¸º95©úc*^Ñëø±^5¥"+²*D\á·»4: ¼²GéX‰Äwx�â±"ÇÊÇxÍ{í–áC�ϧ>ç©äSLÉ)Rn꾇MaòÖÄ[¬vV³¦yÍ#÷ Í…zÇaÙ²eÔ××388ÈÞ½{1M“óçϳvíZŽ=ŠeY¤R):;;ñ<�T*Åää$SSSƒA<ÏãĉlÞ¼™“'OÒÑÑÁáÇihhà¹çžãÂ… ¬X±‚p8Loo/ëׯçüùó”J%,˺ŽäÍuêšÿwî×u‰{S#€5ÜWdŒ ϶<˯Fý��ª’-7åVÉᜇð¾Öørþeü_ØPÿàéºÂ¥$Jœš9ÅéÒiÒå4ë’ëØšÙJ@Üqѱ±Éi9ŽOç\öëôuìJì"±$Q ñ~; #3#lÓ·Ýô’åY¨!•P·Ÿ÷'tAþã<²,±†,Ô¸ .¨a%ên¸ž‹'½ëù* “~>_dS¤Ús\¯×qó.Bõ ª<Ó£ðy-­at¨!¯ì¡„¤#1/™8Ù«��Pã*J@lÏ^°Ü $e§¨‹ÖqÀ<ÀºÈºûÚQÃR,~=ùkÖ8kXÝ´ú‘4 æçú�9s†h4ÊÒ¥K9sæ RJ–-[F2™Äu]¿Ï¼<þøãU2ÖÓÓC  ¿¿Ÿžž¶mÛ†¢JâlÛF×uV®\ PýŒ?ýÓ?EQV¯^]ݾvíZtýZšÃ|ò7ÏéïÖ<€5Ôð #¥¦x¡ùÞ{‡=Í{H(¾jg¤OzìÙw]˜ø~AuUBáe«L(z žÞY{–³å³ qK.ë"ëøaò‡„›ïœW%4_äùlé,çsç©·ëY—XÇŽ¶�Œ Â#³%¥b‰t:}ÓkA‚HKâ<Ì+&�–Ø ÄbÛb® b [83�ÖšÔüîW÷ù ¥ŠO�q>‰ «(A{ÂÆ-º~° ^ÞCѼ¢‡Ô¨JåB£Ý ¸øˆ®Ä½%€3ÌПïçØì1’n’îp7Ï%Ÿ#/*Dëà0`p,w «d±*¸ŠÝ±ÝÄbµï#€™Â ºÔIªÉ›^k�´Ò¹¼“³å³hºÒ׿SB z·~¡ñlý³ìÊìºehUA:�æ¿?ùßùë-�'}O�D’u²üóÀ?s±rÑ/ê°@Ñô•‹+(’Rb” iD×õê¸­È ïN¾ËZÖÒšlÅÅå‹Ñ/Ø“Ús<^ªä·“¿¥½ÒþÈ“¿#¹#üÓ¡b¬4†¡HæyÖD€ç»žçµÎ×0„ñ¥Û×%ƒ5Ô` BZˆW[^å­‘·ØÓ¼‡:­€ñ8Òá“©Oxºáéû&ÅòBÌj³¸®û� …º¸”%úÆú8R9B}¹žÕñÕ|¿áû„ŒÐ¢òš\é2Â}Ù>úgúYc¬ag|'©úÔ·^|¶†ëaU,Â^˜Hàzi ‰ä�áwø¢ï ” âçÝ]Õéš ´.„=fc�Øhi gÒág¡Ÿ±³n'››\tz‚˜»J“ð.ý:è{ò\@ñ�¡&Uœñ«ù~óu¨ wÖE +ˆ€`&;ÃÇ�³‰MØžM±Ÿýûy,ø+}¹—!{ˆ¤›$ŒÝóëìI�¦? ­ÒöH‘?ÏÏý”W+¶¨¨Þ:ÿÌý¯ì! �;ë"-‰Tˆ?göýYÎÎóxóã´-7y”¿IÜxŒ9ƒÂ󼬡†ûFÕ¯f^åí‘·Ù“ÙSõZ¬N¬Æ›ñ88y�- [î Ô…N“ÚĤ9IS¸éî.ÒÂ"/òôeû8_>OªœbU|–ü3B™Ð¢¾¯ƒCI+q|ú8gógiwÚY—ZÇÓO×òúehXdDæ¦|)‰äJñ BÁeAœœCåTéJœq‡`Gû²MpI�âDÓ4É»ùÕ¹Pš¢(Uo�”’œ�CM¨„–ù¤Òè4°ÇlP|ñi­A#´"„[v±Glô&£Ý`ö÷³huZ\£4Yâ¬y–ÿ}øÓ4ÜÄÆ¦�|·ý»¤b)ñV<ŽåŽñTÝSÜëéÁ“ïN¼KÆË|«ÉŸ'<ÊN™‚Ràbî"¶m3®Œ3Qœ `ˆ84OÃu\ÎN�EKhˆ”@«ÓpfJGJxŽçW~»P´Š˜¶‰%¬jÇ|y–1?<{·®ñÜ1EÁqœ›ž“¬¡†{ˆ ä•Ì+¼3ú{šö×â¬M­åÀÌ�ûJW7¬æâøÅ¯M‚’(1mOsböY+KÊòIߦÔ&‚Í‹“¯ð�¬™¥ÏêcxfÃ1X[Ç–ôŒ€QL5Ðy³µ IDAToõÓ©wÞšy~(VKiTÎU0: *N¡ ” ‚b(hõŠ¡ ·éXC·îÃ;ѾU¥@€ JX!¸,è†Wµ\Ðê4¼’‡ÓÍ=£ãL;U hþ9¨Q•„HðFç¬]µ–D"qÝqN•NÑXi$^¿ç°÷³ï“´“ôdz¾UäOAE©�­d9gžc&7CÞÍÓAÍ‘fRá[•­ÉëŹÕá\éçÆÏùfrþ½ÄhI =£cå,ÛÁvµ`ã^^¿[ AßësøVÀééir¹\µtÿAI°|PÎãVI·Û竜÷ƒ8˜çWkÕÕÕ‘L&o"�/7¿Ì¯FÅwÒß!H€„͉Íünüw|1õ›ë7ßóóN‰ûå~<é}%‰‰¬È2iNrdúÒ’´h-ôDz¨�×cè‹$l&ÝI.–/r1{‘ˆŒÐìfCz!½¦×WÃõ˜ 7Ü{“Á4×¶Ís(|^À+x¸9c™¯Áç•< , 4Aùt¡ûï�kñÛvø¸Ic AT�â–\ÊgʸÓ.Z½†3í FUŒ.gÜÁ‹x”O—QSªO4mßKdÛHG"I�RG("º~œÔ'ÇNòzËë÷œü}Zø½¨³%³å[“Sk ›áâ0‡K‡ñL�):ƒ�lªßDHÝñ{jžFG¼óSÓùJB!¼1Œ=m3õÓ)P`ýòõd Ú"îë’¯¯òÞùkÑ|9šü𣣣ƒ¥K—.Øð^^èG¥ºça „ó-¯ááa²Ù,©Têº} Õà¥Ö—Ø7´�çÓÏ“$PPØ•ÞÅ;#ïš ±&±æžžwX £¢’·ò$ŒÄ¢ZK¥©TŠJ¥r�ºöBa‚{q£mÛ¾NèV–ŒçyxžwËDÿ9 Á󼪥0§~£žÑü²rÛ¶ 8Žƒ¢( ZPsŠåš¦aÛ¾[|î3mÛo®®iŽã i®ëVžùÚGƒ÷oîÁK&“œþ9‡úQ° ×Z»©Â/ðna˜ ªZƒBU +’ �Ø®n÷ç]$3ö o½És‰çHER÷ôÚ^âW†®ðBÛ 5i°¤Å™ÒçÓj·².¾Žt{úË“¾Ób¼¼ìe<€Q0ª}�ç;bÌY“1p�×í~À9çT<¯yïÇÁqÆÇǹté±X¬j%I)‰F£Äb÷NŠ"ŸÏóOÿôOtuu …¨««£®®!±XŒ™™"‘ ºººxž‡®û¤gÏž¥±±‘'NÐÛÛ[%}|ð+W®dåÊ•˜¦‰®ëD£Q,Ë"‘Hð“Ÿü„ï}ï{Œ��ÑÒÒ‚mÛƒA2™ ³³³˜¦‰ëºüÛ¿ýªªòüóÏóÞ{ïñâ‹/²téRòùϱcÇX»v-ñxœ±±1âñ8³³³,_¾ü•Ô˜EQèêêbllŒ††âñ8Ÿ~ú)Ÿ~ú)?üá±m›ÉÉI‰.\àƒ> ¡¡�åË—sòäIfffø‹¿ø >ûì3TUåüùó¼÷Þ{¼öÚkض�išüã?þ#,_¾œ‰‰ 6lØ€®ë”Ëe>ýôS²Ù,½½½ 300À_ÿõ_cš&ÿ÷Ï_ýÕ_aš&RJNž<‰ªª ÐÖÖÆàà …B�¾¾>†‡‡)•J ‡±,‹U«VaÛ6üþ÷¿§­­�+W®P(ؽ{w•¬jš†”òºž ÷ƒøÍø\×ŶýO �…´‘fOÃÞ}›×Z_#¨фƳégùÅÐ/Ø›ÙK2�üæÏ_èšÎåÒeö ïe§L—ÚÅÊøJ¶…·l\üµ�#}å N–O2Sš!á%XYÁæøfB�5�æ¾:Š•"§² ÐwÉ.¡~a†=æ÷à͘G:kÔ"¸,èw *¾ÐÛ±¯kw[¢!%žô0]£Ý@�©h½nÁœ \­ò[Ñš8ÓŽÿwÆñ�m(E m‰%-® _!.â¬m_K[W›¯_x1` ðÙøg¼ÞòúCIþÆœ1ÌÀ,šlŒndkfë7êÁÔ4�¶¶¶jdm~uø�Dì~;#%ûjæ\ª‘H„xŸ'“ÉÐÔÔÄØØõõõÕV1÷âÂ;ŽC*•" ÑÞÞÎôô4™L†—^z‰¦¦&Âá0ÍÍÍ ³}ûv¦§§1 ƒææf‰–eÑÖÖÆ’%Khoog||œÝ»w“N§Y·n©Tн{÷bÛ6]]]üìg?#�2>>ÎÆ� …B,_¾œxù�W^%l„ª±0j�òYö3ôŠÎ–øÛï™óeÎSžÄ<€RJÇÁu]Òé4£££”J%âñ8ÍÍÍ7‘‚oáp˜mÛ¶U�×ÙÙyS.bsssµ±ôüóêìì¬î³cÇ:::ªÛæú._¾Ü' Ù,¯¿þ:­­­!èèè¸éXsûÎÏ\±bEu[SS…B�cÇŽ‘ÉdصkWõ½O>ùd5dÚØØˆ”’ºº:‰Ñh”ŽŽ„tuuÝdÙ\— }Ÿ1·ˆ,†´EÚªàÝáwy¾ùy¡�¦y.ýËË™—QÅW›Ô<|ëtJ�b$; d8;LFdh ·²6°–'3O¢«:SÞÇ&Žù9}‹�£ñð˜ÕféŸìç˜yŒH)ÂÒÐR?NªÝg®…wk¸ë‹®¡ÐAÇ­;w ØÒ°…·ŠoQ>X2/‰¬�àäŠû‹Uwõ ÈiÉÚ�k1<©Ü™J) ‹0›7rbÿ òWæiJ, lR9_ñÉ `ØÜTd,ÀËz¬\¶’°¼?©eµÌo~ÃK©—HESÅý—HÆÝq>šüˆP9Ä“©'I7¤kD¬†{C…T*V¬XQ%Ls“Æ�Â�ß¼]Õñ­Ž½Øó¸ÕësÛæò Sñ¼Ð>s¹‰===·¶Þoa½©ªÊÒ¥Ko"›·êux·Hà—Öã77àžË] Zƒ­E~;ú[žm~E(4†YÇ:Þ~‡—Z^º- B`cc;6%µD¡Ÿ²Y¦ßíÇ.Ø,Q–Ðoã ý bKb¨âfrÖ¨4rɼ„ã9èÊÍá)üpÕ¸çâäEúÊ}´Ú­¬ˆ¯àû±ïiŒÔ_ ß8&Å$mzÛ-ŸQ�`{z;ëw®ç|é<º¡_“wIKnÞÿñèã¼’~uÑ@�çW<ÏB ëd¯šÌCdKdÁ¹üB;gÓ[é% î©ç  ì•ùùåŸólâYÒñôß…"ȉO~Œ9còtÝÓ4¤jį†{KZ[[ill¼©h1„í›"¥7C@Ñ-Rp ×ß.Ù˜oD8Y|�s …’]bF™¡bV]¾"¼‚éò4ŸM~Æ“é'–…–1šâÓ‰Oyªñ)¿ŠP—¬–mQÖËôû©”+de–J¹Bm4‡›Y^ÂVuë—ÊßëIô0\fI| BT¨Pp œ+�cº8Íxeœb++y<ö8�@ 6ëÔpOŸ÷˳—IÅR·îª€ä½±÷8pà( ËòºçYè¡ ¼’‡Tð*¿KüŽ—v¿äk.’–½2ÿrú_8wåJX©~ž›uñÊ·Î%šð¥hTÐRΔÃG­±MÛvO¯£…Å›ãoòDø šÍü}/zEŽ�209ÀŽøZ—´Öˆ_ ÷�ÞË_•NÙSüÏ?þOÞ÷›i#$oA–æ¶IàA¹4 �Ó¼0Ð];ßykGÂK ¦/'_¾c�Џú³­~�̱Ùc´ÅÛ0-“F£‘_æ~ÉåþË´‘@„¢S$B„&šh ¶�Ц«aéÀW“ÁÒäRöì'¯ç9›;‹t%1/F§ÑÉšø"õ‘{¡†ù˜ÌM²6´vAxzæ4J@Aoñµõœi{Ô—›ŠlŽ IñP‘pw˜âá"³ùY&ÍIdxq9€RJŠ^‘‹Óª ²*Be BhegÚ!ÿI® â•=_vÆ”„{ÂT.T°-„*¯“ߟg¬4FÁ+Ü»ž­*|˜ý�ÕÞj–Ö/} ïµ«¹š=DßdÛÃÛy¬õ±Z Èî?œOo´œÎ�æýsïßdz=ìqÛŸðÊž_½–Ò@5¢"=‰;ãú Ô=_ÌÔ͹ !º-ŠyÅD�©˜MBk}¡ÞÒ‘±1ŠÇ‹¸3.±í1Ì!¿:μl¢„¬AË·ÆM‰bøÁN? Á5«XB¨;Då|…àò æ=­£„üÏ�Žô­kÏŸÄ­¤)q¦´”†›wñl¿'£b(HO¢·è„V„(Ÿ-h `˜Ø#6BD6D°&,ܬK¸7Œ5d!‚J_5ªâ™W'dŒ.{Ò‡ªŽà÷ÿ·qg\ôŒNx]k̳™�˜åƒ¿ Um%Õ�Õ'™sÉæH1ÁŒ7Ãhy”l)‹c9è¶N®Ÿh0ÊæºÍÄ”/†^dn?+¢+X“Zƒ&´»BĤ�L0ÁTaŠ>»�ÂL�ƒi£�Ý�»‰"èš^›Yjx`P)VG.Vð¤‡b(­Ö Ehy{ÐFMªš~µ®ë‡çZ·Íô_ ”žÿë ;¸.nÖõç· Žß,Ð@(·äâæý\`wÆ­Î!^Þóu ]y�ŒÈ7�ý“û‰cô¶ô>°)É%ëŸ B�ÒÃ3?¬µ�¬áÁ"€wòÀÝ×HJlÏ·zÕ° E4­ !+¡ Ÿš~µ²WöPÂ Žæê !=‰–Ò((àdÔ°ŠÑì?€¦g"¨!­IC(£Õ 4UÂ+yþD¨+h1 %ª „{˜—LÜ’ë¿/¥\Ì+fU"Ah‚ðš0nÑ%¾=ŽWöp‹.F‹�±ÄÀ-ºÕ~šNÁAKj—ýK¯ôË}�ÃXm\¤|¾Œb(„–…�Hô{ÐFo×Ñ’šï±KƒðnÞEoÐ ´üs“à9ž/%Ñfàæ\bOÄpó.ö¤íÌ”Ž3åË>ÌIJ(ªßo²v–#}Gh+¶‘¬K…QU]èD€@>@�¨£[ëÆ¨7Ð4 �À–6¿þÝñnZÂ-´5´ñoCÿÆ’Äêu_zLxWÆctz”‹ò"3³3´«í´EÚØiì$Ñž ;Öýÿ³÷f±u]Wž÷oŸùÎ÷ržE ÔhJ¢eK¶lG¶c;ž-I:U�ª4ú¡P]õT/ T¡ ¨~ø€.  ¨ªn úën Q�$vId;qâQ²[£5X²D‰ç™w:÷L»ïEQ%Q–,sAÉsÏ=ãÞkÿ×ô_¤ÔéhzI£,É­ 4‰�?/`Y2Zʧt¦„;ä†:NÓÓïM‡½{;,‚R@ôÎ(ùƒy Œ ¸+âϧSË?J\(õ•r¡ÞsúCïxãaE°0Š©P:SBKk¹ ìKWP3*š§¡_O8s_~Ùé,ßmùî-B•H&‚ vOíÆ˜6ØY»“D4±4ð—äÖ€·š(„Öh©·Da�øö8Š®à}Ü1|(�+‘¼/IPðF=d*1òá6­.ô®)‘°À%~wor¦çeƒ‰7á¡%Cà"”°9vP @ESPªdQÜíŽ� Õ³=¼ � ¤Ô¨JvO–Ô£)J=¥Š·O–$~.]öi›Xg wÄ ›ªa!%C�†Ñb„q"ŠA>ÀñP“3…xC~sØéÄÏû¡§QÁªôdxÿv¸pÍþ´�UÃOžDM¨(FHòªÆT<7¼�â‰À¹2‚t%)‘bûªí¬ëXwU¼TÏ5>ÇË}/³³i'i=MD�ðBã ìÚÅÓ�OQ/ͧ'‰íÙøÂ§×ïedb„!1D>›g¹ºœÖd+�ë�ËÄ.ZÖT­á�žwèHw,M¢%¹¥¤ ûÃÄ#ñKê¼g—=ˇ=rvò,FÔ@ø‚Ô½)‚|€3ä„^¿<øy?l×ù«£«/ðþ]ŽÆKJI‚/nx‘3þ¦JSadäX ¡ ’÷&C08à„]AA«@ ¦T"÷†¡b瘃ê©<�z€‰î�ë‘=œ9ÅÓ�Oß’ižæñùôçœ:ÅŽôš–5ݘï%Y€×#³ÉËã|–âÍôþ4YMÔGëé?Ð ävç0×›”Ž—BÏ—0þÊøüùo3Ÿ;ÝN·3ÛL É‚EñÂÊ7¥á;ìVÂ+•»É~�½èö—veßñŸÍ=ö t°{mì£vxü9Ç.�.àôœ¿ÞâábåšEL¹3‚7á�¿…ÔZ5 × û¸ç.AÛ8'œ ]ƒFdKgÀÁïC=ö1¡ Z;Z¯Ùº�i1^lz‘]C»x¶ñY,Õ"®Çy î~Ñ÷ ^jy S ©+¦ƒilÛfXæìøY�0îŽSTÓ¤5Ñ‘ìànín¬ê+·m‹‰X!¡nT�.i•%¹e¤èµG/ÛÙÀ•. 3�èúLð((RÁJXè1�`¤8Âéüi6¥6¡HeAú5�¡7=‰cëö…yÄ£áq­øÏ×>O2’dÉé·$ßXNÜ� ún ”’7úßà§ø)ÖZ wÐÅÏùÙ ½•Z�!�AB9ßÊhŽË œ^'ì{9éc®0A€sÖA«Ópû\„&0W›aQFµ†Óã K2ÌÏ›íHÓÃPˆ7ê]´]‰„!]Sj•Šb*¡Gq§²VÒ((%Ì×+\œD­7ë¨ñ°pÅZ’³J;lÆn­ Q„.Pt5²öËÒÅïOI*ÓÁe·ëM:Z�†‹#*ÅsEÞkx�.µ+|'ó±Ùcų<&½I¦‹Ó”ìBLhœÎžæøôqŽ}~Œ®TU‘*ÖGÖS§Ö1íLó½Öï¡)ÚüyK×Hû³*²Š7ÇßdmfímG´$ß\ ü€z¿þ²œ˜öŠ¥Y©è„ÜžJD SV¦<ð{÷ÆUBÏ¢'½ ú¹ÎÇòPáø <òNû+-¥á¹XízZ§è† ¾5•"k™E~*�?�¸#¡ÎôJ¶oß�"�’Zâ��7x4þ(™Ø­Cô,‘±�ðùÀç<�x€öÖö%Z—%ùfÀr�2¼ž‰{#ÀbYI}9ñ%BèI#mP<^ÄXVÁ 5,ª@¨a^›SBEÕf!�0®p°€°±õ1Ô”J~4Otu”ì§Yô:�XgŒ©‘)@‹h˜ëLŠÇŠÄÖ†ìûÒ —–Ðð¦ÂÂ�À ˆ´Gð >A!@M…í›ô� „ j…ŠŸ÷Qã*ZR [-MÏä :ÑUQ‚Ö§ÏA‰+µö§×ÁZqMÒ•[ ¤/¾@:’è†(jZ¥$KÄÖÄ@€;êb4„•Åz­Ž_ô‘NXQYÁ>c#Ô°ªO:%æúM¾;I� ¯Ç>mcÖ™(qwÐåËé/y½ø:ŸŸûÓ2ÃïãS𠸞‹ô%Z ‘¤”15FÄŒ�0Dý(u^›¢›°ÖY™<‚¸l©Þæ.JŸ]ƒ»è)ô°*¹jQÇPT‹’°ŒÛãd¬Ì’fY’[C ¨SëæíR1$åLuoN¿ƒÙb†Æc( ×èXþxX(b´¡Hqað|ú¹’+( �ëÎü!×>a%°öúU£jî�öÃ*aŒ6ƒØ1¦~?^ï�Ë2à7¿áõZ«[o™W8íMóÖø[49Mü é·\HzI–à=€e8;ð¦[U³ªÔ¤”øAHá¢ÆU _P“j8Z•†;ââMy˜­&JL!¿?�µÒœ¡N «ˆ#+#¹³Å¬@äåInOâOùH[b´8§¤+ÉïÏÛC–$A. ²:‚Ð~.Nþ´RФ5‚b€µÂB('p* ôŒzeXÁìgÃû‰mŒ‘ûÓoO—w<Ÿã«Â²»–¡:�¸¼°¬_MLV¥VqÔ?JnO.̹ýé8ÂX�¡~˜Ú5u>O9t{¡ÖªD6Ep\Æþu 4µ7‘P‹ê xkô-ZƒV:êo�b._øÉáÐØ!N=LcmãRç %ùfÀ . ßJ 0""B00ý»iü¬�–Ñ0W˜OC  B’ªÄÜs.þtè‘sûÝ0‡¥ )õ”ÈïÉ£·é¨)5¬V¹?ä¦ÃJaµFEº!ý‹PöIo4 ÏOC¿\@PЛuJgK¯_©·„PÂj]éɰZW¸ýnH±2£µT^uS.²(±OÛ(1…ÒW%„z1…*P¢ …ƒ…Ð"7ÂJawÈÅh6ðÆ<Œ# ]Wk”ºKHG¢Õh­!È,-¢7êäöæP«ÂQP �ª¢*äöäPkTðC>/½6LX.)à�úäæ ¦ÔS˜T§«©­­½"ôB@ÙŽê¼Ö÷UV�V#º¢óHÃ#¼Úû*ßo«Î¶ IDATþ>1mñšÉ7E›ølò3ºü. u©ãÇ’Ü\†²C$ÌKWÌ ßmü.»ïßÍ�ü Ý8¯—£@ÃÅû§Õ4Õéjöç÷“12¬¶V£)Úeu¬%-þhí±ªfE¯x>MBÎJ»˜ãtBT@§¨8«NõœbGõªbU‹Gp,`o~/Ñb” Ín �5LòÎè;4;Í¼Ôø–a- è%ùæ@!D%ì;·øzÛ�‚O5–‚B¡Àî/w³"µ‚ªªªE«>ãŸadx„ï5M½¹Ìh�p`êÇÇŽópæaêkë¿q^¿\.Çääd¸”øþ-ãìYÈ8SU•t:M<ÿVè‡>Ú£Ñ(SSShš¶hÀ5˜Z�Vþ®óïðdÈï7_Ãò%YTEÈ…(ƒÃƒŒ�޳¼f9µÕµ‹Ú¾ÈP «{ŒWνÂ�Z~DT‹Òmb køýàïy¸ñáEãÏZŸ^χƒÒo^4`¹$Kr­bmZâ²ðã¡�iAÕ]Uاí0ŸwÊ'÷Yü°E›;è‚þD¸ ›í&ZµFþ³<ýwÇïFG¿rËO1«¹¸roòÙÇSU•h,JUUÕ¢-ÐçÜs||îc^l~C¿¹^û‚(ðFÿ,ó–ñbÓ‹ßÈNŽãP,éìì¼åR¾.üf�µãÇ�£ªêe©“–७­�žžz{{±mÏó®ÞÈB�ÙžÊo‚åò�Ä}s&�ªªÔÖÖÒ¹º“êêj‰Åg²Oi)¯{œw‡ß婯§P„šèú²}aŽ`jÃâœGIá«>“¥Iª¬ÅÉ1t‡¡¡!ǹåÆc<§¾þú¼å4‘¥ùvåysUI‚°7µqùùäîi— #À>n‡ŒbTŠ3ôzky˜§—ý$‹´%n¿¶ÅôÃïA€Tä5逅ìW^œ à ‰,Š÷/§åx§÷vÖ켩!V)$'ó'Ù=²›ÇR�Ñ�jøÆ�444 ( ¾ï_Ô.pîüžÝ ìrDâåýæî_Þ6,t|ÍÝ_A*•bxx˜eË–-ÀëEQhoo§­­ Ïó.€·R. \HX}½ðÛ´�]íâ?W©+Š‚a˜¦yUÝ?®Vš£Ídý, ÀŽº!x¨þ!~=ôkRZŠ–XË¢œg[z‡&±ÃÚqÝžEß÷eݺu t³æÍÜ÷,„ —ËÑÝÝÍŠ+®é˜¥R‰ÁÁA„¸®{Kê…[aN•牔’ööö _õÉëy±ËÀˆˆ„ô+y\Âè‡/Á `Ps‘¥3³Í­MÃh2(LÌ˶„[LQUuQtD)(ñ«¾_ñDâ ªU7í=—d‰ßOþeJáõ? j}³‰äËk|¡P```ß÷+ã¶œÖÝÝM¡P`Íš5;vŒššÒé4±XŒ3gÎpç�w2>>ÎþýûY¶l©TŠ“'O²mÛ6>ùä“ʾ===tuu‘H$øì³ÏX¹r%Ç�çÎ;ïDQTUåÔ©S,_Õ–£�eCb¾ù¦( žç‘Ëå.—Kð:� a=ø[Y–¼·h\,Y_M_¡�/§¿dmjmØÚªæ!^>÷2;õ�¤�ëïçÛ`6ð¡û!y7O\¿¾P•ïû¤R)LÓÄuÝʸœ ”®æY^ÊŠ^è{™ë½M¥RLNN^óâ?00À–-[*ƒ[=dt³æHÙXšœœäĉtt\¹Rµä—è+ö­½4¨PPx´ùQ~ßú{F�¢ÇtŠ»‹ @ò®$”N•*½Ã� KÁ>mSx¿@CM÷VÝæóÝ€÷6{¼–¿^ïŸ'<^y�.¥‹ÆtãÍy·º�nvífkd++ZV ¨ßü”‘²!0>>Α#G°mEQhjjÂqr¹o¿ý6étšúúzvíÚE}}=wÞy'MMM|ðÁtvv222ÂG}Äøø8O=õ»wïæî»ï¦±±‘½{÷bš&‡¦±±Ó4ùøã�ùôÓO9yò$–eñÞ{ï±råJ>ÌŽ;8pàÍÍ͸®K{{;Û¶m›wŽ•A®ëÁ-Ùð —@Ê’Ü ¢…‡êâWC¿"©'iŠ6ScìlÚÉ»ÃïòLã3Êõ)÷WßÏçcŸ³£aÇu§ì•¶m›l6K.—C×u²Ù,†a`YÖ†•çy¼ûî»LMMÑÒÒÂÁƒ©­­¥­­�l6Ë]wÝůýkÖ®]‹¦iåç°PÀíz.ãùñ+î×m ®­Ž\>‡¦içsŸg<€Vã¬÷êy°ê-‚ÚH-µFí €Wk \ñxšàÓéOiwÛYÕ´ê¦è WºìÉíajlŠ'kŸ$Kß6úµ<\×EÓ4‰D%jQ[[‹ïû$ Z[[±m›††LÓ$‘HpàÀ2™LDnÚ´‰l6ËØØ“““ òꫯ²eËhnnÆq‚  ®®Ž•+W’J¥Èçóxž‡¢(ttt iÙlÏóÈçólÞ¼¹Ò»z¾òÜHË\’%¹Í@àõó“ÞŸðƒæ�Фõ4›3›ykà-žjzê²�äŒ4°w|/Y'{Ŭ…À\.Çàà ù|ß÷ùüóÏYµjË–-C×ujjÂòôl6ËÀÀ«W¯¦®®ŽD"ÁöíÛ1 ƒ_ýêW455ÑÖÖÆ»ï¾ ÀŸþéŸòË_þ’ññq<Èôô47nÄó¼y“ùgÿ]íxžwͰ2:}ú4gΜ!‹¡ªje»¢(‹E¤”ÔÔÔðòË/óãÿ˜ÉÉIöîÝËóÏ?O|ðÁÔÕÕ1>>Žišd2N�:E4 ¸®Ëðð0÷Üs555xž‡¦iŒŒŒðÁ�FÉd2´µµ1>>N¡P —˱iÓ&öîÝ˦M›8sæL…о³³“Ó§Osúôi„Ü}÷ݼÿþûèº^I$Ïår466rúôib±™LÏóbåÊ•œ9s†ææfNŸ>ͺuë8{ö,uuutuu]ôìËá3Ïó°m»Z»ìØñt[G—¡hAò˾_rlìf«‰=e‡�„Æ\œ3NÈ)ÚfàœsÂJà™*£Ý@�«:xˆw»Þå‡õ?D‘Ê ]4Ë‹õõ€À}ãû(M•¸§ùž›ÒIcDŽðîÀ»¬ëØÞ²}Q‹Ýn%�Ïçihh �399ÉÈÈmmmüùŸÿ9RJTU¥££!š¦±zõjt]GQV­ZÅÊ•++@í¯ÿú¯QU•¿ø‹¿¨Ì‡²ÎQU•^xUUÙ´iB6mÚ„¦i•±rÇw (J%4|©ÈìZ€%¸$Kr›JL�ñBã ¼=ò6Ï5<‡®è¬Œ®d¼8Ρ‰CtUu]Ÿ§Á=Õ÷ð騧<ÒøÈu[Ô¾ïÓÐÐÀ™3gð<�{H$Âèè(mmm•ÜÓ41 ƒ¡¡!R©étMÓ¤P( igÏžeóæÍ(ŠÂï~÷;jjjˆÇã¬_¿ž©©)TU½¤‡i¶Å<;O¶l1_íâ\æcbb‚ªªªÊq\×err’¾¾>âñ8ÝÝݨªÊèè(GŽaÆ ìß¿Ÿ|>�mÛTWWóÅ_P,inn®{ÿþý†ACC§N�"™LòÞ{ï±mÛ6‚ À4ME¡ººšŸýìgÜÿýôõõ‹Å8|ø0çÎ�ctt”ÚÚZ†††Ø°a_}õgÏžEJÉ /¼P •AÞ©S§ð}Û¶+ÏçÀ�•ëøè£�X½z5¹\Žžžt]§T*‘ÍfÙ¸qcåÙÏÍ—-nÏó” — 2—çèC2PÀévˆvDÉïË“¸'�UqºðAKj˜ÛÌ i½t%n�‹±Ù@�ªŒ:£j—Ùù›‹A™Ïûz­Çíöº99v’g›ŸEU¾ÞО‡Ç±â1 à{Uߣ.Uw[êÕòˆF£´µµÑÚÚŠªªx×/õþ¢ÑèEÞîrv> \ÿBt]GJYÙoö±fï;{.-ä>–à’,Ém*3CWº‹]ý»xª9ôúÝ]s7»wqbê«S«¯ëøµV-žâ1R¡6R{] `xx˜©©)ššš©�²²++À—^z©â!Zµ* s¥ÓiV¯^�”’ÖÖÖŠÌf³KýRá�+)˲gêZï/‚Š૯¾¢­­ )%¥R‰•+W²jÕ*|ß§®®Ž©©),Ë"‰ ë:MMM!8{ö,Bî»ï¾ …CÄnذ��>úˆêêj:::PU•Gy)%º®s÷Ýw£( õõõ:tˆõë×ÓÑÑQñpvttTÎçy¦Òs¬_¿×u™šš"‹±}ûvÎ�;‡eYìØ±×u‰ÇãH)+×ñá‡R]]]y–eQUUE,£X,‹Å*Ïd¾ç>—¥àrâG}ªŒ*4ý j^r�TZ² U ‚�,^K‡ß—ÞL>ž'‰nŠ¢D‚É ,™'/u1йãñZÁß´:ÍÇg?æÙúg¿öŠßœ’ã�þw¨ukùaËo:ÝÌ�ȉ \ÀJ¥hM$`r¯<Ê?¯ðµd1M¸š÷}µ•åó�©o[îñ\’oµ¬ˆ®`²8É�ÉlÉl ×>Ì+ç^¡:RMµQ}ÍÇVP¸·ú^~?ü{ž±ž¹¦°r9¡ë:…B�ŽŽ,Ë¢ººú’ž’Ùô|Êo¶G%‘HT@Ê\z)oÎ\¯L^Ë]>‡çyôöö²}ûöŠÇ¬¥¥Ã0.ð‚UU…›íííÜÓì¿ç“ÇœH$rA>!€iš,[¶¬rO?ý4ÉdòB _{eð.„À²,V­ZuÉEDJY¹ŽÙï¡ Òç{.óm/·Ö¼’ ;ÃdŒÌeÙ‚öD;�ý4K� ()ôzô°¿¹ôdØ»<£!êÞ¸‡3ä =I£ÕxQµûbVPÎͼÚãfƒ,/Ÿ}™�Õ;IF“_ŸrQ »ØÍûƒïóÝäwi®k¾)aç¯K†ÞŸƒÿõ¿â  H‰¼†÷/,‹æçžcãŸýj<¾ w½ÐÜÚ%Y€K²$-€]Õ]üvä·¯Ÿ©˜¼Ðü¯¼ÊΆ�×Õ..¥¥h‹¶qbêëÒë®våƒñqü\áû´rb‚@^Ø?A\Ϊ%•‚hôåLNäó,°â Öù¥òÕ±1ìl¯ºúª«üˡ㖖2é4¦aÌ ~Älœû[ˆ`6ȽÔ>ªª’J¥®{!¹œG!6;4u©û™Ý£\„ÐJ �"rl—9!/w�£ÙQ"Jä²Þ8�à™Ög˜~hšƒƒIµ§d)À$äöKÍ#¦[M–ÂV‹-­-�Á�È%Z Ò`ø½÷xëoþgæ}\Ò‹2ã….k¬„Ïdä�wˆüË¿PÇ—¼^Ïñ¨Òª®Øo ‰qW×]œ>}š‰‰ J¥Òyç¶i+WY.æ3,{ÿt]§¾¾žÆÆÆyó~žîûgêŠu¬i_óµè¦€€3Þ>èý€ÇÒ�ÑT×ôíYg€Ý ëbAO©Ä½É${³Y EaØuQ… ¦(@)Hk.`û>™  ßqh4 üEÊû¼/à\¸—¤"Ùl–††+D‘—*_H•Ñ­ êú�²rÞ»w/étº’ÿM•˜ãþêûysàM^lyMh¬‰¯á­ì[œÊžbUâÚyÃRZŠ;wðÉÄ'�Ä5�;âq&=�ù<~9d(%¾ëR­itÍ|>éyDU•]'�’@J¦}Ÿ˜ª’õ}ª5�ýùeÚ÷9žÏ“ŸžÆ)•ƒ©)ÆOž¤A×±;È1E!®ªŒy ºNÞ÷i5MΖJÀ9Ç!®ª$UKU9cÛŒ�<‰—Ë¡g2·œ"¥${ö,^Ðn𔂀ÂLhLW,!(IIBQ0GJ\)u]Æ}Ÿ&]'¡ªä}Ÿ1ßgm$‚&‡s9&¾úŠÔªUóö,ª@�ª$äÂ=ΑH„õë×ãy^E¿ÍöÂ\*o¶.˜ýùbð÷Í>öl€yÙï(‚sî9ԜʓMOb™7¾èÃU\Þy™“ü°é‡DÌÛ¿�ìÏ<£aÃ>úÃfá+ãã(À9×�×9T,V¶ÎDÕκ.-+V ¥Ó‹oŒ]fý�)(çÙúW0Ø–à·LÊ•yŽãTøØE¹ b¯œ3Ÿâ+ºb±H?uuuär9ŠÅ"MMMŒ��ã8­­­LMMUøßš››éïï ©©‰¾¾>,Ë¢¾¾Çq¦¾¾ž¡¡!ZZ.ng6<<\I€�Åb RÊ Í·(WJ …o<h�´Ò•éb÷Èn¾S÷«Œ×_#£g¨¶®=�hmj-î´Ë§£Ÿ²­fÛ埯”B`* ¶”ô:]±‡ ª4�é…éJI,|!”¤d¹eÑ[*a( yßÇ-/àeåȰQ’’!×eu$€ã` Á€ça Á°ëÒ¦ª@!XaYœ¶m"3¼ ¬°,œ™ãû„äÈ M�ð}ŸØ’H�ó}&<�3Å Hjž”T«*Ù ©iXŠ‚>“ Woä}Ÿj]=f ¬X¾Y 0£iÜ‹1五“ âšFÞ÷ ¤Ä‘MFJ%ÚM3ôÀÎO½®Ó[*…ž“b‘R©4/ô¤G¿ÛÏÆøÆ«\º®ßЖŒ7ZÎÏqhð¹â/‰X7ˆ »Ã¼3ú[Ô-t´t ©ß¾e5Ð4Öýñ“�*Ä3&‘h´b@hÒÀ$ÎMqù8ŠP«VaÌT÷Ï56k>ÚÒÆ—>Y‰ì!Š /óü¶m�“;½—@ ãããLOO399‰‚l6*ùL†ÚÚÚJ•ߥ䗿ü%ÍÍͼýöÛ(ŠÂÙ³gyæ™gøøã�Y¿~=ÕÕÕ|úé§?~œ�7ræÌ>þøcòù»�ýcûéªîBS4©}„Ÿ÷ýœÛöo1•kº’Ø=º›Ý#»Ù^»ý’ûyBP³f ûÞy‡á|žb005…-%}®‹•ð­:ó.=�bàJɰïWòmMMDËïXJªÖ¯g�¢0–Ïc}ž‡'%ÇK%”™ãz@_.‡&šƒGJ&s9t!p¤¬xî4 ÖØH�L.¼ û pû²XD‚^Ça¹ir®T¢Å4t]Ü @‚:àà+Ç!­ª8R’ó}| Z×áγR¢QF<�3¥#®KFÓr]Òš†ä|g&ô[«ëL¶”œ,•P„ ¥ªt».ͦÉIÛÆ˜Y½ =Ÿ8®Ã[oñôò§¿Uúy‚ v îâÅêo8ø 8æcï~«yŒºdßfÙÏ—üÏeûÐ4 C £ ¦bðüªçÙšÞŠ".‘"0 ÎN-XlðçKŸ‡?ä'~�'ç7VKn‰‚S ;–åéÄÓ˜†yÛ¾³%x ÖC¹3�뺤R)èîî&—ËUB¡—´ŽãPUUE±X$‰°|ùr¦§§¹ï¾ûxë­·hll¤¡¡�¯¾úŠt:ÍÐК¦UBÏ÷Ýwûöíc||Ã0ضm½½½ÔÖÖ²gÏÚÚÚ8tèö2·ÙgŸ}Fkk+{öìAÓ4*$Â×g·û:šÁ"|§ú;¼9ð&�ÅF" ¤õ4O6=É;ïðTÓSEvÕ`…í5Û92}„ßÿž{kîÅRæQ-á¶F@8Ò™ªÐ¢äE9]�)¢(3…*¼K!ˆ67cͰêÔíØÁ‹ÕÕ”ÆÇC¯ô&|[æ ä`Q©È¸ø:O?ŠZÛŽR]½àÞ™fs3›ücŽÿæ·X™ )�ÉÀ,å$�r©Ê4Ð0ó»=ó¹eY¬{á°¬ øó‹‡îzu…”’ÆdÍþýô‡9�3�qê¢q£³þnrr3÷Ò÷±GFiÚº• ¥å’Ï;+²”dé[E‹á>oõ¼ÅÉ'ÈÄ37ô\¶jóÛ�ß)Fø~Ë÷ok�0w�º¸ç[¥Ép’Ô?=ôSŽ9Gqφ=Ë…%šÀŸò±K6wÜwI%9o—›Ë­3‹9³^–øüè›è#ÒÁésp‡ÃëUj庽a�ÿkÿ_:Ó�tÔvܶóh ^à ‚€h4Z ÓNNNVB1ÖÌBt©A,¥ä±Ç£¿¿ŸýèG‹E …555Œ��ñƒüß÷Y±bÉd’\.ÇöíÛY¾|y… ·Üã°¹¹Û¶Y½z5étš‘‘t]gdd„ææfr¹Š¢àyñxœ±±±Ê~kÖ¬©PI\�÷oîþe7ÿíÖ;Q*�7=ÎOz~ /�4’4 4Døt4, áoWA¡3ÙÉq†ŸõþŒmÕÛX_‰6g:výüÿUÓçö¡ëÆù­sNœP’üqû±5³EQÏtÌ‚屋¢»ã¢sŠ˜$’S…SüÏ“ÿ‹s¥Þ¯ùÕ9¶'Tž·îZ0T¢Q2?zœüŠ ZV­B’”ZÃrs5ŠP¯HISÁ¢š†H¥BoÃLJÆ­¤¬¥”èuuÜûŸþ~.WÜòò_ÂÇçTñ“Þ蹮˩c‡ ³‘Ödò’´;Y7‹å[((ß ½ìâò‹³¿`›±�æLó @ƒÎ ¿îý5X°²uåmÅí'i~Å!o瑚¤Ïëc`r€B®@ ÎçèŸêÇw|tOGqìÀfÿØ~¬õjRE‹k¥ÚÇó)–Š8�Ò ÍyóåãͦfYŒu¥| Û·É;yâãäöæˆo�£¥4ü¢�Ùh”àõ{äJ9ƧÇñ2Þ7:%b .²èºNww7---•>£†a\  .Å2žJ¥H&“ ìLæB«µ¹¹¹ò½öööÊþëÖ­«ünYV¥kCC詪ªBA"‘¸àR©Tåóù&ÇB&Ñ켌ÙUОçÁ‚À7A,,ž¨}‚ß�þŽgC"ç»2wñ»‘ßÑ=ÝÍòÄòë:~{¢�úh=ûG÷s`â­©VZÕVLÝD*ÿëÄÿâóáÏQ¢ þ°�´%ÂÈ@†Iwô&�s¹sücþùï÷þw2dœðœž IDAT³ÈŽg·m»Ñóì¿ÚóSöœÙMôŽ(A1 x¬ˆô$j\E«×pz„"�®D5¡¢Õh¼Ñÿ�u�Ô:µ$Í$E·ˆë»4ÄÐ|­0Ë2æ�ñÿú{öÙûà“™�ø/Ûÿ [“[+´'W°H.2Ìnµ<ÀÊ1 ÈdÎ{[/3çðÑø‡üí¾¿…Â.#³4¶€û ­ÆòK‚�&³‰¦B–nÝöúX*’¦>`y°œöLû ;O@À�âN �bgõNªâU·àóŸQw”¢Wä˜}ŒB®Àˆ7žñ=ä§òÜ+îeyj9MfµÉZ¾ýZ•†ªª¨ŠŠ‚ 1Áß¼ó7œ’§P4wØE¯ÑQëU¼i|ðƒ°°¢¼F\®c±½€³‹;ð!(˜ËM¤/ÑRz­Ž7î�£Á@oÖ ò¶m/¸íâü–H*•¢­­­ÒCt6ÏÓ\ÏËåÀÕ¥ö›Ï2š½x–—R^’“\ôÙ%¯Àè"ÊÇÂáÏ pß÷o»‰R©g�¿†÷†Þã¡ú‡P„Â}5÷ñÓÞŸ’6ÓdŒë 5EÔÛë·SòJŒFéžîfÊžb*?ÅW_¡˜ V›…Sp]Ìe&þ”�7íØzµN©¿„c;ؾM ‚ Æáå*1gWx–�GÖÉb¶™”N—0 â[âØÝ6f³‰’Pˆ®Œâ޹  t¶„Ùj¢DJ}%>û˜a{˜T<ÅÑà(þÀ}Ö}ÔêµTŪ¨µj‰ªQLßäÔô)öõí#¾!NP Áiþ`ž“S'éŠwU¨r.·Ìmâ.H ýu¿Ë=ëˉ‡Ç�јÕf¼Sކª¢ëÜÞsŸ�c÷ønRfŠ®]Ýk·Û�‹K ·½>>V:†˜t5wÝ0oœ­Ú¼?ö>VÖâ™Æg¾övr‹:6‘ Œ»ãì›Ü‡Y0Iè ê#õ¸y—“¹“´ÑÆ_Õÿ-«ZHDèÚåõz•¨¢.SÇÁ=ÁéKœ³JR!ÈdVf0}“0k÷â*ò«qD\41iJ4qäGP«UJ'JC Ì0T­Dœ>Y”$Ò Ï¸­é`–àµ<4M#™LÎö¼Ú\º+Y;_LÁ'ßàHçšCŽ‹h.^Ä…§…�ÜÍf3ÕùjêÍúÛ._bm|-ùNeOÑ‘ìÀR,v6ïäíÁ·ÙÙ¸C½þÞž¦fÒœl¦9Ùz³Ô€ÃÓ‡9;qéHœ)EWPL…@ Т&ÌUQ- !/Ì—<½(‰7ìY ~ÑÇZaág}TT„!PLéI¬A>€(àöØ6[ñ©t _úLç¦AR©RµK6®ãRòK xaßY!Pt¿à£D<ß «ëåÂ9宕ïïf€Á+-&!`øC!°dL¢×è¡'6¥Rê)� š¯q¤ç'í“<Ùñ$ ã<åK±T¤VÔ¢‰Û[ÅŸ´Or|ð8O·<}êoGœ^z�æ:Z¾yù`>>%­ÄÉñ“œõÏ2<1L»ÖÎòørv&wÔÉáÀÄ:�Nh{€d4yU`Ú”&ÿ®óßÑ-ºãäHG”�·<Žh"¸!Å �‡ºÔyvý³L$'pw~Ì÷P *�ó8Q¢·uû¾%x­Xè´<š»@ô9}ü‡ßý “”¸‚?壯ÕК)Êкš m%.Š©/´ú…æuHG.D -lø~¹íJDADA>@º¡1‰ñRü¥¯…wë뎂ä§=?%®ÅiŒ6’ÒRt¥»øÍÐox¢é‰E͵B *M±&Ü~·ï¼¢ ì€âáb�„?Ww¬Æ ,¤ra!ÄÕ(Ç@,³–ñIß'¸ýn%Á;$% Ï錄石=•I‘Ñ3¦††P©I×\pŽ˜už‚¨X,Bv·z ‰ÌI”@A!ºR÷Ž[ÅÛw­^Á˽MjØ_ØX-¼眃µÖ¢x¬ˆPHhŠ5ñDûäEžW»_åÞ¦{éHt ¸žËºØ:âV|áÞýo˜LhìÚÃκ�š±èÇdÀaç0Gû�òbí‹ß¨�oI–˜–ÓŸ:N¯Û‹•·X[Íw¢ß!Ñž@Q²A–Ϧ>£¨Ÿ­ñ­ÜÝt7†~mÏ1P>ø˜S=§ì™õG5©äþÇÐÿ áî–YËuý¼* ’}SûøçÝÿL�BxÝùà¼.a«C¡ ‚\À©ÖSl7¶ßViMKp‘àìdÕÅ:n¹ºjÂ� ?™'qgoÊC]¡¢&Td ñ§ý�³èP!éGÒää‰nˆ’?öxE IQ; Ò¡Ô[ A)¨Æ  Õiø“> AôŽ(…CÔ¤ŠŸ÷‘ŽDèáöìGY‚b@ü®8Þ”.”úKøã>'&N�Ífo;  O×?Íë¯óRËKXŠEG¼ƒÉÒ$‡'³)³iÑÏù'ëÿ„he ?@2–ÜÞ,‘œ<Ë�™;i�¶/¸p“Ñijkžå�¯Þ�¤„öÚv:S�€ ÷šÝN"¥DAᮚ»x·î]&ÏLV¨ü—ù²K‡ÎÆN–ëË1M“šD ?Jÿˆ? ¯ØÇ½5÷âØi5}Û‚¿œŸcWÿ.žªzŠd4¹èÇ/*E>ù-¯ñ|Óó·¼Nla3j�òEö FíQêƒzV%VqWò."uç)qlis¨pˆÓ£§ÙÛÆwZ¾3o»ºò´€¢,b;6žë�%®0P`¼8N¡P`º4Í›_½ItCû´®SžÄh0šàôîӜȟ Õh õé×\¬Uö¬ïßGAˆ®�†Å.}Ñj ×éH7Ì ÌíÏq"w‚¢(.…€—ä*€Û¢ŽÚPÙ«q•ìGYR�¤B�‹î�‹µÌ"²*‚7íá�zD:"ŀȪ2�MPê)a-³*žÁèÚhÅ:Ë}šÃZcá�zÄ:cxãjF…ÔthõDWGñ²^X"„ɳ!°ôG}Ôd¸?䳋ömC3Wz‚Gåõ¾×y¡ùtEçÎê;yµïU2F†¶XÛ¢ž/¥§øãµÌ�ˆ©1||üà<ýB9ÏS c‚!eèÅzµÀòÏ-Á–0†Œ‹'&oçô) T—ªi²š¨�Ô!‚)M2�P i_Œ&éJÔ¨zsøTÒ;äÒö9¿gÏ+_†úS�¨ÅéIÌ6“ `µ[x“^˜fc)aT"�ê¬%¸$ ªe̦ä0„ Ø'mâ÷Ä).`µ[!¸a%¦ÓëàOùˆ@„E+M„2šû?ä8òÁ sżqY’N€ÐÎ9³Å¤øe½ZGÚwÂEµÔ0¹ÿ\ 5¡âOùvþ>éãœs(�,a­µˆ®�RØ_Àò­Û¾}N½QÏúÄzöŽíå¾ÚûPQy¶ñÙ°Sˆ‘!¡'õ|VÄ¢ëÎ.™žžÆ+y•Šë <”³Ze•C &ež5–Ëí g÷�� Äf÷ÍTeX¾b9étúŠè\#Åw;¿ËÁƒ˜Àu\Ü”ü [QÇ”ßEYTUÅ4M¶·o§¦¦†ªªª‹<7+’+¨�ÕóO'þ‰*½êö{.šä7#¿a«²•ÖªÖE¶·%ì8ÎÎÚ�db™[3§THÆÄG§�Ò=ÚM»ÞΖØª[ªÃâ©y.yLã·¿¥Íkãùú牘rjŽ“Ó'É«yO&bGH*I²2Kßd#ö›ôMl®ÙLs´™d<‰¦ÌTÿÎ ‹N+Ó¼Öûý…€ „¤/}´mÆçp¾kÖ×Í×YN­P¥Š{ÎE¯ÕQt…Â�Öj wÈEú7ë’ÿ<�Ö ¡Ø ª¢ÞÖzf ^¥”•òìEr^+þz ”4ëÍüûíÿž_ôü‚`4LüglæÅéŒB¼&RE‘eðB!„¡A«n&tá*†ì¬“• X'@³4ÈyÂdUÀ8¨R xg$º>ŠšTñ†<Ìõ!ù©ÛãÒZÓJ§è¼­fËÒ™éäw£¿ãøäqÖ¦×b*&×<Ìký¯ñÃÖb(‹›�dš&Ë–-«Œ·ùúOÏ×6éZ=€ó9s=q³{´ªªzÍ e,cÛ¶m‹ElÛ¾ ˆeIÏœ€ågmF…oôRs-¢FhL7rªïŒ}À¶Ì¶ëê^sË$ï¾O“ÓĺÆu‹:FŠ¢Èžìç– ùJ$QàØä1æÒè4rGêîi¹]Õ/ù,<<å±ox÷Õ݇QùtêSާÕo¥5ÑJ‹ÞÂêôjF�Qþ0ö´i�çZž£)ÖDÄ\˜Q— ÁC«âèûG±};ySAÈ01-ÙØ°‘«c^½òu‰�‚;Ówò^æ=†WÚÀ•ö•.ÚOôî^s7)=µT²$³&”çUþ_j1¾ÞA]¶VMâíÞ·q]÷Â\Á…�䜟k¸Z]™¿Äý{0ÌaA?ðQ4UWÑuýö�îÏÜÏÏÏýœºhUFµF-Û«¶³{l7;jvÜ3Ÿå};ˆªªÄãqâñø’‚¹0¿Z0zê4Öúgõ"?;÷3v6î\tïô× €öæö67o^´ùå)Ç Çxåì+|?ù}6´n¸¥Œ‡Á`�C#‡˜.NÓëâG™�D/û=_ñ÷ÆyõÜ«™:Âk gFϰ,¾ŒÍÖæ0ïOÕ(EŽæŽrxä0kÄž¬z’DM⪟�;Wí¤Yif,?FCM~à‡})H( ª•ê›þl×F×òŸwüç0µbn$o&�¦¸;ksGÕTeªæÍ�\€ßRñ}ÿ‚ÿמï{åm¥ Ä¿~õ¯ Œ YÁ/†Ej2 ãjÕ¥Þn¯KdC˜h6šŽB¢Ë™Š`ÐÒèàžsº‹: AP ÃÀåjRH´ ¡¼‰0Ù·¼]¯ÓCÂç!a ’;’”ºK¡+}Záô‘ÓtWus�¸ç[1 Åà™ægxµïU^jz‰¨eEb=vG¦ŽpGúޥɲ$×ﵸ†S"™ÊNaÖ„ÔBI#ɯ‡~Í#��P­V#ŸÃÿ ý£ý<Ùôä¢AÉ;Cïð¿þ7[÷·l¨¿5ÀŸ@0æ�q¢t‚îÑnE#÷d¦ EU.ù[ ‹@Ž�sbäƒV©ô-Wb…0è5*ЛôJÖ²°u�´eå<~Î'Þ¯ìã¸XÞ¸‡×ï…%®a;6jDE�©p¤Sy&߆^\�ó`õƒ¼3ôÏ5?ö®ùo¾I�YG]¤niÂ,ÉÍp—¡¹Z¥"»Îªã¹ÆçxsàM6¦6²&±æu;ƒ¥A>íÿ”çšž[´ž»¶°ùhò#Ì¢É?¶þ# 醛®³|ásÖ9ËçŸ# ‚M±MlnÜŒ¥Y—ŒàتM¿ÝϾ©}ù€¨ˆ²odÛÍíüÉ�B,»ð|é~É'ŸÐe„… ¦¾x麮ÓÙÙYiqê8ÎEi ³#D %z¿^ãivjËÜçÙ€TÓ4,Ë"�bšæm¿Ž-À«”òà¹Q°Ör+L–u$Ц mIàXmÖÿcï=ƒ¤:Ó<ßßqé}e–ÉòÞ{!„„$@Â#$µº{fvæölìÆ�Ø™��;³½±»Ÿ6fcc¢#v?MÏÞ��é³£nI-H€¼a¶(((›•åÒgw?dUBAáŠÂêüˆ ªÒœ†Ô¡fÞDíR1†Šé]›yÈw䱕ÙJùÿI@”Eô¼NáR¹B.Œ `˜ŽFGÑ*(“ýŠ6°7Ø12FÑ/ðš(d}@GíQ]Å× æ�Ïo«Î]G‚ßľaEù $AbUdo_y›ŸÕý §ä´:�ŃE·à.E8$›£›ù°ëC²Z–ùÁù�ÅWéÓûø°ëC^¯x§}búÒ�8Ä;—ßáiùi¦×L¨.+&&I’œÉœá‡ØL—§³*° Ø?f‚‰‰*©œOžçtú4zJg²k2/8_ÀYídû…í¼Y÷&“C“o°”vÝìíÙK£Ñȶòm¸ì®û"pDQÄívãv»Ë*?&ÿcKÞí*mxåp+‡ü‰hô‚!°0¸�“§ORh/S­˜€Hñw¹(ÄÐ!ñE¢”xd`8wp^È_}ßHò^­ø{¡­@ár¡d5(='€–ÕÈ�Ì�z\ð 8æ9дÒgj}Åú‰Š]!j�¢(Ê�Î�¦k&»†vq1}‘Fw#^ÙËÆš�|Òý ë£ë‘Éê8Œ„”Àƒç†”<Ѝ°¾j=ûbû8ÔwˆÅe‹o¨ìó(‘“rììÜÉ«þW'$ן‰Is®™cÝÇJµ|ÖX¥¡ÑK/‡c‡I§Ò,ð-à£8¦…ÓĤ`hɵp!{�Ä`‚Y®Y¬ö¬ÆWæC”D.f.²ýÜv^õ¿Je rÔûóä94xˆØ`Œµekè÷þ±sYð BTU¡7bÞžè-`ÑÙTµ‰º—êHªÉ«9ÙÆ?úÝæË�ñØÊ›§ ˜|5Ü¥ŽKÔ:j™ïš�Ûíþѵ Qy¹êe~×ñ;¼’—°#L¹RN�«†ƒñƒ,�,·:�Å#žŠã6Ç(³ºb5_Ç¿æPüKÂKI¨š*»:v±Ò¾’ª`Õ½‹Ir8€šTÙZ¹—Ãõàçòbž–t Ç�(X\J8¾Á ) �³tºiîk¦?×Ï,e+ý+K•<LÑä|á<ͱfÞ¬xsTµ€n³›=í{˜¯ÌgyÝòE– KÞ¼^/ÝÝÝô÷÷—„à½nÿÞì½#�W•T÷.ïÃ`6"ˆ+ÍJê"uTUUár¹~œI�y¹üevuïâõš×±‰6òiϧ´&Z™ä›du ‹B:“&ÀÍ�×%AâÙð³|ÿŠïâß±4¼ô‘�õ~Ätq: e ÷|¼s€íÛY,-ffÍÌ.‚ÌNeOÑ>ØN#�¼x¯óÆh[]ÐéÉõp,uŒT.E 5<åŠ`YI–n¸N’ÈÆ³¬«^7*©s�GÓGéèí`}x=AOÐê–¼'K�(2eÊÒé4™L†|>?Ê™t¢|¯=ÖX[ÍéW1ýµ9àEÁï÷ãóùp¹\?j“ÈbEù övï啪W‘å‘åüîÊï»ÂødŸÕ‰,î;ºªã·ûo=– "φŸå³ÞÏ8Ú”…¡…�Ĺ›¢ÉÁÔA"…S£SïùxdzÇ9;ÁúÐzÊ<8 ‰=zâȧó,p/`QdÑ A¦`ÒGgg¸Ð�©†ùžùD*"£DݵÌÆ?¤2_É 5/ KW§òŒ�á½ö÷˜%ÌbcíÆQÏYXXðE ×ëÅëõŽ)ú&:ä~V™ñx¯‰€Ÿ4êõ\Q®pzè4³³pŠN6E7ñQ÷Gl©Ú‚]²[Éâ¾pÝ>}…(ˆ<yž]]»ð§ý4¹›ò€-Z éÞ4/V¿xOUòäÙ?´uPeKÅ–ºå«™­j+ß~O0ä)ÿSDj#£‚2LLòbžs‰sJ¢¢PÁlßl–D—`“m·OóFž÷ºÞc¾m>S£SG]§KÚ%¾êüŠ—ü/Qî+·Æe KÞOdu0‹ëYYÁ»�ïPT»«ñ+~æùæñuük^¬xñ‘v¼·xüɸ2„œwVN$ÖV­eW×.¼¢÷¡¦.:›<Ë™øÖÕ¬»§ä»ƒ òþ•÷Y$/bVí¬¶å›7òœÉœáÈà&›“Y_¶_ùh«¿ŠJ¯Ñˉ¾t¥»Xà\À›�7q»Üw4.(ðnû»¬ð® .xµÖ³.è4'šiékaSd>·µÛ`a @ ‹‡Â+寓D;¶á”œÌðÍ _ë§y ™ÙA+I´Åýã\âωÏÝñë%AbUÕ*Þm—ÍU›ñȾ"KK¾…£½GÙÝ4~ñ'‰8?Áú²·å›3œJ�âLïf(3x#üƨ` QP8“;Ã¥¾K� ó‚óX\uWßUu¨loÛÎJßJªÕ¥ÇsfŽ=ý{¤l¬ÙøDW®°° …Å#�[vóJÕ+¼ßñ>Ûj¶¡ˆ OŸâ�öw¨pVqD¬‹d1ᘘ\3pw9󜂓—#/óyìsÖV­} ©‹º…nöwígSŦqçú˾øgÊÉkU¯MXÎÀ[Ñoös,yŒø`œ¹Ž¹l«Ú†Ãvµš†i3iM¶r‹�q(Ž»¦Y3ËöÖí¬t¯¤Ê5*:£gx·û]Ë‹™V=ÍŠòµ° …Å£@Äa®o.û²"²Y�Ù\»™·/¿Í¶ªm¸·u‘,&œ|&�£âîËzEj=µ<΂à‚r®{†=-{X^?î\13ÆîöÝ,s,crõäû*‚LLbZŒýƒûQ“*KýKy®ú¹R …‰IÂHp"u‚óƒç™"Má9ßsB�qû4¦å4Û/lgUp¾ŠÒã)[Ší­ÛYåYE•¿ÊrG²° …Å£ÄÌàLöõîãÌদãÀÁêðjöÄö”ÊÇYXL¢]Ä�cÜù8çúæ²+¶‹êlõ}÷Li)Þm{—õeãKSb'’'hîmfCdÃ}Mu¢ :]¹.ö÷ïÇ–³ñTð)ÊëËK¢NG§]mçXò¹Á‹|‹XRuû€ŽÛ‘Sr¼ßú>/_"⻺kÐn¶ói˧¼|•°7l5| KZXŠDy¶œm5Û°)¶ûrž¡Àí Çú�áM{y¡ìB¡’•1e¤hQ[8;x–²\ËƒË 7†'Ä—ÖÒ¼×ö/_%þÎæÏò}×÷lŒl$àX�Þ€�*6ÑÆËÑ—ùý•ß󓚟à�4yšøŸmÿ“pO˜£/ZÛ7B<Ç©9¹òGö0-8�3ƒg˜œ5áç˜!Ãïc¿g¥s´?Û�rQ½È·ÝßòŒëêªëîË–o–,g3gih¦Þ¬gMh Þp1q³)šôê½î?L<g®}.‚pÙ'.ÕLZOóNç;¼|…rßUKìÉÌINÅN±©b§Çjð–´°xÔñ‹~^({�/c_²¦j NÓÉ_7þ5{ño™œC¹«ÜºH÷L.—#hïÙµ`¦g&¿¿ò{&û&OhîÊ‚X`gÏNž’ž¢¡¬á®>š q4U¬n±.²î¾X¿Òfš?paà³l³Ø\¶¹Ñ«)çÏqdè!-Ä<ß<Ê«Ëoš¬yÜç`¤ùmçoY\K…÷ªÏßÑÔQ.Å/±¾rýC)ega @ ‹qRï©§£ÐÁ÷ñïY^BÄá/&ý{b{x½æudÑêŽ÷†¡wœðVˆ¦ÈâàbNôŸ`qdñÄœ›b°'¶‡Ùúl¦TL¹+ñ—“|på¦0…õµë'´º…‰IJLq¸ï0íí,ö,fat!Å�)˜ COçÜÀ9æØç°1°qÌ2nA–,ït¿Ã‹î©ôV–ÿ>ñ=]ý]¼Rõʉp¶° ……ų,¸Œ»>¤+ÛE•³Š°#ÌÒÈR>éú„W¢ÅòqãEwèTx*&DœT»«9–>Æ\}.6é}ìlðeß—Ôæj™Q9ãŽÏÏ ZÕV¾lÿ’W¯Lh´kÁ,0ć‡ÓßßÏÓþ§y¦îYA4Ú mK#?˜g‘OU?uß| TIåãÞ�yJzª”äÙÄäpâ0==¬‰®•fÆÂ€�¢ ²ºj5ÿ§íÿ°¡rG„&gÝöîRù8 ‹ñÒšo¥^ªŸ ÉAf’kmÉ6¦¦Œû8†iðeìK¹s*çܱ€Ë’eO|Δ“·ªÞš0ËWNÌñEÏ|—úŽº|«+WSÕP…$J$ŒÇsǹ0t�p><¡A·ÂM¾M|Kc¾‘i•ÓŠ¾†˜M¥{ ›5Uk°Û¬2’–´°x¬q N^jx‰¿:ùWüͤ¿¡ÜUÎÓ‘§y§ý�Rù8 ‹»E@àbßEfJ3'옓\“ø4ö)SŸ4Lƒ½±½”›åÌ«˜wGBJ�Zs­|Õó+œ+h¨m˜�@S|Áìè IDAT19?tžý±ýt¥»XXϬêY(…˜ãHßSƒÌ±ÏaCpÃÛj51ù¢÷ Üy7s«æ–®ÑñÜq.Ç/óJÕ+–ø³° …Å“B¥PɆàvtíà�ÿY”YW±Ž�];ÙèØˆS²ü|,îžl2‹«râÜR±Fm²�ÄkóÞµøû¸÷c*�JæUÞ™øSM•/¾$?˜gSx~—ÿž¿ƒ&kœIœáHÿ&1‰M‘M8kœ¤¤§Os¬÷zE)¨ãA–R31ù&ù bVdqõâÒ5:­�æ|×yÖE×�;¥�……%-,ÅŽgÊlˆn`ûåíœ<ÇÌÐLܲ›g#ϲ£s[«·ZA!w'&dÉ”ðy}xP˜˜F,»+h˜»ã»‹ÂêN,ôh=ì�íež8�éÕÓï)ÐÃÄ$+d9‘:Á‰Þ,°-൲×p:�Ä´ÿíÒ#SȰѹ‘­[ñ8=%Ó9ý}¼\ýréó/r‘WN°¾j½ðaa @ ‹'²ó 2ê6ðO—ÿ‰ weö2¢Ž(Ó=Ó92p„%eK¬J!wÌ 1H^ËOx @P r¼pœ&šîèõº©³;¾›rµœ• n»}«™§ÔS4w4óRhtÅ‹ñ0¤q$q„¶¡6–8—ðÑ?@´‹t:ø!þùDžgÄgX]DE â¡Ý¯®B?tüÀÆèÆRpI\Žs°å «6Z©^,,hañ$cll‹ncG϶TmÁ!9˜šÃÎÎ�´&[iò6YÉâŽHf’Øò¶ ·d…!LÓÄ0Œ;s»c»‰QæUÌ»íëd_Ï>¢Z”m5ÛÆ�SO�úÌ>öd 5ÀRÏRVT¯ +e9ž?Î¥®KD ž >CYcÙCO¼ÓbìíÚËÖèÖÒoO¡‡=÷°¹j³%þ,,hañcÀ£xXZÆ®®]l®ÙŒ` ¼\ñ2ïw¿OØÆoó[ÉⶪADŽL¸¸‘t‰x!Žj¨ØEû-Å߇Ý2IœÄ¬ŠY·g´3n?Ì‹þ‰VDÇuÞ¦`rE»Â÷½ß#¦EùQYWI\ˆóÉÀ'¤R)æ8æ°>´§roR&ŠŒ”awÇn^ ¼‚Û^L2ݧ÷ñQ×GlŠlÂã°*|XXÐÂâGC£«‘!uˆÃñÃ,/Æ&ÚXS¾†wÛßåçu?Ç&Ú¬‹dqK4›FÔEïCý^{Q´ÝJüíèÚÁLe&SÃSo)æÍA¾ˆ}A°äõèëãÚ²ÖÑiɵppà Q5ÊóeÏã�ú9=pš½Ý{‰êQù® OhÒè{¾Gh|Òó Ï;ž§Ü_¬þ“ÓìèØÁ˾— yBVC¶° …Å��Ù¾Ù|Ðùé jݵå k*ÖðyÏ笩\cÕ ¶¸)çóç H�ûbåò:¼æØPWtvuïbŽm“Óoz C08[8ËÁŽƒ¬ö­¦º¼úîÚ´ª¡r&}†˜nNgkåVD§ÈñÄqšÏ73ß>Ÿ×ƒ¯ãvº¹þ¢£óQü#f0ƒº²b¢gC1ø¨ó#^p¼@4}äÛ™išttt iªªbšféç±é+ÃíB$I"  -haañ;£ ³.ºŽßwþžW•WñÙ|Ô8k¸”¾Ä±�c,-°.’ÅMéˆwP«ÔÞ—cK’„®ëp�‹^BL°»}7Kå¥4„n:áöëý|3ô þ´Ÿ·¢oÝ]^;Ò¤9›=ËÉøIfŠ3ÙV¶�y€}‰}¨�*‹|‹XT½EQÙûs2u_ÆÇäê¢HÖL�.À|y>õeõ�Eëéé¡©©‰@ €¦i��¼vQ ‚ pìØ1ìv;.×�ÏïÒ€�vÁÎʲ•ìêÞŶÚmH‚Äò²åìîÞMe¦’*W•u‘,ÆDËjøÊ|÷åØy=�híÓ×­w³§}/º_¤:8vòr]Ò92t„‹ýYî]NMuÍ[(bZŒc©cô%û˜gŸÇ«å¯ÒªµòIü¢b”�„ÊB�¼uü\ö—û.³¶z-’$aŸ }F­QKSÙãè•L&q8 TUÅ0Œ`2™$“É @’$‚Á ½½½8�¹\Ã0Jï÷x<ôõõ•¬r‡ƒL&C("™L’N§ ƒØí£º®“H$ƒ¤ÓiœNç-}O¯€¢(âr¹èëë³ ……ÅçÊQÅÂÐB¾Š}ÅÊŠ•H‚ÄÊò•üöÊoùIíOpIOþ@eš&ýýý$ 4MÃ0ŒÒã⳯�(F>óv"ãú÷M”µBœN'µµ7·îå¥ûŒ\.ÇÐÐÏ?ÿ¦n°ŽÅ�Å„kÃH’ôØÜÓŒžáÃ˲®lnG1â÷\ú{/òJÍ+´êÈD-Ž Ã ««‹ãÇ�£( ‡ƒ¥K—ÒÙÙÉ©S§Ðu�ÆÆFöíÛ‡ÛíFQDQdþüù4773{ölt]§¿¿ŸC‡‰D8yò$UUUÔÖÖòý÷ß—¬…º®£ë:õõõìܹ“;v Š"^¯—Ó§O#I_|ñ3fÌÀívc³Ù8þ<™LÃ0¨¯¯çÀ�lܸqTºöÃ0JÖÌÇ©mYÐâ‘$‘H044TZ->é÷í&qY–ñù|„Bwá' °¦b ow¾M@qF(·—Sïªç`ßAž‰<óD_Ç|>O  P(”ÚЭ|�TU-Y($IBQ”Ò½Èçó£þÎår8�£+,Œ|ŽÍfCÓ´^/Ë2’$•SUQE‘|>€Ãáó¼$IB’$LÓDUUE¡P(`³ÙFµ™ë¿Ûˆ�’išÈ²L2™D×õ1')Ó0©*î‹ ÈɹbºfÒÛÝ˦êM¥äÅ%Ë�`p±p‘ý±ý,’±²nåm'T“ šSÍœŠŸb¶m6ËüËhWÛù¼çsæ{æóVÙ[8ÎÇÎ:£Š*;{wò¬ûYʼet›Ýè=Àk¯�;çᣠMÓ¤¶¶–\.‡Ýn§··EQ˜5kóçÏgpp�­[·ÒÙÙ‰išƒA†††hjjÂ4MÚÚÚ°ÛíüùŸÿ9†a ===´··³bÅ ¼^/ýýýƒAfÏžM__üÇŒ®ëtwwSQQÁ¢E‹¨¬¬ä/ÿò/1 ƒ§žz Q™2e ¦iÒÓÓC.—cÙ²e¥Ï¸¾ �Œ)×ÏS–´°çÄ-Ë2K–,)mÝM¤¼Ùdù( @A8þ<©T �çÎs|‰‚ÈÚòµ|ÐùoÖ¾‰]´³0¸�½ñ½´$Z˜ì›üĶ£AÖ××Ç•+WP¥è;5¼ìp8Èf³†Ayy9ÿ÷Ï‹/¾Hgg'===üô§?E–e.\¸À�X° ¸ÕV^^Îo~óþìÏþlT[z÷ÝwI$Ô××#Š"3fÌ```§ÓÉ¡C‡p8Ô××ãp8P…ÎÎNªªª¨¬¬ä׿þ5ù|ž×_�t:MCC]]]Øl6ÚÛÛq»ÝTWWãõzùàƒزe Ÿþ9sçÎe``»ÝŽ¢(ÔÔÔÜ0I]ûw.—£P(Ü ^†CÔ;êï‹L ¢é;ã;‰ªÑ1-WƒÒ ûºöÌÙÙ‚Ûé¾å1uQ§�>v$•KÑàh ÑÛÈ¥Ì%"ææ®?¶“ï¾cRaõUõ%kàž®=Ekàm®Ï£,GÚ¥¦i̘1MÓJã~$A–åR›½vÑ[VVV:ÆÂ… …B£¶¿ËËËG}V8\t-¨©©¡ºúª�éÈï׿þzª««q:�¥…ÖXßaä÷£ø³ ÅÄ[ r9¼^oÉIøvÖ›D"�(Šd³Yt]§²²rÌUÚX«ÿX,†ªª”——£(Ê]uâL&ƒÍfC–e à �—AÈd2ôõõáñx7x©TŠL&ƒÃáÀçó�)þAÀn·‹ÅîJ”«*W±»k7ë£ë‘‰gCÏòö•· 9B„lOfÞ0Ã0Ðu�¡¡!zzzp»Ýäóyòù<.— —Ëʼn'PU•úúzÒé4D"œN'ÍÍÍhšÆ¡C‡ðx<|ûí·ÔÕÕÑÜÜ\j—--- fÍš…ªª<óÌ3ìÛ·�p8L[[åååô÷÷ÓÝÝM]]1mÇ7ß|ÃÑ£GÙ¼y3º®—¶Ž¦N�ÊÑ£G9}ú4‹-¢³³“sçÎñì³Ï²}ûv~ùË_–|�r¹===œ9s†¡¡!ZZZ¨¨¨àµ×^Ãf³Ýt«J×õ1 €@sW39r_îE[¡�ï/~Ï¿˜ñ/¨�Ô�š´óRžýýû‰ÄYZI8Fon©K‰)Î'Îsvè,¢*â³ùÐE�ö\;s�sY^±§íñ¯{,sŒÌP†§kžFòäy·ç]^p¿P²>®Ð4MÊË˱ÛícîjŒei»þïH$rƒðº•…w<Öß[�Û×Ï+�[*KZ<’Œøl$ Ž9‚ÇãAQ‰’$á÷ûéííE’$"‘~ø!sçÎ%ðá‡ò‹_ü·ÛMKK ���Ìœ9“ææf¼^/‹/Õyÿáþ�P(ÄŒ3èëëcΜ9\¹r›ÍF<§±±‘l6K__Š¢0yòd:;;¸pá>Ÿ·Û�(Š ÒÔÔÄàà K—.åôéÓ¼ÿþû¬[·UUÉd2% Pé;z<Z[[™9s&3gÎÓz#BéwRNëz¢ö(UŽ*~ø�…¡…ØE;[«·òA÷l©Ü‚S~ò Æ�XŽE¡²²’¾¾>êëëiooG’$&MšD¡P`úôé$I¢½½¯×ˤI“¡´Àèîî&—Ë1kÖ,ªªª0 ƒ†††Ò–Ö´iÓèèè`Íš5¤R)>ÿüsyúé§ ‡Ã$“I¨©©!�âñx�$‰•+W2sæLâñ8n·›ÚÚZl6õõõTTT°mÛ6úûû©ªª¢©©‰ãÇ�3eÊB¡Ñh”h4Z²¨]»UuýÂgD�µ Ê¥r”ù&XXp1w‘ß]øÿºá_ÓXÖXzJ4N¦NÒÜßÌRçRVÔ®¸iÂeS4é,trhàét ‰¼�GÕTjmµ, .!à ÜŸÖ�£‡3=gØÝRt�E¾øšyÂ<ªÕ�í÷4¹(\Ä‘p`J&šOc ;€IQ8ÙE; öDA¼¥h»VtÝ�èK =èÀ,KZXܦÆA*•âôéÓ,[¶Œ¡¡!Ο?O]]�‡|>ÏŽ;X·n‰D‚ŽŽDQ¤¡¡�X,ÆéÓ§¹pá6›�T*E àÈ‘#,X°€£G�âñx˜2e š¦1uj±êÀ™3gÐ4�––¶nÝÊ7ß|C{{;v»¿ßO¡P@Ó4vîÜÉüùóq¹\444°cÇþäOþ„_ÿú× ±víZLÓDÓ4ªªªPU•K—.�ÇK~\­­­¸Ýn:Ć "“É ëú¨ÕïµpÄÿK×õ»€KÂKØÙ½“²dõÞz<²‡§ƒOóIÏ'lˆn( ºOšÔu�ÁÁAB¡ªª�Ç ƒTWW—RB‚ÀÔ©Shjj59¬X±¢8)÷ôàóùp8¥×\»½8gΜQL*•â¹çž£±±·Û}ƒëÁµ“Êœ9s¨¨¨ ¼¼A˜ã\ìÏzž¥!ÚP¼/\6.óe÷—Ì0g°µ|+Nû� “‚T y°™C�r ýô3SšÉÜÀ\ÊÝå(²òDMÐú^þ�7*ß(ÕøÝß¿{ÂÎìèìÇö»f„ ¿:ø+>:÷æáë„Øp7 ÙBüÕ³Ås‘çnãÆúÞw{-ÆsíîD þX­–´¸/p䧺ºšÁÁA²Ù,uuud³Yb±~¿Ÿ?ýÓ?Åív3mÚ4òù<‰D‚I“&QVVÆË/¿LGGñxœÚÚZÚÛÛEÃ0˜?~és6oÞLCCÉdY–)++cúôéƒAž}öY¼^o) `Ä‘>�’Ïç‰D" ²aÃÜn7o¼ñ¢(ÒÙÙ‰Ûí¦¦¦I’p»Ýøý~víÚEcc#ñxœçž{UU™6m‘H¤äßu½uodõ)Šâ=; ¦Àªð*Þm—ל¯á’]4ºIè ŽôaQÙ"žœ‰tD ‚€Ï罹¦fLv;+Ã7ïûœyâ< ¿Á,Û,� £ÐÁþÞý”iel‰l3XE¥Ÿ~Çsdàèàü„å0óüó¨óÖ�)ŸTQå£î�X\‹ÇéÚŒ6ÚûÛY]ÿè¦{¡ÀÕ¼~8!A‚D.A®�C-¨tä;Ø~z;ŽZZ¿†è)\)€ öÉvdŸLìhŒ/º¾`EÙŠ;êc�°ôûý$“Ië YÜ×:æ>÷Üswü¾kÛÕÕÕ%gß`08f´I“&aš&>Ÿï†Éz¬É®úž\ÿ{}ýèLü#å�F&äŸÿüçx½^*++o+€¯#bØ0ŒRTôxqI.^­z•÷:Þã�Ú7°‰6f{g³£s‘t„zwý“шr9ÌžÒmm¸ 첌:MȈxº™ð+^x�$lee×EÚÞÎ"q§>Ic=?–¸7’IÔDâFÁgŽlœ]sÎ7ùŒ‘ÏQz{ɇB¨ee£`Á,POý˜Á!wƒŽÎ©Ü)ÎtŸaux5.Ÿ‹#mGн:;b;P2 /„nôae‘Þ|/-ùŽöåLâ A‚4ؘïŸÏ¤À$<Ï-ttövïe¡¸�JqŒèWûù¶ë[6Tl¸»Ê'÷ S2Iè ²ù,ÝR7—ã—QL…”˜"§æpn‚ñl’ ›`CÖeÈ*ØÊm(aAP;TÌ‚‰Ú®"M“@UW1LÑ-ñ÷8 Àt:ÍÅ‹­+dñ@±ðÜlbƒc:ß­ur¼Û×:߉ôZ ÐÈcªª¢ªê=MÔe¶2––ðuï×¼Xñ"’ ±®jïu½‡_ö°ï†�ÍrôïþŽs¿ù ç cü“�Ýμóohض ñºˆÕ»õ;ºÓ6sý6±ÖßÏ¡¿ý[:wïžÛ¬ N�Šãßÿ{*çÍ+Y”N§NÓ 5 Ëò¸,Ì‚ 'Χ]ŸR­W³­aŸô~Âù+ç –—)‹Œ²b¥Ä—’—8š<Ê�¡èªÎ"yoÞb²o2gàÇ‘WM(&v¶çìL«™† dÅ,Û{¶³Î·®h |ˆdÄ =ÙÎåÏ‘J "R&••¢,÷-Ç);QY’‹»Ü؆ìù¢€Uã*fÖ,*µØ(• ¥( %¡˜+Òâñ€#—#{þ<ˆ"öŠ äpøŽ¶Æò�ºÓÉýf÷ÕW#{áz¡pÓ-»ët£%jøøùþ~¤K—ÈVWãñxîi;hª*1=ƱþcÌÍGVGVóûÎßó³úŸal�m;Ètwsâÿ pJyÓD3MÀ#I é:~QĪm6.äó¸D‘>M£L–1LQ,8»s'µ«W# §Ÿ¸>D·"ÕÒÂÙÝ»EE(˜&úðù¹�¤aà“$l‚€O’è(P�A]G’DJ×Ñ—(2Éá ùÜ9zÀ?u*nw17ß`z�º@ݸğ®è|ÿŠ�ÁV…WñG¸”»ÄÞÞ½tµuñ/§þK*B%a�—ó´$Z8–9Æá¾Ã$ –(KøCßÒäi"àÜ4äIåJî g»Î²±zcÑUƒ}±}¬PVPî+àçcb’–ÒœOžçÈÐ|Yµ®Zæ:æª ¡ ¢$Þô½cQ­T³vòZv7ï.õ {“É-‘»�#q)�ßíçÙòg‹%ÞcTÿy”¬�×ûh÷°Ð´ …Å�N§yÿ¿Q¤zñbVüÇÿˆ-¹«ä^™Qy¡ ƒË;vðͯ~…šËÝò}¢ `ÜN|Rôý2.\Àýg†§lü›OžæýÎ÷‰º¢”;Ê)³•±ºr5Ÿv}ÊKU/=¶A!`˜&+dA OÓP ƒ°¢ ]ªJ¥¢�2 4ÓdŽ$¡š&Š à“e²Œ h MüIÔìýŸ„ÁÔ4L`U €ÄT•¼aP®(ØE‘Ö\Ž ,# ƒšÆ|�‡>UÅ­ë„d§(âENe³Ìp:1MȧR¤Òi\.ª¡òuÏ×ü»úww . ..ñÅ•/Xî\Î3“ž¡5ßÊ®Ž]„r!úóýüñ´?¦"TA�­ÙVNfN²§{ÝÁáõŠ×±)6LLö 죪PÅäªÉ®T�Y=ËùìyN&O¢df{gó¦ÿM\•®{\‚‹ç§>Ï!áŠSµèöÎô" 0Û=›ižiº�ŽþHFà^{>¢(ZÐ’�JǬ’eÒ†Aûþý¤/^D¹Æ øÀWÑšFÇ'Ÿ�Ïd¨´ÙÈè: ]Ç=l±1LE€z»�–\—(Ò¥ªd ƒEATÓ$gLw:Ô4®|ó õ›6á ÜÛö˜,ʬ�®çíö·ÙZµ¯â¥ÆQC›­�£ýGYT¶è±l¢i¢½šFÞ0ˆ«*óÜn.ärx%‰öBuØŠæEzT•)N''²YDA@4 �$aŠâM}ëP£FâšÆÐðÏ4§“öaK_ßðc~YÆ.Št TÛl´äóÈ‚@@–‰i“Ž¢Ù0Pm8ÿ @ÂHàOùñy}wÖ®E“˜ã뾯ñå}lªÝDó`3Û2GžÃ–ÀÚÊÚÈ\ÊЦ´±§k»ã»i4YàXÀŸ‡þœZo-^—÷GïãU0 loÛÎËÞ—ñ9‹×ÿ¼zmHcnÍÜr}ttbzŒ‰tv2Ï1�õþõx+Æwn$ÀŠi|þzÿ_#ºE ç ’€©š`‚cªÑ%²÷à^ê—Õó³êŸ7©¾ñ(ˆ¿‘óE±”½Á€kâšœN†t�cªŠ9:VÚ�£MtUe²ÓI�ÍF\Ó° &P¦(¤tEHë:NIB7MšœNL `šLþ=kô x$‰nUEÓ4RÉäM+:Ü ÑÁšðöÆö²1ºI�X^¶œ�{>¦=ÝN�»æñk>‹óÝ÷ß—,iW¸jEí?æp»i.Åv©PàR¡PjOË.�ç†vt¿S>Œ|ž£¶–ÈÔ©|yî\éÜGÎoä¸úØÙakóù|ž–|þ† 9·Í†‰”¦é³}g™ã˜SJ‰sS¡ êtj�|ßó=±ÁÎ r‚/.Á|ï|æÔÌ!'æØÛÇ?ŸþgºƒÅÙÅÌöÌæ¿Fÿ+aW§Ãi T#bBørðKfH3¨ ûY‡ÚÁ‘ö#l©Þrß˼Ä-ùŽ÷Ç«{™ïŸÏªÚUHò�-*LÑ$I’T6…¡¤\)Ú’m$ÓIPA6d$SÂ4LZó­h w�{…Á.�ü*‰©›äÏåñ<åAÚ’mhº6*Öý»Ç{ü‘±à^ó,ø(N"¢ˆÍf÷@_(~´y�p1—C¶ñ°S&Øl\)°‰"r9æ»\L¥˜îr‘Ô´’_— ªøe™„®“7M.«*Ã@Ã4 ) …NQ$!¨Ãɲ'‚¨+Ê4c_žbeùJDAä¹Èsü¶ý·¼f{ Ÿâ{¬Ú�³ô—ÿÆÇÿĤ©“0t»éÁ‰·$’îĪ'9xfÌÀ.7²õ #c‡RYɪ¿ùR/–¶¢o·è0�YRf?&fIè™@Ë©ÓH®*„)SPƒ/;¿ä­è[7=dJJq~ð<_÷~MGNÑÉ$÷$$I¢ÆQCBIð~ì}:Û;Qò CÆÿ¶ìß2µa*n›ûŽÅ��? ŠÌ©.fÈ’e_Ï>Ö…×Ý׈߂T 9Õ̱ÞcÌ”g²6´ŸÃwËê+#¨‚JG®ƒ´‘æTæ…d·äÆ-»ñ‹~‚é slsp…\H‚„ÈpBgSàtö4ÿ[ûßH. =­#èB±Qjà{ч©™˜Y³”¬\@(m±>ªÛÀ#ðQ³TZðq o™hšF:�Æáp”DÝH¤œ¢(%±�e_o IDATg·ÛÉçóÔÔÔàr¹¸téÒ¨”$ÖðTLkrmDßÈwI¥RO¬05€–áô +Và®Òpý¤ú &oD‘ÚW_¥åÜ9Žf2 I|™Ïƒ¢ðC¡P!×ùŽ\¶ä(ŠÂÑëÚ�iš`Ô-]Šá÷OhN°ž´'ÛiMµÒämÂ-¹ÙÝÄžØ6VmDŸ¢ó9r|Pø’*�àW[‹…äÝ~1íOipÔßrÅ?Vª—‡9À†�RQA ¼ü†d³7kÏ}Zÿpþÿ¥­ïæ¨Ó5ékè'lëæ§î(ŠBËP ®>Õ³«GÌ:è†NNÌq!y�é|Ñú½™^D»H“§‰œ’ãPöþ¤Ÿj˜æŸÆçÊ¢e|4ðËíËGUü°¸‘¡‡“½'y­æ5$IBEeGמw=ßʼ©’ÊñÔqŽôa¡}!oV¼‰Ë溹�¡†i�´%¹¿ÀÙÂYŒ!ƒG ÕÎjVÙVá©õ ò�Ea1Œ¬È$¿Jk”jÉ-‘>’ÆÈ˜“ ¼ÁðQ ¹~|xds4Zp|¯ÏçC’$‚Áà 7þÚ†išf±­ªÒ�#‰"CÉ$a‡ã¡¤ÕiŒétšt:]ª3:"fŸTèv¹Øö?þG1 ¸²Ñç+9ç>èAdd²®ZµŠ­MMh¹\Ñ:s³ä½×XptSg@ïA5GŽ$ ZΞƒI˰¹Ý¥Ò_2¨!°ºr5ow¼�WöRî,'¤„˜î�Îg=Ÿ±¦jÍ„&‰$Ã,þ˜¦‰Éðõ1‡# Íë" o×l‡Ÿo˵ñ÷ßÿ=¦×äÊå+Å>ª™LL§º6Š,È7�cå`¼¶ =¬Ý�¿‘Ç�'޳ë‡�Pjä€LîL#oSl„$zÚ»9:NÍ` ÿëÊÿbQÙ"DËkhN�O:?át÷iò¹ýêäÍèjeòE”†äÛ” yÎ0 >�}ʯÎüŠ‚VáªSµa˜Õ&«¼2“m[J@(æ¬XÇöÎí¼^û:NÑÉlßlóƒœ:Å´²ih¦†¦kè†NŸÚ‡i3ñçý¥ãˆ^‘”‘"¥¦HRd  Z]Ó1uÁ�L ›ËF^È“Ósh¦†j¨h†Vƒ#ç$‚)”®�ÀMþ60ÀTMÚÕvTUÅ]ãÆ9ɉžÖɞ͒VÓhºVê+·ó }h¾£·²*ßæyÓ4Éé9D—ˆ½ÒNþJÏR™ãì“ì(e CŸ ±«gǾ9F}U==�2± Š¡à=¬ ¬å�&ý^—»hÇítãö¸o~�D“ƒéƒˆ}" £ ´–�;!©'ùÇŽäUÏ«=EÃÂéôizc½¬­];¡×ΔLÚrm|ÕóuFÛ"Ûn™O0a$8—?Ç…� Ø vf{góóðÏo)ïJ(2›ê71%7…¼–Çív—*•\P "-óÈoÿ^kXˆÅbÌœ9óGÙöŸH˜ÏçéêêÞ}«AXÃà/ª«1—͆GQ®¥a¸§c¤¡ŒÔåöér‹béµÎk~7…áiÏ0ÐMýšÁš¦•ür9>íìD×4\."n7ªª>ñ~‰×¿G!�À�®dMÓ$i&ùÝ…ß‘WóØëíè)�Üù¦nb«µa¦M¾êüŠ%•K�S26ÅVŠ3ÿÖu‰6Ói’7‹â+oäÉkyòzñ'§æèÔ;ùïÍÿ�_ š©ñ^û{Ìê›EÈ¢˜UN'Nch!!D£§ŸìÃÞgÇ)8±‰6ÂrEVPì ²SFÄ¢ÐDBDd†qôF˜Çï–L ¿ùî7Åm&´˜†ä-:¢†��qƒÿnýGQ^ÛÎ ÝÀÌ›’€­Â†™7±×ÅŸžÐA‡gÏò“WBC}N§s”5ùnÆS29�:€7yºúiKüÝ‚„žàï:þŽ©ù©ÔT-}ýf?ßžc[å¶ Mx=` ðyüsìi;ëBëz‚c[¼E“¸ç»øw ¥†ŠÀ¡õ¸ì®ûÒæEQdÆÔ\¾|™öövr¹š¦•ê#}M’¤*Ç“ÜäÇn·3yòäR¡K> Ö¤á›{7š Dl÷/�®p��”i¤±� ØÃ×8�üß“'£ib.ÇÀHT£øä—Ö¹^hÝIù®û=Qßécº®“ÖÒØkìd›³xžö ûdòíyÜ3ÝdÏe)t8Õs -§#+EQ¼* ¸ÎB6üOBBÊJÈ /^‚bpØr,"‰’Sbµg5§{O+ÄXU· �õ•ëÙÞ¹�C/â±Kr©*˜ "–¶'ôZ2¾ÅŠKq1Å7… G.SPL� jjÅüu#À'©?Œ´©°=ŒM³‘þ>�`Ðt¤ Dîl®¸P0MªÕ„CaÜn÷ø£še8�<}°¬z™%þnéô)b‰¿¨ÿŠ¢�6Ól¿¼�MáM¸î ùŒŽeŽqªû««¨®­ó¾´ä[8Ðw€°f�oåÕåêVr3$I¢±±‘šš4MS>hëßx«;�œ«,Ë– ÅÃçÚŽ$ 7ÈÒöâuw+p‡ p¬„œÓwën­„¦a"éÚ�†s–3gbê&ŽFÆ@PCaqh1Ï4<ƒ×{r©-­YʾØ>ÚÓíLñOÁ!;x©æ%¶wmç�š7°‹vlÒ£Y-¤R©ä?¯ùÏì8ºƒHy¤¢YàYPÜ*檋Ǔ"¯]øÌrÌâ?­þO´§Û1½ƒqîò9¦ø§ð”÷©[nëÞŽëûŒ@2ÀÒèRKü݆ÞB/çûÎóËÚ_ô1E“O㟲¹¢´|¯‹¥+…+ì�íe®0—·jß*ú^‡fj´ä[øºïkšô&6•mÂçò=”> (J)�Ò€�,Á`°´2 Qº®c³ÙJ«•L&C*•z(ç)•••躎×ë%—Ë•ÎE–eTU%—¬5ñxœ¡¡¡'ê^= 9o% SaYù2þùð?—´¹¹·s˜];›ˆAQ”ûú]W„WðÛ+¿%⌰(·•³,¸Œoãßò|Åó2¡ý�þ¦¸xñ"‡=£Ó;Ô;ʪð$ZÃG!3DÀ Œò§Ìæ²4Ú©-¯¥¢¼¢Xn¤ôïÇÞgó˜�e‰¿Û�sìˆí`Sd!O“oû¿¥<[Îäèä{îGy3ÏW‰¯è`sd3!oèÆqQÐi͵òeï—4M¼Qö>·Ïº9–¼…B�®®.) ¢ª*v‡µ R[[‹Ýn/¥Zy‰bÇü “¡þ!2é bU1;¹^v²ÕM»2*�…®ëź“OP›GÝÇñvçgÃÆë ¯3»|6Y-;ê¹¾�>’CI”- ÒW‰Ív-pvÑΖÚ-lïØÎÖª­8e'S}SÐø>þ=‹Ã‹](TUUáv»éíí%›ÍŽÊ›ø¤Š–’àðBuÄÂ)Š"~¿Ÿ`0ˆÏçÃétŽKˆìhÛÁ ¾¨ Ôüè+yÜvAjêìíÞËsŽç(ó”�mF±Áëª×ÝÛõ¡5ÓÊçñÏY*/åùúço¨¡lbb|Öýá|˜×ïãsYÂÏ€wŒËåÂáp Š".— 6.~u‘Œ‘ÁÀ õt+22†`�ÉgðÎô"9|òÓL[†Öÿ¯‘K\BFFTSEœ)²è‹Š¢pØJ¦� EÎȬ ­"\¶»…%ïvBN¥R‚@6[´Èä‡ò\üú"’ZLàibâÀ�††&iL�:‰/maÞõ^L­˜KÎ4MDDlØ�=2±x I¹z^ªª>ñÀ¢,SSS󨶳±,8#“¾¢(¸\.¼^/n·û�Þ·Fw#)-ÅþÞý<yº˜.¦jïv½‹Gò²?Úb`$ªÐb|äÌ_ }�œ�ÙR¹ežtZÓ­ôÆ{Y[½A¸¢^á¿´üþ•ÿ_áuyÇ}Ü.µ‹�;?f¡°�53n(W |7ð.òBà¢uQk›Þ€ã�<ìv;Ùlöj0…Í`Æÿ5£´Â6MŦc-E�ÏSêp¦i288ø@¬²G&º$::u W}€®· ŒÔþ¼ö±Ç9UŒÍf£±ññ¬@0Vúšk£â®�Œ{ÌôÍdW÷.Z’-LöNFe^.™w;Þå§µ?Å)Y5^ŸDâÄÙÕ¾‹Å¶ÅL«™f ‰;d@àóØçü¤ê'(r±äÞw½ß±Æ¶†™‘™ãêǺ©s8w˜³�gY^K…¿bô˜!˜´Úø¬ç3æ‰óx£ú�ûZRÎÂ≀#[G†a�F1M“––$I¢ ðx<är¹R²Û‚Z ¢ª¢‚€a >0 I§Ó”••!Š"—.]¢P( ë:.— UUK%â … Å<`#ïoooh%ì&B¬[ÔýA$^©x…�]; *AÊe”ë*Ö±/¶�µ•k‘ËÊö¤ ¢r:}𱬠¯3°ÀblòFžÝ]»Y\‡Ëîàhî(a-ÌŠ†ã²F'„{ºöVüY÷æ ÛÇi3Í矣êl‰l!à X7€÷ʈó´Ãá(P45ý9ú[ûé;Ù‡ˆXL·0œíBÇ…Ò Ï0 t»Ž§É÷Ù°fæLÛɤ3Hi Qñþÿì½i�TWšßý;÷æ]r¯}/Šbi�Bm#$�@ Z ÕZÚ1ápØ1v„ÛpŒã�·ãýâ�혘/v8Æn{^;<óZÓZBµ¶F�6$uƒ v jßs»ë9ï‡[™TA�±” ÿY™•™uó.çüÏóüŸÿCe(0¢†ÜÄÀL˜˜Y“ 0 c†8þf7Š®â{\èZŒGšá•Ó¯ð|Çó¤b)Zâ-tù]|8ð!7?|Åm¢ª˜#‹( ð›Þß°@.àùŽç¿—VíVƒÒÛG¶³ÚZ]‰ÐwŽsªï›:6]>ùp²t’ý;ؘÚHWs׌E®Òrølø3ˆ?À®…ÕEpUxµ ¥¤P(àº.…BaÆßz¿íe`ç†0(ªbÔÆ KXäUS˜¸¸ÔÜ]CzaúŠ�m/ãߌ3úßF15“oÕ·De{r2GJKá*—ú-õÜöâmhº†ëº3Èßt2XEç"ËòDó¼7ô›Z6¡ �eéeìrvq8˜%é%Õ�ô…§<>ÉÂÐÈë7RŸª¯ \&öŒî¡±ØÈÂö(H�'χƒ²¥iËei©$ïO¼Ïøè8/6½HMrfT¯¤•ØÞ¿�L)ÃO~R­î­¢J¯fmåe/´éúÓÙŸ7•»[º¡_—ÈZvYñ � %IÎ µ5¢H`†mÐ×ß7k—Œ[¡[Hß-ñ‡‹y¯ÿ=my!4=Àk}¯‘ÒR´'Û«;é%gÔÞ=ó.«�ÕÜßqÿuéq³á {�þñ~žèˆzúºÊeë™­<–yŒl2{YŸUÐ ¼=ð6-N v<8ƒ<*ÇJÇxoð=Ö'×ÓÕÑ…®UåUT àUGSSSdË1Õ/³X,R(*Ðe� eYxžW ñ×ÔÔÌH÷öö^sÛÝÒ©mý~ý«)à*.KSKé/ôs w€Û3·££óxãã¼|æež·Ÿ'©W+Eç< Ëav ì"^г¥iK5Št…È9öœÜó�ÏÓc(¡Ø•ÛÅ2}-Ù–Ë:&}N¯ô¿Âc‰ÇXرpÆxî)�&? ?’ç…æ.›XVQE•^\×¥T*¡ëzeU¬ë:†aà¹�TìV?ÀW‘µJà=T†ÈðÚ›- !H§Ó­b™|Îöº²Alù~zwÇqð¦úWQÅìó”àáæ‡Ù6°�´–¦3ÕI2–dsûfÞè}ƒŸ´ýdÎ¶Š« ²ÀžÜÇy æZ[ªÚ±+D^æùõÑ_óLÃ3{—}“ûc‚eíË.yA.‘|ã}Ãï{ÏóuÏÓ”mšñ÷Þ°—÷úßãqë»ÖW­�ª¨Àk Ó4I§£‹Ú÷} Ȉ ‚¾ßö1òþH¥ß¦@ èèH$q''s¤ïMÓ|_ó5�° !ð<)%ííQîÛo¿=Ïæ¥üʦÐ���•¶PBFFF~Ðv0U\?¸¾a=ÿçôÿa‹µ…¬‘¥&VêÚU¼Ýÿ6Oµ?5g;…ܪðt�½“{90t€ûR÷ñ@ÇÕtï÷€2;ûwò`üAê’Q¥t¿êçëá¯y®ý94ýÒHu¨‡ììÛIPx®í9âÖY[¥P„|Sú†/Î|Ù¿dš©^VUT àuÀÄÄš¦�ç²àg}Ä�#²§££P=""ƒ‰��Õvíý˜Š=Eàyþ©ÈÊÅÀˆ¶kŠ”ZÊB Iº=M¶3[!}åô´â¦j WŵEBO°¥m ;wòLË3XºÅâÔbJ²ÄÃ_°ºauu'Í„±�ý…ý|9ð%+Ì<ßö<¶iWwÌ÷!(Þ|ŸÞ´, æØÑ³ƒ-M[.ÙƒoT޲}`;w„w°¬sÙ cgOx¼Þ÷:µn-/v¾HܬúmVQ%€× åHØlѰTgŠdÇwk�ÊÅ×4¢¦`òà$ù½ù³$AI•ˆM"�@:Žrh~¼=£ÏZRîxRE—‚¬‘åîš»ÙÑ¿ƒ§ÚžŠ*ƒ3ËØ9°“㸭æ¶êNºQÄO ùÖý–�{>æ®Ø]¼ØüâŒèRWŽ}Þ>ÂÉ�óV „À×|¶öoe]fÝ%ióŠþ ŸWN¾Â¦ì&ºšºfŒÇ½A/oö½ÉƒÖƒ,îX\MùVQ%€× š¦aÆU%måèÚ51ZÐøp#©5©}Ok¨9o ªLîïïŸñ\†a ëgÉá…pU”±0¹� w‚?Œý�»ëîFCc]ã:^;óuñ:š­æêNºž WÍçDp‚Ïû>g±ZÌÏš~V%~W}^‡Îâéö§£qÉG#±\-§³¶ó;ß/‘|Yú’C}‡øãæ?žaÜ,…ä¨{”{>ä™úgh®©^;UT àu…išX–…®ëÔÔœïªîû~E_§i±X ×uÉf³íG§N�º&Û\ßX�”Û¶gÕõ‹E&''+æÏåe&“Á0ŒóHbù~bb‚‰‰‰ê™^ÅEqwC<4qˆ%Ù%˜šÉ³íϲµo+7’5«‹×AŽJ8:z”…b!ÏÖ=[íÝ{•1ªFÙÑ·ƒZ_À6mŠ=ù=ˆü¥}„zÈ;}ïCžëxnF*ÞW>K0ðÚÿ ;QÝáUT àõFÙÚE×õóÍ‘•"ßÛKÃTK5„@…!F¢MN2tø0”£g•Áyêâ|ÏÃH¥®ú6{¥ãã465Kœ?phR"Â9Í Ã�|.GÂ4±- 1K`¹RXÓ´ª6°Šï k°®a¿>ýkìê­z,Íb]Ó:^í}•Û_$«F¡®Æä_N~ÉÀÄ+ìlnÜ\Õø]øÒçݾwÙ˜ÙX!Ö½²—ž¡žjê;+©'Ôo÷¿ÍÂ`!wvÜ9Cï7©MòÖ™·è»¹»ó8Çq†‡‡¯úœý}-Цg½.¸Ðšö?.ôº«á¥›N§Éfçþ¢ù¦Ç¯lËÖßßÏèè(uus»÷M»L9Wû¦Â�?üêW{{qOŸFÅãJLªP‚)òJR¢ �¯¦¦/ )NN¢a_½ºF¿ø‚°¿j›�² Dÿ[)4¨Ü\¥Ð…  ÍŸO®¯�æeËHÔT‰Wqå°…ÍúÆõlíÛÊO;~Š©™t$:ð”Ç»ýï²¾y}µÛÌ÷‰Bás¤t„={hpX]·š†ú†j7ˆk9ÅîünZü4F¿^ÌãÍÞ7y¢ö‰‹¦ÙŠ#ê»Îìâùúç+=‚BB¾ö¿fß™}¼Øø"õ™ú›~_NNNÒÐÐ@,#‚Jfiú\{¹ãÕŽ'ßõ¾sýr¿ëuWƒ8–o���|ñÅÔÖÖÎéñò¦$€aR*•fT ñÛnCëè ½fM…\•� ¶mÃùæ›(â¦iH@×4¥Ð§ž32D‚ïƒqõVëJ)t¥°„À“}ŠèI@N=_¾œÐ²*ÑA9õš¡Ï>£øÅX��ĉ'ܵî`RÅ͇¶x÷ÕÝÇGÃñpãÃhB£;ÙM_©�ÏF>cMÚj$ðr Á¨7ʾâ>NNžä6í66×l&Ý’®î›ë€ƒÅƒ”†KÜß~?𦍀7N½ÁZsíE‹4<<>ûˆ‘±^j{i†¦/Û¶am^˜÷Âe÷ þ!Ï­aâºn¥CV¹©ïû•ÎYccc´··³hÑ"€Šf½¬_×u�ÉÉIöìÙC}}=Ë–-# ÃÀu] ÃààÁƒtwwcÛ6º®ã8†a ¥DJ‰iš¸®Ë¾}ûøñ�ÌØØ_|ñóçÏGÓ4t]§X,�Ç`ÕªUxžÇ_|Á’%KøöÛoY³fM…Ìööö²`Á<Ïò,|߯•Ÿ¯|§*¼é¹*´sv …™¨[²ä‚ÿ3ô<&öíà `WV€IÔ… T #‘€xŠÅ«F…R) 4 „@h¡RHÀ„`¼TbÁ?ü‡dçÏ¿à>Ðuý<˜jW�*®ÝénFÃQ>ú˜ûïGÁ}õ÷ñîÀ»ÍeQjQu'}\ÝåTñßL|CX ¹+u÷5ßwË�…¹€oKßòíà·ljßéò4Ø5±‹.ÕEw}÷ìóŠ^ÕËÿ8þ?X*—ò̼gf¤ˆÇÅ8;vò£ðG,ë\FL¿µÌ¸¥”‹EÇá믿Æu]ÚÚÚ8tè‹/¦¶¶–�?þ˜0 9|ø0Žã°lÙ2ŠÅ"¿ûÝïX´hwÝuããã|ùå—œ>}šðÈ#�ðñÇÓÕÕE±Xdÿþý<ñÄX–ů~õ+Z[[I&“ ²`ÁNœ8AOO+W®d||œ÷Þ{�åË—ÓÔÔÄG}„mÛ,]º”ãÇ�³páB‚  X,òË_þ’G}”½{÷âû>Ÿ~ú)Ë—/g÷îÝ<ñÄìÚµ‹G}”O?ý”G}”… òù石jÕ*>úè#6nÜH†ç]–#€aâû~•^Ùl¶ÒÕ¢¼:(‹(¥Ã�X,Vñß“RV~7Mó<�íðð0®ëÎ8A'''g5€¾ÚŸ> i+U!~œÃðMËŠ G‚!%\…L*§ñ_ü‹ˆ¼IÉl[Ú,~<Îd.wÁ�ò¹ß±üºj·€*.ÿ¤„•™•ììßÉ¡Ü!–f–¢ �õÍëÙ9¸[Øt$;ªûéÜÝ&á{Ç÷2še¡±�3RÓPSm×v�1 øtàS¶´l©Œ�&à�ùÜÕv×ÇÑ!oˆŸþ9?³~ÆSóžšAþN9§x³ÿM6e6ÑÙØyKÊ!Êsr]]©T Ïóèèè¨8Utuu�Gc?þñ�ùÝï~ÇW_}EKK £££xžWéXÕÑÑÁ† صkñxœcÇŽáû>ûöí£««‹ññq<Ï£P(P,9~ü8óæÍcÁ‚8pÛ¶+Û#¥äÎ;ïdbb‚††Ö¬YC2™d||œeË–ñÑG±~ýz¤”ÜÿýÚ××G"‘ ¦¦†––òù<¶mW‚)ÝÝÝ•`’a¬X±‚¯¾úŠ+VTßtÝc™«”£¥sbê¦æÉ“'ŸÏd2ÓßßO=zôš�@Ùl–0 I½*Ò[ IDAT&“$¦ª^{{{q]ß÷±, ËŒB°RIlËF*I&�!™JžMA èïï¿îȪXŒ¢uñ«Sy)É¥h.çù¹„ÎÎÎê9WûJòÖÀ[¬Ê¬¢%ÑE¶”Ë˧_fcÃFšâMUÒ‡¢$JsŽñéð§t„,Ï,§1ÕX-ê¸A(^=ý*O6L{{;CCC!èìì$�NA%+†a%æyš¦Uˆ[††A8Žƒišø¾_ I)+¿—J¥ŠÇm™dù¾�®ë˜¦Y!‚a¢iZe>)Gã4M#�£”š‘nÖ4 Çq°m»’¢v˲*A'!Ç�§¹¹™0 ±,«òü¹Q@MÓ‚€>ø€•+WÎjEw­‹-b```ïÒ¥Kÿ81u$ªkõyÃBC¦i’J¥fk´¶¶¢”bÇá=mb (;˜LÝ ]€K$„ÓÁömo~¡®ßjLÄã�ÏW|:�¾$«•r·Žsµy—BÒª½U\o”£~{êoy¦ùêíz,añl˳üý™¿ç§?%»µl”d‰Sþ)M"?™gEr?«ûñx¼ª‘¼‘ä/,ð÷½Ï“õgÉ_A+ðAß<Ýøô¬äO"ÙSÜÃá¾Ãüqë“IdÎN¦*à݉wqÆ^ìxÛºu-zÊ$K×uFGG¹ýöÛ±m›¦¦¦$¨œö,“µòãr$¶LœÊ$-‹‘š²W3MsÖy3™Lž7žë�[öõ�Ž2A¬\ºÓ¶Ç²¬ŸSn ‘šÅê­»»{ÖŠçs‹MÊÏ•‰ç\Æ #€å�í¹:=€þ\?/÷¾\©‚1 T ¢çFä`µXÍÆÆ�èâ:VÒ•C¿R¢¦…|•R …ÊʤÜAÄ4MæÏŸ�®ë ’Ëå®Û¦†ñ�eôÓÃØ‹6J)«…%·la³¹u3oö¿ÉO;~Š­Ù¤�tÔGx`'O·<�©ßüº6� @�~·Ÿ#…# ä¸=v;k³kÉÖfZ•ôÝh”T‰_üšõ™õ4¦(Ê"¯œz…Çj£6U{Þ{B=ä7ý¿!È<×9ÓÜÙÕ]ÞèyƒÖ°•õ�ëom9�R�˸.J)ÚR)´b‘ XüŽëfêíS¿ë‰$Qúñ:U ×ç”ç>Y(:Nô]/‘œû™šaˆÇÍåjkçìysöªT*U˜ò¹Z½’WbIjIDò¦e•?U®UTˆ¸ ,׺1׃mC±ˆS,öõEzÆX C):o¿}ªbiºN ÃÊs×#¢§”"“Éàº.µµµ••Ððð0c£cA€·©©©axx˜x<ަiø¾OMMÍ 3K!…B����ª±ô-†Z³–Ç›çíþ·y²åI ͠ƬáÁÆÙÚ·•gÚžÁÔnNèh}…>¾.|M±X¤;ÖÍÝ™»©m«E�Uí[æ =`ûàv~¬ÿ˜öläõç �×_ã�Ä4gίø�PìèÛÁâp1Ë;—WRöJ(ŽäŽðæà›lJobaËÂ[Z¢"”bìóÏùÃ_ýîÉ“hS^´L5S¸dè:-6°üŸý3ŒÚÚK&u—cò|©FÐ3^+¥žöüÕ_1¹g\Îüvîÿ”‚6�þ§ÿ”lKK•N?Åb‘|>‘PJñ`탬 ×€úŽeÅÔ’ÂŽÙÄ´ØõO‘Æb(Ó$ÿå—LL�4ðu�º_ü‚x]-JSS«U!~¡ºúÂÐrÊIhUþQ Ozœ;‰úL0ù�)üÏòÐl�¼ÌSÊ—P¶ÂÃÃÖmZk[Ñ„VÑrTÙ©²ÿ*¼õÐ`5°¼f9Ûû¶³©u1-F“ÙÄŠì Þì{“§ZŸÂÐ~øš7…Â.'s'ÙëìEæ%]V÷&略¶ö–«úüACðáè‡,ò±´ei´èŽ v�íâvyû¬¿Á/÷¼Ì©'XÐrÖÜ9$äÕþWÙ5¼‹Ÿ7üœ… oyïKå8zåŽçÄè þ´îOYи :îJJœñqîN¥h6 ú<KÓ0„ ­ëøS� )±4�¼”Ì·m¾Êç1… Ý²è°,tàýÉIBߟ[s‹R„®K‡a°,™ÄWŠ\¢ Am,F ®RøR’— X˜H°¿P .cžeá(E õ}:,‹>Ï#PŠÂø8¥R©J/ïxüp 4ÃÀ C¼ ¨A‹cc¸“.™Bæ¬f14Ȉ ÙXvf_BTDÄü³Y…Bi éJàiE½HQ+" ’´JãKŸ¸'m§ñ|W¸øÊÇS–nÄR2…¯| *P¨@!¤(�ˆ‘ÕŽÔ"­e¨p'Úæs.NÇqª3Î-Ž»ëîæ“¡OØ?¾Ÿå5ËX^³œ`4`ÏøÖÔÎ}£h!..¹ Ç¡ü!zý^üœÏmñÛXŸ^Oº!�&ªUé?âþÛáß’ ’ÜÝrweÜüªð“<ÚþèŒÔ­¯û¼7ðA.à…ŽftlØJª”â_.ú—sÚ¿í†ÌÉB0:¥ÿ>âºÜŸNs¨T¢Í4éu]  L†!Š©šÆpÐ)%caHjªÁ¹ү¹òýòJ1†œñ<š GJßgrÊó¯†BÐh û>-¦É¾R‰à ¡iŒ‡!óm›Ñ0$®ëQFpÊp.â†ÀD"q^äoz1Èô²êé'ɹ !ĬiäëN5 ×ugT5´gÛiI·ÌHe_L§PNÝJMâà ‘ "×ù%,,²Z–†°�À pC34‰©¾ç#¤ VÔbi9#‡.u²ñ,Z󧸞Eµ²æ�³÷гdAÆÌ06:†;_ÓU.“¯âÖÆ½�÷òÞð{?Àm5·°²n%_�}ÅïGÏʺ•s�ô!˜d’Ñ`”o&¾!ïçI{i'³"µ‚D}¢íù¡‘¿Ñßbù«[VWŽÝÑð(LJŽódë“3lxF‚¶õmã.îâŽÎ;fóÇãí¾·¹�Û¢¿UÓü3¯Ó¤~åJÞû䓈À¯Ž�!€SA5Í‹`¿ãT¦¾¦3šjk±[[çÖµ¦i$çÍc<xª@óÐÔ÷){åMï¦�³SûÇ…Båo_r¶à¥.“ANÓÑßòpz —šššŠaäñãÇÉçó(¥H$Äb±Š§�mÛxžGKs ÉTr™*‹sB�v©+šòk”P„„ø" ‡ëSeÍšÔ-@35<ßÃRš®Ä AÛ·©‰Ǫ̃¦¸§¯ù(¡H«4"&x°íÁ+>VçB×õªþ¯ ¬�­½[±u›îtw…~<ú1_Ž|ÉÝõwßø�´àLþ á‡bJ“.£‹•É•Ôeêª]9~¨Ñ(M~XÓ¾¦­‰�ðù‰Ïy¦åâÖY�ÖÓê4o�|‹�5™W7où˜aî\ÛY]ÌÃ`ÑOJAW¶"‘JEJ¡Ã$>#À1õàü<€¤»ºH.Z4£wî\Øç�÷ÜÃsÿå¿Pìë›=@sÎs�òð(ͬUÑBSÊ�ƒŸE¢ûTssµ xæ¹dTH^Ù){Þ¼y(¥86rŒ¡ÒÐY»�Š~mph1éâУ‚‡üDžŽD†˜›áz%>>RHB"…Œt€*†@ ¡aªhrc.§@LFÝQ4¡�qâÊ@÷uLjºÈ×up0¤qMÒpUòWEå:›Z6ñZïk$­$Mf(XS³†wßeïØ^óú.0•¤`89r’Ãáa Zb-,ˆ/à醧I‰jûf ¹�pÇ\j{¨¢Ïò†Øqb[Z¶�´“•×îšÜEßp/µ¾D:qÖ³R"9äâó3Ÿó|ã󷬹ó¥â„>Àß,ü¶a£TÔµ*&b<=ÿ)~\ûc4M¯tÐç?¦EЦ5ßhTȧ®S¿jµJ¡¦æ¹ó|ÿ¢'9Q<Áßù)zÅH¾Uáå Ÿääý§Y™ªgsܘ³r‚Bu]¯.žkóaχürø—N;ÚÒ—€�~W¾Š¼<¢á-üG”ôI’•(Ú�€)¢•‘QA†‰‰®ttôŠ/a”â%K$É0‰¥[„FH L¥wc—y˜<ãƫ©ŽXU\sØºÍæ¶ÍlëßÆÆ¦�dŒ ºÐY׏޷úßBwÔÜqÍ4�*¦8Z<ʘ7ÆÞ‰½´x-,H-àaûa2ó2hB«FunH$L|€šP<ÔöP%‚; óZïk<×ð);2ñÍ©Km¾–Í›g´t TÀ[#oA^è|a†÷ß-ƒ©‹T©ä¬¶l‘4]áâò¿ýoöŒ|Ž,H”« z�N0ðíø!þëÃÿ•q¶åá…®¹s»gÌ…ksú6èB f±i+?ö•Ïßû;ÞÞ·ƒømq‚Éï”´DTÈ©gu¼>�~«—{Zîa±\\%€••ãTL“ÏݹE¿HZ¥+éL!§œµC…j¨PEP•[B�ZÉåÉ“$‰Æµ[Y”Ù~(Bòä§vb %šÔ°°B⩨2×ÃC*‰��¥,PQšÖS:: Qñ>::¶´¯ø;„"šª Ø«¸~$pSë&¶ömåɦ'IibZŒ'Zž`{ßv4¡q[ö¶ïM‚@äýÅ ˆçy$“É*B099Y)ú8wG/�-§ÝnŸVE§bQB HF+ �Údmt²)H� H‘©«¶Ú,ëô<‘¸²ÖD—qÓ„‘=¼¨xC9(†4H’uö EHQD!cKYXÂÂÃÃÁ!.ãW%zˆèâþ>“­R*jg5ÕŤ¼¢;·HçÜã7}5§”BÓ4Â0Ä=G\Å͇¸gCÓ^ë{�-­[H)bZŒ'[Ÿ¬�À¥™¥—OøŒ€áâ0E£È¾á}„NH¨Bè X–YF�QsK·æº•Èß;#ï�q3¬i]S!mL°­o�§§¥¦“ìÞÍé±Ó¼ÐøÂŒÎBN”NðVß[<™y’κžÞOD"3J±Å / Hœ,œd(7„p#‹°BX@G'©’¤I“Ñ2Ôšµ´[íqƒ1t¡c` ›‘ÜHhbÆ}9à¡”"äØ?¼Ÿòš¥ásIÜ‘@£Ù@„"jr GY­r¯Þ ‘¨‹Eç ¼”R‚å*Œú¨Ë–fiXó,ˆ�œ�ÄjbXÝ~�O©[•N#3lP¦vô‚Æt7t_ògM'!:Qëµ@—�6 D€œú)ÿS±È:EjQäND)§€€@x2Šà™Ê$Mú‚$Ò..::¾ˆ¢†4.ø¾+Ž®jñ0þýÛ0$‘HP(p§¢_ð<Ï󨯯'ŸÏcÛ6£££$‰JƒmÓ4I$8ŽCww7žçÑÓÓSMÁݨ3êØÐ´�Wz_áùŽç‰ëñ |³ïM4¡ñ£ô�.ú¾æÓëô2Œ³/·˵HjIæó¸7q/ÙlS7«m×n1ò·}p;mª�»Zîªh8 z�m§·±Î^G{m;ãÁ8o ¾Å|o>›Û7ÏH놄ì)ìáTÿ)^lzqÖ–ps�íáZ.ý“ý8%‡Qk”Ãã‡%A:–ÆÔLjb5dóYºcÝ,Ë,ÊYĈ±J+RÅåGØfèÚ„À6l–Ö,åËÏ¿ ”£ÈMäÐÓ:²(ionÇTfE#>= 8Û<ÿC¹„P(Á¢ô">þòcbÍ1ÏA•ZVÃùÖA÷„ lÓÆç®�Ð í|5L%¡â(\�X)u»�H¤ˆnH00*E†2¢Èß”NO YY}™Ò$>Uñt± ›/üJÔМúñ…O¨Bli_3­bI•H¨Ä÷Û¯®ËðÑ£H)I$à8d[Z°[[ÏNöuQ“õéMÀà `ôÔ)ü‘L!è=xßó„À˜ƒáï*®>ZìÖ5®ã�¾7ØÜ¾S˜Ä´›Ú6±½7Š.J-ŠìŽ”Ä�»9FNåø:÷5ébšV»•yñy<“~†Dc]èW¼€�RÒßß�çy3&§*f.¤ZZZ¢ë}Á7|Þê‹ÅÚb–6,­l«k¸¼qú þÈü#:ê;ØïîçÓ3ŸòhöQ:ÛgFö-`ë™­d½,Ïv>;·*¿ÅY ^1VäTîE¿È!ÿ…É­Z+#CƒÝ@—ìâŽìÄãhB»$më•�¿Ù`bòÜâçØëì¥7è%¦Ç*Ÿm “—Ú^ÂRVå›^Ý{3\oÓSÃB Öv¬åã?&çä.øz§àðHøM~Sµ "ÿ¿s+ÎM!NG¡P¸ì“§¬�“H¤e "â'‘h*ÒéiD¦Ç>‘eJ@0UÒ®E�HdT¼q9Z<…Â�ˆÉžð¢öjÊÆ–—žªº‚|®€V ‰+] ¾ßŠÃåÈÿõg¡D ×XþË_2oõê‹828ðþÞ�o�OIIl=Fý/þê—.¥Š[‰î•÷òÎà;<Öü1C"y¨é!þWÿÿâðøa‚xÀ™ü:e'm‰6ºãÝÜUwv‹}U£ÅCCC,]º˲*}¸ouxîþ-?Þ¿?š¦aÛs#¥>Î8Û{¶s_ì>ºÏf…J±[Ooå>ã>êkëy{ômԤ⥶—HØ3 ì@0À›ƒorì~w,¾áàJS¸�‹‹Ë1÷ŽçpÊ?Åhn”yÌ£%ÞB[¼�Ûã·G¾”Ì�" )$_Ž|ÉþSû‘^4B ¥5”£øO}ÿ‰¶{ÚXœX\‰<ÞŒY¥g¼3üûÏþ=Ç'Ž#LAX+V0B �–Ôó!GjްÎ^7g�®+Ô4 ÏóhhhÀ¶m' Câñ8óçÏŸqÒôôôTÚ«]lu#…Ä.!a¤"`X“‘l f� ððPBá(Äd KEEåŸ+½8òäñ¤G†LD¢´HL›P‰Ë.èH$d³ÙJzÛqlÛ®èÊbÒB¡ÀÐÐÐŒ–3Ü*/çÄF!…Äu.»ÿïUÈ à�– Ü×vf<þÝ]@”bÅ¿ý·œ™<ÍÿwäWH-$ $t꫼èæ'S'ž£;¤¬–cñß�ýw:⌋qtWgµ¶šS£§¨M×ò�çýãkj¸«”¢P(T|Dƒ ˜a8!�Òl ÔÙ ÜËÑ�Ù&ºK%˜ç~Æ…Þ7›vjº�þbßáR‘BR©ããã´Ì�Æõ½^/;v²!µ�öÚöÊó9=Ç=o°6±/íñÊ™WXc®áGó~4#¸ ‘òñééOy¼öqÚjÛnÈõàë>“Î$Ží°h?žï1è Ò,›é°:èNu³Â\A¼.~Md 3æ4<Íà ¼Š.MÊÈ– ETx™‚¢,âKŸ@A€ïûäܯ{«Ã"̇•ó/¾(†3Ì�üºíîÊ\³ÀòuÖãôp´ï(©Õ)¤' Ʊa ì…6a>$VÃò9|ì0]svÁy]  iš¤R©ŠÿŸŠ+Š^W¹ìíÝ{vpCQÊ—¨5k+†OEé[�È,¹\}k(S˜‘f�œÊ×â„"ª66•IŠB‰2ãù~' _ø¸ÂES66hà KZÄÕ•ëð|é38:ˆeZºA©T¢è)¹¥hÿY±ˆL‡ÞŒÉÃõz=ÈËûžîEÉR'¡Ô™ tf—ªËÈâ)…_*A©t‰£ŽIGz92&‘�$egªìè&%|NÌa¸8Œ¸ �06>†©G¶Gí¢�¬Èrpü Ï/|¾¢ÉZݱš]C»ølô3îk¸ïšNeâ×××G.—Ã÷ýŠ™¹Ã0¨««Ã÷}<Ïã“O>áî»ïf÷îݬX±‚îîn”R sôèQîºë.‚ `bbÏóèîî>» C^yåâñ8«W¯fhhˆE‹†!¾ïsòäI‚ `É’%A@2™äã�?æÁd``€?ü�E‹qç�wây¶m“ËåH§Ó¼óÎ;<ôÐC˜¦Éøø8½½½,[¶Œ�?þ˜•+WV¾�eYèºÎ‘#GÐ4�o¾ù†öövlÛ¦¥¥¥"ݘNËŒ‚ ¨˜ñߨÉ[ Å)uŠOú>á™úg¨K�ÝÞqmœ7O½ÉêÔj~/�Þ£³¹q3™ÄÌñÅÕ\Þé{­¤ñbû‹3 ¡¯9áÓ|�AFÃQö�ï'Æ@Á<}KâK¨«©‹´±±ØUIÏV|ç„¢d”˜,Mâº.JW iCœÎ�¦ä”Ð|�¸‡X QJaCE÷¦01GL,,b"ò©Õ•Ž&5üÀ�º]%¢¢�`$Àî´ÑS:ád!ø¡O„]\PøC'J)Â0D35¤#)(‘¼+‰ª�œI”UP5þ€� RÊ9ëŸ{]  mÛ˜¦YY…ÿ¾ï÷üÙÑ?‹dO"t�0ÊWHGò”ý›lÆÓ<âı•�…Ui™Š��0jo6Um+¤ÀƦV«%¥¦¾«„@D=yCF¤S™xÂÃW>qÿÞ†Ô%QbÛñmŒÇÐt�—n‰î–nÞ9üŸ~ ‘¢A)à¹ÎçȘÑÄÞ¥kx|áãKZ‘õÎjŒÖ7®¯œô…Bá²¾ƒ…Åúæõ•JàX,VÕ]ý�¿©‰jÜgp|�RXâPpˆüDž­�d,I³ÙÌòØr²-YLaVÒ@mNŽ|Ȇ– hBC:k×òùèç¼?ø>�4=rÍ& )%¾ïsøðaNž<ÉâÅ‹q‡`ªow.—ãØ±c¬X±‚Ó§O3<<ÌÚµk©««cdd×uåøñãÄb1<ˆiš†ÁâÅ‹ Ã�'NP__O"‘àðáüð ¼ñÆ´µµñÅ_ ë:‹-âĉœÏoû[zè!Nž<Éý÷ßÏää$ŒŒŒ°wï^NŸ>ͦM›xýõ×yì±Çøè£�‚€�7R,9}ú4K—.eß¾}•íK§ÓüèG?bÍš5ìÛ·� 6P*•8uê ,àÈ‘#¬Zµjƾž®Ó’Râº.RÊ’² UÈà�{<ÛúìŒ"Ža†yåÄ+$œ¿u~Ë#™GXйà|å# ‰ð¾éÓSì!—ËQ° ÊB/ê´ëí4$h2šxÜ|œdWä¹ù]zŸv»Y/ÙÑ·ƒÇ[¯Ø×Ô¯áëɯ£T_ˆ«îßY°¥”Äb1Å0 êëëéïï§¡¡�“'O’Ëåhnn¦§§‡Ã‡³{÷n6oÞL"‘ ?ÕÇ4‹199I}}=†a iRJlÛ®,rÂ0d×®],[¶Œ±±1\×¥P(0þ|2™ óçÏg||œL&ÃÄÄ–eU¢w«V­â®»îâÕW_eÁ‚ôôôÐØØH©T"›Í’Ëå*–JÇŽcïÞ½²ÖÑÑASSŽã ¥$�322Âk¯½ÆÃ?Œã83"Ÿ³/)ee|¾Þ°¨+ŸÜV IDATùÍÐo˜ïÏçööÛgˆæ{üþúè_ã�ú<Ñþ«šWU:}L;?ɱÁc<[ÿ,MÙ¦«OPEdAÔ§úÈ pÐ=ˆÌIæÅæ1?=Ÿ-é-$›’ßk—BÊ� Ðãô019AÑ(òmñ[ÂBH›ÖF�YCc¼‘n·›eÉeØuvô?¹vmÕlaóȼGøêݯ(y%„øÏʳڳí,‰/©dÖ.&³ø¡“À£ƒ%mK8và>~” ùÔ9�°ûc>wµßE³Þ‰MÿŒòÂ<"áÿõ €¾æó»Ñß111ÁO›~J:~v±î �Ï&?ã­¯ÞâçM?gùÂåç�—J(ö–öò‡�?°6µ–®Ž®+ÎÂ�0æŽÑï÷ówCljü V³š{3÷²(»ˆ5æâõñ+"6�k¸ŒÆ™? ÆÜ1êU=í±v–d–�Ñ3Äkãs²jÔÔLÖ´¯áÛ· Àó¼JG¯QA‡ŽŽ¡ 4¡á W¹$dâš¶—+£$K$H`†f%¤ï+©$žï¡ŠQÁ‹rBQßE¡EÕ»8Ô‡õ'zžD˜¸¢È¥Rªb�Ïçñ<�t:�ïû•¢Ã0èêêÂ÷}Μ93g…¯7;`Ì£P*ú!ãö8ÇsÇ)Šdô (04ƒŒÈР5p‡u©†–°04=vc¤yö<¬Z‹;ØÐ²‹ˆh4Ç›yºåiÞz—{êî¡ÍþþU›2ŸGœ>MáÈj‡0pOœ€rôŒïà B f[bÚþ]¤ïBãÙ¥Lˆ—[Á«Âï̤ï_²ªl¶íB ÂéÓ8óç“H$®é>à ð›‘ßpwìnþ¨ã�*Z½��C…ClØJÛpÿêöEC}Ãyï Æxwô]êœ:žk~î<û—K‚}Ny•竱¯ ‘œ.£‹§Ì§èhí )Õù^î®0©O2<>LŸèãØØ1$°±éŒu²:±š¬™šÏUr0+aˆÅXºt)óæÍ£X,Î �eòs©À¹ž¾�#ÀtòwnK[MÓÐuÛ¶I$³ZßU à÷@œxÅ[/GŽ ™ ë±p„ƒ©Î'€JD ®Tª�,uéð…�!�Š¥Ëõ‚ºÔµ(]!b¢Ò†G¡h϶óù¶ nCYQ?d �P…äE[]8%­PbÁ’?%YâàðAbzŒ¥�K1tã¼4VYA•¡]æþ�žFPºb26ÉX!"xJ*òñ<ÇóÇ™(L÷ãÔÅê°4‹´•&«eišY`- ž�"ÆÚœµbh¶›¹·æ^¶önåÉ–'IjQÚ7cdx¦õÞí—3öV׬¾âíÆÆøèÏÿœ3¿ý-ßTöóçêÓ^«¯çžþÏiÛ¸ñ¼ýy±f÷ó7½Øds9“¢RŠSÛ¶ñÙþÏ„ããßÅG¾› *…4 ÌÁAò'į�y»óÙ=²›‘Ñžj~Šl< "*9X<ÈŽáœ<ÈŸØÂ=+ï9Ï´Y‚/&¿`oï^ÖÕ¬££³£Ò¶ì»®1)$ƒ 2˜ä€s7çÒ¤7Ñ•èb½½žt]CWdÇ"•$oäéë¡Oôqxø0­´Òoaž1� +°�¨ã‡Ó4�T*E*•ªê»¯ÒbîA+­enàABDQ0­8r*wAM ŽyãM#}%J•ΦŒRË>~¥ ãJ|û®ÚwS‘aHˆô$1#†¥[èBçÑÅ�ÒØÛÈö‰íø®�fj8®CÁ+'Ž!� GþDþ²ÉŸ#œŠèYSÞyø/ÉŠ,ÿñþÿH�i ÏõiBÌèºpY$HSùOˆÈìUJ9C#ƒ‚02Œ1^§è ½¡ &Ì zK½œ–oQKm¤û±’dì )-E›ÓÆ�ñ;±k숈ðöXh°X_¿žWϼʦ–MÔQÊR:Z6°w|/Ûû¶³¶y-iýòu¼ÎÉ“yÿ}RSûÈW 95n¥u�É0¤F× ”¢Í4éñ"K¥Ñ0¤Å0�Sƒc_§Þ{�ÖDMuɸ”èÆ•—Ky_%áyœxýu&††hŒÅ(JIQJš†­iäÃ�„¦¡€.Ëâ˜ã�Ðu†|O)c1|¥¢}t[“¾OϯMó£�b/ZtÕί��#¥#ìÚÍ=Ö=Üßy?†aàI�ÃùÃìÝ…r*Pü¢ítÔÍ$v ÅC¼yòM‡‹y±ãâö.�ˆRÙCÚgÆÎÐ+z¦[ï¦+ÝÅö¤êRWT°¡Pø*rP8’?¨?ÊÁÜAÚƒvº’]ÜißÉÚöµ1ã¦o�YmñùÃÆeÀl6K¤Óé jDΜ9S©$«¬ú|Ã0°,‹R©D&“axx˜ÚÚZ&&&*aÓxÉ0$©ë4™&®”xpÖ¹àúãÍ~G)¨ÛâqæYãA@IJl]§)cÈ÷Ié:ÃA@RÓ°4�Û •JQ5£e¡ A1 q¤$®ëŒ2ÈMLÐß»zQ¡tyì}:‚^j~‰¸g,ãëɯ92q„;Ì;¸=~;§ƒÓ¼ðÿ·w¦Áu^ç}ÿ�ó®wÇN$ÀU$%Š2eI”(;íÔ²ä´Iã4Mjg2m&ÓI“fÚɤ™Ô™L2ù”�N§ÓÍ_g²ŽÛÚIÕ£Æq¤ÚÖfKŒ6K²DJâ‚�p/îòîçôË{  p‰ ùü8w¸÷}�{ðŸó<ÿÿè?f°²º•¥£;<³ø sõ9þþÐßg¨2tyˆ�–šŽê§1ïfï2WŸcQ,u"öнì¬îäˆ{„ÒžÒ–__‚Üq¡�µ ‷ӷY\Zä|zžévvs¬tŒ‡ÇÞ¶“žÃM€Ý=ïîÛõþð’e­¸Eš¥HKö†R•â÷ù¤2¥:\%Õ)•¡JÞ’¦hK³ØYÌÅCwÏbùmoñA/cWIE”EÔE�Ѝ‰ˆT§½û[ÊB °±IDB ƒË¦ÐÙöYÌ*Ï0Iò8˜�¦w»„Y.LìÈF8¹°Ó:�J!Ë“,ºb¯·êÝv=•)lm£,…HE¾}‹€,ÛB¸¹-ƒk»yÞ®]¤¥[*@")Š"–²ò!�7­¾ß[¼›¸d¾œZbåX©›Òˆ¤Iê”ÐÊ œ»ŽëmiD"B£¯G§P´En«SQ•Ì/ì•bJˆÕBíV?žáÖ` ‹��|Œ¿]ø[Ÿ~œÏŒ}¦g#L”'øÉâOrjáOןæß`ÔÝxl|œ�ýÎoñÒ·þ‚ƒGŽäÓzWX6è+–Ÿ²ìÇfµOžU(P:|x[ ÔôĪ”üüç»ûnÒ0\¾Òù`˜Z$ÓÉšc½Rì­¬&.-Öyçõ3XGÀ-6½]š‘QWu¾ßü>/.¼Èc ùC´ËmþüÒŸsÀ>À±Ê1†v a[6ÚÓ|mækdK?:ò£”ý2m¿Íù¥ó´:-žm<Ëï¾Á~?#C#´Â%]â¤}’ÊdKXùÂuþ822bóNû.%—x¥ñ #É; ;Ùãïá£cågÆ~Ïöni’ˆÁð�€­V‹N§sÍ­�(‹¸3½“ýÎòvŽÍe»éîÊã¬ú+{ùÙdË÷S€»ü9wùs*G$Pq*ØË6†¾ö)Q¢)›h­±µ½ÊçÎÓEŠkªn›]po5YžŠ‚À±–Sbų3ϲ¿o?U¿Ê�Ú¾hñr•/¨¹«{S‘_Õ;’±Ž)éÒ¦^„”J×îŸéž§4M·g0wÜÍŽh_�ù*<̈7²¢çp|è8G’#Ò»(OUÊWÏ}•ÿùÎÿ$SÙ¦Ï;@â&DwD|~ð6ûÞ†ö$™Ìˆ’ˆy1Ï;‹ïðíÆ·9=wš}Ö>Š~‘¸S±+vSÝUűtAs>:Ï…… |}æëˆEÁ®�»ø“…?!‰æ¢9Yƒ�é�ñhõQ~îøÏQ*–nxýR©ˆ(�x'{‡ù¥yÞŠßb `ÒŸä¶òm9Žçzf Á`àµ*€›™–HÛóØWÛ×_IYUÝl8J_ÖešºIK¶¨êËö066ÝA }yKOëž9ªã8,--õzí”RÔj5\×EkM7¥»uջƩžç­òúŠã8ß _a¸ze¼R×;o~~>_¸Ez9G8Í…®FóJø í°MÑ.R´‹ì®îî=Nè…«¦p•P" ¬ÊV³Z"OÙJBŠ”’$I‚€ùùùžÉ¬eYÄË[LÝH¬½{÷’¦)a®Û'j0\‹qoœGåo.þ ûªû¸½|ûªä�’Sâ“cŸ¤�´yké-ž_|žª¬2ZeÒšàÕö«|奯`�X,œ[È[Mb�·Ï#>£S�ô$²,ñ&=ÒFÊÙ—ÎòÐØC<4øÐÁ·]Ím»â´»ÖÌf³üé©?eÖŸÅw‰§bÒ™|ír&’é2pÆâ³1  tw +‚×wçξ;)4 X¶…²3r†©Æ� Ãœ3ÇŸMý³­Y†b‡ÜA±\„ [ÃÔŠ5.ÉKœnŸ&nĸÚå½ö{LÇÓœÔ'ùÜÀçš ¦˜â…‹/p»};÷ ÞC©ÿú'¨¹…RºD�œIΰÔ^âbr‘RRb¯»—#•#a†yàÈÈ�F#·žXž~®T*4›Mâ(¶,”ÖìÙ³Û²z³.I–P¶–}•$L2‰FSÔEâ$&MÓ5 ãÊJlWÜm”훈„ʺ¼e/Àha!??ŽÃ¸ç!£ˆÁ}û°VD.­L¸ÒÓ`Ø*·Â?ÿ¼¸ð"OL?ÁÉÑ“åê ~É)qlðÑ¡ÕYì,òjëUZq‹S­SùësÔßðщ¦õ�î˜Kr!¡p´€;æ‚‚d&¡x¨Hô^D�k’nV]Èn³ßë•æ´Zë|p* )ì-°ôÍ%jŸª‘ަèDãíôh.5Éšîp~ú>ÙGä=Ö² ß ™Š§øâ7¾È`ÿ }ý}T‹U˃ø®�g{ôòÛƒ¿Mi´„gy¸Ž›ßlKäƒP¶´i:M^l¼H9+óó;ž¡ÊuQç�μ5÷¬üáHµPݲðb'æbç"­´Å÷:ß# rëªr;��Ü^½�¢,Þ”x8ƒá^n•r¹¼jëðJqÐÛÊX*_/"©ÕjmºšdcããÒ¦M™¼æâ“‰|«5i4øÞŸþ)RΉÖìùÔ§hḬ̈øÊ+Ì A¼|‚ìÝ»}ðA´Ö„ÓÓ4^yËuóš¦ìÿáflïÞÞ±”Ëk+pý}}¼õõ¯SŸ™àÔÓO#³ {×.j’fiÞ߈ೣŸ¥µÉ² )ò8º+·TµÖX–Õ;_óbž>Õ·î–n ó©éª®n¹WNõ7^§þ�Ç¢H¢Á®T8þoþ •‘µ�ë E¦�4ÜœêÖ=ƒ÷p1¼ÈWÎ…ûîç@ùÀšßa!ý~?ý~?ûö寷Å~þü; ÒÅ4÷ •ù$}ñÎ"VÙ"kdèXÏÄ8£d 3½áµ]·¯L(©@E*¯ê5VÉB$i=Ez’l1ÃvðúèT“ΦȂD–$ÂôÉ>~þÁŸçÈmGð ~Ϧk³Ç¯ÑÌ3Ï K/Оjs_õ>FŽr¦y†'fŸÀ \î.ßͱ±cøÎæ&u5íkηÎÓl59g�ãìâYÔ»Â.oºR­âç•®a0|h ã8„aØó÷›™™avv–R©D†øžO¦2,ËêmÁîÝ»·7bßÝR »MΛÀÃ#%%##$ìMÿ–É·ˆ´sµt–Ð×·‡Ñþ¸Õ*BJ4oÏÿôÈ?gr`O>±[©¬ùyÔÃ:ðÊ�¤ wïäpÿaó 3Ü0Ãþ0ÿdâŸðÂ¥øË¥¿äÄÐ ÝÁ«^Ì {yÎñÅg/^~½(h}³•¯z)W c�Î4�‹ j^�odÕÅævW Àª¨²o`Ï?ó<ÂË… °DÞW�äWt:ÕÔÿo2ßóÞꌞåÖþÁýyµÏ÷¶T9KEÊ…ôÏÍ?-x`ôÚ£m^j¾Ä7Þü·û·ó™Êg¨ ×Öí'Ö2ßJ+:n‡óÍót:nƒ7ßd8fØf¼<νâ^>1ö ËùÐæÎ xèy^/W2Ë2†††$U)/O¿Ì\:—o¦Ëb"ƒ©ÓS—^K¶CöWöo©zU¢Ä’X"Ô!ËÎl=3#ëü×/‘¨„Ÿí;ʱÛÈ«mÃÃȉñÿð&ß “ãìH/áÚ.ï–êü¿�þÕ±¿GÅ­ÀòâÓn·¯¹�ÞoRïÌ{Åÿ˜–‡¬K «ÖšÉÍŠÝHF%ÖÄÝ%"éõ^kÐC£{ÉÑ!ÓE]Ä—~îÑhI¾._ásŽÀZ¶‡ˆ“®H©·ê|uî«"`RNšW—á¦a ‹ãÃÇiÄ ž�-5÷ößË�7´îˆëm…ÛøüÇ>Ï—Î Ï÷®Ùƒ,ì©ìɇÒw”ï`Àø@Ûk�G5¾Ö~�vÒæàÁƒœ:Oª¶ɘ¦)~äs<9Žk¹›úê¨õ¬Î닯óìÒ³ô‡ýTÜ ÚÓ<9û$‡œC<Ð÷ù`›•û¶¦i Ú^›s�s´Z-´¯9Ó:ƒ9Œ‹qýAvwpPääØIã»g0|РeYض½Ê:FA¢¾rö+|3û&*THO®²†ž@'¹à}ê>~ùö_îÙBl–².ÓMÚºM… ’À¨?J¿Ûß³’:›œå˯~™F§�ê\®Î˲İH¦¤'Q­üs²"™iÎð¬ý,hxhßCüÚÑ_£d•¶m¦rw-ÉÈxüÂãü‡çþÖ …5ª®.¯ð:¿°¶‡m°![Ìr֎έ¤*µ¤ò57Õ¼2ù Ç�ãyºÎJßMO³ÐY Nc.p�ÓÓ§ùÛößòÆ¥78` X*b—l&ÊŒ{ãôõõѶڼÞ~�…‹ ÈXbù�¤�T’ªª2(©zU&J”e™þ!#ô †ƒÔZAoJm% �¦ÔTž{‹BÅ*OºˆÂ½ª Îòi½ž÷Ý�HJºD‹-‘O¯¼â¯zUØ`¬ßî_÷ã®t«ŒÝð¹é/öÓ_ì šºI‘â5“; 7\ V“BÑ”Mì̦$J~]$¢<ã‰#Z´¢�Ü ¡�¶†éΊ|bäœè?��ÍÒÒÒÆÇ$úùÍÉßDAÑ-šW—á–1èòé±OÓLšœ]:ËÓ�§iÆMbS’%Þ ßcº>MñHæ†êÁ¥ÛKdÍ ï.�d.AŽæóvÍÜsÒMIDAT¿MÚÈ+f:Ñ<=÷4óÉ<>~o d;ŠÀ<Î.âåK/ãïòžÀtˆÎG$³ ¥£%T h¿Ô¦°/o‰í­4Ö¸…pö�M2—àíòh~»É3�g(¶‹ ¾=˜¯Ã*¿0ÏœŒš/ðvð6ãbœ~ÙÏ€=À‰�Œ8#ìŽëСÃùè<‹³‹TD…ÝÎnî¨Þ�k¹¸¸ØÒîõ;ŸMƒáC\\XXèõÇ­´�I²„ÏùŸ#QIþ,d¯4•¿/µìAg—p¤s�hãáéˆ6mJ”¶åB I•*Mš½èº }¡iÓ^eå’Šôò”¯°Ö­F"B‘W»é+‘Ž(èÊU8ÚÁ×k,aQr¯}Þ,,Û\½þŽÄš²SæŽÁ;¸]ßN¢â4¦Óî ’ÜÞDXá D*�¶ ΈCz)Å÷Hæìª�‹ÄD#IROòž,%“Ù¸„àʾ¿T§dY†U´H朚ƒ?é£c�U±ˆgbHÁò—#&}‰;îž )ì.�.¤Ø5i²F†ïûºpˆÝ£»ñ=Û¶óÜjßçgwü,¿ÐËàîF0v£…XNxÚäÏÏ`0|H`wÑ\o‚̶mïØü�À�L•(ô¢âbâkÆ£½/,§YT¨Ð¤I™ z÷4Es•ÝKwÊ·¢×nùF""1J(„H-ñ„‡£+ÀÎl”TUqËö0öyù‘W˜,—²Wf™@Ä‚èl”·’(�Ž4ѻɅwgî‘çîr‘IðV€už2T³ RÉÞÅëz»ÛH ãâÒ9Õ¡x¬Hr1!­§¸£.�×:HO‚‚ÎkÒÅwÂ¥óR�ß;'½JhÙ+stÿQí?dìS #oΕêûI‰ 4iRÓµ-÷ÞÒs$t¯÷O (S¦#:«zûº,‰%J*OðÐhZrý)ß@¹ðC!‘x™‡‹›‹J -Z%PRmèh0|P9P8À£剳O ì¼½DTi=E”I=`Ê‚ŒøÕ˜¬³Ü<‚züôÁŸ¦Oôõz—·«øÓZãàððØÃ¼>û:Ó§§{•µàtлŸì“¤Ílˆ§c��vRâ31:ZîÍÖùÚó©Ú§(ù%“}k0ø!© ¨R¥N�&MúèÛ6¢'!YÕ÷'‘-òx¸åªœFÓ±;Ó¼R·Þ”¯š–n‘Y¹•‚­m*º²f;¹cuòŠú•l0l–>»�_ýø¯r⥌��c[6™Ê.‹9½áB�DR“5\é~ ,F‚�Ö>Êï~üwIT’{æ]íW²w9iIÀé÷N#3Émƒ·180¸é,`ƒÁ`à¶G")ˆÁ*“è÷›0 )Z«‡&Šéˆ%]B“÷üy©‡…EGv@CUå J(¤j€¢.®+pCæ…î}½Áð¡�^Ýþ¯½ö³õYâ8î™Ò¤I‹5j[îô<�0 ‰ã¥®ë2??Ïž={XZZ¢Ñh`YVO$&IB¹\F)E¹\fxx˜ÅÅÅ\®óЩHYJ—°D@AˆEÌy—jZÍ«‚›xÞM[äYÂ[øT@Dœ‡BJI’$8N¿Ô­4¬<ï›Íl6 ƒÁ`à5Œ¦±¸HÇø~NŽR( X,ör„»U7˲°¤d©ž§{ ²$!�ãž¡Dæ}vthÒ¤JuKÏgéìY„Rض�eÛxR2zä–eQ­Vë5Q !ˆ;êgÏ¢•"nµ˜ž›£Ýj!úsµ,Ê4šØŠ¹¨/â[> �XÔ‹”T‰19¶e»––̣抪¸a,\FF"bç÷ðäÌ“|íÜ×xlçcüÂ}¿€%­knu·ÕL6§Á`0 FÞI³É…ÇG¤)H´fò“ŸdσæFÑ¥µ&Å­¹9Þûã?FdYžƒ.:Iÿìgqj5\\bâ-™D«4åÜ×þ’ä½÷@KR rä÷ÿÒ/õîÓ­@viÏÏóúþÏdI²Üƒ û~å×q‹yb@@Ãi€ÍV3÷š>úð„Çõ -wD¥Þš„�&166–²òžHZi||æå<ˆÂhSÙ ÉùÀƒÁ`0Œ¼.”+™n³gä¾[Fþð0išnXi®Ëà}÷¡³ŒT¥¼5û<íæ"Ãær&qÁ.f!�¬C&3„ÎS8„¨LaYZ,om.·õe"ã»Çá®G~Œ»FïÎO¤ã-W×ÃëïçÎ/|¥aò{oüG¡âd4Ű7Â\{Ž��~¯Û±Ù/÷ãIï†Üò‘`ikMÂG(Bbãé\¦2oŽ·±{64;œüxéÇ©E5æçç7e ±mÍr ƒÁ`0|p`(Bf¸ÄÿÚu†_?ô“Œ×&zâ­Ýn_õkwÜ{/hM;ióÿ^û:ßOÞa§XD«ü°¥�Xi^õj¨11E±œÅ«W4ëŠNKæB³mµ)fËÿ¿ñ§„¢¥[¸Ú¥¤/W63‘Ñ-<í!µ$"�‚ó3uo €£ýG¹³ïN”RdY¶aß•fû×`0 #¯ �¦%Z¸¸ìöwóû¾€Y\¼xqK£[‘ú¹±†ÖšZñŠ��e�åYK,‘�àã÷’9Öh0‘ߎï8¾Fül×rylïc¹À[þþ=c笼aŸÞVÎ]“&VÅÃ…"$&㑉(éÒ5' »v[=߃Á`0ŒÜ EK¶(©Roà¡ìÞ˜…ƒÇÕýì$’%:thÓ¦Âæ|ò®Wðt¿N E „7͘¹%ZÔt-�Œš­Ü*F jÔÌo¿Á`0 Fn22Ú¢½i¯º›Iw($ m½ù¡�ë€BŠ�T§UqË“½Ê…ÂÕ..noË×Ñ©HWEÇ ƒÁ`0ð}G EGtÞñ×¥@�Lgd"#Ô7?)D£ D€’ /ó(P¸iß;&&Ò–°(èÜ70$ÄÒy?aUo½Âx³úùÌÖ°Á`0 F®K Šªø¾‰¿.%J,±DLî‰çâÞð÷ÌDF ´Öt;»¹§>i/飤J,‰¥žŸŸ£œkn�o$ÚÆÆÆXZZBk�ã8ÄqÜë t]·û¥R‰N§ƒëº¸®Û3½. œ?žh“C2ƒÁ`0~@`&r›•›µz#U]¥Aƒ�‰¼®¸8…"’))–¶(©Ò-·J(šº‰%òÇh‰B R‘RQ•ë>§Y’Ðn4HÂ0{�Åj•4MW™nÛ¶�b/Gn%QD†Ä�dI‚™6 ƒÁÀU"F«|òv›h� B…-Ú´)SÞ”�JEJ*R� øÊ¿©Û¼ë‰Ì¦hb G;´e›¶n3¤‡nHp6_x�3ÿå‹È,!SI¹ÌÃôGÔ†‡¯º5üö_ýgþí¿E;Êr‘Bà>ò÷Ø÷ùÏ�e™W�Á`0 F^\J¨mur,,Š EHK·¨PY3@‘‰,|ä ¶¶q”sÓ{¯&þ$� 1–¶P(†Þò÷ËdF¦3 ¥sïýË{¸o×£ WGAJÜjõš}�;Ž£ðû¿�ÒŠ§ßù�`š’]b�füÄ`0 #W>›ˆˆ˜ø¦ôÜÝ,´Ö„„,±DI”HD.ö ©òÁ¢.b‹Ü,úF œ…(um!¬„Ê­],‹P…øÚÏ=û´uuñ)ò„�îóW¨^•ÐÒ–¶°¥�ŠK…˜ßïû÷ŽžÁѹð¢×÷·^­ÆPµJ¦2^X¸ÄS¥çx,­q3b0 ƒ€WÐõወ(QzßìJºNKMGwˆuLJJ� ÙCXÚºüütÞ ×××G’$dY†mÛ(¥zYÅ =SånM�mÛ¤iŠçydYÆÀÀ­V‹™™™ +m©HóŒ_‘ˆ¨è !!eU^Õ«¨Ñ  C~_ rOD‹îø�ßæ¯^ÿ2�h�AfÅô[¥´A$"õ"¾ïãh‡NÖÁÓAP‘,mõª£)io ÛWþ ÛêôÄ›Á`0 #·‹8ì Kl,tôM.=98XÚ¢­ÛØÂÆÆ¦é¤ü÷â³X‹ûíãœÛ&I’k ,9ÚÏ_Ÿ;Í›þ[èT#¥ä.ùÓxÒÃ6™ÎHH°°”ƒØäÛÌ™ÈK,eá G;ÛÂGÑ`0 ƒ€J$’Ѝäƒ!tT‡Ù‡´$gÔi¾õηøäžOæý€×€_÷ëÌëyú¨"Š‚4Ii¨^êêG:ôë~Èò ¤#òÉd¡ÅåmkS¤3 ƒÁ`àßÕ ´©Páhõ(¿>öëÄin‘2`ô'®ÅAû ¿\þå\ÄY` ›=ý{°åŠO¶R5ò¾Çå ƒÁ`0ðž‚U`ÿðþÞ¤ìV'FûFÙQ[Ý/húî ƒÁ`0ø`åÀ„pƒÁ`0¶+&™Ë`0 ƒÁ@ƒÁ`0 ƒ€ƒÁ`0 # ƒÁ`0 F ƒÁ`0>¬;ì8Åb‘Ç›3dø;!Iâ86'Â`0 †Àqœë€ƒƒƒT«Us& ƒÁ`0>d˜-`ƒÁ`0 †0Œ´Á`0 † ÑZ“eÙ†ŸB ¥ÜRÖ¥J©ô91Ð`0 ÆBçÒ¥K”J¥ �”’v»Íððð*Á³¸¸Hÿ-}~išbÛ·VÊ\yY–±°°@¡PØ0õkå9i4´Ûí[’–¦)žç­Ÿ¾ï_óñŒ4 ƒÁ°¡,‹ìØ‘çÕAÀ»ï¾Kìܹ“‘‘¦¦¦PJaYVO„´Z­- ÀN§C’$—EŠmS*•6¼ÿÌÌ »víºeÇ¿Þq$IBµZíÍK´Z-Þ{ï=¢(`rr’¡¡!Ξ=K’$8ŽÃ¡C‡nºBðÎ;ïpàÀ�5Ÿ›››c~~Þ@ƒÁ`0 [Gk�R ­5QqêÔ)¦§§ð<Ÿ0 9wî÷Ýwßšê ÖšÑÑÑ-=V£Ñ P(P.—W‰­«U»ÂôVÿ•Ç¡µîÝ‚ àÔ©SLMM±´Ô X,qþüy|ðÁÞ6±mÛkª”óóóLMM111A\ºt‰r¹ÌÞ½{{¢òܹs8p€v»�ã8ë a!Y–‘eo¾ù&Ķm„¤ij Á`0 †ë£+dæææ¸xq¥rQÔjµ˜žžæìÙ³Œ��p©Ñâ̹¹ëªvK üÈ#'{UÄ.¯¾ú*�Nç}ï§“R244´JOMMqéÒÅe¡8F«Õ"Î�;G­V[÷ûÄqÌ—¿üe>ýéOðÌ3ÏpâÄ *•Jï>O?ý4Õj•S§N1;;Ë£�>ºáóJÓ”'žx‚ÇœG}”Ç{lSçß@ƒÁ`0 Ò;q†Y–qþüyÂ0D)Eš¦=Áñ�7¦ù½K#\ÏnçÇ yl�>Ã,ËB¼ï°ÛY«ÕPJ‘eÙò9 ‘Ò"IZ­•J…$I6aÍf“7ß|“ÁÁAöíÛÇÄÄO=õB~ê§~ €]»vñòË/ãº.Õj•—_~™cÇŽ­ÇÝÊìáÇ9xð B¢(ºê€Š€ƒÁ`06%|ºÛŒÕj… Ù·oõz�ÙÙY …BOpLŒT¹w~ÁÖ`Ÿn¢”Z#ôÒ4%‚÷]Z–ÕëûëVEÓ4¥¿¿Ÿz½ÁùóçCkE©TÚPº®ËÑ£G9yò$O>ù$?öc?Æ]wÝÅþᢵFÁwÜÁää$O=õ�FÇq˜››clll�Bô¯+”�4 ƒÁpÃ\¼x‘z½êõE^}õFGÇ8pà@oààν;8ºoôºcff†8Žñ}¿÷±(ŠØ¹s';wîÜ–*ݾ?€ééi¢("Žc‚ ```€$‰™˜˜drr’ÙÙÙu¿G©TâÀ�<ÿüóœ8q‚_|‘z½Î‘#GVã™3gxøá‡yûí·¹xñâ†}�Y–ñÔSOõ¶€äG~Äl ƒÁ`¸1º[Ÿ33ÓÄqÂ#�|š,Ëp‡r¹ŒeY«G÷ýN§C±XÜôã 3;;Û›¦…¼ZÖôXOÔlõ1®‡v»½fCkÍÌÌ ÓÓÓX–Å#�ú({öìáСCض½²’ª—o¬x»Fjókn0 ƒ¡K·"µk×.Çatt”B¡°F ®¼o÷ý………-‰3)%###[z~õzý– Àîq¬€B´ÖìÞ½›R©Äøøø/¾®³,‹0 ¯j¤}£â¼û½> ä[æqw3�õFïÊ  �ƒÁ`0€¼ï-Ë2�®ë²°°°®p»2õBk}ËM �Þôñ­b½ãp]—ÅÅE”R¸®Ë¥K—Ö='¶mã8I’ðöÛoß#hÈ=ÿ®­år•+óȸ\ ìUW À4MÓ4}9Ë2G)å(¥l­µÔZ @˜—‚Á`0 ?X”J¥kößi­iµZk>Þl6?çàÊã(—Ë›:'ݯ»ÕÛÔkZëV«5 „@¼B^Š+nà5`0 ô…e¡(�4 ƒÁ`ضh [ù €0Ìõå�¥€ZYì~A4kùŽÞòûFü ƒÁ`0l˜-ë¹Ö² €dYë­Úî*Æè,,ZV‹6—«„ƒÁ`0 †í-5—‹z�åÛ*ØuÝ·’¼Úç,ß,Voýh0 ƒÁ°}Å_÷­"¯&Ë·lùcz¥ [)å áw¥è3"Ð`0 ƒa{Š¿•ÿï AµRüqqw¥ð3¢Ï`0 ƒáƒ'×5ƒ^OØág0 ƒÁðá‚=þ?cciæ6mIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewZoomRange.png0000644000000000000000000000013215225434775017776 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/viewZoomRange.png0000644000175000001440000000254315225434775017772 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<àIDAT8��•_L“gÆýZÛ HöYD((a2”é()n.Ä Ü²?™nsQÀÍ?˜¹LÙŠ&&î‰É¶ /–é0:ÙÅ *uln&�¸UÝ.YI€:pX&³¬-(HKKû}»è׌9Mv’“÷æÍóžó<Ïy� 0OY€Ä€¿€Ÿ�Û@PÍK5ÿD6ð8`4€ø�Ÿj<Ï\(’�€<22";ŽYà9@¯<¦D X¬6÷õõMÏÎÎÊápXžšš’�NgxU,MMMÕœ>}:îp8ÆÕjõÌôôô 0¦T·Xéj•Ñh\e6›—ËukhhhâСC½ÙÙÙ9²,/,--}Àb±,2¬^¯WÞ½{÷8ðð ð(�,^®¨¨h¿páÛ©ÉÉ )“ÇÆFåÑQ¿l³µ…,K'ðpxçÎ�#===2Ð sÑhT!8nû>-µµµoŸ;gÛ�–¶(eß¾ú?Ÿ|ò©+••뿯¯Ë-Š¢¦­íìš­[·n�NE'¶Øíö;ÕÕÕ?e€NáÔR^^~ftÔ/57Ÿ ¥§§ÔÀZ`³N§;züxSphÈ-™Íæ®�7þh·ÛÃÀ+Ok�ÔÊùÚÙ³gÆ»»�qQ[€u€X¨äb`�^¯ÿ´««3ÚÜ|*|T)š VªÔ*€œ”””F¿ß'544ø�×�¥Š¥æÛÎTïÝ»÷�ÁÁßåüüüÏ€@/(>�QŇ _¾|yŽV«U € ¤Ü••”€0àéíí½­×ë1™L¹ �’À}"‹Ijµ:Ù…ê>WUZ­VˆÇãqåAî,á7n ONNJf³9ÈÒ”öçS‘ä———çÎÌÌÈýýýƒ@ˆß¯âH4½uþü7�ªª-¢ÉdZObâ2°â­ÈÌ̬¬­­Íºxñ»;~¿¿G–ÿÓ°€„ªoäåå9úúzã—/;"_oÏÏuYYY­—.u„º»�RIIÉ—Àc$8V©îÕ9À‹GŽ9‹ÅD›Íæ?zô“lQUíííNg·?�ÇKKK37mzÉ011Áþý~éèèø¸Œ'yNÚ-(ꛚšÆ‚Á ìt:e«Õ5�—ß÷vvvÆÜÛ}S¾v­+ÞØØ8VXXøð‚RÐBK­"1 %Š�%ÇŽ³ÖÔÔB¡N§›Íæ;qâÄ»À4°,///W¥R ÃÃÃI’ÜÀoÀ°Òµ X\Øâr¹îlÛ¶íW«ÕzÛçóÉ^¯W¶ÛíòŽ;Æ€zߥHbHò”ÌÒ• uÀ#Û·oÿÊår…�* ��›››b0–]½zÕ[YY™-˲ÐÖÖ8yòäaàà!ñ±$­†¢|Òvz 3##£HE=`Ô $IRÚ®®®ë”‹ŠŠ ---íó@“— A±^‰M²:‰, ‡Ã ÍÍÍQVV¶hÏž=ëÕjuÔçó�}€oèÝ¡ wíÚõžÑh\‰DRW®\)ƒAYøbÅÅÅšâââ Aðx<†ÖÖÖä?r/P–ÔÕÕ­3�iáp˜`0ˆÛí–¿Šÿ·LïjKt- «ýk™þ EürÙôIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/editRedo.png0000644000000000000000000000013215225434775016741 xustar0030 mtime=1784035837.876250108 30 atime=1784035837.876250108 30 ctime=1784035837.876250108 qpwgraph-1.0.3/src/images/editRedo.png0000644000175000001440000000125615225434775016735 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<+IDAT8�í”Mˆ’AǾ¯KíF®ÑB‹xpAéRìÂ^ºDuˆDºzP:ì!èÐQêàmOCr»t뼉G13PŠÀˆ" íÃ�Ô×w¦ƒ#ˆû.ívè´˜~ó<ÏügàXJ¶ˆ×Ô\ò @ýˆÐEà,àR`ã øaÁ6ÀxÃáðÝ­­­[ÅbѾ¿Õ!‡éÀI`XVY†www›B™N§¿jšvXæ™i«Ýª)Øà<àQë 0 ’ɤÇçóÝ�ÅbžV«•j@ÕšÙËÓ�ÓÀE¿ß3‰\ +n·{niiIët:x½^Ýétb·Û1M“Z­6ŠD"O+•Ê#à0²*ß܈Çãoúý¾�Fò0*—Ë]àšjÝ>Ù�õÍÍÍç†aH!Ä�àɾa²Óéˆh4ú ¸œ°Ï×wvvÃáPJ)e³Ù”¡P赦i�À³L&30MS†!ëõº¹±±ñ¸ªZ¨Y�—�;{{{ƒI6Ùl¶lkŒý{;—Ëý�RÊB¡Ð_]]} \N1cÝiWØ€y—Ë¥!èv»Cà3ðM]J­T*ÛíöZ"‘xÒëõ^°ðò4XÂápØt]GÁº€ �÷©Tj›±õ> µ¿ïõMƒGÀ§|>߃+€¬V«Uà‹ àðv*^ZA'åO¤nàpn’!P~Ζú7Y=�95$ãOÆ8*ôXÿGÌàð»¯IEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewColors.png0000644000000000000000000000013215225434775017336 xustar0030 mtime=1784035837.879146906 30 atime=1784035837.879146906 30 ctime=1784035837.879146906 qpwgraph-1.0.3/src/images/viewColors.png0000644000175000001440000000256115225434775017332 0ustar00rncbcusers‰PNG  IHDRÄ´l;8IDAT8Ë�•Kl\g€¿ÿÞ;sïÜßOÆã±ÇqÝ&NB¡T4›"  ¥ˆR‘J¥›n`Á‚- Ä"b…X TX!RHÓ¤-A}ФMMó°cãG<�3¯;÷Þ¹ÿƒ JÚZâHGç,þÿ;gõÁ.qõÇy1:S·Î7L¤uÁ¸ä)v'$¾°Lç¹'|m'|æ´4÷ßÙ Üúw*ÚíN9·?x¬\É=JÆ®Ki2ƒ¾ìêá[§Ï…úÅ%½ÈÿüÊ)ËËÔý‡÷LåŸ.–½/ùžS² Ž­ CSµŸhöô‘£KƒŸ¥°´Ð0é‡öG OÛ#þLñÛ“³¥ŸŽ�ú_ôÞ‰ ꥎ­1‚²#ϲ<×v{í¤vnaø¯fÈʳ_p“W•Ùuã—žr|"øN}vôGe+SK~Û;¯v‰ûŠt5Æw!Øç¢‡š45^4doœ’»ÑÔ�/hÝm~}\Xùñü£µÙÒËS…š^ŽEûb‡^WÒr«ÁN¢P©"Ž¥¾y[ö»‘NâԘ߿�š]ÁûNxc¥‰üwóÁLâx"ÝW!úô[E‡Û‡sXór´A5ƒXGJѶ-à#`ûídÄ©me«#'‹ªß»^;ZüCæ$ 1NiÆFLœYAm&K9°J#ŒY­œx W£�y·^¢·ÙGß~î«�ÝÆÎxåÂ3òÁû£û¸õËæ)þº|ŒÞÀ0?½Àt%¥à‚Ð-5FŠ9áΔÄþ¹šxàØ8ùýEvbÉ`3üï§txœÅÆfÆñ�z,söúZ‘è¦Dmk–††ø`ŠhŒ4h¥ÑÊ ¥Â¶…˜™òª“eûÉN;yÜ÷ä‹¡üù冹$VñÄ´–]™s„òÆeƒ¹­×¨_?ËìÚ Ìógò¦�N5FÞM…–K¢Pj¥ dFM:_û䄸~5Ï^ÀrÒ É@¡u*)ª6_^æ¡O½N˜M˜ËtØc)´4 5JiŒÒ¤‘2ïߊ¯]K#®PŸßïÔËy‘Í‹ãÀA`Íi]xÏJ¢¨—†*NMÑת6¹D Z6´²LÄ”ÁH�j®­DƒÓç£Ë¯,™¿M4f+kÕNLó•©Që¹ õv¨“…†ÙLm@Ù¿zÞØÂ*™0pÍáʈý‰JêXfÓðÀšÍ“ý*õõCÈ7N2XŸB¶Dµ4̪éÕ¼5ŸÍPUJ‹+ëúÎóWy¹rØøŸ„.­šè¡@wG]ñÈ´oï9’fÄ1eS×ÛŸçö›ÇYoxÄùU‚êºp,åØ˜Œ–Z¬ïèøwï˜w/orFjÞz÷ÚÍÄ1Û¶VIÁanÜ·‚¢ƒ@iúqJ˜ÙB�]#7y ßï‚ÒB¥Úl´uòÂUsóÅE΄)ç�5@ݧÍ�>éNO/‡¹mk5íZ¦”µ¥íMá�/Q¨ÝbdäF)ÂHË…Û¦ûüUóÞ_nr¦“pxHÄÇxÞÊ;ï��ßÜ7f}.ȉ²çàeD˜hÙê›xqÛ4/®pe¥Ã?)¯7€ð®�Ä.—In9Ǥçp`¢ =S3†l+"Zí°Kš‘dXZÀð^Ëí¾W« ûA½û^é)á~³üý±Ë+™ºIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemAlsamidi.png0000644000000000000000000000013215225434775017604 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemAlsamidi.png0000644000175000001440000000177715225434775017610 0ustar00rncbcusers‰PNG  IHDRàw=ø pHYs  ÒÝ~ütEXtSoftwarewww.inkscape.org›î<ŒIDATH‰í”ÏK\WÇ?÷ýpŒO§“Ž1šEŠTÒB‘”7",ÒâÆ…‹.²ýÅ…ÿ�‰ 7 ÒMwN NÈ„,‚˜Ž2ÓÉÄqœqƾ_¾ûºè2¶)mÓE7ùÂãÜ{ι÷¼óã~áþoˆ¿Ù7#üƒ�hÒý•ïghU2NÕ§)é+¿V Лô`(»8J†B)#@/ðÐ@8¢@ØU_ú€sʾ|¬ÎéêÜ6ð3࢔—;;;ï%“Éúþþ¾|þüy°¸¸ø¦»»ûÇt:íÞºuë10|·°°ð2—ËétZ& ¯§§çñòòr1ŸÏ™LF®¬¬Ôc±Ø=à2 7R»<11q³¿¿ßÚÞÞs¹œ¸qãÆ…ÞÞÞk¦i¶˜¦ùp ø|jjêJ©T›››A*•rãñø§wîÜéÚÙÙ/^¼àâÅ‹ÖðððMÀÐT–ïûVµZåþýûv"‘�®ë´ãyž©Êx®R©�J¥ÂµµµúüüüŽëºa­VciiÉ{ô葞絠7£I)5Çq˜œœlÕ«Õ*RJ¡iBM5¿šÉdœÁÁÁÖ���ØÁÁÁÕ™™™¼çyô÷÷ëcccF.—# C¡/´æÑªÕj<|øðpuuÕ1 !Ñh]×Û�k€5>>þdzzº´±±´··kÑhÔ*—Ë<}úô×ÙÙÙj†gÆÔPOJéW*êõú/{{{ÒuÝKÀ©”’Û·o_ù>™L¾ê´,ë\$Ñ …¾ïûÅb)e-“É_†áj$�Êåò+Çq®8޳S«Õl×u»óÙl6ÖÑÑ¡Y–) çûúúÚÂ0ÔŠÅ"[[[GÙlö¥ã8ñ“““¬mÛyß÷¿°mûpBeñ ð-ðµz &SÁ£@›Z»Êf6~ ¨«7qÜtv Øö 5E±»wï~3777R­Vñàßá7�³ªþæ TIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/qpwgraph.svg0000644000000000000000000000013215225434775017046 xustar0030 mtime=1784035837.877146934 30 atime=1784035837.876615721 30 ctime=1784035837.877146934 qpwgraph-1.0.3/src/images/qpwgraph.svg0000644000175000001440000002407015225434775017041 0ustar00rncbcusers qpwgraph-1.0.3/src/images/PaxHeaders/viewZoomOut.png0000644000000000000000000000013215225434775017511 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/viewZoomOut.png0000644000175000001440000000251615225434775017505 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<ËIDAT8��•mLSgÇ÷–BQ‹�–2|a�" Ù¤¤83�!¾Dg6£Éb6—,qâ·%3‹‰ûB2Ý—-!‹,›„—“Q NÁŒ•0µ€TÚ"ÐPè‹LK¡”öÞ}èe! nÉNrò|¸ÏýÝsÎÿœsþÝTÀ* 0Àj@¢@pcÀK ÈÂ+€‚ÕÅÅÅGN�:UµsçÎ\½>/ ÀçóI< Ûl¶‡wîÜiî)ˆò«ÀiÀk¢(»téRÍÙ³ŸÌÍÍ ^¯—P(€N§#''Ahkk‹\¾üõ�SSS×€a`~%ðR¤Õ ׿:xð ®··—ööŽX__ߘ×ëuÅãñ¨ÑhÜh6›ß8zôˆÖh4âtö%.\¸pmrrò*0²R Öï_¼xqìÅ‹¿äë×–÷îÝ뾪€RàMà]àãÒÒÒßš››� ȵµµQQφ•¢-***úÞnß¶Ë•••£ÀGÀE¸ Å3�\ jÛ¶m6‡ã©««S>|øð°KL«�¢ãÇ�WI’$tuÝ‹ÛíöŸ€?��d+®S´x:<<ÜØÜÜâ7 ”••mLi)ePúòòrÃôô4~`jݺu»t:�1‘H¨e9)¸ 2�D"“¡PÈmµZ¯©9ÿáæÍ›24ͦå`HöçšœœõêÙÙY‚Á`ÐÛl¶½^Ÿ�F…D"€$I„Ãay|||ñĉçFFFžÎÏÏËjµZ0�©`iaa�ôôtT*•ÈOžý©R×Ô@$ Ì”••ÉÌÔ333LMMyS/&€ÉÞÞ^oeåþ­{ö¼·¾±±±(�×>å¹¼LAôÀÉ“'ß ƒŒŒ<��Åbc©eHг³³’œN§|æÌ™iàÉ]¡4$ÛmÉÝñÁ¾}ûº¿ÜÐÐ [,P¡JN ””””Ðét·o[UXX¸Z«ÕšûûûÕ‰D"“d›é�`uuõguuuæÇ�‡…›7oÎZ­·j�na4 0[,–s--͇ÔjµØÑaÇdz�FC ”îÞ½r»Ý¾X,3™LyUU•‹Å’~ÿþ}¬Ö[ õõõ߆Ãá:à™° Zf±X>¿qã—CyyzÑårÑÓÓƒÃሙÍfõÖ­[„ÜÜ\´Z-¢(�Fñù| ÑÙÙlmmý.�6`APÒ/·X,ç[Zš ù¢Ë墻»›ÁÁAÉjµÞöûýÏvïÞ½Ëd2fei3DQ%<þ<>11p:�÷ÆÇÇ­@�"p em¾UQQñeSSã±ü|ã?P§Ó)Ùíöv—Ëu…äžÍŒJv"0LÏ”3²¼kà¨Óù°Åh,H÷ûý8œN§ÔÖÖf½<fIŽû’/µ¦,*çR‚r)ËãñlÑéÖnr»=BŸÔÑao÷x<ß(З $¡@•tã,û¥š ˆz<ž‰@ `\»6«à×_[[].×ÕePi¥ÿË–6ZÉ0*5UÒÿ_P€¿ •Eä©3nIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewCenter.png0000644000000000000000000000013215225434775017315 xustar0030 mtime=1784035837.879146906 30 atime=1784035837.879146906 30 ctime=1784035837.879146906 qpwgraph-1.0.3/src/images/viewCenter.png0000644000175000001440000000234715225434775017313 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<dIDAT8�••_L“gÆ­|ö+t$Û‚M„BbÆÐ!Ó´�,héV”NÓ«a¼ZâÈbÌ” ÞM½’àœ1Ù4âf›ÀbtÀ"±À–&`‘À@ii�™ì +_ûu}18‹ËNòÞœsÞ''ç<ç9:Þl:@¬ÀáÂ@HüÇÿ”€z Ød±XšÂá°zçN�j6››€M"¦¹¯ÙšU@ À:Qåî ¾ùÂf³Éóóóz£Ñ¸Ñçó½TQõO²Òô@ðž,Ë;>¼o÷îϬf³yíää$Š¢�™™ÉÜÜ_KwïÞ�ºråÊÕh4ú 0ü h©€—A·Øl¶º‹¿Ý%I’ÔÞÞñÂï÷Ï>}úôO�N‡Ùl~·¬¬,Ëé,7D"õøñ†###Í${ÿ ø²É€­´´´-žŠ_¿þcÌn·ÿj€2ñj€£Û¶mû½««+æóùâEEE­€M`¼Öëõ&“éÔðp@½v­%f±X<@`&9(ƒxéÂW‘““ã½}»;æõz–ÒÓÓ�ì•sÓ‰äÊ'N�{‰ââbà2 0o‰g¾LÀåp8úÆÆFû÷ïŸv,�^T‘8wíªÉ¹yóÖó@ Ð Œ‹¬Ô`0ì“$©° ß"0ÒÛÛÛzÿþÈóššê\àc�e(´X,M--Wç'&ÆÕÕÕ!`§¨Luuu�ÙÙYÍï÷kn·;8DÌì|˜ŸŸŸ?öx<_ÔÖÖ¾�F�æççË>Ÿïkཕ••Ô½{÷ô±±±o�> NçVVVt¯×+Çb1cccã…Çã±µµµ9€Ö†††¾p8œH§Ó¶gÏžIÛÛÛ¯•J%·¦i¶B¡ ¬®®^ÚvwwUEQäH$R¼8¬‚ª›››)§ÓI,ËÞºuë§@ ðÝòòrª§§Ç><<üv±X¼j·ÛõB¡ —J%¸ ëzNQž?^ê³ÙlÚn·“L&3¢ªl[–eÓårÑØØX~"kkkQ·Û�Ãá8\ljjÂårØgcc£äv»…>ït:å+W® (JùeÜjJɲŒÝnG0 èr¹TÃ0(—Ë I’D]]À…ÁÁAoggg½ªª˜¦y±»»ûM¯×ÛtxxH.—ˉëj5€ªª\¾|ÙvíÚ5OKK‹k||¼7‘H°´´´ †a^ºtIöûýoÌÎÎv(Š¢ÔÖÖÒ×××>33ÓšËåjfggw‰D8°X¤nŸÏ÷y.—3÷÷÷ͽ½=óààÀ\__7�À&ð%ði$Éîìì˜[[[æãÇ�Íû÷ï—-ÆE"stt4|Ü®5'2(‹D£Ñr(ÊK’TŒÇã¿ÏÌÌ��ßø|>&“±=xð@‹Çãé‘‘�®ër4MÎÍÍ},?‡@ù@(ÊNNN†€_€màW` (ƒÁL0  Tnoibb¢†JH rl�ŒjS’$‹›À"øS C¥u¤� Fغ £L¥� =k¦ZÜÈçóH’d) <Ê"`A€Yì39ÙÏÊ"°ñ’8©ÔßßߪiNáp$Ò´6!æuγڰŽS©Ôî£G�RÉdRg˜¡rŒ³ü]q�]]]ŸØl¶��.ÀÉŸß‹,ÖùÉT˜P/ì<•óÖù—|À[jVé\þÉÕ½`/×¾eIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemActivate.png0000644000000000000000000000013215225434775017621 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemActivate.png0000644000175000001440000000112115225434775017604 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<ÞIDAT8�Ք˫RQ‡¿ããÚƒBäE‘ äÍ „�œ5n,Ž�…³ i\�3h"5> 8nAXhM⌚ ×Ûƒ®Ç½÷j Þnâ»Ñý`OöZûwÖYëǂㆵFÎt®ŒÏFÂÎ'ÿø~|~fÖÃÀá-àZ&“y‡¯h­ƒ–e™@ Ðwçm¿ßÿüXTù,|À…T*õL)edŠjµú¸tä/Vf Ø®T*ï´Ö‡‚Zkév»?£Ñèà«Íé 8 �<Ï3""Æñ(wBÞ‰&ÐoÄ(k”·¬”IEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemPin.png0000644000000000000000000000013215225434775016607 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemPin.png0000644000175000001440000000175215225434775016604 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<wIDAT8��•]HdeÇçc޶ËF²FR¸^l[‰ÓP퀂3ê…”�Ý„IAÝËF….D”ݬÂ(8(£xáÌ€wÒ´3äÝ9Á3.È8âˆ�TŽê9OûÎ6k~ÌÞ‹s~çÿüŸ�Wª MÓ´òY®øP«¬ 5À5à8ì‹~ W«0€—ü~ÿdžat�€U™ÅS!"U=Jio"‘ø3�þ¥ëú}à¶ÊB;û~UŠ5M3€À­ÍÍM«­­í¹ééé/u]ÿxå<åW‚ôYÀÓÑÑÑÛØØX»ººJggçõP(tÏ4ÍóágÒÕ p©³ÔÝ~¿?¶³³c Y\\”H$"¹\NfffLÓü¦lËÖèóÀ»À[@ðBšÏçmÛ¶¥P(H6›•……‰D"²µµ%¡PèÀ4ÍûÀ«€qìÞ‡ÃÉþþþ‡ÀWÀg>Ÿ/^†Èþþ¾ìîîÊÆÆ†„Ãa™ŸŸ—õõu™˜˜øÛ0ŒU-´J� 855õûÈȈ?•J}×ÕÕ54;;ë«««Ó)•Jض]B}}={{{v4-žœœœz<ž•}ÿ ˆ2þp·µµõ‹P(Ô©ëº>77çôôôè ˜¦‰mÛ‹EöööÈd2 W’ÉdØÖ�9>¯xu@·×ëý9™L:étZ†††œ••Éf³òèÑ#YZZ’`0è´´´$€÷U-®)µÚSWÀ U¸OÜnw¦ tâñ¸,//Ëää¤ÓÜÜœÞSBžÏe‡ ¸YSSóÆðððËñxÜ.•JÒ××§Åãq'ŸÏËøøø/©Têð+ð�ˆügT(-/™;µµµß'‰Ãl6+cccG�'“L&�T*å´··ç€ÏUúÆ…+ \ܶ,ëÛX,VÌårŽ\.×àS幋ÅNT+6ú¥`åÑ-˲¾®„Z–õð¦‚t{½ÞŸÖÖÖìÞÞÞ‡jˆ\—,-4à:ðA ÈoooK 8´,kx¨­í.ŸÏ÷ãèèèojB­ËÀ:pÓívßK§Ó§;–e ¯)h¹×u/£;À;jü/´B++njjjq¹\w3™Ì&�þŽ+«­º¦¼œNEÄá‚ÐDMÓtào¶c 8WÝk—ÅÙ‘®¾(«‰ÇúiTò³ IEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewZoomFit.png0000644000000000000000000000013215225434775017464 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.879146906 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/viewZoomFit.png0000644000175000001440000000265315225434775017462 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<(IDAT8��•]lSeÇ=íé°lÝܬ~lNdÌ`¡|¦‹²˜Å ^¹„¸�8nt*w‰!ÑÄ`†�&ÌãjÃ˜Ž­fv.ng´ ší¬c[c¥–­tkO{N½ ˜eNM|’7ÏÕû{ÿÏÇû<:þÝô€ °å@ °€8F€1`HiÝ?uèJ`ãš5k^ݵkWÍ–-[ŠV®\a¸}û¶Öß?u¹\���wæ�8�þ'°X%ÂëühÏž†EQtáp˜X,F2™DErsshoo�}úég_OMM�~æ—?Túæùóç>q8yýýýôôüœìëë»;66–L&“ZQQvû†"‡Ã±¼¸¸IT÷îÝ{úÖ­[‡�á¥R� ì8pàÀØÌ̽ô¥KÓ555£ÀI³ÙüƒÛíNº\®”Åbù hZ»v­ûòå˪ÏçM:t(.‡@ñRjËÊÊÊŽOOßѺ»»ÒµµµcÀ¾üüüÏ=Obdd$ÝÕÕ•>uêTª´´ôÐPQQÑÑ×÷‹ÖÝÝ•v8^`«~8 x¶¡¡¡qÓ¦M�º\®ÔÑ£G¿ÍÏÏ×mذÁ&ŠbÁµk×T¿ß¯Äãñ¸¢(  òò­¨ªªZ ÝÍs»ÝÃR�°~ýúâ™™{HÒàlNNNY[[ÛæÑÑQ­®®î8à4`Mssó»•••›wìØñckkëÐûï¿WôÄ�g-[¶ìIa‘bÈ.,,‰Á`Pu:�ϯZµÊ8>>®çAϺ€ï�ßUU5ꛚš^ …BÂÜ܃«ÕZbàï¦% 4M`xxXK$ÌÏÏ/¬€NQdYfrrRK¥RªN§CUUR©Tj¡â4 ÷'&‚ £ÑHii©ØÐÐÐ-IÒ\AAA¨ÞÞ*ãñ¸âõz•}ûö¹ÊÊÊĬ¬,"‘{LMM+V�[}}}ÁmÛ^~¦ºúEóÙ³gïìß¿ÿ‹ÊÊJ[ggç;z½Þ‹Å˜��U�;Öëõz{¢Ñ¨P__ÿB$A–‡çE[œc¸ÓÒÒÒ�ÞO¯[·Nؽ{÷+Š¢è¼^ï¯ápX³X,¢ÉdƒÁ Þçóý‰D¦ª««_ª«{;Ïï÷30 ]†.¨±ÙlíGŽQÆÇÇÒnwwº±±1d4ω¢ØqæÌ™Ä‰'’ÙÙÙ]Àñ�;wú&'ƒZgç�éúúw¢À H·jlv»½±¥åR­(ŠÂÕ«”—?…ÉdbvvFkoïˆú|¾d,SW¯^­ß¾}»Ùn·%œÎïÍÍÍ_E£Ñ“ÀMÝèsv»ýƒÖÖojW¬X)ȲLoo/�G±ÙlâÓO—ë 1™AÓ4E!sãÆ º»ÝwÛÚÚŽÅãñ @Hè€`½Ýnÿ°¥åRmq±E�e™žžü~¿æt:˜žž¾YUUµµ¼ü©ÇrrÌY‚ è"‘?RÁ�$Iîññq'Ð Ü2cóÙ�7~|ñâ…×-ë_PI’´ŽŽŽvY–›x0gWÖLt0Ünf|,Sü¿ýk’4Ðbµ–§§§ñxÔj5uttdËå2ZëT«ý~Ÿ››šÍ¦T«U´Ö�V«õø“+à}†³ÍfSR*•¤R©�ËåRÁ¾ïcŒáôô”jµŠ1& ¸N"WDDæææ(•JÌÌ̤‚ƒ `~~¥Tr¿G‡žc­M(¥F‘¦×æÒ'2ŠD‹s¯�GŠãÏóxxx Š"Dä‰;k-¾ïÓívq]wzðÚÚÜÞÞR(Èçó8ÎÓÔ ¸¸¸`kkk2x¸Ôz½N½^ ÓéÐn·Ÿ9¨ÕjT*•é'µ··‡çy‹E<Ïãúúš0 ±Ö²¹¹ÉîîîtŽ“ÒZ³´´ÄÂÂQq~~Îöö6…B�ýý}Œ1‹¤�•Rh­ÑZ#"ôz=ŠÅ"ù|cLª¼’�^¼;Ék•ãyž2›ÍÄb±°ûýþ𸟭~'£P(X«Õ*ˆj2™¦iÓéÔ¯×ë�À5 É JäYi]׋™Læ°”T Ã0Bˆn·kÖjµ;à<¬2¼ƒË| àR©Ðét‡¢ÕjY¹\îHG³q›'U6›¥Ñh`Û6ív×u…®ë—óù\c»©û^VU•r¹ŒeY,—K6› ½^/�UxX�¦ihšF>ŸßÙÒ!íåwèüÇÀ¾Âû* ÌúQ?zŽ`í8Îh0¸Õjõàª~Ôx<öǹu ä p ÜgHL¨7à/ÑUÇý!¶ƒEIÈ!LÚŠß§w,õ~™íÅåâIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemFind.png0000644000000000000000000000013215225434775016741 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemFind.png0000644000175000001440000000252315225434775016733 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<àIDAT8�•”]LSgÆŸ·ç=¥ZJi»R´ŒbF4t8–‰”¸«Ý,“ƒN 7\ˆá#1Kpɼ€.[‚áÂÍÄxá¸X51).j„YŒFÉø0ActØ¥ÐJ�ÐsÊ9ïywa1| —ý“÷\½ù�'çù�?`ð!€"¼À Î9Ûê"PÚÞÞÞÕÔÔT&Š¢i«‹†a€RÊ ÃP[ZZ~–eù!Dáœó­ÀÅÁ`°<¼zõJL§ÓÐ4�¯Â8ç`ŒÎ9)(( •••²Ãáð˲üå}‰E]×I,�?÷z½qJ)3 ƒ0Æ„••QQ”Y–óÝn·÷ôéÓ9Š¢¸ä� !ìßRSÄd2Ál6ÃëõÆëêêž ‚À‰„ôìÙ3w2™,,))Ùì™LFÎ[ZZÚ`À€$!Dß§`2™ Š"(¥L>??Ÿ÷ðáò`0X÷;=Ñ4 „X,©¹ù@Soï©ü±±1Àqú&ðš‚L‰DBzðàAù�_vèÐ!éÚµk+‘È•ù¹¹¹”ªª555î£G¿¥‘È庎ŽÂÐÐPÀ(!$Å97VYï,àœC×uÓìì¬'<(õõõËgÏþrÏf³Ý®©©¹ët:ÿ¤”&Î$®_¿®�93P]QQñ€âÚ�ëÀ™LƋŊŽ9â¼uëÖÊèèè£úúú©ªªª—~¿?YXX˜Ú¹s§²ÿþDwwÏ£d2Iººº¾PÀB!›Àš¦EQr‹ŠŠ¶û|ÛI$re¡¼¼ü¥ÏçKY,=''‡QJ™Õj5vìØ¡,,,LG"‘7ûö58ø˜¬3öö’eÙf·ÛóEQÄ‹/R6›MaŒMÓ„ååeQ×uAE.I’@ž�}ºäv»©Õjµoì‹04M¥eee^]×s9çÐ4­`zzº$�Û)¥Œ1&Äb1·$IÄ0 @�$IÐu�«ªªؤ[*•J©ËËËr[[ÛÊÕ«W¥t:� …\ªªVVWW+V«•†UU‰ßï7ž?nଯÿ4rr*£ëúkÚFðL__ßO6›­\Ó4W:�Þ³wïç¡Ã‡[hwwu:�jii©*Š¢¡ë:ÆÇÇéÍ›7M���744äö÷‡gÌmôx.pfµù¦²²r|bâ±qãÆH¦¸¸ø€  jllœ™™™fÑhTu¹\¿hPšUŽpγ�·š€}¡PèD8Ü_íñx022òffæÉ³Ù,ÔÖ~b¯­­•îß¿Ÿéí=õº§§Ç½¸¸˜ìèèøÀeÐÁ9w²p  À—>ŸïÂÉ“'c##QmjjŠOL<6.]N·¶¶>q8#ápXŸœœä²,ósçÎ-èÎ&§dãbʦXlð€mx»Í )1;vì‡æææ¢Ý»wÃjµbxxøukkë÷~[—xCzSVz€BîlùÙd�mmm Ñh”Çb1žH$øààà4€ªuR¯�ìBY!„¬jD°ÞU@äüùó`Œ}Gq3Æô‹/Þ o™ø¿NöEb6y{ggçÔ®]»~°@Þ¦oüfM®l‹þ�þ®q¬a6„uIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemDisconnect.png0000644000000000000000000000013215225434775020152 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemDisconnect.png0000644000175000001440000000221215225434775020137 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<IDAT8�¥•MHgÇãìºVt76kG÷Û@Ñ@i$´¦KõÒk*Ù–^ZD¨"9xh‚¥7„ n)E´Ú]‘BÍaÕ­ëîZ¿Ò­âú‘hcT4®ÎLÎ4Sµ9´/ Ì ÿ÷7ϼÏóü�ÿ¿ýzÕKÈÌšàP^Ë25½ iO�4 a&àŠ�C`Ødc$­x(Dàøøx¢‹Eà†Õjm …BÏÀGÚ‡LþÌÜ0™L-Á`ð`~~^‡ÃòØØ˜ÜÜܼ ¼XLqqÿçåååÖìì웺»»E`ØÖ"�׀ꮮ®/ÊÊÊrŽ��999áèèˆt:}¸˜DØn³ÙÞ±ÛívI’„ÂÂÂk¢(–F£Ñ ` xX�{MMMü~¿cmm�¡¡!5«ÉdRÝÚÚÚˆÅb?hGZ" €û555ËÃÃÃJ"‘P‡‡‡¿ßŸ>|ÀÝêêêÐúúº’H$ÔÖÖV¥¤¤$ ´Ÿï�¡G¬jmÇb±”¢(·Ýnw^QQ‘àt:mf³¹4�>¤þþþOÓé´8::J0\™ššú¦�ß�@ÑÁ:üØŽÇã)UUÿ†»Ýn[ffæíÙÙÙíååesNNŽcffæÏ���¯€ –ƒçÀ™^AFð¿Â}>Ÿàv»­‹ÅÓ××7±°°ðprrò[Y–CÚùŸ]à\á�ãñxJ„[�'Ïëõ .—+WÅ‚‰‰‰ ,Ë?œ7Å¥uX‡Ÿû±Xì©(Š%^¯÷šÛíœN§Õd2•D£ÑU`8⟠ôR° (ÈÊʺ[\\\ …–�B�Çcóù|‚Ëå²™Íæ;†R¼¿4¨|ÐÓÓÓV__ÿîââ¢088ø£¢(.—ëbµ\ ×Á€È^Þjlll©­­umll‹E ƒßÅãñ‡ªªÞ¼¢�pEgv ¨îUTTÔµ··ßÙÛÛFFFÔÞÞÞ_677¿"ñx|]UÕ[^¯7Ïçó ‡Ã&ŠbéÜÜ\ H#·•�VVVΉ„œL&•¥¥%µ­­M­¬¬\>äÜtWû¸®®îÑÐÐ�‰DÔ@  ÔÔÔü¼­ñ0i—=??_ÊÍÍeYfuu•H$ÂôôôÓñññ¯�IàçFôø)@KUUUÑþþ¾ I’È5fÿ Ø�Åb»©Tê•��ÙÝÝ=êíímç…»éM kÏ£�@@Q¥I’$Wggç7À¢®Ó'A>ðçFcÑ6oΛ Íe³µÜ¼ähД®Õ Ü8f2xÑ i-Ë—À°OM§F­pAxå`ü/ë/M�»ê�+VIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemPatchbay.png0000644000000000000000000000013215225434775017614 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemPatchbay.png0000644000175000001440000000346315225434775017612 0ustar00rncbcusers‰PNG  IHDR szzô pHYsÃÃÇo¨dtEXtSoftwarewww.inkscape.org›î<ÀIDATX…Å—]l×€¿¹3³c³8¶0ØÛÁ`(ÛP³Æ€ •!qòÐVª–¤ªTEU_[Ҕ䡉ú@¥þQµ*R¢Ò*R¢”V)Å &PZ¥Ø;…`ƒXÛ°{ýo“õîÎìÌÜ>˜XÆ^#þªž§Ý3sîùιçž9þÏ¢ÌV”VV–ïÿéÁc9çÙÝÄql7¹xnÿë/¿”J¥b÷j§ÍV˜“¥k×U¯¼çŸIxý¦Ê¹�í?~å_›˜˜8~/6b®êþ"Ÿ-ë7Ôüú7�Á`p' >Àƒ‹ëº ùxnû3ÚÏ~þËw}+ëw³™³#¶�ah Ž_NðÍv«®ë¼ñÚë¯iŒF£f6›G›ÇåÖ­1nÞ¼‰Çãá…çw‹W÷½ú;Ó¶¿1_&)€c;D"—éîîFUUvïþºØ÷ò¾7ãCCÏdó7§à/.Þzªõò?Ĺ™N¹x�O518ЇÏ ²r+WT»~-õ£~?lYVçL›GZ𮳤¸„ uõ¤RIüþù‹æ=FqùçüK—ørw4äÿ@•¼ürqUÕÐt ¡TMcùŠÕ+»£Ñ;¡Ö©”ÓÁ ( ºîAÓôéÿ3ßóùüSuà<4€”DZ1Ó&òvFU!ðx¼U�v.¥Ä¶3˜¦‰i™:� X@p³H×Ew? –e»ÖÍÕÎ $ EÁç°tÙã¬\µ†€a ¥d|l”‹íçèctx`ùêÕ«¿gÆx[[ÛŸ��¬–e¡{t„ÈÞI?[89ãéú ôõõáñxÐ4�¦ã'0Ó)¾P½3�ædÓߨRóy6‡+Ù¾mÓ�@à‰ÁÁA¹wïÞà™3g~‘Àu2è²BH)q‡D"ÁÕ«W S®âýNP¸¤˜�þ›,Zè%ZÃÙ³g‰Ç㔕•�Ç‘R.Ô¬¶m£ªæ�B�—_€êÏçL[CCøŽM¬·—�;vPQ¶”�šÿÅØÈ /~ûyšššxïH#]Ñ+×2–yÆï÷§Z[[ßRYÇž±Ù!¼^ëjj)¯¨Äu]’ÉIN¼„ÁÁAª«Ãüöà4|øƒìE˜É jSŠ¢ÜÑѰ@Ó5|þÍ‘(ËŠ 8z´qÚYIÉRÖ®]ËÈÈ^¯—ÎÎNN77ã:.–e:EEEÒçó9���Í@:+@ff XÙÞ#Ç vës\¼pŽÕUuhšÎ•H““I2™ ---Œ��c]=1jj·ò§·ÿpºýüÁ_Ý^µ0³X¤æÞõlR%\Hhí:2‹ÉO?%réuuµD">>ß�m;ìÚµ“�ëÃ8Rb¹ à ÉÔ)Èþ-Èd,$³˜YétŠèåKŒ\§÷F©d‚/Öm$pþü'lؼ�¡xýýýlÙ²™wÿ…¿Vµk×®†aÜ:tèÐ+@÷¼50_ÂÔð92»ƒE…Kó¸ÞåhãßñztlÛ& ‘H$èííµ%+€i¦Ñµ»NÓäää\TÊ¿[;p—`a9†‘úÍË(+_�ÇãE×=<õì—ˆ\jgb|Œ7ì?}¥³ã-ŸÏ'’ÉdóµâŒ5ÏÌUÕx¼b%ËW¬ºÝ°TTUE÷xÑ4}úø..¤º¦‰¤ñ½woD.^ø}2™t˜Jä<£ÌÔ%ãD÷¦Ú„‚¢¤ë"¥œîŠ¢ jSn„.�¦Ÿ{74'�è•Î^!„ B4UEB EAÓ49�@‰”ªP· EEÈ,7ý}½#³uÙî� u]/»§ðïS„)Ó4/ÏÔýí�ïÖðËÄAIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewZoomTool.png0000644000000000000000000000013215225434775017657 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/viewZoomTool.png0000644000175000001440000000237015225434775017651 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<uIDAT8��”]LSgÇç´•2,6ÊG9 ?X§Hf6[rHÜ4Ã25:—�x§‹Ù¼X¶9ï–Ì,&î†dº›-!‹,Q"0c"µÊMX O« –"ÐQú�LAhËi»‹LGÐ%{’7ïÕû{>Þÿÿx}è€7€ÀªÝˆa`˜ž*�^4h1P]QQqôÔ©Suûöí+,..ÒƒÁÔÐÐý§Óy¿§§§¸Lj Ó¯ë�7EQ<~áÂ…oΜù¼tiiIÌÍÍ`6›)((@:::^\¼øÃo³³³W€Q`y=ðj¥õÍÍ×¾?tè�ypp�Îή„Çã™ >UUc’$m³Ûío;vÔ$IŠâIž;wîÊÌÌÌeàñz#Ø||þüùÉgÏþN_»v5]SS3| Ôï•ÀÀg{öìù£µµ%ùðáƒtCCCLų€e½jËËËË ‡C©înWº¶¶vø(rµ1é�` P»{÷n§ÛýgêÎ�¾ô‘#GûÅ5`P~âĉ:UU…žžÕår5Ý@PûU;q`½ÚÒÒ¶X,Øl¶�€5üR UUU–P(Œ×û x€(�D“RV¤€ç€ÇétÞÍËËcÇŽí9F£qûÚŠE`cAÁÃââÑh4¤A_ês�H‘±±±áååå´Á`@’¤Òµ`€T<GEôz½È«µžÝ©èrrŒ¬¬¨¨ªªfƒÓZöÅéé@Ül6³uëÖ2ÒÓ¿&�(²Ùl{ss�Âüü<³³³­­8 Ì L&55èõúj2òY _ý3 Ÿ¾$³+Ldd–Cf‡HÀ'ì�DÂéæææ´,Ën Z—µ‡••••™Íæm{÷¾§+++Ë3™Lv¯×kH&“¹Z…ÅdLR[__ÿEcc£ýÑ£QáæÍ›‹Çí _È‚ævY–¿jkk=l0Ä®.Vë[�F"‘hª··wn||<˜H$V«µ¨®®Ö"Ëò†{÷îápÜŽ755ý´°°Ð<² 6Y–¿¾qã÷ÃEEÅ¢Ïçc``·Û�°Ûí†]»v ………˜L&DQ$‹ ¡¯¯/ÚÞÞþs,»ø�¸ µ_%ËòÙ¶¶ÖÃK‰èóùèïïgxx8åp8ºÃáð“ì·Z­eùù¦QÔ OŸ>U§§§#Š¢Ü�ššrdÜ™@[›ïVWW×ÒrýxI‰ôª(JÊåruú|¾Kdöl‘öYùZ—K@x¢Ý/Èr§S”ûm’Tº!ãv»Q%ÕÑÑ᜘˜¸ ‹š´VϪ4SÀŠvÿË™: ßï÷ï4›7m÷ ^¯'ÕÕåêôûý?jÐç$©AV´vW—ѺV×1¿ß?‰D¤M›òKoÝjo÷ù|—³ ©õþW¬º'ŸŒ$mž~­ýÿøžwÛÀåÁÞðIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewZoomReset.png0000644000000000000000000000013215225434775020024 xustar0030 mtime=1784035837.880146893 30 atime=1784035837.880146893 30 ctime=1784035837.880146893 qpwgraph-1.0.3/src/images/viewZoomReset.png0000644000175000001440000000250415225434775020015 0ustar00rncbcusers‰PNG  IHDRÄ´l;sBIT|dˆ pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<ÁIDAT8��•]LSgÇç´uØÒŒïVœBEe3›-–D£&:űyAb23‰ÛÅf¼ÙwKfŒ‰»!™îfK¸�%j„ΛJ;†&XB´¥Ê‚ÃZ>ÊWK‘B?hOwÁ�0‚[²'9y.ÎûüÎóñŸ#ðï¦Þ £ì×"BÀ0 ÌI ¼(ÈÐ| bÛ¶mÕµµµwïÞ�›ŸŸ§—=rÏÚl6w{{{3p’?˜~X lEñèùóç¿=uê+Ãüü¼022Âôô4’$¡ÓéÈÉÉAìvûÜ… ?ü:11qè¢k�—2­¹víê¹Ã‡뺻»q8œ ·Û=4::ê_XXH † f³ÉX]]­Ñëõx<=©ºººËccc—€gkµ`=ðÉÙ³g‡ff^¦¯^½’®¬¬||TUÀg@ pzÇŽ¿777¥ž=@˜Eåfgg«�©T*CŽyôØl¶û™™™lÞ¼)C­VoZ�±¬ÏÉÉVE"³„Ãá  M-µ*‘H0??�$IKƒ—€ÉþþþÞh4šV©TèõzÃj0€�ÇE¥R)²Jë’$‘N§WLj€"#CÍÂB’d2™\ NË™E�‘¸N§cãÆ�…,JO¹|(�F’¤¥ó‚ü.ßd2í\·N-LOO3111º:ã0ÖÝÝ=¢Ñh¨¬ü G©TV°(%�š™™!�§AX�áYÀ®'Nì ‡Ã<{ÖM$Ck�ƒV«µsv6’޵ˢ8yòd-ð`^ùýþ¹¹¹¹x$´Àžýû÷~üø§oy<=¸Ý/ð—bUŸ4@YYYÙ!�N÷öÎ�ï+ŠŠŠ25�ÙëõŠ©T*zëÖ­'N§óA4} ˜jjjN744˜Ÿ>ínÞ¼im½St + ZÀl±X¾¶Z›«T*•ØÖæÀh,A­VóâÅ ©½ý�)ŸÏŒÇ㉒’’ÜC‡>̳X,o<|ø�ÖÖ;ñÆÆÆŸfgg€AaÔd±X¾¹qã·ª¼¼|ÑçóÑÕÕ…ËåJ˜ÍfÕÖ­¥Bnn.™™™(•Jb±ãããôöþIGGG¸¥¥åçX,vðqA.¿Üb±œ±Z›« EŸÏGgg'½½½Rkk«3 îÛ·o¯Ñh,Òj5¢¨¦¦¦’�@`ÒãñÜnº€q �¼6ß«¨¨ø¾©éúÑÂBý2ÔãñH‡ã®Ïç»ÈâžÍôru"0�AÙÏÉÃ_^ô{"‡8ÃÇÊ¥¯;%‡Î¹½ÉINrÎóž÷ÿüÿÏ#¤”<íBˆä»|B H~7‚ �ÀŽ”2‘šX�` Ø}\Ťù€ pv!„fªR»\®FUU«€Ý|‹}ÇH¨G00�˜šš’‰ÆÆÆ9 µ´´4¸´´´‹Åäðð°ììì” ²½½}SÓ´½Ê÷Qa¢ãpåìÙ³„Ãá=pŸÏ·˜��ýÝøøx|uuU.//Ëp8,;;;åüü¼ ƒ›š¦]Žj*°´··¿366öKGGÇÝÙÙYYPP êêêò*++‹kjjfnÞ¼™ˆÇã8TUåÖ­[”””d��TU= X… ¼ ¬wuuM\¿~݉DfÌàõõõÙÕÕÕ¹>ŸoúöíÛ‰X,Fnn.+++»ÝÝÝ÷wvvâÅÅÅy†ÂöQ!€ à­ŠŠŠo†††þ¶Ùlãn·{9IK¿ôz½«™™™¿^»v- …vÇ(Ð ÔG}Ǧ&>œr¹\¡ÁÁÁ{6›íG·Ûýg ø_YYYß?~<TR´ÕŠG€Mf±^§ÓíëëÛ>>‹ˆžˆD�F“EDtàÀþèÑ£DDÔØØH âv»©¡¡áÙ† t ˜åò~uuµqfffwÿþ}""ÊÎΦÌÌL�ˆ(>>>g³�jµZÇûúú|ýýýd±X¨¡¡�Ìf3�ŽŽ’Ïç£ééiºpáÂT˃øAAAÁ—/^¼ ¯×K===ÔÞÞN555dµZ©¿¿ŸŒF£0000 àý•È’““sëëëÿqãÆ�§jµºÀ>?ß·o_¹ÅbñŠ¢H‹…ÏÊÊú#€°¬"==ýs“ÉÄ ‚@ÝÝÝÞÝ»wWø@ÞÖ­[ÿ\]]m¨««û:))éS²×øPø.€àû»—/_n²ÛíÄó<µ´´LFEE}€`›››'8Ž£‰‰ :uêÔßl{“ß׬¥2™ì§MMMC�‡ìv;•••} `+€¸ÒÒÒ‡SSSäv»éÖ­[#r¹Ÿ�– Çq433CN§“œN'9r8ÔÔÔDÇQii)�9sFðxÛ´iÓo|ÿõ?Á�?¾Ýét’Íf£¢¢¢¶OOO“Ýn§³gÏvXuXý¯�HºcÇŽ+���^AèñãÇ•JuN­V—µ´´xA G�qÛ¶m«þ­À$IfmmíˆËå¢çÏŸÓ‰'Ú$øá±cǾ¥ÙÙYºzõê¸L&û-¾I+Ƈ†@P€]“ŸŸß:66F^¯—êëë§ EüÃ(44ôS½^?5??Of³™rssÿx«ù]B€T§Óeß¹s§µ®®®C¥Rý @€¼]»v}ÞÙÙÉñ¤k×®‘Õj%£ÑHƒAœ�ö�…•,==]Xi%³ÙlTQQaðÁJç €XRRb_Ä®dû÷ïm% \JSÍfó]�Ç£,•JñàÁ$%%�ˆ022bËÏÏ¿b³Ù*‰È‡„1&‰ˆˆ8S^^~Z¥Rm&"˜L&:t^¯‚ ˆ‰Ä¡ÑhQ°t+–%''túôéÏæææÂ.]ºÔ966öï÷Ïh&"ÇJÁHl� Ìo’ÄÄÄ$k¥R©«²²òzOOÏmZ¾–ûÁ †Wa­`o+kù]B`=dÝï†ëNà?‘à -‚­lIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/viewThumbview.png0000644000000000000000000000013215225434775020047 xustar0030 mtime=1784035837.879146906 30 atime=1784035837.879146906 30 ctime=1784035837.879146906 qpwgraph-1.0.3/src/images/viewThumbview.png0000644000175000001440000000130415225434775020035 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<QIDAT8�µ•¿NQÆ¿sÿ쬗XI„Ä;ÙF¬ØPPûv„ÆXPó<ÁZ Þ€‚ŠØY)!!†-Öl4t’™{Ï=»C–a•q�_yÏ™ß|9gæ»$" " ÀPM€�‹H¤>hÀ<€Yc#‚8ðÀWÓw:¿±±ñvuuuÖC@DR…&"!ÙÜÜ<[__ à=¸àU·Ûmb?3Æ„¿±B0Íf3™››Ãäääï z£pÖZÚÛÛKwww?LOO±Ör¨÷^·ÛíGyž?o4€ LÑ@DbŒá™™™ÎÂÂÂgç\%×iššâÙÁñ™Á&"k-;çB½^¯<k-—w2ê§u§þczn†ž–ÄÌ”e™öÞkf¦ÁZA1ó-ƒw‚™™:�ÎØÁÁÁãn·û „` —J)Ôjµ<ƨbŒ7^x'Ø{¯�ŽŽ¦_6›MW®Ç±¶¶vÎ̺øY€ 3ffº¼¼tZëÚ°ºR "26­î»R"¿ÿÃËc¨ EUÁ $¥Ô­ú�å‰yïuš¦è-&Ë2�$‰WJq¹¿�Ö:G)n¯ûPsqq1%"T„PŒ‘®®®ÜöööÙÎÎÎ3k×Ѫ”ÊÚíöw/ƒ#€4„ ËËË÷’$yQŽM!fV"B·¶¯”‹1Ž/--Õò<ô?ô®““­­­ó•••'�F£^¸)Ç�¡�÷^Z­Ö1€c¾¸šxà)zy:Š~8ð À7ú_—é/ÆF??;¡ÏIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/itemEdit.png0000644000000000000000000000013215225434775016746 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/itemEdit.png0000644000175000001440000000137315225434775016742 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<ˆIDAT8�­ÕMKQ†áûÄOb?[hwm¡ˆ±Ò®„ú/ºkuë/ܸò£R�R„*dwj6Ò…h%…(m²°v3Xk e2‰IÞ.œ„ Qcè ³šáâ<Ï™9£D„FF)¥4E@¤ S�À6êÀCÀ~ù2îhuOÛÚÚ†fggÃ^¯wx¸íû "u_vtôù|ïc±˜u||,KKK9­õ Ð ´ˆHýp5ªµžÙØØ°,Ë’L&#2??Ÿóz½cÀcÀQWÕñ}>ßàÚÚÚHOOOK©TÂ4M …ííí®ááá7vçM7ÂUè­õÛh4:ÒÝÝí*•Jd2NNN8::boo/ …"œod±Þø�Zëérül6+†aH"‘�ÕÕUÏy<žuuÜúÂ~^] Û¨»Qô:Ø <×ZO5‚Ö„9ÿhîj­777MÓ4Å4Í º²²RFg€®Z¨ˆà¼ð8€;@—ˆ¼´,ËyzzŠRŠt:Íáá!ÛÛÛù‰‰‰�Ùlö �¬ê3¢2Vëzûûû¿®¯¯gý~ÿÏH$RÜÙÙ‘h4Z?xÕJ/UaWðÈår�%“ÉB:�–ÅÅÅb 8 …Bù©©)ÓãñL_ÿ*¸x5::ºk††!±XLæææ¬ÖÖÖOZëwÀs;Õµh¥c¥Tp¿¹¹¹syy9×××WRJ9R©TqaaaË4Íþ…š�ÖêØ^íëÉÉÉD<?ëèèØØu:�c@/ppÜæ$¬~+\Á`Ї$“ÉÏûûû߀_ÀÎðÒ�«¬%"å*Ø»�¾Ûà~9·‚¡Òs9AÃà%øÏ?-·£š¬ÒfïIEND®B`‚qpwgraph-1.0.3/src/images/PaxHeaders/fileOpen.png0000644000000000000000000000013215225434775016743 xustar0030 mtime=1784035837.876615721 30 atime=1784035837.876615721 30 ctime=1784035837.876615721 qpwgraph-1.0.3/src/images/fileOpen.png0000644000175000001440000000141215225434775016731 0ustar00rncbcusers‰PNG  IHDRÄ´l; pHYs & &Q©©3tEXtSoftwarewww.inkscape.org›î<—IDAT8�Õ”¿kaÇ?w¹4¶©˜z*¡[k8Y:;. b)T�Ò¡ÿ€£!¡C·V©àÒÁÒ•Ö-E:Ì�¡C!Ä%*E“¦=0g~ô‡»„�¤švüÀÃËÃ{÷}~¼ïûÀÿ†Òâ«€ÛYëX@ÇÎ-¬×€àªã×€#àPh ðW4gu7Ünw0‰<ÖuýºeYªªª–aGKKK/LÓŒß�Ó¦ÿÅ nu ¨W€Ñh4'X^^þ <îAÇ&�»À- ¿¥z'뱉‰‰èþþþ¬×ëegg‡B¡€×ëe~~žJ¥ÂÊÊŠ!"%EQ™¹\®J2™|�ÇŸ§U .“ëëëE‘½½=™žž–H$"sss‹Å¬NUÔI§Óeàp™¦C×§_Ó4ÀÁÁ�@€@ Àðð0‰DBÉçóx<Dìd-Ëb``€P(DOO�øŸÓ�_@µ~x�HªªR,I¥RsrrÂöö6¦i""ˆ¥R ¿ßO(BDàÐ|RÀǺpƒjµJ&“accƒÁÁÁÖí6ÆÇÇ™¦ ð‚ÁàÓd2¹ª¶~X.—YXXèJ´•ÞÞ^µV«õj›ðÐÐ333çX[[û‘J¥’€vonnnš""ù|þO—àLr¹Ü©®ëÏ�›€«žqãrëº~¡lÃápºP(¼¾–êˆj>Ÿ¯­-Ý‹Å~nmm½Ò€‰ó¼û€ÙÃÃÃÊEZP«Õdttôp{2ö©ÙÅÅÅD8¾§(JÛ< UU­ÝÝÝ÷Ùlö5ð{ �Óû9ŽcØ©uNŸ… |Þa�U«Y¸¾º� \] ×Õ1«y³“@·Ù6øwüPØ‹´T»´àIEND®B`‚qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_options.ui0000644000000000000000000000013215225434775017172 xustar0030 mtime=1784035837.887146797 30 atime=1784035837.887146797 30 ctime=1784035837.887146797 qpwgraph-1.0.3/src/qpwgraph_options.ui0000644000175000001440000004367615225434775017202 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_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 Merger 50 false Whether to merge / unify nodes on the patchbay Enable merge / unify nodes 50 false Node name (regex) true 72 24 50 false Add node name (regex) to merger / unify list &Add Qt::ToolButtonTextBesideIcon 320 80 50 false Node name (regex) merger / unify list QAbstractItemView::SelectRows true 72 24 50 false Remove node name (regex) from merger / unify list &Remove Qt::ToolButtonTextBesideIcon Qt::Vertical 20 8 72 24 50 false Clear merger / unify 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 MergerNodesEnabledCheckBox MergerNodesNameComboBox MergerNodesAddToolButton MergerNodesListWidget MergerNodesRemoveToolButton MergerNodesClearToolButton DialogButtonBox qpwgraph-1.0.3/src/PaxHeaders/config.h.cmake0000644000000000000000000000013215225434775015724 xustar0030 mtime=1784035837.876250108 30 atime=1784035837.876250108 30 ctime=1784035837.876250108 qpwgraph-1.0.3/src/config.h.cmake0000644000175000001440000000201315225434775015710 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 unique/single instance is enabled. */ #cmakedefine CONFIG_XUNIQUE @CONFIG_XUNIQUE@ #endif // __CONFIG_H qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_command.cpp0000644000000000000000000000013215225434775017262 xustar0030 mtime=1784035837.883146851 30 atime=1784035837.883146851 30 ctime=1784035837.883146851 qpwgraph-1.0.3/src/qpwgraph_command.cpp0000644000175000001440000001677515225434775017272 0ustar00rncbcusers// qpwgraph_command.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_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 // Constructors. 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), m_center(false) { 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->saveNode(node); } if (canvas && canvas->isRepelOverlappingNodes()) { foreach (qpwgraph_node *node, nodes) canvas->repelOverlappingNodes(node, this); } } qpwgraph_move_command::qpwgraph_move_command ( qpwgraph_canvas *canvas, const QHash& positions ) : qpwgraph_command(canvas), m_nexec(0), m_center(true) { qpwgraph_command::setText(QObject::tr("Arrange")); foreach (qpwgraph_node *n, positions.keys()) { addItem(n, n->pos(), positions.value(n)); } } // 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); } qpwgraph_canvas *canvas = qpwgraph_command::canvas(); if (canvas) canvas->saveNode(node); } // 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->saveNode(node); } } } } if (m_center) { canvas->centerView(true); } 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-1.0.3/src/PaxHeaders/qpwgraph_systray.cpp0000644000000000000000000000013215225434775017362 xustar0030 mtime=1784035837.890146755 30 atime=1784035837.890146755 30 ctime=1784035837.890146755 qpwgraph-1.0.3/src/qpwgraph_systray.cpp0000644000175000001440000001031515225434775017352 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-1.0.3/src/PaxHeaders/CMakeLists.txt0000644000000000000000000000013215225434775015767 xustar0030 mtime=1784035837.875350384 30 atime=1784035837.875350384 30 ctime=1784035837.875350384 qpwgraph-1.0.3/src/CMakeLists.txt0000644000175000001440000000711115225434775015757 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_toposort.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_toposort.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/${PROJECT_NAME}.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) qpwgraph-1.0.3/src/PaxHeaders/qpwgraph_config.cpp0000644000000000000000000000013215225434775017111 xustar0030 mtime=1784035837.884146838 30 atime=1784035837.883146851 30 ctime=1784035837.884146838 qpwgraph-1.0.3/src/qpwgraph_config.cpp0000644000175000001440000004543015225434775017107 0ustar00rncbcusers// qpwgraph_config.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 "config.h" #include "qpwgraph_config.h" #include #include #include #include // Local constants. static const char *GraphGeometryGroup = "/GraphGeometry"; static const char *GraphLayoutGroup = "/GraphLayout"; static const char *GraphViewGroup = "/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 *MergerNodesGroup = "/MergerNodes"; static const char *NodesEnabledKey = "/Enabled"; static const char *NodesListKey = "/List"; static const char *HistoryGroup = "/History"; // Legacy main-form class renaming support (> v0.7.7) // #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), m_merger_enabled(false), m_merger_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::setMergerNodesEnabled ( bool enabled ) { m_merger_enabled = enabled; } bool qpwgraph_config::isMergerNodesEnabled (void) const { return m_merger_enabled; } void qpwgraph_config::setMergerNodesList ( const QStringList& nodes ) { m_merger_nodes = nodes; } const QStringList& qpwgraph_config::mergerNodesList (void) const { return m_merger_nodes; } void qpwgraph_config::setMergerNodesDirty ( bool dirty ) { m_merger_dirty = dirty; } bool qpwgraph_config::isMergerNodesDirty (void) const { return m_merger_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(MergerNodesGroup); m_merger_enabled = m_settings->value(NodesEnabledKey, false).toBool(); m_merger_nodes = m_settings->value(NodesListKey).toStringList(); m_settings->endGroup(); m_settings->beginGroup(FilterNodesGroup); m_filter_enabled = m_settings->value(NodesEnabledKey, false).toBool(); m_filter_nodes = m_settings->value(NodesListKey).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(GraphViewGroup); 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(GraphGeometryGroup); #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(GraphLayoutGroup); #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(MergerNodesGroup); m_settings->setValue(NodesEnabledKey, m_merger_enabled); m_settings->setValue(NodesListKey, m_merger_nodes); m_settings->endGroup(); m_settings->beginGroup(FilterNodesGroup); m_settings->setValue(NodesEnabledKey, m_filter_enabled); m_settings->setValue(NodesListKey, 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(GraphViewGroup); 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(GraphGeometryGroup); const QByteArray& geometry_state = widget->saveGeometry(); m_settings->setValue('/' + widget->objectName(), geometry_state); m_settings->endGroup(); m_settings->beginGroup(GraphLayoutGroup); 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(); } // Widget geometry persistence helpers. // void qpwgraph_config::loadWidgetGeometry ( QWidget *widget ) { if (widget) { m_settings->beginGroup(GraphGeometryGroup); const QByteArray& geometry = m_settings->value('/' + widget->objectName()).toByteArray(); if (!geometry.isEmpty()) widget->restoreGeometry(geometry); m_settings->endGroup(); } } void qpwgraph_config::saveWidgetGeometry ( QWidget *widget ) { if (widget) { const QByteArray& geometry = widget->saveGeometry(); if (!geometry.isEmpty()) { m_settings->beginGroup(GraphGeometryGroup); m_settings->setValue('/' + widget->objectName(), geometry); m_settings->endGroup(); } } } // end of qpwgraph_config.cpp qpwgraph-1.0.3/src/PaxHeaders/qpwgraph.h0000644000000000000000000000013215225434775015231 xustar0030 mtime=1784035837.881713957 30 atime=1784035837.881713957 30 ctime=1784035837.881713957 qpwgraph-1.0.3/src/qpwgraph.h0000644000175000001440000000560715225434775015231 0ustar00rncbcusers// qpwgraph.h // /**************************************************************************** Copyright (C) 2021-2026, 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 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; } const QString& remoteName() const { return m_remote_name; } #ifdef CONFIG_XUNIQUE // 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_XUNIQUE 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; QString m_remote_name; }; #endif // __qpwgraph_h // end of qpwgraph.h