package org.geogebra.web.web.gui.exam;
import java.util.Date;
import org.geogebra.common.gui.Layout;
import org.geogebra.common.gui.toolbar.ToolBar;
import org.geogebra.common.main.Feature;
import org.geogebra.common.main.Localization;
import org.geogebra.common.util.GTimer;
import org.geogebra.common.util.GTimerListener;
import org.geogebra.common.util.debug.Log;
import org.geogebra.web.html5.gui.GuiManagerInterfaceW;
import org.geogebra.web.html5.main.AppW;
import org.geogebra.web.html5.main.ExamEnvironmentW;
import org.geogebra.web.web.css.GuiResources;
import org.geogebra.web.web.gui.dialog.DialogBoxW;
import org.geogebra.web.web.gui.dialog.InputDialogW.DialogBoxKbW;
import org.geogebra.web.web.gui.layout.DockManagerW;
import org.geogebra.web.web.gui.layout.DockPanelW;
import com.google.gwt.dom.client.StyleInjector;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.VerticalPanel;
/**
* Exam start dialog
*/
public class ExamDialog {
private static boolean examStyle;
/** Application */
protected AppW app;
/** Wrapped box */
protected DialogBoxKbW box;
private Localization loc;
private Label instruction;
private Button btnOk;
/**
* @param app
* application
*/
public ExamDialog(AppW app) {
this.app = app;
}
/**
* Show the wrapped dialog
*/
public void show() {
ensureExamStyle();
loc = app.getLocalization();
final GuiManagerInterfaceW guiManager = app.getGuiManager();
box = new DialogBoxKbW(false, true, null, app.getPanel());
if (app.has(Feature.DIALOGS_OVERLAP_KEYBOARD)) {
box.setOverlapFeature(true);
}
VerticalPanel mainWidget = new VerticalPanel();
FlowPanel btnPanel = new FlowPanel();
FlowPanel cbxPanel = new FlowPanel();
btnOk = new Button();
Button btnCancel = new Button();
Button btnHelp = new Button();
// mainWidget.add(btnPanel);
btnPanel.add(btnOk);
// we don't need cancel and help buttons for tablet exam apps
if (!(app.getArticleElement().hasDataParamEnableGraphing())) {
btnPanel.add(btnCancel);
btnPanel.add(btnHelp);
box.addStyleName("boxsize");
btnCancel.setText(loc.getMenu("Cancel"));
btnHelp.setText(loc.getMenu("Help"));
} else {
box.addStyleName("ExamTabletBoxsize");
}
int checkboxes = 0;
if (!app.getSettings().getCasSettings().isEnabledSet()) {
checkboxes++;
final CheckBox cas = new CheckBox(loc.getMenu("Perspective.CAS"));
cas.addStyleName("examCheckbox");
cas.setValue(true);
app.getSettings().getCasSettings().setEnabled(true);
cbxPanel.add(cas);
cas.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
app.getSettings().getCasSettings().setEnabled(cas.getValue());
guiManager.updateToolbarActions();
}
});
}
if (!app.getSettings().getEuclidian(-1).isEnabledSet()) {
checkboxes++;
final CheckBox allow3D = new CheckBox(loc.getMenu("Perspective.3DGraphics"));
allow3D.addStyleName("examCheckbox");
allow3D.setValue(true);
app.getSettings().getEuclidian(-1).setEnabled(true);
cbxPanel.add(allow3D);
allow3D.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
app.getSettings().getEuclidian(-1).setEnabled(allow3D.getValue());
guiManager.updateToolbarActions();
}
});
}
guiManager.updateToolbarActions();
if (checkboxes > 0) {
Label description = new Label(loc.getMenu("exam_custom_description"));
mainWidget.add(description);
mainWidget.add(cbxPanel);
cbxPanel.addStyleName("ExamCheckboxPanel");
btnPanel.addStyleName("DialogButtonPanel");
box.getCaption().setText(loc.getMenu("exam_custom_header"));
} else {
if (app.getArticleElement().hasDataParamEnableGraphing()) {
boolean supportsCAS = app.getSettings().getCasSettings().isEnabled();
boolean supports3D = app.getSettings().getEuclidian(-1).isEnabled();
Log.debug(supportsCAS + "," + supports3D + "," + app.enableGraphing());
// CAS EXAM: cas && !3d && ev
if (!supports3D && supportsCAS) {
// set CAS background view for Exam CAS
app.getGgbApi().setPerspective("4");
box.getCaption().setText(loc.getMenu("ExamCAS"));
}
// GRAPH EXAM: !cas && !3d && ev
else if (!supports3D && !supportsCAS) {
if (app.enableGraphing()) {
box.getCaption().setText(loc.getMenu("ExamGraphingCalc.long"));
} else {
// set algebra view in background of start dialog
// for tablet Exam Simple Calc
// needed for GGB-1176
app.getGgbApi().setPerspective("A");
box.getCaption().setText(loc.getMenu("ExamSimpleCalc.long"));
// disable context menu in AV
app.setRightClickEnabledForAV(false);
}
}
} else {
box.getCaption().setText(loc.getMenu("exam_custom_header"));
}
}
if (app.has(Feature.BIND_ANDROID_TO_EXAM_APP) && runsOnAndroid()) {
startExamForAndroidWebview(mainWidget);
} else {
// start exam button
btnOk.setText(loc.getMenu("exam_start_button"));
btnOk.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
startExam(box, app);
}
});
}
mainWidget.add(btnPanel);
box.setWidget(mainWidget);
if ((app.getArticleElement().hasDataParamEnableGraphing())) {
btnOk.addStyleName("ExamTabletStartButton");
}
app.invokeLater(new Runnable() {
public void run() {
box.center();
}
});
// Cancel button
btnCancel.addStyleName("cancelBtn");
btnCancel.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
cancelExam();
}
});
// Help button
btnHelp.addStyleName("cancelBtn");
btnHelp.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
app.getFileManager().open("https://www.geogebra.org/tutorial/exam");
}
});
}
/**
* Cancel button handler
*/
protected void cancelExam() {
app.getExam().exit();
app.setExam(null);
app.getLAF().toggleFullscreen(false);
app.fireViewsChangedEvent();
GuiManagerInterfaceW guiManager = app.getGuiManager();
guiManager.updateToolbarActions();
guiManager.setGeneralToolBarDefinition(
ToolBar.getAllToolsNoMacros(true, false, app));
guiManager.updateToolbar();
guiManager.resetMenu();
box.hide();
}
private void startExam(boolean needsFullscreen) {
startExam(box, app, needsFullscreen);
}
/**
* @param box
* dialog
* @param app
* application
*/
public static void startExam(DialogBoxW box, AppW app) {
startExam(box, app, true);
}
/**
* @param box
* wrapped box
* @param app
* application
* @param needsFullscreen
* whether switch to fullscreen needs to be called
*/
public static void startExam(DialogBoxW box, AppW app, boolean needsFullscreen) {
final GuiManagerInterfaceW guiManager = app.getGuiManager();
if (needsFullscreen) {
app.getLAF().toggleFullscreen(true);
}
ensureExamStyle();
blockEscTab();
Date date = new Date();
guiManager.updateToolbarActions();
app.getLAF().removeWindowClosingHandler();
app.fileNew();
app.updateRounding();
// do this *before* perspective so that we have CAS toolbar for CAS
guiManager.setGeneralToolBarDefinition(
ToolBar.getAllToolsNoMacros(true, true, app));
if (app.enableGraphing()) {
// don't check for CAS supported but for data param
if (app.getArticleElement().getDataParamEnableCAS(false)) {
// set CAS start view for Exam CAS
app.getGgbApi().setPerspective("4");
} else {
app.getGgbApi().setPerspective("1");
}
} else {
app.getGgbApi().setPerspective("A");
}
app.getKernel().getAlgebraProcessor().reinitCommands();
app.getExam().setStart(date.getTime());
app.fireViewsChangedEvent();
guiManager.updateToolbar();
guiManager.updateToolbarActions();
Layout.initializeDefaultPerspectives(app, 0.2);
guiManager.updateMenubar();
guiManager.resetMenu();
DockPanelW dp = ((DockManagerW) guiManager.getLayout().getDockManager()).getPanelForKeyboard();
if (dp != null && dp.getKeyboardListener().needsAutofocus()) {
app.showKeyboard(dp.getKeyboardListener(), true);
}
if (box != null) {
box.hide();
}
}
////////////////////////////////////
// ANDROID TABLETS
////////////////////////////////////
private static native void blockEscTab() /*-{
$doc.body.addEventListener("keyup", function(e) {
if (e && e.keyCode == 27 && $doc.querySelector(".examToolbar")) {
e.preventDefault()
}
});
$doc.body.addEventListener("keydown", function(e) {
if (e && e.keyCode == 9 && $doc.querySelector(".examToolbar")) {
e.preventDefault()
}
});
}-*/;
private static void ensureExamStyle() {
if (examStyle) {
return;
}
StyleInjector.inject(GuiResources.INSTANCE.examStyleLTR().getText());
examStyle = true;
}
final private boolean runsOnAndroid() {
return app.getVersion().isAndroidWebview();
}
final private void startExamForAndroidWebview(VerticalPanel mainWidget) {
// bind methods to javascript
setJavascriptTargetToExamDialog();
exportGeoGebraAndroidMethods();
// needs a label for airplane mode / lock task instructions
instruction = new Label();
mainWidget.add(instruction);
// button
btnOk.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
onButtonOk();
}
});
// task locking available?
lockTaskIsAvailable = ExamEnvironmentW.checkLockTaskAvailable();
Log.debug("Task locking available: " + lockTaskIsAvailable);
// start airplane mode / lock task check
startCheckAirplaneMode();
}
private boolean lockTaskIsAvailable;
private enum DialogState {WAIT_FOR_AIRPLANE_MODE, WAIT_FOR_TASK_LOCK, CAN_START_EXAM}
private DialogState dialogState;
/**
* Android exam OK button pressed
*/
protected void onButtonOk() {
switch (dialogState) {
default:
case WAIT_FOR_TASK_LOCK:
// airplane mode off: ask again
if (!isAirplaneModeOn()) {
setAirplaneModeDialog();
return;
}
// ask Android to lock
askForTaskLock();
break;
case CAN_START_EXAM:
// airplane mode off: ask again
if (!isAirplaneModeOn()) {
setAirplaneModeDialog();
return;
}
// task not locked: ask again
if (lockTaskIsAvailable && !ExamEnvironmentW.checkTaskLocked()) {
setLockTaskDialog();
return;
}
// go to full screen
updateFullscreenStatusOn();
// set wifi & bluetooth off if needed
setWifiOffIfNeeded();
setBluetoothOffIfNeeded();
// all set: start exam
ExamEnvironmentW.setJavascriptTargetToNone();
// dont go to fullscreen if lock task is available
startExam(!lockTaskIsAvailable);
break;
}
}
private boolean wasAirplaneModeOn;
private void startCheckAirplaneMode() {
if (isAirplaneModeOn()) {
Log.debug("Airplane mode is on");
wasAirplaneModeOn = true;
setLockTaskDialog();
} else {
Log.debug("Airplane mode is off");
wasAirplaneModeOn = false;
setAirplaneModeDialog();
}
}
private void setAirplaneModeDialog() {
instruction.setText(loc.getMenu("exam_set_airplane_mode_on"));
instruction.setVisible(true);
btnOk.setVisible(false);
box.center();
dialogState = DialogState.WAIT_FOR_AIRPLANE_MODE;
}
private void setLockTaskDialog() {
// if task locking is not available, go to start exam dialog
if (!lockTaskIsAvailable) {
setStartExamDialog();
return;
}
instruction.setText(loc.getMenu("exam_accept_pin"));
instruction.setVisible(true);
btnOk.setText(loc.getMenu("exam_pin"));
btnOk.setFocus(false);
btnOk.setVisible(true);
box.center();
dialogState = DialogState.WAIT_FOR_TASK_LOCK;
}
private void setStartExamDialog() {
instruction.setVisible(false);
btnOk.setText(loc.getMenu("exam_start_button"));
btnOk.setFocus(false);
btnOk.setVisible(true);
box.center();
dialogState = DialogState.CAN_START_EXAM;
}
private GTimer checkTaskLockTimer = null;
private void askForTaskLock() {
Log.debug("ask for task lock");
ExamEnvironmentW.startLockTask();
// set timer to check continuously if task is locked
if (checkTaskLockTimer != null && checkTaskLockTimer.isRunning()) {
checkTaskLockTimer.stop();
}
checkTaskLockTimer = app.newTimer(new GTimerListener() {
@Override
public void onRun() {
checkTaskLock();
}
}, 100);
checkTaskLockTimer.startRepeat();
}
/**
* Regular check for airplane mode
*/
protected void checkTaskLock() {
Log.debug("check task lock");
if (!isAirplaneModeOn()) {
Log.debug("(check) airplane mode off");
checkTaskLockTimer.stop();
setAirplaneModeDialog();
return;
}
if (ExamEnvironmentW.checkTaskLocked()) {
Log.debug("(check) task is locked");
checkTaskLockTimer.stop();
setStartExamDialog();
} else {
Log.debug("(check) task is NOT locked");
}
}
private static native boolean updateFullscreenStatusOn() /*-{
return $wnd.GeoGebraExamAndroidJsBinder.updateFullscreenStatusOn();
}-*/;
private static native void stopLockTask() /*-{
$wnd.GeoGebraExamAndroidJsBinder.stopLockTask();
}-*/;
private static native boolean setJavascriptTargetToExamDialog() /*-{
return $wnd.GeoGebraExamAndroidJsBinder
.setJavascriptTargetToExamDialog();
}-*/;
private static native boolean isAirplaneModeOn() /*-{
return $wnd.GeoGebraExamAndroidJsBinder.isAirplaneModeOn();
}-*/;
private native void exportGeoGebraAndroidMethods() /*-{
var that = this;
$wnd.examDialog_airplaneModeTurnedOn = $entry(function() {
that.@org.geogebra.web.web.gui.exam.ExamDialog::airplaneModeTurnedOn()();
});
$wnd.examDialog_airplaneModeTurnedOff = $entry(function() {
that.@org.geogebra.web.web.gui.exam.ExamDialog::airplaneModeTurnedOff()();
});
}-*/;
/**
* this method is called through js (see exportGeoGebraAndroidMethods())
*/
public void airplaneModeTurnedOn() {
Log.debug("airplane mode turned on");
if (!wasAirplaneModeOn) {
setLockTaskDialog();
wasAirplaneModeOn = true;
}
}
/**
* this method is called through js (see exportGeoGebraAndroidMethods())
*/
public void airplaneModeTurnedOff() {
Log.debug("airplane mode turned off");
if (wasAirplaneModeOn) {
setAirplaneModeDialog();
wasAirplaneModeOn = false;
}
}
/**
* Exit the app
*/
public static void exitApp() {
if (ExamEnvironmentW.checkLockTaskAvailable()) {
stopLockTask();
}
exitAppJs();
}
private static native void setWifiOffIfNeeded() /*-{
$wnd.GeoGebraExamAndroidJsBinder.setWifiOffIfNeeded();
}-*/;
private static native void setBluetoothOffIfNeeded() /*-{
$wnd.GeoGebraExamAndroidJsBinder.setBluetoothOffIfNeeded();
}-*/;
private static native void exitAppJs()/*-{
$wnd.GeoGebraExamAndroidJsBinder.exitApp();
}-*/;
}