001: package com.xoetrope.awt.survey;
002:
003: import java.awt.Color;
004: import java.awt.Container;
005: import java.awt.Dimension;
006: import java.awt.Font;
007: import java.awt.FontMetrics;
008: import java.awt.Graphics;
009: import java.awt.Image;
010: import java.awt.Point;
011:
012: import com.xoetrope.event.XClickListener;
013: import com.xoetrope.event.XStateListener;
014: import com.xoetrope.survey.Question;
015: import com.xoetrope.survey.QuestionManager;
016: import com.xoetrope.survey.XSurveyManager;
017: import net.xoetrope.xui.XProjectManager;
018: import net.xoetrope.xui.XAttributedComponent;
019: import net.xoetrope.xui.XProject;
020: import net.xoetrope.xui.XTextHolder;
021: import net.xoetrope.xui.data.XDataBinding;
022: import net.xoetrope.xui.data.XModel;
023: import net.xoetrope.xui.style.XStyle;
024: import net.xoetrope.xui.style.XStyleManager;
025:
026: /**
027: * <p>Title: XQuestion</p>
028: * <p>Description: A component for rendering a question and a set of mutually
029: * exclusive answers. The possible answers are rendered as a set of check marks</p>
030: * <p> Copyright (c) Xoetrope Ltd., 2001-2006, This software is licensed under
031: * the GNU Public License (GPL), please see license.txt for more details. If
032: * you make commercial use of this software you must purchase a commercial
033: * license from Xoetrope.</p>
034: * <p>$Revision: 1.8 $</p>
035: */
036: /** @todo fix the data binding */
037: public class XQuestion extends Container implements XStateListener,
038: XTextHolder, XAttributedComponent//, XDataBinding
039: {
040: protected XClickListener clickListener;
041: protected XModel model;
042:
043: protected static final int MIN_SCALE = 5;
044: protected int numResponses = 0;
045: protected Question question;
046: protected int scale = 5;
047: protected int xScale;
048:
049: private boolean isLastQuestion = false;
050: private String cueImage;
051: private int offset = 40;
052: private int headerHeight;
053: private Point initialPoint;
054: private int responseState = 0;
055:
056: protected static XStyleManager styleManager;
057: protected static Color cueColor, cueBkColor, responseTextColor,
058: responseTextBkColor, questionBkColor, questionTextColor,
059: checkBkColor, checkColor;
060:
061: private boolean printing;
062: private XSurveyManager surveyManager;
063:
064: /**
065: * The owner project and the context in which this object operates.
066: */
067: protected XProject currentProject = XProjectManager
068: .getCurrentProject();
069:
070: /**
071: * Create a new question component
072: */
073: public XQuestion() {
074: if (questionTextColor == null) {
075: styleManager = currentProject.getStyleManager();
076: XStyle questionStyle = styleManager.getStyle("Question");
077: XStyle responseStyle = styleManager
078: .getStyle("Question/Response");
079: XStyle checkStyle = styleManager.getStyle("Question/Check");
080: XStyle cueStyle = styleManager.getStyle("Question/Cue");
081:
082: questionTextColor = questionStyle
083: .getStyleAsColor(XStyle.COLOR_FORE);
084: questionBkColor = questionStyle
085: .getStyleAsColor(XStyle.COLOR_BACK);
086: cueBkColor = cueStyle.getStyleAsColor(XStyle.COLOR_BACK);
087: cueColor = cueStyle.getStyleAsColor(XStyle.COLOR_FORE);
088: responseTextColor = responseStyle
089: .getStyleAsColor(XStyle.COLOR_FORE);
090: responseTextBkColor = responseStyle
091: .getStyleAsColor(XStyle.COLOR_BACK);
092: checkBkColor = checkStyle
093: .getStyleAsColor(XStyle.COLOR_BACK);
094: checkColor = checkStyle.getStyleAsColor(XStyle.COLOR_FORE);
095: }
096: clickListener = new XClickListener(this , offset);
097: addMouseListener(clickListener);
098: addMouseMotionListener(clickListener);
099: surveyManager = ((XSurveyManager) currentProject
100: .getObject("SurveyManager"));
101:
102: /** @todo fix the data binding */
103: //surveyManager.bindQuestion( this );
104: }
105:
106: /**
107: * Gets the preferred size of the component.
108: * @return The PreferredSize for the component
109: */
110: public Dimension getPreferredSize() {
111: return getSize();
112: }
113:
114: /**
115: * Sets the size and location of the component and rescales the content to match
116: * @param x
117: * @param y
118: * @param w
119: * @param h
120: */
121: public void setBounds(int x, int y, int w, int h) {
122: super .setBounds(x, y, w, h);
123: setScale(scale);
124: }
125:
126: /**
127: * Sets the scale. The scale controls the horizontal division of the question
128: * area. Normally this would be set to the maximum number of responses in a
129: * survey. For example if most questions have 5 possible answers then the scale
130: * would be set to 5
131: * @param newScale
132: */
133: public void setScale(int newScale) {
134: scale = newScale;
135: xScale = (getSize().width - offset * 2) / scale;
136: }
137:
138: /**
139: * Marks the question as being the last question on the form. The last
140: * question is rendered with an additioanl footer
141: * @param isLast
142: */
143: public void setLastQuestion(boolean isLast) {
144: Dimension d = getSize();
145: isLastQuestion = isLast;
146: d.height += 5;
147: Point p = getLocation();
148: setBounds(p.x, p.y, d.width, d.height);
149: }
150:
151: /**
152: * Adds a new response option
153: * @param value the value of the response
154: * @param caption the text
155: */
156: public void addResponse(int value, String caption) {
157: question.addOption(value, caption);
158: numResponses++;
159: }
160:
161: /**
162: * Refresh the display
163: * @param g the graphics context
164: */
165: public void update(Graphics g) {
166: paint(g);
167: }
168:
169: /**
170: * Print the page
171: * @param g the printer graphics context
172: */
173: public void print(Graphics g) {
174: printing = true;
175: paint(g);
176: printing = false;
177: }
178:
179: /**
180: * Render the question. A header is drawn containing the question text, then
181: * the body of the question is filled. Within the body the cue image is drawn
182: * followed by each response spaced equally and left aligned
183: * @param sg the graphics context
184: */
185: public void paint(Graphics sg) {
186: Image offscreen;
187: Graphics g;
188: if (!printing) {
189: offscreen = createImage(getSize().width, getSize().height);
190: g = offscreen.getGraphics();
191: } else {
192: offscreen = null;
193: g = sg;
194: }
195:
196: super .paint(g);
197:
198: Dimension d = getSize();
199: g.setClip(0, 0, d.width, d.height);
200: int xOffset = offset;
201:
202: XStyle questionStyle = styleManager.getStyle("Question");
203: Font f = styleManager.getFont(questionStyle);
204: if (f == null)
205: f = new Font("Dialog", 0, 10);
206:
207: g.setFont(f);
208: FontMetrics fm = g.getFontMetrics(f);
209: headerHeight = fm.getHeight() + 15;
210:
211: g.setColor(cueColor);
212: g.fillRect(0, 0, d.width + 1, 5);
213: g.setColor(questionBkColor);
214: g.fillRect(0, 6, d.width + 1, d.height + 1);
215: String qText = question.getText();
216:
217: if (qText != null) {
218: g.setColor(questionTextColor);
219: g.drawString(question.getText(), xOffset,
220: fm.getAscent() + 10);
221: }
222:
223: if (cueImage != null) {
224: Image img = currentProject.getImage(cueImage);
225: if (img != null)
226: g.drawImage(img, xOffset, headerHeight + 2, cueBkColor,
227: this );
228: }
229:
230: XStyle responseStyle = styleManager
231: .getStyle("Question/Response");
232: Font f2 = styleManager.getFont(responseStyle);
233: if (f2 == null)
234: f2 = f;
235: g.setFont(f2);
236: fm = g.getFontMetrics(f2);
237:
238: headerHeight = Math.min(headerHeight, d.height - fm.getAscent()
239: - 11);
240:
241: xOffset += offset / 2;
242: int responses = 0;
243: if (question != null) {
244: responses = question.getNumOptions();
245: }
246: for (int i = 0; i < responses; i++) {
247: String responseText = question.getOptionText(i);
248: if (responseText != null) {
249: g.setColor(checkBkColor);
250: g.fillRect(xOffset + 2, headerHeight + 13, 8, 10);
251: g.setColor(responseTextBkColor);
252: g.drawRect(xOffset + 1, headerHeight + 13, 10, 10);
253: g.setColor(checkColor);
254: g.drawRect(xOffset, headerHeight + 12, 10, 10);
255: g.setColor(responseTextColor);
256: g.drawString(responseText, xOffset + 19, headerHeight
257: + fm.getAscent() + 10);
258: }
259: xOffset += xScale;
260: }
261:
262: xOffset = offset + offset / 2;
263: if (clickListener != null) {
264: if (clickListener.getIsEntered()) {
265: g.setColor(checkColor);
266: g.drawRect(0, 5, d.width - 1, d.height - 6);
267:
268: int activeResponse = clickListener.getActiveResponse();
269: if (activeResponse >= 0) {
270: g.setColor(responseTextBkColor);
271: g.fillRect(xOffset + activeResponse * xScale + 1,
272: headerHeight + 14, 9, 8);
273: }
274: }
275: int selResponse = clickListener.getSelectedResponse();
276: if (selResponse >= 0) {
277: g.setColor(checkColor);
278: g.fillRect(xOffset + selResponse * xScale + 3,
279: headerHeight + 15, 5, 5);
280: }
281: }
282:
283: if (isLastQuestion) {
284: g.setColor(cueColor);
285: g.fillRect(0, d.height - 5, d.width + 1, 5);
286: }
287:
288: if (!printing) {
289: sg.drawImage(offscreen, 0, 0, getSize().width,
290: getSize().height, null);
291: g.dispose();
292: offscreen = null;
293: }
294: }
295:
296: /**
297: * Gets the question's id
298: * @return The ID
299: */
300: public int getId() {
301: return question.getId();
302: }
303:
304: /**
305: * Gets the selected response
306: * @return the response value
307: */
308: public String getResponse() {
309: if (clickListener.getSelectedResponse() < 0)
310: return "0";
311:
312: if (question.getQuestionType() == Question.MULTIPLE_CHOICE) {
313: int responses = question.getNumOptions();
314: String response = new String();
315: for (int i = 0; i < responses; i++) {
316: if (response.length() > 0)
317: response += QuestionManager.tokenSeparator;
318: if (isOptionSelected(i))
319: response += new Integer(question.getOptionId(i))
320: .toString();
321: else
322: response += "-";
323: }
324: return response;
325: }
326:
327: return new Integer(question.getOptionId(clickListener
328: .getSelectedResponse())).toString();
329: }
330:
331: /**
332: * Checks to see if a praticular option is selected
333: * @param idx the index of the option
334: * @return true if the option is selected
335: */
336: public boolean isOptionSelected(int idx) {
337: int response = 1;
338: response <<= idx;
339: return (responseState & response) > 0;
340: }
341:
342: /**
343: * Find a response state based on the coordinates of the mouse click
344: * @param x the x coordinate of the mouse click
345: * @param y the y coordinate of the mouse click
346: * @param defResponse the default response
347: * @return true if a response was found
348: */
349: public boolean setState(int x, int y, int defResponse) {
350: int activeResponse = findCurrentResponse(x, y);
351: if (activeResponse == Integer.MIN_VALUE)
352: activeResponse = defResponse;
353:
354: clickListener.setActiveResponse(activeResponse);
355: return (defResponse != activeResponse);
356: }
357:
358: /**
359: * Find a response state based on the coordinates of the mouse click
360: * @param x the x coordinate of the mouse click
361: * @param y the y coordinate of the mouse click
362: * @return the response id or Integer.MIN_VALUE if a response was not selected
363: */
364: public int findCurrentResponse(int x, int y) {
365: int activeResponse = x / xScale;
366: if ((activeResponse == 0) && (x < 0))
367: activeResponse = Integer.MIN_VALUE;
368: else if (activeResponse >= numResponses)
369: activeResponse = Integer.MIN_VALUE;
370:
371: return activeResponse;
372: }
373:
374: /**
375: * Called by XClickListener to check if a response event should be sent to the
376: * parent form. The control can also use this event to do post click processing
377: * @return true if the parent is to be notified
378: */
379: public boolean fireActionEvent() {
380: return true;
381: }
382:
383: /**
384: * Repaints the responses so that the current response is shown
385: */
386: public void paintStates() {
387: repaint();
388: }
389:
390: /**
391: * Upates the state of the options
392: */
393: public void updateSelectedState() {
394: int selectedResponse = -1;
395: if (clickListener != null)
396: selectedResponse = clickListener.getSelectedResponse();
397: if (selectedResponse >= 0) {
398: int response = 1;
399: response <<= selectedResponse;
400: if (question.getQuestionType() == question.MULTIPLE_CHOICE)
401: responseState ^= response;
402: else
403: responseState = response;
404: }
405: }
406:
407: /**
408: * Unchecks all the responses
409: */
410: public void clear() {
411: clickListener.setSelectedResponse(-1);
412: repaint();
413: }
414:
415: /**
416: * Sets the control's text. Used to localize the control. In the case of this
417: * control the method does nothing since the questions are localized as part
418: * of an entire questionaire.
419: *
420: * @param s The new text to display.
421: */
422: public void setText(String s) {
423: if (question != null)
424: question.setText(s);
425: }
426:
427: /**
428: * Gets the questions text
429: * @return The question text
430: */
431: public String getText() {
432: return question.getText();
433: }
434:
435: /**
436: * Gets the question id.
437: * @return The Question ID
438: */
439: public int getQuestionId() {
440: return question.getId();
441: }
442:
443: /**
444: * Gets the question.
445: * @return the question or null if none isreferenced
446: */
447: public Question getQuestion() {
448: return question;
449: }
450:
451: /**
452: * Gets the question type.
453: * @return The Question type
454: */
455: public int getQuestionType() {
456: return question.getQuestionType();
457: }
458:
459: /**
460: * Set one or more attributes of the component. Currently this handles the
461: * attributes
462: * <OL>
463: * <LI>cue, value=Cue image filename</LI>
464: * </OL>
465: * @return 0 for success, non zero otherwise
466: */
467: public int setAttribute(String attribName, Object attribValue) {
468: String attribNameLwr = attribName.toLowerCase();
469: String attribValueStr = (String) attribValue;
470: if (attribNameLwr.equals("cue"))
471: cueImage = attribValueStr;
472: else if (attribNameLwr.equals("scale"))
473: setScale(new Integer(attribValueStr).intValue());
474: else if (attribNameLwr.equals("id"))
475: question.setId(new Integer(attribValueStr).intValue());
476: else if (attribNameLwr.equals("last"))
477: isLastQuestion = (attribValueStr.equalsIgnoreCase("true"));
478: else if (attribNameLwr.equals("ref")) {
479: question = surveyManager.getNextQuestion();
480: //if ( question != null )
481: //setScale( Math.max( numResponses = question.getNumOptions(), Math.max( question.getScale(), MIN_SCALE )));
482: }
483:
484: repaint(100);
485: return 0;
486: }
487:
488: /**
489: * Updates the TextComponent with the value obtained from the data model.
490: */
491: public void get() {
492: setupModel();
493: String s = (String) model.get();
494: clickListener.setSelectedResponse(s != null ? new Integer(s)
495: .intValue() : -1);
496: }
497:
498: /**
499: * Set the model node. This method is called when the bound path is modified
500: * @param newNode
501: */
502: public void setModel(XModel newNode, String newPath) {
503: model = newNode;
504: }
505:
506: /**
507: * Updates the data model with the value retrieved from the TextComponent.
508: */
509: public void set() {
510: setupModel();
511: model.get(new Integer(question.getOptionId(clickListener
512: .getSelectedResponse())).toString());
513: }
514:
515: private void setupModel() {
516: if (model == null) {
517: model = (XModel) currentProject.getModel().get(
518: "currentSurvey/question/"
519: + new Integer(question.getId()).toString());
520: model.setTagName("data");
521: }
522: }
523:
524: /**
525: * Get the model path
526: */
527: public String getSourcePath() {
528: return "currentSurvey/question/"
529: + new Integer(question.getId()).toString();
530: }
531:
532: /**
533: * Get the output/save path
534: */
535: public String getOutputPath() {
536: return "currentSurvey/question/"
537: + new Integer(question.getId()).toString();
538: }
539:
540: /**
541: * Set the model path for the source data
542: */
543: public void setSourcePath(String newPath) {
544: }
545:
546: /**
547: * Set the model path for the output/state data
548: */
549: public void setOutputPath(String newPath) {
550: }
551:
552: /**
553: * Update the model node used in the binding. Note that this method does not
554: * modify the path values stored by this node.
555: * @param newNode the new model for the data source
556: */
557: public void setSource(XModel newNode) {
558:
559: }
560:
561: /**
562: * Update the path values stored by this node. The output path is used to
563: * store selection data and state.
564: * @param newModel the new model for saving the output data
565: */
566: public void setOutput(XModel newModel, String outputPath) {
567: }
568:
569: /**
570: * Get the component to which this binding is attached
571: */
572: public Object getComponent() {
573: return this ;
574: }
575:
576: /**
577: * Get the file name of the cue image
578: * @return the file name
579: */
580: public String getCueFileName() {
581: return cueImage;
582: }
583:
584: /**
585: * Gets the scale being used by this question for its layout
586: * @return the scale
587: */
588: public int getScale() {
589: return scale;
590: }
591:
592: /**
593: * Is this question rendered as the last question on the page?
594: * @return true if rendered as the last question
595: */
596: public boolean isLast() {
597: return isLastQuestion;
598: }
599: }
|