Skip to content

Commit

Permalink
Add possibility to drag&drop subjects and groups in tree view.
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikgolda committed May 17, 2019
1 parent b1dea0f commit 772efa3
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<OutputPath>..\bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
Expand Down Expand Up @@ -88,6 +88,9 @@
<Compile Include="VisualConfig\WindowSettings.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="gong-wpf-dragdrop">
<Version>2.0.1</Version>
</PackageReference>
<PackageReference Include="Humanizer.Core">
<Version>2.6.2</Version>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@

namespace Soloplan.WhatsON.GUI.Common.SubjectTreeView
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using GongSolutions.Wpf.DragDrop;
using NLog;
using Soloplan.WhatsON.GUI.Common.VisualConfig;
using Soloplan.WhatsON.Serialization;

/// <summary>
/// Top level viewmodel used to bind to <see cref="SubjectTreeView"/>.
/// </summary>
public class SubjectTreeViewModel : IHandleDoubleClick
public class SubjectTreeViewModel : IHandleDoubleClick, IDropTarget
{
/// <summary>
/// The logger.
Expand All @@ -28,6 +32,8 @@ public class SubjectTreeViewModel : IHandleDoubleClick
/// </summary>
private ObservableCollection<SubjectGroupViewModel> subjectGroups;

public event EventHandler ConfigurationChanged;

/// <summary>
/// Gets observable collection of subject groups, the top level object in tree view binding.
/// </summary>
Expand All @@ -43,6 +49,65 @@ public class SubjectTreeViewModel : IHandleDoubleClick
/// </summary>
public bool OneGroup => this.SubjectGroups.Count == 1;

/// <summary>Updates the current drag state.</summary>
/// <param name="dropInfo">Information about the drag.</param>
/// <remarks>
/// To allow a drop at the current drag position, the <see cref="P:GongSolutions.Wpf.DragDrop.DropInfo.Effects" /> property on
/// <paramref name="dropInfo" /> should be set to a value other than <see cref="F:System.Windows.DragDropEffects.None" />
/// and <see cref="P:GongSolutions.Wpf.DragDrop.DropInfo.Data" /> should be set to a non-null value.
/// </remarks>
public void DragOver(IDropInfo dropInfo)
{
if (object.ReferenceEquals(dropInfo.TargetItem, dropInfo.Data))
{
return;
}

if (dropInfo.Data is SubjectViewModel)
{
if (dropInfo.TargetItem is SubjectGroupViewModel)
{
dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;
dropInfo.Effects = DragDropEffects.Move;
}
else if (dropInfo.TargetItem is SubjectViewModel)
{
dropInfo.Effects = DragDropEffects.Move;
dropInfo.DropTargetAdorner = (dropInfo.InsertPosition & RelativeInsertPosition.TargetItemCenter) != 0 ? DropTargetAdorners.Highlight : DropTargetAdorners.Insert;
}
}
else if (dropInfo.Data is SubjectGroupViewModel)
{
if (dropInfo.TargetItem is SubjectGroupViewModel)
{
dropInfo.Effects = DragDropEffects.Move;
dropInfo.DropTargetAdorner = (dropInfo.InsertPosition & RelativeInsertPosition.TargetItemCenter) != 0 ? DropTargetAdorners.Highlight : DropTargetAdorners.Insert;
}
}
}

/// <summary>Performs a drop.</summary>
/// <param name="dropInfo">Information about the drop.</param>
public void Drop(IDropInfo dropInfo)
{
if (dropInfo.Effects != DragDropEffects.Move)
{
log.Warn("Unexpected drop operation. {data}", new { Effect = dropInfo.Effects, dropInfo.Data, Target = dropInfo.TargetItem });
return;
}

if (dropInfo.Data is SubjectGroupViewModel drggedGroup)
{
this.DropGrup(dropInfo, drggedGroup);
}
else if (dropInfo.Data is SubjectViewModel draggedSubject)
{
this.DropSubject(dropInfo, draggedSubject);
}

this.ConfigurationChanged?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Initializes the model.
/// </summary>
Expand Down Expand Up @@ -93,6 +158,29 @@ public void Update(ApplicationConfiguration configuration)
}
}

/// <summary>
/// Writes model settings to <paramref name="configuration"/>.
/// </summary>
/// <param name="configuration">Application configuration.</param>
public void WriteToConfiguration(ApplicationConfiguration configuration)
{
var subjectConfigurations = configuration.SubjectsConfiguration.ToList();
configuration.SubjectsConfiguration.Clear();
foreach (var subjectGroupViewModel in this.SubjectGroups)
{
foreach (var subjectViewModel in subjectGroupViewModel.SubjectViewModels)
{
var config = subjectConfigurations.FirstOrDefault(cfg => cfg.Identifier == subjectViewModel.Identifier);
if (config != null)
{
config.GetConfigurationByKey(Subject.Category).Value = subjectGroupViewModel.GroupName;
}

configuration.SubjectsConfiguration.Add(config);
}
}
}

public void OnDoubleClick(object sender, MouseButtonEventArgs e)
{
foreach (var subjectGroupViewModel in this.SubjectGroups)
Expand Down Expand Up @@ -137,11 +225,110 @@ public void ApplyGroupExpansionState(IList<GroupExpansionSettings> groupExpansio
}
}

/// <summary>
/// Moves object given by <paramref name="source"/> to <paramref name="target"/>
/// </summary>
/// <param name="source">Information about source location of moved object.</param>
/// <param name="target">Information about desired location of moved object.</param>
/// <param name="insertPosition">Additional information about where in relation to <paramref name="target"/> the object should be placed.</param>
private static void MoveObject(MovedObjectLocation source, MovedObjectLocation target, RelativeInsertPosition insertPosition)
{
var insertPositionInternal = insertPosition;
if ((insertPositionInternal & RelativeInsertPosition.TargetItemCenter) != 0)
{
insertPositionInternal = RelativeInsertPosition.AfterTargetItem;
}

var targetIndex = target.Index;
if ((insertPositionInternal & RelativeInsertPosition.AfterTargetItem) != 0)
{
targetIndex = targetIndex + 1;
}

if (object.ReferenceEquals(target.List, source.List))
{
if (targetIndex > source.Index)
{
targetIndex = targetIndex - 1;
}

if (source.Index == targetIndex)
{
return;
}
}

var obj = source.List[source.Index];
source.List.RemoveAt(source.Index);
target.List.Insert(targetIndex, obj);
}

private IEnumerable<IGrouping<string, SubjectConfiguration>> ParseConfiguration(ApplicationConfiguration configuration)
{
return configuration.SubjectsConfiguration.GroupBy(config => config.GetConfigurationByKey(Subject.Category)?.Value?.Trim() ?? string.Empty);
}

/// <summary>
/// Handles dropping of <see cref="SubjectViewModel"/>.
/// </summary>
/// <param name="dropInfo">All drop information.</param>
/// <param name="droppedSubject">The dropped subject.</param>
private void DropSubject(IDropInfo dropInfo, SubjectViewModel droppedSubject)
{
var currentSubjectGroupModel = this.GetSubjectGroup(droppedSubject);
if (dropInfo.TargetItem is SubjectGroupViewModel model)
{
if (object.ReferenceEquals(currentSubjectGroupModel.List, model.SubjectViewModels))
{
return;
}

MoveObject(currentSubjectGroupModel, new MovedObjectLocation(model.SubjectViewModels, model.SubjectViewModels.Count - 1), RelativeInsertPosition.AfterTargetItem);
}

if (dropInfo.TargetItem is SubjectViewModel targetModel)
{
var targetGroup = this.GetSubjectGroup(targetModel);
MoveObject(currentSubjectGroupModel, targetGroup, dropInfo.InsertPosition);
}
}

/// <summary>
/// Handles dropping of <see cref="SubjectGroupViewModel"/>.
/// </summary>
/// <param name="dropInfo">All drop information.</param>.
/// <param name="droppedGroup">The dropped group.</param>
private void DropGrup(IDropInfo dropInfo, SubjectGroupViewModel droppedGroup)
{
if (dropInfo.TargetItem is SubjectGroupViewModel targetModel)
{
var index = this.SubjectGroups.IndexOf(droppedGroup);
var targetIndex = this.SubjectGroups.IndexOf(targetModel);
MoveObject(new MovedObjectLocation(this.SubjectGroups, index), new MovedObjectLocation(this.SubjectGroups, targetIndex), dropInfo.InsertPosition);
}
}

/// <summary>
/// Gets information about location in parent collection.
/// </summary>
/// <param name="subjectViewModel">The subject view model.</param>
/// <returns>The information about location in target collection.</returns>
private MovedObjectLocation GetSubjectGroup(SubjectViewModel subjectViewModel)
{
foreach (var subjectGroupViewModel in this.SubjectGroups)
{
var index = subjectGroupViewModel.SubjectViewModels.IndexOf(subjectViewModel);
if (index < 0)
{
continue;
}

return new MovedObjectLocation(subjectGroupViewModel.SubjectViewModels, index);
}

return null;
}

private void SchedulerStatusQueried(object sender, Subject e)
{
foreach (var subjectGroupViewModel in this.SubjectGroups)
Expand All @@ -160,5 +347,27 @@ private ObservableCollection<SubjectGroupViewModel> CreateSubjectGroupViewModelC
var subject = new ObservableCollection<SubjectGroupViewModel>();
return subject;
}

/// <summary>
/// Helper class with information about where the moved object is or should be in <see cref="List"/>.
/// </summary>
private class MovedObjectLocation
{
public MovedObjectLocation(IList list, int index)
{
this.List = list;
this.Index = index;
}

/// <summary>
/// Gets the source/target list.
/// </summary>
public IList List { get; }

/// <summary>
/// Gets the actual/desired location of object.
/// </summary>
public int Index { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dd="urn:gong-wpf-dragdrop"
xmlns:local="clr-namespace:Soloplan.WhatsON.GUI.Common.SubjectTreeView"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Expand Down Expand Up @@ -34,7 +35,10 @@
</ResourceDictionary>
</UserControl.Resources>

<TreeView Name="mainTreeView">
<TreeView Name="mainTreeView"
dd:DragDrop.DropHandler="{Binding}"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True">
<TreeView.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type TreeViewItem}}" TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="OnTreeItemDoubleClick" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Soloplan.WhatsON.GUI.Common.SubjectTreeView
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
Expand Down Expand Up @@ -39,9 +40,15 @@ public SubjectsTreeView()
}
}

/// <summary>
/// Event fired when configuration is changed by user interaction with <see cref="SubjectTreeView"/>.
/// </summary>
public event EventHandler ConfigurationChanged;

public void Init(ObservationScheduler scheduler, ApplicationConfiguration configuration, IList<Subject> initialSubjectState)
{
this.model = new SubjectTreeViewModel();
this.model.ConfigurationChanged += (s, e) => this.ConfigurationChanged?.Invoke(this, EventArgs.Empty);
this.model.Init(scheduler, configuration, initialSubjectState);
this.DataContext = this.model;
this.SetupDataContext();
Expand All @@ -53,6 +60,15 @@ public void Update(ApplicationConfiguration configuration)
this.SetupDataContext();
}

/// <summary>
/// Writes current settings from <see cref="SubjectsTreeView"/> to <paramref name="configuration"/>.
/// </summary>
/// <param name="configuration">Configuration to which data should be written.</param>
public void WriteToConfiguration(ApplicationConfiguration configuration)
{
this.model.WriteToConfiguration(configuration);
}

public TreeListSettings GetTreeListSettings()
{
return new TreeListSettings
Expand Down
8 changes: 8 additions & 0 deletions src/Soloplan.WhatsON.GUI/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public MainWindow(ObservationScheduler scheduler, ApplicationConfiguration confi
this.ShowInTaskbar = this.config.ShowInTaskbar;
this.Topmost = this.config.AlwaysOnTop;
this.DataContext = this;
this.mainTreeView.ConfigurationChanged += this.MainTreeViewOnConfigurationChanged;
}

public bool IsTreeInitialized
Expand Down Expand Up @@ -212,5 +213,12 @@ private void NewConnectorClick(object sender, RoutedEventArgs e)
}
}
}

private void MainTreeViewOnConfigurationChanged(object sender, EventArgs e)
{
this.mainTreeView.WriteToConfiguration(this.config);
SerializationHelper.SaveConfiguration(this.config);
this.ConfigurationApplied?.Invoke(this, new ValueEventArgs<ApplicationConfiguration>(this.config));
}
}
}

0 comments on commit 772efa3

Please sign in to comment.