From 7c390b1d257ad4a0161c1689b73572b516720a4a Mon Sep 17 00:00:00 2001 From: Heng Sin Low Date: Thu, 10 Jul 2014 16:50:05 +0800 Subject: [PATCH] IDEMPIERE-2050 Improvement to Menu Lookup. --- .../webui/apps/DocumentSearchController.java | 365 +++++++++++++ .../adempiere/webui/apps/GlobalSearch.java | 147 ++++++ .../org/adempiere/webui/apps/MenuItem.java | 87 ++++ .../webui/apps/MenuSearchController.java | 481 ++++++++++++++++++ .../adempiere/webui/panel/HeaderPanel.java | 16 +- .../theme/default/css/theme.css.dsp | 12 + 6 files changed, 1102 insertions(+), 6 deletions(-) create mode 100644 org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java create mode 100644 org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java create mode 100644 org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuItem.java create mode 100644 org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java new file mode 100644 index 0000000000..e88cfaf869 --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java @@ -0,0 +1,365 @@ +/****************************************************************************** + * Copyright (C) 2014 Low Heng Sin * + * Copyright (C) 2014 Trek Global * + * This program is free software; you can redistribute it and/or modify it * + * under the terms version 2 of the GNU General Public License as published * + * by the Free Software Foundation. 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 General Public License for more details. * + * You should have received a copy of the GNU General Public License along * + * with this program; if not, write to the Free Software Foundation, Inc., * + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * + *****************************************************************************/ +package org.adempiere.webui.apps; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.adempiere.webui.component.Label; +import org.compiere.model.I_AD_SearchDefinition; +import org.compiere.model.MColumn; +import org.compiere.model.MLookup; +import org.compiere.model.MLookupFactory; +import org.compiere.model.MLookupInfo; +import org.compiere.model.MQuery; +import org.compiere.model.MRole; +import org.compiere.model.MSearchDefinition; +import org.compiere.model.MTable; +import org.compiere.model.MWindow; +import org.compiere.model.Query; +import org.compiere.util.DB; +import org.compiere.util.DisplayType; +import org.compiere.util.Env; +import org.compiere.util.Util; +import org.zkoss.zk.ui.Component; +import org.zkoss.zk.ui.event.Event; +import org.zkoss.zk.ui.event.EventListener; +import org.zkoss.zk.ui.event.Events; +import org.zkoss.zul.A; +import org.zkoss.zul.Textbox; +import org.zkoss.zul.Vlayout; + +/** + * @author hengsin + * + */ +public class DocumentSearchController implements EventListener{ + + private static final String SEARCH_RESULT = "search.result"; + private static final String ON_SEARCH_DOCUMENTS = "onSearchDocuments"; + private Vlayout layout; + + /** + * + */ + public DocumentSearchController() { + } + + public void create(Component parent) { + layout = new Vlayout(); + layout.setStyle("padding: 3px;"); + layout.setWidth("200px"); + + parent.appendChild(layout); + + layout.addEventListener(ON_SEARCH_DOCUMENTS, this); + } + + public void search(String value) { + layout.getChildren().clear(); + Events.echoEvent(ON_SEARCH_DOCUMENTS, layout, value); + } + + private void onSearchDocuments(String searchString) { + if (Util.isEmpty(searchString)) { + return; + } + + List list = doSearch(searchString); + if (list.size() > 0) { + Collections.sort(list, new Comparator() { + @Override + public int compare(SearchResult o1, SearchResult o2) { + int r = o1.getWindowName().compareTo(o2.getWindowName()); + if (r == 0) + r = o1.getLabel().compareTo(o2.getLabel()); + return r; + } + }); + String windowName = null; + for(SearchResult result : list) { + if (windowName == null || !windowName.equals(result.getWindowName())) { + windowName = result.getWindowName(); + Label label = new Label(windowName); + label.setStyle("padding: 3px; font-weight: bold; display: inline-block;"); + layout.appendChild(label); + } + A a = new A(); + a.setAttribute(SEARCH_RESULT, result); + a.setLabel(result.getLabel()); + layout.appendChild(a); + a.setStyle("padding-left: 3px; display: inline-block;"); + a.addEventListener(Events.ON_CLICK, this); + } + layout.invalidate(); + } + } + + private List doSearch(String searchString) { + final MRole role = MRole.get(Env.getCtx(), Env.getAD_Role_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), true); + + List list = new ArrayList(); + Query query = new Query(Env.getCtx(), I_AD_SearchDefinition.Table_Name, "", null); + List definitions = query.setOnlyActiveRecords(true).list(); + for(MSearchDefinition msd : definitions) { + MTable table = new MTable(Env.getCtx(), msd.getAD_Table_ID(), null); + StringBuilder sql = null; + MWindow window = msd.getAD_Window_ID() > 0 && role.getWindowAccess(msd.getAD_Window_ID()) != null ? MWindow.get(Env.getCtx(), msd.getAD_Window_ID()) : null; + MWindow powindow = msd.getPO_Window_ID() > 0 && role.getWindowAccess(msd.getPO_Window_ID()) != null ? MWindow.get(Env.getCtx(), msd.getPO_Window_ID()) : null; + if (window == null && powindow == null) + continue; + List params = new ArrayList(); + // SearchDefinition with a given table and column + if (msd.getSearchType().equals(MSearchDefinition.SEARCHTYPE_TABLE)) { + MColumn column = new MColumn(Env.getCtx(), msd.getAD_Column_ID(), null); + sql = new StringBuilder("SELECT ").append(table.getTableName()).append("_ID, ") + .append(column.getColumnName()); + sql.append(" FROM ") + .append(table.getTableName()) + .append(" "); + // search for an Integer + if (msd.getDataType().equals(MSearchDefinition.DATATYPE_INTEGER)) { + sql.append("WHERE ").append(column.getColumnName()).append("=?"); + // search for a String + } else { + sql.append("WHERE UPPER(").append(column.getColumnName()).append(") LIKE UPPER(?)"); + } + + // search for a Integer + if (msd.getDataType().equals(MSearchDefinition.DATATYPE_INTEGER)) { + params.add(Integer.valueOf(searchString.replaceAll("\\D", ""))); + // search for a String + } else if (msd.getDataType().equals(MSearchDefinition.DATATYPE_STRING)) { + if (searchString.endsWith("%")) + params.add(searchString); + else + params.add(searchString+"%"); + } + // SearchDefinition with a special query + } else if (msd.getSearchType().equals(MSearchDefinition.SEARCHTYPE_QUERY)) { + sql = new StringBuilder().append(msd.getQuery()); + // count '?' in statement + int count = 1; + for (char c : sql.toString().toCharArray()) { + if (c == '?') { + count++; + } + } + for (int i = 1; i < count; i++) { + if (msd.getDataType().equals(MSearchDefinition.DATATYPE_INTEGER)) { + params.add(Integer.valueOf(searchString.replaceAll("\\D", ""))); + } else if (msd.getDataType().equals(MSearchDefinition.DATATYPE_STRING)) { + if (searchString.endsWith("%")) + params.add(searchString); + else + params.add(searchString+"%"); + } + } + } + MLookupInfo lookupInfo = MLookupFactory.getLookupInfo(Env.getCtx(), -1, -1, DisplayType.Search, Env.getLanguage(Env.getCtx()), table.getTableName() + "_ID", 0, false, null); + MLookup lookup = new MLookup(lookupInfo, -1); + + if (sql != null) { + if (powindow != null) { + if (window != null) { + doRetrieval(msd, sql, params, lookup, window, table.getTableName(), " AND IsSOTrx='Y' ", list); + } + doRetrieval(msd, sql, params, lookup, powindow, table.getTableName(), " AND IsSOTrx='N' ", list); + } else if (window != null) { + doRetrieval(msd, sql, params, lookup, window, table.getTableName(), null, list); + } + + } + } + return list; + } + + private void doRetrieval(MSearchDefinition msd, StringBuilder builder, List params, MLookup lookup, MWindow window, String tableName, + String extraWhereClase, List list) { + PreparedStatement pstmt = null; + ResultSet rs = null; + try { + String sql = builder.toString(); + if (!Util.isEmpty(extraWhereClase)) + sql = sql + extraWhereClase; + pstmt = DB.prepareStatement(sql, (String)null); + if (params.size() > 0) + DB.setParameters(pstmt, params); + pstmt.setQueryTimeout(1); + rs = pstmt.executeQuery(); + int count = 0; + while (rs.next() && count < 3) { + count++; + int id = rs.getInt(1); + SearchResult result = new SearchResult(); + result.setLabel(lookup.getDisplay(id)); + result.setRecordId(id); + result.setWindowName(window.get_Translation("Name")); + result.setWindowId(window.getAD_Window_ID()); + + result.setTableName(tableName); + if (rs.getMetaData().getColumnCount() > 1) { + result.setName(rs.getString(2)); + } + list.add(result); + } + } catch (SQLException e) { + e.printStackTrace(); + } finally { + DB.close(rs, pstmt); + } + + } + + @Override + public void onEvent(Event event) throws Exception { + if (Events.ON_CLICK.equals(event.getName())) { + if (event.getTarget() instanceof A) { + SearchResult result = (SearchResult) event.getTarget().getAttribute(SEARCH_RESULT); + doZoom(result); + } + } else if (event.getName().equals(ON_SEARCH_DOCUMENTS)) { + onSearchDocuments((String)event.getData()); + } + } + + private void doZoom(SearchResult result) { + MQuery query = new MQuery(); + query.addRestriction(result.getTableName()+"_ID", "=", result.getRecordId()); + AEnv.zoom(result.getWindowId(), query); + } + + private class SearchResult { + private String windowName; + private int windowId; + private String tableName; + private int recordId; + private String label; + private String name; + + /** + * @return the windowId + */ + public int getWindowId() { + return windowId; + } + /** + * @param windowId the windowId to set + */ + public void setWindowId(int windowId) { + this.windowId = windowId; + } + /** + * @return the tableName + */ + public String getTableName() { + return tableName; + } + /** + * @param tableName the tableName to set + */ + public void setTableName(String tableName) { + this.tableName = tableName; + } + + /** + * @return the windowName + */ + public String getWindowName() { + return windowName; + } + /** + * @param windowName the windowName to set + */ + public void setWindowName(String windowName) { + this.windowName = windowName; + } + /** + * @return the recordId + */ + public int getRecordId() { + return recordId; + } + /** + * @param recordId the recordId to set + */ + public void setRecordId(int recordId) { + this.recordId = recordId; + } + /** + * @return the label + */ + public String getLabel() { + return label; + } + /** + * @param label the label to set + */ + public void setLabel(String label) { + this.label = label; + } + /** + * @return the name + */ + public String getName() { + return name; + } + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + } + + public boolean onOk(Textbox textbox) { + String text = textbox.getText(); + if (Util.isEmpty(text)) + return false; + text = text.toLowerCase(); + int size = layout.getChildren().size(); + A firstStart = null; + A exact = null; + for(int i = 0; i < size; i++) { + if (!(layout.getChildren().get(i) instanceof A)) continue; + A a = (A) layout.getChildren().get(i); + SearchResult result = (SearchResult) a.getAttribute(SEARCH_RESULT); + if (result.getLabel().equalsIgnoreCase(text)) { + exact = a; + break; + } else if (text.equalsIgnoreCase(result.getName())) { + exact = a; + break; + } else if (firstStart == null && result.getLabel().toLowerCase().startsWith(text) && text.length() >=3 ) { + firstStart = a; + } + } + + SearchResult result = null; + if (exact != null) + result = (SearchResult) exact.getAttribute(SEARCH_RESULT); + else if (firstStart != null) + result = (SearchResult) firstStart.getAttribute(SEARCH_RESULT); + if (result != null) { + doZoom(result); + } + + return false; + } +} diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java new file mode 100644 index 0000000000..1562fb77f3 --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java @@ -0,0 +1,147 @@ +/****************************************************************************** + * Copyright (C) 2014 Low Heng Sin * + * Copyright (C) 2014 Trek Global * + * This program is free software; you can redistribute it and/or modify it * + * under the terms version 2 of the GNU General Public License as published * + * by the Free Software Foundation. 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 General Public License for more details. * + * You should have received a copy of the GNU General Public License along * + * with this program; if not, write to the Free Software Foundation, Inc., * + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * + *****************************************************************************/ +package org.adempiere.webui.apps; + +import org.adempiere.webui.component.Bandbox; +import org.zkoss.zk.ui.event.Event; +import org.zkoss.zk.ui.event.EventListener; +import org.zkoss.zk.ui.event.Events; +import org.zkoss.zk.ui.event.InputEvent; +import org.zkoss.zk.ui.event.KeyEvent; +import org.zkoss.zk.ui.util.Clients; +import org.zkoss.zul.Bandpopup; +import org.zkoss.zul.Div; +import org.zkoss.zul.Hlayout; +import org.zkoss.zul.Separator; + +/** + * @author hengsin + * + */ +public class GlobalSearch extends Div implements EventListener { + + private static final String ON_ENTER_KEY = "onEnterKey"; + + private static final String ON_POST_ENTER_KEY = "onPostEnterKey"; + + private static final String ON_CREATE_ECHO = "onCreateEcho"; + + private static final String ON_SEARCH = "onSearch"; + + /** + * generated serial id + */ + private static final long serialVersionUID = -8793878697269469837L; + + private Bandbox bandbox; + + private MenuSearchController menuController; + private DocumentSearchController docController; + + /** + * + */ + public GlobalSearch(MenuSearchController menuController) { + this.menuController = menuController; + docController = new DocumentSearchController(); + init(); + } + + private void init() { + bandbox = new Bandbox(); + appendChild(bandbox); + bandbox.setWidth("100%"); + bandbox.setAutodrop(true); + bandbox.addEventListener(Events.ON_CHANGING, this); + bandbox.setCtrlKeys("#up#down"); + bandbox.addEventListener(Events.ON_CTRL_KEY, this); + + Bandpopup popup = new Bandpopup(); + popup.setWidth("700px"); + popup.setHeight("600px"); + bandbox.appendChild(popup); + + Hlayout hlayout = new Hlayout(); + hlayout.setHflex("1"); + popup.appendChild(hlayout); + menuController.create(hlayout); + + Separator separator = new Separator(); + separator.setHeight("100%"); + separator.setBar(true); + separator.setHflex("0"); + separator.setOrient("horizontal"); + hlayout.appendChild(separator); + docController.create(hlayout); + + addEventListener(ON_SEARCH, this); + addEventListener(ON_CREATE_ECHO, this); + bandbox.addEventListener(ON_ENTER_KEY, this); + addEventListener(ON_POST_ENTER_KEY, this); + + Events.echoEvent(ON_CREATE_ECHO, this, null); + } + + @Override + public void onEvent(Event event) throws Exception { + if (Events.ON_CHANGING.equals(event.getName())) { + InputEvent inputEvent = (InputEvent) event; + String value = inputEvent.getValue(); + Events.postEvent(ON_SEARCH, this, value); + } else if (Events.ON_CTRL_KEY.equals(event.getName())) { + KeyEvent ke = (KeyEvent) event; + if (ke.getKeyCode() == KeyEvent.UP) { + if (bandbox.getFirstChild().isVisible()) { + MenuItem selected = menuController.selectPrior(); + if (selected != null) { + bandbox.setText(selected.getLabel()); + } + } + } else if (ke.getKeyCode() == KeyEvent.DOWN) { + if (bandbox.getFirstChild().isVisible()) { + MenuItem selected = menuController.selectNext(); + if (selected != null && !"...".equals(selected.getType())) { + bandbox.setText(selected.getLabel()); + } + } + } + } else if (event.getName().equals(ON_SEARCH)) { + String value = (String) event.getData(); + menuController.search(value); + docController.search(value); + bandbox.focus(); + } else if (event.getName().equals(ON_CREATE_ECHO)) { + StringBuilder script = new StringBuilder("jq('#") + .append(bandbox.getUuid()) + .append("').bind('keydown', function(e) {var code=e.keyCode||e.which;console.log(code);if(code==13){") + .append("var widget=zk.Widget.$('#").append(bandbox.getUuid()).append("');") + .append("var event=new zk.Event(widget,'") + .append(ON_ENTER_KEY) + .append("',{},{toServer:true});") + .append("zAu.send(event);") + .append("}});"); + Clients.evalJavaScript(script.toString()); + } else if (event.getName().equals(ON_ENTER_KEY)) { + Clients.showBusy(bandbox, null); + Events.echoEvent(ON_POST_ENTER_KEY, this, null); + } else if (event.getName().equals(ON_POST_ENTER_KEY)) { + Clients.clearBusy(bandbox); + if (menuController.onOk(bandbox)) { + return; + } else { + docController.onOk(bandbox); + } + } + } +} diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuItem.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuItem.java new file mode 100644 index 0000000000..48874cfed4 --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuItem.java @@ -0,0 +1,87 @@ +/** + * + */ +package org.adempiere.webui.apps; + +/** + * @author hengsin + * + */ +public class MenuItem { + + private String label; + private String description; + private String image; + private String type; + private Object data; + + /** + * + */ + public MenuItem() { + } + + /** + * @return the label + */ + public String getLabel() { + return label; + } + + /** + * @param label the label to set + */ + public void setLabel(String label) { + this.label = label; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description the description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @return the image + */ + public String getImage() { + return image; + } + + /** + * @param image the image to set + */ + public void setImage(String image) { + this.image = image; + } + + public void setData(Object data) { + this.data = data; + } + + public Object getData() { + return data; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java new file mode 100644 index 0000000000..d4ab059688 --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java @@ -0,0 +1,481 @@ +/****************************************************************************** + * Copyright (C) 2014 Low Heng Sin * + * Copyright (C) 2014 Trek Global * + * This program is free software; you can redistribute it and/or modify it * + * under the terms version 2 of the GNU General Public License as published * + * by the Free Software Foundation. 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 General Public License for more details. * + * You should have received a copy of the GNU General Public License along * + * with this program; if not, write to the Free Software Foundation, Inc., * + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * + *****************************************************************************/ +package org.adempiere.webui.apps; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.adempiere.webui.component.Label; +import org.adempiere.webui.component.ListHead; +import org.adempiere.webui.component.ListItem; +import org.adempiere.webui.component.Listbox; +import org.adempiere.webui.theme.ThemeManager; +import org.adempiere.webui.util.TreeItemAction; +import org.adempiere.webui.util.TreeNodeAction; +import org.adempiere.webui.util.TreeUtils; +import org.compiere.model.MTreeNode; +import org.compiere.util.Env; +import org.compiere.util.Msg; +import org.compiere.util.Util; +import org.zkoss.zk.ui.Component; +import org.zkoss.zk.ui.Executions; +import org.zkoss.zk.ui.event.Event; +import org.zkoss.zk.ui.event.EventListener; +import org.zkoss.zk.ui.event.Events; +import org.zkoss.zk.ui.util.Clients; +import org.zkoss.zul.A; +import org.zkoss.zul.DefaultTreeNode; +import org.zkoss.zul.ListModel; +import org.zkoss.zul.ListModelList; +import org.zkoss.zul.ListModels; +import org.zkoss.zul.ListSubModel; +import org.zkoss.zul.Listcell; +import org.zkoss.zul.Listheader; +import org.zkoss.zul.Listitem; +import org.zkoss.zul.ListitemRenderer; +import org.zkoss.zul.ListitemRendererExt; +import org.zkoss.zul.Textbox; +import org.zkoss.zul.Toolbarbutton; +import org.zkoss.zul.Tree; +import org.zkoss.zul.Treechildren; +import org.zkoss.zul.Treeitem; +import org.zkoss.zul.Vlayout; +import org.zkoss.zul.impl.LabelElement; +import org.zkoss.zul.impl.LabelImageElement; + +/** + * @author hengsin + * + */ +public class MenuSearchController implements EventListener{ + + private static final String ON_SEARCH_ECHO = "onSearchEcho"; + private static final String ON_LOAD_MORE = "onLoadMore"; + private static final String ONSELECT_TIMESTAMP = "onselect.timestamp"; + private Tree tree; + private Listbox listbox; + private ListModelList model; + private Vlayout layout; + private ListModelList fullModel; + + private static final String ON_POST_SELECT_TREEITEM_EVENT = "onPostSelectTreeitem"; + + /** + * + */ + public MenuSearchController(Tree tree) { + this.tree = tree; + } + + public void refreshModel() { + final List list = new ArrayList(); + if (tree.getModel() == null) { + TreeUtils.traverse(tree, new TreeItemAction() { + public void run(Treeitem treeItem) { + if (treeItem.isVisible()) + addTreeItem(list, treeItem); + } + }); + } else { + TreeUtils.traverse(tree.getModel(), new TreeNodeAction() { + public void run(DefaultTreeNode treeNode) { + addTreeItem(list, treeNode); + } + }); + } + model = new ListModelList(list, true); + model.sort(new Comparator() { + + @Override + public int compare(MenuItem o1, MenuItem o2) { + return o1.getLabel().compareTo(o2.getLabel()); + } + }, true); + } + + private void addTreeItem(List list, DefaultTreeNode treeNode) { + MTreeNode mNode = (MTreeNode) treeNode.getData(); + if (!mNode.isLeaf()) + return; + + MenuItem item = new MenuItem(); + item.setLabel(mNode.getName()); + item.setDescription(mNode.getDescription()); + item.setImage(mNode.getImagePath()); + item.setData(treeNode); + list.add(item); + } + + private boolean isFolder(Treeitem treeItem) { + List list = treeItem.getChildren(); + for (Component c : list) { + if (c instanceof Treechildren && ((Treechildren)c).getChildren().size() > 0) { + return true; + } + } + return false; + } + + private void addTreeItem(List list, Treeitem treeItem) { + if (isFolder(treeItem)) + return; + + MenuItem item = new MenuItem(); + item.setLabel(getLabel(treeItem)); + item.setDescription(treeItem.getTooltiptext()); + + String image = getImage(treeItem); + if (image == null || image.length() == 0) + { + image = ThemeManager.getThemeResource("images/Folder16.png"); + } + item.setImage(image); + item.setData(treeItem); + list.add(item); + item.setType((String) treeItem.getAttribute("menu.type")); + } + + private String getLabel(Treeitem treeItem) { + String label = treeItem.getLabel(); + if (label == null || label.trim().length() == 0) + { + if (treeItem.getTreerow().getFirstChild().getFirstChild() != null && + treeItem.getTreerow().getFirstChild().getFirstChild() instanceof LabelElement) + { + LabelElement element = (LabelElement) treeItem.getTreerow().getFirstChild().getFirstChild(); + label = element.getLabel(); + } + } + return label; + } + + private String getImage(Treeitem treeItem) { + String image = treeItem.getImage(); + if (image == null || image.trim().length() == 0) + { + if (treeItem.getTreerow().getFirstChild().getFirstChild() != null && + treeItem.getTreerow().getFirstChild().getFirstChild() instanceof LabelImageElement) + { + LabelImageElement element = (LabelImageElement) treeItem.getTreerow().getFirstChild().getFirstChild(); + image = element.getImage(); + } + } + return image != null ? image.intern() : null; + } + + public void create(Component parent) { + refreshModel(); + + layout = new Vlayout(); + layout.setHeight("100%"); + parent.appendChild(layout); + + Label label = new Label(Util.cleanAmp(Msg.getMsg(Env.getCtx(),"Menu"))); + label.setStyle("padding: 3px; font-weight: bold; display: block;"); + layout.appendChild(label); + listbox = new Listbox(); + listbox.setEmptyMessage(Util.cleanAmp(Msg.getMsg(Env.getCtx(), "FindZeroRecords"))); + listbox.setStyle("border: none"); + listbox.setWidth("500px"); + layout.appendChild(listbox); + listbox.setItemRenderer(new MenuItemRenderer()); + listbox.addEventListener(Events.ON_SELECT, this); + listbox.addEventListener(ON_POST_SELECT_TREEITEM_EVENT, this); + ListHead listhead = new ListHead(); + listbox.appendChild(listhead); + Listheader listheader = new Listheader(); + listhead.appendChild(listheader); + listheader = new Listheader(); + listheader.setWidth("32px"); + listhead.appendChild(listheader); + + layout.addEventListener(ON_SEARCH_ECHO, this); + layout.addEventListener(ON_LOAD_MORE, this); + updateListboxModel(model); + } + + @Override + public void onEvent(Event event) throws Exception { + if (Events.ON_SELECT.equals(event.getName())) { + ListItem selected = listbox.getSelectedItem(); + if (selected == null) return; + onSelect(selected, Boolean.FALSE); + } else if (event.getName().equals(ON_POST_SELECT_TREEITEM_EVENT)) { + onPostSelectTreeitem((Boolean) event.getData()); + } else if (Events.ON_CLICK.equals(event.getName())) { + if (event.getTarget() instanceof ListItem) { + ListItem item = (ListItem) event.getTarget(); + Long onSelect = (Long) item.getAttribute(ONSELECT_TIMESTAMP); + if (onSelect == null) { + onSelect(item, Boolean.FALSE); + } else if (System.currentTimeMillis() - onSelect.longValue() > 1000) { + onSelect(item, Boolean.FALSE); + } + } else if (event.getTarget() instanceof Toolbarbutton) { + ListItem item = null; + Component parent = event.getTarget(); + while (parent != null) { + if (parent instanceof ListItem) { + item = (ListItem) parent; + break; + } + parent = parent.getParent(); + } + if (item != null) { + onSelect(item, Boolean.TRUE); + } + } + } else if (event.getName().equals(ON_SEARCH_ECHO)) { + onSearchEcho((String) event.getData()); + } else if (event.getName().equals(ON_LOAD_MORE)) { + loadMore(); + } + } + + private void onSelect(ListItem selected, Boolean newRecord) { + MenuItem item = selected.getValue(); + if (item == null) return; + if ("...".equals(item.getType())) { + selected.setAttribute(ONSELECT_TIMESTAMP, System.currentTimeMillis()); + Clients.showBusy(selected, null); + Events.echoEvent(ON_LOAD_MORE, layout, null); + } else { + selectTreeitem(item.getData(), newRecord); + selected.setAttribute(ONSELECT_TIMESTAMP, System.currentTimeMillis()); + } + } + + private void loadMore() { + ListModel listModel = listbox.getModel(); + ListModelList lml = (ListModelList) listModel; + lml.remove(lml.size()-1); + List subList = fullModel.subList(50, fullModel.size()); + lml.addAll(subList); + fullModel = null; + listbox.setSelectedIndex(50); + Clients.scrollIntoView(listbox.getSelectedItem()); + } + + private void selectTreeitem(Object node, Boolean newRecord) { + if (Executions.getCurrent().getAttribute(listbox.getUuid()+".selectTreeitem") != null) + return; + + Treeitem treeItem = null; + if (node == null) { + return; + } else if (node instanceof Treeitem) { + treeItem = (Treeitem) node; + } else { + DefaultTreeNode sNode = (DefaultTreeNode) node; + int[] path = tree.getModel().getPath(sNode); + treeItem = tree.renderItemByPath(path); + tree.setSelectedItem(treeItem); + } + if (treeItem != null) { + Executions.getCurrent().setAttribute(listbox.getUuid()+".selectTreeitem", Boolean.TRUE); + + select(treeItem); + Events.postEvent(ON_POST_SELECT_TREEITEM_EVENT, listbox, newRecord); + } + } + + private void select(Treeitem selectedItem) { + Treeitem parent = selectedItem.getParentItem(); + while (parent != null) { + if (!parent.isOpen()) + parent.setOpen(true); + + parent = parent.getParentItem(); + } + selectedItem.getTree().setSelectedItem(selectedItem); + } + + private void onPostSelectTreeitem(Boolean newRecord) { + Event event = null; + if (tree.getSelectedItem().getTreerow().getFirstChild().getFirstChild() instanceof A) + { + event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow().getFirstChild().getFirstChild(), newRecord); + } + else + { + event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow(), newRecord); + } + Events.postEvent(event); + } + + public void search(String value) { + listbox.setVisible(false); + Events.echoEvent(ON_SEARCH_ECHO, layout, value); + } + + public void onSearchEcho(String value) { + ListModelList newModel = null; + if (Util.isEmpty(value)) { + newModel = model; + } else { + @SuppressWarnings("unchecked") + ListSubModel subModel = (ListSubModel) ListModels.toListSubModel(model, new MenuListComparator(value), model.size()); + newModel = (ListModelList) subModel.getSubModel(null, -1); + } + updateListboxModel(newModel); + listbox.setVisible(true); + } + + private void updateListboxModel(ListModelList newModel) { + fullModel = null; + if (newModel.size() > 50) { + List list = newModel.getInnerList(); + List subList = list.subList(0, 50); + fullModel = newModel; + newModel = new ListModelList(subList.toArray(new MenuItem[0])); + MenuItem more = new MenuItem(); + more.setLabel("..."); + more.setType("..."); + newModel.add(more); + } + listbox.setModel(newModel); + } + + private class MenuListComparator implements Comparator { + + private String compare; + + private MenuListComparator(String compare) { + this.compare = compare; + } + + @Override + public int compare(MenuItem o1, MenuItem o2) { + compare = compare.toLowerCase().trim(); + boolean match = false; + if (compare.length() < 3) + { + match = o2.getLabel().toLowerCase().startsWith(compare); + } + else + { + match = o2.getLabel().toLowerCase().contains(compare); + } + return match ? 0 : -1; + } + + } + + public MenuItem selectPrior() { + int i = listbox.getSelectedIndex(); + if (i > 0) { + listbox.setSelectedIndex(i-1); + ListItem selected = listbox.getSelectedItem(); + if (selected == null) return null; + Clients.scrollIntoView(selected); + MenuItem item = selected.getValue(); + return item; + } + return null; + } + + public MenuItem selectNext() { + int i = listbox.getSelectedIndex(); + if (i < 0 && listbox.getItemCount() > 0) { + listbox.setSelectedIndex(0); + ListItem selected = listbox.getSelectedItem(); + if (selected == null) return null; + MenuItem item = selected.getValue(); + return item; + } else if (i+1 < listbox.getItemCount()) { + listbox.setSelectedIndex(i+1); + ListItem selected = listbox.getSelectedItem(); + if (selected == null) return null; + MenuItem item = selected.getValue(); + if (item == null) return null; + if ("...".equals(item.getType())) { + onSelect(selected, Boolean.FALSE); + } + Clients.scrollIntoView(selected); + return item; + } + return null; + } + + public boolean onOk(Textbox textbox) { + String text = textbox.getText(); + if (Util.isEmpty(text)) + return false; + text = text.toLowerCase(); + ListItem exact = null; + ListItem firstStart = null; + int count = listbox.getItemCount(); + for(int i = 0; i < count; i++) { + ListItem item = listbox.getItemAtIndex(i); + String label = item.getLabel(); + if (Util.isEmpty(label)) continue; + if (label.equalsIgnoreCase(text)) { + exact = item; + break; + } else if (firstStart == null && label.toLowerCase().startsWith(text) && text.length() >= 3) { + firstStart = item; + } + } + if (exact != null) { + textbox.setText(exact.getLabel()); + onSelect(exact, false); + return true; + } else if (firstStart != null) { + textbox.setText(firstStart.getLabel()); + onSelect(firstStart, false); + return true; + } + return false; + } + + private class MenuItemRenderer implements ListitemRenderer, ListitemRendererExt { + @Override + public Listitem newListitem(org.zkoss.zul.Listbox listbox) { + return new ListItem(); + } + + @Override + public Listcell newListcell(Listitem item) { + return new Listcell(item.getLabel()); + } + + @Override + public int getControls() { + return ListitemRendererExt.DETACH_ON_RENDER; + } + + @Override + public void render(Listitem item, MenuItem data, int index) + throws Exception { + Listcell cell = new Listcell(data.getLabel(), data.getImage()); + item.appendChild(cell); + cell.setTooltip(data.getDescription()); + item.setValue(data); + item.addEventListener(Events.ON_CLICK, MenuSearchController.this); + + cell = new Listcell(); + item.appendChild(cell); + boolean isWindow = data.getType() != null && data.getType().equals("window"); + if (isWindow) { + Toolbarbutton newBtn = new Toolbarbutton(null, ThemeManager.getThemeResource("images/New16.png")); + newBtn.addEventListener(Events.ON_CLICK, MenuSearchController.this); + newBtn.setSclass("fav-new-btn"); + newBtn.setTooltiptext(Util.cleanAmp(Msg.getMsg(Env.getCtx(), "New"))); + cell.appendChild(newBtn); + } + } + + } +} diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java index 4135df2abf..35645176c3 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java @@ -17,6 +17,8 @@ package org.adempiere.webui.panel; +import org.adempiere.webui.apps.GlobalSearch; +import org.adempiere.webui.apps.MenuSearchController; import org.adempiere.webui.component.Panel; import org.adempiere.webui.theme.ThemeManager; import org.adempiere.webui.window.AboutWindow; @@ -49,6 +51,8 @@ public class HeaderPanel extends Panel implements EventListener protected LabelImageElement btnMenu; protected Popup popMenu; + private MenuTreePanel menuTreePanel; + public HeaderPanel() { super(); @@ -62,9 +66,9 @@ public class HeaderPanel extends Panel implements EventListener image.addEventListener(Events.ON_CLICK, this); image.setStyle("cursor: pointer;"); - createSearchPanel(); - createPopupMenu(); + + createSearchPanel(); btnMenu = (LabelImageElement) getFellow("menuButton"); btnMenu.setLabel(Util.cleanAmp(Msg.getMsg(Env.getCtx(),"Menu"))); @@ -74,7 +78,7 @@ public class HeaderPanel extends Panel implements EventListener protected void createPopupMenu() { popMenu = new Popup(); popMenu.setId("menuTreePopup"); - new MenuTreePanel(popMenu); + menuTreePanel = new MenuTreePanel(popMenu); popMenu.setSclass("desktop-menu-popup"); popMenu.setHeight("90%"); popMenu.setWidth("600px"); @@ -82,11 +86,11 @@ public class HeaderPanel extends Panel implements EventListener } protected void createSearchPanel() { - MenuSearchPanel menuSearchPanel = new MenuSearchPanel(this); + GlobalSearch globalSearch = new GlobalSearch(new MenuSearchController(menuTreePanel.getMenuTree())); Component stub = getFellow("menuLookup"); - stub.getParent().insertBefore(menuSearchPanel, stub); + stub.getParent().insertBefore(globalSearch, stub); stub.detach(); - menuSearchPanel.setId("menuLookup"); + globalSearch.setId("menuLookup"); } public void onEvent(Event event) throws Exception { diff --git a/org.adempiere.ui.zk/theme/default/css/theme.css.dsp b/org.adempiere.ui.zk/theme/default/css/theme.css.dsp index 8fc37ba256..f4687241b6 100644 --- a/org.adempiere.ui.zk/theme/default/css/theme.css.dsp +++ b/org.adempiere.ui.zk/theme/default/css/theme.css.dsp @@ -1376,6 +1376,18 @@ tbody.z-grid-empty-body td { text-align: left; } +tbody.z-listbox-empty-body td { + text-align: left; +} + +div.z-listbox-body .z-listcell { + padding: 2px; +} + +.z-listbox-autopaging .z-listcell-cnt { + height: 20px; +} + <%-- notification message --%> .z-notification .z-notification-cl, .z-notification .z-notification-cnt {