Skip to content

Commit

Permalink
Add context menu and quick commands in tree views, allow to remove da…
Browse files Browse the repository at this point in the history
…ta from source
  • Loading branch information
ReMinoer committed Jun 5, 2022
1 parent 9be623d commit e9925eb
Show file tree
Hide file tree
Showing 11 changed files with 435 additions and 83 deletions.
55 changes: 3 additions & 52 deletions Calame/EventAggregatorExtension.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
using System;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Caliburn.Micro;

namespace Calame
Expand All @@ -17,58 +14,12 @@ static public Task PublishAsync(this IEventAggregator eventAggregator, object me

static public void Subscribe(this IEventAggregator eventAggregator, object subscriber)
{
eventAggregator.Subscribe(subscriber, async task =>
{
// If already on UI thread, just await
if (Application.Current.Dispatcher.CheckAccess())
{
await task();
return;
}

try
{
await task();
}
catch (OperationCanceledException)
{
// Rethrow cancellation to publisher
throw;
}
catch (Exception ex)
{
// Throw on UI thread if messaging throw
Execute.BeginOnUIThread(() => ExceptionDispatchInfo.Capture(ex).Throw());
}
});
eventAggregator.Subscribe(subscriber, TaskHandler.Handle);
}

static public void SubscribeOnUI(this IEventAggregator eventAggregator, object subscriber)
{
eventAggregator.Subscribe(subscriber, task =>
{
// If already on UI thread, just return the task
if (Application.Current.Dispatcher.CheckAccess())
return task();

var taskCompletionSource = new TaskCompletionSource<bool>();

Execute.BeginOnUIThread(async () =>
{
// Let messaging throw on UI thread (just keep cancellation)
try
{
await task();
taskCompletionSource.SetResult(true);
}
catch (OperationCanceledException)
{
taskCompletionSource.SetCanceled();
}
});

return taskCompletionSource.Task;
});
eventAggregator.Subscribe(subscriber, TaskHandler.HandleOnUIThread);
}
}
}
62 changes: 62 additions & 0 deletions Calame/TaskHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using System.Windows;
using Caliburn.Micro;
using Action = System.Action;

namespace Calame
{
static public class TaskHandler
{
static public async Task Handle(Func<Task> task)
{
// If already on UI thread, just await
if (Application.Current.Dispatcher.CheckAccess())
{
await task();
return;
}

try
{
await task();
}
catch (OperationCanceledException)
{
// Rethrow cancellation to publisher
throw;
}
catch (Exception ex)
{
// Throw on UI thread if messaging throw
Execute.BeginOnUIThread(() => ExceptionDispatchInfo.Capture(ex).Throw());
}
}

static public Task HandleOnUIThread(Func<Task> task)
{
// If already on UI thread, just return the task
if (Application.Current.Dispatcher.CheckAccess())
return task();

var taskCompletionSource = new TaskCompletionSource<bool>();

Execute.BeginOnUIThread(async () =>
{
// Let messaging throw on UI thread (just keep the cancellation for publisher)
try
{
await task();
taskCompletionSource.SetResult(true);
}
catch (OperationCanceledException)
{
taskCompletionSource.SetCanceled();
}
});

return taskCompletionSource.Task;
}
}
}
32 changes: 29 additions & 3 deletions Calame/UserControls/CalameTreeView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
xmlns:attachedProperties="clr-namespace:Calame.AttachedProperties"
xmlns:behaviors="clr-namespace:Calame.Behaviors"
xmlns:icons="clr-namespace:Calame.Icons"
xmlns:converters="clr-namespace:Calame.Converters"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="200">
<UserControl.Resources>
<converters:ObjectToVisibilityConverter x:Key="ObjectToVisibilityConverter" />
</UserControl.Resources>
<DockPanel DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}">
<DockPanel DockPanel.Dock="Top">
<StackPanel DockPanel.Dock="Left"
Expand Down Expand Up @@ -44,6 +48,7 @@
</DockPanel>
<TreeView ItemsSource="{Binding TreeItems}"
Background="White"
HorizontalContentAlignment="Stretch"
attachedProperties:Focus.IsFocused="{Binding IsTreeViewFocused}">
<b:Interaction.Behaviors>
<behaviors:TreeViewBindableSelectedItemBehavior SelectedItem="{Binding SelectedTreeItem, Mode=TwoWay}" />
Expand Down Expand Up @@ -104,15 +109,36 @@
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type utils:ITreeViewItemModel}">
<Border Padding="2">
<Border>
<Border.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}" />
</Border.ContextMenu>
<DockPanel>
<userControls:CalameIcon DockPanel.Dock="Left"
Margin="0 0 3 0"
Margin="2 2 3 2"
IconDescription="{Binding IconDescription}"
IconSize="14"
IconProvider="{Binding IconProvider, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type userControls:CalameTreeView}}}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding DisplayName}" FontWeight="{Binding FontWeight}" Background="Transparent">
<Button DockPanel.Dock="Right"
Margin="2 0 0 0"
Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
Command="{Binding QuickCommand}"
CommandParameter="{Binding}"
ToolTip="{Binding QuickCommandToolTip}"
Visibility="{Binding QuickCommand, Converter={StaticResource ObjectToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" Margin="1 0">
<userControls:CalameIcon IconSize="12"
IconDescription="{Binding QuickCommandIconDescription}"
IconProvider="{Binding IconProvider, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type userControls:CalameTreeView}}}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding QuickCommandLabel}"
Visibility="{Binding QuickCommandLabel, Converter={StaticResource ObjectToVisibilityConverter}}"
FontSize="10"
Margin="3 0 0 0" VerticalAlignment="Center"></TextBlock>
</StackPanel>
</Button>
<TextBlock Margin="0 2" Text="{Binding DisplayName}" FontWeight="{Binding FontWeight}" Background="Transparent">
<b:Interaction.Behaviors>
<behaviors:TextBlockHighlightFilteredBehavior ItemText="{Binding DisplayName}"
FilterText="{Binding FilterText, ElementName=This}"
Expand Down
170 changes: 170 additions & 0 deletions Calame/Utils/AddCollectionItemCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Input;
using Calame.Icons;
using Diese.Collections;
using Gemini.Framework;

namespace Calame.Utils
{
public class AddCollectionItemCommand : ICommand
{
private readonly IList _list;
private readonly IList<Type> _newTypeRegistry;
private readonly IIconProvider _iconProvider;
private readonly IIconDescriptor _iconDescriptor;

private IList<Type> _newItemTypes;
private readonly ICommand _addItemCommand;

private bool CanAddItem => _list != null && !_list.IsFixedSize;
public object AddedItem { get; private set; }

public event EventHandler CanExecuteChanged;
public event EventHandler ItemAdded;

public AddCollectionItemCommand(IList list, IList<Type> newTypeRegistry, IIconProvider iconProvider, IIconDescriptor iconDescriptor)
{
_list = list;
_newTypeRegistry = newTypeRegistry;
_iconProvider = iconProvider;
_iconDescriptor = iconDescriptor;
_addItemCommand = new RelayCommand(x => AddItemOfType((Type)x));

RefreshNewItemTypes();
}

public bool CanExecute(object _) => _newItemTypes != null && _newItemTypes.Count > 0;
public void Execute(object _)
{
if (_newItemTypes.Count == 1)
{
AddItemOfType(_newItemTypes[0]);
return;
}

var contextMenu = new ContextMenu();

string[] typeNames = _newItemTypes.Select(x => x.Name).ToArray();
ReduceTypeNamePatterns(typeNames);

for (int i = 0; i < _newItemTypes.Count; i++)
{
var menuItem = new MenuItem
{
Header = typeNames[i],
Command = _addItemCommand,
CommandParameter = _newItemTypes[i],
Icon = _iconProvider.GetControl(_iconDescriptor.GetTypeIcon(_newItemTypes[i]), 16)
};

contextMenu.Items.Add(menuItem);
}

contextMenu.IsOpen = true;
}

private void AddItemOfType(Type itemType) => AddItem(CreateItem(itemType));
private void AddItem(object item)
{
_list.Add(item);

AddedItem = item;
ItemAdded?.Invoke(this, EventArgs.Empty);
}

private object CreateItem(Type type)
{
if (type.IsGenericType && type.GetConstructor(type.GenericTypeArguments) != null)
return Activator.CreateInstance(type, type.GenericTypeArguments.Select(Activator.CreateInstance).ToArray());

return Activator.CreateInstance(type);
}

private void RefreshNewItemTypes()
{
Type itemType = GetNewItemType();
if (itemType == null)
return;

IList<Type> newItemTypes = _newTypeRegistry?.Where(x => itemType.IsAssignableFrom(x)).ToList() ?? new List<Type>();
if (!newItemTypes.Contains(itemType) && IsInstantiableWithoutParameter(itemType))
newItemTypes.Insert(0, itemType);

_newItemTypes = newItemTypes;
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

private Type GetNewItemType()
{
if (!CanAddItem)
return null;

Type[] interfaces = _list.GetType().GetInterfaces();
if (!interfaces.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>), out Type collectionType))
return null;

return collectionType.GenericTypeArguments[0];
}

static private bool IsInstantiableWithoutParameter(Type type)
{
if (type.IsValueType)
return true;

if (type.IsInterface)
return false;
if (type.IsAbstract)
return false;
if (type.IsGenericType && type.GetConstructor(type.GenericTypeArguments) != null)
return false;
if (type.GetConstructor(Type.EmptyTypes) == null)
return false;

return true;
}

static private void ReduceTypeNamePatterns(string[] values)
{
while (true)
{
int upperIndex = values[0].Skip(1).IndexOf(char.IsUpper) + 1;
if (upperIndex <= 0)
break;

string prefix = values[0].Substring(0, upperIndex);
if (!values.Skip(1).All(x => x.StartsWith(prefix)))
break;

for (int i = 0; i < values.Length; i++)
values[i] = values[i].Substring(prefix.Length);
}

while (true)
{
int upperIndex = LastIndexOf(values[0], char.IsUpper);
if (upperIndex == -1)
break;

int suffixLength = values[0].Length - upperIndex;
string suffix = values[0].Substring(upperIndex, suffixLength);
if (!values.Skip(1).All(x => x.EndsWith(suffix)))
break;

for (int i = 0; i < values.Length; i++)
values[i] = values[i].Substring(0, values[i].Length - suffixLength);
}
}

static public int LastIndexOf(string value, Predicate<char> predicate)
{
for (int i = value.Length - 1; i >= 0; i--)
if (predicate(value[i]))
return i;
return -1;
}
}
}
Loading

0 comments on commit e9925eb

Please sign in to comment.