package org.nightlabs.jfire.issuetracking.ui.overview; import java.util.Collection; import javax.jdo.FetchGroup; import javax.jdo.FetchPlan; import javax.jdo.JDOHelper; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.PartInitException; import org.nightlabs.base.ui.job.Job; import org.nightlabs.base.ui.notification.NotificationAdapterJob; import org.nightlabs.base.ui.notification.SelectionManager; import org.nightlabs.base.ui.resource.SharedImages; import org.nightlabs.base.ui.table.AbstractTableComposite; import org.nightlabs.base.ui.util.RCPUtil; import org.nightlabs.jdo.NLJDOHelper; import org.nightlabs.jdo.query.QueryCollection; import org.nightlabs.jfire.base.jdo.GlobalJDOManagerProvider; import org.nightlabs.jfire.base.ui.overview.Entry; import org.nightlabs.jfire.base.ui.overview.search.JDOQuerySearchEntryViewer; import org.nightlabs.jfire.base.ui.security.UserSearchComposite; import org.nightlabs.jfire.base.ui.security.UserSearchDialog; import org.nightlabs.jfire.issue.Issue; import org.nightlabs.jfire.issue.dao.IssueDAO; import org.nightlabs.jfire.issue.id.IssueID; import org.nightlabs.jfire.issue.query.IssueQuery; import org.nightlabs.jfire.issuetracking.ui.IssueTrackingPlugin; import org.nightlabs.jfire.issuetracking.ui.issue.IssuePropertyDialog; import org.nightlabs.jfire.issuetracking.ui.issue.IssueTable; import org.nightlabs.jfire.issuetracking.ui.issue.editor.IssueEditor; import org.nightlabs.jfire.issuetracking.ui.issue.editor.IssueEditorInput; import org.nightlabs.jfire.issuetracking.ui.overview.action.DeleteIssueAction; import org.nightlabs.jfire.issuetracking.ui.resource.Messages; import org.nightlabs.jfire.jdo.notification.DirtyObjectID; import org.nightlabs.jfire.security.User; import org.nightlabs.jfire.table.config.IColumnConfiguration; import org.nightlabs.notification.NotificationEvent; import org.nightlabs.notification.NotificationListener; import org.nightlabs.progress.NullProgressMonitor; import org.nightlabs.progress.ProgressMonitor; import org.nightlabs.progress.SubProgressMonitor; /** * The view containing an {@link IssueTable}, listing all {@link Issue}s matching a given {@link IssueQuery}. * * @author Chairat Kongarayawetchakun * @author Marius Heinzmann - marius[at]nightlabs[dot]com * @author Khaireel Mohamed - khaireel at nightlabs dot de */ public class IssueEntryListViewer extends JDOQuerySearchEntryViewer<Issue, IssueQuery> { private QueryCollection<? extends IssueQuery> previousSavedQuery; public IssueEntryListViewer(Entry entry) { super(entry); } private IssueTable issueResultTable; private IColumnConfiguration columnConfiguration; private Job loadColumnConfigurationJob; @Override public AbstractTableComposite<Issue> createListComposite(final Composite parent) { // TODO we should pass the QueryMap obtained via this.getQueryMap() to the IssueTable so that it can filter new Issues agains it. issueResultTable = new IssueTable(parent, SWT.NONE, false); loadColumnConfigurationJob = new Job("Load Column Configuration") { @Override protected IStatus run(ProgressMonitor monitor) throws Exception { columnConfiguration = IssueTable.getDefaultColumnConfiguration(monitor); if (!parent.isDisposed()) { parent.getDisplay().syncExec(new Runnable() { @Override public void run() { if (issueResultTable.isDisposed()) return; issueResultTable.setIssueTableConfigurations(columnConfiguration); issueResultTable.layout(true, true); } }); } return Status.OK_STATUS; } }; loadColumnConfigurationJob.schedule(); // [Observation; 02.07.2009] // It seems a good idea to have a NotificationListener here, to note whether an // Issue has been deleted, so that we can remove it from the list of table. // We dont have to make this 'completely' active, since its showing the results of the current search... right?? // --> Let's give this a go first, and then decide later on. Kai. previousSavedQuery = null; GlobalJDOManagerProvider.sharedInstance().getLifecycleManager().addNotificationListener(Issue.class, issueChangeNotificationListener); issueResultTable.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent event) { GlobalJDOManagerProvider.sharedInstance().getLifecycleManager().removeNotificationListener(Issue.class, issueChangeNotificationListener); } }); issueResultTable.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { Issue issue = issueResultTable.getFirstSelectedElement(); // IssueDescriptionView issuePropertyView = (IssueDescriptionView)RCPUtil.findView(IssueDescriptionView.VIEW_ID); // IssueLinkView issueLinkView = (IssueLinkView)RCPUtil.findView(IssueLinkView.VIEW_ID); // IssueHistoryView issueHistoryView = (IssueHistoryView)RCPUtil.findView(IssueHistoryView.VIEW_ID); // // if (issuePropertyView != null) issuePropertyView.setIssue(issue); // if (issueLinkView != null) issueLinkView.setIssue(issue); // if (issueHistoryView != null) issueHistoryView.setIssue(issue); IssueID issueID = (IssueID)JDOHelper.getObjectId(issue); SelectionManager.sharedInstance().notify( new NotificationEvent(this, IssueTrackingPlugin.ZONE_PROPERTY, issueID, Issue.class) ); } }); return issueResultTable; } /** * An implicit listener to handle the refreshing of the table's entry (or entries), in the case * of a currently displayed Issue changed its fields or was deleted. */ private NotificationListener issueChangeNotificationListener = new NotificationAdapterJob() { @Override public void notify(final NotificationEvent event) { final ProgressMonitor monitor = getProgressMonitor(); monitor.beginTask(Messages.getString("org.nightlabs.jfire.issuetracking.ui.overview.IssueEntryListViewer.task.updatingIssues"), 100); //$NON-NLS-1$ try { if (previousSavedQuery != null && !getComposite().isDisposed()) getComposite().getDisplay().asyncExec(new Runnable() { @Override public void run() { Collection<Issue> issues = issueResultTable.getElements(); if (issues.isEmpty()) return; Collection<IssueID> issueIDs = NLJDOHelper.getObjectIDList(issues); for (Object obj : event.getSubjects()) { if (obj == null) continue; // In both DIRTY and DELETED cases, where the dirtyObjectID matches any of our table contents, // we want to refresh it again -- making sure that the refreshed contents adhere to the // constraints of the previous query. Kai. DirtyObjectID dirtyObjectID = (DirtyObjectID) obj; if ( issueIDs.contains(dirtyObjectID.getObjectID()) ) { final Collection<Issue> foundIssues = doSearch(previousSavedQuery, new SubProgressMonitor(monitor, 90)); if (loadColumnConfigurationJob.getResult() == null){ // job is not finished loadColumnConfigurationJob.addJobChangeListener(new JobChangeAdapter(){ @Override public void done(IJobChangeEvent event) { if (Status.OK_STATUS == event.getResult()){ getComposite().getDisplay().asyncExec(new Runnable() { @Override public void run() { issueResultTable.setInput(foundIssues); handleContextMenuItems(); } }); } super.done(event); } }); }else{ issueResultTable.setInput(foundIssues); handleContextMenuItems(); } break; } } } }); } finally { monitor.done(); } } }; @Override protected void addResultTableListeners(AbstractTableComposite<Issue> tableComposite) { super.addResultTableListeners(tableComposite); } public IssueTable getResultTable() { return issueResultTable; } @Override protected Collection<Issue> doSearch (QueryCollection<? extends IssueQuery> queryMap, ProgressMonitor monitor) { // We save the previous query; used later in the notification listener, in case we need // to refresh the table entries, given the query constraints. previousSavedQuery = queryMap; // We need to join the load column config job here in order to have the correct fetch-groups try { loadColumnConfigurationJob.join(); } catch (InterruptedException e) { logger.error("Could not join loadColumnConfigurationJob", e); } return IssueDAO.sharedInstance().getIssuesForQueries( queryMap, issueResultTable.getIssueTableFetchGroups(), // IssueTable.FETCH_GROUPS_ISSUE, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, monitor); } @Override public Class<Issue> getTargetType() { return Issue.class; } /** * The ID for the Quick search registry. */ public static final String QUICK_SEARCH_REGISTRY_ID = IssueEntryListViewer.class.getName(); @Override protected String getQuickSearchRegistryID() { return QUICK_SEARCH_REGISTRY_ID; } // -----------------------------------------------------------------------------------------------------------------------------------| // ---[ Context-menu handling ]-------------------------------------------------------------------------------------------------------| // -----------------------------------------------------------------------------------------------------------------------------------| private MenuManager menuMgr; private IssueEditAction editIssueAction; private IssueDeleteAction deleteIssueAction; private IssueResolveAllAction resolveAllAction; private IssueCloseAllAction closeAllAction; private IssueAssignAllAction assignAllAction; private IssueSetPropertyAction setPropertyAction; /** * Sets up the context menu to allow for the following operations: * 1. Edit selected Issue(s) --> Opens the Issue pages. * 2. Delete selected Issue(s) --> Invokes the delete dialogs. */ private void createContextMenu() { // Prepare the menu items. editIssueAction = new IssueEditAction(); deleteIssueAction = new IssueDeleteAction(); resolveAllAction = new IssueResolveAllAction(); closeAllAction = new IssueCloseAllAction(); assignAllAction = new IssueAssignAllAction(); setPropertyAction = new IssueSetPropertyAction(); // Relegate the menu-items to the menu-manager. menuMgr = getMenuManager(); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager m) { menuMgr.add(editIssueAction); menuMgr.add(deleteIssueAction); menuMgr.add(resolveAllAction); menuMgr.add(closeAllAction); menuMgr.add(assignAllAction); menuMgr.add(setPropertyAction); } }); // Create the menu and attach it to the IssueTable. Menu menu = menuMgr.createContextMenu(issueResultTable); issueResultTable.setMenu(menu); // <-- Hmm... this seems enough? Do we need to do this: 'getSite().registerContextMenu(menuMgr, issueTable);' // Define the behaviour of the menu items wrt the selections, or lack thereof, in the IssueTable. issueResultTable.addSelectionChangedListener(new ISelectionChangedListener(){ @Override public void selectionChanged(SelectionChangedEvent event) { handleContextMenuItems(); } }); } /** * Depending on what item(s) is(are) selected from the {@link IssueTable}, the context-menu item * should be correspondingly activated or deactivated. */ private void handleContextMenuItems() { assert editIssueAction != null && deleteIssueAction != null; // From the current behaviour, we can DELETE multiple Issues, but we can only EDIT one Issue. int ctr = issueResultTable.getSelectionCount(); editIssueAction.setEnabled( ctr > 0 ); // OR should we also allow multiple Issues to be edited by opening multiple editors? Kai. deleteIssueAction.setEnabled( ctr > 0 ); } @Override public Composite createComposite(Composite parent) { // Note: We can only setup the context-menu once the composite is created. // And even then, we should check that the MenuManager in the super class is not null. Composite resultComposite = super.createComposite(parent); if (getMenuManager() != null) createContextMenu(); return resultComposite; } // -----------------------------------------------------------------------------------------------------------------------------------| /** * Handles the action to remove the selected Issue(s). */ private class IssueDeleteAction extends Action { public IssueDeleteAction() { setId(IssueDeleteAction.class.getName()); setImageDescriptor(SharedImages.DELETE_16x16); setToolTipText("Delete selected Issue(s)"); setText("Delete selected Issue(s)"); } @Override public void run() { DeleteIssueAction deleteAction = new DeleteIssueAction(); Collection<IssueID> issueIDs = NLJDOHelper.getObjectIDList(issueResultTable.getSelectedElements()); deleteAction.setSelectedIssueIDs(issueIDs); deleteAction.run(); } } // -----------------------------------------------------------------------------------------------------------------------------------| /** * Handles the action to edit the selected Issue(s). */ private class IssueEditAction extends Action { public IssueEditAction() { setId(IssueEditAction.class.getName()); setImageDescriptor(SharedImages.EDIT_16x16); setToolTipText("Edit selected Issue(s)"); setText("Edit selected Issue(s)"); } @Override public void run() { Collection<IssueID> issueIDs = NLJDOHelper.getObjectIDList(issueResultTable.getSelectedElements()); for (IssueID issueID : issueIDs) try { RCPUtil.openEditor(new IssueEditorInput(issueID), IssueEditor.EDITOR_ID); } catch (PartInitException e) { throw new RuntimeException(e); } } } /** * Handles the action to set resolved to the selected Issue(s). */ private class IssueResolveAllAction extends Action { public IssueResolveAllAction() { setId(IssueResolveAllAction.class.getName()); setImageDescriptor(SharedImages.SAVE_16x16); setToolTipText("Resolve selected Issue(s)"); setText("Resolve selected Issue(s)"); } @Override public void run() { Collection<IssueID> issueIDs = NLJDOHelper.getObjectIDList(issueResultTable.getSelectedElements()); for (IssueID issueID : issueIDs) { IssueDAO.sharedInstance().signalIssue( issueID, "resolve", false, new String[] {FetchPlan.DEFAULT}, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, new NullProgressMonitor() ); } } } /** * Handles the action to set closed to the selected Issue(s). */ private class IssueCloseAllAction extends Action { public IssueCloseAllAction() { setId(IssueCloseAllAction.class.getName()); setImageDescriptor(SharedImages.DISCARD_16x16); setToolTipText("Close selected Issue(s)"); setText("Close selected Issue(s)"); } @Override public void run() { final Collection<IssueID> issueIDs = NLJDOHelper.getObjectIDList(issueResultTable.getSelectedElements()); Job job = new Job("Signal Issues..............") { @Override protected IStatus run(ProgressMonitor monitor) throws Exception { for (IssueID issueID : issueIDs) { IssueDAO.sharedInstance().signalIssue( issueID, "close", false, new String[] {FetchPlan.DEFAULT}, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, new NullProgressMonitor() ); } return Status.OK_STATUS; } }; job.setPriority(Job.SHORT); job.schedule(); } } /** * Handles the action to set assigned to the selected Issue(s). */ private class IssueAssignAllAction extends Action { public IssueAssignAllAction() { setId(IssueAssignAllAction.class.getName()); setImageDescriptor(SharedImages.EDIT_16x16); setToolTipText("Assign selected Issue(s)"); setText("Assign selected Issue(s)"); } @Override public void run() { final Collection<Issue> issues = issueResultTable.getSelectedElements(); UserSearchDialog userSearchDialog = new UserSearchDialog(getComposite().getShell(), null, UserSearchComposite.FLAG_TYPE_USER); int returnCode = userSearchDialog.open(); if (returnCode == Dialog.OK) { final User assigneeUser = userSearchDialog.getSelectedUser(); if (assigneeUser != null) { Job job = new Job("Signal Issues..............") { @Override protected IStatus run(ProgressMonitor monitor) throws Exception { for (Issue issue : issues) { issue.setAssignee(assigneeUser); IssueDAO.sharedInstance().storeIssue(issue, false, new String[] {FetchGroup.DEFAULT}, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, new NullProgressMonitor()); } return Status.OK_STATUS; } }; job.setPriority(Job.SHORT); job.schedule(); } } } } /** * Handles the action to change properties to the selected Issue(s). */ private class IssueSetPropertyAction extends Action { public IssueSetPropertyAction() { setId(IssueAssignAllAction.class.getName()); setImageDescriptor(SharedImages.EDIT_16x16); setToolTipText("Edit properties"); setText("Edit properties"); } @Override public void run() { final Collection<Issue> selectedIssues = issueResultTable.getSelectedElements(); final IssuePropertyDialog issuePropertyDialog = new IssuePropertyDialog(selectedIssues, getComposite().getShell()); int returnType = issuePropertyDialog.open(); if (returnType == Window.OK) { Job job = new Job(Messages.getString("org.nightlabs.jfire.issuetracking.ui.overview.IssueEntryListViewer.task.updatingIssues")) { @Override protected IStatus run(ProgressMonitor monitor) throws Exception { for (Issue issue : selectedIssues) { issue.setIssueType(issuePropertyDialog.getSelectedIssueType()); issue.setIssuePriority(issuePropertyDialog.getSelectedIssuePriority()); issue.setIssueSeverityType(issuePropertyDialog.getSelectedIssueSeverityType()); issue.setIssueResolution(issuePropertyDialog.getSelectedIssueResolution()); IssueDAO.sharedInstance().storeIssue(issue, false, new String[] {FetchGroup.DEFAULT}, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, new NullProgressMonitor()); } return Status.OK_STATUS; } }; job.setPriority(Job.LONG); job.schedule(); } } } // private static String[] FETCH_GROUP_FOR_ISSUE = new String[] {FetchGroup.DEFAULT, // Issue.FETCH_GROUP_ISSUE_TYPE, // Issue.FETCH_GROUP_ISSUE_PRIORITY, // Issue.FETCH_GROUP_ISSUE_SEVERITY_TYPE, // Issue.FETCH_GROUP_ISSUE_RESOLUTION}; }