/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.Drawing; using System.IO; using System.Reflection; using System.Text; using System.Windows.Forms; using JetBrains.DataStructures; using JetBrains.Omea.Base; using JetBrains.Omea.GUIControls; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.ResourceTools; using Tasks; namespace JetBrains.Omea.Tasks { [PluginDescription("Tasks", "JetBrains Inc.", "Tasks viewer and editor. Control task completion, alerts and statuses.", PluginDescriptionFormat.PlainText, "Icons/TasksPluginIcon.png")] public class TasksPlugin : IPlugin, IResourceDisplayer, IResourceTextProvider, IResourceUIHandler, IResourceDragDropHandler { #region IPlugin Members : Registration and Startup public void Register() { RegisterTypes(); Core.ResourceTreeManager.SetViewsExclusive( "Task" ); Core.TabManager.RegisterResourceTypeTab( "Tasks", "Tasks", new[]{ "Task" }, 99 ); Core.RightSidebar.RegisterPane( new TasksViewPane(), "ToDo", "To Do", Utils.TryGetEmbeddedResourceImageFromAssembly( Assembly.GetExecutingAssembly(), "Tasks.Icons.TODO24.png" ) ); Core.PluginLoader.RegisterResourceTextProvider( "Task", this ); Core.PluginLoader.RegisterResourceDisplayer( "Task", this ); Core.PluginLoader.RegisterResourceSerializer( "Task", new TaskSerializer() ); Core.PluginLoader.RegisterResourceUIHandler( "Task", this ); Core.PluginLoader.RegisterResourceDragDropHandler( Core.ResourceTreeManager.ResourceTreeRoot.Type, this ); // Root res Core.PluginLoader.RegisterResourceDragDropHandler( "Task", new DragDropLinkAdapter( this )); // Tasks Core.PluginLoader.RegisterDefaultThreadingHandler( "Task", _linkSuperTask ); Core.PluginLoader.RegisterViewsConstructor( new TasksUpgrade1ViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new TasksViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new TasksUpgrade2ViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new TasksUpgrade3ViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new TasksUpgrade4ViewsConstructor() ); //----------------------------------------------------------------- // Register Search Extensions to narrow the list of results using // simple phrases in search queries: for restricting the resource // type to Tasks (two synonyms). //----------------------------------------------------------------- Core.SearchQueryExtensions.RegisterResourceTypeRestriction( "in", "tasks", "Task" ); Core.SearchQueryExtensions.RegisterResourceTypeRestriction( "in", "task", "Task" ); Core.WorkspaceManager.WorkspaceChanged += WorkspaceManager_WorkspaceChanged; Core.WorkspaceManager.RegisterWorkspaceType( "Task", new int[] { }, WorkspaceResourceType.Container ); _ImageList.Images.Add( LoadIconFromAssembly( "Attached.ico" )); Core.ResourceIconManager.RegisterResourceLargeIcon( "Task", LoadIconFromAssembly( "TaskLarge.ico" )); Core.ResourceIconManager.RegisterOverlayIconProvider( "Task", new TaskOverlayIconProvider() ); IDisplayColumnManager colManager = Core.DisplayColumnManager; colManager.RegisterPropertyToTextCallback( _propStatus, TaskStatus2String); colManager.RegisterPropertyToTextCallback( _propCompletedDate, Date2String); ImageListColumn priorityCol = new PriorityColumn(); ImageListColumn statusCol = new StatusColumn(); colManager.RegisterCustomColumn( _propPriority, priorityCol ); colManager.RegisterCustomColumn( _propStatus, statusCol ); Core.ResourceBrowser.RegisterLinksPaneFilter( "Task", new TasksLinksPaneFilter() ); Core.PluginLoader.RegisterResourceDeleter( "Task", new DefaultResourceDeleter() ); } private void RegisterTypes() { IResourceStore store = Core.ResourceStore; _propDescription = store.PropTypes.Register("Description", PropDataType.String); store.ResourceTypes.Register("Task", "Task", "Subject", ResourceTypeFlags.ResourceContainer, this); _propStatus = store.PropTypes.Register("Status", PropDataType.Int); _propPriority = store.PropTypes.Register("Priority", PropDataType.Int); _propRemindDate = ResourceTypeHelper.UpdatePropTypeRegistration("RemindDate", PropDataType.Date, PropTypeFlags.AskSerialize); _propStartDate = ResourceTypeHelper.UpdatePropTypeRegistration("StartDate", PropDataType.Date, PropTypeFlags.Normal); store.PropTypes.RegisterDisplayName(_propStartDate, "Start Date"); _propCompletedDate = store.PropTypes.Register("CompletedDate", PropDataType.Date); store.PropTypes[_propCompletedDate].Flags = store.PropTypes[_propCompletedDate].Flags & ~PropTypeFlags.Internal; _propIsRoot = store.PropTypes.Register("IsRoot", PropDataType.Int, PropTypeFlags.Internal); _linkTarget = ResourceTypeHelper.UpdatePropTypeRegistration("Target", PropDataType.Link, PropTypeFlags.DirectedLink); _linkSuperTask = ResourceTypeHelper.UpdatePropTypeRegistration("SuperTask", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal); _propRemindWorkspace = ResourceTypeHelper.UpdatePropTypeRegistration("RemindWorkspace", PropDataType.Link, PropTypeFlags.Internal); /** * delete obsolete properties */ if(store.PropTypes.Exist("ReminderActive")) { IResourceList tasks = store.GetAllResources("Task"); foreach(IResource task in tasks) { if(task.HasProp("ReminderActive") && task.GetIntProp("ReminderActive") == 0) { task.DeleteProp(_propRemindDate); } } store.PropTypes.Delete(store.PropTypes["ReminderActive"].Id); } if(store.PropTypes.Exist("WorkspaceActivationReminder")) { store.PropTypes.Delete(store.PropTypes["WorkspaceActivationReminder"].Id); } store.PropTypes.RegisterDisplayName(_propCompletedDate, "Completed"); store.PropTypes.RegisterDisplayName(_linkTarget, "Task", "Resources"); } public void Startup() { CreateRootTask(); IResourceStore store = Core.ResourceStore; _allTasks = store.GetAllResourcesLive("Task").Minus( store.FindResourcesLive("Task", _propStatus, (int)TaskStatuses.Completed)); if(store.PropTypes.Exist("DueDate")) { foreach(IResource task in _allTasks) { DateTime dueDate = task.GetDateProp("DueDate"); if(dueDate > DateTime.MinValue) { task.SetProp(Core.Props.Date, dueDate); } } } _allTasks.ResourceAdded += _allTasks_ResourceAdded; _allTasks.ResourceChanged += _allTasks_ResourceUpdated; _allTasks.ResourceDeleting += _allTasks_ResourceDeleting; Core.StateChanged += Core_StateChanged; } bool IResourceTextProvider.ProcessResourceText(IResource task, IResourceTextConsumer consumer) { string subject = task.GetPropText(Core.Props.Subject); if(subject.Length > 0) { consumer.AddDocumentHeading(task.Id, subject); } string description = task.GetPropText(_propDescription); if(description.Length > 0) { consumer.AddDocumentFragment(task.Id, description); } return true; } public void Shutdown() { } #endregion IPlugin Members : Registration and Startup #region IResourceDisplayer Members public IDisplayPane CreateDisplayPane(string resourceType) { return new TaskDisplayPane(); } #endregion #region IResourceUIHandler Members public bool CanDropResources(IResource targetResource, IResourceList dragResources) { throw new NotImplementedException("Use ResourceDragDropHandler."); } public bool CanRenameResource(IResource res) { return false; } public void ResourcesDropped(IResource targetResource, IResourceList droppedResources) { throw new NotImplementedException("Use ResourceDragDropHandler."); } public bool ResourceRenamed(IResource res, string newName) { return false; } public void ResourceNodeSelected(IResource res) { } #endregion #region IResourceDragDropHandler Members /// /// Called to supply data in additional formats when the specified resources are being dragged. /// /// The dragged resources. /// The drag data object. public void AddResourceDragData(IResourceList dragResources, IDataObject dataObject) { if(!dataObject.GetDataPresent(typeof(string))) { StringBuilder sb = StringBuilderPool.Alloc(); try { foreach(IResource resource in dragResources) { if(sb.Length != 0) sb.Append(", "); string text = resource.DisplayName; if(text.IndexOf(' ') > 0) { sb.Append("\""); sb.Append(text); sb.Append("\""); } else sb.Append(text); } dataObject.SetData(sb.ToString()); } finally { StringBuilderPool.Dispose(sb); } } } /// /// Called to return the drop effect when the specified data object is dragged over the /// specified resource. /// /// The resource over which the drag happens. /// The containing the dragged data. /// The drag-and-drop operations which are allowed by the /// originator (or source) of the drag event. /// The current state of the SHIFT, CTRL, and ALT keys, /// as well as the state of the mouse buttons. /// The target drop effect. public DragDropEffects DragOver(IResource targetResource, IDataObject data, DragDropEffects allowedEffect, int keyState) { if(data.GetDataPresent(typeof(IResourceList))) // Dragging resources over { // The resources we're dragging IResourceList dragResources = (IResourceList)data.GetData(typeof(IResourceList)); // Restrict the allowed target res-types if(!((targetResource.Type == "Task") || (targetResource == Core.ResourceTreeManager.GetRootForType("Task")))) return DragDropEffects.None; // Collect all the direct and indirect parents of the droptarget; then we'll check to avoid dropping parent on its children IntHashSet ancestors = new IntHashSet(); IResource parent = targetResource; while(parent != null) { ancestors.Add(parent.Id); parent = parent.GetLinkProp(TasksPlugin._linkSuperTask); } // Measure some metrics on the dragged resources, don't allow mixing tasks/resources and prohibit the internal resources bool bAllTasks = true; bool bNoTasks = true; bool bNoInternal = true; foreach(IResource res in dragResources) { bAllTasks = bAllTasks && (res.Type == "Task"); bNoTasks = bNoTasks && (res.Type != "Task"); bNoInternal = bNoInternal && (!Core.ResourceStore.ResourceTypes[res.Type].HasFlag(ResourceTypeFlags.Internal)); if(ancestors.Contains(res.Id)) return DragDropEffects.None; // Dropping parent on a child } if(((!bAllTasks) && (!bNoTasks)) || (!bNoInternal)) return DragDropEffects.None; // Link attachments, move the tasks return bAllTasks ? DragDropEffects.Move : DragDropEffects.Link; } return DragDropEffects.None; } /// /// Called to handle the drop of the specified data object on the specified resource. /// /// The drop target resource. /// The containing the dragged data. /// The drag-and-drop operations which are allowed by the /// originator (or source) of the drag event. /// The current state of the SHIFT, CTRL, and ALT keys, /// as well as the state of the mouse buttons. public void Drop(IResource targetResource, IDataObject data, DragDropEffects allowedEffect, int keyState) { if(data.GetDataPresent(typeof(IResourceList))) { // The resources we're dragging IResourceList dragResources = (IResourceList)data.GetData(typeof(IResourceList)); if(dragResources.Count > 0) Core.ResourceAP.QueueJob(JobPriority.Immediate, "Drop Resources on a Task", new ResourcesDroppedDelegate(ResourcesDroppedImpl), targetResource, dragResources); } } #endregion #region implementation details private class RemindUOW : AbstractJob { private IResource _task; public RemindUOW(IResource task) { _task = task; } #region System.Object overrides public override int GetHashCode() { return _task.Id; } public override bool Equals(object x) { RemindUOW remindUOW = x as RemindUOW; return (remindUOW != null) && _task.Id == remindUOW._task.Id; } #endregion protected override void Execute() { if(_task.GetDateProp( _propRemindDate ) > DateTime.Now) { RemindAboutTask( _task ); } else if( !_task.IsDeleted && _task.GetIntProp( _propStatus ) != (int)TaskStatuses.Completed ) { Core.UIManager.QueueUIJob(new ResourceDelegate(ReminderForm.AddTask), _task); } } } internal static void CreateRootTask() { IResource newRoot = RootTask; Core.ResourceTreeManager.SetResourceNodeSort(newRoot, "Subject"); IResourceStore store = Core.ResourceStore; IResource oldRootTask; if((oldRootTask = store.FindUniqueResource("Task", _propIsRoot, 1)) != null) { oldRootTask.Delete(); IResourceList tasks = store.GetAllResources("Task"); foreach(IResource task in tasks) { task.SetProp(_linkTarget, newRoot); } } } internal static IResource RootTask { get { return Core.ResourceTreeManager.GetRootForType("Task"); } } private static void _allTasks_ResourceAdded(object sender, ResourceIndexEventArgs e) { RemindAboutTask(e.Resource); UpdateToDoCount(); } private static void _allTasks_ResourceUpdated(object sender, ResourcePropIndexEventArgs e) { RemindAboutTask(e.Resource); UpdateToDoCount(); } private static void _allTasks_ResourceDeleting(object sender, ResourceIndexEventArgs e) { IResource task = e.Resource; if(task.GetDateProp(_propRemindDate) != DateTime.MinValue) { Core.ResourceAP.CancelTimedJobs(new RemindUOW(task)); } UpdateToDoCount(); } private static void RemindAboutTask(IResource task) { if(task.GetDateProp(_propRemindDate) != DateTime.MinValue) { AbstractJob uow = new RemindUOW(task); Core.ResourceAP.CancelTimedJobs(uow); Core.ResourceAP.QueueJobAt(task.GetDateProp(_propRemindDate), uow); } } private static void WorkspaceManager_WorkspaceChanged(object sender, EventArgs e) { IResource workspace = Core.WorkspaceManager.ActiveWorkspace; if(workspace != null) { IResourceList tasks = workspace.GetLinksOfType( "Task", _propRemindWorkspace ); foreach(IResource task in tasks) { ReminderForm.AddTask( task ); } } UpdateToDoCount(); } private void Core_StateChanged(object sender, EventArgs e) { if(Core.State == CoreState.Running) { foreach(IResource task in _allTasks) { RemindAboutTask(task); } UpdateToDoCount(); } } private static string TaskStatus2String(IResource task, int propId) { if(task.Type == "Task") { int status = task.GetIntProp(propId); if(status < 0 || status >= _statuses.Length) { status = 0; } return _statuses[status]; } return string.Empty; } private static string Date2String(IResource res, int propID) { DateTime dt = res.GetDateProp(propID); if(dt == DateTime.MinValue) { return string.Empty; } if(dt.Date == DateTime.Today) { return dt.ToString("HH:mm:ss"); } return dt.ToString(); } internal static Icon LoadIconFromAssembly(string name) { Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Tasks.Icons." + name); return new Icon( stream ); } private static void UpdateToDoCount() { if(!Core.UserInterfaceAP.IsOwnerThread) { Core.UserInterfaceAP.QueueJob(new MethodInvoker(UpdateToDoCount)); return; } string caption = "To Do"; int count = 0; foreach(IResource res in Core.ResourceStore.GetAllResources("Task").ValidResources) { if(TasksViewPane._todoFilter.AcceptNode(res, 0)) { ++count; } } if(count > 0) { caption = caption + " (" + count + ")"; } Core.RightSidebar.SetPaneCaption("ToDo", caption); } #region Supertask parameters recalculation internal static void RecalculateSupertaskParameters(ArrayList parentTasks) { ArrayList doneRoots = new ArrayList(); foreach(IResource task in parentTasks) { IResource root = RootOfTask(task); if(doneRoots.IndexOf(root) == -1) { RecalculateTreeParameters(root); doneRoots.Add(root); } } } internal static IResource RootOfTask(IResource task) { IResource root = task.GetLinkProp(TasksPlugin._linkSuperTask); while(root != null && root.Id != TasksPlugin.RootTask.Id) { task = root; root = task.GetLinkProp(TasksPlugin._linkSuperTask); } if(root == null) throw new ApplicationException("TasksPane -- Illegal structure of the SuperTask/SubTask for task: " + task.DisplayName); return task; } internal static void RecalculateTreeParameters(IResource root) { DateTime start = DateTime.MaxValue, finish = DateTime.MinValue, dueDate = DateTime.MaxValue; bool isInProgress = false, isComplete = true; IResourceList subtasks = root.GetLinksTo(null, TasksPlugin._linkSuperTask); if(subtasks.Count > 0) { foreach(IResource task in subtasks) { RecalculateTreeParameters(task); DateTime taskDue = task.GetDateProp(Core.Props.Date); DateTime taskStart = task.GetDateProp(TasksPlugin._propStartDate); DateTime taskFinish = task.GetDateProp(TasksPlugin._propCompletedDate); if(dueDate < taskDue) dueDate = taskDue; if(taskStart < start) start = taskStart; if(taskFinish > finish) finish = taskFinish; int status = task.GetIntProp(TasksPlugin._propStatus); isComplete = isComplete && (status == 2); isInProgress = isInProgress || (status == 1); } if(isComplete) root.SetProp(TasksPlugin._propStatus, 2); else { root.DeleteProp(TasksPlugin._propCompletedDate); if(isInProgress) root.SetProp(TasksPlugin._propStatus, 1); else root.SetProp(TasksPlugin._propStatus, 0); } if(start != DateTime.MaxValue) root.SetProp(TasksPlugin._propStartDate, start); if(finish != DateTime.MinValue) root.SetProp(TasksPlugin._propCompletedDate, finish); if(dueDate != DateTime.MaxValue) root.SetProp(Core.Props.Date, dueDate); } } #endregion Supertask parameters recalculation /// /// Serializer for sending/receiving tasks. /// private class TaskSerializer : IResourceSerializer { public void AfterSerialize(IResource parentResource, IResource res, System.Xml.XmlNode node) { } public IResource AfterDeserialize(IResource parentResource, IResource res, System.Xml.XmlNode node) { res.AddLink(TasksPlugin._linkSuperTask, TasksPlugin.RootTask); return res; } public SerializationMode GetSerializationMode(IResource res, string propertyType) { return SerializationMode.Default; } } private class TasksLinksPaneFilter : ILinksPaneFilter { public bool AcceptLinkType(IResource displayedResource, int propId, ref string displayName) { return propId != TasksPlugin._linkTarget; } public bool AcceptLink(IResource displayedResource, int propId, IResource targetResource, ref string linkTooltip) { return true; } public bool AcceptAction(IResource displayedResource, IAction action) { return true; } } /// /// A delegate for queueing the function. /// protected delegate void ResourcesDroppedDelegate(IResource resTarget, IResourceList resDropped); /// /// An implementation-helper for the method that handles d'n'd in the Resource thread. /// protected static void ResourcesDroppedImpl( IResource resTarget, IResourceList resDropped) { // Separate the special case — new task creation by dropping on the root bool bNoTasks = true; foreach(string sResType in resDropped.GetAllTypes()) bNoTasks = bNoTasks && (sResType != "Task"); if((bNoTasks) && (resTarget == Core.ResourceTreeManager.GetRootForType("Task"))) { // Create a new task from the root-dropped (or empty-space-dropped) attachments Core.UserInterfaceAP.QueueJob(JobPriority.Immediate, "New Task from Dropped Attachments", new ExecuteActionDelegate(new NewTaskAction().Execute), new ActionContext(resDropped)); } else if(bNoTasks) { // Dropping attachments on a normal task AddDescendants(resTarget, resDropped, _linkTarget); } else { // Moving the tasks along the tree, change the parent AddDescendants(resTarget, resDropped, _linkSuperTask); /* foreach(IResource res in resDropped) { res.SetProp(TasksPlugin._linkTarget, resTarget); Core.WorkspaceManager.AddToActiveWorkspace(res); Core.WorkspaceManager.CleanWorkspaceLinks(res); } */ } } /// /// A delegate for the method. /// protected delegate void ExecuteActionDelegate(IActionContext context); /// /// Processes drop of something on the task. May either link-as-attachment or link-as-supertask. /// Must be called on the Resource thread. /// internal static void AddDescendants(IResource task, IResourceList descendants, int linkId) { ArrayList parentTasks = new ArrayList(); task.BeginUpdate(); try { foreach( IResource res in descendants ) { // Attached resources may be linked to many tasks, while // tasks may have only one supertask. if( res.Type != "Task" ) res.AddLink( linkId, task ); else { // Collect current parents of the tasks in order to // recalculate their stati on completeness and dates IResource parent = res.GetLinkProp(linkId); if(parent != null && parent.Id != TasksPlugin.RootTask.Id ) parentTasks.Add( parent ); res.SetProp( linkId, task ); } } } finally { task.EndUpdate(); } if( task.Id != TasksPlugin.RootTask.Id ) parentTasks.Add( task ); TasksPlugin.RecalculateSupertaskParameters( parentTasks ); } #endregion #region Attributes private IResourceList _allTasks; internal static int _propDescription; internal static int _propStatus; internal static int _propPriority; internal static int _propRemindDate; internal static int _propStartDate; internal static int _propCompletedDate; internal static int _propIsRoot; internal static int _linkTarget; internal static int _linkSuperTask; internal static int _propRemindWorkspace; internal static ImageList _ImageList = new ImageList(); internal static readonly string[] _statuses = new string[] { "Not Started", "In Progress", "Completed", "Waiting", "Deferred" }; internal static readonly string[] _priorities = new string[] { "Normal", "High", "Low" }; #endregion Attributes } #region Comparers /// /// Task priorities comparer. /// public class TasksComparerByPriority : IResourceComparer { public int CompareResources(IResource r1, IResource r2) { int priority1 = r1.GetIntProp("Priority"); int priority2 = r2.GetIntProp("Priority"); if(priority1 == priority2) { return 0; } if(priority1 != 0 && priority2 != 0) { return priority1 - priority2; } if(priority1 == 0) { return 3 - (priority2 * 2); } return (priority1 * 2) - 3; } } #endregion Comparers #region UI Handlers internal class TaskPriorityUIHandler : IStringTemplateParamUIHandler { string Value = string.Empty, Representation = string.Empty; public DialogResult ShowUI(IWin32Window h) { TaskPriorityForm form = new TaskPriorityForm(Value); DialogResult result = form.ShowDialog(h); Value = form.Value; Representation = form.Representation; form.Dispose(); return result; } public IResource Template { set { } } public string CurrentValue { set { Value = value; } } public string Result { get { return Value; } } public string DisplayString { get { return Representation; } } } #endregion UI Handlers internal class TaskOverlayIconProvider : IOverlayIconProvider { private Icon _attachmentIcon = null; private Icon[] _retVal; public Icon[] GetOverlayIcons( IResource res ) { if( _attachmentIcon == null ) LoadIcon(); return (res.GetLinksTo( null, TasksPlugin._linkTarget ).Count > 0) ? _retVal : null; } private void LoadIcon() { _attachmentIcon = TasksPlugin.LoadIconFromAssembly( "Attached.ico" ); _retVal = new Icon[ 1 ] { _attachmentIcon }; } } }