Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

WIP: True Borderless Window #49

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open

Conversation

Jerakin
Copy link
Collaborator

@Jerakin Jerakin commented Feb 16, 2020

Creating this PR for discussion, will not be merged in its current state.

Would like to discuss problems and solutions to using a "better" method to create a borderless window. The base work and idea of it comes from @gmarull prep work mentioned here #37 (comment). It seem to be mainly based on this repository https://github.com/dfct/TrueFramelessWindow.

Here are some discussion points:

  • Keep backwards compatible if possible, but this might not be feasible. If not feasible we need to discuss how we should simply bump the version as needed or do more drastic things (i.e. qtmodern2) though I do not like that solution.

  • Support the same platforms, if it is backwards compatible it need to support the same platforms. As of now this only have macOS and Windows support.

  • Keep the same simple interface, in other words wrap your application with the modernwindow and in simple works.

Pros/Cons

There are a few upsides to doing it this way, here are some I can think of.

  • Keep native window features like Aero and snapping on windows
  • The window buttons behaves natively out of the box
  • Window resizing more convenient to implemented

There are also a few downsides.

  • A lot of added complexity, not "one code base for all OS"
  • Currently only macOS and Windows is implemented.
  • This bug on macOS is quite a show stopper QTBUG-81370

Whats left

  • Linux (and other?) support.
  • Would have to make sure the QT version people are using on a macOS machine is a working one (or find a work around for the mentioned bug)
  • Code review
  • Clean up the code

@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 16, 2020

I do not have a machine with Linux on it, therefore it would be hard for me to do the required research to replicate the same kind of behavior.

Alternatively we could solve it by using the "old way" on any machine that isn't macOS or Windows.

@gmarull
Copy link
Owner

gmarull commented Feb 17, 2020

@Jerakin Thanks for this PR, great job. I think it is fine to break compatibility, it's a major change but with significant benefits. Regarding Linux I couldn't find a way to make the window borderless but at the same time keep snap functionality. GTK supports client side decorations and Wayland also allows applications to do so, but I failed trying on X11 + Qt...

One change I'd add is to compile resources, at least for releases. It makes packaging much easier (just add & import a resources_rc.py file).

@razaqq
Copy link
Contributor

razaqq commented Feb 17, 2020

i will try on linux with x11, will report back

@razaqq
Copy link
Contributor

razaqq commented Feb 18, 2020

btw your latest commit 407d82b broke resource paths on windows for me, window buttons are not visible

@talonrenaud
Copy link

This is a great project, thank you for the hard work. Any thoughts on talking to the Qt team about sharing your work with them to add this to a future Qt release ?

@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 19, 2020

btw your latest commit 407d82b broke resource paths on windows for me, window buttons are not visible

Were a typo (icon instead of icons), sorry about that :)

@razaqq
Copy link
Contributor

razaqq commented Feb 19, 2020

there is another error:

CLOSE_ICON = ':/icon/maximize.svg'

should instead be

CLOSE_ICON = ':/icon/close.svg'

Another few small things I found while testing (windows):

  • After minimizing/maximizing, the window button used stays selected/highlighted
  • calling resize() or move() on QMainWindow does not work anymore, this is especially important for applications which save window size and position and restore them at startup
  • setWindowTitle not working

i havent got it to work on x11 yet, will try again in the next days
will probably have to use python-xlib

@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 20, 2020

Great comments!

there is another error:
Opps! Will commit the fix

After minimizing/maximizing, the window button used stays selected/highlighted
Works as intended on macOS, will check on my windows PC later

calling resize() or move() on QMainWindow
I do not think this have ever worked. It's related to this #23 and #41 - Would love some ideas on how to solve this.

setWindowTitle not working
Related to the above one, but this one used to have a separate fix for it. Should add that one back in for backwards compatibility.

i havent got it to work on x11 yet, will try again in the next days
Awesome!

@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 20, 2020

calling resize() or move() on QMainWindow
If someone stumbles on this: For this you would have to save the reference to the modern window and use that, like something like this.

class MainWindow(QMainWindow):
    def __init__(self):
        mw.move(500, 400)

if __name__ == '__main__':
    app = QApplication(sys.argv)

    qtmodern.styles.dark(app)
    mw = ModernWindow(MainWindow())
    mw.show()

    sys.exit(app.exec_())

@razaqq
Copy link
Contributor

razaqq commented Feb 20, 2020

calling resize() or move() on QMainWindow used to work. I know because I was using it ;)

@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 20, 2020

Does it still work for you on master?

This is the behaviour I get when trying self.move and self.resize on macOS.

Move

move

Resize

resize

@razaqq
Copy link
Contributor

razaqq commented Feb 20, 2020

Actually its not working anymore. Im using an older version 0.1.4 in my app with that one its fine. It must have been broken somewhen since then

@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 20, 2020

That's bad, we definitely need to figure that out. Might have some time this weekend, will try then if someone isn't faster :) Tried it out on 0.1.4 and I can not make it work 🤔

@razaqq
Copy link
Contributor

razaqq commented Feb 21, 2020

it does work, but you need to put it in __init__ of QMainWindow. You cant call it after creating ModernWindow. So you can not assign it to a button. I just tried again, it works in current master branch aswell (sorry for assuming it was broken since 0.1.4), just the borderless branch has that issue. I think its really important to find a workaround for this, since its not possible to save window size and position otherwise.

EDIT: okay on the borderless branch you have to use resize() and move() on ModernWindow (only tried in __init__) and not QMainWindow anymore, but then it works just fine!

Also I found out that closeEvent() does not get called on QMainWindow, this can be easily fixed, by adding this to ModernWindow:

    def __init__(self, window):
        super().__init__()
        self.w = window

    def closeEvent(self, event):
        self.w.closeEvent(event)

@razaqq
Copy link
Contributor

razaqq commented Feb 21, 2020

@Jerakin could you give me write access to your fork, so i can commit progress on X11? Im not changing any of the other stuff.

Btw its a bit more tricky than the other implementations, because you need to wait for the window to actually be created and shown, before you can make any changes to it.
Also the docs are horrible...

@talonrenaud
Copy link

After minimizing/maximizing, the window button used stays selected/highlighted
Works as intended on macOS, will check on my windows PC later

Last 2 lines below fixes that issue:

    def eventFilter(self, obj, event):
        if event.type() == QEvent.WindowStateChange:
            if self._window.windowState() == Qt.WindowMaximized:
                self.btn_maximize.setVisible(False)
                self.btn_restore.setVisible(True)
            else:
                self.btn_maximize.setVisible(True)
                self.btn_restore.setVisible(False)
            
            # brute forces hover state off
            self.btn_restore.setAttribute(Qt.WA_UnderMouse, False)
            self.btn_maximize.setAttribute(Qt.WA_UnderMouse, False)

@razaqq
Copy link
Contributor

razaqq commented Feb 21, 2020

I also found a cool implementation for ModernDialog, that is far simpler than on master.

class ModernDialog(QDialog, BorderlessWindow):
    def __init__(self, parent):
        QDialog.__init__(self, parent=parent)
        BorderlessWindow.__init__(self, parent=self)

        self.window_content = QWidget(self)
        self.hLayout = QVBoxLayout(self)
        self.hLayout.setContentsMargins(0, 0, 0, 0)
        self.hLayout.setSpacing(0)

        self.title_bar = _WindowsTitleBar(self, self)
        self.add_window_mover(self.title_bar.lbl_title)

        self.title_bar.application_icon.setVisible(False)
        self.title_bar.btn_close.setVisible(False)
        self.title_bar.btn_maximize.setVisible(False)
        self.title_bar.btn_minimize.setVisible(False)
        self.title_bar.btn_restore.setVisible(False)

        self.hLayout.addWidget(self.title_bar)
        self.hLayout.addWidget(self.window_content)

    def setLayout(self, layout):
        self.window_content.setLayout(layout)

you dont need to add the titlebar if you dont want (i have hidden all widgets from it here).
this creates you some popups like this (Window is still moveable through titlebar)

grafik

EDIT:
Actually have one issue with this: If I do it like in the example obove, setting QMainWindow as parent results in the window mover of the parent being disabled, even after the dialog has been cancelled. If you dont set QMainWindow as parent, the Dialog does not spawn centered... Havent found a solution for this yet.

Fixed

@razaqq
Copy link
Contributor

razaqq commented Feb 21, 2020

Another few points that i found:

  • Can resize the ModernWindow up- and downwards, but not sideways (EDIT: only happened once and has not happened since, will keep an eye on it; EDIT 2: Happened again...)
    gif

  • Spacing on title bar seems off (very close together). Is this intentional? If not move window title and icon into serparate widget and set spacing to 5 and left margin to 10 maybe? Should be more like this imo
    grafik

  • Window title and icon setting can be fixed as follows (this allows changing it from both ModernWindow and QMainWindow). Its a bit hacky, but not too bad i think

Add to _WindowsTitleBar:

        icon = self._window.windowIcon() if not self._window.windowIcon().isNull() else self._window._window.windowIcon()
        self.application_icon.setIcon(icon)
        self._window.windowIconChanged.connect(self.on_window_icon_changed)
        self._window._window.windowIconChanged.connect(self.on_window_icon_changed)

        title = self._window.windowTitle() if self._window.windowTitle() else self._window._window.windowTitle()
        self.lbl_title.setText(title)
        self._window.windowTitleChanged.connect(self.on_window_title_changed)
        self._window._window.windowTitleChanged.connect(self.on_window_title_changed)

Add QMainWindow as member to ModernWindow:

class ModernWindow(BorderlessWindow):
    def __init__(self, window):
        self._window = window

Renamed variables so the code is easier to follow, fixed bug where self was sent in twice to _WindowsTitleBar making titles not work
@Jerakin
Copy link
Collaborator Author

Jerakin commented Feb 22, 2020

Can resize the ModernWindow up- and downwards, but not sideways (EDIT: only happened once and has not happened since, will keep an eye on it; EDIT 2: Happened again...)

I can not reproduce this, are you testing on a special window or the example window provided by qtmodern?

@razaqq
Copy link
Contributor

razaqq commented Feb 22, 2020

Can resize the ModernWindow up- and downwards, but not sideways (EDIT: only happened once and has not happened since, will keep an eye on it; EDIT 2: Happened again...)

I can not reproduce this, are you testing on a special window or the example window provided by qtmodern?

was custom, will try with default but I dont think that should matter

@razaqq
Copy link
Contributor

razaqq commented Feb 23, 2020

Some findings on X11

I have spent countless hours trying to get this to work. The only thing I could not get to work, was to remove decorations (frame and buttons) from the window.

Every window manager handles window decorations separately. Some use NETWM and some use Motif hints for changing properties of the window.

The implementation of how to hide the default decoration will depend on the window manager used. This is already bad in itself, but could be solved.

In my case KWin uses NETWM.

Window hints i have tried with no or only partial success:

  1. setting _NET_FRAME_EXTENTS to [0, 0, 0, 0] has no effect, negative values dont work because the underlaying c functions use unsigned ints
  2. _NET_WM_WINDOW_TYPE
    1. _NET_WM_WINDOW_TYPE_DOCK hides the border, but window is always on top
    2. _NET_WM_WINDOW_TYPE_TOOLBAR actually hides window buttons, but not the border

I have found many discussions about people requesting implementation of _NET_WM_STATE_UNDECORATED, but its not yet supported.

Other window managers support it, however we are obviously looking for a universal solution.

The solution I see is to let the window manager handle the window frame. Any other application does it the same way. Then people can choose their own theme for frame and icons. It also unifies the look with the rest of the system.

I will keep looking for a solution, but there might not be an easy one.

@razaqq
Copy link
Contributor

razaqq commented Mar 4, 2020

Can resize the ModernWindow up- and downwards, but not sideways (EDIT: only happened once and has not happened since, will keep an eye on it; EDIT 2: Happened again...)

I can not reproduce this, are you testing on a special window or the example window provided by qtmodern?

okay i am able to reproduce this with the example window too. It happens only on my 2nd monitor (not set as primary). On my main monitor its not happening. Like i can just drag the window over and it stops working. This is very, very weird.

@gmarull
Copy link
Owner

gmarull commented Mar 4, 2020

NOTE: These changes may not be relevant after Qt 5.15 is released, see https://github.com/johanhelsing/qt-csd-demo

@razaqq
Copy link
Contributor

razaqq commented May 16, 2020

https://www.qt.io/blog/custom-window-decorations

final release planned in 3 days from now
https://wiki.qt.io/Qt_5.15_Release

They only provide you with the following:
bool QWindow::startSystemResize(Qt::Edges edges)
bool QWindow::startSystemMove()
which tells the native window handler to either start resizing or moving the top-level window.
Notice this doesnt work on QWidgets, just QWindow.

This means Qt::FramelessWindowHint is still necessary and thus adding a custom titlebar and handling close/max/min button events.

For moving you can do smth like this

void NativeWindow::mousePressEvent(QMouseEvent* event)
{
	if (this->titleBar->underMouse())
		this->windowHandle()->startSystemMove();
};

@bactone
Copy link

bactone commented Apr 24, 2021

hi, another problem, when use the frameless window:

mw = qtmwindows.ModernWindow(win)
mw.show()

the windowIcon is disabled. is it fixed in the future version ? for example:

  1. if I use the code:
    app = QApplication(sys.argv)
    win = MainWindow()
    qtmstyles.dark(app)
    win.show()
    the window shows correctly.

  2. if I change it to:
    app = QApplication(sys.argv)
    win = MainWindow()
    qtmstyles.dark(app)
    mw = qtmwindows.ModernWindow(win)
    mw.show()
    the window icon is not shown.

@tisaconundrum2
Copy link

@bactone that sounds like an issue with qtmwindows.ModernWindow it's the only difference between those two pieces of code. Have you tried debugging within qtmwindows to see why it may not be allowing the icon through?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants