1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Robert
7f5b677cc3 Capture view model references in WhenActivated of views to avoid NRE 2025-12-11 23:58:15 +01:00
Robert
5609065974 Workshop - Add --alt-login-callback startup argument
UI - Rounded corners on profile icons
2025-12-11 23:54:50 +01:00
Robert
06c5294e88 Profile editor - Fix race condition causing the editor to fail to activate on suspended profiles
Surface editor - Implemented left-handed preset
2025-12-11 22:55:31 +01:00
22 changed files with 172 additions and 95 deletions

View File

@ -157,10 +157,11 @@ internal class DeviceService : IDeviceService
} }
} }
/// <param name="leftHanded"></param>
/// <inheritdoc /> /// <inheritdoc />
public void AutoArrangeDevices() public void AutoArrangeDevices(bool leftHanded)
{ {
SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(); SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(leftHanded);
surfaceArrangement.Arrange(_devices); surfaceArrangement.Arrange(_devices);
foreach (ArtemisDevice artemisDevice in _devices) foreach (ArtemisDevice artemisDevice in _devices)
artemisDevice.ApplyDefaultCategories(); artemisDevice.ApplyDefaultCategories();

View File

@ -46,7 +46,8 @@ public interface IDeviceService : IArtemisService
/// <summary> /// <summary>
/// Applies auto-arranging logic to the surface /// Applies auto-arranging logic to the surface
/// </summary> /// </summary>
void AutoArrangeDevices(); /// <param name="leftHanded"></param>
void AutoArrangeDevices(bool leftHanded);
/// <summary> /// <summary>
/// Apples the best available to the provided <see cref="ArtemisDevice" /> /// Apples the best available to the provided <see cref="ArtemisDevice" />

View File

@ -48,22 +48,42 @@ internal class SurfaceArrangement
} }
} }
internal static SurfaceArrangement GetDefaultArrangement() internal static SurfaceArrangement GetDefaultArrangement(bool leftHanded)
{ {
SurfaceArrangement arrangement = new(); SurfaceArrangement arrangement = new();
SurfaceArrangementType keypad = arrangement.AddType(RGBDeviceType.Keypad, 1); SurfaceArrangementType keyboard, keypad, mousepad, mouse;
keypad.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Equal, VerticalArrangementPosition.Equal, 20)); if (leftHanded)
{
mousepad = arrangement.AddType(RGBDeviceType.Mousepad, 1);
mousepad.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10));
SurfaceArrangementType keyboard = arrangement.AddType(RGBDeviceType.Keyboard, 1); mouse = arrangement.AddType(RGBDeviceType.Mouse, 2);
keyboard.AddConfiguration(new SurfaceArrangementConfiguration(keypad, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 20)); mouse.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Center, 0));
mouse.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Center, 10));
SurfaceArrangementType mousepad = arrangement.AddType(RGBDeviceType.Mousepad, 1); keyboard = arrangement.AddType(RGBDeviceType.Keyboard, 1);
mousepad.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10)); keyboard.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10));
keyboard.AddConfiguration(new SurfaceArrangementConfiguration(mouse, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 100));
SurfaceArrangementType mouse = arrangement.AddType(RGBDeviceType.Mouse, 2); keypad = arrangement.AddType(RGBDeviceType.Keypad, 1);
mouse.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Center, 0)); keypad.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Equal, VerticalArrangementPosition.Equal, 20));
mouse.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Center, 100)); }
else
{
keypad = arrangement.AddType(RGBDeviceType.Keypad, 1);
keypad.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Equal, VerticalArrangementPosition.Equal, 20));
keyboard = arrangement.AddType(RGBDeviceType.Keyboard, 1);
keyboard.AddConfiguration(new SurfaceArrangementConfiguration(keypad, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 20));
mousepad = arrangement.AddType(RGBDeviceType.Mousepad, 1);
mousepad.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10));
mouse = arrangement.AddType(RGBDeviceType.Mouse, 2);
mouse.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Center, 0));
mouse.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Center, 100));
}
SurfaceArrangementType headset = arrangement.AddType(RGBDeviceType.Headset, 1); SurfaceArrangementType headset = arrangement.AddType(RGBDeviceType.Headset, 1);
headset.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Bottom, 100)); headset.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Bottom, 100));

View File

@ -32,7 +32,7 @@ internal class SurfaceArrangementConfiguration
public int MarginBottom { get; } public int MarginBottom { get; }
public SurfaceArrangement SurfaceArrangement { get; set; } public SurfaceArrangement SurfaceArrangement { get; set; }
public bool Apply(List<ArtemisDevice> devices) public bool Apply(List<ArtemisDevice> devicesToArrange, List<ArtemisDevice> devices)
{ {
if (Anchor != null && !Anchor.HasDevices(devices)) if (Anchor != null && !Anchor.HasDevices(devices))
return false; return false;
@ -42,10 +42,10 @@ internal class SurfaceArrangementConfiguration
new SurfaceArrangementType(SurfaceArrangement, RGBDeviceType.All, 1).GetEdge(HorizontalPosition, VerticalPosition); new SurfaceArrangementType(SurfaceArrangement, RGBDeviceType.All, 1).GetEdge(HorizontalPosition, VerticalPosition);
// Stack multiple devices of the same type vertically if they are wider than they are tall // Stack multiple devices of the same type vertically if they are wider than they are tall
bool stackVertically = devices.Average(d => d.RgbDevice.Size.Width) >= devices.Average(d => d.RgbDevice.Size.Height); bool stackVertically = devicesToArrange.Average(d => d.RgbDevice.Size.Width) >= devicesToArrange.Average(d => d.RgbDevice.Size.Height);
ArtemisDevice? previous = null; ArtemisDevice? previous = null;
foreach (ArtemisDevice artemisDevice in devices) foreach (ArtemisDevice artemisDevice in devicesToArrange)
{ {
if (previous != null) if (previous != null)
{ {

View File

@ -28,18 +28,18 @@ internal class SurfaceArrangementType
public void Arrange(List<ArtemisDevice> devices) public void Arrange(List<ArtemisDevice> devices)
{ {
devices = devices.Where(d => d.DeviceType == DeviceType).ToList(); List<ArtemisDevice> devicesToArrange = devices.Where(d => d.DeviceType == DeviceType).ToList();
if (!devices.Any()) if (!devicesToArrange.Any())
return; return;
AppliedConfiguration = null; AppliedConfiguration = null;
foreach (SurfaceArrangementConfiguration configuration in Configurations) foreach (SurfaceArrangementConfiguration configuration in Configurations)
{ {
bool applied = configuration.Apply(devices); bool applied = configuration.Apply(devicesToArrange, devices);
if (applied) if (applied)
{ {
AppliedConfiguration = configuration; AppliedConfiguration = configuration;
foreach (ArtemisDevice artemisDevice in devices) foreach (ArtemisDevice artemisDevice in devicesToArrange)
artemisDevice.ZIndex = ZIndex; artemisDevice.ZIndex = ZIndex;
return; return;
} }
@ -52,7 +52,7 @@ internal class SurfaceArrangementType
VerticalArrangementPosition.Equal, VerticalArrangementPosition.Equal,
10 10
) {SurfaceArrangement = SurfaceArrangement}; ) {SurfaceArrangement = SurfaceArrangement};
fallback.Apply(devices); fallback.Apply(devicesToArrange, devices);
AppliedConfiguration = fallback; AppliedConfiguration = fallback;
} }

View File

@ -6,7 +6,6 @@ using Avalonia.Controls;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
@ -43,7 +42,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon) if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon)
{ {
Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon) Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon)
? new MaterialIcon {Kind = parsedIcon!} ? new MaterialIcon {Kind = parsedIcon}
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; : new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
} }
else if (ConfigurationIcon.IconBytes != null) else if (ConfigurationIcon.IconBytes != null)
@ -65,19 +64,28 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
return; return;
_stream = new MemoryStream(ConfigurationIcon.IconBytes); _stream = new MemoryStream(ConfigurationIcon.IconBytes);
if (!ConfigurationIcon.Fill) Border border = new()
{ {
Content = new Image {Source = new Bitmap(_stream)}; CornerRadius = CornerRadius,
return; ClipToBounds = true,
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
if (ConfigurationIcon.Fill)
{
// Fill mode: use Foreground as Background and the bitmap as opacity mask
border.Background = TextElement.GetForeground(this);
border.OpacityMask = new ImageBrush(new Bitmap(_stream));
}
else
{
// Non-fill mode: place the image inside the rounded border
border.Child = new Image { Source = new Bitmap(_stream) };
} }
Content = new Border Content = border;
{
Background = TextElement.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
OpacityMask = new ImageBrush(new Bitmap(_stream))
};
} }
catch (Exception) catch (Exception)
{ {

View File

@ -185,12 +185,15 @@ internal class ProfileEditorService : IProfileEditorService
{ {
// Activate the profile if one was provided // Activate the profile if one was provided
if (profileConfiguration != null) if (profileConfiguration != null)
{
_profileService.FocusProfile = profileConfiguration;
_profileService.ActivateProfile(profileConfiguration); _profileService.ActivateProfile(profileConfiguration);
}
// If there is no profile configuration or module, deliberately set the override to null // If there is no profile configuration or module, deliberately set the override to null
_moduleService.SetActivationOverride(profileConfiguration?.Module); _moduleService.SetActivationOverride(profileConfiguration?.Module);
}); });
_profileService.FocusProfile = profileConfiguration;
_profileConfigurationSubject.OnNext(profileConfiguration); _profileConfigurationSubject.OnNext(profileConfiguration);
ChangeTime(TimeSpan.Zero); ChangeTime(TimeSpan.Zero);

View File

@ -21,11 +21,14 @@ public partial class DebugView : ReactiveAppWindow<DebugViewModel>
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Observable.FromEventPattern(x => ViewModel!.ActivationRequested += x, x => ViewModel!.ActivationRequested -= x).Subscribe(_ => DebugViewModel vm = ViewModel!;
{ Observable.FromEventPattern(x => vm.ActivationRequested += x, x => vm.ActivationRequested -= x)
WindowState = WindowState.Normal; .Subscribe(_ =>
Activate(); {
}).DisposeWith(d); WindowState = WindowState.Normal;
Activate();
})
.DisposeWith(d);
}); });
} }

View File

@ -19,14 +19,11 @@ public partial class PluginSettingsWindowView : ReactiveAppWindow<PluginSettings
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
Observable.FromEventPattern( PluginSettingsWindowViewModel vm = ViewModel!;
x => ViewModel!.ConfigurationViewModel.CloseRequested += x, Observable.FromEventPattern(x => vm.ConfigurationViewModel.CloseRequested += x, x => vm.ConfigurationViewModel.CloseRequested -= x)
x => ViewModel!.ConfigurationViewModel.CloseRequested -= x
)
.Subscribe(_ => Close()) .Subscribe(_ => Close())
.DisposeWith(disposables); .DisposeWith(disposables);
} }
); );
} }
} }

View File

@ -79,11 +79,6 @@ public partial class ProfileTreeViewModel : TreeItemViewModel
public override bool SupportsChildren => true; public override bool SupportsChildren => true;
public void UpdateCanPaste()
{
throw new NotImplementedException();
}
protected override Task ExecuteDuplicate() protected override Task ExecuteDuplicate()
{ {
throw new NotSupportedException(); throw new NotSupportedException();

View File

@ -89,7 +89,7 @@
Background="{DynamicResource ControlFillColorDefaultBrush}" Background="{DynamicResource ControlFillColorDefaultBrush}"
IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Margin="8"> <StackPanel Orientation="Horizontal" Margin="8">
<shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" Margin="0 0 5 0" /> <shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" CornerRadius="3" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding ProfileConfiguration.Name}" /> <TextBlock Text="{CompiledBinding ProfileConfiguration.Name}" />
</StackPanel> </StackPanel>
</Border> </Border>

View File

@ -27,8 +27,9 @@ public partial class VisualEditorView : ReactiveUserControl<VisualEditorViewMode
this.WhenActivated(d => this.WhenActivated(d =>
{ {
ViewModel!.AutoFitRequested += ViewModelOnAutoFitRequested; VisualEditorViewModel vm = ViewModel!;
Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); vm!.AutoFitRequested += ViewModelOnAutoFitRequested;
Disposable.Create(() => vm.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d);
}); });
this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit(true)); this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit(true));

View File

@ -19,7 +19,8 @@ public partial class SplashView : ReactiveWindow<SplashViewModel>
#endif #endif
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
Observable.FromEventPattern(x => ViewModel!.CoreService.Initialized += x, x => ViewModel!.CoreService.Initialized -= x) SplashViewModel vm = ViewModel!;
Observable.FromEventPattern(x => vm.CoreService.Initialized += x, x => vm.CoreService.Initialized -= x)
.Subscribe(_ => Dispatcher.UIThread.Post(Close)) .Subscribe(_ => Dispatcher.UIThread.Post(Close))
.DisposeWith(disposables); .DisposeWith(disposables);
}); });

View File

@ -72,15 +72,20 @@
Background="Transparent" Background="Transparent"
ContextFlyout="{StaticResource ProfileMenuFlyout}" ContextFlyout="{StaticResource ProfileMenuFlyout}"
Classes.flyout-open="{CompiledBinding IsOpen, Source={StaticResource ProfileMenuFlyout}}"> Classes.flyout-open="{CompiledBinding IsOpen, Source={StaticResource ProfileMenuFlyout}}">
<Border CornerRadius="4" ClipToBounds="True" Grid.Column="0" Width="22" Height="22" Margin="0 0 5 0" VerticalAlignment="Center"> <shared:ProfileConfigurationIcon Grid.Column="0"
<shared:ProfileConfigurationIcon x:Name="ProfileIcon" ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}"> x:Name="ProfileIcon"
<shared:ProfileConfigurationIcon.Transitions> VerticalAlignment="Center"
<Transitions> ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}"
<DoubleTransition Property="Opacity" Duration="0:0:0.2" /> Width="22"
</Transitions> Height="22"
</shared:ProfileConfigurationIcon.Transitions> CornerRadius="4"
</shared:ProfileConfigurationIcon> Margin="0 0 5 0">
</Border> <shared:ProfileConfigurationIcon.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.2" />
</Transitions>
</shared:ProfileConfigurationIcon.Transitions>
</shared:ProfileConfigurationIcon>
<Panel Grid.Column="1" HorizontalAlignment="Left"> <Panel Grid.Column="1" HorizontalAlignment="Left">
<TextBlock Classes="fadable" <TextBlock Classes="fadable"

View File

@ -132,6 +132,6 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
public bool Matches(string s) public bool Matches(string s)
{ {
return s == $"profile/{ProfileConfiguration.ProfileId}/editor"; return s.StartsWith($"profile/{ProfileConfiguration.ProfileId}");
} }
} }

View File

@ -27,20 +27,27 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService;
private readonly ISettingsVmFactory _settingsVmFactory; private readonly ISettingsVmFactory _settingsVmFactory;
private readonly Progress<StreamProgress> _progress = new(); private readonly Progress<StreamProgress> _progress = new();
[Notify] private bool _isInstalled; [Notify] private bool _isInstalled;
[Notify] private bool _shouldInstall; [Notify] private bool _shouldInstall;
[Notify] private float _installProgress; [Notify] private float _installProgress;
public DefaultEntryItemViewModel(ILogger logger, IEntrySummary entry, IWorkshopService workshopService, IWindowService windowService, IPluginManagementService pluginManagementService, public DefaultEntryItemViewModel(ILogger logger,
IEntrySummary entry,
IWorkshopService workshopService,
IWindowService windowService,
IPluginManagementService pluginManagementService,
IProfileService profileService,
ISettingsVmFactory settingsVmFactory) ISettingsVmFactory settingsVmFactory)
{ {
_logger = logger; _logger = logger;
_workshopService = workshopService; _workshopService = workshopService;
_windowService = windowService; _windowService = windowService;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_profileService = profileService;
_settingsVmFactory = settingsVmFactory; _settingsVmFactory = settingsVmFactory;
Entry = entry; Entry = entry;
@ -62,19 +69,18 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
if (!result.IsSuccess) if (!result.IsSuccess)
{ {
await _windowService.CreateContentDialog().WithTitle("Failed to install entry") await _windowService.CreateContentDialog()
.WithTitle("Failed to install entry")
.WithContent($"Failed to install entry '{Entry.Name}' ({Entry.Id}): {result.Message}") .WithContent($"Failed to install entry '{Entry.Name}' ({Entry.Id}): {result.Message}")
.WithCloseButtonText("Skip and continue") .WithCloseButtonText("Skip and continue")
.ShowAsync(); .ShowAsync();
} }
// If the entry is a plugin, enable the plugin and all features // If the entry is a plugin, enable the plugin and all features
else if (result.Entry?.EntryType == EntryType.Plugin) else if (result.Entry?.EntryType == EntryType.Plugin)
{
await EnablePluginAndFeatures(result.Entry); await EnablePluginAndFeatures(result.Entry);
} else if (result.Entry?.EntryType == EntryType.Profile) // If the entry is a profile, move it to the General profile category
{ else if (result.Entry?.EntryType == EntryType.Profile)
PrepareProfile(result.Entry);
}
return result.IsSuccess; return result.IsSuccess;
} }
@ -107,14 +113,37 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
_logger.Warning(e, "Failed to enable plugin feature '{FeatureName}', skipping", pluginFeatureInfo.Name); _logger.Warning(e, "Failed to enable plugin feature '{FeatureName}', skipping", pluginFeatureInfo.Name);
} }
} }
// If the plugin has a mandatory settings window, open it and wait // If the plugin has a mandatory settings window, open it and wait
if (plugin.ConfigurationDialog != null && plugin.ConfigurationDialog.IsMandatory) if (plugin.ConfigurationDialog != null && plugin.ConfigurationDialog.IsMandatory)
{ {
if (plugin.Resolve(plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel) if (plugin.Resolve(plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel)
throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}");
await _windowService.ShowDialogAsync(new PluginSettingsWindowViewModel(viewModel)); await _windowService.ShowDialogAsync(new PluginSettingsWindowViewModel(viewModel));
} }
} }
private void PrepareProfile(InstalledEntry entry)
{
if (!entry.TryGetMetadata("ProfileId", out Guid profileId))
return;
ProfileConfiguration? profile = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId);
if (profile == null)
return;
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "General") ?? _profileService.CreateProfileCategory("General", true);
if (category.ProfileConfigurations.Contains(profile))
return;
// Add the profile to the category
category.AddProfileConfiguration(profile, null);
// Suspend all but the first profile in the category
profile.IsSuspended = category.ProfileConfigurations.Count > 1;
_profileService.SaveProfileCategory(category);
}
} }

View File

@ -24,15 +24,14 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="0 0 10 0" Margin="0 0 10 0"
Width="280" Width="280"
Height="280" Height="280">
IsEnabled="False">
<StackPanel> <StackPanel>
<avalonia:MaterialIcon Kind="HandBackLeft" Width="150" Height="150" HorizontalAlignment="Center" /> <avalonia:MaterialIcon Kind="HandBackLeft" Width="150" Height="150" HorizontalAlignment="Center" />
<TextBlock TextAlignment="Center" Classes="h4" Margin="0 10 0 0"> <TextBlock TextAlignment="Center" Classes="h4" Margin="0 10 0 0">
Left-handed preset (NYI) Left-handed preset
</TextBlock> </TextBlock>
<TextBlock TextAlignment="Center" Classes="subtitle" TextWrapping="Wrap"> <TextBlock TextAlignment="Center" Classes="subtitle" TextWrapping="Wrap">
A preset with the mouse on the left side of the keyboard A preset with the mouse on the left side of the keyboard (are you the 10%?)
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
</Button> </Button>

View File

@ -27,9 +27,7 @@ public class SurfaceStepViewModel : WizardStepViewModel
private void ExecuteSelectLayout(string layout) private void ExecuteSelectLayout(string layout)
{ {
// TODO: Implement the layout _deviceService.AutoArrangeDevices(layout == "left");
_deviceService.AutoArrangeDevices();
Wizard.ChangeScreen<SettingsStepViewModel>(); Wizard.ChangeScreen<SettingsStepViewModel>();
} }
} }

View File

@ -13,6 +13,7 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Avalonia; using Avalonia;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using SkiaSharp; using SkiaSharp;
@ -180,11 +181,18 @@ public partial class SurfaceEditorViewModel : RoutableScreen, IMainScreenViewMod
private async Task ExecuteAutoArrange() private async Task ExecuteAutoArrange()
{ {
bool confirmed = await _windowService.ShowConfirmContentDialog("Auto-arrange layout", "Are you sure you want to auto-arrange your layout? Your current settings will be overwritten."); ContentDialogResult contentDialogResult = await _windowService.CreateContentDialog()
if (!confirmed) .WithTitle("Auto-arrange layout")
return; .WithContent("Which preset would you like to apply? Your current settings will be overwritten.")
.HavingPrimaryButton(b => b.WithText("Left-handed preset"))
_deviceService.AutoArrangeDevices(); .HavingSecondaryButton(b => b.WithText("Right-handed preset"))
.WithCloseButtonText("Cancel")
.ShowAsync();
if (contentDialogResult == ContentDialogResult.Primary)
_deviceService.AutoArrangeDevices(true);
else if (contentDialogResult == ContentDialogResult.Secondary)
_deviceService.AutoArrangeDevices(false);
} }
private void RenderServiceOnFrameRendering(object? sender, FrameRenderingEventArgs e) private void RenderServiceOnFrameRendering(object? sender, FrameRenderingEventArgs e)

View File

@ -33,16 +33,17 @@ public partial class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
this.WhenActivated(d => this.WhenActivated(d =>
{ {
ViewModel!.AutoFitRequested += ViewModelOnAutoFitRequested; NodeScriptViewModel vm = ViewModel!;
ViewModel.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d); vm.AutoFitRequested += ViewModelOnAutoFitRequested;
if (ViewModel.IsPreview) vm.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d);
if (vm.IsPreview)
{ {
BoundsProperty.Changed.Subscribe(BoundsPropertyChanged).DisposeWith(d); BoundsProperty.Changed.Subscribe(BoundsPropertyChanged).DisposeWith(d);
ViewModel.NodeViewModels.ToObservableChangeSet().Subscribe(_ => AutoFitIfPreview()).DisposeWith(d); vm.NodeViewModels.ToObservableChangeSet().Subscribe(_ => AutoFitIfPreview()).DisposeWith(d);
} }
Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle); Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle);
Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); Disposable.Create(() => vm.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d);
}); });
} }

View File

@ -38,6 +38,7 @@
Grid.Column="0" Grid.Column="0"
ConfigurationIcon="{CompiledBinding Icon}" ConfigurationIcon="{CompiledBinding Icon}"
VerticalAlignment="Center" VerticalAlignment="Center"
CornerRadius="4"
Width="22" Width="22"
Height="22" Height="22"
Margin="0 0 10 0" /> Margin="0 0 10 0" />

View File

@ -180,13 +180,15 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
{ {
await _authLock.WaitAsync(cancellationToken); await _authLock.WaitAsync(cancellationToken);
// Start a HTTP listener, this port could be in use but chances are very slim
// IdentityServer only accepts these two redirect URLs
string redirectUri = Constants.StartupArguments.Contains("--alt-login-callback") ? "http://localhost:56789" : "http://localhost:57461";
try try
{ {
if (_isLoggedInSubject.Value) if (_isLoggedInSubject.Value)
return; return;
// Start a HTTP listener, this port could be in use but chances are very slim
string redirectUri = "http://localhost:57461";
using HttpListener listener = new(); using HttpListener listener = new();
listener.Prefixes.Add(redirectUri + "/"); listener.Prefixes.Add(redirectUri + "/");
listener.Start(); listener.Start();
@ -249,7 +251,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
} }
catch (HttpListenerException e) catch (HttpListenerException e)
{ {
throw new ArtemisWebClientException($"HTTP listener for login callback failed with error code {e.ErrorCode}", e); // I've seen the Nvidia app do this after a login. What are the odds...
if (e.ErrorCode == 32)
throw new ArtemisWebClientException($"HTTP listener for login callback failed because another application is already listening on '{redirectUri}', please close that application and try again", e);
else
throw new ArtemisWebClientException($"HTTP listener for login callback failed with error code {e.ErrorCode}", e);
} }
finally finally
{ {