0

I wrote the following code adapting this.

It is started using: python3 embed.py gnome-terminal

import time
import re
import subprocess
import sys, os, shutil
from PySide2.QtCore import (Qt, QProcess,)
from PySide2.QtGui import (QWindow,)
from PySide2.QtWidgets import (QApplication, QWidget, QVBoxLayout, QMessageBox,)

class Window(QWidget):
    def __init__(self, program, arguments):
        super().__init__()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
        self.external = QProcess(self)
        self.external.start(program, arguments)
        time.sleep(1)
        p = subprocess.run(['xprop', '-root'], stdout=subprocess.PIPE)
        for line in p.stdout.decode().splitlines():
            m = re.fullmatch(r'^_NET_ACTIVE_WINDOW.*[)].*window id # (0x[0-9a-f]+)', line)
            if m:
                self.embedWindow(int(m.group(1), 16))

                # this is where the magic happens...
                self.external.finished.connect(self.close_maybe)
                break
        else:
            QMessageBox.warning(self, 'Error',  'Could not find WID for curreent Window')

    def close_maybe(self):
        pass

    def closeEvent(self, event):
        self.external.terminate()
        self.external.waitForFinished(1000)

    def embedWindow(self, wid):
        window = QWindow.fromWinId(wid)
        # window.setFlag(Qt.FramelessWindowHint, True)
        widget = QWidget.createWindowContainer(
            window, self, Qt.FramelessWindowHint)
        self.layout().addWidget(widget)


if __name__ == '__main__':

    if len(sys.argv) > 1:
        if shutil.which(sys.argv[1]):
            app = QApplication(sys.argv)
            window = Window(sys.argv[1], sys.argv[2:])
            window.setGeometry(100, 100, 800, 600)
            window.show()
            sys.exit(app.exec_())
        else:
            print('could not find program: %r' % sys.argv[1])
    else:
        print('usage: python %s <external-program-name> [args]' %
              os.path.basename(__file__))

The completely equivalent PyQt5 version (just cosmetic variations and attempts to pass more Flags):

import os
import re
import shutil
import subprocess
import sys
import time
from PyQt5.QtCore import QProcess, Qt
from PyQt5.QtGui import QWindow
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QApplication


class Window(QWidget):
    def __init__(self, program, arguments):
        super().__init__()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
        self.external = QProcess(self)
        self.external.start(program, arguments)
        time.sleep(1)
        p = subprocess.run(['xprop', '-root'], stdout=subprocess.PIPE)
        for line in p.stdout.decode().splitlines():
            m = re.fullmatch(r'^_NET_ACTIVE_WINDOW.*[)].*window id # (0x[0-9a-f]+)', line)
            if m:
                win = QWindow.fromWinId(int(m.group(1), 16))
                win.setFlag(Qt.ForeignWindow, True)
                win.setFlag(Qt.FramelessWindowHint, True)
                win.setFlag(Qt.BypassGraphicsProxyWidget, True)
                wid = QWidget.createWindowContainer(win, self, Qt.FramelessWindowHint)
                self.layout().addWidget(wid)

                # this is where the magic happens...
                self.external.finished.connect(self.close_maybe)
                break
        else:
            QMessageBox.warning(self, 'Error',  'Could not find WID for curreent Window')

    def close_maybe(self):
        pass

    def closeEvent(self, event):
        self.external.terminate()
        self.external.waitForFinished(1000)


if __name__ == '__main__':

    if len(sys.argv) > 1:
        if shutil.which(sys.argv[1]):
            app = QApplication(sys.argv)
            window = Window(sys.argv[1], sys.argv[2:])
            window.setGeometry(100, 100, 800, 600)
            window.show()
            sys.exit(app.exec_())
        else:
            print('could not find program: %r' % sys.argv[1])
    else:
        print('usage: python %s <external-program-name> [args]' %
              os.path.basename(__file__))

behaves in the same (wrong) way. I want to have both versions to reach a larger amount of developers as I suspect this is a rather corner case and not many programmers have the needed expertise.

This code has two problems:

  1. [unimportant] Closing check (self.external.finished.connect(self.close)) does not work because gnome-terminal actually communicates with underlying gnome-terminal-server, requests a new window and then exits immediately.
  2. [question topic] Embedding doe not take place. I end up with two different windows (original gnome-terminal is resized and moved over MainWindow, but remains distinct). gnome-terminal is closed at application shutdown as expected.

Uncommenting window.setFlag(Qt.FramelessWindowHint, True) embedding does happen, but there's no redraw and I get a pretty useless movable/resizable window with static background content. The underlying gnome-terminal is functional: blindly typing commands in the "frozen" window *will * execute them.

Setting win.setFlag(Qt.ForeignWindow, True) has no visible effect.

Note also "a certain degree" of embedding is achieved because resizing application window will also resize gnome-terminal window and will move it at top left of screen (0, 0).

This must be something with underlying Qt5 libraries because I get the exactly identical behavior also with PyQt5.

What should I try?

ZioByte
  • 2,690
  • 1
  • 32
  • 68
  • @eyllanesc: why did you remove the pyqt5 and qt5 tags? I specified explicitly I *did* test and behavior is the same. I just chose to post a single version (my current tests are under PyQt5, but they will stop soon because I have no more ideas :( ) – ZioByte Feb 06 '21 at 16:23
  • The code you provide is written in pyside2 (or am I seeing something different?) So the pyqt5 tag doesn't make sense. Although both binding are similar, they always have differences that can affect the solution, so please use the specific tags and do not abuse them. – eyllanesc Feb 06 '21 at 16:27
  • @ZioByte On arch linux with openbox wm, the terminal embeds correctly and behaves normally afterwards. The closing issue can be fixed by running the script like this: `python3 embed.py gnome-terminal --wait` (and by also using `self.external.finished.connect(self.close)`). I would guess your embedding issues are caused by whatever window manager you are using. For me, none of the window-flags seem to have any effect one way or the other. PS: I can type commands and execute them only after clicking on the embedded window (i.e. by switching keyboard focus). – ekhumoro Feb 06 '21 at 19:50
  • @ekhumoro: thanks. I am on a fairly up-to-date Linux Mint (20.1 "Ulyssa") with "cinnamon" desktop. I can setup some different distribution/wm for testing, if you think that may be useful. I am usually testing by running directly from Pycharm-Pro, but i cannot se differences when running from command-line. OTOH I strongly doubt using `self.external.finished.connect(self.close)` can be useful because `esternal` *does* finish quite soon (even when you type "gnome-terminal" at the commandline it does return immediately, before background `gnome-terminal-server` opens a new window). – ZioByte Feb 06 '21 at 20:58
  • @ZioByte I know `external.finished` works, because I tested it. You need to start `gnome-terminal` with the `--wait` option. I suggest you try it too because it may also help with the embedding issue. I am using openbox-3.6.1 with xorg-1.20.10, and don't have any problems at all embedding gnome-terminal-3.38.2 (with or without the `--wait` option). – ekhumoro Feb 06 '21 at 21:43
  • @ekhumoro: I stand corrected. `--wait` really works and allows using `self.close`. Unfortunately this doen't change embedding behavior. I'm now installing a VirtualBox VM (debian10/lxde) for further testing. Thanks. – ZioByte Feb 06 '21 at 22:08
  • @ekhumoro: Ok. verified. under debian-buster/lxde the same identical code works wirth both "native" lxterminal and gnome-terminal (although he latter will not shut down cleanly, but this is another issue). Question now becomes: is there a(n easy) way to strengthen this code to make it portable across WM/distributions? (I don't care about going MSWindows/OSx) I'm a bit scared (Linux Mint is installed on *many* machines, after all). On the original machine lxterminal behaves like gnome-terminal, pointing straight to WM/environment, as you said. – ZioByte Feb 06 '21 at 23:17
  • @ZioByte Qt's [support for embedding foreign windows is incomplete](https://gist.github.com/torarnv/c5dfe2d2bc0c089910ce#issues-with-the-current-qt-apis), so there's no guarantee that it will work for any particular platform or WM. I would guess that the chances of finding a reliable, portable solution are pretty low. You might have better luck with xterm/urxvt, which have built-in support for embedding - although I see from [this answer](https://stackoverflow.com/q/51975678/984421), that you're already aware of it. Why can't you use that? – ekhumoro Feb 07 '21 at 01:04

0 Answers0