Repository: qutebrowser/qutebrowser

Issue:
Feature: option to hide title bar (frameless/borderless)
It would be great if there was an option to hide qute's title bar. I know that usually this is a task for the window manager but it would be a nice addition and would be great for people (like me) that doesn't use a window manager with support for this kind of customization.

Taking from [this stackoverflow question](https://stackoverflow.com/a/20290751/1438462) it looks simple to implement. All we'd have to do is set this window flag:
```
setWindowFlags(Qt::Window | Qt::FramelessWindowHint)
```


Discussion:
I've tried that before, but it means you can't move/resize the window anymore, which isn't really acceptable.
I didn't know about that, but wouldn't this be ok if the option description explained it? The user could revert this option to move/resize the window, or use an external tool.
Hmm, I guess it could work - however, the window needs to be hidden and re-shown to change those flags, so I'm not sure if moving/resizing like that would work. Out of curiosity, what window manager / DE do you use?
I use macOS's default wm and betterSnapTool for moving/resizing windows.

Another option would be to use a command line argument for this behavior. There'd be no need to re-show the title bar, qb would be started as frameless and stay like that.
If you re-open qutebrowser, then do `:debug-console` and there do:

```python
from PyQt5.QtCore import Qt
win = objreg.get('main-window', scope='window', window=0)
win.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
win.hide()
win.show()
```

it should be frameless, right? Can you then still resize/move it using betterSnapTool?
It is frameless. I'm able to move it using betterSnapTool, but can't resize it.
What if you call `win.setWindowFlags(Qt.Window)`, resize it, and call it with `Qt.FramelessWindowHint` again - does it stay with the new position/size?
It stays with the new position/size.

The only issue that I noticed is that if I set the window to occupy the entire screen, after setting `Qt.FramelessWindowHint` the window (obviously) loses the height of the title bar leaving an empty space at the bottom, but I guess this could be handled when setting it to frameless.
I use [chunkwm](https://github.com/koekeishiya/chunkwm) to manage windows on a mac, and would also like this feature. When I tested the above, I could neither resize the resulting frameless window nor move it with chunkwm. Using the Accessibilty Inspector, I was able to figure out that the role of the window is AXWindow and the subrole is AXUnknown. With that, I was able to get chunkwm to move the window around. But it doesn't seem to be able to resize it.
@dsanson adding `chunkc tiling::rule --owner "Python" --state tile` to my .chunkwmrc + entering the following console debug command made qutebrowser work together with chunkwm without a title bar:
```(python)
from PyQt5.QtCore import Qt
window = objreg.get('main-window', scope='window', window=0)
window.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint)
window.hide()
window.show()
```
(additional cool tip: add `Qt.NoDropShadowWindowHint` to disable shadows which are unneeded in tiling window managers)

The forced tiling rule for Python is not ideal, but maybe it is possible to change qutebrowser title to something other than "Python". I'm fine regardless as I don't really use any other Python apps anyway.

Now, if anyone could tell me how to apply these flags at start (preferably without compiling from source myself) that would be of great help.  
@rien333 Neat! That worked for me too!

> Now, if anyone could tell me how to apply these flags at start (preferably without compiling from source myself) that would be of great help.

If you are willing to run from source (or compile from source), adding the line

```python
self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint)
```
to qutebrowser/mainwindow/mainwindow.py seems to do the trick.

```
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 05482a1d5..c8de407f5 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -169,6 +169,7 @@ class MainWindow(QWidget):
         objreg.register('message-bridge', message_bridge, scope='window',
                         window=self.win_id)
 
+        self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint)
         self.setWindowTitle('qutebrowser')
         self._vbox = QVBoxLayout(self)
         self._vbox.setContentsMargins(0, 0, 0, 0)
```
@dsanson I've been running a similar built like that for a bit now, and it's been working great. Thanks anyway! Would be cool if this were to become an official option maybe (@The-Compiler), so that I don't have to deal with multiple python versions/binaries and virtual environments and the like. Oh and, an additional stupid quirk of this method is that tool tips will be floated as well due to chunkwm forcing all Python windows to be floated, which can be really annoying. EDIT: (I think building qutebrowser as an .app fixes this, but this has been quite a pain so far)

~Bit off topic maybe, but it does really bother me now that there is no way to let chunkwm/skhd spawn new qutebrowser windows as far as I know of (say, by means of scripting).~ EDIT: stupid me, you can spawn a new window globally by just calling `$ qutebrowser`
Hmm, still not sure how an option for this would look. I suppose there could just be an option to set any window type/flags, but I'm not looking forward to the issues people will get themselves into with that :wink:

What I don't really understand so far: What's the difference between `Qt::FramelessWindowHint` and `Qt::CustomizeWindowHint`?


I wasn't really suggesting an option to set any QT window flags, but more something along the lines of `c.window.hide_wayland_decoration = True` for hiding the macOS titlebar. Say something like `c.window.hide_cocoa_titlebar = True`/`c.window.hide_cocoa_decoration = True`. Or make a general option to hide window decorations, and possibly document it as only working under wayland and macOS (for now). 

The  difference between those two types of window hints is that the [QT documentation](https://doc.qt.io/archives/qtjambi-4.5.2_01/com/trolltech/qt/core/Qt.WindowType.html) states `Qt::FramelessWindowHint` to not be resizable:
> Produces a borderless window. The user cannot move or resize a borderless window via the window system.

The `QT::CustomizeWindowHint` on the other hand produces a window that only disables the window title (in the case of cocoa, the titlebar)
> Turns off the default window title hints.

In practice, a window with `Qt::FramelessWindowHint` set produces a window that is fairly unusable and only meant for very specific purposes such as dialogue boxes. It's a pretty specific cocoa window type probably. `Qt::FramelessWindowHint` still allows window control (resizing, moving, mouse resizing), especially in conjunction with the various linux-y window managers and snapping tools available for macOS. In essence it's like any other cocoa window, but without the aqua/standard UI titlebar. The idea of the option mentioned above should therefore probably stay clear of using the  `Qt::FramelessWindowHint` and use something like the code provided by @dsanson.
Hm, I like the more general `window.hide_decoration` option, and adding migrations to `configfiles.py` for that should be straightforward too. Anyone here feels like picking this up? I'm currently busy with other stuff and reviewing PRs, so it'll probably take a while until I get to this.

@rien333, also let me note that you linked to some ~2009 version of Qt Jambi (Qt for Java) documentation there :wink: The up-to-date docs are here: http://doc.qt.io/qt-5/qt.html#WindowType-enum
I'm picking this up, but I have some questions. As this is relatively easy I hope answering my questions doesn't take you longer than implementing it yourself. 

Firstly, does migrating the old settings entail that the old value for `window.wayland_decoration` should be copied over to `window.hide_decoration`? (I already wrote the code for this behavior I think)

Secondly, I don't think it makes much sense conceptually to have window shadows when using tiling window managers,  which I think make up the fair majority of the people hiding window decorations given that (most) other types of window managers lack the options to deal with borderless windows.  Is it a good idea to disable window shadows when the user wants to hide window decorations?  iTerm and mpv (at least on non macOS systems, and on older versions) for example also have this behavior when the window decorations are hidden. 

Borderless windows can look a bit odd under some circumstances though, so it might also be a possibility to add another config item which determines window shadow visibility. Or just always leave shadows on, it's your call. (I'm pretty sure most people would prefer hiding shadows though) 
Argh, something went wrong when trying to write my answer before, let's try again.

> I'm picking this up, but I have some questions. As this is relatively easy I hope answering my questions doesn't take you longer than implementing it yourself. 

That happens from time to time (with reviews and all), but it's not necessarily bad - I like getting people to contribute to open source (and qutebrowser) - and some of those people stick around and become regular contributors :smile:

> Firstly, does migrating the old settings entail that the old value for `window.wayland_decoration` should be copied over to `window.hide_decoration`? (I already wrote the code for this behavior I think)

Indeed - this would be in `qutebrowser/config/configfiles.py` in `_handle_migrations`. Make sure to also add a test in `tests/unit/config/test_configfiles.py` (similar to `test_merge_persist`).

> Secondly, I don't think it makes much sense conceptually to have window shadows when using tiling window managers,  which I think make up the fair majority of the people hiding window decorations given that (most) other types of window managers lack the options to deal with borderless windows.  Is it a good idea to disable window shadows when the user wants to hide window decorations?  iTerm and mpv (at least on non macOS systems, and on older versions) for example also have this behavior when the window decorations are hidden. 
> 
> Borderless windows can look a bit odd under some circumstances though, so it might also be a possibility to add another config item which determines window shadow visibility. Or just always leave shadows on, it's your call. (I'm pretty sure most people would prefer hiding shadows though) 

Let's just hide them for borderless windows and see if someone complains :wink:

Resolving patch:
diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py
--- a/qutebrowser/config/configinit.py
+++ b/qutebrowser/config/configinit.py
@@ -90,7 +90,7 @@ def _init_envvars():
     if config.val.qt.force_platform is not None:
         os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
 
-    if config.val.window.hide_wayland_decoration:
+    if config.val.window.hide_decoration:
         os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
 
     if config.val.qt.highdpi:
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -230,6 +230,7 @@ def __init__(self, *, private, geometry=None, parent=None):
         config.instance.changed.connect(self._on_config_changed)
 
         objreg.get("app").new_window.emit(self)
+        self._set_decoration(config.val.window.hide_decoration)
 
     def _init_geometry(self, geometry):
         """Initialize the window geometry or load it from disk."""
@@ -344,6 +345,8 @@ def _on_config_changed(self, option):
         elif option == 'statusbar.position':
             self._add_widgets()
             self._update_overlay_geometries()
+        elif option == 'window.hide_decoration':
+            self._set_decoration(config.val.window.hide_decoration)
 
     def _add_widgets(self):
         """Add or readd all widgets to the VBox."""
@@ -493,6 +496,16 @@ def _connect_signals(self):
             completion_obj.on_clear_completion_selection)
         cmd.hide_completion.connect(completion_obj.hide)
 
+    def _set_decoration(self, hidden):
+        """Set the visibility of the window decoration via Qt."""
+        window_flags = Qt.Window
+        refresh_window = self.isVisible()
+        if hidden:
+            window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
+        self.setWindowFlags(window_flags)
+        if refresh_window:
+            self.show()
+
     @pyqtSlot(bool)
     def _on_fullscreen_requested(self, on):
         if not config.val.content.windowed_fullscreen:


Test patch:
diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py
--- a/tests/unit/config/test_configinit.py
+++ b/tests/unit/config/test_configinit.py
@@ -292,7 +292,7 @@ def test_setting_monospace_fonts_family(self, init_patch, args):
          'QT_XCB_FORCE_SOFTWARE_OPENGL', '1'),
         ('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'),
         ('qt.highdpi', True, 'QT_AUTO_SCREEN_SCALE_FACTOR', '1'),
-        ('window.hide_wayland_decoration', True,
+        ('window.hide_decoration', True,
          'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1')
     ])
     def test_env_vars(self, monkeypatch, config_stub,