Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and date.

### Added

- A keyboard shortcut opens and closes the mini panel, Ctrl + Alt + M by
default, beside the mix's shortcuts and changed the same way. It opens the
panel by the clock, on the screen whose taskbar holds it.
- The mini panel has spatial sound, between noise cancellation and the
equaliser: Off, Windows Sonic or Dolby Atmos, whichever the system offers. It
is left out where the system has none, and it stays when the headset is away,
Expand Down Expand Up @@ -73,6 +76,14 @@ and date.

### Changed

- On Windows, the mini panel opens by Neap's icon, on the screen whose taskbar
holds it, rather than in the corner of the main screen. Opened from the
icon's menu, it goes in the corner of the screen the menu was on.
- The mini panel fades up into place as it opens and fades away as it
closes. With "Show animations in Windows" off, it appears and goes at once.
- A settings row whose control says more than a switch, such as the
microphone's "Muted by the boom arm" in the mini panel, puts the control
under the name rather than breaking the name mid-word.
- The app's text is set in Figtree, and the name beside the mark in Outfit,
in place of Inter. Both are carried inside the app, so Windows and Linux
show the same letters. The text is a little rounder and a little wider.
Expand Down
37 changes: 27 additions & 10 deletions src/Neap.Core/MiniPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ public enum TrayClick
/// <summary>Decides where the mini panel opens, what a click on the tray icon does, and whether it opens the panel.</summary>
/// <remarks>
/// <para>
/// The panel opens beside the notification area, as the system's own volume
/// flyout does. A desktop does not say where its icons are, but it does say
/// where its taskbar is: the work area is the screen less the bars, so the
/// side a bar takes shows as a gap between the two. The panel goes in the
/// corner next to that bar, on the side the icons are on: the right, except
/// beside a bar on the left edge. With no bar to find, as with one that hides
/// itself or a desktop that reports none, it goes bottom right.
/// The panel opens by the icon that was clicked. A desktop does not say where
/// its icons are, but a click on one lands on a taskbar, outside the work
/// area, which is the screen less its bars. So a click on a bar puts the panel
/// against that bar, centred on the click as far as the work area allows,
/// which keeps it by the icon on whichever screen the notification area is.
/// </para>
/// <para>
/// Without such a click, as from the icon's menu or on a desktop that does not
/// say where the pointer is, the panel goes in a corner, as the system's own
/// volume flyout does. The side a bar takes shows as a gap between the screen
/// and its work area, and the panel goes in the corner next to that bar, on
/// the side the icons are on: the right, except beside a bar on the left edge.
/// With no bar to find, as with one that hides itself or a desktop that
/// reports none, it goes bottom right.
/// </para>
/// <para>
/// Clicking the icon while the panel is open first takes focus from the
Expand All @@ -38,13 +45,23 @@ public static class MiniPanel
/// <param name="width">The panel's width, in pixels.</param>
/// <param name="height">The panel's height, in pixels.</param>
/// <param name="margin">The gap kept to the work area's edges, in pixels.</param>
public static Box Place(Box screen, Box work, int width, int height, int margin)
/// <param name="click">Where the pointer was when the icon was clicked, or null when that is not known.</param>
public static Box Place(Box screen, Box work, int width, int height, int margin, (int X, int Y)? click)
{
int w = Math.Min(width, work.Width - 2 * margin), h = Math.Min(height, work.Height - 2 * margin);
int right = work.X + work.Width - w - margin, bottom = work.Y + work.Height - h - margin;
if (click is { } c)
{
if (c.Y < work.Y || c.Y >= work.Y + work.Height)
return new(Math.Clamp(c.X - w / 2, work.X + margin, right), c.Y < work.Y ? work.Y + margin : bottom, w, h);
if (c.X < work.X || c.X >= work.X + work.Width)
return new(c.X < work.X ? work.X + margin : right, Math.Clamp(c.Y - h / 2, work.Y + margin, bottom), w, h);
}

bool top = work.Y > screen.Y && work.Y + work.Height == screen.Y + screen.Height;
bool left = work.X > screen.X && work.X + work.Width == screen.X + screen.Width;
int x = left ? work.X + margin : work.X + work.Width - w - margin;
int y = top ? work.Y + margin : work.Y + work.Height - h - margin;
int x = left ? work.X + margin : right;
int y = top ? work.Y + margin : bottom;
return new(x, y, w, h);
}

Expand Down
3 changes: 3 additions & 0 deletions src/Neap.Desktop/Controls/MixKeysPanel.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
<c:SettingsCard x:Name="CentreRow" loc:Uid.Value="Mix_CentreRow">
<Button x:Name="CentreKey" MinWidth="190" />
</c:SettingsCard>
<c:SettingsCard x:Name="MiniRow" loc:Uid.Value="Mix_MiniRow">
<Button x:Name="MiniKey" MinWidth="190" />
</c:SettingsCard>
<c:SettingsCard loc:Uid.Value="Mix_KeysResetRow">
<Button x:Name="KeysReset" loc:Uid.Value="Mix_KeysReset" />
</c:SettingsCard>
Expand Down
6 changes: 4 additions & 2 deletions src/Neap.Desktop/Controls/MixKeysPanel.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
namespace Neap.Desktop.Controls;

/// <summary>
/// The keys that move the game and chat mix from anywhere, and a way to
/// change them.
/// The keys that move the game and chat mix or open the mini panel from
/// anywhere, and a way to change them.
/// </summary>
/// <remarks>
/// <para>
Expand Down Expand Up @@ -49,6 +49,7 @@ public MixKeysPanel()
Capture(GameKey, MixKey.TowardGame);
Capture(ChatKey, MixKey.TowardChat);
Capture(CentreKey, MixKey.Balanced);
Capture(MiniKey, MixKey.MiniPanel);
}

protected override void OnLoaded(RoutedEventArgs e)
Expand Down Expand Up @@ -144,6 +145,7 @@ private void Paint()
Show(GameKey, GameRow, MixKey.TowardGame);
Show(ChatKey, ChatRow, MixKey.TowardChat);
Show(CentreKey, CentreRow, MixKey.Balanced);
Show(MiniKey, MiniRow, MixKey.MiniPanel);

_painting = true;
KeysOn.IsChecked = AppServices.Hotkeys.Enabled;
Expand Down
31 changes: 29 additions & 2 deletions src/Neap.Desktop/Controls/SettingsCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public Geometry? Icon
/// <summary>
/// The width below which the control moves under the name, so neither is
/// squeezed. A panel whose rows are short, like the mini panel, sets it
/// to zero in a style to keep a switch beside its name.
/// to zero in a style to keep a switch beside its name while the name
/// still fits on one line.
/// </summary>
public double SideBySideWidth
{
Expand All @@ -75,12 +76,16 @@ public double SideBySideWidth

private Grid? _layout;
private Control? _control;
private Control? _glyph;
private Control? _header;

protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_layout = e.NameScope.Find<Grid>("Layout");
_control = e.NameScope.Find<Control>("Control");
_glyph = e.NameScope.Find<Control>("Glyph");
_header = e.NameScope.Find<Control>("HeaderText");
Place(Bounds.Width);
}

Expand Down Expand Up @@ -113,7 +118,7 @@ protected override Size MeasureOverride(Size availableSize)
private void Place(double width)
{
if (_layout is null || _control is null || width <= 0) return;
bool beside = width >= SideBySideWidth;
bool beside = width >= SideBySideWidth && NameFitsBeside(width);
Grid.SetRow(_control, beside ? 0 : 1);
Grid.SetColumn(_control, beside ? 2 : 1);
Grid.SetColumnSpan(_control, beside ? 1 : 2);
Expand All @@ -128,4 +133,26 @@ private void Place(double width)
// Likewise the gap above it when it sits under the name, so a row with nothing under it has no spare room.
_control.Margin = beside ? new Thickness(Space.M, 0, 0, 0) : new Thickness(0, Space.S, 0, 0);
}

/// <summary>Whether the name keeps to one line with the control beside it.</summary>
/// <remarks>
/// A control that says more than a switch, such as the reason the microphone
/// cannot be changed, can leave a short name a few letters wide, broken
/// mid-word. Then the control goes under the name instead.
/// </remarks>
private bool NameFitsBeside(double width)
{
if (_header is null || _control is null) return true;
_header.Measure(Size.Infinity);
_control.Measure(Size.Infinity);
double glyph = 0;
if (_glyph is { IsVisible: true })
{
_glyph.Measure(Size.Infinity);
glyph = _glyph.DesiredSize.Width;
}
double edges = Padding.Left + Padding.Right + BorderThickness.Left + BorderThickness.Right;
double control = _control.DesiredSize.Width - _control.Margin.Left - _control.Margin.Right;
return edges + glyph + _header.DesiredSize.Width + Space.M + control <= width;
}
}
88 changes: 78 additions & 10 deletions src/Neap.Desktop/MainWindow.Tray.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Avalonia;
using Avalonia.Controls;
Expand Down Expand Up @@ -58,7 +59,7 @@ private void StartTray()
var open = new NativeMenuItem(Strings.Get("Main_TrayOpen"));
open.Click += (_, _) => Reveal();
var mini = new NativeMenuItem(Strings.Get("Main_TrayMini"));
mini.Click += (_, _) => ToggleMini();
mini.Click += (_, _) => ToggleMini(PointerAt());
mini.IsVisible = AppSettings.Current.MiniPanel;
_miniItem = mini;
var quit = new NativeMenuItem(Strings.Get("Main_TrayQuit"));
Expand All @@ -69,11 +70,8 @@ private void StartTray()
Menu = new NativeMenu { open, mini, new NativeMenuItemSeparator(), quit },
};
if (Application.Current?.PlatformSettings is { } settings) settings.ColorValuesChanged += RepaintTrayPicture;
_tray.Clicked += (_, _) =>
{
if (MiniPanel.Click(AppSettings.Current.MiniPanel) == TrayClick.ToggleMiniPanel) ToggleMini();
else Reveal();
};
_tray.Clicked += (_, _) => Summon(PointerAt());
AppServices.Hotkeys.MiniPanelPressed += () => Summon(NotificationArea());
if (Application.Current is { } app) TrayIcon.SetIcons(app, new TrayIcons { _tray });

AppServices.Updates.Found += release => Dispatcher.UIThread.Post(() => TellAboutUpdate(release));
Expand All @@ -92,21 +90,29 @@ public void MiniPanelSettingChanged()
if (!on) _mini?.Close();
}

/// <summary>Opens the mini panel, or closes it when it is open.</summary>
/// <summary>Does what a click on the icon or the mini panel's key does: toggles the panel while it is on, or else brings the window forward.</summary>
private void Summon(PixelPoint? at)
{
if (MiniPanel.Click(AppSettings.Current.MiniPanel) == TrayClick.ToggleMiniPanel) ToggleMini(at);
else Reveal();
}

/// <summary>Opens the mini panel by the given point, or closes it when it is open.</summary>
/// <param name="click">Where the icon was clicked, or the notification area for its key; null puts it in the main screen's corner.</param>
/// <remarks>
/// A click on the icon takes focus from an open panel, which closes it
/// before the click arrives; <see cref="MiniPanel.Opens"/> tells that
/// click apart from one that asks to open it.
/// </remarks>
private void ToggleMini()
private void ToggleMini(PixelPoint? click)
{
if (!MiniPanel.Opens(_mini is not null, Stopwatch.GetElapsedTime(_miniClosedAt)))
{
_mini?.Close();
return;
}
AppLog.Write("mini panel: opened from the notification area");
var panel = new MiniPanelWindow { Main = this };
AppLog.Write("mini panel: opened");
var panel = new MiniPanelWindow { Main = this, Click = click };
panel.Closed += (_, _) =>
{
_miniClosedAt = panel.ClosedAt;
Expand All @@ -117,6 +123,68 @@ private void ToggleMini()
panel.Activate();
}

/// <summary>Where the pointer is on the desktop, in physical pixels, or null where that cannot be read.</summary>
/// <remarks>
/// The icon's click carries no place, and the toolkit has no pointer
/// outside its own windows, so Windows is asked. Elsewhere the panel goes
/// in the main screen's corner.
/// </remarks>
private static PixelPoint? PointerAt() =>
OperatingSystem.IsWindows() && GetCursorPos(out var at) ? new PixelPoint(at.X, at.Y) : null;

/// <summary>The clock's end of the main taskbar, in physical pixels, or null where that cannot be read.</summary>
/// <remarks>
/// A key press has no pointer on the taskbar to open by, so the panel goes
/// by the clock instead: on the taskbar that holds the notification area,
/// whichever screen that is. Windows gives that taskbar's place but not the
/// clock's, which sits at its right end, or its foot when it stands upright.
/// </remarks>
private static PixelPoint? NotificationArea()
{
var bar = new AppBarData { Size = (uint)Marshal.SizeOf<AppBarData>() };
if (!OperatingSystem.IsWindows() || SHAppBarMessage(GetTaskbarPosition, ref bar) == 0) return null;
var r = bar.Rect;
return r.Right - r.Left >= r.Bottom - r.Top
? new PixelPoint(r.Right - 1, (r.Top + r.Bottom) / 2)
: new PixelPoint((r.Left + r.Right) / 2, r.Bottom - 1);
}

private const uint GetTaskbarPosition = 5;

[StructLayout(LayoutKind.Sequential)]
private struct NativeRect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}

[StructLayout(LayoutKind.Sequential)]
private struct AppBarData
{
public uint Size;
public IntPtr Window;
public uint CallbackMessage;
public uint Edge;
public NativeRect Rect;
public IntPtr Param;
}

[DllImport("shell32.dll")]
private static extern UIntPtr SHAppBarMessage(uint message, ref AppBarData data);

[StructLayout(LayoutKind.Sequential)]
private struct NativePoint
{
public int X;
public int Y;
}

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out NativePoint point);

/// <summary>The notification-area glyph: light on a dark taskbar, dark on a light one.</summary>
/// <remarks>
/// Windows has a taskbar shade, set apart from the app's own theme. A Linux
Expand Down
2 changes: 1 addition & 1 deletion src/Neap.Desktop/MiniPanelWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
in a window of its own so it can sit by the notification area and
go away when it loses focus.
-->
<Border Classes="card">
<Border x:Name="Card" Classes="card">
<StackPanel Spacing="{StaticResource NeapSpaceM}">

<!-- The headset, how it is connected, its battery, and the way to the window. -->
Expand Down
Loading
Loading