How to override default behavior of the Zoom button in Qt? - macos

I have an application written using Qt5 in Python (PyQt5).
I want to override default behavior of the zoom (green "+" at top left) so it will set appropriate size of the window rather than maximize it to cover all available space.
I tried to override showMaximize, but that method was never called. My window is a subclass of QWidget without any additional window flags set (i.e. defaults are used).
How can I override the event or how can I tell the layout system size I want to be set once user clicks that button?

You cannot override showMaximized because it is not virtual. There is no way to override this action. But you can override changeEvent. It will be called when user maximizes the window. You can reset normal window's position and adjust its size.
void MyWidget::changeEvent(QEvent *e) {
QWidget::changeEvent(e);
if (e->type() == QEvent::WindowStateChange) {
if (isMaximized()) {
showNormal();
resize(200, 200);
move(100, 100);
}
}
}

Related

Restore WinForms window to correct position after Aero Snap

When I drag a WinForms window to the top of the screen to perform a Maximize "Aero Snap", if I then hit the "Restore" button after this, the window is restored to the correct size but at the wrong location. It flickers to the correct location for a moment, but then it immediately moves to the top of the screen with its title bar halfway off the screen.
Apparently, the WinForms window restores to its last dragged location before it was maximized, which was at the top of the screen where it was dragged in order to do the Aero Snap.
This behavior is incorrect and annoying. You can see the correct behavior by following those same steps for a Windows Explorer window. After an Aero Snap, the Windows Explorer window correctly restores to the last dropped restore location (wherever it was sitting before it was dragged to do the Aero Snap).
How can I make a WinForms window restore to the correct location after an Aero Snap, like a Windows Explorer window does?
I could try and hook into the form positioning events and save the last-dropped restore location, and restore that after a Restore after an Aero Snap, but I'm hoping there's a simpler way.
You can override the OnResizeBegin, OnResizeEnd and OnSizeChanged methods to:
store a Form's current Location when it's first dragged (when you begin to drag a Form around the OnResizeBegin is called)
clear the stored values if the Form is released (OnResizeEnd is called) while it's WindowState is FormWindowState.Normal
finally restore the Form.Location when OnSizeChanged notifies that the Form.WindowState changes from FormWindowState.Maximized to FormWindowState.Normal.
If you use a Controller or similar, you can subscribe to the corresponding events instead.
Point beginDragLocation = Point.Empty;
FormWindowState beginDragFormState = FormWindowState.Normal;
protected override void OnResizeBegin(EventArgs e)
{
base.OnResizeBegin(e);
beginDragLocation = this.Location;
}
protected override void OnResizeEnd(EventArgs e)
{
base.OnResizeEnd(e);
if (this.WindowState != FormWindowState.Maximized) {
beginDragLocation = Point.Empty;
}
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
if (beginDragFormState == FormWindowState.Maximized && beginDragLocation != Point.Empty) {
BeginInvoke(new Action(() => this.Location = beginDragLocation));
}
beginDragFormState = this.WindowState;
}

How to align a toolbar (or its items) with the leading edge of a split view controller's child?

In iOS, a toolbar can be added to any view. In macOS however, it seems only possible to add a toolbar to a window.
I'm working on an app with a split view controller with a toolbar but the toolbar's items only have a meaning with respect to the right view controller's context.
E.g. let's say I have a text editor of some sort, where the left pane shows all documents (like in the Notes app) and the right pane shows the actual text which can be edited. The formatting buttons only affect the text in the right pane. Thus, it seems very intuitive to place the toolbar within that right pane instead of stretching it over the full width of the window.
Is there some way to achieve this?
(Or is there a good UX reason why this would be a bad practice?)
I've noticed how Apple solved this problem in terms of UX in their Notes app: They still use a full-width toolbar but align the button items that are only related to the right pane with the leading edge of that pane.
So in case, there is no way to place a toolbar in a view controller, how can I align the toolbar items with the leading edge of the right view controller as seen in the screenshot above?
Edit:
According to TimTwoToes' answer and the posts linked by Willeke in the comments, it seems to be possible to use Auto Layout for constraining a toolbar item with the split view's child view. This solution would work if there was a fixed toolbar layout. However, Apple encourages (for a good reason) to let users customize your app's toolbar.
Thus, I cannot add constraints to a fixed item in the toolbar. Instead, a viable solution seems to be to use a leading flexible space and adjust its size accordingly.
Initial Notes
It turns out this is tricky because there are many things that need to be considered:
Auto Layout doesn't seem to work properly with toolbar items. (I've read a few posts mentioning that Apple has classified this as a bug.)
Normally, the user can customize your app's toolbar (add and remove items). We should not deprive the user of that option.
Thus, simply constraining a particular toolbar item with the split view or a layout guide is not an option (because the item might be at a different position than expected or not there at all).
After hours of "hacking", I've finally found a reliable way to achieve the desired behavior that doesn't use any internal / undocumented methods. Here's how it looks:
How To
Instead of a standard NSToolbarFlexibleSpaceItem create an NSToolbarItem with a custom view. This will serve as your flexible, resizing space. You can do that in code or in Interface Builder:
Create outlets/properties for your toolbar and your flexible space (inside the respective NSWindowController):
#IBOutlet weak var toolbar: NSToolbar!
#IBOutlet weak var tabSpace: NSToolbarItem!
Create a method inside the same window controller that adjusts the space width:
private func adjustTabSpaceWidth() {
for item in toolbar.items {
if item == tabSpace {
guard
let origin = item.view?.frame.origin,
let originInWindowCoordinates = item.view?.convert(origin, to: nil),
let leftPane = splitViewController?.splitViewItems.first?.viewController.view
else {
return
}
let leftPaneWidth = leftPane.frame.size.width
let tabWidth = max(leftPaneWidth - originInWindowCoordinates.x, MainWindowController.minTabSpaceWidth)
item.set(width: tabWidth)
}
}
}
Define the set(width:) method in an extension on NSToolbarItem as follows:
private extension NSToolbarItem {
func set(width: CGFloat) {
minSize = .init(width: width, height: minSize.height)
maxSize = .init(width: width, height: maxSize.height)
}
}
Make your window controller conform to NSSplitViewDelegate and assign it to your split view's delegate property.1 Implement the following NSSplitViewDelegate protocol method in your window controller:
override func splitViewDidResizeSubviews(_ notification: Notification) {
adjustTabSpaceWidth()
}
This will yield the desired resizing behavior. (The user will still be able to remove the space completely or reposition it, but he can always add it back to the front.)
1 Note:
If you're using an NSSplitViewController, the system automatically assigns that controller to its split view's delegate property and you cannot change that. As a consequence, you need to subclass NSSplitViewController, override its splitViewDidResizeSubviews() method and notify the window controller from there. Your can achieve that with the following code:
protocol SplitViewControllerDelegate: class {
func splitViewControllerDidResize(_ splitViewController: SplitViewController)
}
class SplitViewController: NSSplitViewController {
weak var delegate: SplitViewControllerDelegate?
override func splitViewDidResizeSubviews(_ notification: Notification) {
delegate?.splitViewControllerDidResize(self)
}
}
Don't forget to assign your window controller as the split view controller's delegate:
override func windowDidLoad() {
super.windowDidLoad()
splitViewController?.delegate = self
}
and to implement the respective delegate method:
extension MainWindowController: SplitViewControllerDelegate {
func splitViewControllerDidResize(_ splitViewController: SplitViewController) {
adjustTabSpaceWidth()
}
}
There is no native way to achieve a "local" toolbar. You would have to create the control yourself, but I believe it would be simpel to make.
Aligning the toolbar items using autolayout is described here. Align with custom toolbar item described by Mischa.
The macOS way is to use the Toolbar solution and make them context sensitive. In this instance the text attribute buttons would enable when the right pane has the focus and disable when it looses the focus.

NSTextView not changing font size or colour from NSFontPanel

I created a basic NSTextView, I selected the following options in Interface Builder:
Editable
Selectable
Field Editor
Rich Text
Undo
Graphics
Non-contiguous Layout
Font Panel
Ruler
Inspector Bar
I set the NSViewController to be the delegate of the NSTextView and the only other custom thing I've done for this NSTextView is to enable inserting tabs and new lines (by accepting First responder):
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(insertNewline(_:)) {
textView.insertNewlineIgnoringFieldEditor(self)
return true
} else if commandSelector == #selector(insertTab(_:)) {
textView.insertTabIgnoringFieldEditor(self)
return true
} //else if commandSelector == #selector(changeColor(_:)) {
//textView.setTextColor(NSFontPanel.colo, range: <#T##NSRange#>)
//}
return false
}
When I try to use the commands from the Font Panel + Inspector Bar, All the commands work fine except changing Font size or colour, is there anything that could be wrong? Or do I need to do extra binding/delegates, etc for this to work?
It is strange because if I change the Font itself (of a selected text) or the weight, it works fine (no coding was needed).
Update
I've found the root of the problem causing this. I'm displaying the TextView in a ViewController that is displayed using a Modal segue. If I change from Modal to Show, the size and colour work fine. There's also no need for the extra commands for insert new line and tab.
Is there any reason why this is the case? Is there any customisation that should be done to the segue to avoid this? And, why is the view controller presentation affecting the behaviour of the font panel?
NSFontPanel has a 'worksWhenModal' property which sounds as if it might be set to 'false'.
A Boolean that indicates whether the receiver allows fonts to be changed in modal windows and panels.
Documentation:

How can you get the shared NSColorPanel to an show alpha / opacity slider?

When you add an NSColorWell control, and click it, it displays the shared instance of NSColorPanel.
Unfortunately, by default it does not show the alpha / opacity slider.
This is also true when it is invoked from the default MainMenu > Format > Font > Show Colors
Simply call the following line at any time in your app.
[[NSColorPanel sharedColorPanel] setShowsAlpha:YES];
You can call it once in applicationDidFinishLaunching: or you can easily tie it to a switch like an NSButton checkbox with a simple IBAction method like this:
- (IBAction)showAlphaSliderInColorPanel:(id)sender {
if ([sender state] == NSOnState) {
[[NSColorPanel sharedColorPanel] setShowsAlpha:YES];
} else {
[[NSColorPanel sharedColorPanel] setShowsAlpha:NO];
}
}
Just connect that to the Sent Actions selector item in the Connections Inspector with for a button configured to have an on / off state.
The change will occur live as you click.
A great example of how awesome Cocoa is when you want it to be.

Key Preview and Accept Button

Using winforms, I have set the KeyPreview property to true and have event handles for the proper key events within the base form as well.
Within the forms that inherit from it, I set the AcceptButton property based on the requirements of the application.
There are certain cases in which I want the enter key to have functionality different than that of the AcceptButton.
I was hoping to capture the enter key press within my base form and check for the special cases where I do not want the AcceptButton event to fire.
It appears though, that the AcceptButton click is fired before any of the key events within my basef form. I could write functionality into the click events of the possible acceptbuttons, but, in my opinion, that would be a hack.
Any suggestions?
Thanks.
Another way to handle this is to override the form's ProcessDialogKey() method where you can suppress the accept and/or cancel buttons. For example, I have an application with a filter editor that filters a grid based on user input. I want the user to be able to hit the return key when the filter editor control has the focus to apply the filter. The problem is the accept button code runs and closes the form. The code below resolves the issue.
protected override bool ProcessDialogKey(Keys keyData)
{
// Suppress the accept button when the filter editor has the focus.
// This doesn't work in the KeyDown or KeyPress events.
if (((keyData & Keys.Return) == Keys.Return) && (filterEditor.ContainsFocus))
return false;
return base.ProcessDialogKey(keyData);
}
You can take this even further by dropping the following code in a base dialog form. Then you can suppress the accept button for controls in subclasses as necessary.
private readonly List<Control> _disableAcceptButtonList = new List<Control>();
protected override bool ProcessDialogKey(Keys keyData)
{
if (((keyData & Keys.Return) == Keys.Return) && (_disableAcceptButtonList.Count > 0))
{
foreach (Control control in _disableAcceptButtonList)
if (control.ContainsFocus)
return false;
}
return base.ProcessDialogKey(keyData);
}
protected virtual void DisableAcceptButtonForControl(Control control)
{
if (!_disableAcceptButtonList.Contains(control))
_disableAcceptButtonList.Add(control);
}
As our workaround, we captured the enter and leave event for the control that we wanted to have override the acceptbutton functionality. Inside the enter event, we held the current accept button in a private variable and set the acceptbutton to null. On leave, we would reassign the acceptbutton back to the private variable we were holding.
The KeyPreview events could have done something similar to the above. If anyone has a more elegant solution, I would still love to know.
Thanks.

Resources