From b3da45eb9485838bad5ab95309a7f61efaeb1f70 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sun, 16 Mar 2014 19:01:22 +0000 Subject: [PATCH] v1.0.0.0 --- ProgramQueuer.sln | 20 + ProgramQueuer/App.xaml | 8 + ProgramQueuer/App.xaml.cs | 16 + ProgramQueuer/Helpers/BoolToVisibility.cs | 50 + ProgramQueuer/Helpers/BooleanInverter.cs | 44 + ProgramQueuer/Helpers/DragAdorner.cs | 175 ++++ .../Helpers/ListViewDragDropManager.cs | 864 ++++++++++++++++++ ProgramQueuer/Helpers/MouseUtilities.cs | 58 ++ ProgramQueuer/Helpers/ValueConverterGroup.cs | 177 ++++ ProgramQueuer/Helpers/ValueConverters.cs | 45 + ProgramQueuer/MainWindow.xaml | 95 ++ ProgramQueuer/MainWindow.xaml.cs | 118 +++ ProgramQueuer/ProgramQueuer.csproj | 142 +++ ProgramQueuer/Properties/AssemblyInfo.cs | 55 ++ .../Properties/Resources.Designer.cs | 71 ++ ProgramQueuer/Properties/Resources.resx | 117 +++ ProgramQueuer/Properties/Settings.Designer.cs | 98 ++ ProgramQueuer/Properties/Settings.settings | 24 + ProgramQueuer/Queuer/EntryManager.cs | 175 ++++ ProgramQueuer/Queuer/ProcessIOManager.cs | 342 +++++++ ProgramQueuer/Queuer/ProgramEntry.cs | 85 ++ ProgramQueuer/Resources/accept.png | Bin 0 -> 984 bytes ProgramQueuer/Resources/add.png | Bin 0 -> 940 bytes ProgramQueuer/Resources/application.png | Bin 0 -> 554 bytes ProgramQueuer/Resources/play.png | Bin 0 -> 923 bytes ProgramQueuer/Resources/remove.png | Bin 0 -> 1004 bytes ProgramQueuer/Resources/stop.png | Bin 0 -> 913 bytes ProgramQueuer/Resources/warning.png | Bin 0 -> 713 bytes ProgramQueuer/Resources/window.png | Bin 0 -> 307 bytes ProgramQueuer/Settings.xaml | 12 + ProgramQueuer/Settings.xaml.cs | 26 + ProgramQueuer/app.config | 30 + ProgramQueuer/reports.ico | Bin 0 -> 111253 bytes 33 files changed, 2847 insertions(+) create mode 100755 ProgramQueuer.sln create mode 100755 ProgramQueuer/App.xaml create mode 100755 ProgramQueuer/App.xaml.cs create mode 100755 ProgramQueuer/Helpers/BoolToVisibility.cs create mode 100755 ProgramQueuer/Helpers/BooleanInverter.cs create mode 100755 ProgramQueuer/Helpers/DragAdorner.cs create mode 100755 ProgramQueuer/Helpers/ListViewDragDropManager.cs create mode 100755 ProgramQueuer/Helpers/MouseUtilities.cs create mode 100755 ProgramQueuer/Helpers/ValueConverterGroup.cs create mode 100755 ProgramQueuer/Helpers/ValueConverters.cs create mode 100755 ProgramQueuer/MainWindow.xaml create mode 100755 ProgramQueuer/MainWindow.xaml.cs create mode 100755 ProgramQueuer/ProgramQueuer.csproj create mode 100755 ProgramQueuer/Properties/AssemblyInfo.cs create mode 100755 ProgramQueuer/Properties/Resources.Designer.cs create mode 100755 ProgramQueuer/Properties/Resources.resx create mode 100755 ProgramQueuer/Properties/Settings.Designer.cs create mode 100755 ProgramQueuer/Properties/Settings.settings create mode 100755 ProgramQueuer/Queuer/EntryManager.cs create mode 100755 ProgramQueuer/Queuer/ProcessIOManager.cs create mode 100755 ProgramQueuer/Queuer/ProgramEntry.cs create mode 100755 ProgramQueuer/Resources/accept.png create mode 100755 ProgramQueuer/Resources/add.png create mode 100755 ProgramQueuer/Resources/application.png create mode 100755 ProgramQueuer/Resources/play.png create mode 100755 ProgramQueuer/Resources/remove.png create mode 100755 ProgramQueuer/Resources/stop.png create mode 100755 ProgramQueuer/Resources/warning.png create mode 100755 ProgramQueuer/Resources/window.png create mode 100755 ProgramQueuer/Settings.xaml create mode 100755 ProgramQueuer/Settings.xaml.cs create mode 100755 ProgramQueuer/app.config create mode 100755 ProgramQueuer/reports.ico diff --git a/ProgramQueuer.sln b/ProgramQueuer.sln new file mode 100755 index 0000000..469107f --- /dev/null +++ b/ProgramQueuer.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProgramQueuer", "ProgramQueuer\ProgramQueuer.csproj", "{B1825A4D-32CB-45E2-9CFE-77622DCC1641}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B1825A4D-32CB-45E2-9CFE-77622DCC1641}.Debug|x86.ActiveCfg = Debug|x86 + {B1825A4D-32CB-45E2-9CFE-77622DCC1641}.Debug|x86.Build.0 = Debug|x86 + {B1825A4D-32CB-45E2-9CFE-77622DCC1641}.Release|x86.ActiveCfg = Release|x86 + {B1825A4D-32CB-45E2-9CFE-77622DCC1641}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/ProgramQueuer/App.xaml b/ProgramQueuer/App.xaml new file mode 100755 index 0000000..1aeb5f9 --- /dev/null +++ b/ProgramQueuer/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/ProgramQueuer/App.xaml.cs b/ProgramQueuer/App.xaml.cs new file mode 100755 index 0000000..29aa2a3 --- /dev/null +++ b/ProgramQueuer/App.xaml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Windows; + +namespace ProgramQueuer +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/ProgramQueuer/Helpers/BoolToVisibility.cs b/ProgramQueuer/Helpers/BoolToVisibility.cs new file mode 100755 index 0000000..48e02e6 --- /dev/null +++ b/ProgramQueuer/Helpers/BoolToVisibility.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace ProgramQueuer.Helpers +{ + /// + /// Simple boolean value converter that converts it to a visbility value where true is visible and false is collapsed. + /// + [ValueConversion(typeof(bool), typeof(Visibility))] + public class BoolToVisibility : IValueConverter + { + /// + /// Convert from a boolean value of true to Visibility.Visible and false to Visibility.Collapsed. + /// + /// A boolean value to convert. + /// The type of the target value. This property is ignored. + /// Parameter to pass to the converter. This property is ignored. + /// A reference to the target CultureInfo. This property is ignored. + /// A Visibility value of either Visible or Collapsed depending on the value parameter. + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + bool visible = (bool)value; + if (visible) + return Visibility.Visible; + return Visibility.Collapsed; + } + + /// + /// Convert back from a Visibility value to a boolean value where Visibility.Visible converts to true and Visibility.Collapsed to false. + /// + /// The visibility value to convert back to a boolean value. + /// The type of the target value. This property is ignored. + /// Parameter to pass to the converter. This property is ignored. + /// A reference to the target CultureInfo. This property is ignored. + /// A boolean value representing the visibility of the control. + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + var visible = (Visibility)value; + if (visible == Visibility.Collapsed) + return false; + return true; + } + } +} diff --git a/ProgramQueuer/Helpers/BooleanInverter.cs b/ProgramQueuer/Helpers/BooleanInverter.cs new file mode 100755 index 0000000..25d1789 --- /dev/null +++ b/ProgramQueuer/Helpers/BooleanInverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace ProgramQueuer.Helpers +{ + /// + /// A simple boolean converter that inverts the boolean value. + /// + [ValueConversion(typeof(bool), typeof(bool))] + public class BooleanInverter : IValueConverter + { + /// + /// Convert a boolean value to the inverted value. Seriously, if I have to explain this, then you are in the wrong business. + /// + /// The boolean value to invert. + /// The type of the target value. This property is ignored. + /// Parameter to pass to the converter. This property is ignored. + /// A reference to the target CultureInfo. This property is ignored. + /// An inverted boolean value. + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !(bool)value; + } + + /// + /// Convert an inverted boolean value to it's original value. Basically inverted. + /// + /// The boolean value to invert back. + /// The type of the target value. This property is ignored. + /// Parameter to pass to the converter. This property is ignored. + /// A reference to the target CultureInfo. This property is ignored. + /// An inverted boolean value. + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !(bool)value; + } + } +} diff --git a/ProgramQueuer/Helpers/DragAdorner.cs b/ProgramQueuer/Helpers/DragAdorner.cs new file mode 100755 index 0000000..53d7990 --- /dev/null +++ b/ProgramQueuer/Helpers/DragAdorner.cs @@ -0,0 +1,175 @@ +// Copyright (C) Josh Smith - January 2007 +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Documents; +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows.Media.Animation; +using System.Windows.Controls; + +namespace WPF.JoshSmith.Adorners +{ + /// + /// Renders a visual which can follow the mouse cursor, + /// such as during a drag-and-drop operation. + /// + public class DragAdorner : Adorner + { + #region Data + + private Rectangle child = null; + private double offsetLeft = 0; + private double offsetTop = 0; + + #endregion // Data + + #region Constructor + + /// + /// Initializes a new instance of DragVisualAdorner. + /// + /// The element being adorned. + /// The size of the adorner. + /// A brush to with which to paint the adorner. + public DragAdorner(UIElement adornedElement, Size size, Brush brush) + : base(adornedElement) + { + Rectangle rect = new Rectangle(); + rect.Fill = brush; + rect.Width = size.Width; + rect.Height = size.Height; + rect.IsHitTestVisible = false; + this.child = rect; + } + + #endregion // Constructor + + #region Public Interface + + #region GetDesiredTransform + + /// + /// Override. + /// + /// + /// + public override GeneralTransform GetDesiredTransform(GeneralTransform transform) + { + GeneralTransformGroup result = new GeneralTransformGroup(); + result.Children.Add(base.GetDesiredTransform(transform)); + result.Children.Add(new TranslateTransform(this.offsetLeft, this.offsetTop)); + return result; + } + + #endregion // GetDesiredTransform + + #region OffsetLeft + + /// + /// Gets/sets the horizontal offset of the adorner. + /// + public double OffsetLeft + { + get { return this.offsetLeft; } + set + { + this.offsetLeft = value; + UpdateLocation(); + } + } + + #endregion // OffsetLeft + + #region SetOffsets + + /// + /// Updates the location of the adorner in one atomic operation. + /// + /// + /// + public void SetOffsets(double left, double top) + { + this.offsetLeft = left; + this.offsetTop = top; + this.UpdateLocation(); + } + + #endregion // SetOffsets + + #region OffsetTop + + /// + /// Gets/sets the vertical offset of the adorner. + /// + public double OffsetTop + { + get { return this.offsetTop; } + set + { + this.offsetTop = value; + UpdateLocation(); + } + } + + #endregion // OffsetTop + + #endregion // Public Interface + + #region Protected Overrides + + /// + /// Override. + /// + /// + /// + protected override Size MeasureOverride(Size constraint) + { + this.child.Measure(constraint); + return this.child.DesiredSize; + } + + /// + /// Override. + /// + /// + /// + protected override Size ArrangeOverride(Size finalSize) + { + this.child.Arrange(new Rect(finalSize)); + return finalSize; + } + + /// + /// Override. + /// + /// + /// + protected override Visual GetVisualChild(int index) + { + return this.child; + } + + /// + /// Override. Always returns 1. + /// + protected override int VisualChildrenCount + { + get { return 1; } + } + + #endregion // Protected Overrides + + #region Private Helpers + + private void UpdateLocation() + { + AdornerLayer adornerLayer = this.Parent as AdornerLayer; + if (adornerLayer != null) + adornerLayer.Update(this.AdornedElement); + } + + #endregion // Private Helpers + } +} \ No newline at end of file diff --git a/ProgramQueuer/Helpers/ListViewDragDropManager.cs b/ProgramQueuer/Helpers/ListViewDragDropManager.cs new file mode 100755 index 0000000..11c7743 --- /dev/null +++ b/ProgramQueuer/Helpers/ListViewDragDropManager.cs @@ -0,0 +1,864 @@ +// Copyright (C) Josh Smith - January 2007 +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Input; +using WPF.JoshSmith.Adorners; +using WPF.JoshSmith.Controls.Utilities; + +namespace WPF.JoshSmith.ServiceProviders.UI +{ + #region ListViewDragDropManager + + /// + /// Manages the dragging and dropping of ListViewItems in a ListView. + /// The ItemType type parameter indicates the type of the objects in + /// the ListView's items source. The ListView's ItemsSource must be + /// set to an instance of ObservableCollection of ItemType, or an + /// Exception will be thrown. + /// + /// The type of the ListView's items. + public class ListViewDragDropManager where ItemType : class + { + #region Data + + bool canInitiateDrag; + DragAdorner dragAdorner; + double dragAdornerOpacity; + int indexToSelect; + bool isDragInProgress; + ItemType itemUnderDragCursor; + ListView listView; + Point ptMouseDown; + bool showDragAdorner; + + #endregion // Data + + #region Constructors + + /// + /// Initializes a new instance of ListViewDragManager. + /// + public ListViewDragDropManager() + { + this.canInitiateDrag = false; + this.dragAdornerOpacity = 0.7; + this.indexToSelect = -1; + this.showDragAdorner = true; + } + + /// + /// Initializes a new instance of ListViewDragManager. + /// + /// + public ListViewDragDropManager(ListView listView) + : this() + { + this.ListView = listView; + } + + /// + /// Initializes a new instance of ListViewDragManager. + /// + /// + /// + public ListViewDragDropManager(ListView listView, double dragAdornerOpacity) + : this(listView) + { + this.DragAdornerOpacity = dragAdornerOpacity; + } + + /// + /// Initializes a new instance of ListViewDragManager. + /// + /// + /// + public ListViewDragDropManager(ListView listView, bool showDragAdorner) + : this(listView) + { + this.ShowDragAdorner = showDragAdorner; + } + + #endregion // Constructors + + #region Public Interface + + #region DragAdornerOpacity + + /// + /// Gets/sets the opacity of the drag adorner. This property has no + /// effect if ShowDragAdorner is false. The default value is 0.7 + /// + public double DragAdornerOpacity + { + get { return this.dragAdornerOpacity; } + set + { + if (this.IsDragInProgress) + throw new InvalidOperationException("Cannot set the DragAdornerOpacity property during a drag operation."); + + if (value < 0.0 || value > 1.0) + throw new ArgumentOutOfRangeException("DragAdornerOpacity", value, "Must be between 0 and 1."); + + this.dragAdornerOpacity = value; + } + } + + #endregion // DragAdornerOpacity + + #region IsDragInProgress + + /// + /// Returns true if there is currently a drag operation being managed. + /// + public bool IsDragInProgress + { + get { return this.isDragInProgress; } + private set { this.isDragInProgress = value; } + } + + #endregion // IsDragInProgress + + #region ListView + + /// + /// Gets/sets the ListView whose dragging is managed. This property + /// can be set to null, to prevent drag management from occuring. If + /// the ListView's AllowDrop property is false, it will be set to true. + /// + public ListView ListView + { + get { return listView; } + set + { + if (this.IsDragInProgress) + throw new InvalidOperationException("Cannot set the ListView property during a drag operation."); + + if (this.listView != null) + { + #region Unhook Events + + this.listView.PreviewMouseLeftButtonDown -= listView_PreviewMouseLeftButtonDown; + this.listView.PreviewMouseMove -= listView_PreviewMouseMove; + this.listView.DragOver -= listView_DragOver; + this.listView.DragLeave -= listView_DragLeave; + this.listView.DragEnter -= listView_DragEnter; + this.listView.Drop -= listView_Drop; + + #endregion // Unhook Events + } + + this.listView = value; + + if (this.listView != null) + { + if (!this.listView.AllowDrop) + this.listView.AllowDrop = true; + + #region Hook Events + + this.listView.PreviewMouseLeftButtonDown += listView_PreviewMouseLeftButtonDown; + this.listView.PreviewMouseMove += listView_PreviewMouseMove; + this.listView.DragOver += listView_DragOver; + this.listView.DragLeave += listView_DragLeave; + this.listView.DragEnter += listView_DragEnter; + this.listView.Drop += listView_Drop; + + #endregion // Hook Events + } + } + } + + #endregion // ListView + + #region ProcessDrop [event] + + /// + /// Raised when a drop occurs. By default the dropped item will be moved + /// to the target index. Handle this event if relocating the dropped item + /// requires custom behavior. Note, if this event is handled the default + /// item dropping logic will not occur. + /// + public event EventHandler> ProcessDrop; + + #endregion // ProcessDrop [event] + + #region ShowDragAdorner + + /// + /// Gets/sets whether a visual representation of the ListViewItem being dragged + /// follows the mouse cursor during a drag operation. The default value is true. + /// + public bool ShowDragAdorner + { + get { return this.showDragAdorner; } + set + { + if (this.IsDragInProgress) + throw new InvalidOperationException("Cannot set the ShowDragAdorner property during a drag operation."); + + this.showDragAdorner = value; + } + } + + #endregion // ShowDragAdorner + + #endregion // Public Interface + + #region Event Handling Methods + + #region listView_PreviewMouseLeftButtonDown + + void listView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (this.IsMouseOverScrollbar) + { + // 4/13/2007 - Set the flag to false when cursor is over scrollbar. + this.canInitiateDrag = false; + return; + } + + int index = this.IndexUnderDragCursor; + this.canInitiateDrag = index > -1; + + if (this.canInitiateDrag) + { + // Remember the location and index of the ListViewItem the user clicked on for later. + this.ptMouseDown = MouseUtilities.GetMousePosition(this.listView); + this.indexToSelect = index; + } + else + { + this.ptMouseDown = new Point(-10000, -10000); + this.indexToSelect = -1; + } + } + + #endregion // listView_PreviewMouseLeftButtonDown + + #region listView_PreviewMouseMove + + void listView_PreviewMouseMove(object sender, MouseEventArgs e) + { + if (!this.CanStartDragOperation) + return; + + // Select the item the user clicked on. + if (this.listView.SelectedIndex != this.indexToSelect) + this.listView.SelectedIndex = this.indexToSelect; + + // If the item at the selected index is null, there's nothing + // we can do, so just return; + if (this.listView.SelectedItem == null) + return; + + ListViewItem itemToDrag = this.GetListViewItem(this.listView.SelectedIndex); + if (itemToDrag == null) + return; + + AdornerLayer adornerLayer = this.ShowDragAdornerResolved ? this.InitializeAdornerLayer(itemToDrag) : null; + + this.InitializeDragOperation(itemToDrag); + this.PerformDragOperation(); + this.FinishDragOperation(itemToDrag, adornerLayer); + } + + #endregion // listView_PreviewMouseMove + + #region listView_DragOver + + void listView_DragOver(object sender, DragEventArgs e) + { + e.Effects = DragDropEffects.Move; + + if (this.ShowDragAdornerResolved) + this.UpdateDragAdornerLocation(); + + // Update the item which is known to be currently under the drag cursor. + int index = this.IndexUnderDragCursor; + this.ItemUnderDragCursor = index < 0 ? null : this.ListView.Items[index] as ItemType; + } + + #endregion // listView_DragOver + + #region listView_DragLeave + + void listView_DragLeave(object sender, DragEventArgs e) + { + if (!this.IsMouseOver(this.listView)) + { + if (this.ItemUnderDragCursor != null) + this.ItemUnderDragCursor = null; + + if (this.dragAdorner != null) + this.dragAdorner.Visibility = Visibility.Collapsed; + } + } + + #endregion // listView_DragLeave + + #region listView_DragEnter + + void listView_DragEnter(object sender, DragEventArgs e) + { + if (this.dragAdorner != null && this.dragAdorner.Visibility != Visibility.Visible) + { + // Update the location of the adorner and then show it. + this.UpdateDragAdornerLocation(); + this.dragAdorner.Visibility = Visibility.Visible; + } + } + + #endregion // listView_DragEnter + + #region listView_Drop + + void listView_Drop(object sender, DragEventArgs e) + { + if (this.ItemUnderDragCursor != null) + this.ItemUnderDragCursor = null; + + e.Effects = DragDropEffects.None; + + if (!e.Data.GetDataPresent(typeof(ItemType))) + return; + + // Get the data object which was dropped. + ItemType data = e.Data.GetData(typeof(ItemType)) as ItemType; + if (data == null) + return; + + // Get the ObservableCollection which contains the dropped data object. + ObservableCollection itemsSource = this.listView.ItemsSource as ObservableCollection; + if (itemsSource == null) + throw new Exception( + "A ListView managed by ListViewDragManager must have its ItemsSource set to an ObservableCollection."); + + int oldIndex = itemsSource.IndexOf(data); + int newIndex = this.IndexUnderDragCursor; + + if (newIndex < 0) + { + // The drag started somewhere else, and our ListView is empty + // so make the new item the first in the list. + if (itemsSource.Count == 0) + newIndex = 0; + + // The drag started somewhere else, but our ListView has items + // so make the new item the last in the list. + else if (oldIndex < 0) + newIndex = itemsSource.Count; + + // The user is trying to drop an item from our ListView into + // our ListView, but the mouse is not over an item, so don't + // let them drop it. + else + return; + } + + // Dropping an item back onto itself is not considered an actual 'drop'. + if (oldIndex == newIndex) + return; + + if (this.ProcessDrop != null) + { + // Let the client code process the drop. + ProcessDropEventArgs args = new ProcessDropEventArgs(itemsSource, data, oldIndex, newIndex, e.AllowedEffects); + this.ProcessDrop(this, args); + e.Effects = args.Effects; + } + else + { + // Move the dragged data object from it's original index to the + // new index (according to where the mouse cursor is). If it was + // not previously in the ListBox, then insert the item. + if (oldIndex > -1) + itemsSource.Move(oldIndex, newIndex); + else + itemsSource.Insert(newIndex, data); + + // Set the Effects property so that the call to DoDragDrop will return 'Move'. + e.Effects = DragDropEffects.Move; + } + } + + #endregion // listView_Drop + + #endregion // Event Handling Methods + + #region Private Helpers + + #region CanStartDragOperation + + bool CanStartDragOperation + { + get + { + if (Mouse.LeftButton != MouseButtonState.Pressed) + return false; + + if (!this.canInitiateDrag) + return false; + + if (this.indexToSelect == -1) + return false; + + if (!this.HasCursorLeftDragThreshold) + return false; + + return true; + } + } + + #endregion // CanStartDragOperation + + #region FinishDragOperation + + void FinishDragOperation(ListViewItem draggedItem, AdornerLayer adornerLayer) + { + // Let the ListViewItem know that it is not being dragged anymore. + ListViewItemDragState.SetIsBeingDragged(draggedItem, false); + + this.IsDragInProgress = false; + + if (this.ItemUnderDragCursor != null) + this.ItemUnderDragCursor = null; + + // Remove the drag adorner from the adorner layer. + if (adornerLayer != null) + { + adornerLayer.Remove(this.dragAdorner); + this.dragAdorner = null; + } + } + + #endregion // FinishDragOperation + + #region GetListViewItem + + ListViewItem GetListViewItem(int index) + { + if (this.listView.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated) + return null; + + return this.listView.ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem; + } + + ListViewItem GetListViewItem(ItemType dataItem) + { + if (this.listView.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated) + return null; + + return this.listView.ItemContainerGenerator.ContainerFromItem(dataItem) as ListViewItem; + } + + #endregion // GetListViewItem + + #region HasCursorLeftDragThreshold + + bool HasCursorLeftDragThreshold + { + get + { + if (this.indexToSelect < 0) + return false; + + ListViewItem item = this.GetListViewItem(this.indexToSelect); + Rect bounds = VisualTreeHelper.GetDescendantBounds(item); + Point ptInItem = this.listView.TranslatePoint(this.ptMouseDown, item); + + // In case the cursor is at the very top or bottom of the ListViewItem + // we want to make the vertical threshold very small so that dragging + // over an adjacent item does not select it. + double topOffset = Math.Abs(ptInItem.Y); + double btmOffset = Math.Abs(bounds.Height - ptInItem.Y); + double vertOffset = Math.Min(topOffset, btmOffset); + + double width = SystemParameters.MinimumHorizontalDragDistance * 2; + double height = Math.Min(SystemParameters.MinimumVerticalDragDistance, vertOffset) * 2; + Size szThreshold = new Size(width, height); + + Rect rect = new Rect(this.ptMouseDown, szThreshold); + rect.Offset(szThreshold.Width / -2, szThreshold.Height / -2); + Point ptInListView = MouseUtilities.GetMousePosition(this.listView); + return !rect.Contains(ptInListView); + } + } + + #endregion // HasCursorLeftDragThreshold + + #region IndexUnderDragCursor + + /// + /// Returns the index of the ListViewItem underneath the + /// drag cursor, or -1 if the cursor is not over an item. + /// + int IndexUnderDragCursor + { + get + { + int index = -1; + for (int i = 0; i < this.listView.Items.Count; ++i) + { + ListViewItem item = this.GetListViewItem(i); + if (this.IsMouseOver(item)) + { + index = i; + break; + } + } + return index; + } + } + + #endregion // IndexUnderDragCursor + + #region InitializeAdornerLayer + + AdornerLayer InitializeAdornerLayer(ListViewItem itemToDrag) + { + // Create a brush which will paint the ListViewItem onto + // a visual in the adorner layer. + VisualBrush brush = new VisualBrush(itemToDrag); + + // Create an element which displays the source item while it is dragged. + this.dragAdorner = new DragAdorner(this.listView, itemToDrag.RenderSize, brush); + + // Set the drag adorner's opacity. + this.dragAdorner.Opacity = this.DragAdornerOpacity; + + AdornerLayer layer = AdornerLayer.GetAdornerLayer(this.listView); + layer.Add(dragAdorner); + + // Save the location of the cursor when the left mouse button was pressed. + this.ptMouseDown = MouseUtilities.GetMousePosition(this.listView); + + return layer; + } + + #endregion // InitializeAdornerLayer + + #region InitializeDragOperation + + void InitializeDragOperation(ListViewItem itemToDrag) + { + // Set some flags used during the drag operation. + this.IsDragInProgress = true; + this.canInitiateDrag = false; + + // Let the ListViewItem know that it is being dragged. + ListViewItemDragState.SetIsBeingDragged(itemToDrag, true); + } + + #endregion // InitializeDragOperation + + #region IsMouseOver + + bool IsMouseOver(Visual target) + { + // We need to use MouseUtilities to figure out the cursor + // coordinates because, during a drag-drop operation, the WPF + // mechanisms for getting the coordinates behave strangely. + + Rect bounds = VisualTreeHelper.GetDescendantBounds(target); + Point mousePos = MouseUtilities.GetMousePosition(target); + return bounds.Contains(mousePos); + } + + #endregion // IsMouseOver + + #region IsMouseOverScrollbar + + /// + /// Returns true if the mouse cursor is over a scrollbar in the ListView. + /// + bool IsMouseOverScrollbar + { + get + { + Point ptMouse = MouseUtilities.GetMousePosition(this.listView); + HitTestResult res = VisualTreeHelper.HitTest(this.listView, ptMouse); + if (res == null) + return false; + + DependencyObject depObj = res.VisualHit; + while (depObj != null) + { + if (depObj is ScrollBar) + return true; + + // VisualTreeHelper works with objects of type Visual or Visual3D. + // If the current object is not derived from Visual or Visual3D, + // then use the LogicalTreeHelper to find the parent element. + if (depObj is Visual || depObj is System.Windows.Media.Media3D.Visual3D) + depObj = VisualTreeHelper.GetParent(depObj); + else + depObj = LogicalTreeHelper.GetParent(depObj); + } + + return false; + } + } + + #endregion // IsMouseOverScrollbar + + #region ItemUnderDragCursor + + ItemType ItemUnderDragCursor + { + get { return this.itemUnderDragCursor; } + set + { + if (this.itemUnderDragCursor == value) + return; + + // The first pass handles the previous item under the cursor. + // The second pass handles the new one. + for (int i = 0; i < 2; ++i) + { + if (i == 1) + this.itemUnderDragCursor = value; + + if (this.itemUnderDragCursor != null) + { + ListViewItem listViewItem = this.GetListViewItem(this.itemUnderDragCursor); + if (listViewItem != null) + ListViewItemDragState.SetIsUnderDragCursor(listViewItem, i == 1); + } + } + } + } + + #endregion // ItemUnderDragCursor + + #region PerformDragOperation + + void PerformDragOperation() + { + ItemType selectedItem = this.listView.SelectedItem as ItemType; + DragDropEffects allowedEffects = DragDropEffects.Move | DragDropEffects.Move | DragDropEffects.Link; + if (DragDrop.DoDragDrop(this.listView, selectedItem, allowedEffects) != DragDropEffects.None) + { + // The item was dropped into a new location, + // so make it the new selected item. + this.listView.SelectedItem = selectedItem; + } + } + + #endregion // PerformDragOperation + + #region ShowDragAdornerResolved + + bool ShowDragAdornerResolved + { + get { return this.ShowDragAdorner && this.DragAdornerOpacity > 0.0; } + } + + #endregion // ShowDragAdornerResolved + + #region UpdateDragAdornerLocation + + void UpdateDragAdornerLocation() + { + if (this.dragAdorner != null) + { + Point ptCursor = MouseUtilities.GetMousePosition(this.ListView); + + double left = ptCursor.X - this.ptMouseDown.X; + + // 4/13/2007 - Made the top offset relative to the item being dragged. + ListViewItem itemBeingDragged = this.GetListViewItem(this.indexToSelect); + Point itemLoc = itemBeingDragged.TranslatePoint(new Point(0, 0), this.ListView); + double top = itemLoc.Y + ptCursor.Y - this.ptMouseDown.Y; + + this.dragAdorner.SetOffsets(left, top); + } + } + + #endregion // UpdateDragAdornerLocation + + #endregion // Private Helpers + } + + #endregion // ListViewDragDropManager + + #region ListViewItemDragState + + /// + /// Exposes attached properties used in conjunction with the ListViewDragDropManager class. + /// Those properties can be used to allow triggers to modify the appearance of ListViewItems + /// in a ListView during a drag-drop operation. + /// + public static class ListViewItemDragState + { + #region IsBeingDragged + + /// + /// Identifies the ListViewItemDragState's IsBeingDragged attached property. + /// This field is read-only. + /// + public static readonly DependencyProperty IsBeingDraggedProperty = + DependencyProperty.RegisterAttached( + "IsBeingDragged", + typeof(bool), + typeof(ListViewItemDragState), + new UIPropertyMetadata(false)); + + /// + /// Returns true if the specified ListViewItem is being dragged, else false. + /// + /// The ListViewItem to check. + public static bool GetIsBeingDragged(ListViewItem item) + { + return (bool)item.GetValue(IsBeingDraggedProperty); + } + + /// + /// Sets the IsBeingDragged attached property for the specified ListViewItem. + /// + /// The ListViewItem to set the property on. + /// Pass true if the element is being dragged, else false. + internal static void SetIsBeingDragged(ListViewItem item, bool value) + { + item.SetValue(IsBeingDraggedProperty, value); + } + + #endregion // IsBeingDragged + + #region IsUnderDragCursor + + /// + /// Identifies the ListViewItemDragState's IsUnderDragCursor attached property. + /// This field is read-only. + /// + public static readonly DependencyProperty IsUnderDragCursorProperty = + DependencyProperty.RegisterAttached( + "IsUnderDragCursor", + typeof(bool), + typeof(ListViewItemDragState), + new UIPropertyMetadata(false)); + + /// + /// Returns true if the specified ListViewItem is currently underneath the cursor + /// during a drag-drop operation, else false. + /// + /// The ListViewItem to check. + public static bool GetIsUnderDragCursor(ListViewItem item) + { + return (bool)item.GetValue(IsUnderDragCursorProperty); + } + + /// + /// Sets the IsUnderDragCursor attached property for the specified ListViewItem. + /// + /// The ListViewItem to set the property on. + /// Pass true if the element is underneath the drag cursor, else false. + internal static void SetIsUnderDragCursor(ListViewItem item, bool value) + { + item.SetValue(IsUnderDragCursorProperty, value); + } + + #endregion // IsUnderDragCursor + } + + #endregion // ListViewItemDragState + + #region ProcessDropEventArgs + + /// + /// Event arguments used by the ListViewDragDropManager.ProcessDrop event. + /// + /// The type of data object being dropped. + public class ProcessDropEventArgs : EventArgs where ItemType : class + { + #region Data + + ObservableCollection itemsSource; + ItemType dataItem; + int oldIndex; + int newIndex; + DragDropEffects allowedEffects = DragDropEffects.None; + DragDropEffects effects = DragDropEffects.None; + + #endregion // Data + + #region Constructor + + internal ProcessDropEventArgs( + ObservableCollection itemsSource, + ItemType dataItem, + int oldIndex, + int newIndex, + DragDropEffects allowedEffects) + { + this.itemsSource = itemsSource; + this.dataItem = dataItem; + this.oldIndex = oldIndex; + this.newIndex = newIndex; + this.allowedEffects = allowedEffects; + } + + #endregion // Constructor + + #region Public Properties + + /// + /// The items source of the ListView where the drop occurred. + /// + public ObservableCollection ItemsSource + { + get { return this.itemsSource; } + } + + /// + /// The data object which was dropped. + /// + public ItemType DataItem + { + get { return this.dataItem; } + } + + /// + /// The current index of the data item being dropped, in the ItemsSource collection. + /// + public int OldIndex + { + get { return this.oldIndex; } + } + + /// + /// The target index of the data item being dropped, in the ItemsSource collection. + /// + public int NewIndex + { + get { return this.newIndex; } + } + + /// + /// The drag drop effects allowed to be performed. + /// + public DragDropEffects AllowedEffects + { + get { return allowedEffects; } + } + + /// + /// The drag drop effect(s) performed on the dropped item. + /// + public DragDropEffects Effects + { + get { return effects; } + set { effects = value; } + } + + #endregion // Public Properties + } + + #endregion // ProcessDropEventArgs +} \ No newline at end of file diff --git a/ProgramQueuer/Helpers/MouseUtilities.cs b/ProgramQueuer/Helpers/MouseUtilities.cs new file mode 100755 index 0000000..5e5655e --- /dev/null +++ b/ProgramQueuer/Helpers/MouseUtilities.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Media; + +namespace WPF.JoshSmith.Controls.Utilities +{ + /// + /// Provides access to the mouse location by calling unmanaged code. + /// + /// + /// This class was written by Dan Crevier (Microsoft). + /// http://blogs.msdn.com/llobo/archive/2006/09/06/Scrolling-Scrollviewer-on-Mouse-Drag-at-the-boundaries.aspx + /// + public class MouseUtilities + { + [StructLayout(LayoutKind.Sequential)] + private struct Win32Point + { + public Int32 X; + public Int32 Y; + }; + + [DllImport("user32.dll")] + private static extern bool GetCursorPos(ref Win32Point pt); + + [DllImport("user32.dll")] + private static extern bool ScreenToClient(IntPtr hwnd, ref Win32Point pt); + + /// + /// Returns the mouse cursor location. This method is necessary during + /// a drag-drop operation because the WPF mechanisms for retrieving the + /// cursor coordinates are unreliable. + /// + /// The Visual to which the mouse coordinates will be relative. + public static Point GetMousePosition(Visual relativeTo) + { + Win32Point mouse = new Win32Point(); + GetCursorPos(ref mouse); + + // Using PointFromScreen instead of Dan Crevier's code (commented out below) + // is a bug fix created by William J. Roberts. Read his comments about the fix + // here: http://www.codeproject.com/useritems/ListViewDragDropManager.asp?msg=1911611#xx1911611xx + return relativeTo.PointFromScreen(new Point((double)mouse.X, (double)mouse.Y)); + + #region Commented Out + //System.Windows.Interop.HwndSource presentationSource = + // (System.Windows.Interop.HwndSource)PresentationSource.FromVisual( relativeTo ); + //ScreenToClient( presentationSource.Handle, ref mouse ); + //GeneralTransform transform = relativeTo.TransformToAncestor( presentationSource.RootVisual ); + //Point offset = transform.Transform( new Point( 0, 0 ) ); + //return new Point( mouse.X - offset.X, mouse.Y - offset.Y ); + #endregion // Commented Out + } + } +} \ No newline at end of file diff --git a/ProgramQueuer/Helpers/ValueConverterGroup.cs b/ProgramQueuer/Helpers/ValueConverterGroup.cs new file mode 100755 index 0000000..16bdc26 --- /dev/null +++ b/ProgramQueuer/Helpers/ValueConverterGroup.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Globalization; +using System.Reflection; +using System.Text; +using System.Windows.Data; + +namespace ProgramQueuer.Helpers +{ + /// + /// A value converter which contains a list of IValueConverters and invokes their Convert or ConvertBack methods + /// in the order that they exist in the list. The output of one converter is piped into the next converter + /// allowing for modular value converters to be chained together. If the ConvertBack method is invoked, the + /// value converters are executed in reverse order (highest to lowest index). Do not leave an element in the + /// Converters property collection null, every element must reference a valid IValueConverter instance. If a + /// value converter's type is not decorated with the ValueConversionAttribute, an InvalidOperationException will be + /// thrown when the converter is added to the Converters collection. + /// + [System.Windows.Markup.ContentProperty("Converters")] + public class ValueConverterGroup : IValueConverter + { + #region Data + + private readonly ObservableCollection converters = new ObservableCollection(); + private readonly Dictionary cachedAttributes = new Dictionary(); + + #endregion // Data + + #region Constructor + + public ValueConverterGroup() + { + this.converters.CollectionChanged += this.OnConvertersCollectionChanged; + } + + #endregion // Constructor + + #region Converters + + /// + /// Returns the list of IValueConverters contained in this converter. + /// + public ObservableCollection Converters + { + get { return this.converters; } + } + + #endregion // Converters + + #region IValueConverter Members + + object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + object output = value; + + for (int i = 0; i < this.Converters.Count; i++) + { + //Run the converter. + output = this.Converters[i].Convert(output, this.GetTargetType(i, targetType, true), parameter, culture); + if (output == Binding.DoNothing) + break; + } + + return output; + } + + object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + object output = value; + + for (int i = this.Converters.Count - 1; i > -1; i--) + { + //Run the converter. + output = this.Converters[i].Convert(output, this.GetTargetType(i, targetType, true), parameter, culture); + if (output == Binding.DoNothing) + break; + } + + return output; + } + + #endregion // IValueConverter Members + + #region Private Helpers + + #region GetTargetType + + /// + /// Returns the target type for a conversion operation. + /// + /// The index of the current converter about to be executed. + /// The 'targetType' argument passed into the conversion method. + /// Pass true if calling from the Convert method, or false if calling from ConvertBack. + protected virtual Type GetTargetType(int converterIndex, Type finalTargetType, bool convert) + { + // If the current converter is not the last/first in the list, + // get a reference to the next/previous converter. + IValueConverter nextConverter = null; + int nextIndex = converterIndex; + + //Grab the index for the next converter. + if (convert && nextIndex < this.Converters.Count - 1) + nextIndex++; + else if (!convert && nextIndex > 0) + nextIndex--; + + //Check to see if we have a new index. + if (nextIndex != converterIndex) + //Make sure the converter at our new index is not null. + if (this.Converters[nextIndex] == null) + throw new InvalidOperationException("The Converters collection of the ValueConverterGroup contains a null reference at index: " + nextIndex); + else + //Get our next converter. + nextConverter = this.Converters[nextIndex]; + + //Check if we have to go through another converter. If this (current) is the last converter then this will be null. + if (nextConverter != null) + { + var conversionAttribute = cachedAttributes[nextConverter]; + + // If the Convert method is going to be called, we need to use the SourceType of the next + // converter in the list. If ConvertBack is called, use the TargetType. + return convert ? conversionAttribute.SourceType : conversionAttribute.TargetType; + } + + // If the current converter is the last one to be executed return the target type passed into the conversion method. + return finalTargetType; + } + + #endregion // GetTargetType + + #region OnConvertersCollectionChanged + + void OnConvertersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // The 'Converters' collection has been modified, so validate that each value converter it now + // contains is decorated with ValueConversionAttribute and then cache the attribute value. + + IList convertersToProcess = null; + if (e.Action == NotifyCollectionChangedAction.Add || + e.Action == NotifyCollectionChangedAction.Replace) + { + convertersToProcess = e.NewItems; + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (IValueConverter converter in e.OldItems) + this.cachedAttributes.Remove(converter); + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + this.cachedAttributes.Clear(); + convertersToProcess = this.converters; + } + + if (convertersToProcess != null && convertersToProcess.Count > 0) + { + foreach (IValueConverter converter in convertersToProcess) + { + object[] attributes = converter.GetType().GetCustomAttributes(typeof(ValueConversionAttribute), false); + + if (attributes.Length != 1) + throw new InvalidOperationException("All value converters added to a ValueConverterGroup must be decorated with the ValueConversionAttribute attribute exactly once."); + + this.cachedAttributes.Add(converter, attributes[0] as ValueConversionAttribute); + } + } + } + + #endregion // OnConvertersCollectionChanged + + #endregion // Private Helpers + } +} diff --git a/ProgramQueuer/Helpers/ValueConverters.cs b/ProgramQueuer/Helpers/ValueConverters.cs new file mode 100755 index 0000000..43665f9 --- /dev/null +++ b/ProgramQueuer/Helpers/ValueConverters.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Windows.Data; +using System.Windows; + +namespace ProgramQueuer.Helpers +{ + [ValueConversion(typeof(bool), typeof(Visibility))] + public class BoolToVisibililty : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + bool visible = (bool)value; + if (visible) + return Visibility.Visible; + return Visibility.Hidden; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + Visibility visible = (Visibility)value; + if (visible == Visibility.Hidden) + return false; + return true; + } + } + + [ValueConversion(typeof(bool), typeof(bool))] + public class InvertBool : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !(bool)value; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !(bool)value; + } + } +} diff --git a/ProgramQueuer/MainWindow.xaml b/ProgramQueuer/MainWindow.xaml new file mode 100755 index 0000000..14b5701 --- /dev/null +++ b/ProgramQueuer/MainWindow.xaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ProgramQueuer/MainWindow.xaml.cs b/ProgramQueuer/MainWindow.xaml.cs new file mode 100755 index 0000000..9ba69c0 --- /dev/null +++ b/ProgramQueuer/MainWindow.xaml.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using ProgramQueuer.Queuer; +using WPF.JoshSmith.ServiceProviders.UI; + +namespace ProgramQueuer +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + EntryManager _manager; + + public MainWindow() + { + InitializeComponent(); + + _manager = new EntryManager(); + this.DataContext = _manager; + this.listPrograms.ItemsSource = _manager.QueueList; + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + new ListViewDragDropManager(this.listPrograms); + } + + private void GridSplitter_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e) + { + ProgramQueuer.Properties.Settings.Default.split_height = (int)((sender as GridSplitter).Parent as Grid).RowDefinitions[2].Height.Value; + } + + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + if (_manager.Working) + if (MessageBox.Show("Are you sure you want to stop current worker and batch and exit application?", "You sure you want to exit?", MessageBoxButton.YesNo) == MessageBoxResult.No) + { + e.Cancel = true; + return; + } + ProgramQueuer.Properties.Settings.Default.Save(); + } + + private void ButtonExit_Click(object sender, RoutedEventArgs e) + { + this.Close(); + } + + private void ButtonWork_Click(object sender, RoutedEventArgs e) + { + _manager.RunQueuer(); + } + + private void ButtonStop_Click(object sender, RoutedEventArgs e) + { + if (MessageBox.Show("Are you sure you want to stop current batch?", "Stop worker", MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + _manager.ForceStop(); + } + } + + private void ListView_DragEnter(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + e.Effects = DragDropEffects.Link; + else + e.Effects = DragDropEffects.None; + } + + private void ListView_Drop(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + string[] files = (string[])e.Data.GetData(DataFormats.FileDrop, false); + foreach (string file in files) + { + _manager.QueueList.Add(new ProgramEntry { Name = file , Status = "Queued"}); + } + } + } + + private void buttonRemove_Click(object sender, RoutedEventArgs e) + { + _manager.QueueList.Remove((sender as Control).DataContext as ProgramEntry); + } + + private void buttonStopCurrent_Click(object sender, RoutedEventArgs e) + { + if (MessageBox.Show("Are you sure you want to stop current running process and continue to next?", "Stop current worker?", MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + _manager.ForceStopCurrent(); + } + } + + private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + { + textboxStatus.CaretIndex = textboxStatus.Text.Length; + textboxStatus.ScrollToEnd(); + } + + private void buttonAdd_Click(object sender, RoutedEventArgs e) + { + + } + } +} diff --git a/ProgramQueuer/ProgramQueuer.csproj b/ProgramQueuer/ProgramQueuer.csproj new file mode 100755 index 0000000..f0a3638 --- /dev/null +++ b/ProgramQueuer/ProgramQueuer.csproj @@ -0,0 +1,142 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {B1825A4D-32CB-45E2-9CFE-77622DCC1641} + WinExe + Properties + ProgramQueuer + ProgramQueuer + v3.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + reports.ico + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + + + + + + + + + + + Settings.xaml + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + MainWindow.xaml + Code + + + Designer + MSBuild:Compile + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ProgramQueuer/Properties/AssemblyInfo.cs b/ProgramQueuer/Properties/AssemblyInfo.cs new file mode 100755 index 0000000..c18c9f5 --- /dev/null +++ b/ProgramQueuer/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ProgramQueuer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("ProgramQueuer")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ProgramQueuer/Properties/Resources.Designer.cs b/ProgramQueuer/Properties/Resources.Designer.cs new file mode 100755 index 0000000..88d729b --- /dev/null +++ b/ProgramQueuer/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.1 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ProgramQueuer.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ProgramQueuer.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/ProgramQueuer/Properties/Resources.resx b/ProgramQueuer/Properties/Resources.resx new file mode 100755 index 0000000..ffecec8 --- /dev/null +++ b/ProgramQueuer/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ProgramQueuer/Properties/Settings.Designer.cs b/ProgramQueuer/Properties/Settings.Designer.cs new file mode 100755 index 0000000..cb7f332 --- /dev/null +++ b/ProgramQueuer/Properties/Settings.Designer.cs @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.1 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ProgramQueuer.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("500")] + public double width { + get { + return ((double)(this["width"])); + } + set { + this["width"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("500")] + public double height { + get { + return ((double)(this["height"])); + } + set { + this["height"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("200")] + public double split_height { + get { + return ((double)(this["split_height"])); + } + set { + this["split_height"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("300")] + public double columnWidth1 { + get { + return ((double)(this["columnWidth1"])); + } + set { + this["columnWidth1"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("50")] + public double columnWidth2 { + get { + return ((double)(this["columnWidth2"])); + } + set { + this["columnWidth2"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("200")] + public double columnWidth3 { + get { + return ((double)(this["columnWidth3"])); + } + set { + this["columnWidth3"] = value; + } + } + } +} diff --git a/ProgramQueuer/Properties/Settings.settings b/ProgramQueuer/Properties/Settings.settings new file mode 100755 index 0000000..35c7044 --- /dev/null +++ b/ProgramQueuer/Properties/Settings.settings @@ -0,0 +1,24 @@ + + + + + + 500 + + + 500 + + + 200 + + + 300 + + + 50 + + + 200 + + + \ No newline at end of file diff --git a/ProgramQueuer/Queuer/EntryManager.cs b/ProgramQueuer/Queuer/EntryManager.cs new file mode 100755 index 0000000..4cada2e --- /dev/null +++ b/ProgramQueuer/Queuer/EntryManager.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; +using System.ComponentModel; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Text; + +namespace ProgramQueuer.Queuer +{ + public class EntryManager : INotifyPropertyChanged + { + bool _working; + bool _clearNext; + string _buffer; + ProgramEntry _currentEntry; + Process _currentProcess; + ProcessIOManager _processManager; + + public EntryManager() + { + QueueList = new ObservableCollection(); + _buffer = ""; + _currentEntry = null; + _processManager = new ProcessIOManager(); + _processManager.StderrTextRead += new StringReadEventHandler(_processManager_StdoutTextRead); + _processManager.StdoutTextRead += new StringReadEventHandler(_processManager_StdoutTextRead); + _currentProcess = new Process(); + _currentProcess.StartInfo.UseShellExecute = false; + _currentProcess.StartInfo.RedirectStandardOutput = true; + _currentProcess.StartInfo.RedirectStandardError = true; + _currentProcess.StartInfo.RedirectStandardInput = true; + _currentProcess.StartInfo.CreateNoWindow = true; + _currentProcess.EnableRaisingEvents = true; + _currentProcess.Exited += new EventHandler(_currentProcess_Exited); + } + + public ObservableCollection QueueList; + public event PropertyChangedEventHandler PropertyChanged = delegate { }; + + public ProgramEntry CurrentEntry + { + get { return _currentEntry; } + set + { + _currentEntry = value; + PropertyChanged(this, new PropertyChangedEventArgs("CurrentEntry")); + } + } + + public bool Working + { + get { return _working; } + set + { + _working = value; + PropertyChanged(this, new PropertyChangedEventArgs("Working")); + } + } + + public void RunQueuer() + { + if (QueueList.Count > 0) + { + Working = true; + RunProgram(QueueList[0]); + } + } + + public void ForceStopCurrent() + { + _currentProcess.Kill(); + } + + public void ForceStop() + { + this.Working = false; + _currentProcess.Kill(); + } + + private void RunProgram(ProgramEntry entry) + { + try + { + _currentEntry = entry; + _currentEntry.Output = ""; + _currentEntry.Working = true; + _currentEntry.Status = "Starting"; + _currentProcess.StartInfo.FileName = _currentEntry.Name; + _currentProcess.StartInfo.WorkingDirectory = new FileInfo(_currentEntry.Name).DirectoryName; + _currentProcess.Start(); + _processManager.RunningProcess = _currentProcess; + _processManager.StartProcessOutputRead(); + } + catch (Exception e) + { + _currentEntry.Working = false; + _currentEntry.Finished = false; + _currentEntry.Output += string.Format("Error while starting {0}:\n\n{1}", _currentEntry.Name, e.ToString()); + if (RunNext()) + { + _currentEntry = null; + this.Working = false; + } + } + } + + private bool RunNext() + { + _currentEntry.Status = "Finished"; + if (QueueList.IndexOf(_currentEntry) >= 0 && QueueList.IndexOf(_currentEntry) < QueueList.Count - 1) + RunProgram(QueueList[QueueList.IndexOf(_currentEntry) + 1]); + else + return true; + return false; + } + + void _currentProcess_Exited(object sender, EventArgs e) + { + _processManager.StopMonitoringProcessOutput(); + _currentEntry.Working = false; + _currentEntry.Finished = true; + if (!this.Working) + return; + if (RunNext()) + { + _currentEntry = null; + this.Working = false; + } + } + + void _processManager_StdoutTextRead(string text) + { + string[] lines = text.Split('\r'); + if (!text.EndsWith("\r") && !text.EndsWith("\n") && _clearNext) + { + _buffer += text; + return; + } + else + { + text = _buffer + text; + _buffer = ""; + } + + + if (_clearNext && text == "\n") + _clearNext = false; + + while (text.IndexOf('\b') >= 0) + { + if (_currentEntry.Output.Length > 0 && _currentEntry.Output[_currentEntry.Output.Length - 1] != '\n') + _currentEntry.Output = _currentEntry.Output.Remove(_currentEntry.Output.Length - 1); + text = text.Remove(text.IndexOf('\b'), 1); + } + + if (_clearNext && text.Replace("\n", "").Replace("\r", "").Trim() != "") + if (_currentEntry.Output.LastIndexOf('\n') < _currentEntry.Output.Length - 1) + _currentEntry.Output = _currentEntry.Output.Remove(_currentEntry.Output.LastIndexOf('\n') + 1) + text; + else + _currentEntry.Output += text; + else + _currentEntry.Output += text; + + if (text.Replace("\n", "").Trim() != "") + _currentEntry.Status = text.Replace("\n", "").Replace("\r", ""); + + if (lines.Length == 2 && lines[1] == "") + _clearNext = true; + else + _clearNext = false; + } + } +} diff --git a/ProgramQueuer/Queuer/ProcessIOManager.cs b/ProgramQueuer/Queuer/ProcessIOManager.cs new file mode 100755 index 0000000..cabd5a8 --- /dev/null +++ b/ProgramQueuer/Queuer/ProcessIOManager.cs @@ -0,0 +1,342 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; + +  +/*************************************************************************************** +* Author: Curt C. +* Email : harpyeaglecp@aol.com +* +* Project File: "Solving Problems of Monitoring Standard Output and Error Streams Of A Running Process" +* for http://www.codeproject.com +* +* This software is released under the Code Project Open License (CPOL) +* +* See official license description at: http://www.codeproject.com/info/cpol10.aspx +* +* This software is provided AS-IS without any warranty of any kind. +* +* >>> Please leave this header intact when using this file in other projects <<< +***************************************************************************************/ + +namespace ProgramQueuer.Queuer +{ + // Delegate definition used for events to receive notification + // of text read from stdout or stderr of a running process. + public delegate void StringReadEventHandler(string text); + + /// + /// Class that manages the reading of the output produced by a given 'Process' + /// and reports the output via events. + /// Both standard error (stderr) and standard output (stdout) are + /// managed and reported. + /// The stdout and stderr monitoring and reading are each performed + /// by separate background threads. Each thread blocks on a Read() + /// method, waiting for text in the stream being monitored to become available. + /// + /// Note the Process.RedirectStandardOutput must be set to true in order + /// to read standard output from the process, and the Process.RedirectStandardError + /// must be set to true to read standard error. + /// + public class ProcessIOManager + { + #region Private_Fields + // Command line process that is executing and being + // monitored by this class for stdout/stderr output. + private Process runningProcess; + + // Thread to monitor and read standard output (stdout) + private Thread stdoutThread; + + // Thread to monitor and read standard error (stderr) + private Thread stderrThread; + + // Buffer to hold characters read from either stdout, or stderr streams + private StringBuilder streambuffer; + #endregion + + #region Public_Properties_And_Events + /// + /// Gets the process being monitored + /// + /// The running process. + public Process RunningProcess + { + get { return runningProcess; } + set { runningProcess = value; } + } + + // Event to notify of a string read from stdout stream + public event StringReadEventHandler StdoutTextRead; + + // Event to notify of a string read from stderr stream + public event StringReadEventHandler StderrTextRead; + + #endregion + + #region Constructor_And_Initialization + + /// + /// Initializes a new instance of the class. + /// + /// + public ProcessIOManager() + { + this.streambuffer = new StringBuilder(256); + } + + /// + /// Initializes a new instance of the class. + /// + /// The process. + /// + /// Does not automatically start listening for stdout/stderr. + /// Call StartProcessOutputRead() to begin listening for process output. + /// + /// + public ProcessIOManager(Process process) + { + if (process == null) + throw new Exception("ProcessIoManager unable to set running process - null value is supplied"); + + if (process.HasExited == true) + throw new Exception("ProcessIoManager unable to set running process - the process has already existed."); + + this.runningProcess = process; + this.streambuffer = new StringBuilder(256); + } + + #endregion + + #region Public_Methods + /// + /// Starts the background threads reading any output produced (standard output, standard error) + /// that is produces by the running process. + /// + public void StartProcessOutputRead() + { + // Just to make sure there aren't previous threads running. + StopMonitoringProcessOutput(); + + // Make sure we have a valid, running process + CheckForValidProcess("Unable to start monitoring process output.", true); + + // If the stdout is redirected for the process, then start + // the stdout thread, which will manage the reading of stdout from the + // running process, and report text read via supplied events. + if (runningProcess.StartInfo.RedirectStandardOutput == true) + { + stdoutThread = new Thread(new ThreadStart(ReadStandardOutputThreadMethod)); + // Make thread a background thread - if it was foreground, then + // the thread could hang up the process from exiting. Background + // threads will be forced to stop on main thread termination. + stdoutThread.IsBackground = true; + stdoutThread.Start(); + } + + // If the stderr is redirected for the process, then start + // the stderr thread, which will manage the reading of stderr from the + // running process, and report text read via supplied events. + if (runningProcess.StartInfo.RedirectStandardError == true) + { + stderrThread = new Thread(new ThreadStart(ReadStandardErrorThreadMethod)); + stderrThread.IsBackground = true; + stderrThread.Start(); + } + } + + /// + /// Writes the supplied text string to the standard input (stdin) of the running process + /// + /// In order to be able to write to the Process, the StartInfo.RedirectStandardInput must be set to true. + /// The text to write to running process input stream. + public void WriteStdin(string text) + { + // Make sure we have a valid, running process + CheckForValidProcess("Unable to write to process standard input.", true); + if (runningProcess.StartInfo.RedirectStandardInput == true) + runningProcess.StandardInput.WriteLine(text); + } + #endregion + + #region Private_Methods + /// + /// Checks for valid (non-null Process), and optionally check to see if the process has exited. + /// Throws Exception if process is null, or if process has existed and checkForHasExited is true. + /// + /// The error message text to display if an exception is thrown. + /// if set to true [check if process has exited]. + private void CheckForValidProcess(string errorMessageText, bool checkForHasExited) + { + errorMessageText = (errorMessageText == null ? "" : errorMessageText.Trim()); + if (runningProcess == null) + throw new Exception(errorMessageText + " (Running process must be available)"); + + if (checkForHasExited && runningProcess.HasExited) + throw new Exception(errorMessageText + " (Process has exited)"); + } + + /// + /// Read characters from the supplied stream, and accumulate them in the + /// 'streambuffer' variable until there are no more characters to read. + /// + /// The first character that has already been read. + /// The stream reader to read text from. + /// if set to true the stream is assumed to be standard output, otherwise assumed to be standard error. + private void ReadStream(int firstCharRead, StreamReader streamReader, bool isstdout) + { + // One of the streams (stdout, stderr) has characters ready to be written + // Flush the supplied stream until no more characters remain to be read. + // The synchronized/ locked section of code to prevent the other thread from + // reading its stream at the same time, producing intermixed stderr/stdout results. + // If the threads were not synchronized, the threads + // could read from both stream simultaneously, and jumble up the text with + // stderr and stdout text intermixed. + lock (this) + { + // Single character read from either stdout or stderr + int ch; + // Clear the stream buffer to hold the text to be read + streambuffer.Length = 0; + + //Console.WriteLine("CHAR=" + firstCharRead); + streambuffer.Append((char)firstCharRead); + + // While there are more characters to be read + while (streamReader.Peek() > -1) + { + // Read the character in the queue + ch = streamReader.Read(); + + if (ch != '\n' && streambuffer.Length > 0 && streambuffer[streambuffer.Length - 1] == '\r') + NotifyAndFlushBufferText(streambuffer, isstdout); + + // Accumulate the characters read in the stream buffer + streambuffer.Append((char)ch); + + // Send text one line at a time - much more efficient than + // one character at a time + if (ch == '\n') + NotifyAndFlushBufferText(streambuffer, isstdout); + } + // Flush any remaining text in the buffer + NotifyAndFlushBufferText(streambuffer, isstdout); + } // End lock() + } + + /// + /// Invokes the OnStdoutTextRead (if isstdout==true)/ OnStderrTextRead events + /// with the supplied streambuilder 'textbuffer', then clears + /// textbuffer after event is invoked. + /// + /// The textbuffer containing the text string to pass to events. + /// if set to true, the stdout event is invoked, otherwise stedrr event is invoked. + private void NotifyAndFlushBufferText(StringBuilder textbuffer, bool isstdout) + { + if (textbuffer.Length > 0) + { + try + { + if (isstdout == true && StdoutTextRead != null) + { // Send notificatin of text read from stdout + StdoutTextRead(textbuffer.ToString()); + } + else if (isstdout == false && StderrTextRead != null) + { // Send notificatin of text read from stderr + StderrTextRead(textbuffer.ToString()); + } + } + catch (Exception e) + { + } + // 'Clear' the text buffer + textbuffer.Length = 0; + } + } + + /// + /// Method started in a background thread (stdoutThread) to manage the reading and reporting of + /// standard output text produced by the running process. + /// + private void ReadStandardOutputThreadMethod() + { + // Main entry point for thread - make sure the main entry method + // is surrounded with try catch, so an uncaught exception won't + // screw up the entire application + try + { + // character read from stdout + int ch; + + // The Read() method will block until something is available + while (runningProcess != null && (ch = runningProcess.StandardOutput.Read()) > -1) + { + // a character has become available for reading + // block the other thread and process this stream's input. + ReadStream(ch, runningProcess.StandardOutput, true); + } + } + catch (Exception ex) + { + Console.WriteLine("ProcessIoManager.ReadStandardOutputThreadMethod()- Exception caught:" + + ex.Message + "\nStack Trace:" + ex.StackTrace); + } + } + + /// + /// Method started in a background thread (stderrThread) to manage the reading and reporting of + /// standard error text produced by the running process. + /// + private void ReadStandardErrorThreadMethod() + { + try + { + // Character read from stderr + int ch; + // The Read() method will block until something is available + while (runningProcess != null && (ch = runningProcess.StandardError.Read()) > -1) + ReadStream(ch, runningProcess.StandardError, false); + } + catch (Exception ex) + { + Console.WriteLine("ProcessIoManager.ReadStandardErrorThreadMethod()- Exception caught:" + + ex.Message + "\nStack Trace:" + ex.StackTrace); + } + } + + /// + /// Stops both the standard input and stardard error background reader threads (via the Abort() method) + /// + public void StopMonitoringProcessOutput() + { + // Stop the stdout reader thread + try + { + if (stdoutThread != null) + stdoutThread.Abort(); + } + catch (Exception ex) + { + Console.WriteLine("ProcessIoManager.StopReadThreads()-Exception caught on stopping stdout thread.\n" + + "Exception Message:\n" + ex.Message + "\nStack Trace:\n" + ex.StackTrace); + } + + // Stop the stderr reader thread + try + { + if (stderrThread != null) + stderrThread.Abort(); + } + catch (Exception ex) + { + Console.WriteLine("ProcessIoManager.StopReadThreads()-Exception caught on stopping stderr thread.\n" + + "Exception Message:\n" + ex.Message + "\nStack Trace:\n" + ex.StackTrace); + } + stdoutThread = null; + stderrThread = null; + } + #endregion + } +} \ No newline at end of file diff --git a/ProgramQueuer/Queuer/ProgramEntry.cs b/ProgramQueuer/Queuer/ProgramEntry.cs new file mode 100755 index 0000000..86752f7 --- /dev/null +++ b/ProgramQueuer/Queuer/ProgramEntry.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ProgramQueuer.Queuer +{ + public class ProgramEntry : INotifyPropertyChanged + { + bool _error; + bool _working; + bool _finished; + string _name; + string _output; + string _status; + + public ProgramEntry() + { + Finished = false; + } + + public event PropertyChangedEventHandler PropertyChanged = delegate { }; + + public string Name + { + get { return _name; } + set + { + _name = value; + PropertyChanged(this, new PropertyChangedEventArgs("Name")); + } + } + + public string Output + { + get { return _output; } + set + { + _output = value; + PropertyChanged(this, new PropertyChangedEventArgs("Output")); + } + } + + public string Status + { + get { return _status; } + set + { + _status = value; + PropertyChanged(this, new PropertyChangedEventArgs("Status")); + } + } + + public bool Finished + { + get { return _finished; } + set + { + _finished = value; + PropertyChanged(this, new PropertyChangedEventArgs("Finished")); + } + } + + public bool Working + { + get { return _working; } + set + { + _working = value; + PropertyChanged(this, new PropertyChangedEventArgs("Working")); + } + } + + public bool Error + { + get { return _error; } + set + { + _error = value; + PropertyChanged(this, new PropertyChangedEventArgs("Error")); + } + } + } +} diff --git a/ProgramQueuer/Resources/accept.png b/ProgramQueuer/Resources/accept.png new file mode 100755 index 0000000000000000000000000000000000000000..219bc71a79d439182667d10aa4989e88dc722c01 GIT binary patch literal 984 zcmV;}11J26P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZvIXXoSFPOrta=4Ih7`WpzzJ>CPVp^{JFJCX-o)#>$(7lk>Ozg zq?B57Ds$pCt4I4Le+ph*xXDU&9e{d&gq@9@^lW*$oHoO!XYStaRVe3wfkJO@-PC_} z;d1xMtDhr;Bp!_7(LD&jvTc@1c^uc}-L4}%AMYBNn7-8`gs^oKv}Ib}IlFYZ`?X&` zB3J(aQLm0ixM%_Z)-8u8Tlb=rhgYt=50BHmzv+>wsk<|8>hXw?o6`!%PX7KGJJK1F zK1oos@k-Eyz;aEDXoBADhcSFHgm5{2?PIp@`{0-ni1y36c>STT=g#|Df`ZKt zi<;%IT2X|(Is60`rDp4c+1L*9es7>7 z8jc;*{c@Kp%q9Tgc;?NDJya;>&y}kiV@fG(+eRtHzXJdRJ7npG?+q&e0000_c# literal 0 HcmV?d00001 diff --git a/ProgramQueuer/Resources/add.png b/ProgramQueuer/Resources/add.png new file mode 100755 index 0000000000000000000000000000000000000000..0a975084692583b3412f38c5a7239d5d404386cb GIT binary patch literal 940 zcmV;d15^BoP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ1RCwA%lTBz`WfX>=^UXJN@7&B}CX<;oe>Ok0X{fr8(#BROl_H4P3N8$k zP!L>7pfup3*4VgkQ|U&*g(8GBP!wDUB6N{bsMS=1l2{W7iD_uk$RwGDWaiGyoqJp) z!2{>zUA#Pt!+YexJ0hP7693JikDiRW8m;>#pX)l#^U2g!;04a z2Pn3u^U&DM)rtO*U%o=bknvJ@u^1>6s}(kiPf=Aq?0bi|8cyAsUHE%IL;{OKcU#9t zH`gZmFW>)+-o|!Zs~kzSbM^QjwJ_xJy-!JbhEf1S_diEZ(SNS>*!=wC#Sd&lGS#~j z*&G^~`jQjPZR8w-#izT@(w%GtAm&I=O7_qAl6*Ott(QXItWUP6w%I z908i*&AV3?=&tW1VKW38vhEjYa!ORoQ&gO{&sd*Qvt^T>WP)=&?~#eOq5!G@IL0t^ zY;YF_Os_BT-P|DMut3F>n{7lOYCx!ABtZ9qh_FBVPWMz5Vg@1t)|0v?NhjB};m2pWyYwS18w%Fa8+8V+utn1!dw=K%+-+i9fNCz~1Rf_nn+tp1_T)p(xd` zgsKl*Aw*axOwnNOA>t7cVJE2ZVs-%W<&Jr}wK00K>CLheI|L}!xI|5ysBr#lx=Roa+TMPf} zR(T&l#M($xW6L$X;-$X|x4p&9pV%s|08sC=(wTml-qhJ*wP0_o{<}OJ1i|hz%}h@t zB1Ei3RdHkSqxFgO1?!sAp;TG{q!wMNl!CjPh5UHY-+8F21VMnR^4|a!?^21~;1k;b O0000(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-ut`KgRCwB)lfh~fK@f(&uBz_Y*=#nmS&fSz5lGt zauQK51`i@6kz{wLr`tpC3^?=VT}<_)U;PD|EpfAtF_JcPW(L6N z$Il!e9WwTYO&oadsq31`+idu(6@G(gQBf%bDWwu2XGDrs2!+tQgn0+Sd&boWZ$%WtNu zmG`JBM#2mT!85L_K@__VwLiDfdqY()2WCUmfPR3=}08(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZlCM*bvQHwDpm9~}&MC?$8vF&s^^F8mqF0`KYS)C)Etgj=4KuSqs3>nXx zE0-s3DxF$PjBNu*K{pP=$A^c955~T~C4|6Q3xEc2&f%<8?PlYym8m2K0n>8-cB{R&-|c=bwx2z_SgF-F9{%{NSDlz3m&+glr4)rifhY>G){-QaD2^Ei z0f)yYEM2=4?*IAfvMg81xA)&2dF4`(>O_fhsfaNNU$3pPy|azyc@zqH^0_RfLjHf} z?a_%hIa$4}b=q69_LQk+9p^wRg%E;XugBUq-?H=jb3VRym6@4olvW5Sm}=HA#^Pmj zOIk|VR7z4RYEb_Q!5W8cRZOG<03H3vG5!bkaBX1;$kAfL^UBnE(~R*O#;7r1)mL!^+5#y(LL zVXS2ogw!e%_~YRt@qBmpV!hek*n9oPOKF9dPJ?r3B`Fq*gi(kwmc$qmV+f;|Br$Z_ zweZy+dzWPp1Uvp{cz>o#y_MloR&Gu?cT!D0V?7=}Ai z2tmK!UrAwBJ5vpev5b75(b3Nw& xglGd;W4eLwKOPJQ4}u`ra?X(?3C=nG833RdlEfrpX0ZSO002ovPDHLkV1hM+mzw|p literal 0 HcmV?d00001 diff --git a/ProgramQueuer/Resources/remove.png b/ProgramQueuer/Resources/remove.png new file mode 100755 index 0000000000000000000000000000000000000000..7645cfa2e650416b685f06740882e0fdc0ce24f5 GIT binary patch literal 1004 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 literal 0 HcmV?d00001 diff --git a/ProgramQueuer/Resources/window.png b/ProgramQueuer/Resources/window.png new file mode 100755 index 0000000000000000000000000000000000000000..494cceca2d0b7fa1b047c8b7a0063463f25e495a GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP*5S+ zBgmJ5p-Pp3p`n?9;pcxK{gQ#9)PRBERRRNp)eHs(@%%~gN8NyGcY3-whE&{IGI=-e zAqN4M^lx?%hYqktFez~BM)mF zIkbw{4C)#cLH-)uLo*`3(1k*)7~`Qo{=4~cs3=wLl3_()K` zpJ8K)$pJ1!;ZE}to*vT@43req7!y1?I^Q1_5SU!RtlY4og|n+gqLckaNQ0o$=Iq@2 z{~r8)c;4lHRBd(G_u#*oyXscheOTf2Q=Z|onvU^u?j`eqo?!5F^>bP0l+XkKdBSZz literal 0 HcmV?d00001 diff --git a/ProgramQueuer/Settings.xaml b/ProgramQueuer/Settings.xaml new file mode 100755 index 0000000..c4865de --- /dev/null +++ b/ProgramQueuer/Settings.xaml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/ProgramQueuer/Settings.xaml.cs b/ProgramQueuer/Settings.xaml.cs new file mode 100755 index 0000000..3794ab3 --- /dev/null +++ b/ProgramQueuer/Settings.xaml.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace ProgramQueuer +{ + /// + /// Interaction logic for Settings.xaml + /// + public partial class Settings : Window + { + public Settings() + { + InitializeComponent(); + } + } +} diff --git a/ProgramQueuer/app.config b/ProgramQueuer/app.config new file mode 100755 index 0000000..b549024 --- /dev/null +++ b/ProgramQueuer/app.config @@ -0,0 +1,30 @@ + + + + +
+ + + + + + 500 + + + 500 + + + 200 + + + 300 + + + 50 + + + 200 + + + + \ No newline at end of file diff --git a/ProgramQueuer/reports.ico b/ProgramQueuer/reports.ico new file mode 100755 index 0000000000000000000000000000000000000000..7969ccbfa731cdf2cc94aa43062b8b64d9be98a4 GIT binary patch literal 111253 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

d*-S#o_8 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 literal 0 HcmV?d00001