UNPKG

blossom

Version:

Modern, Cross-Platform Application Framework

546 lines (451 loc) 16.5 kB
// ========================================================================== // Project: Blossom - Modern, Cross-Platform Application Framework // Copyright: ©2012 Fohr Motion Picture Studios. All rights reserved. // License: Licensed under the GPLv3 license (see BLOSSOM-LICENSE). // ========================================================================== /*globals sc_assert */ sc_require('widgets/widget'); sc_require('mixins/control'); sc_require('mixins/button'); var base03 = "#002b36"; var base02 = "#073642"; var base01 = "#586e75"; var base00 = "#657b83"; var base0 = "#839496"; var base1 = "#93a1a1"; var base2 = "#eee8d5"; var base3 = "#fdf6e3"; var yellow = "#b58900"; var orange = "#cb4b16"; var red = "#dc322f"; var magenta = "#d33682"; var violet = "#6c71c4"; var blue = "#268bd2"; var cyan = "#2aa198"; var green = "#859900"; var white = "white"; SC.CreateRoundRectPath = function(ctx, x, y, width, height, radius) { if (radius === undefined) radius = 5; ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); }; SC.ButtonWidget = SC.Widget.extend(SC.Control, SC.Button, { displayProperties: 'href icon title value toolTip'.w(), render: function(ctx) { // console.log('SC.ButtonWidget#render()', SC.guidFor(this)); var title = this.get('displayTitle') || "(no title)", selected = this.get('isSelected'), disabled = !this.get('isEnabled'), mixed = (selected === SC.MIXED_STATE), active = this.get('isActive'), isDefault = this.get('isDefault'), bounds = this.get('bounds'), w = bounds.width, h = bounds.height; selected = (selected && (selected !== SC.MIXED_STATE)); switch (this.get('theme')) { case 'checkbox': sc_assert(false, "Please use SC.CheckboxWidget instead."); break; case 'radio': sc_assert(false, "Please use SC.RadioWidget instead."); break; case 'square': SC.CreateRoundRectPath(ctx, 1.5, 1.5, w-3, h-3, 0); break; case 'capsule': SC.CreateRoundRectPath(ctx, 0.5, 1.5, w-1, h-3, 12); break; case 'regular': SC.CreateRoundRectPath(ctx, 1.5, 1.5, w-3, h-3, 5); break; default: SC.CreateRoundRectPath(ctx, 1.5, 1.5, w-3, h-3, 5); break; } if ((disabled && !selected) || (disabled && !active && !selected)) { ctx.globalAlpha = 1.0; ctx.fillStyle = base3; ctx.fill(); ctx.globalAlpha = 0.5; ctx.strokeStyle = base03; ctx.lineWidth = isDefault? 2 : 1; ctx.stroke(); ctx.fillStyle = base03; ctx.font = "11pt Helvetica"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.shadowBlur = 0; ctx.shadowColor = "rgba(0,0,0,0)"; ctx.fillText(title || "(no title)", w/2, h/2); } else if (disabled && selected) { ctx.globalAlpha = 0.5; ctx.fillStyle = base03; ctx.fill(); ctx.strokeStyle = base03; ctx.lineWidth = isDefault? 2 : 1; ctx.stroke(); ctx.fillStyle = base3; ctx.font = "11pt Helvetica"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.shadowBlur = 0; ctx.shadowColor = "rgba(0,0,0,0)"; ctx.fillText(title, w/2, h/2); } else if (active || selected) { ctx.fillStyle = base03; ctx.fill(); ctx.strokeStyle = base03; ctx.lineWidth = isDefault? 2 : 1; ctx.stroke(); ctx.fillStyle = base3; ctx.font = "11pt Helvetica"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.shadowBlur = 0; ctx.shadowColor = "rgba(0,0,0,0)"; ctx.fillText(title, w/2, h/2); } else { // console.log('rendering normally'); ctx.globalAlpha = 1.0; ctx.fillStyle = base3; ctx.fill(); ctx.strokeStyle = base03; ctx.lineWidth = isDefault? 2 : 1; ctx.stroke(); ctx.fillStyle = base03; ctx.font = "11pt Helvetica"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.shadowBlur = 0; ctx.shadowColor = "rgba(0,0,0,0)"; ctx.fillText(title || "(no title)", w/2, h/2); } }, /** Optionally set this to the theme you want this button to have. This is used to determine the type of button this is. You generally should set a class name on the HTML with the same value to allow CSS styling. The default SproutCore theme supports "regular", "capsule", "checkbox", and "radio". @property {String} */ theme: 'square', /** Optionally set the behavioral mode of this button. Possible values are: - *SC.PUSH_BEHAVIOR* Pressing the button will trigger an action tied to the button. Does not change the value of the button. - *SC.TOGGLE_BEHAVIOR* Pressing the button will invert the current value of the button. If the button has a mixed value, it will be set to true. - *SC.TOGGLE_ON_BEHAVIOR* Pressing the button will set the current state to true no matter the previous value. - *SC.TOGGLE_OFF_BEHAVIOR* Pressing the button will set the current state to false no matter the previous value. @property {String} */ buttonBehavior: SC.PUSH_BEHAVIOR, /* If buttonBehavior is SC.HOLD_BEHAVIOR, this specifies, in miliseconds, how often to trigger the action. Ignored for other behaviors. @property {Number} */ holdInterval: 100, /** If true, then this button will be triggered when you hit return. This is the same as setting the keyEquivalent to 'return'. This will also apply the "def" classname to the button. @property {Boolean} */ isDefault: false, isDefaultBindingDefault: SC.Binding.oneWay().bool(), /** If true, then this button will be triggered when you hit escape. This is the same as setting the keyEquivalent to 'escape'. @property {Boolean} */ isCancel: false, isCancelBindingDefault: SC.Binding.oneWay().bool(), /** The button href value. This can be used to create localized button href values. Setting an empty or null href will set it to javascript:; @property {String} */ href: '', /** The name of the action you want triggered when the button is pressed. This property is used in conjunction with the target property to execute a method when a regular button is pressed. These properties are not relevant when the button is used in toggle mode. If you do not set a target, then pressing a button will cause the responder chain to search for a view that implements the action you name here. If you set a target, then the button will try to call the method on the target itself. For legacy support, you can also set the action property to a function. Doing so will cause the function itself to be called when the button is clicked. It is generally better to use the target/action approach and to implement your code in a controller of some type. @property {String} */ action: null, /** The target object to invoke the action on when the button is pressed. If you set this target, the action will be called on the target object directly when the button is clicked. If you leave this property set to null, then the button will search the responder chain for a view that implements the action when the button is pressed instead. @property {Object} */ target: null, /** If true, use a focus ring. @property {Boolean} */ supportFocusRing: false, /** Called when the user presses a shortcut key, such as return or cancel, associated with this button. Highlights the button to show that it is being triggered, then, after a delay, performs the button's action. Does nothing if the button is disabled. @param {Event} evt @returns {Boolean} success/failure of the request */ triggerAction: function(evt) { // If this button is disabled, we have nothing to do if (!this.get('isEnabled')) return false; // Set active state of the button so it appears highlighted this.set('isActive', true); // Invoke the actual action method after a small delay to give the user a // chance to see the highlight. This is especially important if the button // closes a pane, for example. this.invokeLater('_sc_triggerActionAfterDelay', 200, evt); return true; }, /** @private Called by triggerAction after a delay; this method actually performs the action and restores the button's state. @param {Event} evt */ _sc_triggerActionAfterDelay: function(evt) { this._sc_action(evt, true); this.didTriggerAction(); this.set('isActive', false); }, /** This method is called anytime the button's action is triggered. You can implement this method in your own subclass to perform any cleanup needed after an action is performed. @property {function} */ didTriggerAction: function() {}, /** The minimum width the button title should consume. This property is used when generating the HTML styling for the title itself. The default width of 80 usually provides a nice looking style, but you can set it to 0 if you want to disable minimum title width. Note that the title width does not exactly match the width of the button itself. Extra padding added by the theme can impact the final total size. @property {Number} */ titleMinWidth: 80, // ................................................................ // INTERNAL SUPPORT /** @private - save keyEquivalent for later use */ init: function() { arguments.callee.base.apply(this, arguments); // Cache the key equivalent. if (this.get('keyEquivalent')) { this._sc_defaultKeyEquivalent = this.get('keyEquivalent'); } }, /** @private {String} used to store a previously defined key equiv */ _sc_defaultKeyEquivalent: null, /** @private Whenever the isDefault or isCancel property changes, update the display and change the keyEquivalent. */ _sc_isDefaultOrCancelDidChange: function() { var isDef = !!this.get('isDefault'), isCancel = !isDef && this.get('isCancel') ; if (this.didChangeFor('defaultCancelChanged','isDefault','isCancel')) { this.triggerRendering(); // make sure to update the UI if (isDef) { this.set('keyEquivalent', 'return'); // change the key equivalent } else if (isCancel) { this.setIfChanged('keyEquivalent', 'escape') ; } else { //restore the default key equivalent this.set('keyEquivalent',this._sc_defaultKeyEquivalent); } } }.observes('isDefault', 'isCancel'), isMouseDown: false, /** @private On mouse down, set active only if enabled. */ mouseDown: function(evt) { // console.log('SC.ButtonWidget#mouseDown()', SC.guidFor(this)); var buttonBehavior = this.get('buttonBehavior'); if (!this.get('isEnabled')) return true ; // handled event, but do nothing this.set('isActive', true); this.isMouseDown = true; if (buttonBehavior === SC.HOLD_BEHAVIOR) { this._sc_action(evt); } else if (!this._sc_isFocused && (buttonBehavior!==SC.PUSH_BEHAVIOR)) { this._sc_isFocused = true ; this.becomeFirstResponder(); } return true ; }, /** @private Remove the active class on mouseExited if mouse is down. */ mouseExited: function(evt) { document.body.style.cursor = "default"; if (this.isMouseDown) { this.set('isActive', false); } return true; }, /** @private If mouse was down and we renter the button area, set the active state again. */ mouseEntered: function(evt) { if (this.get('isEnabled')) document.body.style.cursor = "pointer"; if (this.isMouseDown) { this.set('isActive', true); } return true; }, /** @private ON mouse up, trigger the action only if we are enabled and the mouse was released inside of the view. */ mouseUp: function(evt) { var wasOver = this.get('isActive'); if (this.isMouseDown) this.set('isActive', false); // track independently in case isEnabled has changed this.isMouseDown = false; if (this.get('buttonBehavior') !== SC.HOLD_BEHAVIOR) { if (wasOver && this.get('isEnabled')) this._sc_action(evt); } return true; }, /** @private */ keyDown: function(evt) { // handle tab key if (evt.which === 9) { var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView'); if(view) view.becomeFirstResponder(); else evt.allowDefault(); return true; // handled } else if (evt.which === 13) { this.triggerAction(evt); return true; // handled } else { return false; } }, /** @private Perform an action based on the behavior of the button. - toggle behavior: switch to on/off state - on behavior: turn on. - off behavior: turn off. - otherwise: invoke target/action */ _sc_action: function(evt, skipHoldRepeat) { // console.log('_sc_action'); switch(this.get('buttonBehavior')) { // When toggling, try to invert like values. i.e. 1 => 0, etc. case SC.TOGGLE_BEHAVIOR: var sel = this.get('isSelected') ; if (sel) { this.set('value', this.get('toggleOffValue')) ; } else { this.set('value', this.get('toggleOnValue')) ; } break ; // set value to on. change 0 => 1. case SC.TOGGLE_ON_BEHAVIOR: this.set('value', this.get('toggleOnValue')) ; break ; // set the value to false. change 1 => 0 case SC.TOGGLE_OFF_BEHAVIOR: this.set('value', this.get('toggleOffValue')) ; break ; case SC.HOLD_BEHAVIOR: this._sc_runHoldAction(evt, skipHoldRepeat); break ; // otherwise, just trigger an action if there is one. default: //if (this.action) this.action(evt); this._sc_runAction(evt); } }, /** @private */ _sc_runAction: function(evt) { var action = this.get('action'), target = this.get('target') || null; if (action) { if (this._sc_hasLegacyActionHandler()) { // old school... V this._sc_triggerLegacyActionHandler(evt); } else { SC.app.sendAction(action, target, this, this.get('surface')); } } }, /** @private */ _sc_runHoldAction: function(evt, skipRepeat) { if (this.get('isActive')) { this._sc_runAction(); if (!skipRepeat) { // This run loop appears to only be necessary for testing SC.RunLoop.begin(); this.invokeLater('_sc_runHoldAction', this.get('holdInterval'), evt); SC.RunLoop.end(); } } }, /** @private */ _sc_hasLegacyActionHandler: function() { var action = this.get('action'); if (action && (SC.typeOf(action) === SC.T_FUNCTION)) return true; if (action && (SC.typeOf(action) === SC.T_STRING) && (action.indexOf('.') != -1)) return true; return false; }, /** @private */ _sc_triggerLegacyActionHandler: function( evt ) { if (!this._sc_hasLegacyActionHandler()) return false; var action = this.get('action'); if (SC.typeOf(action) === SC.T_FUNCTION) this.action(evt); if (SC.typeOf(action) === SC.T_STRING) { console.log("this.action = function(e) { return "+ action +"(this, e); };"); // this.action(evt); } }, /** tied to the isEnabled state */ acceptsFirstResponder: function() { return this.get('isEnabled'); }.property('isEnabled'), willBecomeKeyResponderFrom: function(keyView) { // focus the text field. if (!this._sc_isFocused) { this._sc_isFocused = true ; this.becomeFirstResponder(); if (this.get('isVisibleInWindow')) { // var elem=this.$()[0]; // if (elem) elem.focus(); } } }, willLoseKeyResponderTo: function(responder) { if (this._sc_isFocused) this._sc_isFocused = false ; } });