Tech
Tech
namespace TodoApp
{
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't
initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
/// <summary>
/// Called when tags get updated by user
/// </summary>
/// <param name="tags"></param>
public delegate void TagsChangedEventHandler(List<TagInfo> tags);
/// <summary>
/// Tags that are marked to be displayed
/// </summary>
public List<TagInfo> CurrentlyVisibleTags => _displayedTags;
/// <summary>
/// Current info of the whole project
/// </summary>
/// <value></value>
public ProjectInfo Info
{
//TODO: Add auto update for column and card info based on events
get => _info;
}
/// <summary>
/// Removes card from it's current column and creates a new copy in the
destination column<para/>
/// Throws null reference exception if either column or card reference invalid
objects
/// </summary>
/// <param name="cardId">Id of the card that will be moved</param>
/// <param name="columnId">Destination column id</param>
/// <param name="preferredPosition">If not null card will be positioned at this
position, shifting every other card further down</param>
public void ChangeCardColumn(Guid cardId, Guid columnId, int?
preferredPosition)
{
CardColumn column = _columns.FirstOrDefault(c => c.HasCard(cardId)) ??
throw new NullReferenceException("No column has card with given id");
//technically this will never return null because we established that column
does indeed have needed card
//but just in case >_>
Card card = column.GetCard(cardId) ?? throw new
NullReferenceException("No card has given id in this column");
CardColumn dstColumn = _columns.FirstOrDefault(c => c.Id ==
columnId) ?? throw new NullReferenceException("No column has id of the
destination column");
dstColumn.AddNewCard(card.Info, preferredPosition);
column.RemoveCard(cardId);
_updateInfo();
}
/// <summary>
/// Moves columns in the window. This simply reorders columns
/// </summary>
/// <param name="columnId">Which column to move</param>
/// <param name="position">Which slot should the column occupy</param>
public void MoveCardColumn(Guid columnId, int? position)
{
CardColumn moved = _columns.FirstOrDefault(p => p.Id == columnId) ??
throw new NullReferenceException("Column with given id is not present in the
project");
_columns.Remove(moved);
ColumnPanel.Children.Remove(moved);
if (position == null)
{
_columns.Add(moved);
ColumnPanel.Children.Add(moved);
}
else
{
_columns.Insert(position.Value, moved);
ColumnPanel.Children.Insert(position.Value, moved);
}
}
/// <summary>
/// Removes card from it's current column and creates a new copy in the
destination column<para/>
/// Throws null reference exception if either column or card reference invalid
objects<para/>
/// This version uses relies on ever changing _columns array making it unreliable
and it will probably be deprecated in the future
/// </summary>
/// <param name="cardId">Id of the card that will be moved</param>
/// <param name="columnId">Index of the destination column in the column
array</param>
/// <param name="preferredPosition">If not null card will be positioned at this
position, shifting every other card further down</param>
[Obsolete("his version uses relies on ever changing _columns array making it
unreliable and it will probably be deprecated in the future")]
public void ChangeCardColumn(Guid cardId, int columnId, int?
preferredPosition)
{
CardColumn column = _columns.FirstOrDefault(c => c.HasCard(cardId)) ??
throw new NullReferenceException("No column has card with given id");
//technically this will never return null because we established that column
does indeed have needed card
//but just in case >_>
Card card = column.GetCard(cardId) ?? throw new
NullReferenceException("No card has given id in this column");
CardColumn dstColumn = _columns[columnId];
dstColumn.AddNewCard(card.Info);
column.RemoveCard(cardId);
_updateInfo();
}
public MainWindow()
{
InitializeComponent();
_info = new ProjectInfo();
_info.Tags.Add(new TagInfo(0, "Very important", TagColor.DarkRed, 2));
_info.Tags.Add(new TagInfo(1, null, TagColor.LightYellow, 1));
_info.Tags.Add(new TagInfo(2, null, TagColor.Blue, 0));
AddHandler(DragDrop.DragOverEvent, _dragOver);
AddHandler(DragDrop.DragLeaveEvent, _dragLeave);
AddHandler(DragDrop.DropEvent, _dragDrop);
}
/// <summary>
/// Bind events to the card and mark project as edited
/// </summary>
/// <param name="card"></param>
public void RegisterCard(Card card)
{
card.OnCardDragged += _cardBeginDrag;
card.OnCardFinishedDrag += _cardFinishedDrag;
}
if (info.ItemType != DragItemType.Column)
{
return;
}
int index = _columns.IndexOf(column);
if (_previewDragDrop != null)
{
ColumnPanel.Children.Remove(_previewDragDrop);
}
_previewDragDrop = new ColumnDragPreview() { Id = index };
ColumnPanel.Children.Insert(index, _previewDragDrop);
}
/// <summary>
/// Creates new column using provided info or default column if info is null
/// </summary>
/// <param name="info"></param>
private void _addColumn(CardColumnInfo? info, int? position = null)
{
CardColumn column = new CardColumn();
column.Init(this, info);
column.OnColumnRemoved += _onColumnRemoved;
column.OnOtherColumnDraggedOver += _onColumnDraggedOverColumn;
if (position == null)
{
_columns.Add(column);
ColumnPanel.Children.Add(column);
}
else
{
_columns.Insert(position.Value, column);
ColumnPanel.Children.Insert(position.Value, column);
}
OnNewColumnAdded?.Invoke(column.Info, _columns.Count);
}
/// <summary>
/// Regenerate column info by iterating over current columns
/// </summary>
private void _updateInfo()
{
List<CardColumnInfo> columns = new List<CardColumnInfo>();
foreach (CardColumn column in _columns)
{
columns.Add(column.Info);
}
_info.Columns = columns;
}
//since tags were changed we have to notify all card of changed tags
OnTagsChanged?.Invoke(Tags);
Info.CurrentTagId = dialog.CurrentTagId;
}
private void _onDisplayLabelEditWindowPressed(object? sender,
RoutedEventArgs e)
{
_displayLabelEditWindow();
}
/// <summary>
/// Opens a file save menu and waits for user to pick a file
/// </summary>
/// <returns>Path to the opened file</returns>
private async System.Threading.Tasks.Task<string?> _getProjectFilePath()
{
SaveFileDialog dialog = new SaveFileDialog();
dialog.Filters?.Add(new FileDialogFilter() { Name = "Todo Projects",
Extensions = { "todo" } });
return await dialog.ShowAsync(this);
}
private async void _onSaveRequested(object? sender, RoutedEventArgs e)
{
CurrentProjectFilePath ??= await _getProjectFilePath();
if (CurrentProjectFilePath == null)
{
return;
}
_updateInfo();
string info = Newtonsoft.Json.JsonConvert.SerializeObject(Info);
File.WriteAllText(CurrentProjectFilePath, info);
}
/// <summary>
/// Writes info from project info to columns and sets given info as current info
/// </summary>
/// <param name="info">Project info to make current</param>
private void _loadFromFile(ProjectInfo info)
{
_info = info;
foreach (CardColumnInfo columnInfo in info.Columns)
{
_addColumn(columnInfo);
}
}
}
}
Листинг 3. Код файла App.xaml.cs
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace TodoApp
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
base.OnFrameworkInitializationCompleted();
}
}
}
Листинг 4. Код файла MainWindow.xaml
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:todo="clr-namespace:TodoApp"
mc:Ignorable="d" d:DesignWidth="50"
Width="1000"
d:DesignHeight="50"
x:Class="TodoApp.MainWindow"
Title="Todo App"
DragDrop.AllowDrop="True">
<Window.ContextMenu>
<ContextMenu>
<MenuItem Header="Add column"
Click="_onNewColumnButtonClicked" />
</ContextMenu>
</Window.ContextMenu>
<Grid>
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New" Click="_onNewRequested" />
<MenuItem Header="_Open" Click="_onOpenRequested" />
<MenuItem Header="_Save" Click="_onSaveRequested" />
<MenuItem Header="Save _as" Click="_onSaveASRequested" />
<Separator />
<MenuItem Header="_Quit" Click="_onExitRequested" />
</MenuItem>
<MenuItem Header="_Project">
<MenuItem Header="Edit _Labels"
Click="_onDisplayLabelEditWindowPressed" />
</MenuItem>
<MenuItem Header="_Card filter"
ToolTip.Tip="Open the menu for selecting labels that should be
displayed"
Click="_onFilterRequested"
/>
</Menu>
<DockPanel Background="#0079bf" LastChildFill="False">
<Button Click="_onNewColumnButtonClicked" Background="White"
DockPanel.Dock="Top">Add new column</Button>
<ScrollViewer Width="{Binding $parent[Window].Width}"
HorizontalScrollBarVisibility="Visible">
<StackPanel x:Name="ColumnPanel" Orientation="Horizontal"
HorizontalAlignment="Left"></StackPanel>
</ScrollViewer>
</DockPanel>
</DockPanel>
</Grid>
</Window>
Листинг 5. Код файла App.xaml
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="TodoApp.App">
<Application.Styles>
<FluentTheme Mode="Light"/>
</Application.Styles>
</Application>
Листнг 6. TagFilterDialog.axaml.cs
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
namespace TodoApp
{
public partial class TagFilterDialog : Window
{
public ObservableCollection<LabelCheckButton> Labels { get; } = new
ObservableCollection<LabelCheckButton>();
private List<TagInfo>? _displayedTags;
/// <summary>
/// Tags that user has selected to be displayed<para/>
/// Null if user chose to select all
/// </summary>
public List<TagInfo>? DisplayedTags => _displayedTags;
public TagFilterDialog()
{
InitializeComponent();
this.DataContext = this;
}
_displayedTags.Add(info);
}
else
{
_displayedTags.Remove(info);
}
}
Листинг 8. TagInfo.cs
using Avalonia.Media;
public TagInfo() { }
}
Листинг 9. ProjectInfo.cs
using System;
using System.Collections.Generic;
/// <summary>
/// Creates an empty project
/// </summary>
public ProjectInfo()
{
}
}
Листинг 10. CardInfo.cs
using System.Collections.Generic;
using System;
/// <summary>
/// Contains all of the information about card itself
/// </summary>
public class CardInfo
{
public string Name = string.Empty;
/// <summary>
/// Optional description of the card
/// </summary>
public string? Description;
public List<uint> Tags = new List<uint>();
public DateTimeOffset? DueDate;
public bool HasDueDate;
namespace TodoApp
{
public partial class ProjectLabelsEditWindow : Window
{
private List<TagInfo> _tags = new List<TagInfo>();
private ObservableCollection<ListBoxItem> _tagItems = new
ObservableCollection<ListBoxItem>();
private uint _currentTagId = 0;
public uint CurrentTagId => _currentTagId;
/// <summary>
/// All of the tags that this window worked with, including unchanged ones
/// </summary>
public List<TagInfo> Tags => _tags;
private int _selectedLabel = -1;
public ProjectLabelsEditWindow()
{
InitializeComponent();
this.DataContext = this;
}
/// <summary>
/// Currently selected tag, meant for binding with the UI<para/>
/// Setting this value will cause tag info to be updated
/// </summary>
public int LabelSelection
{
get => _selectedLabel;
set
{
_selectedLabel = value;
_displayTagEdit();
//_tags[value].Text = string.IsNullOrWhiteSpace(NameBox.Text) ? null :
NameBox.Text;
}
}
editDialog.Init(_tags[LabelSelection], true);
await editDialog.ShowDialog(this);
if (editDialog.WasRemoved)
{
_tags.RemoveAt(LabelSelection);
}
else if (editDialog.Info != null)
{
_tags[LabelSelection] = editDialog.Info;
}
Init(_tags, _currentTagId);
}
namespace TodoApp
{
/// <summary>
/// Window that allows changing properties for the tags<para/>
/// </summary>
public partial class LabelPropsEditWindow : Window
{
private TagInfo? _info;
/// <summary>
/// Current value of the info including the edits<para/>
/// If value is null then it means that no edits should be written
/// </summary>
public TagInfo? Info
{
get => _info;
set => _info = value;
}
namespace TodoApp
{
public partial class LabelCheckButton : UserControl
{
public delegate void TagToggledEventHandler(TagInfo info, bool enabled);
TagInfo _info;
namespace TodoApp
{
public partial class CardEditWindow : Window
{
public CardEditWindow()
{
InitializeComponent();
this.DataContext = this;
}
private List<LabelCheckButton> _tagButtons = new
List<LabelCheckButton>();
}
}
/// <summary>
/// Regenerates tag list based on new tags
/// </summary>
/// <param name="tags">New tags</param>
public void UpdateTags(List<TagInfo> tags)
{
TagListPanel.Children.Clear();
_tagButtons.Clear();
foreach (TagInfo tag in tags)
{
LabelCheckButton btn = new LabelCheckButton();
TagListPanel.Children.Add(btn);
_tagButtons.Add(btn);
btn.Enabled = CardInfo?.Tags.Contains(tag.Id) ?? false;
btn.OnTagToggled += _onTagToggled;
btn.Init(tag);
}
}
private void _onTagToggled(TagInfo info, bool enabled)
{
if (enabled)
{
_cardInfo?.Tags.Add(info.Id);
}
else
{
_cardInfo?.Tags.Remove(info.Id);
}
}