001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016:
017: package com.google.gwt.user.client.ui;
018:
019: import com.google.gwt.user.client.DOM;
020: import com.google.gwt.user.client.Element;
021: import com.google.gwt.user.client.Event;
022:
023: /**
024: * Custom Button is a base button class with built in support for a set number
025: * of button faces.
026: *
027: * Each face has its own style modifier. For example, the state for down and
028: * hovering is assigned the CSS modifier <i>down-hovering</i>. So, if the
029: * button's overall style name is <i>gwt-PushButton</i> then when showing the
030: * <code>down-hovering</code> face, the button's style is <i>
031: * gwt-PushButton-down-hovering</i>. The overall style name can be used to
032: * change the style of the button irrespective of the current face.
033: *
034: * <p>
035: * Each button face can be assigned is own image, text, or html contents. If no
036: * content is defined for a face, then the face will use the contents of another
037: * face. For example, if <code>down-hovering</code> does not have defined
038: * contents, it will use the contents defined by the <code>down</code> face.
039: * </p>
040: *
041: * <p>
042: * The supported faces are defined below:
043: * </p>
044: * <p>
045: * <table border="4">
046: * <tr>
047: *
048: * <td><b>CSS style name</b></td>
049: * <td><b>Getter method</b></td>
050: * <td><b>description of face</b></td>
051: * <td><b>defaults to contents of face</b></td>
052: * </tr>
053: *
054: * <tr>
055: * <td>up</td>
056: * <td> {@link #getUpFace()} </td>
057: * <td>face shown when button is up</td>
058: * <td> none</td>
059: * </tr>
060: *
061: * <tr>
062: * <td>down</td>
063: * <td> {@link #getDownFace()} </td>
064: * <td>face shown when button is down</td>
065: * <td> up </td>
066: * </tr>
067: *
068: * <tr>
069: * <td>up-hovering</td>
070: * <td> {@link #getUpHoveringFace()} </td>
071: * <td>face shown when button is up and hovering</td>
072: * <td> up </td>
073: * </tr>
074: *
075: * <tr>
076: * <td>up-disabled</td>
077: * <td> {@link #getUpDisabledFace()} </td>
078: * <td>face shown when button is up and disabled</td>
079: * <td> up</td>
080: * </tr>
081: *
082: * <tr>
083: * <td>down-hovering</td>
084: * <td> {@link #getDownHoveringFace()} </td>
085: * <td>face shown when button is down and hovering</td>
086: * <td> down</td>
087: * </tr>
088: *
089: * <tr>
090: * <td>down-disabled</td>
091: * <td> {@link #getDownDisabledFace()} </td>
092: * <td>face shown when button is down and disabled</td>
093: * <td>down</td>
094: * </tr>
095: * </table>
096: * </p>
097: *
098: */
099: public abstract class CustomButton extends ButtonBase implements
100: SourcesKeyboardEvents {
101: /**
102: * Represents a button's face. Each face is associated with its own style
103: * modifier and, optionally, its own contents html, text, or image.
104: */
105: public abstract class Face implements HasHTML, HasText {
106: private static final String STYLENAME_HTML_FACE = "html-face";
107: private final Face delegateTo;
108: private Element face;
109:
110: /**
111: * Constructor for <code>Face</code>. Creates a new face that delegates
112: * to the supplied face.
113: *
114: * @param delegateTo default content provider
115: */
116: private Face(Face delegateTo) {
117: this .delegateTo = delegateTo;
118: }
119:
120: /**
121: * Gets the face's contents as html.
122: *
123: * @return face's contents as html
124: *
125: */
126: public String getHTML() {
127: return DOM.getInnerHTML(getFace());
128: }
129:
130: /**
131: * Gets the face's contents as text.
132: *
133: * @return face's contents as text
134: *
135: */
136: public String getText() {
137: return DOM.getInnerText(getFace());
138: }
139:
140: /**
141: * Set the face's contents as html.
142: *
143: * @param html html to set as face's contents html
144: *
145: */
146: public void setHTML(String html) {
147: face = DOM.createDiv();
148: UIObject.setStyleName(face, STYLENAME_HTML_FACE, true);
149: DOM.setInnerHTML(face, html);
150: updateButtonFace();
151: }
152:
153: /**
154: * Set the face's contents as an image.
155: *
156: * @param image image to set as face contents
157: */
158: public final void setImage(Image image) {
159: face = image.getElement();
160: updateButtonFace();
161: }
162:
163: /**
164: * Sets the face's contents as text.
165: *
166: * @param text text to set as face's contents
167: */
168: public final void setText(String text) {
169: face = DOM.createDiv();
170: UIObject.setStyleName(face, STYLENAME_HTML_FACE, true);
171: DOM.setInnerText(face, text);
172: updateButtonFace();
173: }
174:
175: @Override
176: public final String toString() {
177: return this .getName();
178: }
179:
180: /**
181: * Gets the ID associated with this face. This will be a bitwise and of all
182: * of the attributes that comprise this face.
183: */
184: abstract int getFaceID();
185:
186: /**
187: * Get the name of the face. This property is also used as a modifier on the
188: * <code>CustomButton</code> style. <p/> For instance, if the
189: * <code>CustomButton</code> style is "gwt-PushButton" and the face name
190: * is "up", then the CSS class name will be "gwt-PushButton-up".
191: *
192: * @return the face's name
193: */
194: abstract String getName();
195:
196: /**
197: * Gets the contents associated with this face.
198: */
199: private Element getFace() {
200: if (face == null) {
201: if (delegateTo == null) {
202: // provide a default face as none was supplied.
203: face = DOM.createDiv();
204: return face;
205: } else {
206: return delegateTo.getFace();
207: }
208: } else {
209: return face;
210: }
211: }
212:
213: private void updateButtonFace() {
214: if (curFace != null && curFace.getFace() == this .getFace()) {
215: setCurrentFaceElement(face);
216: }
217: }
218: }
219:
220: private static final String STYLENAME_DEFAULT = "gwt-CustomButton";
221:
222: /**
223: * Pressed Attribute bit.
224: */
225: private static final int DOWN_ATTRIBUTE = 1;
226:
227: /**
228: * Hovering Attribute bit.
229: */
230: private static final int HOVERING_ATTRIBUTE = 2;
231:
232: /**
233: * Disabled Attribute bit.
234: */
235: private static final int DISABLED_ATTRIBUTE = 4;
236:
237: /**
238: * ID for up face.
239: */
240: private static final int UP = 0;
241:
242: /**
243: * ID for down face.
244: */
245: private static final int DOWN = DOWN_ATTRIBUTE;
246:
247: /**
248: * ID for upHovering face.
249: */
250: private static final int UP_HOVERING = HOVERING_ATTRIBUTE;
251:
252: /**
253: * ID for downHovering face.
254: */
255: private static final int DOWN_HOVERING = DOWN_ATTRIBUTE
256: | HOVERING_ATTRIBUTE;
257:
258: /**
259: * ID for upDisabled face.
260: */
261: private static final int UP_DISABLED = DISABLED_ATTRIBUTE;
262:
263: /**
264: * ID for downDisabled face.
265: */
266: private static final int DOWN_DISABLED = DOWN | DISABLED_ATTRIBUTE;
267:
268: /**
269: * The button's current face element.
270: */
271: private Element curFaceElement;
272:
273: /**
274: * The button's current face.
275: */
276: private Face curFace;
277:
278: /**
279: * Face for up.
280: */
281: private Face up;
282:
283: /**
284: * Face for down.
285: */
286: private Face down;
287:
288: /**
289: * Face for downHover.
290: */
291: private Face downHovering;
292:
293: /**
294: * Face for upHover.
295: */
296: private Face upHovering;
297:
298: /**
299: * Face for upDisabled.
300: */
301: private Face upDisabled;
302:
303: /**
304: * Face for downDisabled.
305: */
306: private Face downDisabled;
307:
308: /**
309: * If <code>true</code>, this widget is capturing with the mouse held down.
310: */
311: private boolean isCapturing;
312:
313: /**
314: * If <code>true</code>, this widget has focus with the space bar down.
315: */
316: private boolean isFocusing;
317:
318: /**
319: * Constructor for <code>CustomButton</code>.
320: *
321: * @param upImage image for the default (up) face of the button
322: */
323: public CustomButton(Image upImage) {
324: this ();
325: getUpFace().setImage(upImage);
326: }
327:
328: /**
329: * Constructor for <code>CustomButton</code>.
330: *
331: * @param upImage image for the default (up) face of the button
332: * @param downImage image for the down face of the button
333: */
334: public CustomButton(Image upImage, Image downImage) {
335: this (upImage);
336: getDownFace().setImage(downImage);
337: }
338:
339: /**
340: * Constructor for <code>CustomButton</code>.
341: *
342: * @param upImage image for the default (up) face of the button
343: * @param downImage image for the down face of the button
344: * @param listener clickListener
345: */
346: public CustomButton(Image upImage, Image downImage,
347: ClickListener listener) {
348: this (upImage, listener);
349: getDownFace().setImage(downImage);
350: }
351:
352: /**
353: * Constructor for <code>CustomButton</code>.
354: *
355: * @param upImage image for the default (up) face of the button
356: * @param listener the click listener
357: */
358: public CustomButton(Image upImage, ClickListener listener) {
359: this (upImage);
360: addClickListener(listener);
361: }
362:
363: /**
364: * Constructor for <code>CustomButton</code>.
365: *
366: * @param upText the text for the default (up) face of the button.
367: */
368: public CustomButton(String upText) {
369: this ();
370: getUpFace().setText(upText);
371: }
372:
373: /**
374: * Constructor for <code>CustomButton</code>.
375: *
376: * @param upText the text for the default (up) face of the button
377: * @param listener the click listener
378: */
379: public CustomButton(String upText, ClickListener listener) {
380: this (upText);
381: addClickListener(listener);
382: }
383:
384: /**
385: * Constructor for <code>CustomButton</code>.
386: *
387: * @param upText the text for the default (up) face of the button
388: * @param downText the text for the down face of the button
389: */
390: public CustomButton(String upText, String downText) {
391: this (upText);
392: getDownFace().setText(downText);
393: }
394:
395: /**
396: * Constructor for <code>CustomButton</code>.
397: *
398: * @param upText the text for the default (up) face of the button
399: * @param downText the text for the down face of the button
400: * @param listener the click listener
401: */
402: public CustomButton(String upText, String downText,
403: ClickListener listener) {
404: this (upText, downText);
405: addClickListener(listener);
406: }
407:
408: /**
409: * Constructor for <code>CustomButton</code>.
410: */
411: protected CustomButton() {
412: // Use FocusPanel.impl rather than FocusWidget because only FocusPanel.impl
413: // works across browsers to create a focusable element.
414: super (FocusPanel.impl.createFocusable());
415: sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS
416: | Event.FOCUSEVENTS);
417: setUpFace(createFace(null, "up", UP));
418: setStyleName(STYLENAME_DEFAULT);
419: }
420:
421: /**
422: * Gets the downDisabled face of the button.
423: *
424: * @return the downDisabled face
425: */
426: public final Face getDownDisabledFace() {
427: if (downDisabled == null) {
428: setDownDisabledFace(createFace(getDownFace(),
429: "down-disabled", DOWN_DISABLED));
430: }
431: return downDisabled;
432: }
433:
434: /**
435: * Gets the down face of the button.
436: *
437: * @return the down face
438: */
439: public final Face getDownFace() {
440: if (down == null) {
441: setDownFace(createFace(getUpFace(), "down", DOWN));
442: }
443: return down;
444: }
445:
446: /**
447: * Gets the downHovering face of the button.
448: *
449: * @return the downHovering face
450: */
451: public final Face getDownHoveringFace() {
452: if (downHovering == null) {
453: setDownHoveringFace(createFace(getDownFace(),
454: "down-hovering", DOWN_HOVERING));
455: }
456: return downHovering;
457: }
458:
459: /**
460: * Gets the current face's html.
461: *
462: * @return current face's html
463: */
464: @Override
465: public String getHTML() {
466: return getCurrentFace().getHTML();
467: }
468:
469: @Override
470: public int getTabIndex() {
471: return FocusPanel.impl.getTabIndex(getElement());
472: }
473:
474: /**
475: * Gets the current face's text.
476: *
477: * @return current face's text
478: */
479: @Override
480: public String getText() {
481: return getCurrentFace().getText();
482: }
483:
484: /**
485: * Gets the upDisabled face of the button.
486: *
487: * @return the upDisabled face
488: */
489: public final Face getUpDisabledFace() {
490: if (upDisabled == null) {
491: setUpDisabledFace(createFace(getUpFace(), "up-disabled",
492: UP_DISABLED));
493: }
494: return upDisabled;
495: }
496:
497: /**
498: * Gets the up face of the button.
499: *
500: * @return the up face
501: */
502: public final Face getUpFace() {
503: return up;
504: }
505:
506: /**
507: * Gets the upHovering face of the button.
508: *
509: * @return the upHovering face
510: */
511: public final Face getUpHoveringFace() {
512: if (upHovering == null) {
513: setUpHoveringFace(createFace(getUpFace(), "up-hovering",
514: UP_HOVERING));
515: }
516: return upHovering;
517: }
518:
519: @Override
520: public void onBrowserEvent(Event event) {
521: // Should not act on button if disabled.
522: if (isEnabled() == false) {
523: // This can happen when events are bubbled up from non-disabled children
524: return;
525: }
526:
527: int type = DOM.eventGetType(event);
528: switch (type) {
529: case Event.ONMOUSEDOWN:
530: setFocus(true);
531: onClickStart();
532: DOM.setCapture(getElement());
533: isCapturing = true;
534: // Prevent dragging (on some browsers);
535: DOM.eventPreventDefault(event);
536: break;
537: case Event.ONMOUSEUP:
538: if (isCapturing) {
539: isCapturing = false;
540: DOM.releaseCapture(getElement());
541: if (isHovering()) {
542: onClick();
543: }
544: }
545: break;
546: case Event.ONMOUSEMOVE:
547: if (isCapturing) {
548: // Prevent dragging (on other browsers);
549: DOM.eventPreventDefault(event);
550: }
551: break;
552: case Event.ONMOUSEOUT:
553: if (DOM.isOrHasChild(getElement(), DOM
554: .eventGetTarget(event))
555: && !DOM.isOrHasChild(getElement(), DOM
556: .eventGetToElement(event))) {
557: if (isCapturing) {
558: onClickCancel();
559: }
560: setHovering(false);
561: }
562: break;
563: case Event.ONMOUSEOVER:
564: if (DOM.isOrHasChild(getElement(), DOM
565: .eventGetTarget(event))) {
566: setHovering(true);
567: if (isCapturing) {
568: onClickStart();
569: }
570: }
571: break;
572: case Event.ONCLICK:
573: // we handle clicks ourselves
574: return;
575: case Event.ONBLUR:
576: if (isFocusing) {
577: isFocusing = false;
578: onClickCancel();
579: }
580: break;
581: case Event.ONLOSECAPTURE:
582: if (isCapturing) {
583: isCapturing = false;
584: onClickCancel();
585: }
586: break;
587: }
588:
589: super .onBrowserEvent(event);
590:
591: // Synthesize clicks based on keyboard events AFTER the normal key handling.
592: char keyCode = (char) DOM.eventGetKeyCode(event);
593: switch (type) {
594: case Event.ONKEYDOWN:
595: if (keyCode == ' ') {
596: isFocusing = true;
597: onClickStart();
598: }
599: break;
600: case Event.ONKEYUP:
601: if (isFocusing && keyCode == ' ') {
602: isFocusing = false;
603: onClick();
604: }
605: break;
606: case Event.ONKEYPRESS:
607: if (keyCode == '\n' || keyCode == '\r') {
608: onClickStart();
609: onClick();
610: }
611: break;
612: }
613: }
614:
615: @Override
616: public void setAccessKey(char key) {
617: FocusPanel.impl.setAccessKey(getElement(), key);
618: }
619:
620: /**
621: * Sets whether this button is enabled.
622: *
623: * @param enabled <code>true</code> to enable the button, <code>false</code>
624: * to disable it
625: */
626: @Override
627: public final void setEnabled(boolean enabled) {
628: if (isEnabled() != enabled) {
629: toggleDisabled();
630: super .setEnabled(enabled);
631: if (!enabled) {
632: cleanupCaptureState();
633: }
634: }
635: }
636:
637: @Override
638: public void setFocus(boolean focused) {
639: if (focused) {
640: FocusPanel.impl.focus(getElement());
641: } else {
642: FocusPanel.impl.blur(getElement());
643: }
644: }
645:
646: /**
647: * Sets the current face's html.
648: *
649: * @param html html to set
650: */
651: @Override
652: public void setHTML(String html) {
653: getCurrentFace().setHTML(html);
654: }
655:
656: @Override
657: public void setTabIndex(int index) {
658: FocusPanel.impl.setTabIndex(getElement(), index);
659: }
660:
661: /**
662: * Sets the current face's text.
663: *
664: * @param text text to set
665: */
666: @Override
667: public void setText(String text) {
668: getCurrentFace().setText(text);
669: }
670:
671: /**
672: * Is this button down?
673: *
674: * @return <code>true</code> if the button is down
675: */
676: protected boolean isDown() {
677: return (DOWN_ATTRIBUTE & getCurrentFace().getFaceID()) > 0;
678: }
679:
680: /**
681: * Overridden on attach to ensure that a button face has been chosen before
682: * the button is displayed.
683: */
684: @Override
685: protected void onAttach() {
686: finishSetup();
687: super .onAttach();
688: }
689:
690: /**
691: * Called when the user finishes clicking on this button. The default behavior
692: * is to fire the click event to listeners. Subclasses that override
693: * {@link #onClickStart()} should override this method to restore the normal
694: * widget display.
695: */
696: protected void onClick() {
697: fireClickListeners();
698: }
699:
700: /**
701: * Called when the user aborts a click in progress; for example, by dragging
702: * the mouse outside of the button before releasing the mouse button.
703: * Subclasses that override {@link #onClickStart()} should override this
704: * method to restore the normal widget display.
705: */
706: protected void onClickCancel() {
707: }
708:
709: /**
710: * Called when the user begins to click on this button. Subclasses may
711: * override this method to display the start of the click visually; such
712: * subclasses should also override {@link #onClick()} and
713: * {@link #onClickCancel()} to restore normal visual state. Each
714: * <code>onClickStart</code> will eventually be followed by either
715: * <code>onClick</code> or <code>onClickCancel</code>, depending on
716: * whether the click is completed.
717: */
718: protected void onClickStart() {
719: }
720:
721: @Override
722: protected void onDetach() {
723: super .onDetach();
724: cleanupCaptureState();
725: }
726:
727: /**
728: * Sets whether this button is down.
729: *
730: * @param down <code>true</code> to press the button, <code>false</code>
731: * otherwise
732: */
733: protected void setDown(boolean down) {
734: if (down != isDown()) {
735: toggleDown();
736: }
737: }
738:
739: /**
740: * Common setup between constructors.
741: */
742: void finishSetup() {
743: if (curFace == null) {
744: setCurrentFace(getUpFace());
745: }
746: }
747:
748: /**
749: * Gets the current face of the button.
750: *
751: * @return the current face
752: */
753:
754: Face getCurrentFace() {
755: /*
756: * Implementation note: Package protected so we can use it when testing the
757: * button.
758: */
759: finishSetup();
760: return curFace;
761: }
762:
763: /**
764: * Is the mouse hovering over this button?
765: *
766: * @return <code>true</code> if the mouse is hovering
767: */
768: final boolean isHovering() {
769: return (HOVERING_ATTRIBUTE & getCurrentFace().getFaceID()) > 0;
770: }
771:
772: void setCurrentFace(Face newFace) {
773: /*
774: * Implementation note: default access for testing.
775: */
776: if (curFace != newFace) {
777: if (curFace != null) {
778: removeStyleDependentName(curFace.getName());
779: }
780: curFace = newFace;
781: setCurrentFaceElement(newFace.getFace());
782: addStyleDependentName(curFace.getName());
783: }
784: }
785:
786: /**
787: * Sets whether this button is hovering.
788: *
789: * @param hovering is this button hovering?
790: */
791: final void setHovering(boolean hovering) {
792: if (hovering != isHovering()) {
793: toggleHover();
794: }
795: }
796:
797: /**
798: * Toggle the up/down attribute.
799: */
800: void toggleDown() {
801: int newFaceID = getCurrentFace().getFaceID() ^ DOWN_ATTRIBUTE;
802: setCurrentFace(newFaceID);
803: }
804:
805: /**
806: * Resets internal state if this button can no longer service events. This can
807: * occur when the widget becomes detached or disabled.
808: */
809: private void cleanupCaptureState() {
810: if (isCapturing || isFocusing) {
811: DOM.releaseCapture(getElement());
812: isCapturing = false;
813: isFocusing = false;
814: onClickCancel();
815: }
816: }
817:
818: private Face createFace(Face delegateTo, final String name,
819: final int faceID) {
820: return new Face(delegateTo) {
821:
822: @Override
823: public String getName() {
824: return name;
825: }
826:
827: @Override
828: int getFaceID() {
829: return faceID;
830: }
831: };
832: }
833:
834: private Face getFaceFromID(int id) {
835: switch (id) {
836: case DOWN:
837: return getDownFace();
838: case UP:
839: return getUpFace();
840: case DOWN_HOVERING:
841: return getDownHoveringFace();
842: case UP_HOVERING:
843: return getUpHoveringFace();
844: case UP_DISABLED:
845: return getUpDisabledFace();
846: case DOWN_DISABLED:
847: return getDownDisabledFace();
848: default:
849: throw new IllegalStateException(id
850: + " is not a known face id.");
851: }
852: }
853:
854: /**
855: * Sets the current face based on the faceID.
856: *
857: * @param faceID sets the new face of the button
858: */
859: private void setCurrentFace(int faceID) {
860: Face newFace = getFaceFromID(faceID);
861: setCurrentFace(newFace);
862: }
863:
864: private void setCurrentFaceElement(Element newFaceElement) {
865: if (curFaceElement != newFaceElement) {
866: if (curFaceElement != null) {
867: DOM.removeChild(getElement(), curFaceElement);
868: }
869: curFaceElement = newFaceElement;
870: DOM.appendChild(getElement(), curFaceElement);
871: }
872: }
873:
874: /**
875: * Sets the downDisabled face of the button.
876: *
877: * @param downDisabled downDisabled face
878: */
879: private void setDownDisabledFace(Face downDisabled) {
880: this .downDisabled = downDisabled;
881: }
882:
883: /**
884: * Sets the down face of the button.
885: *
886: * @param down the down face
887: */
888: private void setDownFace(Face down) {
889: this .down = down;
890: }
891:
892: /**
893: * Sets the downHovering face of the button.
894: *
895: * @param downHovering hoverDown face
896: */
897: private void setDownHoveringFace(Face downHovering) {
898: this .downHovering = downHovering;
899: }
900:
901: /**
902: * Sets the upDisabled face of the button.
903: *
904: * @param upDisabled upDisabled face
905: */
906: private void setUpDisabledFace(Face upDisabled) {
907: this .upDisabled = upDisabled;
908: }
909:
910: /**
911: * Sets the up face of the button.
912: *
913: * @param up up face
914: */
915: private void setUpFace(Face up) {
916: this .up = up;
917: }
918:
919: /**
920: * Sets the upHovering face of the button.
921: *
922: * @param upHovering upHovering face
923: */
924: private void setUpHoveringFace(Face upHovering) {
925: this .upHovering = upHovering;
926: }
927:
928: /**
929: * Toggle the disabled attribute.
930: */
931: private void toggleDisabled() {
932: // Toggle disabled.
933: int newFaceID = getCurrentFace().getFaceID()
934: ^ DISABLED_ATTRIBUTE;
935:
936: // Remove hovering.
937: newFaceID &= ~HOVERING_ATTRIBUTE;
938:
939: // Sets the current face.
940: setCurrentFace(newFaceID);
941: }
942:
943: /**
944: * Toggle the hovering attribute.
945: */
946: private void toggleHover() {
947: // Toggle hovering.
948: int newFaceID = getCurrentFace().getFaceID()
949: ^ HOVERING_ATTRIBUTE;
950:
951: // Remove disabled.
952: newFaceID &= ~DISABLED_ATTRIBUTE;
953: setCurrentFace(newFaceID);
954: }
955: }
|