Browse Source

Merge pull request #2091 from AvaloniaUI/dialogs-rework

Dialogs rework
pull/2107/head
danwalmsley 8 years ago
committed by GitHub
parent
commit
cdbc5fd51e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      native/Avalonia.Native/inc/avalonia-native.h
  2. 9
      native/Avalonia.Native/src/OSX/common.h
  3. 2
      native/Avalonia.Native/src/OSX/window.h
  4. 121
      native/Avalonia.Native/src/OSX/window.mm
  5. 5
      samples/ControlCatalog/Pages/DialogsPage.xaml
  6. 14
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  7. 2
      samples/ControlCatalog/Pages/MenuPage.xaml.cs
  8. 5
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  9. 31
      src/Avalonia.Controls/SystemDialog.cs
  10. 38
      src/Avalonia.Controls/Window.cs
  11. 5
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  12. 6
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  13. 4
      src/Avalonia.Native/WindowImpl.cs
  14. 4
      src/Gtk/Avalonia.Gtk3/Interop/Native.cs
  15. 2
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  16. 13
      src/Gtk/Avalonia.Gtk3/WindowImpl.cs
  17. 18
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  18. 30
      src/Windows/Avalonia.Win32/WindowImpl.cs
  19. 7
      tests/Avalonia.Controls.UnitTests/WindowTests.cs

2
native/Avalonia.Native/inc/avalonia-native.h

@ -213,7 +213,7 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{
virtual HRESULT ShowDialog (IUnknown**ppv) = 0;
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0;
virtual HRESULT SetHasDecorations(bool value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0;

9
native/Avalonia.Native/src/OSX/common.h

@ -31,4 +31,13 @@ extern NSSize ToNSSize (AvnSize s);
#define NSDebugLog(...) (void)0
#endif
template<typename T> inline T* objc_cast(id from) {
if(from == nil)
return nil;
if ([from isKindOfClass:[T class]]) {
return static_cast<T*>(from);
}
return nil;
}
#endif

2
native/Avalonia.Native/src/OSX/window.h

@ -18,6 +18,8 @@ class WindowBaseImpl;
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(void) setCanBecomeKeyAndMain;
-(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
@end
struct INSWindowHolder

121
native/Avalonia.Native/src/OSX/window.mm

@ -109,6 +109,7 @@ public:
if(Window != nullptr)
{
[Window orderOut:Window];
[Window restoreParentWindow];
}
return S_OK;
@ -392,30 +393,6 @@ protected:
}
};
class ModalDisposable : public ComUnknownObject
{
NSModalSession _session;
AvnWindow* _window;
void Dispose ()
{
[_window orderOut:_window];
[NSApp endModalSession:_session];
}
public:
ModalDisposable(AvnWindow* window, NSModalSession session)
{
_session = session;
_window = window;
}
virtual ~ModalDisposable()
{
Dispose();
}
};
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{
private:
@ -444,32 +421,27 @@ private:
{
@autoreleasepool
{
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
WindowBaseImpl::Show();
return SetWindowState(_lastWindowState);
}
}
virtual HRESULT ShowDialog (IUnknown**ppv) override
virtual HRESULT ShowDialog (IAvnWindow* parent) override
{
@autoreleasepool
{
if(ppv == nullptr)
{
if(parent == nullptr)
return E_POINTER;
}
auto session = [NSApp beginModalSessionForWindow:Window];
auto disposable = new ModalDisposable(Window, session);
*ppv = disposable;
SetPosition(lastPositionSet);
UpdateStyle();
[Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
auto cparent = dynamic_cast<WindowImpl*>(parent);
if(cparent == nullptr)
return E_INVALIDARG;
[Window pollModalSession:session];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
return S_OK;
}
@ -843,8 +815,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super viewDidChangeBackingProperties];
}
- (bool) ignoreUserInput
{
auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
return TRUE;
return FALSE;
}
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{
if([self ignoreUserInput])
return;
[self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
@ -952,7 +935,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{
{
if([self ignoreUserInput])
return;
auto key = s_KeyMap[[event keyCode]];
auto timestamp = [event timestamp] * 1000;
@ -1125,13 +1110,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
ComPtr<WindowBaseImpl> parent = _parent;
_parent = NULL;
[self restoreParentWindow];
parent->BaseEvents->Closed();
[parent->View onClosed];
[self setContentView: nil];
}
}
-(BOOL)canBecomeKeyWindow
{
return _canBecomeKeyAndMain;
@ -1142,12 +1127,62 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return _canBecomeKeyAndMain;
}
-(bool) activateAppropriateChild: (bool)activating
{
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
[ch activateAppropriateChild:false];
return FALSE;
}
if(!activating)
[self makeKeyAndOrderFront:self];
return TRUE;
}
-(bool)shouldTryToHandleEvents
{
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
return FALSE;
}
return TRUE;
}
-(void)makeKeyWindow
{
if([self activateAppropriateChild: true])
{
[super makeKeyWindow];
}
}
-(void)becomeKeyWindow
{
_parent->BaseEvents->Activated();
[super becomeKeyWindow];
if([self activateAppropriateChild: true])
{
_parent->BaseEvents->Activated();
[super becomeKeyWindow];
}
}
-(void) restoreParentWindow;
{
auto parent = objc_cast<AvnWindow>([self parentWindow]);
if(parent != nil)
{
[parent removeChildWindow:self];
[parent activateAppropriateChild: false];
}
}
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
{
return true;

5
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -3,10 +3,7 @@
<Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button>
<StackPanel Orientation="Horizontal">
<CheckBox Name="IsModal" IsChecked="True"/>
<TextBlock>Modal to window</TextBlock>
</StackPanel>
<Button Name="DecoratedWindow">Decorated window</Button>
<Button Name="Dialog">Dialog</Button>
</StackPanel>
</UserControl>

14
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -31,12 +31,18 @@ namespace ControlCatalog.Pages
}.ShowAsync(GetWindow());
};
this.FindControl<Button>("DecoratedWindow").Click += delegate
{
new DecoratedWindow().Show();
};
{
new DecoratedWindow().ShowDialog(GetWindow());
};
this.FindControl<Button>("Dialog").Click += delegate
{
new MainWindow().ShowDialog(GetWindow());
};
}
Window GetWindow() => this.FindControl<CheckBox>("IsModal").IsChecked.Value ? (Window)this.VisualRoot : null;
Window GetWindow() => (Window)this.VisualRoot;
private void InitializeComponent()
{

2
samples/ControlCatalog/Pages/MenuPage.xaml.cs

@ -83,7 +83,7 @@ namespace ControlCatalog.Pages
public async Task Open()
{
var dialog = new OpenFileDialog();
var result = await dialog.ShowAsync();
var result = await dialog.ShowAsync(App.Current.MainWindow);
if (result != null)
{

5
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -30,10 +30,7 @@ namespace Avalonia.Platform
/// <summary>
/// Shows the window as a dialog.
/// </summary>
/// <returns>
/// An <see cref="IDisposable"/> that should be used to close the window.
/// </returns>
IDisposable ShowDialog();
void ShowDialog(IWindowImpl parent);
/// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc)

31
src/Avalonia.Controls/SystemDialog.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -18,28 +19,40 @@ namespace Avalonia.Controls
public class SaveFileDialog : FileDialog
{
public string DefaultExtension { get; set; }
public string DefaultExtension { get; set; }
public async Task<string> ShowAsync(Window window)
=>
((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, window?.PlatformImpl)) ??
new string[0]).FirstOrDefault();
public async Task<string> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
.ShowFileDialogAsync(this, parent?.PlatformImpl)) ??
new string[0]).FirstOrDefault();
}
}
public class OpenFileDialog : FileDialog
{
public bool AllowMultiple { get; set; }
public Task<string[]> ShowAsync(Window window = null)
=> AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, window?.PlatformImpl);
public Task<string[]> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, parent?.PlatformImpl);
}
}
public class OpenFolderDialog : FileSystemDialog
{
public string DefaultDirectory { get; set; }
public Task<string> ShowAsync(Window window = null)
=> AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, window?.PlatformImpl);
public Task<string> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, parent?.PlatformImpl);
}
}
public abstract class SystemDialog

38
src/Avalonia.Controls/Window.cs

@ -397,11 +397,23 @@ namespace Avalonia.Controls
/// <returns>
/// A task that can be used to track the lifetime of the dialog.
/// </returns>
public Task ShowDialog()
public Task ShowDialog(Window parent)
{
return ShowDialog<object>();
return ShowDialog<object>(parent);
}
/// <summary>
/// Shows the window as a dialog.
/// </summary>
/// <typeparam name="TResult">
/// The type of the result produced by the dialog.
/// </typeparam>
/// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns>
public Task<TResult> ShowDialog<TResult>(Window parent) => ShowDialog<TResult>(parent.PlatformImpl);
/// <summary>
/// Shows the window as a dialog.
/// </summary>
@ -411,8 +423,10 @@ namespace Avalonia.Controls
/// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns>
public Task<TResult> ShowDialog<TResult>()
public Task<TResult> ShowDialog<TResult>(IWindowImpl parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
if (IsVisible)
{
throw new InvalidOperationException("The window is already being shown.");
@ -427,24 +441,18 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList();
var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault();
SetIsEnabled(affectedWindows, false);
var modal = PlatformImpl?.ShowDialog();
PlatformImpl?.ShowDialog(parent);
var result = new TaskCompletionSource<TResult>();
Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>(
x => this.Closed += x,
x => this.Closed -= x)
.Take(1)
.Subscribe(_ =>
{
modal?.Dispose();
SetIsEnabled(affectedWindows, true);
activated?.Activate();
parent.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)));
});
@ -452,14 +460,6 @@ namespace Avalonia.Controls
}
}
void SetIsEnabled(IEnumerable<Window> windows, bool isEnabled)
{
foreach (var window in windows)
{
window.IsEnabled = isEnabled;
}
}
void SetWindowStartupLocation()
{
if (WindowStartupLocation == WindowStartupLocation.CenterScreen)

5
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -86,9 +86,8 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public IDisposable ShowDialog()
public void ShowDialog(IWindowImpl parent)
{
return Disposable.Empty;
}
public void SetSystemDecorations(bool enabled)
@ -111,4 +110,4 @@ namespace Avalonia.DesignerSupport.Remote
{
}
}
}
}

6
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -87,7 +87,9 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public IDisposable ShowDialog() => Disposable.Empty;
public void ShowDialog(IWindowImpl parent)
{
}
public void SetSystemDecorations(bool enabled)
{
@ -157,4 +159,4 @@ namespace Avalonia.DesignerSupport.Remote
public Screen[] AllScreens { get; } =
{new Screen(new Rect(0, 0, 4000, 4000), new Rect(0, 0, 4000, 4000), true)};
}
}
}

4
src/Avalonia.Native/WindowImpl.cs

@ -47,9 +47,9 @@ namespace Avalonia.Native
public IAvnWindow Native => _native;
public IDisposable ShowDialog()
public void ShowDialog(IWindowImpl window)
{
return _native.ShowDialog();
_native.ShowDialog(((WindowImpl)window).Native);
}
public void CanResize(bool value)

4
src/Gtk/Avalonia.Gtk3/Interop/Native.cs

@ -67,6 +67,9 @@ namespace Avalonia.Gtk3.Interop
[UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
public delegate void gtk_window_set_modal(GtkWindow window, bool modal);
[UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
public delegate void gtk_window_set_transient_for(GtkWindow window, IntPtr parent);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] //No manual import
public delegate IntPtr gdk_get_native_handle(IntPtr gdkWindow);
@ -408,6 +411,7 @@ namespace Avalonia.Gtk3.Interop
public static D.gtk_window_new GtkWindowNew;
public static D.gtk_window_set_icon GtkWindowSetIcon;
public static D.gtk_window_set_modal GtkWindowSetModal;
public static D.gtk_window_set_transient_for GtkWindowSetTransientFor;
public static D.gdk_set_allowed_backends GdkSetAllowedBackends;
public static D.gtk_init GtkInit;
public static D.gtk_window_present GtkWindowPresent;

2
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@ -424,7 +424,7 @@ namespace Avalonia.Gtk3
Native.GdkWindowSetCursor(Native.GtkWidgetGetWindow(GtkWidget), cursor?.Handle ?? IntPtr.Zero);
}
public void Show() => Native.GtkWindowPresent(GtkWidget);
public virtual void Show() => Native.GtkWindowPresent(GtkWidget);
public void Hide() => Native.GtkWidgetHide(GtkWidget);

13
src/Gtk/Avalonia.Gtk3/WindowImpl.cs

@ -63,11 +63,18 @@ namespace Avalonia.Gtk3
public Action<WindowState> WindowStateChanged { get; set; }
public IDisposable ShowDialog()
public void ShowDialog(IWindowImpl parent)
{
Native.GtkWindowSetModal(GtkWidget, true);
Show();
return new EmptyDisposable();
Native.GtkWindowSetTransientFor(GtkWidget, ((WindowImpl)parent).GtkWidget.DangerousGetHandle());
Native.GtkWindowPresent(GtkWidget);
}
public override void Show()
{
Native.GtkWindowSetModal(GtkWidget, false);
Native.GtkWindowSetTransientFor(GtkWidget, IntPtr.Zero);
Native.GtkWindowPresent(GtkWidget);
}
public void SetSystemDecorations(bool enabled) => Native.GtkWindowSetDecorated(GtkWidget, enabled);

18
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -780,8 +780,8 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", SetLastError = true, EntryPoint = "SetWindowLong")]
private static extern uint SetWindowLong32b(IntPtr hWnd, int nIndex, uint value);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SetWindowLongPtr(IntPtr hWnd, int nIndex, uint value);
[DllImport("user32.dll", SetLastError = true, EntryPoint = "SetWindowLongPtr")]
private static extern IntPtr SetWindowLong64b(IntPtr hWnd, int nIndex, IntPtr value);
public static uint SetWindowLong(IntPtr hWnd, int nIndex, uint value)
{
@ -791,7 +791,19 @@ namespace Avalonia.Win32.Interop
}
else
{
return SetWindowLongPtr(hWnd, nIndex, value);
return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32();
}
}
public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle)
{
if (IntPtr.Size == 4)
{
return new IntPtr(SetWindowLong32b(hWnd, nIndex, (uint)handle.ToInt32()));
}
else
{
return SetWindowLong64b(hWnd, nIndex, handle);
}
}

30
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -42,6 +42,8 @@ namespace Avalonia.Win32
private OleDropTarget _dropTarget;
private Size _minSize;
private Size _maxSize;
private WindowImpl _parent;
private readonly List<WindowImpl> _disabledBy = new List<WindowImpl>();
#if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag;
@ -165,10 +167,10 @@ namespace Avalonia.Win32
private set;
}
public bool IsEnabled
void UpdateEnabled()
{
get { return UnmanagedMethods.IsWindowEnabled(_hwnd); }
set { UnmanagedMethods.EnableWindow(_hwnd, value); }
EnableWindow(_hwnd, _disabledBy.Count == 0);
}
public Size MaxClientSize
@ -248,6 +250,12 @@ namespace Avalonia.Win32
public void Hide()
{
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
_parent = null;
}
UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide);
}
@ -359,6 +367,7 @@ namespace Avalonia.Win32
public virtual void Show()
{
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, IntPtr.Zero);
ShowWindow(_showWindowState);
}
@ -412,11 +421,13 @@ namespace Avalonia.Win32
}
}
public virtual IDisposable ShowDialog()
public void ShowDialog(IWindowImpl parent)
{
Show();
return Disposable.Empty;
_parent = (WindowImpl)parent;
_parent._disabledBy.Add(this);
_parent.UpdateEnabled();
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, ((WindowImpl)parent)._hwnd);
ShowWindow(_showWindowState);
}
public void SetCursor(IPlatformHandle cursor)
@ -490,6 +501,11 @@ namespace Avalonia.Win32
//Remove root reference to this class, so unmanaged delegate can be collected
s_instances.Remove(this);
Closed?.Invoke();
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
}
//Free other resources
Dispose();
return IntPtr.Zero;

7
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -62,9 +62,11 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = new Window();
parent.Show();
var window = new Window();
var task = window.ShowDialog();
var task = window.ShowDialog(parent);
Assert.True(window.IsVisible);
}
@ -258,12 +260,13 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = new Mock<IWindowImpl>();
var windowImpl = new Mock<IWindowImpl>();
windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.Scaling).Returns(1);
var target = new Window(windowImpl.Object);
var task = target.ShowDialog<bool>();
var task = target.ShowDialog<bool>(parent.Object);
windowImpl.Object.Closed();

Loading…
Cancel
Save