├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------