/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.zkoss.ganttz.resourceload; import static org.zkoss.ganttz.i18n.I18nHelper._; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.joda.time.LocalDate; import org.zkoss.ganttz.IChartVisibilityChangedListener; import org.zkoss.ganttz.data.resourceload.LoadTimeLine; import org.zkoss.ganttz.timetracker.TimeTracker; import org.zkoss.ganttz.timetracker.TimeTrackerComponent; import org.zkoss.ganttz.timetracker.zoom.ZoomLevel; import org.zkoss.ganttz.util.ComponentsFinder; import org.zkoss.ganttz.util.Interval; import org.zkoss.ganttz.util.LongOperationFeedback; import org.zkoss.ganttz.util.LongOperationFeedback.ILongOperation; import org.zkoss.ganttz.util.MutableTreeModel; import org.zkoss.ganttz.util.WeakReferencedListeners; import org.zkoss.zk.au.out.AuInvoke; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.HtmlMacroComponent; import org.zkoss.zk.ui.event.Events; import org.zkoss.zul.Button; import org.zkoss.zul.Comboitem; import org.zkoss.zul.ListModel; import org.zkoss.zul.Separator; import org.zkoss.zul.SimpleListModel; import org.zkoss.zul.Combobox; import org.zkoss.zul.Listbox; import org.zkoss.zul.South; import org.zkoss.zul.Div; public class ResourcesLoadPanel extends HtmlMacroComponent { public interface IToolbarCommand { void doAction(); String getLabel(); String getImage(); } private TimeTrackerComponent timeTrackerComponent; private ResourceLoadLeftPane leftPane; private ResourceLoadList resourceLoadList; private List<LoadTimeLine> groups; private MutableTreeModel<LoadTimeLine> treeModel; private TimeTracker timeTracker; private LocalDate previousStart; private Interval previousInterval; private final Component componentOnWhichGiveFeedback; private WeakReferencedListeners<IFilterChangedListener> zoomListeners = WeakReferencedListeners.create(); private final String FILTER_RESOURCES = _("Resources"); private final String FILTER_CRITERIA = _("Generic allocation criteria"); private final String FILTER_BY_NAME_COMBO_COMPONENT = "filterByNameCombo"; private String feedBackMessage; private Boolean filterbyResources; private boolean refreshNameFilter = true; private int filterByNamePosition = 0; private int numberOfGroupsByName = 10; private int lastSelectedName = 0; private final PaginationType paginationType; private WeakReferencedListeners<IPaginationFilterChangedListener> nameFilterListener = WeakReferencedListeners.create(); private Component loadChart; private boolean visibleChart = true; private WeakReferencedListeners<IChartVisibilityChangedListener> chartVisibilityListeners = WeakReferencedListeners.create(); private final boolean expandResourceLoadViewCharts; private Component firstOptionalFilter; private Component secondOptionalFilter; private Combobox filterByNameCombo; public ResourcesLoadPanel(List<LoadTimeLine> groups, TimeTracker timeTracker, Component componentOnWhichGiveFeedback, boolean expandResourceLoadViewCharts, PaginationType paginationType) { this.componentOnWhichGiveFeedback = componentOnWhichGiveFeedback; this.expandResourceLoadViewCharts = expandResourceLoadViewCharts; this.paginationType = paginationType; init(groups, timeTracker); } public void init(List<LoadTimeLine> groups, TimeTracker timeTracker) { refreshNameFilter = true; this.groups = groups; this.timeTracker = timeTracker; treeModel = createModelForTree(); timeTrackerComponent = timeTrackerForResourcesLoadPanel(timeTracker); resourceLoadList = new ResourceLoadList(timeTracker, treeModel); leftPane = new ResourceLoadLeftPane(treeModel, resourceLoadList); } public TimeTracker getTimeTracker() { return timeTracker; } public ListModel<String> getFilters() { return new SimpleListModel<>(new String[] { FILTER_RESOURCES, FILTER_CRITERIA }); } public void setFilter(String filterBy) { if ( filterBy.equals(FILTER_RESOURCES) ) { this.filterbyResources = true; this.feedBackMessage = _("showing resources"); } else { this.filterbyResources = false; this.feedBackMessage = _("showing criteria"); } refreshNameFilter = true; filterByNamePosition = 0; invalidatingChangeHappenedWithFeedback(); } public boolean getFilter() { return (filterbyResources == null) ? true : filterbyResources; } private void invalidatingChangeHappenedWithFeedback() { LongOperationFeedback.execute(componentOnWhichGiveFeedback, new ILongOperation() { @Override public void doAction() { applyFilter(); } @Override public String getName() { return getFeedBackMessage(); } }); } private String getFeedBackMessage() { return feedBackMessage; } private void applyFilter() { zoomListeners.fireEvent(listener -> listener.filterChanged(getFilter())); } public void addFilterListener(IFilterChangedListener listener) { zoomListeners.addListener(listener); } public ListModel<ZoomLevel> getZoomLevels() { ZoomLevel[] selectableZoomlevels = { ZoomLevel.DETAIL_ONE, ZoomLevel.DETAIL_TWO, ZoomLevel.DETAIL_THREE, ZoomLevel.DETAIL_FOUR, ZoomLevel.DETAIL_FIVE }; return new SimpleListModel<>(selectableZoomlevels); } public void setZoomLevel(final ZoomLevel zoomLevel) { savePreviousData(); getTimeTrackerComponent().updateDayScroll(); timeTracker.setZoomLevel(zoomLevel); } public void zoomIncrease() { savePreviousData(); getTimeTrackerComponent().updateDayScroll(); timeTracker.zoomIncrease(); } public void zoomDecrease() { savePreviousData(); getTimeTrackerComponent().updateDayScroll(); timeTracker.zoomDecrease(); } private void savePreviousData() { TimeTracker timeTracker = getTimeTrackerComponent().getTimeTracker(); this.previousStart = timeTracker.getRealInterval().getStart(); this.previousInterval = timeTracker.getMapper().getInterval(); } public LocalDate getPreviousStart() { return previousStart; } public Interval getPreviousInterval() { return previousInterval; } public TimeTrackerComponent getTimeTrackerComponent() { return timeTrackerComponent; } public void add(final IToolbarCommand... commands) { Component toolbar = getToolbar(); resetToolbar(toolbar); Separator separator = getSeparator(); for (IToolbarCommand c : commands) { toolbar.insertBefore(asButton(c), separator); } } private void resetToolbar(Component toolbar) { List<Component> children = toolbar.getChildren(); List<Button> buttons = ComponentsFinder.findComponentsOfType(Button.class, children); for (Button b : buttons) { toolbar.removeChild(b); } } private Button asButton(final IToolbarCommand c) { Button result = new Button(); result.addEventListener(Events.ON_CLICK, event -> c.doAction()); if ( !StringUtils.isEmpty(c.getImage()) ) { result.setImage(c.getImage()); result.setTooltiptext(c.getLabel()); } else { result.setLabel(c.getLabel()); } return result; } @SuppressWarnings("unchecked") private Separator getSeparator() { List<Component> children = getToolbar().getChildren(); return ComponentsFinder.findComponentsOfType(Separator.class, children).get(0); } private Component getToolbar() { return getFellow("toolbar"); } private MutableTreeModel<LoadTimeLine> createModelForTree() { MutableTreeModel<LoadTimeLine> result = MutableTreeModel.create(LoadTimeLine.class); for (LoadTimeLine loadTimeLine : this.getGroupsToShow()) { result.addToRoot(loadTimeLine); result = addNodes(result, loadTimeLine); } return result; } private MutableTreeModel<LoadTimeLine> addNodes(MutableTreeModel<LoadTimeLine> tree, LoadTimeLine parent) { if ( !parent.getChildren().isEmpty() ) { tree.add(parent, parent.getChildren()); for (LoadTimeLine loadTimeLine : parent.getChildren()) { tree = addNodes(tree, loadTimeLine); } } return tree; } private TimeTrackerComponent timeTrackerForResourcesLoadPanel(TimeTracker timeTracker) { return new TimeTrackerComponent(timeTracker) { @Override protected void scrollHorizontalPercentage(int daysDisplacement) { response("", new AuInvoke(resourceLoadList, "scroll_horizontal", Integer.toString(daysDisplacement))); moveCurrentPositionScroll(); } @Override protected void moveCurrentPositionScroll() { // Get the previous data LocalDate previousStart = getPreviousStart(); // Get the current data int diffDays = getTimeTrackerComponent().getDiffDays(previousStart); double pixelPerDay = getTimeTrackerComponent().getPixelPerDay(); response("move_scroll", new AuInvoke( resourceLoadList, "move_scroll", Integer.toString(diffDays), Double.toString(pixelPerDay))); } protected void updateCurrentDayScroll() { double previousPixelPerDay = getTimeTracker().getMapper().getPixelsPerDay().doubleValue(); response( "update_day_scroll", new AuInvoke(resourceLoadList, "update_day_scroll", Double.toString(previousPixelPerDay))); } }; } @Override public void afterCompose() { super.afterCompose(); clearComponents(); getFellow("insertionPointLeftPanel").appendChild(leftPane); leftPane.afterCompose(); Div insertionPointRightPanel = (Div) getFellow("insertionPointRightPanel"); insertionPointRightPanel.appendChild(timeTrackerComponent); insertionPointRightPanel.appendChild(resourceLoadList); TimeTrackerComponent timeTrackerHeader = createTimeTrackerHeader(); getFellow("insertionPointTimetracker").appendChild(timeTrackerHeader); // Insert additional filters if any Component additionalFilter = getFirstOptionalFilter(); if ( additionalFilter != null ) { getFellow("additionalFilterInsertionPoint1").appendChild(additionalFilter); } additionalFilter = getSecondOptionalFilter(); if ( additionalFilter != null ) { getFellow("additionalFilterInsertionPoint2").appendChild(additionalFilter); } timeTrackerHeader.afterCompose(); timeTrackerComponent.afterCompose(); Listbox listZoomLevels = (Listbox) getFellow("listZoomLevels"); listZoomLevels.setSelectedIndex(timeTracker.getDetailLevel().ordinal()); filterByNameCombo = (Combobox) getFellow(FILTER_BY_NAME_COMBO_COMPONENT); filterByNameCombo.setWidth("85px"); if ( paginationType == PaginationType.INTERNAL_PAGINATION && refreshNameFilter ) { setupNameFilter(); } else if ( paginationType == PaginationType.NONE ) { getFellow(FILTER_BY_NAME_COMBO_COMPONENT).setVisible(false); getFellow("filterByNameLabel").setVisible(false); } getFellow("insertionPointChart").appendChild(loadChart); this.visibleChart = expandResourceLoadViewCharts; this.visibleChart = expandResourceLoadViewCharts; ((South) getFellow("graphics")).setOpen(this.visibleChart); if (!visibleChart) { ((South) getFellow("graphics")).setTitle(_("Graphics are disabled")); } savePreviousData(); } public Component getFirstOptionalFilter() { return firstOptionalFilter; } public void setFirstOptionalFilter(Component firstOptionalFilter) { this.firstOptionalFilter = firstOptionalFilter; } public Component getSecondOptionalFilter() { return secondOptionalFilter; } public void setSecondOptionalFilter(Component secondOptionalFilter) { this.secondOptionalFilter = secondOptionalFilter; } public void clearComponents() { getFellow("insertionPointLeftPanel").getChildren().clear(); getFellow("insertionPointRightPanel").getChildren().clear(); getFellow("insertionPointTimetracker").getChildren().clear(); getFellow("insertionPointChart").getChildren().clear(); } private TimeTrackerComponent createTimeTrackerHeader() { return new TimeTrackerComponent(timeTracker) { @Override protected void scrollHorizontalPercentage(int pixelsDisplacement) {} @Override protected void moveCurrentPositionScroll() {} @Override protected void updateCurrentDayScroll() {} }; } public void addSeeScheduledOfListener(ISeeScheduledOfListener seeScheduledOfListener) { leftPane.addSeeScheduledOfListener(seeScheduledOfListener); resourceLoadList.addSeeScheduledOfListener(seeScheduledOfListener); } private void setupNameFilter() { filterByNameCombo.getChildren().clear(); int size = groups.size(); if ( size > numberOfGroupsByName ) { int position = 0; while (position < size) { String firstName = groups.get(position).getConceptName(); String lastName; int newPosition = position + numberOfGroupsByName; if ( newPosition - 1 < size ) { lastName = groups.get(newPosition - 1).getConceptName(); } else { lastName = groups.get(size - 1).getConceptName(); } Comboitem item = new Comboitem(); item.setLabel(firstName.substring(0, 1) + " - " + lastName.substring(0, 1)); item.setDescription(firstName + " - " + lastName); item.setValue(position); filterByNameCombo.appendChild(item); position = newPosition; } } Comboitem lastItem = new Comboitem(); lastItem.setLabel(_("All")); lastItem.setDescription(_("Show all elements")); lastItem.setValue(-1); filterByNameCombo.appendChild(lastItem); filterByNameCombo.setSelectedIndex(0); refreshNameFilter = false; } /** * @return only the LoadTimeLine objects that have to be show according to the name filter. */ private List<LoadTimeLine> getGroupsToShow() { if ( paginationType != PaginationType.INTERNAL_PAGINATION || filterByNamePosition == -1 ) { return groups; } boolean condition = filterByNamePosition + numberOfGroupsByName < groups.size(); int endPosition = condition ? filterByNamePosition + numberOfGroupsByName : groups.size(); return groups.subList(filterByNamePosition, endPosition); } /** * Should be public! * Used in resourcesLoadLayout.zul */ public void onSelectFilterByName(Combobox comboByName) { if ( comboByName.getSelectedItem() == null ) { resetComboByName(comboByName); } else { Integer filterByNamePosition = comboByName.getSelectedItem().getValue(); if ( paginationType != PaginationType.NONE ) { this.filterByNamePosition = filterByNamePosition; this.lastSelectedName = comboByName.getSelectedIndex(); this.feedBackMessage = _("filtering by name"); changeNameFilterWithFeedback(); } } } private void resetComboByName(Combobox comboByName) { if ( this.lastSelectedName == -1 ) { comboByName.setSelectedIndex(0); } else { comboByName.setSelectedIndex(this.lastSelectedName); } comboByName.invalidate(); } private void changeNameFilterWithFeedback() { LongOperationFeedback.execute(componentOnWhichGiveFeedback, new ILongOperation() { @Override public void doAction() { if ( paginationType == PaginationType.INTERNAL_PAGINATION ) { // If the pagination is internal, we are in charge of repainting the graph treeModel = createModelForTree(); timeTrackerComponent = timeTrackerForResourcesLoadPanel(timeTracker); resourceLoadList = new ResourceLoadList(timeTracker, treeModel); leftPane = new ResourceLoadLeftPane(treeModel, resourceLoadList); } nameFilterListener.fireEvent(listener -> listener.filterChanged(filterByNamePosition)); afterCompose(); } @Override public String getName() { return getFeedBackMessage(); } }); } public void setInternalPaginationDisabled(boolean disabled) { Combobox combo = (Combobox) getFellow(FILTER_BY_NAME_COMBO_COMPONENT); if ( combo != null && combo.isDisabled() != disabled ) { filterByNamePosition = disabled? -1 : (Integer) combo.getSelectedItem().getValue(); combo.setDisabled(disabled); } } public void addPaginationFilterListener(IPaginationFilterChangedListener iFilterChangedListener) { nameFilterListener.addListener(iFilterChangedListener); } /** * It should be public! */ public void changeChartVisibility(boolean visible) { visibleChart = visible; chartVisibilityListeners.fireEvent(listener -> listener.chartVisibilityChanged(visibleChart)); } public boolean isVisibleChart() { return visibleChart; } public void addChartVisibilityListener(IChartVisibilityChangedListener chartVisibilityChangedListener) { chartVisibilityListeners.addListener(chartVisibilityChangedListener); } public void setLoadChart(Component loadChart) { this.loadChart = loadChart; } public int getPaginationFilterPageSize() { return numberOfGroupsByName; } public Combobox getPaginationFilterCombobox() { return paginationType == PaginationType.EXTERNAL_PAGINATION ? (Combobox) getFellow(FILTER_BY_NAME_COMBO_COMPONENT) : null; } public enum PaginationType { /** * Sets the widget to take care of the pagination of all the LoadTimeLine objects received. */ INTERNAL_PAGINATION, /** * The widget will only show the combo box but its content has to be configured externally. * The pagination has to be managed externally too: the widget will show all the LoadTimeLine objects received. */ EXTERNAL_PAGINATION, /** * Disables pagination. * Shows all the LoadTimeLine objects received. */ NONE } }