/*******************************************************************************
* Copyright 2011 Antti Havanko
*
* This file is part of Motiver.fi.
* Motiver.fi is licensed under one open source license and one commercial license.
*
* Commercial license: This is the appropriate option if you want to use Motiver.fi in
* commercial purposes. Contact license@motiver.fi for licensing options.
*
* Open source license: This is the appropriate option if you are creating an open source
* application with a license compatible with the GNU GPL license v3. Although the GPLv3 has
* many terms, the most important is that you must provide the source code of your application
* to your users so they can be free to modify your application for their own needs.
******************************************************************************/
package com.delect.motiver.client.view.nutrition;
import java.util.List;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Widget;
import com.delect.motiver.client.AppController;
import com.delect.motiver.client.Motiver;
import com.delect.motiver.client.Templates;
import com.delect.motiver.client.presenter.nutrition.FoodPresenter;
import com.delect.motiver.client.presenter.nutrition.FoodPresenter.FoodHandler;
import com.delect.motiver.client.res.MyResources;
import com.delect.motiver.client.view.CustomListener;
import com.delect.motiver.client.view.MySpinnerField;
import com.delect.motiver.client.view.widget.ImageButton;
import com.delect.motiver.shared.Constants;
import com.delect.motiver.shared.FoodModel;
import com.delect.motiver.shared.FoodNameModel;
import com.delect.motiver.shared.util.CommonUtils;
import com.delect.motiver.shared.util.CommonUtils.MessageBoxHandler;
import com.extjs.gxt.ui.client.core.XTemplate;
import com.extjs.gxt.ui.client.data.BaseListLoadResult;
import com.extjs.gxt.ui.client.data.BaseListLoader;
import com.extjs.gxt.ui.client.data.BasePagingLoadConfig;
import com.extjs.gxt.ui.client.data.ListLoadResult;
import com.extjs.gxt.ui.client.data.LoadEvent;
import com.extjs.gxt.ui.client.data.Loader;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.ModelReader;
import com.extjs.gxt.ui.client.data.RpcProxy;
import com.extjs.gxt.ui.client.dnd.DragSource;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.DNDEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.FieldEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.util.Margins;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.MessageBox;
import com.extjs.gxt.ui.client.widget.Popup;
import com.extjs.gxt.ui.client.widget.Text;
import com.extjs.gxt.ui.client.widget.form.ComboBox;
import com.extjs.gxt.ui.client.widget.form.ComboBox.TriggerAction;
import com.extjs.gxt.ui.client.widget.layout.BoxLayout.BoxLayoutPack;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayout;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayout.HBoxLayoutAlign;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayoutData;
public class FoodView extends FoodPresenter.FoodDisplay {
private MessageBox box = null;
private ImageButton btnEdit = new ImageButton(AppController.Lang.EditTarget(AppController.Lang.ThisFood().toLowerCase()), MyResources.INSTANCE.iconBtnRename());
protected ComboBox<FoodNameModel> comboName;
protected LayoutContainer containerName = new LayoutContainer();
protected FoodModel food;
private FoodHandler handler;
protected Text labelPortions = new Text();
public LayoutContainer panelButtons = new LayoutContainer();
//widgets
private MySpinnerField spinAmount = new MySpinnerField();
private ListStore<FoodNameModel> store;
//template for combobox
private XTemplate template = XTemplate.create(Templates.getFoodNameTemplate());
//panels
private LayoutContainer thisContent = new LayoutContainer();
private Popup popup = new Popup();
private Timer timerUpdate;
/**
* Food view
*/
public FoodView() {
this.setStyleName("panel-food");
HBoxLayout layout = new HBoxLayout();
layout.setHBoxLayoutAlign(HBoxLayoutAlign.MIDDLE);
thisContent.setLayout(layout);
thisContent.setAutoHeight(true);
thisContent.setAutoWidth(true);
this.add(thisContent);
//show hide header buttons based on mouse position
this.addListener(Events.OnMouseOver, new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
addStyleName("panel-food-active");
panelButtons.setVisible(true);
thisContent.layout(true);
if(!popup.isVisible()) {
popup.showAt(getAbsoluteLeft()+getWidth()+35, getAbsoluteTop()+5);
}
}
});
this.addListener(Events.OnMouseOut, new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
removeStyleName("panel-food-active");
panelButtons.setVisible(false);
thisContent.layout(true);
if(popup.isVisible()) {
popup.hide();
}
}
});
}
@Override
public Widget asWidget() {
try {
//food selection combo
if(food.getUid().equals(AppController.User.getUid())) {
comboName = addFoodCombo();
containerName.add(comboName);
thisContent.add(containerName);
}
else {
Text textName = new Text();
if(food.getName() != null) {
textName.setText( food.getName().getName() );
}
textName.addStyleName("field-readonly");
textName.setStyleAttribute("text-align", "left");
textName.setWidth(325);
thisContent.add(textName);
}
//amount
thisContent.add(getSpinAmount(), new HBoxLayoutData(new Margins(0, 0, 0, 10)));
//portion
labelPortions.setText("-");
labelPortions.setStyleName("label-portions");
thisContent.add(labelPortions, new HBoxLayoutData(new Margins(0, 0, 0, 10)));
updatePortions(food.getAmount());
if(food.getUid().equals(AppController.User.getUid())) {
thisContent.add(getPanelButtons());
}
popup.setSize(150, 25);
popup.setConstrainViewport(false);
popup.setAutoHide(false);
updatePopup();
} catch (Exception e) {
Motiver.showException(e);
}
panelButtons.setVisible(false);
return this;
}
/**
* Updates popup showing total calories
*/
private void updatePopup() {
double e = 0D;
double p = 0;
double c = 0;
double f = 0;
if(food.getName() != null && food.getName().getEnergy() > 0) {
final double amount = food.getAmount();
e = (food.getName().getEnergy() / 100) * amount;
p = (food.getName().getProtein() / 100) * amount;
c = (food.getName().getCarb() / 100) * amount;
f = (food.getName().getFet() / 100) * amount;
}
popup.removeAll();
if(e > 0) {
popup.add(CommonUtils.getTotalPanelFlow(e, p, c, f));
}
popup.layout();
}
protected LayoutContainer getPanelButtons() {
//spacer
HBoxLayoutData flex = new HBoxLayoutData(new Margins(0, 0, 0, 5));
flex.setFlex(1);
thisContent.add(new Text(), flex);
//buttons layout
HBoxLayout layoutButtons = new HBoxLayout();
layoutButtons.setHBoxLayoutAlign(HBoxLayoutAlign.MIDDLE);
layoutButtons.setPack(BoxLayoutPack.END);
panelButtons.setLayout(layoutButtons);
panelButtons.setHeight(16);
panelButtons.setWidth(70);
//edit food link
btnEdit.addListener(Events.OnClick, new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
handler.foodEdited();
}
});
panelButtons.add(btnEdit, new HBoxLayoutData(new Margins(0, 0, 0, 10)));
btnEdit.setVisible(AppController.User.isAdmin() || (food.getName() != null && food.getName().getUid().equals(AppController.User.getUid())));
//drag food
ImageButton btnDrag = new ImageButton(AppController.Lang.DragToCopy(), MyResources.INSTANCE.iconBtnDrag());
DragSource source = new DragSource(btnDrag) {
@Override
protected void onDragStart(DNDEvent event) {
super.onDragStart(event);
event.setData(food);
//set drag panel
String name = (food.getName() != null)? food.getName().getName() : "";
String html = CommonUtils.getDragPanel(AppController.Lang.CopyTargetTo(name, "..."));
event.getStatus().update(html);
}
};
source.setGroup("mealfood");
panelButtons.add(btnDrag, new HBoxLayoutData(new Margins(0, 0, 0, 10)));
//remove food link
ImageButton btnRemove = new ImageButton(AppController.Lang.RemoveTarget(AppController.Lang.ThisFood().toLowerCase()), MyResources.INSTANCE.iconRemove());
btnRemove.addListener(Events.OnClick, new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
//ask for confirm
box = CommonUtils.getMessageBoxConfirm(AppController.Lang.RemoveConfirm(AppController.Lang.ThisFood().toLowerCase()), new MessageBoxHandler() {
@Override
public void okPressed(String text) {
handler.foodRemoved();
}
});
box.show();
}
});
panelButtons.add(btnRemove, new HBoxLayoutData(new Margins(0, 0, 0, 10)));
return panelButtons;
}
protected Widget getSpinAmount() {
//if "portion" set -> only increment 'even' values
if(food.getUid().equals(AppController.User.getUid())) {
spinAmount = new MySpinnerField() {
@Override
protected void doSpin(boolean up) {
try {
final double minValue = this.getMinValue().doubleValue();
final double maxValue = this.getMaxValue().doubleValue();
double increment = (food.getName() != null)? food.getName().getPortion() / 2 : 100D;
if(Double.compare(increment, 0) == 0) {
increment = 100D;
}
if (!readOnly) {
Number value = this.getValue();
double newValue = 0;
double d = (value == null) ? 0d : getValue().doubleValue();
if (up) {
newValue = (Math.max(minValue, Math.min(d + increment, maxValue)));
} else {
newValue = (Math.max(minValue, Math.min(this.getAllowNegative() ? d - increment : Math.max(0, d - increment), maxValue)));
}
if(increment > 0) {
double dRes = (int)(newValue / increment + 0.01);
newValue = dRes * increment;
}
this.setValue(newValue);
//update portions column
updatePortions(this.getValue().doubleValue());
}
} catch (Exception e) {
Motiver.showException(e);
}
}
};
//save value when valid
spinAmount.addListener(Events.Valid, new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
if(handler != null && spinAmount.getValue() != null && spinAmount.isValid() && Double.compare(spinAmount.getValue().doubleValue(), food.getAmount()) != 0) {
food.setAmount(spinAmount.getValue().doubleValue());
saveData();
//update portions text
updatePortions(spinAmount.getValue().doubleValue());
}
}
});
spinAmount.addListener(Events.Invalid, new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
if(spinAmount.getRawValue().contains(",")) {
spinAmount.setValue( Double.parseDouble(spinAmount.getRawValue().replace(",", ".")) );
}
}
});
spinAmount.addStyleName("field-amount");
spinAmount.addListener(Events.OnClick, CustomListener.fieldOnClicked);
spinAmount.setAllowNegative(false);
spinAmount.setIncrement(100);
spinAmount.setMinValue(0);
spinAmount.setMaxValue(5000D);
spinAmount.setEditable(true);
spinAmount.setPropertyEditorType(Double.class);
spinAmount.setFormat(NumberFormat.getFormat(NumberFormat.getDecimalFormat().getPattern() + " g"));
spinAmount.setValue(food.getAmount());
spinAmount.setFireChangeEventOnSetValue(true);
return spinAmount;
}
else {
final Text textValue = new Text();
textValue.setText( NumberFormat.getFormat(NumberFormat.getDecimalFormat().getPattern() + " g").format(food.getAmount()) );
textValue.addStyleName("field-readonly");
textValue.setWidth(125);
return textValue;
}
}
@Override
public void onStop() {
if(timerUpdate != null) {
timerUpdate.cancel();
}
if(box != null && box.isVisible()) {
box.close();
}
if(popup != null && popup.isVisible()) {
popup.hide();
}
}
@Override
public void setHandler(FoodHandler handler) {
this.handler = handler;
}
@Override
public void setModel(FoodModel food) {
this.food = food;
btnEdit.setVisible(AppController.User.isAdmin() || (food.getName() != null && food.getName().getUid().equals(AppController.User.getUid())));
panelButtons.layout();
if(comboName != null && food.getName() != null) {
comboName.setValue(food.getName());
}
if(spinAmount.getValue() != null) {
updatePortions(spinAmount.getValue().doubleValue());
}
}
@Override
public void setNameComboEnabled(boolean enabled) {
comboName.setEnabled(enabled);
}
/**
* Adds food search combo
*/
protected ComboBox<FoodNameModel> addFoodCombo() {
final ComboBox<FoodNameModel> combo = new ComboBox<FoodNameModel>() {
@Override
protected boolean selectByValue(String value) {
if(getStore().getCount() > 0) {
select(0);
return true;
}
else {
return false;
}
}
};
// proxy, reader and loader
RpcProxy<List<FoodNameModel>> proxy = new RpcProxy<List<FoodNameModel>>() {
@Override
protected void load(Object loadConfig, AsyncCallback<List<FoodNameModel>> callback) {
BasePagingLoadConfig config = (BasePagingLoadConfig)loadConfig;
//trim
String query = config.get("query").toString();
query = query.trim();
handler.query(query, callback);
}
};
ModelReader reader = new ModelReader();
BaseListLoader<ListLoadResult<ModelData>> loader = new BaseListLoader<ListLoadResult<ModelData>>(proxy, reader);
loader.addListener(Loader.BeforeLoad, new Listener<LoadEvent>() {
public void handleEvent(LoadEvent be) {
be.<ModelData>getConfig().set("start", be.<ModelData>getConfig().get("offset"));
}
});
loader.addListener(Loader.Load, new Listener<LoadEvent>() {
@SuppressWarnings("unchecked")
public void handleEvent(LoadEvent be) {
//add "add new food" item on list
BaseListLoadResult<FoodNameModel> list = (BaseListLoadResult<FoodNameModel>)be.getData();
final FoodNameModel model = new FoodNameModel(-1L, AppController.Lang.AddNew(AppController.Lang.Food().toLowerCase()));
list.getData().add(model);
}
});
store = new ListStore<FoodNameModel>(loader);
combo.addStyleName("field-name");
if(food.getName() != null) {
if(food.getName().getId() != 0) {
combo.setValue(food.getName());
}
}
combo.setWidth(325);
combo.setValidationDelay(Constants.DELAY_SEARCH);
combo.setForceSelection(false);
combo.setMessageTarget("none");
combo.setDisplayField("n");
combo.setMinChars(Constants.LIMIT_MIN_QUERY_WORD);
combo.setTemplate(template );
combo.setStore(store);
combo.setEmptyText(AppController.Lang.EnterKeywordToSearchForFoods());
combo.setHideTrigger(true);
combo.setTriggerAction(TriggerAction.ALL);
combo.setValidateOnBlur(false);
//save typed value for when adding new name
combo.addListener(Events.BeforeSelect, new Listener<FieldEvent>() {
@Override
public void handleEvent(FieldEvent be) {
ComboBox<FoodNameModel> cb = ((ComboBox<FoodNameModel>)be.getSource());
cb.setData("val", combo.getRawValue());
}
});
//update model when valid value
combo.addListener(Events.Valid, new Listener<FieldEvent>() {
@SuppressWarnings("unchecked")
@Override
public void handleEvent(FieldEvent be) {
try {
ComboBox<FoodNameModel> cb = ((ComboBox<FoodNameModel>)be.getSource());
//if selected something from the list
if(cb.getValue() != null) {
FoodNameModel mo = cb.getValue();
//if user clicked "add new" value
if(mo.getId() == -1) {
String val = combo.getData("val");
handler.newNameEntered(val);
}
//value selected from list
else if(handler != null) {
//only if changed
boolean changed = false;
if(food.getName() != null) {
if(food.getName().getId() != mo.getId()) {
changed = true;
}
}
else {
changed = true;
}
food.setName(mo);
if(changed) {
handler.saveData(food);
}
}
}
} catch (Exception e) {
Motiver.showException(e);
}
}
});
return combo;
}
/**
* Calls handler to save data
* <br>Uses timer so handler is called after 1 sec. (So if user changes value repeately, handler is called only once)
*/
protected void saveData() {
if(timerUpdate != null) {
timerUpdate.cancel();
}
timerUpdate = new Timer() {
@Override
public void run() {
handler.saveData(food);
}
};
timerUpdate.schedule(1000);
}
/**
* Updates portion label based on spinner field value
*/
protected void updatePortions(double value) {
try {
if(food.getName() == null) {
return;
}
String text = "";
double por = food.getName().getPortion();
if(por == 0) {
por = 100;
}
//if equal number
double left = (((value / por) * 4) % 2);
if(left == 0 && food.getName().getPortion() > 0) {
double val = value / por;
//one piece
if(val == 1) {
text = AppController.Lang.PortionsOne();
}
else if(val == 0) {
text = "";
}
else {
text = AppController.Lang.Portions(NumberFormat.getFormat("0.#").format((value / por)));
}
}
labelPortions.setText(text);
updatePopup();
} catch (Exception e) {
Motiver.showException(e);
}
}
}