Thursday, September 30, 2010

C# .NET - Equals() and GetHashCode() investigated

Just the other day I was ready to commit a rather large chunk of code to SVN repository and needed a fellow developer to approve the commit. While going through the changes made to the code my fellow developer Rasmus (you can see his blog here RasmusKL's Blog - Bit by bit...) spotted a weak implementation of Equals().

To illustrate what was wrong have a look at this class.

using System;

namespace Models
{
 public class EmailAddress
 {
  public string Address { get; set; }

  public override bool Equals(Object o)
  {
   if(o==null)
   {
    return false;
   }
   if (this.GetType() == o.GetType())
   {
    return o.GetHashCode()==this.GetHashCode();
   }
   return false;
  }

  public override int GetHashCode()
  {
   return this.Address.GetHashCode();
  }
 }
}

The issue above is that GetHashCode() returns an integer which only can take 2^32 different values, whereas Address is a string which can take an arbitrary large number of combinations depending on the string length, different encoding types ... so eventually we would run out of different int32 distinct values and this would end up in GetHashCode() returning an already used value for a string that did not match the first value.

This was not my code, but I voted for us to leave the code as it was to be changed in a following commit since I felt that the risk of potentially conflicting hashcodes caused by the following line


return o.GetHashCode()==this.GetHashCode();


was relatively low and that this should not necessarily be a big cause of concern.

I was wrong.

As the following will show there is a potentially large risk that 2 different strings generate the same hashcode with a rather small number of iterations.


The problem is that we are comparing

return o.GetHashCode()==this.GetHashCode();


rather than

var other = o as EmailAddress;
...
return other.Address==this.Address;


The MSDN documentation for Object.GetHashCode() states the following

A hash function must have the following properties:
  • If two objects compare as equal, the GetHashCode method for each object must return the same value. However, if two objects do not compare as equal, the GetHashCode methods for the two object do not have to return different values.
  • The GetHashCode method for an object must consistently return the same hash code as long as there is no modification to the object state that determines the return value of the object's Equals method. Note that this is true only for the current execution of an application, and that a different hash code can be returned if the application is run again.
  • For the best performance, a hash function must generate a random distribution for all input.
Out of pure interest I set out to investigate how often on average GetHashCode() will return the same hashcode for different strings. 


On my specific machine (Intel Windows 7 64 bit) this turned out to be on average every 82039'th call to GetHashCode().


The smallest number of calls to GetHashCode() to return the same hashcode for a different string was after 565 iterations and the highest number of iterations before getting a hashcode collision was 296390 iterations. This is the code I used to get the statistics, the code is using Guids for unique strings and pushes them into a Dictionary, since Address in the above class is just a string I have run this test as a string.GetHashCode scenario.

using System;
using NUnit.Framework;
using System.Collections.Generic;

namespace Equals
{
 [TestFixture]
 public class EmailAddressTest
 {
  [Test]
  public void GetHashCode_HowFastCanWeGenerateSameHashCodeWithDifferentValues_NotEqualsHasSameHash()
  {
   Guid guid2;
   int guid2HashCode = 0;
   string guid2AsString = string.Empty;
   long sum = 0;
   Dictionary<int, string> generatedHashesGuids = new Dictionary<int, string>();

   for (int x = 1; x < 10000; x++)
   {
    for (int i = 0; i < 1000000; i++)
    {
     guid2 = Guid.NewGuid();
     guid2AsString = guid2.ToString();
     guid2HashCode = guid2AsString.GetHashCode();

     if (generatedHashesGuids.ContainsKey(guid2HashCode) && generatedHashesGuids[guid2HashCode] != guid2AsString)
     {
      sum = sum + i;
      Console.WriteLine (string.Format("Average object.GetHashCode() iterations before collision (iterations before collision / number of tries): {0} / {1} = {2}", sum.ToString(), x.ToString(), (sum / x).ToString()));
      generatedHashesGuids.Clear();
      break;
     }
     else
     {
      generatedHashesGuids.Add(guid2HashCode, guid2.ToString());
     }
    }
   }
  }
 }
}

The image below show the average number of iterations before a collision occurs. The test ran through 10K collisions.

And here is the graph again after removing the initial 10 first averages that had a large spread.

So the conclusion is that if you think that GetHashCode() (in its default implementation for string) only will clash/collide a few times in Int32.MaxValue then you are mistaken. Personally I was wrong, I thought this happened maybe one time in a million or so, but as this test shows it can happen after 535 iterations and also quite likely after a single iteration.

Kudos to Rasmus for this one.

Tuesday, September 07, 2010

Android: Fading images and forcing them to stay that way

In my attempts at making the world best reverse calculator (in tight competition with the 100's of other developers doing the same thing on Market - talk about reinventing the wheel) I was considering how to visually best show the math concept subtraction.

After contemplating a bit I wanted to implement a small Packman eating the images representing operand2.

This would look something like

5 - 2 =

***** (packman eats the last 2 dots) and the result is ***

My initial though was to use animated gifs, but my findings (here amongst other places www.anddev.org) were that this was a non trivial exercise and that I really wanted to use something that was built in to Android.

Creating a new folder under the res folder in the Eclipse project named anim and placing a file named fadeout.xml with the following content

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
 <alpha android:fromAlpha="1.0" android:toAlpha="0.0" android:interpolator="@android:anim/accelerate_interpolator" android:duration="3000" android:repeatCount="0" />
</set>

allowed me to use the following code in my SubtractionProblem class

...
// Fade out all the imageviews that are representing operand2
for(View view : operand2Views){
 Animation myFadeOutAnimation = AnimationUtils.loadAnimation(view.getContext(), R.anim.fadeout);
 myFadeOutAnimation.setFillAfter(true);
 view.startAnimation(myFadeOutAnimation); //Set animation to your ImageView

 ...
}

This fades the operand2 images and the
myFadeOutAnimation.setFillAfter(true) code makes the images stay faded (otherwise Android will animate them and return the images to the original state)

Add support for hide/show subpanel at either end of relationship in module builder in SugarCrm 5.5.2

There may be situations where we do not want to show specific subpanel under specific module. In the official SugarCrm release there is no way to turn this on/off for a specific relationship between 2 modules in a package.

This hack describes how to hide/show (default is show) a subpanel at either end of a relationship. The hide/show setting is saved as part of the relationship information and is set on the Edit Relationship screen in Module Builder.

To use it go to Admin -> Module Builder. Select package, then module then relationships of module. Create or Edit relationship.


The hide subpanel checkbox will be shown for one-to-many relationships and many-many relationships. The check box will only be shown on the many side of a relationship. This means that on a many-one it is only shown on the "left" side and on a many-many relationship it is shown on both.

When any checkbox is checked, the relationship is saved and the package deployed the subpanel in question will not be shown for the module this change is relevant for.

This hack is not upgrade-safe.

This is the SVN path for a clean 5.5.2 code base with the changes applied.

Index: modules/ModuleBuilder/controller.php
===================================================================
--- modules/ModuleBuilder/controller.php (revision 1)
+++ modules/ModuleBuilder/controller.php (working copy)
@@ -168,7 +168,11 @@
             $zip = $mb->getPackage ( $load ) ;
             require_once ('ModuleInstall/PackageManager/PackageManager.php') ;
             $pm = new PackageManager ( ) ;
-            $info = $mb->packages [ $load ]->build ( false ) ;
+            
+            //HACK : force clean build
+            $info = $mb->packages [ $load ]->build ( false, true ) ;
+            //HACK : force clean build
+
             mkdir_recursive ( $GLOBALS [ 'sugar_config' ] [ 'cache_dir' ] . '/upload/upgrades/module/') ;
             rename ( $info [ 'zip' ], $GLOBALS [ 'sugar_config' ] [ 'cache_dir' ] . '/' . 'upload/upgrades/module/' . $info [ 'name' ] . '.zip' ) ;
             copy ( $info [ 'manifest' ], $GLOBALS [ 'sugar_config' ] [ 'cache_dir' ] . '/' . 'upload/upgrades/module/' . $info [ 'name' ] . '-manifest.php' ) ;
Index: modules/ModuleBuilder/parsers/relationships/AbstractRelationship.php
===================================================================
--- modules/ModuleBuilder/parsers/relationships/AbstractRelationship.php (revision 1)
+++ modules/ModuleBuilder/parsers/relationships/AbstractRelationship.php (working copy)
@@ -80,6 +80,7 @@
         'relationship_type' , 
         'relationship_role_column' , 
         'relationship_role_column_value' , 
+  'hideModuleSubpanel' ,// - hide subpanel  to store comma separated string of module name and relationship name.
         'reverse' ) ;
 
     /*
@@ -268,6 +269,18 @@
       array('widget_class' => "SubPanelTopCreateButton"),
       array('widget_class' => 'SubPanelTopSelectButton', 'mode'=>'MultiSelect')
   );
+  // Start: Module builder - edit relationship - new checkbox - hide subpanel
+     // Following is written to get comma separated string of module name and relationship name
+  // This string contains module name and relationship name of subpanel which we want to hide.
+     // We unset array of subpanel present in string. 
+  $pieces = explode(",",$this->definition['hideModuleSubpanel']);
+  for($i=0;$i<count($pieces);$i++){ 
+   if($sourceModule==$pieces[$i] && $relationshipName==$pieces[$i+1]){
+    unset($subpanelDefinition);
+    $subpanelDefinition=array();
+   }
+  }  
+  /* End */
         
         return array ( $subpanelDefinition );
     }   
Index: modules/ModuleBuilder/parsers/relationships/AbstractRelationships.php
===================================================================
--- modules/ModuleBuilder/parsers/relationships/AbstractRelationships.php (revision 1)
+++ modules/ModuleBuilder/parsers/relationships/AbstractRelationships.php (working copy)
@@ -152,6 +152,14 @@
         }
         }
         
+  // Start: Module builder - edit relationship - new checkbox - hide subpanel
+  // Following is written to make comma separated string of module name and relationship name
+  // This string contains module name and relationship name of subpanel which we want to hide.
+  if($_REQUEST['moduleName']!=""){
+   $hideModuleSubpanel=implode(",",$_REQUEST['moduleName']);
+   $definition['hideModuleSubpanel']=$hideModuleSubpanel;
+  }     
+  // End
         $newRelationship = RelationshipFactory::newRelationship ( $definition ) ;
         // TODO: error handling in case we get a badly formed definition and hence relationship
         $this->add ( $newRelationship ) ;
@@ -255,7 +263,9 @@
      * @param AbstractRelationship The relationship object
      * @return string A globally unique relationship name
      */
-    protected function getUniqueName ($relationship)
+    //HACK : Hide Subpanel, it made public so it can be accessed from outside.
+    public function getUniqueName ($relationship)
+    //HACK : Hide Subpanel
     {
         $allRelationships = $this->getRelationshipList () ;
         $basename = $relationship->getName () ;
Index: modules/ModuleBuilder/parsers/relationships/UndeployedRelationships.php
===================================================================
--- modules/ModuleBuilder/parsers/relationships/UndeployedRelationships.php (revision 1)
+++ modules/ModuleBuilder/parsers/relationships/UndeployedRelationships.php (working copy)
@@ -281,22 +281,29 @@
     protected function saveSubpanelDefinitions ($basepath , $installDefPrefix , $relationshipName , $subpanelDefinitions)
     {
         mkdir_recursive ( "$basepath/layoutdefs/" ) ;
+  //HACK : Hide subpanel
         
         foreach ( $subpanelDefinitions as $moduleName => $definitions )
         {
             $filename = "$basepath/layoutdefs/{$moduleName}.php" ;
-            
+            $addToInstallDefs = false;
             foreach ( $definitions as $definition )
             {
                $GLOBALS [ 'log' ]->debug ( get_class ( $this ) . "->saveSubpanelDefinitions(): saving the following to {$filename}" . print_r ( $definition, true ) ) ;
                if (empty($definition ['get_subpanel_data']) || $definition ['subpanel_name'] == 'history' ||  $definition ['subpanel_name'] == 'activities') {
                  $definition ['get_subpanel_data'] = $definition ['subpanel_name'];
                }
+               
+               if(!empty($definition ['get_subpanel_data'])){
                write_array_to_file ( 'layout_defs["' . $moduleName . '"]["subpanel_setup"]["' . strtolower ( $definition [ 'get_subpanel_data' ] ) . '"]', $definition, $filename, "a" ) ;
+                $addToInstallDefs = true;
+               } 
             }
-            
-            $installDefs [ $moduleName ] = array ( 'from' => "{$installDefPrefix}/relationships/layoutdefs/{$moduleName}.php" , 'to_module' => $moduleName ) ;
+            if($addToInstallDefs){
+             $installDefs [ $moduleName ] = array ( 'from' => "{$installDefPrefix}/relationships/layoutdefs/{$moduleName}.php" , 'to_module' => $moduleName ) ;
+         }
         }
+  //HACK : Hide subpanel
         return $installDefs ;
     }
 
Index: modules/ModuleBuilder/tpls/studioRelationship.tpl
===================================================================
--- modules/ModuleBuilder/tpls/studioRelationship.tpl (revision 1)
+++ modules/ModuleBuilder/tpls/studioRelationship.tpl (working copy)
@@ -150,6 +150,49 @@
     
     
             </tr>
+            {*HACK : Hide subpanel checkbox *}
+            {if $rel.relationship_type == 'one-to-many' }
+            <tr>
+                <td>&nbsp;</td>
+                <td>&nbsp;</td>
+                <td>&nbsp;</td>
+
+                <td align="right" scope="row">Hide {sugar_translate label=$rel.rhs_module} Subpanel On {sugar_translate label=$rel.lhs_module}</td>
+                <td><input type="checkbox" name="moduleName[]" id="moduleName[]" value="{$rel.rhs_module},{$rel_name},"
+                {if in_array($rel.rhs_module,$moduleName)} checked {/if} 
+                /></td>
+
+                <td>&nbsp;</td>
+                <td>&nbsp;</td>
+            </tr>
+            {/if}            
+            {if $rel.relationship_type == 'many-to-many' }
+                <tr>
+                <td align="right" scope="row">Hide {sugar_translate label=$rel.lhs_module} Subpanel On {sugar_translate label=$rel.rhs_module}</td>
+                <td><input type="checkbox" name="moduleName[]" id="moduleName[]" value="{$rel.lhs_module},{$rel_name}," 
+                {if in_array($rel.lhs_module,$moduleName)} checked {/if}  /></td>
+                <td>&nbsp;</td>
+
+                <td align="right" scope="row">Hide {sugar_translate label=$rel.rhs_module} Subpanel On {sugar_translate label=$rel.lhs_module}</td>
+                <td><input type="checkbox" name="moduleName[]" id="moduleName[]" value="{$rel.rhs_module},{$rel_name},"
+                {if in_array($rel.rhs_module,$moduleName)} checked {/if}  /></td>
+
+                <td>&nbsp;</td>
+                <td>&nbsp;</td>
+                </tr>            
+
+            {/if}
+            {if $rel.relationship_type == 'many-to-one' }
+                <tr>
+                <td align="right" scope="row">Hide {sugar_translate label=$rel.lhs_module} Subpanel On {sugar_translate label=$rel.rhs_module}</td>
+                <td><input type="checkbox" name="moduleName[]" id="moduleName[]" value="{$rel.lhs_module},{$rel_name}," 
+                {if in_array($rel.lhs_module,$moduleName)} checked {/if}  /></td>
+                <td>&nbsp;</td>
+                <td>&nbsp;</td>
+                <td>&nbsp;</td>
+                </tr>            
+            {/if}            
+            {*HACK : Hide subpanel checkbox *}
    <tr>
                 {* add in the extended relationship condition *}
                 {* comment out for now as almost no expressed need for this - to revert, uncomment and test, test, test...
Index: modules/ModuleBuilder/views/view.relationship.php
===================================================================
--- modules/ModuleBuilder/views/view.relationship.php (revision 1)
+++ modules/ModuleBuilder/views/view.relationship.php (working copy)
@@ -194,6 +194,22 @@
         $this->smarty->assign ( 'translated_cardinality', $cardinality ) ;
         $this->smarty->assign ( 'selected_cardinality', translate ( $relationship->getType () ) ) ;
         
+        // Start: Module builder - edit relationship - new checkbox - hide subpanel
+  // Following is written to get comma separated string of module name and relationship name
+     // This string contains module name and relationship name of subpanel which we want to hide.
+  // We pass this string to stdioRelationship.tpl file to show checkbox check depending 
+  // checkbox check by user .
+    $moduleName=explode(",",$relationship->hideModuleSubpanel);
+    $this->smarty->assign ( 'moduleName',$moduleName) ;
+
+        if(empty($relationship->relationship_name)){             
+            //$rel_name = $relationship->lhs_module.'_'.$relationship->rhs_module;
+            $rel_name = $relationships->getUniqueName($relationship);
+        } else {
+            $rel_name = $relationship->relationship_name;
+        }
+        $this->smarty->assign ('rel_name', $rel_name) ;
+  // End  
         $relatable = array ( ) ;
         foreach ( $relatableModules as $name => $dummy )
         {

You can download the modified core files and the SVN diff/patch file here HideSubpanel.zip.


Adding AJAX support in Module Builder's "Edit Relationship" in SugarCrm

In SugarCRM custom modules the default buttons in a subpanel are "Create" and "Select". In the framework SugarCrm is built on top there is support for AJAX / inline create in the subpanel as well as the normal full form create. There are also support for single row selection and multiple row selection. To utilize this you need to have knowledge about the various *defs.php files that are code generated by module builder and SugarCrm.

This hack shows how to add support for these buttons in Module Builder. We are going to UI support for selecting what buttons you want to show in your module's subpanel, the different buttons are AJAX Quick Create Button, Full Form Create Button, AJAX Single Select Button and/or AJAX Multiple Select Button.

After the hack is implemented your Edit Relationsship screen in Module Builder is going to look like the following.



After selecting required buttons save the changes, deploy the package (with the affected modules inside) to see the effect. The following screenshot shows the M1 Subpanel on M2 module where we have selected a Quick Create button.



The following screen shows the M2 Subpanel on M1 module where we have selected a Quick Create and Multi Select button.







Likewise we can select any combination we want for our subpanel. (apart from both having a single and a multiple select button - which I did not think made a lot of sense).

This hack is not upgrade safe.




This is the subversion diff file that outlines the changes made to a SugarCrm 5.5.2 code base to achieve the features described above.





Index: modules/ModuleBuilder/javascript/ModuleBuilder.js
===================================================================
--- modules/ModuleBuilder/javascript/ModuleBuilder.js (revision 1)
+++ modules/ModuleBuilder/javascript/ModuleBuilder.js (working copy)
@@ -872,7 +872,10 @@
     ajaxStatus.hideStatus();
     var tab = ModuleBuilder.findTabById('relEditor');
     tab.set("content", o.responseText);
-    SUGAR.util.evalScript(tab);
+    //SUGAR.util.evalScript(tab);
+    //Hack Relations between modules
+    SUGAR.util.evalScript(o.responseText);
+                //Hack Relations between modules
    });
   },
   moduleDropDown: function(name, field){
Index: modules/ModuleBuilder/language/en_us.lang.php
===================================================================
--- modules/ModuleBuilder/language/en_us.lang.php (revision 1)
+++ modules/ModuleBuilder/language/en_us.lang.php (working copy)
@@ -627,6 +627,12 @@
                 'encrypt'=>'Encrypt'
 ),
 
-'parent' => 'Flex Relate'
+'parent' => 'Flex Relate',
+//Hack Relations between modules
+'LBL_AJAX_QUICK_CREATE_BTN' => 'AJAX Quick Create Button',
+'LBL_AJAX_SINGLE_SELECT_BTN' => 'AJAX Single Select Button',
+'LBL_AJAX_MULTIPLE_SELECT_BTN' => 'AJAX Multiple Select Button',
+'LBL_FULL_FORM_CREATE_BTN' => 'Full Form Create Button',
+//Hack Relations between modules
 );
 
Index: modules/ModuleBuilder/parsers/relationships/AbstractRelationship.php
===================================================================
--- modules/ModuleBuilder/parsers/relationships/AbstractRelationship.php (revision 1)
+++ modules/ModuleBuilder/parsers/relationships/AbstractRelationship.php (working copy)
@@ -80,6 +80,7 @@
         'relationship_type' , 
         'relationship_role_column' , 
         'relationship_role_column_value' , 
+        'ext1' ,  //Hack Relations between modules
         'reverse' ) ;
 
     /*
@@ -263,12 +264,52 @@
   }else{
    $subpanelDefinition [ 'title_key' ] = 'LBL_' . strtoupper ( $relationshipName . '_FROM_' . $sourceModule ) . '_TITLE' ;
   }
-        $subpanelDefinition [ 'get_subpanel_data' ] = $relationshipName ;
-        $subpanelDefinition [ 'top_buttons' ] = array(
+  $subpanelDefinition [ 'get_subpanel_data' ] = $relationshipName ;
+  //Hack Relations between modules
+  if(isset($this->definition['ext1']) AND $this->definition['ext1'] != ''){
+   $topBtnOption = explode('_@@_', $this->definition['ext1']);
+   // LHS Module
+   if ($this->definition['lhs_module'] == $sourceModule){
+    // Quick Create Button
+    if($topBtnOption[0] == '1'){
+     $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopButtonQuickCreate');
+    }
+    // Full Form Create Button
+    if($topBtnOption[1] == '1'){
+     $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopCreateButton');
+    }
+    // Single Select Button
+    if($topBtnOption[2] == '1'){
+                    $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopSelectButton');
+    }
+    // Multiple Select Button
+    if($topBtnOption[3] == '1'){
+                        $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopSelectButton', 'popup_module' => $this->definition['rhs_module'], 'mode' => 'MultiSelect', 'initial_filter_fields' => array());
+    }
+   }else{
+    // Quick Create Button
+    if($topBtnOption[4] == '1'){
+     $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopButtonQuickCreate');
+    }
+    // Full Form Create Button
+    if($topBtnOption[5] == '1'){
+     $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopCreateButton');
+    }
+    // Single Select Button
+    if($topBtnOption[6] == '1'){
+                    $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopSelectButton');
+    }
+    // Multiple Select Button
+    if($topBtnOption[7] == '1'){
+                        $subpanelDefinition [ 'top_buttons' ][] = array('widget_class' => 'SubPanelTopSelectButton', 'popup_module' => $this->definition['lhs_module'], 'mode' => 'MultiSelect', 'initial_filter_fields' => array());                        
+    }
+   }
+  }
+  /*$subpanelDefinition [ 'top_buttons' ] = array(
       array('widget_class' => "SubPanelTopCreateButton"),
       array('widget_class' => 'SubPanelTopSelectButton', 'mode'=>'MultiSelect')
-  );
-        
+  );*/
+        //Hack Relations between modules
         return array ( $subpanelDefinition );
     }   
     
Index: modules/ModuleBuilder/parsers/relationships/AbstractRelationships.php
===================================================================
--- modules/ModuleBuilder/parsers/relationships/AbstractRelationships.php (revision 1)
+++ modules/ModuleBuilder/parsers/relationships/AbstractRelationships.php (working copy)
@@ -151,7 +151,21 @@
                     $this->delete ( $_REQUEST [ 'relationship_name' ] ) ;
         }
         }
-        
+  //Hack Relations between modules
+  $buttonArr = array('lhs_quick_create', 'lhs_full_form_create','lhs_signle_select', 'lhs_multiple_select', 'rhs_quick_create',
+                     'rhs_full_form_create', 'rhs_signle_select', 'rhs_multiple_select');
+  $ext1 = '';
+  foreach($buttonArr as $option){
+   $btn = false;
+   if(isset($_REQUEST[$option]) AND $_REQUEST[$option] == '1'){
+    $ext1 .= '1_@@_';
+   }else{
+    $ext1 .= '0_@@_';
+   }
+  }
+  $ext1 = substr($ext1, 0, (strlen($ext1) - 4));
+  $definition ['ext1'] = $ext1;
+  //Hack Relations between modules
         $newRelationship = RelationshipFactory::newRelationship ( $definition ) ;
         // TODO: error handling in case we get a badly formed definition and hence relationship
         $this->add ( $newRelationship ) ;
Index: modules/ModuleBuilder/tpls/studioRelationship.tpl
===================================================================
--- modules/ModuleBuilder/tpls/studioRelationship.tpl (revision 1)
+++ modules/ModuleBuilder/tpls/studioRelationship.tpl (working copy)
@@ -178,6 +178,45 @@
 
    {/if} {* subpanels etc for all but one-to-one relationships *}
    {/if} {* if relationship_only *}
+  {* Hack Relations between modules *}
+  {if $rel.relationship_type != 'one-to-one'}
+  <tr>
+   <td colspan=2>
+   {if $rel.relationship_type == 'many-to-many'}
+   <table class="listView"><tr><td class="listViewThS1">
+   {sugar_translate label=$rel.lhs_module} Subpanel On {sugar_translate label=$rel.rhs_module}
+   </td></tr>
+   <tr><td class="evenListRowS1">
+   <input type="checkbox" name="lhs_quick_create" id="lhs_quick_create" value="1" onclick="lock_create_option(this.id, 'lhs')" 
+   {if $ckBoxOption.0 == 1}checked{/if} /> {$mod_strings.LBL_AJAX_QUICK_CREATE_BTN}<br>
+   <input type="checkbox" name="lhs_full_form_create" id="lhs_full_form_create" value="1" onclick="lock_create_option(this.id, 'lhs')" 
+   {if $ckBoxOption.1 == 1}checked{/if} /> {$mod_strings.LBL_FULL_FORM_CREATE_BTN}<br>
+   <input type="checkbox" name="lhs_signle_select" id="lhs_signle_select" value="1" onclick="lock_multiple_select(this.id, 'lhs')" 
+   {if $ckBoxOption.2 == 1}checked{/if}/> {$mod_strings.LBL_AJAX_SINGLE_SELECT_BTN}<br>
+   <input type="checkbox" name="lhs_multiple_select" id="lhs_multiple_select" value="1" onclick="lock_multiple_select(this.id, 'lhs')" 
+   {if $ckBoxOption.3 == 1}checked{/if} /> {$mod_strings.LBL_AJAX_MULTIPLE_SELECT_BTN}<br>
+   </td></tr></table>
+   {/if}&nbsp;
+   </td>
+   <td>&nbsp;</td>
+   <td colspan=2>
+   <table class="listView"><tr><td class="listViewThS1">
+   {sugar_translate label=$rel.rhs_module} Subpanel On {sugar_translate label=$rel.lhs_module}
+   </td></tr>
+   <tr><td class="evenListRowS1">
+   <input type="checkbox" name="rhs_quick_create" id="rhs_quick_create" value="1" onclick="lock_create_option(this.id, 'rhs')" 
+   {if $ckBoxOption.4 == 1}checked{/if} /> {$mod_strings.LBL_AJAX_QUICK_CREATE_BTN}<br>
+   <input type="checkbox" name="rhs_full_form_create" id="rhs_full_form_create" value="1" onclick="lock_create_option(this.id, 'rhs')" 
+   {if $ckBoxOption.5 == 1}checked{/if} /> {$mod_strings.LBL_FULL_FORM_CREATE_BTN}<br>
+   <input type="checkbox" name="rhs_signle_select" id="rhs_signle_select" value="1" onclick="lock_multiple_select(this.id, 'rhs')" 
+   {if $ckBoxOption.6 == 1}checked{/if} /> {$mod_strings.LBL_AJAX_SINGLE_SELECT_BTN}<br>
+   <input type="checkbox" name="rhs_multiple_select" id="rhs_multiple_select" value="1" onclick="lock_multiple_select(this.id, 'rhs')" 
+   {if $ckBoxOption.7 == 1}checked{/if} /> {$mod_strings.LBL_AJAX_MULTIPLE_SELECT_BTN}
+   </td></tr>            
+            </table>
+  </tr>
+  {/if}
+  {* Hack Relations between modules *}
   </table>
  </td></tr>
 </table>
@@ -190,5 +229,53 @@
 {else}
 ModuleBuilder.helpSetup('studioWizard','relationshipHelp');
 {/if}
+{* Hack Relations between modules *}
+{literal}
+    
+ function lock_multiple_select(id, side){
+  if(!document.getElementById(id)){return false;}
 
+  if(side == 'lhs'){
+   lock_field = (id == 'lhs_signle_select') ? 'lhs_multiple_select' : 'lhs_signle_select';
+  }
+  else{
+   lock_field = (id == 'rhs_signle_select') ? 'rhs_multiple_select' : 'rhs_signle_select';
+  }
+  if(document.getElementById(id).checked){
+   document.getElementById(lock_field).disabled = true;
+  }
+  else{
+   document.getElementById(lock_field).disabled = false;
+  }
+ }
+ function lock_create_option(id, side){
+  if(!document.getElementById(id)){return false;}
+  var lock_field_create="";
+  
+  if(side == 'lhs'){
+   lock_field_create = (id == 'lhs_quick_create') ? 'lhs_full_form_create' : 'lhs_quick_create';
+  }else {
+   lock_field_create = (id == 'rhs_quick_create') ? 'rhs_full_form_create' : 'rhs_quick_create';
+  }
+  
+  if(document.getElementById(id).checked){
+   document.getElementById(lock_field_create).disabled = true;
+  }else{
+   document.getElementById(lock_field_create).disabled = false;
+  }
+ }
+ YAHOO.util.Event.onDOMReady(function(){
+  lock_multiple_select('lhs_signle_select', 'lhs');
+  lock_multiple_select('lhs_multiple_select', 'lhs');
+  lock_multiple_select('rhs_signle_select', 'rhs');
+  lock_multiple_select('rhs_multiple_select', 'rhs');
+  
+  lock_create_option('lhs_quick_create', 'lhs');
+  lock_create_option('lhs_full_form_create', 'lhs');
+  lock_create_option('rhs_quick_create', 'rhs');
+  lock_create_option('rhs_full_form_create', 'rhs');
+ });
+{/literal}
+
+{* Hack Relations between modules *}
 </script>
Index: modules/ModuleBuilder/views/view.relationship.php
===================================================================
--- modules/ModuleBuilder/views/view.relationship.php (revision 1)
+++ modules/ModuleBuilder/views/view.relationship.php (working copy)
@@ -193,7 +193,10 @@
         $this->smarty->assign ( 'cardinality', array_keys ( $cardinality ) ) ;
         $this->smarty->assign ( 'translated_cardinality', $cardinality ) ;
         $this->smarty->assign ( 'selected_cardinality', translate ( $relationship->getType () ) ) ;
-        
+        //Hack Relations between modules
+  $ckBoxOption = explode('_@@_', $relationship->ext1);
+        $this->smarty->assign ( 'ckBoxOption', $ckBoxOption ) ;
+  //Hack Relations between modules
         $relatable = array ( ) ;
         foreach ( $relatableModules as $name => $dummy )
         {

You can download the diff file and the modified core files here MB-RelationshipButtons.zip

The directory modified_files contains the original sugarcrm files with this patch applied for those users that are not yet familiar with SVN.

You are however recommended to use the diff files since you that way have 100% say over what code gets included in your code base rather than trusting the code blindly.
(This is not for these files in particular but goes in general on the internet)

No liability ... normal disclaimer goes here.


Thursday, September 02, 2010

Android development first try - part 2

In Android development first try I was having some problems getting my views centered in my Droidulus Android application.

This showed out to be rather simple after all (it always is when you know how).

The layout file I needed to get this working (this may not be the most appropriate or efficient way) is listed below. And adding the image here as well for reference.






<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/main"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">

 <TextView
 android:id="@+id/score"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="TextView"
 android:textSize="@dimen/point_font_size"
 >
 </TextView>


 <LinearLayout
  android:id="@+id/row1"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
  android:layout_gravity="center_horizontal"
  android:orientation="horizontal">
  
  <TextView
  android:id="@+id/assignment"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="TextView"
  android:textSize="@dimen/font_size"
  android:layout_x="20dip"
  android:layout_y="20dip"
  >
  </TextView>
 </LinearLayout>

 <LinearLayout
  android:id="@+id/row2"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
  android:layout_gravity="center_horizontal"
  android:orientation="horizontal">
 
  <Button
  android:id="@+id/answer1"
  android:layout_width="@dimen/answerButton_width"
  android:layout_height="wrap_content"
  android:layout_x="50dip"
  android:layout_y="150dip"
  >
  </Button>
  <Button
  android:id="@+id/answer2"
  android:layout_width="@dimen/answerButton_width"
  android:layout_height="wrap_content"
  android:layout_x="150dip"
  android:layout_y="150dip"
  >
  </Button>
  <Button
  android:id="@+id/answer3"
  android:layout_width="@dimen/answerButton_width"
  android:layout_height="wrap_content"
  android:layout_x="250dip"
  android:layout_y="150dip"
  >
  </Button>
 </LinearLayout>

  <TableLayout 
      android:id="@+id/myTableLayout"
         android:layout_gravity="center_horizontal"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="20dip"
      >
  </TableLayout>
</LinearLayout>

Android - UI Sleep

I have lately (since I got my HTC Desire phone) been tinkering a little with developing a small application for Android. Since my daughter is learning the multiplication tables and since she finds my phone "cool" then making this little app would be a good starting point.


I am here of course ignoring the fact that there are more than a hundred apps similar available on the Market.


When developing the first version of this app I ran into an interesting point about UI responsiveness and how I could do a "sleep()" function.

The flow is:

  1. The problem is presented
  2. The user answers by clicking on of the available buttons
  3. Mark the problem green for a second before continuing if the answer was correct
  4. If the asnwer was incorrect mark the problem red until the correct answer is chosen

I ended up doing it as follows


private void verifyResult(final View view){
 answerAttempts++; // The user has tried to answer the problem increment answer attempts
 disableAnswerButtons(); // Disable further user input when the text is green/red otherwise the user can click many times on the buttons
 
 int result = Integer.parseInt(view.getTag().toString());
 if (correctResult == result)
 {
  problem.setTextColor(Color.GREEN);
  problem.setText(getProblemText(true)); // Show full problem + correct result
  points++;        
  
  // Wait 1 second before resetting the text color and showing a new problem text
  new Handler().postDelayed(new Runnable() { 
   public void run() { 
    problem.setTextColor(Color.WHITE); 
    generateProblem(); // Generate a new problem and update UI
    enableAnswerButtons();
   } 
  }, 1000);
 }
 else{
  problem.setTextColor(Color.RED); 
  enableAnswerButtons();     
 }
 score.setText(String.format("%s %d/%d", getString(R.string.score), points, answerAttempts));
}

Android development - unable to open ..apk! file

Today when continuing my escapades in Android development I ran into an error in Eclipse.
I just started Eclipse and I was going to build the package when the following error was displayed in the console window. 


[2010-09-02 20:15:11 - Droidulus] ERROR: unable to open 'D:\...\Programming\android\Droidulus\trunk\bin\resources.ap_' as Zip file for writing
[2010-09-02 20:15:11 - Droidulus] Error generating final archive: D:\...\Programming\android\Droidulus\trunk\bin\Droidulus.apk (The system cannot find the path specified)
[2010-09-02 21:32:20 - Droidulus] ------------------------------
[2010-09-02 21:32:20 - Droidulus] Android Launch!
[2010-09-02 21:32:20 - Droidulus] adb is running normally.
[2010-09-02 21:32:20 - Droidulus] Could not find Droidulus.apk!
[2010-09-02 21:33:25 - Droidulus] ------------------------------


After verifying that the path and the files were located where Eclipse and the ADT plug in expected them to be, and that no other compilation bugs was lurking I hit Google. Apparently there are quite a few mentions of this or similar error messages out there.

After nothing useful really showing in the results in 2 or 3 searches I grew impatient and started looking into the error myself.

The first thing I tried was to clean the project (Project Menu/Clean/Clean all).

This fixed the error message for me and I could go back to tinkering.