IMAGINARY / applauncher2

A graphical application launcher based on web technologies Mk2
Apache License 2.0
5 stars 1 forks source link

Back/exit button for executable apps #5

Open porst17 opened 5 years ago

porst17 commented 5 years ago

Currently, external executable apps need to provide their own way to exit to get back to the applauncher. Most executable apps don't provide such a mechanism because they rely on the window system to deal with that and workarounds like qclosebutton are not ideal and quite annoying to work with.

The back button introduced in #4 could be used as a close/exit button so we could have the same visuals for all kind of apps. I imagine implementing this by opening an additional BrowserWindow of the kiosk browser/electron that is semi-transparent, always-on-top and covers only a small part of the screen. This should be implemented by some plugin I guess because it would only work from within an electron based browser with node integration. On the other hand, the executable apps are currently implemented within the applauncher. So I am not sure about the best place to implement this.

elondaits commented 5 years ago

Is this:

Opening an additional BrowserWindow of the kiosk browser/electron that is semi-transparent, always-on-top and covers only a small part of the screen

even possible in electron? Particularly always-on-top + transparent are things very much OS / Window manager dependent... not something you see often on Mac OS for instance... so I'd be a bit surprised that electron allows this. If it does, no problem.

On the other hand, having a plugin that hooks into executable app launch and launches a native program at the same time (the "close" button) is not a problem... the harder part would be how the button program signals the appLauncher to kill the proper app. We could do it via websockets but it's awfully overkill. There aren't many RPC mechanisms available on client-side JS, so we'd have to add something to the kiosk browser... the simpler would be a signal handler... if not something like POSIX message queues... things get ugly architecturally at this point, because the appLauncher is just a page and it's technically possible for many instances to exist at the same time (and sometimes they do, like when we nest an appLauncher within another... but also possible in a multi-screen setting, etc.).

elondaits commented 5 years ago

Second thought: Passing the pid of the process to kill to the close button program on invocation should be enough... I was thinking that a process can change pid if respawning or forking, but I think that I'd loose control of that process from appLauncher anyway if either happens 🤔

porst17 commented 5 years ago

Electron features (can already be applied during window initialization): transparent/frameless, always-on-top, window positioning, window sizing

If these features work indeed depends on the window system, compositor and desktop environment. It will work on our usual openbox/compton setup. On macOS, it is not possible to overlay two apps in fullscreen mode AFAIK. That's why I would probably not add this to the applauncher core, but rather integrate it via a plugin, if possible.

I would close the external executable by killing the PID supplied as an argument somehow. Every message passing approach between the close button and the applauncher seems overkill IMO.

porst17 commented 5 years ago

Multi-screen setups are not so easy to deal with, that's true. I would just ignore that for the moment and rely on the exit button positioning option (top-left, top-right etc.) as a workaround for the moment.

porst17 commented 5 years ago

I thought a bit more about this today and noticed that it might not even be necessary to pass the PID to the plugin. The new browser window that resembles the close button is opened from the plugin that runs within the applauncher that in turn runs in the kiosk-browser respectively electron in general (at least node integration is needed ..). So when the close button is clicked, the plugin will be notified and can directly call back to the applauncher e.g. via the IMAGINARY.* API or whatever seems reasonable.

Maybe passing the PID is simpler because it yields to better separation between the applauncher and the plugin, but at least there is the possibility of direct communication of the close button and the applauncher.

elondaits commented 5 years ago

My suggestion of needing the PID was for the case where the button is not implemented in electron but is the current close button we use, just launched by the browser.

... I agree that if the button is implemented in electron it can be invoked with a callback that does the closing.

porst17 commented 5 years ago

Our qclosebutton takes the command to run as argument, i.e. it starts the program it is supposed to close and is therefore also able to kill it. Same could be done with electron, but with a lot more flexibility because we can utilize full HTML+CSS+JS to implement the close button widget. qclosebutton just displays a static, semi-transparent image on top of all other windows.

porst17 commented 5 years ago

I made some tests today. It works: video

The launcher.html is running in Electron with node integration enabled. When you click "Start app", it will launch an external app and open another BrowserWindow with closebutton.html that is always on top, frameless and transparent. launcher.html will calculate the correct size of the close-button window based on its contents and then position it. The launcher also inserts a hook such that whenever closebutton.html calls IMAGINARY.AppLauncher.closeApp(), a callback in launcher.html is triggered which in this case closes the app and the closebutton.html window.

closebutton.html is just:

<html>
  <body style="width: 40px; height: 40px; margin: 0px; padding: 0px;">
    <a href="javascript:IMAGINARY.AppLauncher.closeApp()">
      <img src="button.svg" width="40" height="40"/>
    </a>
  </body>
</html>

So it is not necessary to expose applauncher (or applaucher plugin) internals to the closebutton.html or the app. I just emulate the regular public AppLauncher API.

launcher.html is

<html>
<head>
</head>
<body>
  <script>
    function open() {
      let { remote } = require('electron');
      let { BrowserWindow } = remote;
      let parentWin = remote.getCurrentWindow();
      let appWin = new BrowserWindow({ parent: parentWin, frame: false });
      appWin.loadURL('http://localhost:8000/app.html');
      let buttonWin = new BrowserWindow({
        fullscreen: false,
        alwaysOnTop: true,
        frame: false,
        transparent: true,
        resizable: false,
        hasShadow: false,
        show: false,
      });
      buttonWin.loadURL('http://localhost:8000/closebutton.html');
      buttonWin.on('close', () => console.log('app closed'));
      buttonWin.webContents.on('dom-ready', () => {
        buttonWin.webContents.executeJavaScript('new Promise((resolve, reject) => window.IMAGINARY = {AppLauncher: {closeApp: resolve}});', true)
          .then(result => {
            buttonWin.close();
            appWin.close();
          });
      });
      buttonWin.once('ready-to-show', () => {
        buttonWin.webContents.executeJavaScript('JSON.parse(JSON.stringify({height:document.body.getBoundingClientRect().height,width:document.body.getBoundingClientRect().width}));', true)
        .then(rect => {
          console.log("closebutton.html window size:" + rect.width + " x " + rect.height);
          let parent_rect = parentWin.getContentBounds();
          buttonWin.setContentSize(rect.width,rect.height);
          buttonWin.setPosition(
            parent_rect.x + parent_rect.width - rect.width,
            parent_rect.y
          );
          buttonWin.show();
        },err => console.log(err));
      });
    }
  </script>
  <br />
  <h1><pre>launcher.html</pre></h1>
  <a href="javascript:open()">Start app</a>
</body>

On Linux, this should also work in fullscreen mode. It currently also does on macOS Mojave, but only because the "external demo app" in this case is just another BrowserWindow that belongs to the same main app (kiosk browser).

porst17 commented 5 years ago

I tested this on Linux and ran into quite some trouble. If the app window is not in full screen mode, it just works fine. However, full screen windows seem to use the always-on-top flag as well (this probably also depends on the window manager) such that the app and the close button were fighting for priority.

The only way I could bypass this and have the close button always above the full screen window was to require('x11') and set X.ChangeWindowAttributes(buttonWin.nativeWindowID, {overrideRedirect: true})), something I needed to do for qclosebutton as well (the Qt::X11BypassWindowManagerHint part). It is a bit annoying to have this platform specific dependency now, but I don't see any way around it. It also needs to be compiled into kiosk-browser, which is easy but still not elegant.