1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 9 /*global ViewBuilder */ 10 sc_require('views/high_light'); 11 /** @class 12 13 A Designer class provides the core editing functionality you need to edit 14 a view in the UI. When your app loads in `design.mode`, a peer Designer 15 instance is created for every view using the class method Designer or 16 `SC.ViewDesigner` if the view class does not define a Designer class. 17 18 Whenever you put your app into design mode, all events will be routed first 19 to the peer designer for an object, which will have an opportunity to 20 prosent a design UI. 21 22 Likewise, the designer palettes provided by the view builder will focus on 23 the designer instead of the view itself. 24 25 ## Designer UI 26 27 The basic ViewDesigner class automatically handles the UI interaction for 28 layout. You can also double click on the view to perform a default action. 29 30 For views with `isContainerView` set to `YES`, double clicking on the view will 31 automatically "focus" the view. This allows you to select the view's 32 children instead of the view itself. 33 34 @extends SC.Object 35 @since SproutCore 1.0 36 */ 37 SC.ViewDesigner = SC.Object.extend( 38 /** @scope SC.ViewDesigner.prototype */ { 39 40 /** The view managed by this designer. */ 41 view: null, 42 43 /** The class for the design. Set when the view is created. */ 44 viewClass: null, 45 46 /** Set to YES if the view is currently selected for editing. */ 47 designIsSelected: NO, 48 49 /** Set to YES if this particular designer should not be enabled. */ 50 designIsEnabled: YES, 51 52 /** 53 The current page. Comes from the view. 54 55 @property {SC.Page} 56 */ 57 page: function() { 58 var v = this.get('view'); 59 return (v) ? v.get('page') : null; 60 }.property('view').cacheable(), 61 62 /** 63 The design controller from the page. Comes from page 64 65 @property {SC.PageDesignController} 66 */ 67 designController: function() { 68 var p = this.get('page'); 69 return (p) ? p.get('designController') : null ; 70 }.property('page').cacheable(), 71 72 /** 73 If set to NO, the default childView encoding will not run. You can use 74 this option, for example, if your view creates its own childViews. 75 76 Alternatively, you can override the `encodeChildViewsDesign()` and 77 `encodeChildViewsLoc()` methods. 78 79 @type Boolean 80 */ 81 encodeChildViews: YES, 82 83 concatenatedProperties: ['designProperties', 'localizedProperties', 'excludeProperties'], 84 85 86 // .......................................................... 87 // SIZE AND POSITIONING SUPPORT 88 // 89 90 /** 91 Set to `NO` to hide horizontal resize handles 92 */ 93 canResizeHorizontal: YES, 94 95 /** 96 Set to `NO` to resize vertical handles 97 */ 98 canResizeVertical: YES, 99 100 /** 101 Allows moving. 102 */ 103 canReposition: YES, 104 105 /** 106 Determines the minimum allowed width 107 */ 108 minWidth: 10, 109 110 /** 111 Determines the minimum allowed height 112 */ 113 minHeight: 10, 114 115 /** 116 Determines maximum allowed width. `null` means no limit 117 */ 118 maxWidth: 100000000, 119 120 /** 121 Determines maximum allowed height. `null` means no limit 122 */ 123 maxHeight: 100000000, 124 125 /** 126 Returns the current layout for the view. Set this property to update 127 the layout. Direct properties are exposed a well. You will usually want 128 to work with those instead. 129 130 @property 131 @type {Hash} 132 */ 133 layout: function(key, value) { 134 var view = this.get('view'); 135 if (!view) return null; 136 137 if (value !== undefined) view.set('layout', value); 138 return view.get('layout'); 139 }.property('view').cacheable(), 140 141 /** 142 The current anchor location. This determines which of the other dimension 143 metrics are actually used to compute the layout. The value may be one of: 144 145 TOP_LEFT, TOP_CENTER, TOP_RIGHT, TOP_HEIGHT, 146 CENTER_LEFT, CENTER_CENTER, CENTER_RIGHT, CENTER_HEIGHT 147 BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT, BOTTOM_HEIGHT, 148 WIDTH_LEFT, WIDTH_CENTER, WIDTH_RIGHT, WIDTH_HEIGHT, 149 null 150 151 @property 152 @type {Number} 153 */ 154 anchorLocation: function(key, value) { 155 var layout = this.get('layout'), 156 K = SC.ViewDesigner, 157 h, v, frame, view, pview, pframe, ret; 158 159 if (!layout) return null; 160 161 // update to refelct new anchor locations... 162 if (value !== undefined) { 163 164 ret = {}; 165 view = this.get('view'); 166 frame = view.get('frame'); 167 pview = view.get('parentView'); 168 pframe = pview ? pview.get('frame') : null; 169 if (!pframe) pframe = SC.RootResponder.responder.computeWindowSize(); 170 171 // compute new layout in each direction 172 if (value & K.ANCHOR_LEFT) { 173 ret.left = frame.x; 174 ret.width = frame.width; 175 176 } else if (value & K.ANCHOR_RIGHT) { 177 ret.right = (pframe.width - SC.maxX(frame)); 178 ret.width = frame.width; 179 180 } else if (value & K.ANCHOR_CENTERX) { 181 ret.centerX = Math.round(SC.midX(frame) - (pframe.width/2)) ; 182 ret.width = frame.width; 183 184 } else if (value & K.ANCHOR_WIDTH) { 185 ret.left = frame.x; 186 ret.right = (pframe.width - SC.maxX(frame)); 187 } 188 189 // vertical 190 if (value & K.ANCHOR_TOP) { 191 ret.top = frame.y; 192 ret.height = frame.height; 193 194 } else if (value & K.ANCHOR_BOTTOM) { 195 ret.bottom = (pframe.height - SC.maxY(frame)); 196 ret.height = frame.height; 197 198 } else if (value & K.ANCHOR_CENTERY) { 199 ret.centerY = Math.round(SC.midY(frame) - (pframe.height/2)) ; 200 ret.height = frame.height; 201 202 } else if (value & K.ANCHOR_HEIGHT) { 203 ret.top = frame.y; 204 ret.bottom = (pframe.height - SC.maxY(frame)); 205 } 206 207 this.set('layout', ret); 208 layout = ret ; 209 } 210 211 if (!SC.none(layout.left)) { 212 h = SC.none(layout.width) ? K.ANCHOR_WIDTH : K.ANCHOR_LEFT; 213 } else if (!SC.none(layout.right)) h = K.ANCHOR_RIGHT; 214 else if (!SC.none(layout.centerX)) h = K.ANCHOR_CENTERX; 215 else h = 0; 216 217 if (!SC.none(layout.top)) { 218 v = SC.none(layout.height) ? K.ANCHOR_HEIGHT : K.ANCHOR_TOP; 219 } else if (!SC.none(layout.bottom)) v = K.ANCHOR_BOTTOM ; 220 else if (!SC.none(layout.centerY)) v = K.ANCHOR_CENTERY ; 221 else v = 0; 222 223 return v | h; 224 }.property('layout').cacheable(), 225 226 _layoutProperty: function(key, value) { 227 var layout = this.get('layout'); 228 if (!layout) return null; 229 230 if (!SC.none(layout) && (value !== undefined)) { 231 layout = SC.copy(layout); 232 layout[key] = value; 233 this.set('layout', layout); 234 } 235 236 return layout[key]; 237 }, 238 239 /** 240 Returns the top offset of the current layout or `null` if not defined 241 */ 242 layoutTop: function(key, value) { 243 return this._layoutProperty('top', value); 244 }.property('layout').cacheable(), 245 246 /** 247 Returns the bottom offset of the current layout or `null` if not defined 248 */ 249 layoutBottom: function(key, value) { 250 return this._layoutProperty('bottom', value); 251 }.property('layout').cacheable(), 252 253 /** 254 Returns the centerY offset of the current layout or `null` if not defined 255 */ 256 layoutCenterY: function(key, value) { 257 return this._layoutProperty('centerY', value); 258 }.property('layout').cacheable(), 259 260 /** 261 Returns the height offset of the current layout or null if not defined 262 */ 263 layoutHeight: function(key, value) { 264 return this._layoutProperty('height', value); 265 }.property('layout').cacheable(), 266 267 /** 268 Returns the top offset of the current layout or `null` if not defined 269 */ 270 layoutTop: function(key, value) { 271 return this._layoutProperty('top', value); 272 }.property('layout').cacheable(), 273 274 /** 275 Returns the left offset of the current layout or `null` if not defined 276 */ 277 layoutLeft: function(key, value) { 278 return this._layoutProperty('left', value); 279 }.property('layout').cacheable(), 280 281 /** 282 Returns the right offset of the current layout or `null` if not defined 283 */ 284 layoutRight: function(key, value) { 285 return this._layoutProperty('right', value); 286 }.property('layout').cacheable(), 287 288 /** 289 Returns the centerX offset of the current layout or `null` if not defined 290 */ 291 layoutCenterX: function(key, value) { 292 return this._layoutProperty('centerX', value); 293 }.property('layout').cacheable(), 294 295 /** 296 Returns the width offset of the current layout or `null` if not defined 297 */ 298 layoutWidth: function(key, value) { 299 return this._layoutProperty('width', value); 300 }.property('layout').cacheable(), 301 302 // .......................................................... 303 // GENERIC PROPERTIES 304 // 305 // Adds support for adding generic properties to a view. These will 306 // overwrite whatever you write out using specifically supported props. 307 308 // .......................................................... 309 // HANDLE ENCODING OF VIEW DESIGN 310 // 311 312 /** 313 Encodes any simple properties that can just be copied from the view onto 314 the coder. This is used by `encodeDesignProperties()` and 315 `encodeLocalizedProperties()`. 316 */ 317 encodeSimpleProperties: function(props, coder) { 318 var view = this.get('view'), proto = this.get('viewClass').prototype ; 319 props.forEach(function(prop) { 320 var val = view[prop] ; // avoid get() since we don't want to exec props 321 322 //handle bindings 323 if (prop.length > 7 && prop.slice(-7) === "Binding" && val !== undefined){ 324 coder.js(prop,val.encodeDesign()); 325 } 326 else{ 327 if (val !== undefined && (val !== proto[prop])) { 328 coder.encode(prop, val) ; 329 } 330 } 331 }, this); 332 }, 333 334 335 /** 336 Array of properties that can be encoded directly. This is an easy way to 337 add support for simple properties that need to be written to the design 338 without added code. These properties will be encoded by 339 `encodeDesignProperties()`. 340 341 You can add to this array in your subclasses. 342 */ 343 designProperties: ['layout', 'isVisible', 'isEnabled', 'styleClass'], 344 345 346 /* 347 Array of properties specifically not displayed in the editable properties 348 list 349 */ 350 351 excludeProperties: ['layout', 'childViews'], 352 353 354 /* 355 Array of properties available to edit in greenhouse 356 357 */ 358 editableProperties: function(){ 359 360 var con = this.get('designAttrs'), 361 view = this.get('view'), 362 ret = [], 363 designProperties = this.get('designProperties'), 364 excludeProperties = this.get('excludeProperties'); 365 if(con) con = con[0]; 366 for(var i in con){ 367 if(con.hasOwnProperty(i) && excludeProperties.indexOf(i) < 0){ 368 if(!SC.none(view[i])) ret.pushObject(SC.Object.create({value: view[i], key: i, view: view})); 369 } 370 } 371 designProperties.forEach(function(k){ 372 if(excludeProperties.indexOf(k) < 0){ 373 ret.pushObject(SC.Object.create({value: view[k], key: k, view: view})); 374 } 375 }); 376 377 return ret; 378 }.property('designProperties').cacheable(), 379 380 381 /** 382 Invoked by a design coder to encode design properties. The default 383 implementation invoked `encodeDesignProperties()` and 384 `encodeChildViewsDesign()`. You can override this method with your own 385 additional encoding if you like. 386 */ 387 encodeDesign: function(coder) { 388 coder.set('className', SC._object_className(this.get('viewClass'))); 389 this.encodeDesignProperties(coder); 390 this.encodeDesignAttributeProperties(coder); 391 this.encodeChildViewsDesign(coder); 392 return YES ; 393 }, 394 395 /** 396 Encodes the design properties for the view. These properties are simply 397 copied from the view onto the coder. As an optimization, the value of 398 each property will be checked against the default value in the class. If 399 they match, the property will not be emitted. 400 */ 401 encodeDesignProperties: function(coder) { 402 return this.encodeSimpleProperties(this.get('designProperties'), coder); 403 }, 404 405 406 encodeDesignAttributeProperties: function(coder){ 407 var designProps = this.get('designProperties'), 408 designAttrs = this.get('designAttrs'), 409 simpleProps = []; 410 411 if(designAttrs) designAttrs = designAttrs[0]; 412 413 for(var attr in designAttrs){ 414 if(designAttrs.hasOwnProperty(attr) && designProps.indexOf(attr) < 0 && attr !== 'childViews'){ 415 simpleProps.push(attr); 416 } 417 } 418 return this.encodeSimpleProperties(simpleProps, coder); 419 }, 420 421 /** 422 Encodes the design for child views. The default implementation loops 423 through child views. If you store your child views elsewhere in your 424 config (for example as named properties), then you may want to override 425 this method with your own encoding. 426 */ 427 encodeChildViewsDesign: function(coder) { 428 if (!this.get('encodeChildViews')) return; 429 var view = this.view, childViews = view.get('childViews'); 430 if (childViews.length>0) coder.object('childViews', childViews); 431 }, 432 433 /** 434 Array of localized that can be encoded directly. This is an easy way to 435 add support for simple properties that need to be written to the 436 localization without added code. These properties will be encoded by 437 `encodeLocalizedProperties()`. 438 439 You can add to this array in your subclasses. 440 */ 441 localizedProperties: [], 442 443 /** 444 Invoked by a localization coder to encode design properties. The default 445 implementation invoked `encodeLocalizedProperties()` and 446 `encodeChildViewsLoc()`. You can override this method with your own 447 additional encoding if you like. 448 */ 449 encodeLoc: function(coder) { 450 coder.set('className', SC._object_className(this.get('viewClass'))); 451 this.encodeLocalizedProperties(coder); 452 this.encodeChildViewsLoc(coder); 453 return YES ; 454 }, 455 456 /** 457 Encodes the localized properties for the view. These properties are 458 simply copied from the view onto the coder. As an optimization, the value 459 of each property will be checked against the default value in the class. 460 If they match, the property will not be emitted. 461 */ 462 encodeLocalizedProperties: function(coder) { 463 return this.encodeSimpleProperties(this.get('localizedProperties'),coder); 464 }, 465 466 /** 467 Encodes the design for child views. The default implementation loops 468 through child views. If you store your child views elsewhere in your 469 config (for example as named properties), then you may want to override 470 this method with your own encoding. 471 */ 472 encodeChildViewsLoc: function(coder) { 473 if (!this.get('encodeChildViews')) return; 474 var view = this.view, childViews = view.childViews; 475 if (childViews.length>0) coder.object('childViews', childViews); 476 }, 477 478 /** 479 This method is invoked when the designer is instantiated. You can use 480 this method to reload any state saved in the view. This method is called 481 before any observers or bindings are setup to give you a chance to 482 configure the initial state of the designer. 483 */ 484 awakeDesign: function() {}, 485 486 487 /** 488 over-ride this method in your designers to customize drop operations 489 default just calls appendChild 490 491 TODO: Come up with a better name for this method. 492 */ 493 addView: function(view){ 494 this.view.appendChild(view); 495 }, 496 497 // .......................................................... 498 // VIEW RELAYING 499 // 500 // View property changes relay automatically... 501 502 /** 503 Invoked whenever the view changes. This will observe all property 504 changes on the new view. 505 */ 506 viewDidChange: function() { 507 var view = this.get('view'), old = this._designer_view ; 508 if (view === old) return; // nothing to do 509 510 var func = this.viewPropertyDidChange ; 511 if (old) old.removeObserver('*', this, func); 512 this._designer_view = view ; 513 if (view) view.addObserver('*', this, func); 514 this.viewPropertyDidChange(view, '*', null, null); 515 }.observes('view'), 516 517 /** 518 Invoked whenever a property on the view has changed. The passed key will 519 be '*' when the entire view has changed. The default implementation here 520 will notify the property as changed on the receiver if the 521 property value is undefined on the receiver. 522 523 It will notify all properties changed for '*'. You may override this 524 method with your own behavior if you like. 525 */ 526 viewPropertyDidChange: function(view, key) { 527 if (key === '*') this.allPropertiesDidChange(); 528 else if (this[key] === undefined) this.notifyPropertyChange(key) ; 529 530 if ((key === '*') || (key === 'layout')) { 531 if (this.get('designIsSelected') && this._handles) { 532 this._handles.set('layout', SC.clone(view.get('layout'))); 533 } 534 } 535 }, 536 537 /** 538 The `unknownProperty` handler will pass through to the view by default. 539 This will often provide you the support you need without needing to 540 customize the Designer. Just make sure you don't define a conflicting 541 property name on the designer itself! 542 */ 543 unknownProperty: function(key, value) { 544 if (value !== undefined) { 545 this.view.set(key, value); 546 return value ; 547 } else return this.view.get(key); 548 }, 549 550 // ...................................... 551 // PRIVATE METHODS 552 // 553 554 init: function() { 555 556 // setup design from view state... 557 this.awakeDesign(); 558 559 // setup bindings, etc 560 sc_super(); 561 562 // now add observer for property changes on view to relay change out. 563 this.viewDidChange(); 564 565 // and register with designController, if defined... 566 var c= this.get('designController'); 567 if (c) c.registerDesigner(this) ; 568 569 }, 570 571 destroy: function() { 572 sc_super(); 573 this.set('view', null); // clears the view observer... 574 }, 575 576 designIsSelectedDidChange: function() { 577 if (SC.kindOf(this.view, SC.Pane)) return this ; 578 579 var isSel = this.get('designIsSelected'); 580 var handles = this._handles; 581 582 if (isSel) { 583 584 if (!handles) { 585 handles = this._handles = SC.SelectionHandlesView.create({ 586 designer: this 587 }); 588 } 589 590 var parent = this.view.get('parentView'); 591 if (!handles.get('parentView') !== parent) parent.appendChild(handles); 592 handles.set('layout', this.view.get('layout')); 593 } else if (handles) { 594 if (handles.get('parentView')) handles.removeFromParent(); 595 } 596 }.observes('designIsSelected'), 597 598 tryToPerform: function(methodName, arg1, arg2) { 599 // only handle event if we are in design mode 600 var page = this.view ? this.view.get('page') : null ; 601 var isDesignMode = page ? page.get('needsDesigner') || page.get('isDesignMode') : NO ; 602 603 // if we are in design mode, route event handling to the designer 604 // otherwise, invoke default method. 605 if (isDesignMode) { 606 return sc_super(); 607 } else { 608 return SC.Object.prototype.tryToPerform.apply(this.view, arguments); 609 } 610 }, 611 612 // .......................................................... 613 // DRAWING SUPPORT 614 // 615 616 /** 617 Update the layer to add any design-specific marking 618 */ 619 didCreateLayer: function() {}, 620 621 /** 622 Update the layer to add any design-specific marking 623 */ 624 didUpdateLayer: function() {}, 625 626 /** 627 Update the layer to add any design-specific marking 628 */ 629 willDestroyLayer: function() {}, 630 631 // .......................................................... 632 // ROOT DESIGNER SUPPORT 633 // 634 635 parentDesignerIsRoot: function(){ 636 var dc = this.get('designController'), view = this.get('view'); 637 return dc.get('rootDesigner') === view.getPath('parentView.designer'); 638 }.property(), 639 640 /** 641 set this property to `YES` if you want your designer to become Root 642 */ 643 acceptRootDesigner: NO, 644 645 isRootDesigner: NO, 646 647 isRootDesignerDidChange: function() { 648 649 var isRoot = this.get('isRootDesigner'), 650 highLight = this._highLight; 651 652 if (isRoot && this.get('designIsEnabled')) { 653 654 if (!highLight) { 655 highLight = this._highLight = SC.RootDesignerHighLightView.create({ 656 designer: this 657 }); 658 } 659 660 var parent = this.view.get('parentView'); 661 highLight.set('targetFrame', this.view.get('frame')); 662 663 if (!highLight.get('parentView') !== parent) parent.insertBefore(highLight,this.view); 664 } 665 else if (highLight) { 666 if (highLight.get('parentView')) highLight.removeFromParent(); 667 } 668 }.observes('isRootDesigner'), 669 670 resignRootDesigner: function(){ 671 var prevRoot = this.get('prevRootDesigner'); 672 if(this.get('isRootDesigner') && prevRoot){ 673 var dc = this.get('designController'); 674 if(dc) dc.makeRootDesigner(prevRoot); 675 } 676 }, 677 678 shouldReleaseRootDesigner: function(evt){ 679 var frame = this.view.get('frame'); 680 if(this.get('isRootDesigner') && !SC.pointInRect({ x: evt.pageX, y: evt.pageY }, frame)){ 681 this.resignRootDesigner(); 682 return YES; 683 } 684 return NO; 685 }, 686 687 // .......................................................... 688 // MOUSE HANDLING 689 // 690 691 HANDLE_MARGIN: 10, 692 693 /** 694 Select on `mouseDown`. If `metaKey` or `shiftKey` is pressed, add to 695 selection. Otherwise just save starting info for dragging 696 */ 697 mouseDown: function(evt) { 698 this.shouldReleaseRootDesigner(evt); 699 if (!this.get('designIsEnabled') || !this.get('parentDesignerIsRoot')) return NO ; 700 701 // save mouse down info 702 var view = this.get('view'), 703 info, vert, horiz, repos, frame, pview, margin, canH, canV; 704 705 if (!view) return NO; // nothing to do 706 707 // save mouse down state for later use 708 this._mouseDownInfo = info = { 709 layout: SC.copy(view.get('layout')), 710 selected: this.get('designIsSelected'), 711 dragged: NO, 712 metaKey: evt.metaKey || evt.shiftKey, 713 source: this, 714 x: evt.pageX, y: evt.pageY 715 }; 716 info.hanchor = info.vanchor = info.reposition = NO; 717 718 // detect what operations are available. 719 repos = this.get('canReposition'); 720 horiz = vert = NO ; 721 if (info.selected) { 722 frame = view.get('frame'); 723 pview = view.get('parentView'); 724 if (frame && pview) frame = pview.convertFrameToView(frame, null); 725 726 margin = this.HANDLE_MARGIN; 727 728 // detect if we are in any hotzones 729 if (frame) { 730 if (Math.abs(info.x - SC.minX(frame)) <= margin) { 731 horiz = "left"; 732 } else if (Math.abs(info.x - SC.maxX(frame)) <= margin) { 733 horiz = "right"; 734 } 735 736 if (Math.abs(info.y - SC.minY(frame)) <= margin) { 737 vert = "top"; 738 } else if (Math.abs(info.y - SC.maxY(frame)) <= margin) { 739 vert = "bottom"; 740 } 741 } 742 743 canH = this.get('canResizeHorizontal'); 744 canV = this.get('canResizeVertical'); 745 746 // look for corners if can resize in both directions... 747 if (canH && canV) { 748 if (!vert || !horiz) vert = horiz = NO ; 749 750 // if can only resize horizonal - must be in middle vertical 751 } else if (canH) { 752 vert = NO ; 753 if (Math.abs(info.y - SC.midY(frame)) > margin) horiz = NO; 754 755 // if can only resize vertical - must be in middle horizontal 756 } else if (canV) { 757 horiz = NO ; 758 if (Math.abs(info.x - SC.midX(frame)) > margin) vert = NO ; 759 760 // otherwise, do not allow resizing 761 } else horiz = vert = NO ; 762 } 763 764 // now save settings... 765 if (horiz) info.hanchor = horiz ; 766 if (vert) info.vanchor = vert ; 767 if (!horiz && !vert && repos) info.reposition = YES ; 768 769 // if not yet selected, select item immediately. This way future events 770 // will be handled properly 771 if (!info.selected) { 772 this.get('designController').select(this, info.metaKey); 773 } 774 775 // save initial info on all selected items 776 if (info.reposition) this.get('designController').prepareReposition(info); 777 778 return YES ; 779 }, 780 781 prepareReposition: function(info) { 782 var view = this.get('view'), 783 layout = view ? SC.copy(view.get('layout')) : {}; 784 info[SC.keyFor('layout', SC.guidFor(this))] = layout; 785 return this ; 786 }, 787 788 /** 789 mouse dragged will resize or reposition depending on the settings from 790 mousedown. 791 */ 792 mouseDragged: function(evt) { 793 if (!this.get('designIsEnabled') || !this.get('parentDesignerIsRoot')) return NO ; 794 var info = this._mouseDownInfo, 795 view = this.get('view'), 796 layout, startX, startY; 797 //do some binding!!! 798 if(evt.altKey && SC._Greenhouse){ 799 startX = evt.pageX; 800 startY = evt.pageY; 801 802 var dragLink = SC.DrawingView.create({ 803 layout: {left: 0, top: 0, right: 0, bottom: 0}, 804 startPoint: {x: startX, y: startY}, 805 endPoint: {x: startX, y: startY}, 806 // private update 807 _pointsDidChange: function(){ 808 var sp = this.get('startPoint'), 809 ep = this.get('endPoint'), 810 xDiff, yDiff, newLink; 811 812 xDiff = Math.abs(sp.x - ep.x); 813 yDiff = Math.abs(sp.y - ep.y); 814 if (xDiff > 5 || yDiff > 5){ 815 newLink = {}; 816 newLink.shape = SC.LINE; 817 newLink.start = {x: sp.x, y: sp.y}; 818 newLink.end = {x: ep.x, y: ep.y}; 819 newLink.style = { color: 'green', width: 3 }; 820 this.setIfChanged('shapes', [newLink]); 821 } 822 }.observes('startPoint', 'endPoint') 823 }); 824 SC.designPage.get('designMainPane').appendChild(dragLink); 825 826 SC.Drag.start({ 827 event: evt, 828 source: this, 829 dragLink: dragLink, 830 dragView: SC.View.create({ layout: {left: 0, top: 0, width: 0, height: 0}}), 831 ghost: NO, 832 slideBack: YES, 833 dataSource: this, 834 anchorView: view 835 }); 836 } 837 //normal drag 838 else{ 839 if (view && (info.hanchor || info.vanchor)) { 840 layout = SC.copy(this.get('layout')); 841 if (info.hanchor) this._mouseResize(evt, info, this.HKEYS, layout); 842 if (info.vanchor) this._mouseResize(evt, info, this.VKEYS, layout); 843 this.set('layout', layout); 844 845 } else if (info.reposition) { 846 this.get('designController').repositionSelection(evt, info); 847 } 848 } 849 850 }, 851 852 // .......................................................... 853 // Drag source and drag data source 854 // 855 dragDataTypes: ['SC.Binding'], 856 857 dragDataForType: function(drag, dataType) { 858 return dataType === 'SC.Binding' ? this.get('view') : null; 859 }, 860 861 /** 862 On `mouseUp` potentially change selection and cleanup. 863 */ 864 mouseUp: function(evt) { 865 if (!this.get('designIsEnabled') || !this.get('parentDesignerIsRoot')) return NO ; 866 867 var info = this._mouseDownInfo; 868 869 // if selected on mouse down and we didn't do any dragging, then deselect. 870 if (info.selected && !info.dragged) { 871 872 // is the mouse still inside the view? If not, don't do anything... 873 var view = this.get('view'), 874 frame = view ? view.get('frame') : null, 875 pview = view.get('parentView'); 876 877 if (frame && pview) frame = pview.convertFrameToView(frame, null); 878 879 if (!frame || SC.pointInRect({ x: evt.pageX, y: evt.pageY }, frame)) { 880 var controller = this.get('designController'); 881 if (info.metaKey) controller.deselect(this); 882 else controller.select(this, NO); 883 } 884 } 885 //double click 886 if(SC._Greenhouse && evt.clickCount === 2){ 887 var dc = this.get('designController'); 888 if(this.acceptRootDesigner && dc) { 889 dc.makeRootDesigner(this); 890 } 891 else{ 892 //TODO: [MB] decide if this is the functionality I want... 893 SC._Greenhouse.sendAction('openInspector', view); 894 } 895 } 896 897 this._mouseDownInfo = null; 898 899 return YES ; 900 }, 901 902 /** 903 Called by `designerController` to reposition the view 904 */ 905 mouseReposition: function(evt, info) { 906 var layout = SC.copy(this.get('layout')); 907 this._mouseReposition(evt, info, this.HKEYS, layout); 908 this._mouseReposition(evt, info, this.VKEYS, layout); 909 this.set('layout', layout); 910 return this; 911 }, 912 913 HKEYS: { 914 evtPoint: "pageX", 915 point: "x", 916 min: "minWidth", 917 max: "maxWidth", 918 head: "left", 919 tail: "right", 920 center: "centerX", 921 size: "width", 922 anchor: "hanchor" 923 }, 924 925 VKEYS: { 926 evtPoint: "pageY", 927 point: "y", 928 min: "minHeight", 929 max: "maxHeight", 930 head: "top", 931 tail: "bottom", 932 center: "centerY", 933 size: "height", 934 anchor: "vanchor" 935 }, 936 937 /** 938 Generic resizer. Must pass one set of keys: VKEYS, HKEYS 939 */ 940 _mouseResize: function(evt, info, keys, ret) { 941 942 var delta = evt[keys.evtPoint] - info[keys.point], 943 layout = info.layout, 944 view = this.get('view'), 945 min = this.get(keys.min), 946 max = this.get(keys.max), 947 948 headKey = keys.head, 949 tailKey = keys.tail, 950 centerKey = keys.center, 951 sizeKey = keys.size, 952 953 hasHead = !SC.none(layout[keys.head]), 954 hasTail = !SC.none(layout[keys.tail]), 955 hasCenter = !SC.none(layout[keys.center]), 956 hasSize = !SC.none(layout[keys.size]), 957 w; 958 959 if (info[keys.anchor] === headKey) { 960 961 // if left aligned, adjust left size and width if set. 962 if (hasHead) { 963 if (hasSize) { 964 w = layout[sizeKey]; 965 ret[sizeKey] = Math.min(max, Math.max(min, Math.floor(layout[sizeKey] - delta))); 966 min = (layout[headKey]+w) - min; 967 max = (layout[headKey]+w) - max; 968 969 ret[headKey] = Math.max(max, Math.min(min, Math.floor(layout[headKey]+delta))); 970 971 } else { 972 ret[headKey] = Math.floor(layout[headKey]+delta); 973 } 974 975 // if right aligned or centered, adjust the width... 976 } else if (hasTail || hasCenter) { 977 if (hasCenter) delta *= 2; 978 ret[sizeKey] = Math.max(min, Math.min(max, Math.floor((layout[sizeKey]||0)-delta))); 979 980 // otherwise, adjust left 981 } else ret[headKey] = Math.floor((layout[headKey]||0)+delta); 982 983 } else if (info[keys.anchor] === tailKey) { 984 985 // reverse above 986 if (hasTail) { 987 988 if (hasSize) { 989 w = layout[sizeKey]; 990 ret[sizeKey] = Math.min(max, Math.max(min, Math.floor(layout[sizeKey] + delta))); 991 min = (layout[tailKey]+w)-min; 992 max = (layout[tailKey]+w)-max; 993 994 ret[tailKey] = Math.max(max, Math.min(min, Math.floor(layout[tailKey]-delta))); 995 996 } else { 997 ret[tailKey] = Math.floor(layout[tailKey]-delta); 998 } 999 1000 } else { 1001 if (hasCenter) delta *= 2; 1002 ret[sizeKey] = Math.max(min, Math.min(max, Math.floor((layout[sizeKey]||0)+delta))); 1003 } 1004 } 1005 1006 return this; 1007 }, 1008 1009 _mouseReposition: function(evt, info, keys, ret) { 1010 var delta = evt[keys.evtPoint] - info[keys.point], 1011 layout = info[SC.keyFor('layout', SC.guidFor(this))], 1012 view = this.get('view'), 1013 1014 headKey = keys.head, 1015 tailKey = keys.tail, 1016 centerKey = keys.center, 1017 sizeKey = keys.size, 1018 1019 hasHead = !SC.none(layout[headKey]), 1020 hasTail = !SC.none(layout[tailKey]), 1021 hasCenter = !SC.none(layout[centerKey]), 1022 hasSize = !SC.none(layout[sizeKey]), 1023 w; 1024 1025 // auto-widths can't be repositioned 1026 if (hasHead && hasTail && !hasSize) return NO ; 1027 1028 // left/top aligned, just adjust top/left location 1029 if (hasHead) { 1030 ret[headKey] = layout[headKey]+delta; 1031 1032 // right/bottom aligned, adjust bottom/right location 1033 } else if (hasTail) { 1034 ret[tailKey] = layout[tailKey]-delta; 1035 1036 } else if (hasCenter) { 1037 ret[centerKey] = layout[centerKey]+delta; 1038 1039 } else ret[headKey] = (layout[headKey]||0)+delta; 1040 1041 return YES ; 1042 }, 1043 1044 // .......................................................... 1045 // Drag data source (for binding lines) 1046 // 1047 /** 1048 This method must be overridden for drag operations to be allowed. 1049 Return a bitwise OR'd mask of the drag operations allowed on the 1050 specified target. If you don't care about the target, just return a 1051 constant value. 1052 1053 @param {SC.View} dropTarget The proposed target of the drop. 1054 @param {SC.Drag} drag The SC.Drag instance managing this drag. 1055 1056 */ 1057 dragSourceOperationMaskFor: function(drag, dropTarget) { 1058 return SC.DRAG_LINK; 1059 }, 1060 1061 /** 1062 This method is called when the drag begins. You can use this to do any 1063 visual highlighting to indicate that the receiver is the source of the 1064 drag. 1065 1066 @param {SC.Drag} drag The Drag instance managing this drag. 1067 1068 @param {Point} loc The point in *window* coordinates where the drag 1069 began. You can use convertOffsetFromView() to convert this to local 1070 coordinates. 1071 */ 1072 dragDidBegin: function(drag, loc) { 1073 }, 1074 1075 /** 1076 This method is called whenever the drag image is moved. This is 1077 similar to the `dragUpdated()` method called on drop targets. 1078 1079 @param {SC.Drag} drag The Drag instance managing this drag. 1080 1081 @param {Point} loc The point in *window* coordinates where the drag 1082 mouse is. You can use convertOffsetFromView() to convert this to local 1083 coordinates. 1084 */ 1085 dragDidMove: function(drag, loc) { 1086 var dragLink = drag.dragLink; 1087 var endX, endY, pv, frame, globalFrame; 1088 if (dragLink) { 1089 // if using latest SproutCore 1.0, loc is expressed in browser window coordinates 1090 pv = dragLink.get('parentView'); 1091 frame = dragLink.get('frame'); 1092 globalFrame = pv ? pv.convertFrameToView(frame, null) : frame; 1093 if (globalFrame) { 1094 endX = loc.x - globalFrame.x; 1095 endY = loc.y - globalFrame.y; 1096 dragLink.set('endPoint', {x: endX , y: endY}); 1097 } 1098 } 1099 }, 1100 1101 /** 1102 This method is called when the drag ended. You can use this to do any 1103 cleanup. The operation is the actual operation performed on the drag. 1104 1105 @param {SC.Drag} drag The drag instance managing the drag. 1106 1107 @param {Point} loc The point in WINDOW coordinates where the drag 1108 ended. 1109 1110 @param {DragOp} op The drag operation that was performed. One of 1111 SC.DRAG_COPY, SC.DRAG_MOVE, SC.DRAG_LINK, or SC.DRAG_NONE. 1112 1113 */ 1114 dragDidEnd: function(drag, loc, op) { 1115 var dragLink = drag.dragLink; 1116 if (dragLink) dragLink.destroy(); 1117 } 1118 }) ; 1119 1120 // Set default Designer for view 1121 if (!SC.View.Designer) SC.View.Designer = SC.ViewDesigner ; 1122 1123 // .......................................................... 1124 // DESIGN NOTIFICATION METHODS 1125 // 1126 // These methods are invoked automatically on the designer class whenever it 1127 // is loaded. 1128 1129 SC.ViewDesigner.mixin({ 1130 1131 ANCHOR_LEFT: 0x0001, 1132 ANCHOR_RIGHT: 0x0002, 1133 ANCHOR_CENTERX: 0x0004, 1134 ANCHOR_WIDTH: 0x0010, 1135 1136 ANCHOR_TOP: 0x0100, 1137 ANCHOR_BOTTOM: 0x0200, 1138 ANCHOR_CENTERY: 0x0400, 1139 ANCHOR_HEIGHT: 0x1000, 1140 1141 /** 1142 Invoked whenever a designed view is loaded. This will save the design 1143 attributes for later use by a designer. 1144 */ 1145 didLoadDesign: function(designedView, sourceView, attrs) { 1146 designedView.isDesign = YES ; // indicates that we need a designer. 1147 designedView.designAttrs = attrs; 1148 //designedView.sourceView = sourceView; TODO: not sure we need this... 1149 }, 1150 1151 /** 1152 Invoked whenever a location is applied to a designed view. Saves the 1153 attributes separately for use by the design view. 1154 */ 1155 didLoadLocalization: function(designedView, attrs) { 1156 // nothing to do for now. 1157 }, 1158 1159 /** 1160 Invoked whenver a view is created. This will create a peer designer if 1161 needed. 1162 */ 1163 didCreateView: function(view, attrs) { 1164 // add designer if page is in design mode 1165 var page = view.get('page'), design = view.constructor; 1166 1167 if (design.isDesign && page && page.get('needsDesigner')) { 1168 1169 // find the designer class 1170 var cur = design, origDesign = design; 1171 while(cur && !cur.Designer) cur = cur.superclass; 1172 var DesignerClass = (cur) ? cur.Designer : SC.View.Designer; 1173 1174 // next find the first superclass view that is not a design (and a real 1175 // class). This is important to make sure that we can determine the 1176 // real name of a view's class. 1177 while (design && design.isDesign) design = design.superclass; 1178 if (!design) design = SC.View; 1179 1180 view.designer = DesignerClass.create({ 1181 view: view, 1182 viewClass: design, 1183 designAttrs: origDesign.designAttrs 1184 //sourceView: origDesign.sourceView TODO: not sure we need this... 1185 }); 1186 } 1187 } 1188 1189 }); 1190 1191 1192 // .......................................................... 1193 // FIXUP SC.View 1194 // 1195 1196 SC.View.prototype._orig_respondsTo = SC.View.prototype.respondsTo; 1197 SC.View.prototype._orig_tryToPerform = SC.View.prototype.tryToPerform; 1198 SC.View.prototype._orig_createLayer = SC.View.prototype.createLayer; 1199 SC.View.prototype._orig_updateLayer = SC.View.prototype.updateLayer; 1200 SC.View.prototype._orig_destroyLayer = SC.View.prototype.destroyLayer; 1201 1202 /** 1203 If the view has a designer, then patch respondsTo... 1204 */ 1205 /*SC.View.prototype.respondsTo = function( methodName ) { 1206 var ret = !!(SC.typeOf(this[methodName]) === SC.T_FUNCTION); 1207 if (this.designer) ret = ret || this.designer.respondsTo(methodName); 1208 return ret ; 1209 } ;*/ 1210 SC.View.prototype.respondsTo = function( methodName ) { 1211 if (this.designer) { 1212 var ret = !!(SC.typeOf(this[methodName]) === SC.T_FUNCTION); 1213 ret = ret || this.designer.respondsTo(methodName); 1214 return ret; 1215 } 1216 else { 1217 return this._orig_respondsTo(methodName); 1218 } 1219 }; 1220 1221 /** 1222 If the view has a designer, give it an opportunity to handle an event 1223 before passing it on to the main view. 1224 */ 1225 /*SC.View.prototype.tryToPerform = function(methodName, arg1, arg2) { 1226 if (this.designer) { 1227 return this.designer.tryToPerform(methodName, arg1, arg2); 1228 } else { 1229 return this._orig_respondsTo(methodName) && this[methodName](arg1, arg2); 1230 } 1231 } ;*/ 1232 SC.View.prototype.tryToPerform = function(methodName, arg1, arg2) { 1233 if (this.designer) { 1234 return this.designer.tryToPerform(methodName, arg1, arg2); 1235 } 1236 else { 1237 return this._orig_tryToPerform(methodName, arg1, arg2); 1238 } 1239 }; 1240 1241 1242 /* 1243 If the view has a designer, also call designers didCreateLayer method to 1244 allow drawing. 1245 */ 1246 SC.View.prototype.createLayer = function() { 1247 var ret = this._orig_createLayer.apply(this, arguments); 1248 if (this.designer) this.designer.didCreateLayer(); 1249 return ret ; 1250 }; 1251 1252 /* 1253 If the view has a designer, also call the designer's didUpdateLayer method 1254 to allow drawing. 1255 */ 1256 SC.View.prototype.updateLayer = function() { 1257 var ret = this._orig_updateLayer.apply(this, arguments); 1258 if (this.designer) this.designer.didUpdateLayer(); 1259 return ret ; 1260 }; 1261 1262 /** 1263 If the view has a designer, also call the designers willDestroyLayer 1264 method. 1265 */ 1266 SC.View.prototype.destroyLayer = function() { 1267 if (this.designer) this.designer.willDestroyLayer(); 1268 return this._orig_destroyLayer.apply(this, arguments); 1269 }; 1270