Java tutorial
/* * Copyright 2013 Hewlett-Packard Development Company, L.P * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hp.alm.ali.idea.content.taskboard; import com.hp.alm.ali.idea.cfg.TaskBoardConfiguration; import com.hp.alm.ali.idea.entity.EntityQuery; import com.hp.alm.ali.idea.entity.EntityRef; import com.hp.alm.ali.idea.entity.queue.QueryQueue; import com.hp.alm.ali.idea.entity.queue.QueryTarget; import com.hp.alm.ali.idea.ui.ComboItem; import com.hp.alm.ali.idea.ui.MultiValueSelectorLabel; import com.hp.alm.ali.idea.ui.QuickSearchPanel; import com.hp.alm.ali.idea.services.EntityService; import com.hp.alm.ali.idea.action.ActionUtil; import com.hp.alm.ali.idea.entity.EntityListener; import com.hp.alm.ali.idea.content.AliContentFactory; import com.hp.alm.ali.idea.ui.ReleaseChooser; import com.hp.alm.ali.idea.ui.SprintChooser; import com.hp.alm.ali.idea.services.SprintService; import com.hp.alm.ali.idea.ui.entity.EntityStatusPanel; import com.hp.alm.ali.idea.model.Entity; import com.hp.alm.ali.idea.model.parser.EntityList; import com.intellij.openapi.actionSystem.ActionPopupMenu; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.ui.components.labels.BoldLabel; import com.intellij.util.ui.UIUtil; import org.apache.commons.lang.StringUtils; import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SortOrder; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; public class TaskBoardPanel extends JPanel implements SprintService.Listener, EntityListener { public static final int BACKLOG_ITEM_PAGE_SIZE = 200; public static final int TASK_PAGE_SIZE = 1000; public static final int MIN_COLUMN_WIDTH = 250; public static final String PLACE = "HPALI.TaskBoard"; private static final List<String> allItemStatuses = Arrays.asList(BacklogItemPanel.ITEM_NEW, BacklogItemPanel.ITEM_IN_PROGRESS, BacklogItemPanel.ITEM_IN_TESTING, BacklogItemPanel.ITEM_DONE); private Project project; private EntityService entityService; private SprintService sprintService; private QueryQueue queue; private EntityStatusPanel status; private Header header; private ColumnHeader columnHeader; private Content content; private TaskPanel forcedTaskPanel; public TaskBoardPanel(final Project project) { super(new BorderLayout()); this.project = project; status = new EntityStatusPanel(project); queue = new QueryQueue(project, status, false); entityService = project.getComponent(EntityService.class); entityService.addEntityListener(this); sprintService = project.getComponent(SprintService.class); sprintService.addListener(this); loadTasks(); header = new Header(); columnHeader = new ColumnHeader(); content = new Content(); add(content, BorderLayout.NORTH); header.assignedTo.reload(); // force mouse-over task as visible (otherwise events are captured by the overlay and repaint quirks) Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent event) { if (isShowing() && event.getID() == MouseEvent.MOUSE_MOVED) { MouseEvent m = (MouseEvent) event; TaskPanel currentPanel = locateContainer(m, TaskPanel.class); if (currentPanel != null) { if (forcedTaskPanel == currentPanel) { return; } else if (forcedTaskPanel != null) { forcedTaskPanel.removeForcedMatch(this); } forcedTaskPanel = currentPanel; forcedTaskPanel.addForcedMatch(this); } else if (forcedTaskPanel != null) { forcedTaskPanel.removeForcedMatch(this); forcedTaskPanel = null; } } } }, AWTEvent.MOUSE_MOTION_EVENT_MASK); Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent event) { if (isShowing()) { MouseEvent m = (MouseEvent) event; switch (event.getID()) { case MouseEvent.MOUSE_PRESSED: case MouseEvent.MOUSE_RELEASED: // implement backlog item popup if (m.isPopupTrigger()) { final BacklogItemPanel itemPanel = locateContainer(m, BacklogItemPanel.class); if (itemPanel != null) { ActionPopupMenu popupMenu = ActionUtil.createEntityActionPopup("taskboard"); Point p = SwingUtilities.convertPoint(m.getComponent(), m.getPoint(), itemPanel); popupMenu.getComponent().show(itemPanel, p.x, p.y); } } break; case MouseEvent.MOUSE_CLICKED: // implement backlog item double click if (m.getClickCount() > 1) { BacklogItemPanel itemPanel = locateContainer(m, BacklogItemPanel.class); if (itemPanel != null) { Entity backlogItem = itemPanel.getItem(); Entity workItem = new Entity(backlogItem.getPropertyValue("entity-type"), Integer.valueOf(backlogItem.getPropertyValue("entity-id"))); AliContentFactory.loadDetail(project, workItem, true, true); } } } } } }, AWTEvent.MOUSE_EVENT_MASK); } private <T extends Component> T locateContainer(MouseEvent event, Class<T> clazz) { Point p = SwingUtilities.convertPoint((Component) event.getSource(), event.getPoint(), TaskBoardPanel.this); Component comp = SwingUtilities.getDeepestComponentAt(this, p.x, p.y); return (T) SwingUtilities.getAncestorOfClass(clazz, comp); } private void loadTasks() { Entity sprint = sprintService.getSprint(); Entity team = sprintService.getTeam(); if (sprint != null && team != null) { loadTasks(sprint, team, Collections.<Entity>emptyList(), Collections.<Entity>emptyList()); } } private void loadTasks(final Entity sprint, final Entity team, final List<Entity> previousBacklogItems, final List<Entity> previousTasks) { EntityQuery query = new EntityQuery("release-backlog-item"); query.setStartIndex(previousBacklogItems.size() + 1); query.setPageSize(BACKLOG_ITEM_PAGE_SIZE); query.setValue("is-leaf", "Y"); query.setValue("team-id", team.getPropertyValue("id")); query.setValue("sprint-id", String.valueOf(sprint.getId())); query.setPropertyResolved("is-leaf", true); query.setPropertyResolved("team-id", true); query.addOrder("rank", SortOrder.ASCENDING); query.addOrder("id", SortOrder.ASCENDING); queue.query(query, new QueryTarget() { @Override public void handleResult(EntityList list) { updateBacklogItems(sprint, team, list, previousBacklogItems, previousTasks); } }); } private void updateBacklogItems(Entity sprint, Entity team, final EntityList items, final List<Entity> previousBacklogItems, final List<Entity> previousTasks) { final Runnable redo = new Runnable() { @Override public void run() { loadTasks(); } }; UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { EntityList merged = EntityList.empty(); merged.addAll(previousBacklogItems); merged.addAll(items); content.retain(merged); if (merged.isEmpty()) { EntityList empty = EntityList.empty(); content.retainTasks(empty); status.loaded(merged, redo); } else if (items.isEmpty()) { // no more items loaded (only possible when paging is inconsistent) String backlogItemsCount = EntityStatusPanel.getItemCountString(previousBacklogItems.size(), previousBacklogItems.size(), "backlog items"); String tasksCount = EntityStatusPanel.getItemCountString(previousTasks.size(), previousTasks.size(), "tasks"); status.info("Loaded " + backlogItemsCount + " and " + tasksCount, null, redo, null); } else { status.info("Loaded " + EntityStatusPanel.getItemCountString(items.getTotal(), merged.size(), "backlog items") + ", loading tasks...", null, redo, null); for (int i = previousBacklogItems.size(); i < merged.size(); i++) { updateBacklogItem(merged.get(i), false, i); } } } }); if (!items.isEmpty()) { loadTasksChunk(sprint, team, items, previousBacklogItems, previousTasks.size(), previousTasks, 1, redo); } } private void loadTasksChunk(final Entity sprint, final Entity team, final EntityList backlogItems, final List<Entity> previousBacklogItems, final int previousTasksTotalCount, final List<Entity> previousTasks, final int startIndex, final Runnable redo) { EntityQuery query = new EntityQuery("project-task"); query.addOrder("id", SortOrder.ASCENDING); query.setOrValues("release-backlog-item-id", backlogItems.getIdStrings()); query.setStartIndex(startIndex); query.setPageSize(TASK_PAGE_SIZE); final EntityList tasks = entityService.query(query); UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { final EntityList merged = EntityList.empty(); merged.addAll(previousTasks); merged.addAll(tasks); content.retainTasks(merged); for (Entity task : tasks) { updateTask(task, false); } Runnable more = null; if (tasks.getTotal() > tasks.size() + startIndex - 1) { more = new Runnable() { @Override public void run() { loadTasksChunk(sprint, team, backlogItems, previousBacklogItems, previousTasksTotalCount, merged, startIndex + tasks.size(), redo); } }; } else if (backlogItems.getTotal() > previousBacklogItems.size() + backlogItems.size()) { more = new Runnable() { @Override public void run() { ArrayList<Entity> mergedItems = new ArrayList<Entity>(); mergedItems.addAll(previousBacklogItems); mergedItems.addAll(backlogItems); loadTasks(sprint, team, mergedItems, merged); } }; } String backlogItemsCount = EntityStatusPanel.getItemCountString(backlogItems.getTotal(), previousBacklogItems.size() + backlogItems.size(), "backlog items"); String tasksCount = EntityStatusPanel.getItemCountString(previousTasksTotalCount + tasks.getTotal(), merged.size(), "tasks"); status.info("Loaded " + backlogItemsCount + " and " + tasksCount, null, redo, more); } }); } private void loadTasksOfNewlyAddedBli(Entity entity) { EntityQuery query = new EntityQuery("project-task"); query.setValue("release-backlog-item-id", entity.getPropertyValue("id")); final EntityList tasks = entityService.query(query); UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { for (Entity task : tasks) { updateTask(task, false); } } }); } private void updateTask(Entity task, boolean created) { ApplicationManager.getApplication().assertIsDispatchThread(); content.updateTask(task, created); } private void removeTask(int id) { ApplicationManager.getApplication().assertIsDispatchThread(); content.removeTask(id); } private boolean updateBacklogItem(Entity item, boolean validate, int index) { ApplicationManager.getApplication().assertIsDispatchThread(); return content.updateItem(item, validate, index); } @Override public void onReleaseSelected(Entity release) { } @Override public void onSprintSelected(Entity sprint) { loadTasks(); } @Override public void onTeamSelected(Entity team) { loadTasks(); } public Component getStatusComponent() { return status; } public JComponent getHeader() { return header; } public JComponent getColumnHeader() { return columnHeader; } @Override public void entityLoaded(final Entity entity, final Event event) { if ("project-task".equals(entity.getType())) { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { updateTask(entity, event == Event.CREATE); } }); } else if ("release-backlog-item".equals(entity.getType())) { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { if (inThisSprint(entity)) { int index = content.getItemIndex(entity); if (updateBacklogItem(entity, true, index) && event != Event.CREATE) { // only load tasks for non-created BLIs only - events will follow for those we create ourselves ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { @Override public void run() { loadTasksOfNewlyAddedBli(entity); } }); } } else { content.removeBacklogItem(entity, true); } } }); } } @Override public void entityNotFound(final EntityRef ref, boolean removed) { if ("project-task".equals(ref.type)) { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { removeTask(ref.id); } }); } else if ("defect".equals(ref.type) || "requirement".equals(ref.type)) { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { content.removeBacklogItemOfWorkItem(ref); } }); } } private boolean inThisSprint(Entity entity) { Entity sprint = sprintService.getSprint(); Entity team = sprintService.getTeam(); return sprint != null && sprint.getPropertyValue("id").equals(entity.getPropertyValue("sprint-id")) && team != null && team.getPropertyValue("id").equals(entity.getPropertyValue("team-id")); } private class Header extends JPanel implements TaskBoardFilter { private QuickSearchPanel quickSearchPanel; private AssignedToComboBox assignedTo; private JCheckBox stories; private JCheckBox defects; private JCheckBox blocked; private MultiValueSelectorLabel statusFilter; public Header() { super(new BorderLayout()); final TaskBoardConfiguration conf = project.getComponent(TaskBoardConfiguration.class); JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0)); // using vertical spacing makes the bottom gap too wide (not sure why) toolbar.setBorder(BorderFactory.createEtchedBorder()); toolbar.add(new ReleaseChooser(project)); quickSearchPanel = new QuickSearchPanel(conf.getFilter(), new QuickSearchPanel.Target() { @Override public void executeFilter(String value) { conf.setFilter(value); content.applyFilter(); } }, true); quickSearchPanel.setBorder(new EmptyBorder(2, 2, 2, 0)); // ad-hoc fix to achieve same layout backlog content has toolbar.add(quickSearchPanel); SprintChooser sprintChooser = new SprintChooser(project); toolbar.add(sprintChooser); assignedTo = new AssignedToComboBox(project, conf.getAssignedTo()); assignedTo.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { conf.setAssignedTo(((ComboItem) assignedTo.getSelectedItem()).getKey().toString()); content.applyFilter(); } } }); toolbar.add(new JLabel("Assigned To:")); toolbar.add(assignedTo); String showStatuses = conf.getShowStatuses(); List<String> selectedItems; if (TaskBoardConfiguration.ALL_STATUSES.equals(showStatuses)) { selectedItems = allItemStatuses; } else { selectedItems = Arrays.asList(showStatuses.split(";")); } statusFilter = new MultiValueSelectorLabel(project, "Status", selectedItems, allItemStatuses); statusFilter.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { Set<String> selectedValues = statusFilter.getSelectedValues(); if (selectedValues.size() == allItemStatuses.size()) { conf.setShowStatuses(TaskBoardConfiguration.ALL_STATUSES); } else { conf.setShowStatuses(StringUtils.join(selectedValues, ";")); } content.applyFilter(); } }); toolbar.add(statusFilter); toolbar.add(new JLabel("Show:")); stories = new JCheckBox("User stories", conf.isShowUserStories()); stories.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { conf.setShowUserStories(stories.isSelected()); content.applyFilter(); } }); toolbar.add(stories); defects = new JCheckBox("Defects", conf.isShowDefects()); defects.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { conf.setShowDefects(defects.isSelected()); content.applyFilter(); } }); toolbar.add(defects); blocked = new JCheckBox("Blocked", conf.isShowBlocked()); blocked.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { conf.setShowBlocked(blocked.isSelected()); content.applyFilter(); } }); toolbar.add(blocked); add(toolbar, BorderLayout.NORTH); add(sprintChooser.getWarningPanel()); } @Override public String getFilter() { return quickSearchPanel.getValue(); } @Override public String getAssignedTo() { Object selectedItem = assignedTo.getSelectedItem(); if (selectedItem == null || AssignedToComboBox.ALL.equals(selectedItem)) { return null; } else if (AssignedToComboBox.UNASSIGNED.equals(selectedItem)) { return ""; } else { return ((ComboItem) selectedItem).getKey().toString(); } } @Override public boolean isUserStories() { return stories.isSelected(); } @Override public boolean isDefects() { return defects.isSelected(); } @Override public boolean isBlocked() { return blocked.isSelected(); } @Override public Set<String> getStatus() { return statusFilter.getSelectedValues(); } } private class ColumnHeader extends JPanel { public ColumnHeader() { super(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 0; c.gridy = 0; c.gridwidth = 1; c.weightx = 0; c.anchor = GridBagConstraints.NORTHWEST; JComponent rbiHeader = columnHeader("Backlog Item"); rbiHeader.setPreferredSize(new Dimension(BacklogItemPanel.DIMENSION.width, 26)); add(rbiHeader, c); c.fill = GridBagConstraints.BOTH; c.gridx++; c.weightx = 1; JPanel taskHeader = new JPanel(new GridLayout(1, 3)); taskHeader.add(columnHeader(TaskPanel.TASK_NEW)); taskHeader.add(columnHeader(TaskPanel.TASK_IN_PROGRESS)); taskHeader.add(columnHeader(TaskPanel.TASK_COMPLETED)); add(taskHeader, c); } } private class Content extends JPanel { private Map<Entity, BacklogItemPanel> items = new HashMap<Entity, BacklogItemPanel>(); public Content() { super(new GridBagLayout()); } public boolean updateItem(Entity item, boolean validate, int index) { GridBagConstraints c = new GridBagConstraints(); c.gridy = index; c.gridx = 0; c.fill = GridBagConstraints.VERTICAL; c.anchor = GridBagConstraints.NORTHEAST; c.insets = new Insets(0, 0, 1, 1); BacklogItemPanel backlogItemPanel = items.get(item); boolean created; if (backlogItemPanel == null) { backlogItemPanel = new BacklogItemPanel(project, item, header); items.put(item, backlogItemPanel); backlogItemPanel.applyFilter(); if (validate) { revalidate(); repaint(); final BacklogItemPanel newItemPanel = backlogItemPanel; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scrollRectToVisible(newItemPanel.getBounds()); } }); } created = true; } else { backlogItemPanel.update(item); created = false; } add(backlogItemPanel, c); c.gridx++; c.fill = GridBagConstraints.BOTH; c.weightx = 1; c.insets = new Insets(0, 0, 1, 0); add(backlogItemPanel.getTaskContent(), c); return created; } public int getItemIndex(Entity item) { BacklogItemPanel backlogItemPanel = items.get(item); if (backlogItemPanel != null) { return ((GridBagLayout) getLayout()).getConstraints(backlogItemPanel).gridy; } else { return items.size(); } } public void updateTask(Entity task, boolean created) { int itemId = Integer.valueOf(task.getPropertyValue("release-backlog-item-id")); BacklogItemPanel backlogItemPanel = items.get(new Entity("release-backlog-item", itemId)); if (backlogItemPanel != null) { backlogItemPanel.updateTask(task, created); } } public void removeTask(int id) { for (final BacklogItemPanel backlogItemPanel : items.values()) { TaskPanel taskPanel = backlogItemPanel.getTaskPanel(id); if (taskPanel != null) { backlogItemPanel.removeTaskPanel(taskPanel); } } } public void removeBacklogItem(Entity backlogItem, boolean validate) { BacklogItemPanel backlogItemPanel = items.remove(backlogItem); if (backlogItemPanel != null) { GridBagLayout gridBagLayout = (GridBagLayout) getLayout(); GridBagConstraints c = gridBagLayout.getConstraints(backlogItemPanel); remove(backlogItemPanel); remove(backlogItemPanel.getTaskContent()); // shift all items bellow us one row up for (BacklogItemPanel item : items.values()) { GridBagConstraints cc = gridBagLayout.getConstraints(item); if (cc.gridy-- > c.gridy) { gridBagLayout.setConstraints(item, cc); cc = gridBagLayout.getConstraints(item.getTaskContent()); cc.gridy--; gridBagLayout.setConstraints(item.getTaskContent(), cc); } } if (validate) { revalidate(); repaint(); } } } public void removeBacklogItemOfWorkItem(EntityRef entity) { for (Entity item : items.keySet()) { if (new EntityRef(item.getPropertyValue("entity-type"), Integer.valueOf(item.getPropertyValue("entity-id"))).equals(entity)) { removeBacklogItem(item, true); break; } } } public void retain(EntityList blis) { for (Entity bli : new LinkedList<Entity>(items.keySet())) { if (!blis.contains(bli)) { removeBacklogItem(bli, false); } } } public void retainTasks(EntityList tasks) { HashSet<Integer> ids = new HashSet<Integer>(tasks.getIds()); for (BacklogItemPanel bliPanel : items.values()) { bliPanel.retainTasks(ids); } } public void applyFilter() { for (BacklogItemPanel rbiPanel : items.values()) { rbiPanel.applyFilter(); } } } private JComponent columnHeader(String name) { BoldLabel label = new BoldLabel(name); label.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0, 1, 0, 0), BorderFactory.createEtchedBorder())); label.setPreferredSize(new Dimension(MIN_COLUMN_WIDTH, 26)); return label; } }