/Application/GUI/Controls/ViewDetails.xaml.cs
C# | 2107 lines | 1265 code | 281 blank | 561 comment | 282 complexity | c1422cf63faa1f0cbaab6c72063f5805 MD5 | raw file
1/** 2 * ViewDetails.cs 3 * 4 * A modified ListView that looks extremely sexy. 5 * 6 * Features: 7 * Drag-n-Drop 8 * Column sort 9 * Column toggle 10 * Icons 11 * Strikethrough 12 * Active items (graphical highlight) 13 * Explorer-like look 14 * 15 * It also sports a convenient storage structure used to 16 * import and export of the configuration in order to allow 17 * easy saving of the configuration between different sessions. 18 * 19 * * * * * * * * * 20 * 21 * This code is part of the Stoffi Music Player Project. 22 * Visit our website at: stoffiplayer.com 23 * 24 * This program is free software; you can redistribute it and/or 25 * modify it under the terms of the GNU General Public License 26 * as published by the Free Software Foundation; either version 27 * 3 of the License, or (at your option) any later version. 28 * 29 * See stoffiplayer.com/license for more information. 30 **/ 31 32using System; 33using System.Collections; 34using System.Collections.Generic; 35using System.Collections.Specialized; 36using System.Collections.ObjectModel; 37using System.ComponentModel; 38using System.Linq; 39using System.Text; 40using System.Windows; 41using System.Windows.Controls; 42using System.Windows.Data; 43using System.Windows.Documents; 44using System.Windows.Input; 45using System.Windows.Media; 46using System.Windows.Media.Imaging; 47using System.Windows.Navigation; 48using System.Windows.Shapes; 49using System.Windows.Threading; 50using System.IO; 51 52namespace Stoffi 53{ 54 /// <summary> 55 /// A modified ListView that looks extremely sexy. 56 /// 57 /// Features: 58 /// Drag-n-Drop 59 /// Column sort 60 /// Column toggle 61 /// Icons 62 /// Strikethrough 63 /// Active items (graphical highlight) 64 /// Explorer-like look 65 /// 66 /// It also sports a convenient storage structure used to 67 /// import and export of the configuration in order to allow 68 /// easy saving of the configuration between different sessions. 69 /// </summary> 70 public partial class ViewDetails : ListView 71 { 72 #region Members 73 74 private ContextMenu headerMenu = new ContextMenu(); 75 private ContextMenu itemMenu = new ContextMenu(); 76 private Hashtable columns = new Hashtable(); 77 private Hashtable columnTable = new Hashtable(); 78 private Hashtable headerMenuTable = new Hashtable(); 79 private GridView columnGrid = new GridView(); 80 private GridViewColumnHeader currentSortColumn = null; 81 private ListSortDirection currentSortDirection = ListSortDirection.Ascending; 82 private ViewDetailsDropTarget dropTarget; 83 private ViewDetailsColumn numberColumn = new ViewDetailsColumn(); 84 private double lastScroll = 0; 85 private bool hasNumber = false; 86 private bool isNumberVisible = false; 87 private int numberIndex = 0; 88 private bool lockSortOnNumber = false; 89 private ViewDetailsConfig config = null; 90 private string filter = ""; 91 private int focusItemIndex = -1; 92 private bool useAeroHeaders = true; 93 94 #endregion Members 95 96 #region Properties 97 98 /// <summary> 99 /// Gets or sets whether the list can be sorted by clicking. 100 /// </summary> 101 public bool IsClickSortable { get; set; } 102 103 /// <summary> 104 /// Gets or sets whether the list can be sorted by dragging. 105 /// Will be ignored if LockSortOnNumber is turned on. 106 /// </summary> 107 public bool IsDragSortable { get; set; } 108 109 /// <summary> 110 /// Gets or sets whether files can be dropped onto the list. 111 /// </summary> 112 public bool AcceptFileDrops { get; set; } 113 114 /// <summary> 115 /// Gets or sets whether to use icons or not (requires an Icon property on the sources). 116 /// </summary> 117 public bool UseIcons { get; set; } 118 119 /// <summary> 120 /// Gets or sets whether the number column is visible (requires the HasNumber property). 121 /// </summary> 122 public bool IsNumberVisible 123 { 124 get { return isNumberVisible; } 125 set 126 { 127 if (HasNumber) 128 { 129 ToggleColumn("#", value); 130 isNumberVisible = value; 131 if (config != null) 132 { 133 config.IsNumberVisible = value; 134 config.NumberColumn.IsVisible = value; 135 } 136 } 137 } 138 } 139 140 /// <summary> 141 /// Gets or sets the position of the number column. 142 /// </summary> 143 public int NumberIndex 144 { 145 get { return numberIndex; } 146 set 147 { 148 if (HasNumber && numberIndex >= 0) 149 { 150 GridViewColumn gvc = columnGrid.Columns[numberIndex]; 151 columnGrid.Columns.RemoveAt(numberIndex); 152 MenuItem mi = (MenuItem)headerMenu.Items[numberIndex]; 153 headerMenu.Items.RemoveAt(numberIndex); 154 155 if (value >= 0) 156 { 157 headerMenu.Items.Insert(value, mi); 158 columnGrid.Columns.Insert(value, gvc); 159 } 160 } 161 numberIndex = value; 162 if (config != null) 163 config.NumberIndex = value; 164 } 165 } 166 167 /// <summary> 168 /// Gets or sets whether to use a number column (requires a Number property on the sources). 169 /// </summary> 170 public bool HasNumber 171 { 172 get { return hasNumber; } 173 set 174 { 175 numberColumn.IsVisible = value; 176 numberColumn.IsAlwaysVisible = value && lockSortOnNumber; 177 hasNumber = value; 178 if (value) 179 AddColumn(numberColumn, NumberIndex, false); 180 else 181 RemoveColumn(numberColumn, false); 182 183 if (config != null) 184 { 185 config.HasNumber = value; 186 config.NumberColumn.IsVisible = value; 187 config.NumberColumn.IsAlwaysVisible = value && lockSortOnNumber; 188 } 189 } 190 } 191 192 /// <summary> 193 /// Gets or sets whether to only allow sorting on the number column. 194 /// Requires HasNumber and IsClickSortable. 195 /// </summary> 196 public bool LockSortOnNumber 197 { 198 get { return lockSortOnNumber; } 199 set 200 { 201 numberColumn.IsAlwaysVisible = value && hasNumber; 202 lockSortOnNumber = value; 203 if (value) 204 { 205 if (Items.SortDescriptions.Count > 0) 206 Items.SortDescriptions.Clear(); 207 Sort(numberColumn, ListSortDirection.Ascending); 208 } 209 if (config != null) 210 config.LockSortOnNumber = value; 211 } 212 } 213 214 /// <summary> 215 /// Gets or sets the configuration of the ViewDetails class. 216 /// This will erase all current configuration. 217 /// </summary> 218 public ViewDetailsConfig Config 219 { 220 get { return config; } 221 set 222 { 223 if (value != null) 224 { 225 // clear current columns 226 columns.Clear(); 227 columnGrid.Columns.Clear(); 228 columnTable.Clear(); 229 headerMenuTable.Clear(); 230 headerMenu.Items.Clear(); 231 232 // copy configuration 233 numberColumn = value.NumberColumn; 234 IsClickSortable = value.IsClickSortable; 235 IsDragSortable = value.IsDragSortable; 236 UseIcons = value.UseIcons; 237 Filter = value.Filter; 238 AcceptFileDrops = value.AcceptFileDrops; 239 SelectIndices(value.SelectedIndices); 240 241 value.Columns.CollectionChanged += new NotifyCollectionChangedEventHandler(Columns_CollectionChanged); 242 243 // add columns 244 foreach (ViewDetailsColumn vdc in value.Columns) 245 AddColumn(vdc, -1, false); 246 247 NumberIndex = value.NumberIndex; 248 HasNumber = value.HasNumber; 249 IsNumberVisible = value.IsNumberVisible; 250 LockSortOnNumber = value.LockSortOnNumber; 251 252 // apply sorting 253 if (value.Sorts != null) 254 { 255 foreach (string sort in value.Sorts) 256 { 257 ListSortDirection dir = sort.Substring(0, 3) == "asc" ? ListSortDirection.Ascending : ListSortDirection.Descending; 258 string name = sort.Substring(4); 259 if (name == "Number") name = "#"; 260 Sort(columns[name] as ViewDetailsColumn, dir); 261 } 262 } 263 264 config = value; 265 config.PropertyChanged += new PropertyChangedEventHandler(Config_PropertyChanged); 266 } 267 } 268 } 269 270 /// <summary> 271 /// Sets the method that will be used to determine whether a specific item matches 272 /// a specific string or not. 273 /// </summary> 274 public ViewDetailsSearchDelegate FilterMatch { get; set; } 275 276 /// <summary> 277 /// Gets or sets the string that is used to filter items. 278 /// </summary> 279 public string Filter 280 { 281 get { return filter; } 282 set 283 { 284 filter = value; 285 286 if (value == "") 287 Items.Filter = null; 288 289 else if (FilterMatch != null) 290 { 291 Items.Filter = delegate(object item) 292 { 293 return FilterMatch((ViewDetailsItemData)item, value); 294 }; 295 } 296 297 if (config != null && config.Filter != value) 298 config.Filter = value; 299 } 300 } 301 302 /// <summary> 303 /// Gets or sets whether or not to use Aero styled headers 304 /// </summary> 305 public bool UseAeroHeaders 306 { 307 get { return useAeroHeaders; } 308 set 309 { 310 useAeroHeaders = value; 311 columnGrid.ColumnHeaderContainerStyle = value ? (Style)FindResource("AeroHeaderStyle") : null; 312 } 313 } 314 315 #endregion PropertiesWindow 316 317 #region Constructor 318 319 /// <summary> 320 /// Creates an instance of the ViewDetails class. 321 /// </summary> 322 public ViewDetails() 323 { 324 U.L(LogLevel.Debug, "VIEW DETAILS", "Initialize"); 325 InitializeComponent(); 326 U.L(LogLevel.Debug, "VIEW DETAILS", "Initialized"); 327 328 // create column headers 329 UseAeroHeaders = System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName != ""; 330 columnGrid.ColumnHeaderContextMenu = headerMenu; 331 View = columnGrid; 332 333 numberColumn.Alignment = System.Windows.HorizontalAlignment.Right; 334 numberColumn.Binding = "Number"; 335 numberColumn.IsVisible = false; 336 numberColumn.SortField = "Number"; 337 numberColumn.Text = "#"; 338 numberColumn.Name = "#"; 339 numberColumn.Width = 60; 340 numberColumn.IsSortable = true; 341 342 IsClickSortable = true; 343 IsDragSortable = true; 344 AllowDrop = true; 345 UseIcons = true; 346 HasNumber = false; 347 LockSortOnNumber = false; 348 FilterMatch = null; 349 AcceptFileDrops = true; 350 351 columnGrid.Columns.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Columns_CollectionChanged); 352 353 if (System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName != "") 354 ItemContainerStyle = (Style)TryFindResource("AeroRowStyle"); 355 else 356 ItemContainerStyle = (Style)TryFindResource("ClassicRowStyle"); 357 } 358 359 #endregion Constructor 360 361 #region Methods 362 363 #region Public 364 365 /// <summary> 366 /// Adds a column to the list 367 /// </summary> 368 /// <param name="column">The column to be added</param> 369 /// <param name="index">The index to insert at (-1 means last)</param> 370 /// <param name="addToConfig">Whether the column should be added to the config</param> 371 public void AddColumn(ViewDetailsColumn column, int index = -1, bool addToConfig = true) 372 { 373 // create header 374 GridViewColumnHeader gvch = new GridViewColumnHeader(); 375 gvch.Tag = column.Binding; 376 gvch.Content = column.Text; 377 gvch.HorizontalAlignment = column.Alignment; 378 gvch.Click += Column_Clicked; 379 gvch.SizeChanged += Column_SizeChanged; 380 gvch.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch; 381 382 if (System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName != "") 383 { 384 gvch.SetResourceReference(GridViewColumnHeader.TemplateProperty, "AeroHeaderTemplate"); 385 gvch.ContentTemplate = (DataTemplate)FindResource("HeaderTemplate"); 386 } 387 388 // create column 389 GridViewColumn gvc = new GridViewColumn(); 390 gvc.Header = gvch; 391 gvc.CellTemplate = CreateDataTemplate(column.Binding, column.Alignment, false, (UseIcons && columnGrid.Columns.Count == 0)); 392 gvc.Width = column.Width; 393 394 // create header menu item 395 MenuItem mi = new MenuItem(); 396 mi.Header = column.Text; 397 mi.IsCheckable = !column.IsAlwaysVisible; 398 mi.Click += new RoutedEventHandler(HeaderMenu_Click); 399 mi.IsChecked = column.IsVisible; 400 mi.Tag = column.Name; 401 402 columns.Add(column.Name, column); 403 columnTable.Add(column.Name, gvc); 404 headerMenuTable.Add(column.Name, mi); 405 406 if (index >= 0) 407 headerMenu.Items.Insert(index, mi); 408 else 409 headerMenu.Items.Add(mi); 410 411 if (column.IsVisible && index >= 0) 412 columnGrid.Columns.Insert(index, gvc); 413 else if (column.IsVisible) 414 columnGrid.Columns.Add(gvc); 415 416 if (config != null && addToConfig) 417 { 418 if (config.Columns == null) 419 config.Columns = new ObservableCollection<ViewDetailsColumn>(); 420 config.Columns.Add(column); 421 } 422 423 RefreshHeaderMenu(); 424 425 column.PropertyChanged += new PropertyChangedEventHandler(ConfigColumn_PropertyChanged); 426 } 427 428 /// <summary> 429 /// Adds a column to the list 430 /// </summary> 431 /// <param name="name">The name of the column</param> 432 /// <param name="text">The text to be displayed</param> 433 /// <param name="binding">The value to bind the column to</param> 434 /// <param name="sortField">The value to sort on when clicked</param> 435 /// <param name="width">The width of the column</param> 436 /// <param name="isSortable">Whether the column is sortable</param> 437 /// <param name="isAlwaysVisible">Whether the column is always visible</param> 438 /// <param name="isVisible">Whether the column is visible (only effective if isAlwaysVisible is false)</param> 439 public void AddColumn(string name, string text, string binding, string sortField, double width, bool isSortable = true, bool isAlwaysVisible = false, bool isVisible = true) 440 { 441 ViewDetailsColumn vdc = new ViewDetailsColumn(); 442 vdc.Name = name; 443 vdc.Text = text; 444 vdc.Binding = binding; 445 vdc.SortField = sortField; 446 vdc.IsAlwaysVisible = isAlwaysVisible; 447 vdc.Width = width; 448 vdc.IsVisible = (isVisible || isAlwaysVisible); 449 vdc.IsSortable = isSortable; 450 AddColumn(vdc); 451 } 452 453 /// <summary> 454 /// Adds a column to the list 455 /// </summary> 456 /// <param name="name">The name of the column</param> 457 /// <param name="text">The text to be displayed</param> 458 /// <param name="binding">The value to bind the column to</param> 459 /// <param name="width">The width of the column</param> 460 /// <param name="isSortable">Whether the column is sortable</param> 461 /// <param name="isAlwaysVisible">Whether the column is always visible</param> 462 /// <param name="isVisible">Whether the column is visible (only effective if isAlwaysVisible is false)</param> 463 public void AddColumn(string name, string text, string binding, double width, bool isSortable = true, bool isAlwaysVisible = false, bool isVisible = true) 464 { 465 AddColumn(name, text, binding, binding, width, isSortable, isAlwaysVisible, isVisible); 466 } 467 468 /// <summary> 469 /// Removes a column from the list 470 /// </summary> 471 /// <param name="column">The column to remove</param> 472 /// <param name="removeFromConfig">Whether the column should be removed from the config</param> 473 public void RemoveColumn(ViewDetailsColumn column, bool removeFromConfig = true) 474 { 475 if (headerMenuTable.ContainsKey(column.Text)) 476 { 477 MenuItem mi = (MenuItem)headerMenuTable[column.Text]; 478 headerMenu.Items.Remove(mi); 479 headerMenuTable.Remove(column.Text); 480 } 481 482 if (columnTable.ContainsKey(column.Text)) 483 { 484 GridViewColumn gvc = (GridViewColumn)columnTable[column.Text]; 485 columnGrid.Columns.Remove(gvc); 486 columnTable.Remove(column.Text); 487 } 488 489 if (columns.ContainsKey(column.Text)) 490 { 491 ViewDetailsColumn vdc = (ViewDetailsColumn)columns[column.Text]; 492 if (config != null && config.Columns != null && removeFromConfig) 493 config.Columns.Remove(vdc); 494 columns.Remove(column.Text); 495 } 496 } 497 498 /// <summary> 499 /// Selects a given list of indices of items 500 /// </summary> 501 /// <param name="indices">The indices of the items to select</param> 502 public void SelectIndices(List<int> indices) 503 { 504 List<object> itemsToSelect = new List<object>(); 505 foreach (int index in indices) 506 if (0 <= index && index < Items.Count) 507 itemsToSelect.Add(Items[index]); 508 SetSelectedItems(itemsToSelect); 509 if (itemsToSelect.Count > 0) 510 ScrollIntoView(itemsToSelect.First<object>()); 511 512 } 513 514 /// <summary> 515 /// Selects an item, gives it focus and scrolls it into view 516 /// </summary> 517 /// <param name="item">The item inside the list</param> 518 public void SelectItem(ViewDetailsItemData item) 519 { 520 if (item == null) 521 return; 522 523 if (Items.Contains(item)) 524 SelectedItem = item; 525 526 else 527 { 528 foreach (ViewDetailsItemData i in Items) 529 { 530 if (i == item) 531 { 532 SelectedItem = i; 533 break; 534 } 535 } 536 } 537 538 focusItemIndex = SelectedIndex; 539 ItemContainerGenerator.ItemsChanged += FocusItem; // in case we have to wait... 540 FocusItem(null, null); 541 } 542 543 /// <summary> 544 /// Removes the current sorting 545 /// </summary> 546 /// <param name="keepPositions">Whether or not to keep all items at their current position</param> 547 public void ClearSort(bool keepPositions = false) 548 { 549 if (Items.SortDescriptions.Count > 0) 550 { 551 // move items in the source so they are in the same 552 // order as the gui items (which are order by sort conditions) 553 ObservableCollection<object> items = ItemsSource as ObservableCollection<object>; 554 for (int j = 0; j < Items.Count; j++) 555 DispatchMoveItem(Items[j], j); 556 557 // remove sort indicators 558 if (!LockSortOnNumber) 559 { 560 Items.SortDescriptions.Clear(); 561 config.Sorts.Clear(); 562 foreach (DictionaryEntry c in columnTable) 563 { 564 ViewDetailsColumn vdc = (ViewDetailsColumn)columns[c.Key]; 565 GridViewColumn gvc = (GridViewColumn)c.Value; 566 gvc.CellTemplate = CreateDataTemplate(vdc.Binding, vdc.Alignment, false, (UseIcons && columnGrid.Columns.IndexOf(gvc) == 0)); 567 ((GridViewColumnHeader)((GridViewColumn)c.Value).Header).ContentTemplate = (DataTemplate)FindResource("HeaderTemplate"); 568 } 569 currentSortColumn = null; 570 if (SelectedItems.Count > 0) 571 ScrollIntoView(SelectedItems[0]); 572 } 573 } 574 } 575 576 /// <summary> 577 /// Gets an item source at a given index in the graphical list 578 /// </summary> 579 /// <param name="index">The graphical index of the item</param> 580 /// <returns>The item source</returns> 581 public ViewDetailsItemData GetItemAt(int index) 582 { 583 return Items[index] as ViewDetailsItemData; 584 } 585 586 /// <summary> 587 /// Returns the graphical index of an item source 588 /// </summary> 589 /// <param name="logicalObject">The item source</param> 590 /// <returns>The graphical index of <paramref name="logicalObject"/></returns> 591 public int IndexOf(ViewDetailsItemData logicalObject) 592 { 593 return Items.IndexOf(logicalObject); 594 } 595 596 /// <summary> 597 /// Request the focus to be set on the specified list view item 598 /// </summary> 599 /// <param name="itemIndex">index of item to receive the initial focus</param> 600 public void FocusAndSelectItem(int itemIndex) 601 { 602 Dispatcher.BeginInvoke(new FocusAndSelectItemDelegate(TryFocusAndSelectItem), 603 DispatcherPriority.ApplicationIdle, itemIndex); 604 } 605 606 /// <summary> 607 /// Places focus on the list and the selected items in the list. 608 /// </summary> 609 public void Focus() 610 { 611 base.Focus(); 612 ListViewItem lvi = ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as ListViewItem; 613 if (lvi != null) 614 { 615 this.ScrollIntoView(lvi); 616 lvi.IsSelected = true; 617 Keyboard.ClearFocus(); 618 Keyboard.Focus(lvi); 619 } 620 } 621 622 #endregion Public 623 624 #region Private 625 626 /// <summary> 627 /// Make sure a list view item is within the visible area of the list view 628 /// and then select and set focus to it. 629 /// </summary> 630 /// <param name="itemIndex">index of item</param> 631 private void TryFocusAndSelectItem(int itemIndex) 632 { 633 ListViewItem lvi = ItemContainerGenerator.ContainerFromIndex(itemIndex) as ListViewItem; 634 if (lvi != null) 635 { 636 this.ScrollIntoView(lvi); 637 lvi.IsSelected = true; 638 Keyboard.ClearFocus(); 639 Keyboard.Focus(lvi); 640 } 641 } 642 643 /// <summary> 644 /// Toggles a columns visibility 645 /// </summary> 646 /// <param name="name">The name of the column</param> 647 /// <param name="visible">Whether the column should be visible</param> 648 private void ToggleColumn(string name, bool visible) 649 { 650 MenuItem item = headerMenuTable[name] as MenuItem; 651 GridViewColumn column = columnTable[name] as GridViewColumn; 652 ViewDetailsColumn vdc = columns[name] as ViewDetailsColumn; 653 item.IsChecked = visible; 654 if (visible) 655 { 656 // calculate the position to insert the column based on the position in the context menu 657 int pos = 0; 658 foreach (MenuItem mi in columnGrid.ColumnHeaderContextMenu.Items) 659 { 660 ViewDetailsColumn c = columns[mi.Tag] as ViewDetailsColumn; 661 if (c == vdc) 662 break; 663 if (c.IsVisible) // only count visible columns 664 pos++; 665 } 666 if (columnGrid.Columns.Contains(column)) 667 columnGrid.Columns.Remove(column); 668 columnGrid.Columns.Insert(pos, column); 669 } 670 else 671 columnGrid.Columns.Remove(column); 672 673 int i = 0; 674 foreach (GridViewColumn gvc in columnGrid.Columns) 675 { 676 string n = (string)((GridViewColumnHeader)gvc.Header).Content; 677 ViewDetailsColumn col = FindColumn(n); 678 gvc.CellTemplate = CreateDataTemplate(col.Binding, col.Alignment, (currentSortColumn == (GridViewColumnHeader)gvc.Header), i < 1); 679 i++; 680 } 681 682 if (HasNumber) 683 { 684 numberIndex = columnGrid.Columns.IndexOf((GridViewColumn)columnTable["#"]); 685 if (config != null) 686 config.NumberIndex = numberIndex; 687 } 688 689 vdc.IsVisible = item.IsChecked; 690 RefreshHeaderMenu(); 691 } 692 693 /// <summary> 694 /// Goes through all items in the header menu, if only 695 /// one column is visible it is disabled, preventing 696 /// the user from hiding all columns. 697 /// </summary> 698 private void RefreshHeaderMenu() 699 { 700 // look for a single visible column (if there is any) 701 ViewDetailsColumn onlyVisible = numberColumn != null && numberColumn.IsVisible ? numberColumn : null; 702 foreach (ViewDetailsColumn column in columns.Values) 703 if (column.IsVisible && onlyVisible == null) 704 onlyVisible = column; 705 else if (column.IsVisible) 706 { 707 onlyVisible = null; 708 break; 709 } 710 711 // by default allow any column to be toggled 712 foreach (MenuItem mi in headerMenu.Items) 713 mi.IsEnabled = true; 714 715 // if there's only one single column visible we need 716 // to disable the ability to hide it 717 if (onlyVisible != null && headerMenuTable.ContainsKey(onlyVisible.Name)) 718 { 719 MenuItem mi = headerMenuTable[onlyVisible.Name] as MenuItem; 720 mi.IsEnabled = false; 721 } 722 } 723 724 /// <summary> 725 /// Gives an item focus and scrolls it into view 726 /// </summary> 727 /// <param name="sender">The sender of the event</param> 728 /// <param name="e">The event data</param> 729 private void FocusItem(object sender, EventArgs e) 730 { 731 if (ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated) 732 { 733 ItemContainerGenerator.StatusChanged -= FocusItem; 734 Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, new Action(delegate 735 { 736 if (focusItemIndex > Items.Count || focusItemIndex < 0) return; 737 ScrollIntoView(Items.GetItemAt(focusItemIndex)); 738 ListBoxItem item = ItemContainerGenerator.ContainerFromIndex(focusItemIndex) as ListBoxItem; 739 if (item != null) 740 { 741 item.Focus(); 742 focusItemIndex = -1; 743 } 744 })); 745 } 746 } 747 748 /// <summary> 749 /// Sorts the list 750 /// </summary> 751 /// <param name="vdc">The column to sort on</param> 752 /// <param name="direction">The sort direction</param> 753 private void Sort(ViewDetailsColumn vdc, ListSortDirection direction) 754 { 755 // try to find the corresponding column header 756 GridViewColumn column = (GridViewColumn)columnTable[vdc.Name]; 757 GridViewColumnHeader header = (GridViewColumnHeader)column.Header; 758 759 foreach (DictionaryEntry c in columnTable) 760 { 761 string key = c.Key as string; 762 GridViewColumn gvc = c.Value as GridViewColumn; 763 GridViewColumnHeader gvch = gvc.Header as GridViewColumnHeader; 764 ViewDetailsColumn vdc_ = (ViewDetailsColumn)columns[key]; 765 bool active = (key == vdc.Name); 766 bool rightMost = columnGrid.Columns.IndexOf(gvc) == 0; 767 gvc.CellTemplate = CreateDataTemplate(vdc_.Binding, vdc_.Alignment, active, rightMost); 768 string headerTemplate = "HeaderTemplate" + (active ? direction == ListSortDirection.Ascending ? "ArrowUp" : "ArrowDown" : ""); 769 if (System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName != "") 770 gvch.ContentTemplate = (DataTemplate)TryFindResource(headerTemplate); 771 } 772 773 // apply sorting 774 Items.SortDescriptions.Insert(0, new SortDescription(vdc.SortField, direction)); 775 776 currentSortColumn = header; 777 currentSortDirection = direction; 778 779 if (SelectedItems.Count > 0) 780 ScrollIntoView(SelectedItems[0]); 781 } 782 783 /// <summary> 784 /// Uses certain parameters to create a DataTemplate which can be used as a CellTemplate for a 785 /// specific column in the list. 786 /// </summary> 787 /// <param name="binding">The value to bind to</param> 788 /// <param name="alignment">Horizontal alignment of the content</param> 789 /// <param name="active">Whether the column is active or not</param> 790 /// <param name="rightMost">Whether the column is the right most</param> 791 /// <returns>DataTemplate to use as a CellTemplate for a column</returns> 792 private DataTemplate CreateDataTemplate(string binding, HorizontalAlignment alignment, bool active, bool rightMost) 793 { 794 FrameworkElementFactory dp = new FrameworkElementFactory(typeof(DockPanel)); 795 dp.SetValue(DockPanel.LastChildFillProperty, true); 796 797 798 if (rightMost && UseIcons) 799 { 800 double iconSize = 16.0; 801 FrameworkElementFactory icon = new FrameworkElementFactory(typeof(Image)); 802 icon.SetBinding(Image.SourceProperty, new Binding("Icon") { Converter = new StringToBitmapImageConverter() }); 803 icon.SetValue(Image.WidthProperty, iconSize); 804 icon.SetValue(Image.HeightProperty, iconSize); 805 icon.SetValue(Image.MarginProperty, new Thickness(15, 0, 5, 0)); 806 icon.SetValue(Grid.ColumnProperty, 0); 807 dp.AppendChild(icon); 808 } 809 810 FrameworkElementFactory tb = new FrameworkElementFactory(typeof(TextBlock)); 811 tb.SetBinding(TextBlock.TextProperty, new Binding(binding)); 812 tb.SetValue(TextBlock.TextTrimmingProperty, TextTrimming.CharacterEllipsis); 813 tb.SetValue(TextBlock.HorizontalAlignmentProperty, alignment); 814 tb.SetValue(Grid.ColumnProperty, 1); 815 if (rightMost && !UseIcons) 816 tb.SetValue(TextBlock.MarginProperty, new Thickness(15, 0, 5, 0)); 817 818 819 if (System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName != "") 820 tb.SetValue(TextBlock.ForegroundProperty, (active ? Brushes.Black : Brushes.Gray)); 821 822 DataTemplate dt = new DataTemplate(); 823 824 dp.AppendChild(tb); 825 826 dt.VisualTree = dp; 827 return dt; 828 } 829 830 /// <summary> 831 /// Find the corresponding column configuration given the content of the column 832 /// </summary> 833 /// <param name="content">The displayed text on the column</param> 834 /// <returns>The column configuration for the column</returns> 835 private ViewDetailsColumn FindColumn(string content) 836 { 837 if (content == "#") 838 return numberColumn; 839 840 foreach (DictionaryEntry i in columns) 841 { 842 if (((ViewDetailsColumn)i.Value).Text == content) 843 return (ViewDetailsColumn)i.Value; 844 } 845 return null; 846 } 847 848 #endregion Private 849 850 #region Overrides 851 852 /// <summary> 853 /// Creates and return a ViewDetailsItem container. 854 /// </summary> 855 /// <returns>A ViewDetailsItem container</returns> 856 protected override DependencyObject GetContainerForItemOverride() 857 { 858 return new ViewDetailsItem(); 859 } 860 861 /// <summary> 862 /// Invoked when the context menu is opening 863 /// </summary> 864 /// <param name="e">The event data</param> 865 protected override void OnContextMenuOpening(ContextMenuEventArgs e) 866 { 867 // prevent the context menu from opening if the item under the mouse is not an item 868 ListViewItem lvi = ViewDetailsUtilities.TryFindParent<ListViewItem>((DependencyObject)e.OriginalSource); 869 GridViewColumnHeader gvch = ViewDetailsUtilities.TryFindParent<GridViewColumnHeader>((DependencyObject)e.OriginalSource); 870 if (lvi == null && gvch == null) 871 e.Handled = true; 872 else 873 { 874 base.OnContextMenuOpening(e); 875 } 876 } 877 878 /// <summary> 879 /// Invoked when the ItemsSource property is changed. 880 /// </summary> 881 /// <param name="oldValue">The old source</param> 882 /// <param name="newValue">The new source</param> 883 /// <remarks>Will enumerate all items and set Number</remarks> 884 protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) 885 { 886 // if all numbers are zero we fix the numbers 887 bool allZero = true; 888 foreach (ViewDetailsItemData item in newValue) 889 if (item.Number != 0) 890 { 891 allZero = false; 892 break; 893 } 894 int i = 1; 895 if (allZero) 896 foreach (ViewDetailsItemData item in newValue) 897 item.Number = i++; 898 899 base.OnItemsSourceChanged(oldValue, newValue); 900 } 901 902 /// <summary> 903 /// Invoked when the user double-clicks the list 904 /// </summary> 905 /// <param name="e">The event data</param> 906 protected override void OnMouseDoubleClick(MouseButtonEventArgs e) 907 { 908 // prevent the context menu from opening if the item under the mouse is not an item 909 ListViewItem lvi = ViewDetailsUtilities.TryFindParent<ListViewItem>((DependencyObject)e.OriginalSource); 910 if (lvi == null) 911 e.Handled = true; 912 else 913 base.OnMouseDoubleClick(e); 914 } 915 916 /// <summary> 917 /// Invoked when something is dropped on the list 918 /// </summary> 919 /// <param name="e">The event data</param> 920 protected override void OnDrop(DragEventArgs e) 921 { 922 if (!(e.Data.GetDataPresent(DataFormats.FileDrop) && AcceptFileDrops) && 923 !(e.Data.GetDataPresent(typeof(List<object>).FullName) && IsDragSortable)) 924 { 925 e.Effects = DragDropEffects.None; 926 return; 927 } 928 929 if (e.Data.GetDataPresent(DataFormats.FileDrop)) 930 { 931 string[] paths = e.Data.GetData(DataFormats.FileDrop, true) as string[]; 932 ListBoxItem lvi = ViewDetailsUtilities.TryFindFromPoint<ListBoxItem>(this, e.GetPosition(this)); 933 if (lvi != null) 934 { 935 int i = this.ItemContainerGenerator.IndexFromContainer(lvi); 936 if (e.GetPosition(lvi).Y > lvi.RenderSize.Height / 2) i++; 937 DispatchFilesDropped(paths, i); 938 } 939 else 940 DispatchFilesDropped(paths, Items.Count); 941 } 942 943 else if (e.Data.GetDataPresent(typeof(List<object>).FullName)) 944 { 945 ListBoxItem lvi = ViewDetailsUtilities.TryFindFromPoint<ListBoxItem>(this, e.GetPosition(this)); 946 if (lvi != null) 947 { 948 List<object> items = e.Data.GetData(typeof(List<object>).FullName) as List<object>; 949 int i = this.ItemContainerGenerator.IndexFromContainer(lvi); 950 if (e.GetPosition(lvi).Y > lvi.RenderSize.Height / 2) i++; 951 952 // items may be out of order so we sort them 953 List<int> indices = new List<int>(); 954 foreach (object t in items) // put all indices in a list 955 indices.Add(Items.IndexOf(t)); 956 indices.Sort(); // sort the list 957 items.Clear(); 958 foreach (int j in indices) // put back all items according to the sorted list 959 items.Add(Items.GetItemAt(j) as object); 960 961 // reorder source and remove GUI sorting 962 ClearSort(true); 963 964 foreach (object t in items) 965 { 966 int j = i; 967 if (Items.IndexOf(t) > i) j++; 968 DispatchMoveItem(t, i); 969 i = j; 970 } 971 972 // change number value if we have a number column 973 if (HasNumber) 974 { 975 foreach (ViewDetailsItemData o in Items) 976 { 977 o.Number = Items.IndexOf(o) + 1; 978 } 979 } 980 981 SetSelectedItems(items); 982 } 983 } 984 dropTarget.Visibility = System.Windows.Visibility.Collapsed; 985 } 986 987 /// <summary> 988 /// Invoked when an item is dragged over the list 989 /// </summary> 990 /// <param name="e">The event data</param> 991 protected override void OnDragOver(DragEventArgs e) 992 { 993 if (!(e.Data.GetDataPresent(DataFormats.FileDrop) && AcceptFileDrops) && 994 !(e.Data.GetDataPresent(typeof(List<object>).FullName) && IsDragSortable)) 995 { 996 e.Effects = DragDropEffects.None; 997 return; 998 } 999 1000 ListBoxItem lvi = ViewDetailsUtilities.TryFindFromPoint<ListBoxItem>(this, e.GetPosition(this)); 1001 if (lvi != null) 1002 { 1003 ScrollViewer sv = ViewDetailsUtilities.GetVisualChild<ScrollViewer>(this); 1004 if (sv != null && sv.ComputedVerticalScrollBarVisibility == System.Windows.Visibility.Visible) 1005 dropTarget.ScrollBar = true; 1006 else 1007 dropTarget.ScrollBar = false; 1008 1009 dropTarget.Visibility = System.Windows.Visibility.Visible; 1010 Point p = lvi.TranslatePoint(new Point(0, 0), this); 1011 if (e.GetPosition(this).Y < p.Y + (lvi.RenderSize.Height / 2)) 1012 dropTarget.Position = p.Y; 1013 else 1014 dropTarget.Position = p.Y + lvi.RenderSize.Height + 1; 1015 1016 double scrollMargin = 50.0; 1017 double scrollStep = 1; 1018 double scrollSpeed = 0.05; 1019 if (sv != null && sv.CanContentScroll && ((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds) - lastScroll > scrollSpeed) 1020 { 1021 if (e.GetPosition(this).Y > this.RenderSize.Height - scrollMargin) 1022 { 1023 sv.ScrollToVerticalOffset(sv.VerticalOffset + scrollStep); 1024 lastScroll = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; 1025 } 1026 1027 else if (e.GetPosition(this).Y < scrollMargin + 20.0) 1028 { 1029 sv.ScrollToVerticalOffset(sv.VerticalOffset - scrollStep); 1030 lastScroll = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; 1031 } 1032 } 1033 } 1034 else 1035 dropTarget.Visibility = System.Windows.Visibility.Collapsed; 1036 } 1037 1038 /// <summary> 1039 /// Invoked when an unhandled DragLeave attached event reaches an element 1040 /// in its route that is derived from this class 1041 /// </summary> 1042 /// <param name="e">The event data</param> 1043 protected override void OnDragLeave(DragEventArgs e) 1044 { 1045 base.OnDragLeave(e); 1046 Point p = e.GetPosition(this); 1047 double x = p.X / ActualWidth; 1048 double y = p.Y / ActualHeight; 1049 if (x < 0.1 || 0.9 < x || y < 0.1 || 0.9 < y) 1050 dropTarget.Visibility = System.Windows.Visibility.Collapsed; 1051 } 1052 1053 /// <summary> 1054 /// Responds to a list box selection change by raising a SelectionChanged event and 1055 /// saving the selection to the config structure is such as structure is set. 1056 /// </summary> 1057 /// <param name="e">The event data</param> 1058 protected override void OnSelectionChanged(SelectionChangedEventArgs e) 1059 { 1060 if (config != null) 1061 { 1062 config.SelectedIndices.Clear(); 1063 foreach (object o in SelectedItems) 1064 config.SelectedIndices.Add(Items.IndexOf(o)); 1065 } 1066 base.OnSelectionChanged(e); 1067 } 1068 1069 #endregion Overrides 1070 1071 #region Event handlers 1072 1073 /// <summary> 1074 /// Invoked when the source collection changes 1075 /// </summary> 1076 /// <param name="sender">The sender of the event</param> 1077 /// <param name="e">The event data</param> 1078 public void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 1079 { 1080 switch (e.Action) 1081 { 1082 case NotifyCollectionChangedAction.Remove: 1083 case NotifyCollectionChangedAction.Add: 1084 foreach (ViewDetailsItemData o in Items.SourceCollection) 1085 o.Number = Items.IndexOf(o) + 1; 1086 break; 1087 1088 default: 1089 case NotifyCollectionChangedAction.Reset: 1090 break; 1091 } 1092 } 1093 1094 /// <summary> 1095 /// Invoked when the list is initialized 1096 /// </summary> 1097 /// <param name="sender">The sender of the event</param> 1098 /// <param name="e">The event data</param> 1099 private void ListView_Loaded(object sender, RoutedEventArgs e) 1100 { 1101 dropTarget = new ViewDetailsDropTarget(this); 1102 dropTarget.Visibility = System.Windows.Visibility.Hidden; 1103 AdornerLayer al = AdornerLayer.GetAdornerLayer(this); 1104 if (al != null) 1105 al.Add(dropTarget); 1106 } 1107 1108 /// <summary> 1109 /// Invoked when a property of the config changes 1110 /// </summary> 1111 /// <param name="sender">The sender of the event</param> 1112 /// <param name="e">The event data</param> 1113 private void Config_PropertyChanged(object sender, PropertyChangedEventArgs e) 1114 { 1115 if (e.PropertyName == "Filter") 1116 Filter = config.Filter; 1117 } 1118 1119 /// <summary> 1120 /// Invoked when a menu item in the header context menu is clicked 1121 /// </summary> 1122 /// <param name="sender">The sender of the event</param> 1123 /// <param name="e">The event data</param> 1124 private void HeaderMenu_Click(object sender, RoutedEventArgs e) 1125 { 1126 MenuItem item = sender as MenuItem; 1127 String name = (string)item.Tag; 1128 1129 if (name == "#") 1130 IsNumberVisible = item.IsChecked; 1131 else 1132 ToggleColumn(name, item.IsChecked); 1133 1134 RefreshHeaderMenu(); 1135 } 1136 1137 /// <summary> 1138 /// Invoked when the size of a column is changed 1139 /// </summary> 1140 /// <param name="sender">The sender of the event</param> 1141 /// <param name="e">The event data</param> 1142 private void Column_SizeChanged(object sender, SizeChangedEventArgs e) 1143 { 1144 GridViewColumnHeader gvch = sender as GridViewColumnHeader; 1145 ViewDetailsColumn vdc = FindColumn((string)gvch.Content); 1146 if (vdc != null) 1147 vdc.Width = gvch.ActualWidth; 1148 } 1149 1150 /// <summary> 1151 /// Invoked when a column is clicked 1152 /// </summary> 1153 /// <param name="sender">The sender of the event</param> 1154 /// <param name="e">The event data</param> 1155 private void Column_Clicked(object sender, RoutedEventArgs e) 1156 { 1157 GridViewColumnHeader column = sender as GridViewColumnHeader; 1158 ListSortDirection direction; 1159 ViewDetailsColumn vdc = FindColumn((string)column.Content); 1160 if (vdc == null) return; 1161 1162 if (!IsClickSortable || !vdc.IsSortable || (vdc != numberColumn && LockSortOnNumber)) 1163 return; 1164 1165 // get direction 1166 if (column != currentSortColumn) direction = ListSortDirection.Ascending; 1167 else if (currentSortDirection == ListSortDirection.Ascending) direction = ListSortDirection.Descending; 1168 else direction = ListSortDirection.Ascending; 1169 1170 // apply sorting 1171 Sort(vdc, direction); 1172 1173 if (config != null) 1174 { 1175 if (config.Sorts == null) 1176 config.Sorts = new List<string>(); 1177 1178 // remove previous sorts on this column 1179 string str1 = "asc:" + vdc.Name; 1180 if (config.Sorts.Contains(str1)) 1181 config.Sorts.Remove(str1); 1182 string str2 = "dsc:" + vdc.Name; 1183 if (config.Sorts.Contains(str2)) 1184 config.Sorts.Remove(str2); 1185 1186 config.Sorts.Add((direction == ListSortDirection.Ascending ? "asc:" : "dsc:") + vdc.Name); 1187 1188 } 1189 } 1190 1191 /// <summary> 1192 /// Invoked when the configuration of columns changed 1193 /// </summary> 1194 /// <param name="sender">The sender of the event</param> 1195 /// <param name="e">The event data</param> 1196 private void ConfigColumns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 1197 { 1198 // TODO: add and remove new columns 1199 } 1200 1201 /// <summary> 1202 /// Invoked when the property of a column changes 1203 /// </summary> 1204 /// <param name="sender">The sender of the event</param> 1205 /// <param name="e">The event data</param> 1206 private void ConfigColumn_PropertyChanged(object sender, PropertyChangedEventArgs e) 1207 { 1208 ViewDetailsColumn vdc = sender as ViewDetailsColumn; 1209 1210 GridViewColumn gvc = columnTable[vdc.Name] as GridViewColumn; 1211 if (gvc == null) return; 1212 1213 // rename headers and menu items 1214 if (e.PropertyName == "Text") 1215 { 1216 GridViewColumnHeader gvch = gvc.Header as GridViewColumnHeader; 1217 gvch.Content = vdc.Text; 1218 1219 MenuItem mi = headerMenuTable[vdc.Name] as MenuItem; 1220 mi.Header = vdc.Text; 1221 } 1222 1223 // TODO: implement other properties 1224 } 1225 1226 /// <summary> 1227 /// Invoked when the columns are reordered 1228 /// </summary> 1229 /// <param name="sender">The sender of the event</param> 1230 /// <param name="e">The event data</param> 1231 private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 1232 { 1233 if (e.Action == NotifyCollectionChangedAction.Move) 1234 { 1235 // move the icon to the far left 1236 if ((e.OldStartingIndex == 0 || e.NewStartingIndex == 0) && UseIcons) 1237 { 1238 int oldIndex = e.OldStartingIndex == 0 ? e.NewStartingIndex : 1; 1239 GridViewColumn oldFirst = columnGrid.Columns[oldIndex]; 1240 GridViewColumn newFirst = columnGrid.Columns[0]; 1241 1242 ViewDetailsColumn oldVdc = FindColumn((string)((GridViewColumnHeader)oldFirst.Header).Content); 1243 ViewDetailsColumn newVdc = FindColumn((string)((GridViewColumnHeader)newFirst.Header).Content); 1244 1245 bool oldIsActive = IsClickSortable && ((GridViewColumnHeader)oldFirst.Header) == currentSortColumn; 1246 bool newIsActive = IsClickSortable && ((GridViewColumnHeader)newFirst.Header) == currentSortColumn; 1247 1248 oldFirst.CellTemplate = CreateDataTemplate(oldVdc.Binding, oldVdc.Alignment, oldIsActive, false); 1249 newFirst.CellTemplate = CreateDataTemplate(newVdc.Binding, newVdc.Alignment, newIsActive, true); 1250 } 1251 1252 // we may need to rearrange the column order in the config as well 1253 if (config != null) 1254 { 1255 // since we may have a number column in the menu but not in the column 1256 // list we have to compensate the indices accordingly 1257 bool wasNumber = false; 1258 int newAdjust = 0; 1259 int oldAdjust = 0; 1260 if (HasNumber) 1261 { 1262 if (NumberIndex == e.OldStartingIndex) 1263 { 1264 numberIndex = e.NewStartingIndex; 1265 config.NumberIndex = numberIndex; 1266 wasNumber = true; 1267 } 1268 else // adjust indices 1269 { 1270 if (NumberIndex < e.OldStartingIndex && IsNumberVisible) oldAdjust = 1; 1271 1272 // adjust number index 1273 if (e.OldStartingIndex <= NumberIndex && NumberIndex < e.NewStartingIndex && IsNumberVisible) numberIndex--; 1274 else if (e.NewStartingIndex <= NumberIndex && NumberIndex < e.OldStartingIndex && IsNumberVisible) numberIndex++; 1275 1276 if (NumberIndex <= e.NewStartingIndex && IsNumberVisible) newAdjust = 1; 1277 } 1278 } 1279 1280 if (!wasNumber) // the number column is special, not in the list we rearrange 1281 { 1282 ViewDetailsColumn vdc = config.Columns[e.OldStartingIndex - oldAdjust]; 1283 config.Columns.Remove(vdc); 1284 config.Columns.Insert(e.NewStartingIndex - newAdjust, vdc); 1285 } 1286 } 1287 1288 MenuItem mi = headerMenu.Items[e.OldStartingIndex] as MenuItem; 1289 headerMenu.Items.Remove(mi); 1290 headerMenu.Items.Insert(e.NewStartingIndex, mi); 1291 } 1292 } 1293 1294 #endregion Event handlers 1295 1296 #region Dispatchers 1297 1298 /// <summary> 1299 /// The dispatcher of the <see cref="ViewDetails.FilesDropped"/> event 1300 /// </summary> 1301 /// <param name="paths">The track that was either added or removed</param> 1302 /// <param name="position">The index where the files where dropped</param> 1303 private void DispatchFilesDropped(string[] paths, int position) 1304 { 1305 if (FilesDropped != null) 1306 FilesDropped(this, new FileDropEventArgs(paths, position)); 1307 } 1308 1309 /// <summary> 1310 /// The dispatcher of the <see cref="ViewDetails.MoveItem"/> event 1311 /// </summary> 1312 /// <param name="item">The item that is to be moved</param> 1313 /// <param name="position">The index that the item is to be moved to</param> 1314 private void DispatchMoveItem(object item, int position) 1315 { 1316 if (MoveItem != null) 1317 MoveItem(this, new MoveItemEventArgs(item, position)); 1318 } 1319 1320 #endregion 1321 1322 #endregion Methods 1323 1324 #region Events 1325 1326 /// <summary> 1327 /// Occurs when files are dropped on the list. 1328 /// </summary> 1329 public event FileDropEventHandler FilesDropped; 1330 1331 /// <summary> 1332 /// Occurs when an item needs to be moved 1333 /// </summary> 1334 public event MoveItemEventHandler MoveItem; 1335 1336 #endregion 1337 } 1338 1339 #region Delegates 1340 1341 /// <summary> 1342 /// Represents the method that will handle the <see cref="ViewDetails.FilesDropped"/> event. 1343 /// </summary> 1344 /// <param name="sender">The sender of the event</param> 1345 /// <param name="e">The event data</param> 1346 public delegate void FileDropEventHandler(object sender, FileDropEventArgs e); 1347 1348 /// <summary> 1349 /// Represents the method that will handle the <see cref="ViewDetails.MoveItem"/> event. 1350 /// </summary> 1351 /// <param name="sender">The sender of the event</param> 1352 /// <param name="e">The event data</param> 1353 public delegate void MoveItemEventHandler(object sender, MoveItemEventArgs e); 1354 1355 /// <summary> 1356 /// Represents the method that will determine whether an item matches a filter string or not 1357 /// </summary> 1358 /// <param name="item">The item which should be examined</param> 1359 /// <param name="filterString">The string which should be matched</param> 1360 /// <returns>True if the item matches the string, otherwise False</returns> 1361 public delegate bool ViewDetailsSearchDelegate(ViewDetailsItemData item, string filterString); 1362 1363 /// <summary> 1364 /// Represents the method that is called to focus and select an item of the ListView. 1365 /// </summary> 1366 /// <param name="itemIndex">The index of the item to focus and select</param> 1367 public delegate void FocusAndSelectItemDelegate(int itemIndex); 1368 1369 #endregion 1370 1371 #region Event arguments 1372 1373 /// <summary> 1374 /// Provides data for the <see cref="ViewDetails.FilesDropped"/> event 1375 /// </summary> 1376 public class FileDropEventArgs 1377 { 1378 #region Properties 1379 1380 /// <summary> 1381 /// Gets the paths of the files that were dropped 1382 /// </summary> 1383 public string[] Paths { get; private set; } 1384 1385 /// <summary> 1386 /// Gets the index where the files were dropped 1387 /// </summary> 1388 public int Position { get; private set; } 1389 1390 #endregion 1391 1392 #region Constructor 1393 1394 /// <summary> 1395 /// Initializes a new instance of the <see cref="FileDropEventArgs"/> class 1396 /// </summary> 1397 /// <param name="paths">The paths that was dropped</param> 1398 /// <param name="position">The index where the files where dropped</param> 1399 public FileDropEventArgs(string[] paths, int position) 1400 { 1401 Paths = paths; 1402 Position = position; 1403 } 1404 1405 #endregion 1406 } 1407 1408 /// <summary> 1409 /// Provides data for the <see cref="ViewDetails.MoveItem"/> event 1410 /// </summary> 1411 public class MoveItemEventArgs 1412 { 1413 #region Properties 1414 1415 /// <summary> 1416 /// Gets the paths of the item that is to be moved 1417 /// </summary> 1418 public object Item { get; private set; } 1419 1420 /// <summary> 1421 /// Gets the index that the item is to be moved to 1422 /// </summary> 1423 public int Position { get; private set; } 1424 1425 #endregion 1426 1427 #region Constructor 1428 1429 /// <summary> 1430 /// Initializes a new instance of the <see cref="MoveItemEventArgs"/> class 1431 /// </summary> 1432 /// <param name="item">The item that is to be moved</param> 1433 /// <param name="position">The index that the item is to be moved to</param> 1434 public MoveItemEventArgs(object item, int position) 1435 { 1436 Item = item; 1437 Position = position; 1438 } 1439 1440 #endregion 1441 } 1442 1443 #endregion 1444 1445 #region Data structures 1446 1447 /// <summary> 1448 /// Describes the data source of an item inside the ViewDetails list 1449 /// </summary> 1450 public class ViewDetailsItemData : INotifyPropertyChanged 1451 { 1452 #region Members 1453 1454 private int number; 1455 private bool isActive; 1456 private string icon; 1457 private bool strike; 1458 1459 #endregion 1460 1461 #region Properties 1462 1463 /// <summary> 1464 /// Gets or sets the index number of the item 1465 /// </summary> 1466 public int Number 1467 { 1468 get { return number; } 1469 set { number = value; OnPropertyChanged("Number"); } 1470 } 1471 1472 /// <summary> 1473 /// Gets or sets whether the item is marked as active or not 1474 /// </summary> 1475 public bool IsActive 1476 { 1477 get { return isActive; } 1478 set { isActive = value; OnPropertyChanged("IsActive"); } 1479 } 1480 1481 /// <summary> 1482 /// Gets or sets the icon of the item 1483 /// </summary> 1484 public string Icon 1485 { 1486 get { return icon; } 1487 set { icon = value; OnPropertyChanged("Icon"); } 1488 } 1489 1490 /// <summary> 1491 /// Gets or sets whether the items should feature a strikethrough 1492 /// </summary> 1493 public bool Strike 1494 { 1495 get { return strike; } 1496 set { strike = value; OnPropertyChanged("Strike"); } 1497 } 1498 1499 #endregion 1500 1501 #region INotifyPropertyChanged Members 1502 1503 /// <summary> 1504 /// Occurs when the property of the item is changed 1505 /// </summary> 1506 public event PropertyChangedEventHandler PropertyChanged; 1507 1508 /// <summary> 1509 /// Dispatches the PropertyChanged event 1510 /// </summary> 1511 /// <param name="name">The name of the property that was changed</param> 1512 public void OnPropertyChanged(string name) 1513 { 1514 if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); 1515 } 1516 1517 #endregion 1518 } 1519 1520 /// <summary> 1521 /// Describes a configuration for the ViewDetails class 1522 /// </summary> 1523 public class ViewDetailsConfig : INotifyPropertyChanged 1524 { 1525 #region Members 1526 1527 string filter = ""; 1528 1529 #endregion 1530 1531 #region Properties 1532 1533 /// <summary> 1534 /// Gets or sets the columns 1535 /// </summary> 1536 public ObservableCollection<ViewDetailsColumn> Columns { get; set; } 1537 1538 /// <summary> 1539 /// Gets or sets the number column configuration 1540 /// </summary> 1541 public ViewDetailsColumn NumberColumn { get; set; } 1542 1543 /// <summary> 1544 /// Gets or sets the indices of the selected items 1545 /// </summary> 1546 public List<int> SelectedIndices { get; set; } 1547 1548 /// <summary> 1549 /// Gets or sets the the sort orders 1550 /// Each sort is represented as a string on the format 1551 /// "asc/dsc:ColumnName" 1552 /// </summary> 1553 public List<string> Sorts { get; set; } 1554 1555 /// <summary> 1556 /// Gets or sets text used to filter the list 1557 /// </summary> 1558 public string Filter 1559 { 1560 get { return filter; } 1561 set 1562 { 1563 filter = value; 1564 OnPropertyChanged("Filter"); 1565 } 1566 } 1567 1568 /// <summary> 1569 /// Gets or sets whether the number column should be enabled 1570 /// </summary> 1571 public bool HasNumber { get; set; } 1572 1573 /// <summary> 1574 /// Gets or sets whether the number column should be visible 1575 /// </summary> 1576 public bool IsNumberVisible { get; set; } 1577 1578 /// <summary> 1579 /// Gets or sets the position of the number column 1580 /// </summary> 1581 public int NumberIndex { get; set; } 1582 1583 /// <summary> 1584 /// Gets or sets whether to display icons or not 1585 /// </summary> 1586 public bool UseIcons { get; set; } 1587 1588 /// <summary> 1589 /// Gets or sets whether files can be dropped onto the list 1590 /// </summary> 1591 public bool AcceptFileDrops { get; set; } 1592 1593 /// <summary> 1594 /// Gets or sets whether the list can be resorted via drag and drop 1595 /// </summary> 1596 public bool IsDragSortable { get; set; } 1597 1598 /// <summary> 1599 /// Gets or sets whether the list can be resorted by clicking on a column 1600 /// </summary> 1601 public bool IsClickSortable { get; set; } 1602 1603 /// <summary> 1604 /// Gets or sets whether only the number column can be used to sort the list 1605 /// </summary> 1606 public bool LockSortOnNumber { get; set; } 1607 1608 #endregion 1609 1610 #region INotifyPropertyChanged Members 1611 1612 /// <summary> 1613 /// Occurs when the property of the item is changed 1614 /// </summary> 1615 public event PropertyChangedEventHandler PropertyChanged; 1616 1617 /// <summary> 1618 /// Dispatches the PropertyChanged event 1619 /// </summary> 1620 /// <param name="name">The name of the property that was changed</param> 1621 public void OnPropertyChanged(string name) 1622 { 1623 if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); 1624 } 1625 1626 #endregion 1627 } 1628 1629 /// <summary> 1630 /// Represents a column of a details list 1631 /// </summary> 1632 public class ViewDetailsColumn : INotifyPropertyChanged 1633 { 1634 #region Members 1635 1636 private string text; 1637 private string binding; 1638 private string sortField; 1639 private bool isAlwaysVisible = false; 1640 private bool isSortable = true; 1641 private double width = 50.0; 1642 private bool isVisible = true; 1643 private HorizontalAlignment alignment = HorizontalAlignment.Left; 1644 1645 #endregion 1646 1647 #region Properties 1648 1649 /// <summary> 1650 /// Gets or sets the name of the column 1651 /// </summary> 1652 public string Name { get; set; } 1653 1654 /// <summary> 1655 /// Gets or sets the displayed text 1656 /// </summary> 1657 public string Text 1658 { 1659 get { return text; } 1660 set 1661 { 1662 text = value; 1663 OnPropertyChanged("Text"); 1664 } 1665 } 1666 1667 /// <summary> 1668 /// Gets or sets the value to bind to 1669 /// </summary> 1670 public string Binding 1671 { 1672 get { return binding; } 1673 set 1674 { 1675 binding = value; 1676 OnPropertyChanged("Binding"); 1677 } 1678 } 1679 1680 /// <summary> 1681 /// Gets or sets the value to sort on 1682 /// </summary> 1683 public string SortField 1684 { 1685 get { return sortField; } 1686 set 1687 { 1688 sortField = value; 1689 OnPropertyChanged("SortField"); 1690 } 1691 } 1692 1693 /// <summary> 1694 /// Gets or sets whether the column is always visible 1695 /// </summary> 1696 public bool IsAlwaysVisible 1697 { 1698 get { return isAlwaysVisible; } 1699 set 1700 { 1701 isAlwaysVisible = value; 1702 OnPropertyChanged("IsAlwaysVisible"); 1703 } 1704 } 1705 1706 /// <summary> 1707 /// Gets or sets whether the column is sortable 1708 /// </summary> 1709 public bool IsSortable 1710 { 1711 get { return isSortable; } 1712 set 1713 { 1714 isSortable = value; 1715 OnPropertyChanged("IsSortable"); 1716 } 1717 } 1718 1719 /// <summary> 1720 /// Gets or sets the width of the column 1721 /// </summary> 1722 public double Width 1723 { 1724 get { return width; } 1725 set 1726 { 1727 width = value; 1728 OnPropertyChanged("Width"); 1729 } 1730 } 1731 1732 /// <summary> 1733 /// Gets or sets whether the column is visible (only effective if IsAlwaysVisible is false) 1734 /// </summary> 1735 public bool IsVisible 1736 { 1737 get { return isVisible; } 1738 set 1739 { 1740 isVisible = value; 1741 OnPropertyChanged("IsVisible"); 1742 } 1743 } 1744 1745 /// <summary> 1746 /// Gets or sets the text alignment of the displayed text 1747 /// </summary> 1748 public HorizontalAlignment Alignment 1749 { 1750 get { return alignment; } 1751 set 1752 { 1753 alignment = value; 1754 OnPropertyChanged("Alignment"); 1755 } 1756 } 1757 1758 #endregion 1759 1760 #region INotifyPropertyChanged Members 1761 1762 /// <summary> 1763 /// Occurs when the property of the item is changed 1764 /// </summary> 1765 public event PropertyChangedEventHandler PropertyChanged; 1766 1767 /// <summary> 1768 /// Dispatches the PropertyChanged event 1769 /// </summary> 1770 /// <param name="name">The name of the property that was changed</param> 1771 public void OnPropertyChanged(string name) 1772 { 1773 if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); 1774 } 1775 1776 #endregion 1777 } 1778 1779 #endregion 1780 1781 /// <summary> 1782 /// The graphical drag-n-drop target of ViewDetails 1783 /// </summary> 1784 public class ViewDetailsDropTarget : Adorner 1785 { 1786 #region Members 1787 1788 private double position; 1789 private bool scrollbar = false; 1790 1791 #endregion 1792 1793 #region Properties 1794 1795 /// <summary> 1796 /// Gets or sets the vertical position of the drop target 1797 /// </summary> 1798 public double Position 1799 { 1800 get 1801 { 1802 return position; 1803 } 1804 set 1805 { 1806 position = value; 1807 this.InvalidateVisual(); 1808 1809 } 1810 } 1811 1812 /// <summary> 1813 /// Gets or sets whether the drop target should make space for a vertical scroll bar 1814 /// </summary> 1815 public bool ScrollBar 1816 { 1817 get 1818 { 1819 return scrollbar; 1820 } 1821 set 1822 { 1823 scrollbar = value; 1824 this.InvalidateVisual(); 1825 1826 } 1827 } 1828 1829 #endregion 1830 1831 #region Constructor 1832 1833 /// <summary> 1834 /// Creates an instance of the DropTarget class 1835 /// </summary> 1836 /// <param name="adornedElement">The element that the drop target should adorn</param> 1837 public ViewDetailsDropTarget(UIElement adornedElement) 1838 : base(adornedElement) 1839 { 1840 position = adornedElement.DesiredSize.Height / 2; 1841 IsHitTestVisible = false; 1842 SnapsToDevicePixels = true; 1843 } 1844 1845 #endregion 1846 1847 #region Override 1848 1849 /// <summary> 1850 /// Invoked when the target is rendered. 1851 /// Does the actual painting of the drop target. 1852 /// </summary> 1853 /// <param name="drawingContext">The drawing context of the rendering</param> 1854 protected override void OnRender(DrawingContext drawingContext) 1855 { 1856 Point left = new Point(0, position); 1857 double width = AdornedElement.DesiredSize.Width; 1858 if (ScrollBar) width -= 18; 1859 Point right = new Point(width, position); 1860 1861 PointCollection points = new PointCollection(); 1862 points.Add(new Point(0, 0)); 1863 points.Add(new Point(0, 6)); 1864 points.Add(new Point(3, 3)); 1865 1866 PathFigure pfig1 = new PathFigure(); 1867 pfig1.StartPoint = new Point(2, position - 2); 1868 pfig1.Segments.Add(new LineSegment(new Point(4, position), true)); 1869 pfig1.Segments.Add(new LineSegment(new Point(width - 4, position), true)); 1870 pfig1.Segments.Add(new LineSegment(new Point(width - 2, position - 2), true)); 1871 pfig1.Segments.Add(new LineSegment(new Point(width - 2, position + 3), true)); 1872 pfig1.Segments.Add(new LineSegment(new Point(width - 4, position + 1), true)); 1873 pfig1.Segments.Add(new LineSegment(new Point(4, position + 1), true)); 1874 pfig1.Segments.Add(new LineSegment(new Point(2, position + 3), true)); 1875 1876 PathGeometry p = new PathGeometry(); 1877 p.Figures.Add(pfig1); 1878 1879 drawingContext.DrawGeometry(Brushes.Black, new Pen(Brushes.Black, 1), p); 1880 } 1881 1882 #endregion 1883 } 1884 1885 /// <summary> 1886 /// A single item in the list of the ViewDetails control 1887 /// </summary> 1888 public partial class ViewDetailsItem : ListViewItem 1889 { 1890 #region Members 1891 1892 private Point startDragPoint; 1893 private bool isDragging = false; 1894 private bool doDeselect = false; 1895 1896 #endregion 1897 1898 #region Override 1899 1900 /// <summary> 1901 /// Called when the user presses the right mouse button over the ViewDetailsItem. 1902 /// </summary> 1903 /// <param name="e">The event data</param> 1904 protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) 1905 { 1906 if (!IsSelected) 1907 base.OnMouseLeftButtonDown(e); 1908 else 1909 doDeselect = true; 1910 isDragging = true; 1911 startDragPoint = e.GetPosition(null); 1912 } 1913 1914 /// <summary> 1915 /// Called when the user releases the right mouse button over the ViewDetailsItem. 1916 /// </summary> 1917 /// <param name="e">The event data</param> 1918 protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) 1919 { 1920 if (doDeselect) 1921 base.OnMouseLeftButtonDown(e); 1922 base.OnMouseLeftButtonUp(e); 1923 doDeselect = false; 1924 } 1925 1926 /// <summary> 1927 /// Called when the user moves the mouse over the ViewDetailsItem. 1928 /// </summary> 1929 /// <param name="e">The event data</param> 1930 protected override void OnMouseMove(MouseEventArgs e) 1931 { 1932 base.OnMouseMove(e); 1933 if (!isDragging) return; 1934 if (e.LeftButton == MouseButtonState.Released) 1935 { 1936 isDragging = false; 1937 return; 1938 } 1939 Vector diff = startDragPoint - e.GetPosition(null); 1940 if (Math.Abs(diff.X) < SystemParameters.MinimumHorizontalDragDistance && Math.Abs(diff.Y) < SystemParameters.MinimumVerticalDragDistance) 1941 return; 1942 1943 ListBoxItem lvi = ViewDetailsUtilities.TryFindFromPoint<ListBoxItem>(this, e.GetPosition(this)); 1944 ViewDetails vd = ViewDetailsUtilities.TryFindParent<ViewDetails>(lvi); 1945 if (vd == null) 1946 return; 1947 1948 if (vd.SelectedItems.Count <= 0) 1949 return; // halt if we don't have any items to drag 1950 1951 List<object> DraggedItems = new List<object>(); 1952 1953 foreach (object DraggedItem in vd.SelectedItems) 1954 DraggedItems.Add(DraggedItem); 1955 1956 DragDropEffects AllowedEffects = DragDropEffects.Move; 1957 1958 DragDrop.DoDragDrop(this, DraggedItems, AllowedEffects); 1959 } 1960 1961 #endregion 1962 } 1963 1964 /// <summary> 1965 /// A collection of some static help methods for ViewDetails 1966 /// </summary> 1967 public static class ViewDetailsUtilities 1968 { 1969 1970 /// <summary>Finds a parent of a given item on the visual tree.</summary> 1971 /// <typeparam name="T">The type of the queried item.</typeparam> 1972 /// <param name="iChild">A direct or indirect child of the queried item.</param> 1973 /// <returns>The first parent item that matches the submitted type parameter. If not matching item can be found, a null reference is being returned.</returns> 1974 public static T TryFindParent<T>(this DependencyObject iChild) 1975 where T : DependencyObject 1976 { 1977 // Get parent item. 1978 DependencyObject parentObject = GetParentObject(iChild); 1979 1980 // We've reached the end of the tree. 1981 if (parentObject == null) 1982 return null; 1983 1984 // Check if the parent matches the type we're looking for. 1985 // Else use recursion to proceed with next level. 1986 T parent = parentObject as T; 1987 return parent ?? TryFindParent<T>(parentObject); 1988 } 1989 1990 /// <summary> 1991 /// This method is an alternative to WPF's <see cref="VisualTreeHelper.GetParent"/> method, which also 1992 /// supports content elements. Keep in mind that for content element, this method falls back to the logical tree of the element! 1993 /// </summary> 1994 /// <param name="iChild">The item to be processed.</param> 1995 /// <returns>The submitted item's parent, if available. Otherwise null.</returns> 1996 public static DependencyObject GetParentObject(this DependencyObject iChild) 1997 { 1998 if (iChild == null) 1999 { 2000 return null; 2001 } 2002 2003 // Handle content elements separately. 2004 ContentElement contentElement = iChild as ContentElement; 2005 if (contentElement != null) 2006 { 2007 DependencyObject parent = ContentOperations.GetParent(contentElement); 2008 if (parent != null) return parent; 2009 2010 FrameworkContentElement frameworkContentElement = contentElement as FrameworkContentElement; 2011 return frameworkContentElement != null ? frameworkContentElement.Parent : null; 2012 } 2013 2014 // Also try searching for parent in framework elements (such as DockPanel, etc). 2015 FrameworkElement frameworkElement = iChild as FrameworkElement; 2016 if (frameworkElement != null) 2017 { 2018 DependencyObject parent = frameworkElement.Parent; 2019 if (parent != null) return parent; 2020 } 2021 2022 // If it's not a ContentElement/FrameworkElement, rely on VisualTreeHelper. 2023 return VisualTreeHelper.GetParent(iChild); 2024 } 2025 2026 /// <summary>Tries to locate a given item within the visual tree, starting with the dependency object at a given position.</summary> 2027 /// <typeparam name="T">The type of the element to be found on the visual tree of the element at the given location.</typeparam> 2028 /// <param name="iReference">The main element which is used to perform hit testing.</param> 2029 /// <param name="iPoint">The position to be evaluated on the origin.</param> 2030 public static T TryFindFromPoint<T>(this UIElement iReference, Point iPoint) where T : DependencyObject 2031 { 2032 DependencyObject element = iReference.InputHitTest(iPoint) as DependencyObject; 2033 if (element == null) 2034 { 2035 return null; 2036 } 2037 else if (element is T) 2038 return (T)element; 2039 else 2040 return TryFindParent<T>(element); 2041 } 2042 2043 /// <summary> 2044 /// Tries to locate a child of a given item within the visual tree 2045 /// </summary> 2046 /// <typeparam name="T">The type of the element to be found on the visual tree of the parent to the element</typeparam> 2047 /// <param name="referenceVisual">A direct or indirect parent of the element to be found</param> 2048 /// <returns>The first child item that matches the submitted type parameter. If not matching item can be found, a null reference is being returned.</returns> 2049 public static T GetVisualChild<T>(Visual referenceVisual) where T : Visual 2050 { 2051 Visual child = null; 2052 for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(referenceVisual); i++) 2053 { 2054 child = VisualTreeHelper.GetChild(referenceVisual, i) as Visual; 2055 if (child != null && (child.GetType() == typeof(T))) 2056 { 2057 break; 2058 } 2059 else if (child != null) 2060 { 2061 child = GetVisualChild<T>(child); 2062 if (child != null && (child.GetType() == typeof(T))) 2063 { 2064 break; 2065 } 2066 } 2067 } 2068 return child as T; 2069 } 2070 } 2071 2072 /// <summary> 2073 /// Represents a converter for turning an icon path into a bitmap image 2074 /// </summary> 2075 public class StringToBitmapImageConverter : IValueConverter 2076 { 2077 /// <summary> 2078 /// Converts a path into a bitmap 2079 /// </summary> 2080 /// <param name="value">The path to the image</param> 2081 /// <param name="targetType">The type of the target (not used)</param> 2082 /// <param name="parameter">Additional parameters (not used)</param> 2083 /// <param name="culture">The current culture (not used)</param> 2084 /// <returns>A bitmap image created from the path given</returns> 2085 public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 2086 { 2087 if (value == null) return null; 2088 string uristring = value as string; 2089 if (uristring.Substring(uristring.Length-4) == ".ico") 2090 return Utilities.GetIcoImage(uristring, 16, 16); 2091 return new BitmapImage(new Uri(uristring, UriKind.RelativeOrAbsolute)); 2092 } 2093 2094 /// <summary> 2095 /// This method is not implemented and will throw an exception if used 2096 /// </summary> 2097 /// <param name="value">The image</param> 2098 /// <param name="targetType">The target type</param> 2099 /// <param name="parameter">Additional parameters</param> 2100 /// <param name="culture">The current culture</param> 2101 /// <returns>Nothing</returns> 2102 public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 2103 { 2104 throw new NotSupportedException(); 2105 } 2106 } 2107}