Widgets not displaying with macOS vibrancy (NSVisualEffectView) - python

I have currently got a PyQt5 application which is quite simple (just one button). I'd like for it to have vibrancy, so I've ported ObjC vibrancy code to Python. My vibrancy code is as follows:
frame = NSMakeRect(0, 0, * self.get_screen_size()) # get_screen_size returns the resolution of the monitor
view = objc.objc_object(c_void_p=self.winId().__int__()) # returns NSView of current window
visualEffectView = NSVisualEffectView.new()
visualEffectView.setAutoresizingMask_(NSViewWidthSizable|NSViewHeightSizable) # equivalent to: visualEffectView.autoresizingMask = NSViewW...
visualEffectView.setFrame_(frame)
visualEffectView.setState_(NSVisualEffectStateActive)
visualEffectView.setMaterial_(NSVisualEffectMaterialDark)
visualEffectView.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
window = view.window()
window.contentView().addSubview_positioned_relativeTo_(visualEffectView, NSWindowBelow, None)
# equal to: [window.contentView addSubview:visualEffectView positioned:NSWindowBelow relativeTo:nul]
window.setTitlebarAppearsTransparent_(True)
window.setStyleMask_(window.styleMask() | NSFullSizeContentViewWindowMask) # so the title bar is also vibrant
self.repaint()
All I'm doing to draw a button is: btn = QPushButton("test", self)
self is a class inherited from QMainWindow and everything else should be fairly unimportant.
Behaviour with the window.contentView().addSubview... line commented out (no vibrancy)
Behaviour without it commented out (with vibrancy)
Thanks!

The UI elements are actually drawn, but the NSVisualEffectView is covering them.
I fixed that issue by adding another view QMacCocoaViewContainer to the window.
You'll also need to set the Window transparent, otherwise the widgets will have a slight background border.
If you use the dark vibrant window also make sure to set appearance correctly, so buttons, labels etc. are rendered correctly.
frame = NSMakeRect(0, 0, self.width(), self.height())
view = objc.objc_object(c_void_p=self.winId().__int__())
visualEffectView = NSVisualEffectView.new()
visualEffectView.setAutoresizingMask_(NSViewWidthSizable|NSViewHeightSizable)
visualEffectView.setWantsLayer_(True)
visualEffectView.setFrame_(frame)
visualEffectView.setState_(NSVisualEffectStateActive)
visualEffectView.setMaterial_(NSVisualEffectMaterialUltraDark)
visualEffectView.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
visualEffectView.setWantsLayer_(True)
self.setAttribute(Qt.WA_TranslucentBackground, True)
window = view.window()
content = window.contentView()
container = QMacCocoaViewContainer(0, self)
content.addSubview_positioned_relativeTo_(visualEffectView, NSWindowBelow, container)
window.setTitlebarAppearsTransparent_(True)
window.setStyleMask_(window.styleMask() | NSFullSizeContentViewWindowMask)
appearance = NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark')
window.setAppearance_(appearance)
Result:

Related

PyQt6 set position of input field and button

I'm trying to create in PyQt6 an input field (location: bottom right of the window) and next to It on the right an "Enter" button (I'm using pg.GraphicView()). I can't use the PySide library because of some interaction problems with the rest of my code. How can I achieve that?
I'm using the following code for generating a button but I can't figure out how to place It at the bottom right of the current window:
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
proxy = QGraphicsProxyWidget()
button = QPushButton("ENTER")
proxy.setWidget(button)
view.addItem(proxy)
Regarding the input field I tried to implement different things without using PySide but they didn't worked.
The pg.GraphicsView class is actually a subclass of QGraphicsView, which is a standard QWidget that inherits from QAbstractScrollArea.
This means that we can potentially add any child widget to it without interfering with the contents of the scene and also ignoring possible transformations (scaling, rotation, etc.).
The solution, in fact, is quite simple: set the view as parent of the widget (either by using the view as argument in the constructor, or by calling setParent()). Then, what's left is to ensure that the widget geometry is always consistent with the view, so we need to wait for resize events and set the new geometry based on the new size. To achieve this, the simplest solution is to create a subclass.
For explanation purposes, I've implemented a system that allows to set widgets for any "corner" of the view, and defaults to the bottom right corner.
Note: since this question could also be valid for Qt5, in the following code I'm using the "old" enum style (Qt.Align...), for newer versions of Qt (and mandatory for PyQt6), you need to change to Qt.Alignment.Align....
class CustomGraphicsView(pg.GraphicsView):
toolWidgets = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.toolWidgets = {}
def setToolWidget(self, widget, position=Qt.AlignBottom|Qt.AlignRight):
position = Qt.AlignmentFlag(position)
current = self.toolWidgets.get(position)
if current:
current.deleteLater()
self.toolWidgets[position] = widget
widget.resize(widget.sizeHint())
# ensure that the widget is reparented
widget.setParent(self)
# setParent() automatically hides widgets, we need to
# explicitly call show()
widget.show()
self._updateToolWidgets()
def _updateToolWidgets(self):
if not self.toolWidgets:
return
viewGeo = self.rect()
for pos, widget in self.toolWidgets.items():
rect = widget.rect()
if pos & Qt.AlignLeft:
rect.moveLeft(0)
elif pos & Qt.AlignRight:
rect.moveRight(viewGeo.right())
elif pos & Qt.AlignHCenter:
rect.moveLeft(viewGeo.center().x() - rect.width() / 2)
if pos & Qt.AlignTop:
rect.moveTop(0)
elif pos & Qt.AlignBottom:
rect.moveBottom(viewGeo.bottom())
elif pos & Qt.AlignVCenter:
rect.moveTop(viewGeo.center().y() - rect.height() / 2)
widget.setGeometry(rect)
def resizeEvent(self, event):
super().resizeEvent(event)
self._updateToolWidgets()
# ...
view = CustomGraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
container = QFrame(autoFillBackground=True, objectName='container')
container.setStyleSheet('''
QFrame#container {
background: palette(window);
border: 1px outset palette(mid);
border-radius: 2px;
}
''')
lineEdit = QLineEdit()
button = QPushButton("ENTER")
frameLayout = QHBoxLayout(container)
frameLayout.setContentsMargins(0, 0, 0, 0)
frameLayout.addWidget(lineEdit)
frameLayout.addWidget(button)
view.setToolWidget(container)
bottomLeftButton = QPushButton('Something')
view.setToolWidget(bottomLeftButton, Qt.AlignLeft|Qt.AlignBottom)

Python GTK 3 Table: Buttons should not expand

Using following code, I have the problems, that the buttons "Log Leeren" and "Auto Scroll" change their height, when I resize the window. They should be exactly one text-line high and the rest of the viewport should be used by the scrolledWindow
What do I need to change:
class ConsoleLogWindow(Gtk.Window):
def __init__(self, server):
self.log = server["log"];
Gtk.Window.__init__(self, title="Meteor Log von %s" % server["name"])
# self.set_icon_from_file("filename")
self.set_size_request(800,500)
table = Gtk.Table(3, 2, False)
self.add(table)
# Should be only one line of thext high
self.button_clear = Gtk.Button(label="Log Leeren")
self.button_scroll = Gtk.Button(label="Auto Scroll")
table.attach(self.button_clear, 2, 3, 1, 2)
table.attach(self.button_scroll, 0, 1, 1, 2)
# should take as much space as is available.
scrollWindow = Gtk.ScrolledWindow()
scrollWindow.set_hexpand(False)
scrollWindow.set_vexpand(True)
self.content_table = Gtk.Table(len(self.log)+1, 4, False)
# self.content_table is filled here.
scrollWindow.add(self.content_table)
table.attach(scrollWindow, 0, 3, 0, 1)
the window class is called in a function like:
def show_errors_menu(self, widget):
print ("Showing Error Menu")
win = ConsoleLogWindow(widget.get_node());
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
Don't use Gtk.Table; it does not always respect the expand and align properties of its child widgets. Its replacement since GTK 3.0 has been Gtk.Grid. Using that, you only have to make sure that expand is set to true on the scrolled window and false on the buttons.
In the Qt side, at least, the frameworks consider for every widget a size attribute, a minimumSize attribute and a maximumSize attribute. Setting these three attributes to the same value for height and width makes them unresizable. Java's widgets have a explicit resizable attribute. I don't know how GTK3 prevents resizing, but doing some web search I came across this recipe which makes unresizable windows by extending and adding self.set_resizable(False) which is said to work. Give it a try and let me know!

A QWidget like QTextEdit that wraps its height automatically to its contents?

I am creating a form with some QTextEdit widgets.
The default height of the QTextEdit exceeds a single line of text and as the contents' height exceeds the QTextEdit's height, it creates a scroll-bar to scroll the content.
I would like to override this behaviour to create a QTextEdit that would rather wrap its height to its contents. This means that the default height would be one line and that on wrapping or entering a new line, the QTextEdit would increase its height automatically. Whenever the contents height exceeds the QTextEdit's height, the latter should not create a scroll bar but simply increase in height.
How can I go about doing this? Thanks.
This is almost exactly like a question I answer the other day about making a QTextEdit adjust its height in reponse to content changes: PySide Qt: Auto vertical growth for TextEdit Widget
I am answering instead of marking a duplicate as I suspect its possible you want a variation on this. Let me know if you want me to expand this answer:
The other question had multiple parts. Here is the excerpt of the growing height widget:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
the following code sets a QTextEdit widget to the height of the content:
# using QVBoxLayout in this example
grid = QVBoxLayout()
text_edit = QTextEdit('Some content. I make this a little bit longer as I want to see the effect on a widget with more than one line.')
# read-only
text_edit.setReadOnly(True)
# no scroll bars in this example
text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# you can set the width to a specific value
# text_edit.setFixedWidth(400)
# this is the trick, we nee to show the widget without making it visible.
# only then the document is created and the size calculated.
# Qt.WA_DontShowOnScreen = 103, PyQt does not have this mapping?!
text_edit.setAttribute(103)
text_edit.show()
# now that we have a document we can use it's size to set the QTextEdit's size
# also we add the margins
text_edit.setFixedHeight(text_edit.document().size().height() + text_edit.contentsMargins().top()*2)
# finally we add the QTextEdit to our layout
grid.addWidget(text_edit)
I hope this helps.

Placing child window relative to parent in Tkinter python

I have a parent widget which contains a button. When the button is pressed I would like to open a borderless (i.e. no Windows decoration buttons) window directly underneath the parent widget aligned to the left hand side of it. I'm puzzled that the only way (it seems) of setting the position of a window is using .geometry() but worse, I can't seem to get the absolute coordinates of the parent widget - which I need for .geometry(), only the offsets from the parent's parent. So far my code is:
# This is the child which appears when the button is pressed.
class ChildPopUpWindow(Frame):
def __init__(self, parentWgdt):
win = Toplevel(parentWgdt)
geom = str(parentWgdt.winfo_x()) + '+' + str(parentWgdt.winfo_y() + parentWgdt.winfo_height())
win.overrideredirect(1) # No win decoration.
win.bd = 10
win.relief = GROOVE
win.geometry( geom )
Frame.__init__(self, win)
# etc. etc.
# ... and this is the handler for the button being pressed.
def onDropDown(self):
popUp = ChildPopUpWindow(self)
This does pop up a window but relative to the desktop, not to the parent widget. It also seems to take no account of the border thickness and relief as far as I can see. Can anyone offer a way that this can be done? Is .geometry() the way to go or are there better ways?
The short answer is, use winfo_rootx and winfo_rooty to get the coordinates relative to the screen. And yes, wm_geometry is the way to place a toplevel window precisely.
For example:
x = parentWgdt.winfo_rootx()
y = parentWgdt.winfo_rooty()
height = parentWgdt.winfo_height()
geom = "+%d+%d" % (x,y+height)
As a bit of friendly advice, I recommend against abbrev var nms. It makes the code hard to read, especially when the abbreviation is wrong (Wgdt should at least be Wdgt). The difference in code size between geom and geometry, and Wgdt and Widget are tiny, but the difference in readability is huge.
According to Tk manual "https://www.tcl.tk/man/tcl8.4/TkCmd/winfo.htm#M52"
If you need the true width immediately after creating a widget, invoke update to force the geometry manager to arrange it, or use winfo reqwidth to get the window's requested width instead of its actual width.
# This code works perfectly
self.update()
self.geometry("+%d+%d" % (self.parent.winfo_rootx()+50,
self.parent.winfo_rooty()+50
)
)
to centring a modal window about a its parent window, I do so:
alto_modal = 100
ancho_modal = 250
alto_parent = parent.winfo_height()
ancho_parent = parent.winfo_width()
x = (ancho_parent - ancho_modal) // 2
y = (alto_parent - alto_modal) // 2
self.geometry('{}x{}+{}+{}'.format(ancho_modal, alto_modal, x, y))

Python+Qt, QScrollArea problem: what's wrong with this code?

I have the following code:
#!/usr/bin/env python
import sys
from PyQt4 import QtGui, QtCore
class SimfilePanel(QtGui.QWidget):
'''This class provides the simfile panel shown on the right side of the main window.'''
def __init__(self, parent=None):
'''Load song info here.'''
QtGui.QWidget.__init__(self, parent)
## Make widgets.
# Pane with simfile information.
simfileInfoPane = QtGui.QWidget()
simfileInfoPane.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
simfileInfoGrid = QtGui.QGridLayout()
simfileInfoPane.setLayout(simfileInfoGrid)
simfileInfoScrollArea = QtGui.QScrollArea()
simfileInfoScrollArea.setWidget(simfileInfoPane)
#if DEBUG: simfileInfoScrollArea.setBackgroundRole(QtGui.QPalette.Dark);
# This will change
labels = []
textfields = []
for i in range(0,20):
labels.append( QtGui.QLabel("Label "+str(i)) )
textfields.append( QtGui.QLineEdit() )
labels[i].setBuddy(textfields[i])
simfileInfoGrid.addWidget(labels[i], i, 0)
simfileInfoGrid.addWidget(textfields[i], i, 1)
## Put widgets in a grid layout.
mainvbox = QtGui.QVBoxLayout()
mainvbox.addWidget(simfileInfoScrollArea)
self.setLayout(mainvbox)
# Standalone testing
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
panel = SimfilePanel()
panel.show()
sys.exit(app.exec_())
I can't get anything that I'm putting into the simfileInfoGrid to display! They'll display if I leave out the scroll area, but I need the scroll area as I will have a lot of fields to edit in the final version and I don't want to stretch the entire window over the screen.
As you see I've tried to add a size policy to simfileInfoPane, but it doesn't seem to affect anything. The area that's supposed to contain my pane stays empty!
Add the pane to the scroll area after you've added all the grid's contents. In particular you need to call QScrollArea.setWidget after you have finished creating the widget you add.
I don't know exactly why this is the problem, but I do know that I tend to initialize widgets "bottom-up": I finish adding all the contents of a sub-layout before I ever add it to a parent layout. I believe this is Qt optimizing order of rendering but I could be wrong about that.
The code below is a patch, mostly so you can see where the one-line change is.
diff -u 1848547.py tmp2.py
--- 1848547.py 2009-12-04 11:19:09.000000000 -0800
+++ tmp2.py 2009-12-04 11:34:58.000000000 -0800
## -19,7 +19,6 ##
simfileInfoPane.setLayout(simfileInfoGrid)
simfileInfoScrollArea = QtGui.QScrollArea()
- simfileInfoScrollArea.setWidget(simfileInfoPane)
#if DEBUG:
simfileInfoScrollArea.setBackgroundRole(QtGui.QPalette.Dark)
## -33,6 +32,8 ##
simfileInfoGrid.addWidget(labels[i], i, 0)
simfileInfoGrid.addWidget(textfields[i], i, 1)
+ simfileInfoScrollArea.setWidget(simfileInfoPane)
+
## Put widgets in a grid layout.
mainvbox = QtGui.QVBoxLayout()
mainvbox.addWidget(simfileInfoScrollArea)
I have recently been struggling with the same thing, and I believe I have found the solution you are looking for.
The problem seems to be that when you add an empty widget to the scroll area, it has dimensions of zero by zero (since there is nothing inside of it).
The reason it doesn't get bigger is because there is a flag called widgetResizable that is False by default.
By simply calling setWidgetResizable(True) on the scroll area, the widget gets bigger as new items are added.
I hope this helps.

Categories