├── .gitignore ├── src └── rqt_embed_window │ ├── __init__.py │ ├── RqtEmbedWindow.ui │ ├── shell_cmd.py │ └── RqtEmbedWindow.py ├── usage.gif ├── screenshot1.png ├── launch └── rqt_embed_window.launch ├── scripts ├── rqt_embed_window └── test_if_window_can_be_embedded.py ├── setup.py ├── CMakeLists.txt ├── package.xml ├── plugin.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode 3 | -------------------------------------------------------------------------------- /src/rqt_embed_window/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomebytes/rqt_embed_window/HEAD/usage.gif -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomebytes/rqt_embed_window/HEAD/screenshot1.png -------------------------------------------------------------------------------- /launch/rqt_embed_window.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /scripts/rqt_embed_window: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | from rqt_embed_window.RqtEmbedWindow import RqtEmbedWindow 6 | from rqt_gui.main import Main 7 | 8 | main = Main(filename='rqt_embed_window') 9 | sys.exit(main.main(standalone='rqt_embed_window')) 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | from catkin_pkg.python_setup import generate_distutils_setup 5 | 6 | d = generate_distutils_setup( 7 | packages=['rqt_embed_window'], 8 | package_dir={'': 'src'} 9 | ) 10 | 11 | setup(**d) 12 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | 3 | project(rqt_embed_window) 4 | # Load catkin and all dependencies required for this package 5 | find_package(catkin REQUIRED COMPONENTS 6 | rqt_gui 7 | rqt_gui_cpp 8 | 9 | ) 10 | 11 | catkin_python_setup() 12 | 13 | catkin_package( 14 | ) 15 | 16 | include_directories( 17 | ${catkin_INCLUDE_DIRS} 18 | ) 19 | 20 | install(PROGRAMS 21 | scripts/rqt_embed_window 22 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 23 | ) 24 | -------------------------------------------------------------------------------- /src/rqt_embed_window/RqtEmbedWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RqtEmbedWindowWidget 4 | 5 | 6 | true 7 | 8 | 9 | rqt_embed_window 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rqt_embed_window 4 | 0.9.0 5 | rqt_embed_window provides a GUI plugin to embed other windows 6 | 7 | Sammy Pfeiffer 8 | BSD 9 | Sammy Pfeiffer 10 | 11 | catkin 12 | 13 | rospy 14 | rqt_gui_py 15 | wmctrl 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | A GUI plugin to execute GUI applications as rqt_ plugins 6 | 7 | 8 | 9 | 10 | folder 11 | Plugins related to miscellaneous tasks 12 | 13 | 14 | view-restore 15 | A GUI plugin to embed (hopefully) any graphical application into an rqt plugin 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /scripts/test_if_window_can_be_embedded.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | def get_window_id_by_pid(pid): 4 | """ 5 | Get the window ID based on the PID of the process, None if not found. 6 | Uses wmctrl and parses it output to find it 7 | """ 8 | from subprocess import check_output 9 | # Looks like: 10 | # 0x03c00041 0 3498 skipper Mozilla Firefox 11 | # WindowID ? PID USER Window Name 12 | # Needs sudo apt-get install wmctrl -lp 13 | 14 | output = check_output('wmctrl -lp', shell=True) 15 | # Find the line with the PID we are looking for 16 | for line in output.splitlines(): 17 | fields = line.split() 18 | if len(fields) >= 3: 19 | this_pid = int(fields[2]) 20 | if this_pid == pid: 21 | return int(fields[0], 16) 22 | return None 23 | 24 | def run_app(window_id): 25 | from PyQt5.QtGui import QWindow 26 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QPushButton 27 | from PyQt5.QtCore import Qt 28 | 29 | app = QApplication([]) 30 | main_widget = QWidget() 31 | main_widget.setWindowTitle('embed_another_window') 32 | layout = QVBoxLayout(main_widget) 33 | 34 | window = QWindow.fromWinId(window_id) 35 | # FramelessWindowHint is NECESSARY 36 | window.setFlags(Qt.FramelessWindowHint) 37 | widget = QWidget.createWindowContainer(window) 38 | layout.addWidget(widget) 39 | 40 | button = QPushButton('Close') 41 | button.clicked.connect(main_widget.close) 42 | layout.addWidget(button) 43 | 44 | # layout.setContentsMargins(0, 0, 0, 0) 45 | # layout.setSpacing(0) 46 | 47 | main_widget.resize(600, 400) 48 | 49 | main_widget.show() 50 | app.exec_() 51 | 52 | 53 | 54 | if __name__ == '__main__': 55 | import sys 56 | import os 57 | if len(sys.argv) < 2: 58 | print("Provide PID as argument") 59 | exit(0) 60 | 61 | pid = sys.argv[1] 62 | window_id = get_window_id_by_pid(int(pid)) 63 | if window_id: 64 | run_app(window_id) 65 | # Kill the process or it will stay running without a graphical window 66 | os.system("kill {}".format(pid)) 67 | else: 68 | print("No window ID for PID: {}".format(pid)) 69 | -------------------------------------------------------------------------------- /src/rqt_embed_window/shell_cmd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | import tempfile 5 | import os 6 | import signal 7 | import time 8 | 9 | 10 | class ShellCmd: 11 | """Helpful class to spawn commands and keep track of them""" 12 | 13 | def __init__(self, cmd): 14 | self.retcode = None 15 | self.outf = tempfile.NamedTemporaryFile(mode="w") 16 | self.errf = tempfile.NamedTemporaryFile(mode="w") 17 | self.inf = tempfile.NamedTemporaryFile(mode="r") 18 | self.process = subprocess.Popen(cmd, shell=True, stdin=self.inf, 19 | stdout=self.outf, stderr=self.errf, 20 | preexec_fn=os.setsid, close_fds=True) 21 | 22 | def __del__(self): 23 | if not self.is_done(): 24 | self.kill() 25 | self.outf.close() 26 | self.errf.close() 27 | self.inf.close() 28 | 29 | def get_stdout(self): 30 | with open(self.outf.name, "r") as f: 31 | return f.read() 32 | 33 | def get_stderr(self): 34 | with open(self.errf.name, "r") as f: 35 | return f.read() 36 | 37 | def get_retcode(self): 38 | """Get retcode or None if still running""" 39 | if self.retcode is None: 40 | self.retcode = self.process.poll() 41 | return self.retcode 42 | 43 | def is_done(self): 44 | return self.get_retcode() is not None 45 | 46 | def is_succeeded(self): 47 | """Check if the process ended with success state (retcode 0) 48 | If the process hasn't finished yet this will be False.""" 49 | return self.get_retcode() == 0 50 | 51 | def wait_until_done(self): 52 | while not self.is_done(): 53 | time.sleep(0.1) 54 | 55 | 56 | def get_pid(self): 57 | return self.process.pid 58 | 59 | def kill(self): 60 | self.retcode = -1 61 | os.killpg(self.process.pid, signal.SIGTERM) 62 | self.process.wait() 63 | 64 | 65 | # Demonstration of usage 66 | if __name__ == '__main__': 67 | import time 68 | cmd = ShellCmd("sleep 3") 69 | try: 70 | while not cmd.is_done(): 71 | print "Still sleeping..." 72 | time.sleep(0.5) 73 | except KeyboardInterrupt: 74 | print "Pressed Control+C, stopping command." 75 | cmd.kill() 76 | if cmd.is_succeeded(): 77 | print "The command finished succesfully" 78 | print "It printed: " + str(cmd.get_stdout()) 79 | else: 80 | print "The command didn't finish succesfully" 81 | print "Its stderr was: " + str(cmd.get_stdout()) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rqt_embed_window 2 | 3 | Embed any graphical program window into a rqt_ plugin. The ultimate productivy tool! 4 | 5 | Warning: This is a hacky approach. The plugin executes your program and captures the window to embed it in rqt_gui. 6 | 7 | Notable examples of applications that are embeddable(*) with `rqt_embed_window`: 8 | 9 | ![Screenshot of SimpleScreenRecorder, Rviz, Plotjuggler, rosbag_editor and a normal rqt_console](screenshot1.png) 10 | 11 | * [Plotjuggler](https://www.plotjuggler.io) Helping the awesome [@facontidavide](https://github.com/facontidavide) to be able to use it alongside [Rviz](https://github.com/facontidavide/PlotJuggler/issues/87)/[Rqt](https://github.com/facontidavide/PlotJuggler/issues/5) as some people requested. 12 | * [Rviz](http://wiki.ros.org/rviz) Works without crashes unlike [rqt_rviz](http://wiki.ros.org/rqt_rviz) [Issue #6](https://github.com/ros-visualization/rqt_rviz/issues/6). (The main motivation for this work). 13 | * [rosbag_editor](https://github.com/facontidavide/rosbag_editor) Another seriously awesome tool by [@facontidavide](https://github.com/facontidavide). 14 | 15 | Other maybe useful or fun ones: 16 | * Google Chrome (if you have an already existing web interface) 17 | * VLC 18 | * Slack 19 | * Wireshark 20 | * SimpleScreenRecorder 21 | * Visual Studio Code 22 | * Some games like Slay the Spire 23 | * Another instance of rqt_gui 24 | 25 | 26 | (*) **Caveat**: Some drag-and-drop behaviours sometimes don't work. As far as I investigated, the problem is that the embedded app does not receive those events (investigated on a Qt app). Some apps have workarounds (see below). 27 | 28 | # Usage 29 | 30 | ![Example usage in rqt_gui](usage.gif) 31 | 32 | Open `rosrun rqt_gui rqt_gui`, go to `Plugins` > `Miscellaneous Tools` > `Embed a graphical program window into RQT`. 33 | 34 | You'll be prompted for the commandline for the program (which will be executed in the plugin), for example `rviz -s None`. 35 | 36 | **Note**: Doing `-s None` disables the splashscreen, which is **necessary** to get the correct PID and Window ID internally. 37 | 38 | You can export your perspective and load it later as any other. 39 | 40 | ## Things that didn't work for me 41 | terminator, gnome-terminal, LibreOffice, kivy-based apps 42 | 43 | 44 | ## How to try if an app works quickly 45 | Use the `test_if_window_can_be_embedded.py` script providing it the PID of the window you want to test. 46 | 47 | You can find the PID by means of `ps aux | grep program-name` or `pstree -p` and look for the process that inherits the rest. 48 | 49 | You can also use `xwininfo` and click on the window and manually modify the script to use the window id provided. 50 | 51 | ## Workarounds for complex commands/programs 52 | If your program needs a long set of parameters or environment variables, set it up in a bash 53 | script and call it from rqt_embed_window, however, you must make sure to prepend the binary with `exec` so the PID reported to rqt_embed_window is the actual PID of the program itself and not the 54 | shell that is executing it. 55 | 56 | ## Useful examples 57 | 58 | Find here suggestions of commands that may be useful (these are to be added in the popup of the plugin). 59 | 60 | ### Run rviz with a config file 61 | ```bash 62 | rviz --splash-screen None --display-config my_rviz_config.rviz 63 | ``` 64 | Workaround needed to re-order displays (done by dragging the item) in the Displays panel, 65 | you must create your `.rviz` config file in a non-embedded Rviz. Other dragging actions seem to work. 66 | 67 | ### Run plotjuggler 68 | ```bash 69 | plotjuggler --nosplash --layout my_layout.xml 70 | ``` 71 | Workaround needed to drag data sources from the left panel, drag-and-drop doesn't work, so you must 72 | create the layout (with the `Save data sources` option enabled) for the embedded plotjuggler before hand in a non-embedded plotjuggler. 73 | 74 | ### Run rosbag_editor 75 | ```bash 76 | rosrun rosbag_editor rosbag_editor 77 | ``` 78 | 79 | # Thanks 80 | 81 | Thanks to [@leggedrobotics](https://github.com/leggedrobotics), specially [@samuelba](https://github.com/samuelba), for the [catkin_create_rqt](https://github.com/leggedrobotics/catkin_create_rqt) package which made cooking the first version of this package quick and painless. 82 | 83 | -------------------------------------------------------------------------------- /src/rqt_embed_window/RqtEmbedWindow.py: -------------------------------------------------------------------------------- 1 | import os 2 | import rospy 3 | import time 4 | 5 | from qt_gui.plugin import Plugin 6 | from python_qt_binding import loadUi 7 | from python_qt_binding.QtWidgets import QWidget, QInputDialog 8 | from python_qt_binding.QtGui import QWindow 9 | from python_qt_binding.QtCore import Qt 10 | from qt_gui.settings import Settings 11 | from shell_cmd import ShellCmd 12 | 13 | 14 | def get_window_id_by_window_name(window_name): 15 | """ 16 | Get the window ID based on the name of the window, None if not found. 17 | Uses wmctrl and parses it output to find it 18 | """ 19 | # Looks like: 20 | # 0x03c00041 0 3498 skipper Mozilla Firefox 21 | # WindowID ? PID USER Window Name 22 | process = ShellCmd('wmctrl -lp') 23 | process.wait_until_done() 24 | 25 | output = process.get_stdout() 26 | # Find the line with the PID we are looking for 27 | for line in output.splitlines(): 28 | fields = line.split() 29 | if len(fields) >= 4: 30 | this_window_name = ' '.join(fields[4:]) 31 | # Avoiding dealing with unicode... 32 | if str(this_window_name) == str(window_name): 33 | return int(fields[0], 16) 34 | return None 35 | 36 | 37 | def get_window_id_by_pid(pid): 38 | """ 39 | Get the window ID based on the PID of the process, None if not found. 40 | Uses wmctrl and parses it output to find it 41 | """ 42 | # Looks like: 43 | # 0x03c00041 0 3498 skipper Mozilla Firefox 44 | # WindowID ? PID USER Window Name 45 | process = ShellCmd('wmctrl -lp') 46 | process.wait_until_done() 47 | 48 | output = process.get_stdout() 49 | # Find the line with the PID we are looking for 50 | for line in output.splitlines(): 51 | fields = line.split() 52 | if len(fields) >= 3: 53 | this_pid = int(fields[2]) 54 | if this_pid == pid: 55 | return int(fields[0], 16) 56 | return None 57 | 58 | 59 | def wait_for_window_id(pid=None, window_name=None, timeout=5.0): 60 | """ 61 | Keep trying to find the Window ID for a PID until we find it or we timeout. 62 | """ 63 | window_id = None 64 | ini_t = time.time() 65 | now = time.time() 66 | while window_id is None and (now - ini_t) < timeout: 67 | if window_name is not None: 68 | window_id = get_window_id_by_window_name(window_name) 69 | elif pid is not None: 70 | window_id = get_window_id_by_pid(pid) 71 | else: 72 | raise RuntimeError("No PID or window_name provided to look for a window on wait_for_window_id") 73 | time.sleep(0.2) 74 | now = time.time() 75 | return window_id 76 | 77 | 78 | class RqtEmbedWindow(Plugin): 79 | """ 80 | This plugin allows to embed a Qt window into a rqt plugin. 81 | """ 82 | 83 | def __init__(self, context): 84 | super(RqtEmbedWindow, self).__init__(context) 85 | self.setObjectName('RqtEmbedWindow') 86 | self._command = '' 87 | self._window_name = '' 88 | self._external_window_widget = None 89 | self._process = None 90 | self._timeout_to_window_discovery = 20.0 91 | 92 | # Create QWidget 93 | self._widget = QWidget() 94 | # Get path to UI file which is a sibling of this file 95 | # in this example the .ui and .py file are in the same folder 96 | ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 97 | 'RqtEmbedWindow.ui') 98 | # Extend the widget with all attributes and children from UI file 99 | loadUi(ui_file, self._widget) 100 | self._widget.setObjectName('RqtEmbedWindowUi') 101 | # Add widget to the user interface 102 | context.add_widget(self._widget) 103 | 104 | self.context = context 105 | 106 | def add_external_window_widget(self): 107 | # The command is prepended with exec so it becomes the shell executing it 108 | # So it effectively has the PID we will look for the window ID 109 | self._process = ShellCmd("exec " + self._command) 110 | 111 | # If a window name is provided, it probably means that's the only way to find the window 112 | # so, let's do that first 113 | if self._window_name: 114 | window_id = wait_for_window_id(window_name=self._window_name, 115 | timeout=self._timeout_to_window_discovery) 116 | else: 117 | # Get window ID from PID, we must wait for it to appear 118 | window_id = wait_for_window_id(pid=self._process.get_pid(), 119 | timeout=self._timeout_to_window_discovery) 120 | if window_id is None: 121 | rospy.logerr("Could not find window id...") 122 | rospy.logerr("Command was: {} \nWindow name was: '{}'\nStdOut was: {}\nStdErr was: {}".format(self._command, 123 | self._window_name, 124 | self._process.get_stdout(), 125 | self._process.get_stderr())) 126 | self._process.kill() 127 | return 128 | 129 | # Create a the window that will contain the program 130 | window = QWindow.fromWinId(window_id) 131 | # FramelessWindowHint is necessary for the window to effectively get embedded 132 | window.setFlags(Qt.FramelessWindowHint) 133 | widget = QWidget.createWindowContainer(window) 134 | 135 | # Store it for later 136 | self._external_window_widget = widget 137 | 138 | # Set all margins and spacing to 0 to maximize window usage 139 | self._widget.verticalLayout.setContentsMargins(0, 0, 0, 0) 140 | self._widget.verticalLayout.setSpacing(0) 141 | 142 | self._widget.verticalLayout.addWidget(self._external_window_widget) 143 | 144 | # Give the title (for rqt_gui compisitions) some information 145 | if self.context.serial_number() < 2: 146 | self._widget.setWindowTitle('{} ({})'.format(self._widget.windowTitle(), self._command)) 147 | else: 148 | self._widget.setWindowTitle('{} ({}) ({})'.format(self._widget.windowTitle(), 149 | self.context.serial_number(), self._command)) 150 | 151 | def shutdown_plugin(self): 152 | # Free resources 153 | self._process.kill() 154 | 155 | def save_settings(self, plugin_settings, instance_settings): 156 | instance_settings.set_value("command", self._command) 157 | instance_settings.set_value("window_name", self._window_name) 158 | 159 | def restore_settings(self, plugin_settings, instance_settings): 160 | self._command = instance_settings.value("command") 161 | self._window_name = instance_settings.value("window_name") 162 | if self._command is not None and self._command != '': 163 | self.add_external_window_widget() 164 | else: 165 | self.trigger_configuration() 166 | 167 | def trigger_configuration(self): 168 | # Enable the gear icon and allow to configure the plugin for the program to execute 169 | text, ok = QInputDialog.getText(self._widget, 'RqtEmbedWindow Settings', 170 | "Qt GUI command to execute (disable splashscreens):", 171 | text=self._command) 172 | if ok: 173 | self._command = text 174 | 175 | # Ask if the user wants to use the Window Name instead of the PID to find the window 176 | # Some apps spawn different processes and it's hard to find the window otherwise 177 | text, ok = QInputDialog.getText(self._widget, 'RqtEmbedWindow Settings', 178 | "If you prefer to find the window by the window name, input it here:", 179 | text=self._window_name) 180 | if ok: 181 | self._window_name = text 182 | 183 | # Refresh plugin! 184 | if self._external_window_widget is not None: 185 | self._widget.verticalLayout.removeWidget(self._external_window_widget) 186 | 187 | if self._process is not None and not self._process.is_done(): 188 | self._process.kill() 189 | 190 | self.add_external_window_widget() 191 | --------------------------------------------------------------------------------