Uploaded image for project: 'Snowstorm'
  1. Snowstorm
  2. STORM-1944

HTML (Shared Media) HUD objects Behave Badly in conjunction with the Mouse Scroll Wheel

    XMLWordPrintableJSON

Details

    • Defect
    • Status: Closed
    • Major
    • Resolution: Duplicate
    • None
    • None

    Description

      HTML (Shared Media) HUDs completely vex users with their very odd behavior when used with the mouse's scroll wheel.

      The Scroll wheel has two major uses in SL, zooming the 3D view in and out, and scrolling text areas that have scroll bars (which is also true for 99% of browsers and text editors). However, The SL viewer does neither when a Shared Media HUD has focus. Instead, it shrinks the HUD view, as if the user was editing a HUD with the Build Tools.

      To resolve this issue, There are a lot of routes available.

      Preferred:
      If the Media has focus, branch scroll wheel behavior as follows:

      • If mouse is over media surface, Scroll the media scrollbar if one exists, if not, do nothing.
      • If mouse is not over the media surface, zoom the 3D view.

      Acceptable:
      If the Media has focus, branch scroll wheel behavior as follows:

      • If mouse is over media surface, do nothing.
      • If mouse is not over the media surface, zoom the 3D view.

      Minimum Acceptable:
      Anything BUT shrinking the HUD view pretty much.

      Just please, please do not shrink the HUD view. It shouldn't be happening.

      A video illustrating the issue has been uploaded here (watch fullscreen and 1080p for best view):
      http://www.youtube.com/watch?v=2_n1ODE9UjA

      Example HUD Script as used in the video: Place in a prim, attach to your HUD, and click the surface to start the media.
      (Code obtained from here, with modifications: https://wiki.secondlife.com/wiki/HTML_HUD_Demo

      /*
      *  HTML-based, single script HUD
      *
      *  To use:
      *  - create a default prim (cube)
      *  - wear it as a HUD on top_left (script needs tweaking for other attachment points)
      *  - edit the cube while wearing
      *  - add animations you want to use
      *  - add notecards and objects you want to hand out
      *  - add this script
      *
      *  License:
      *    This script itself is free to share, modify and use without restriction.
      *    Any linked or referenced files are not included in this license and are licensed by their respective owners under their own respective copyright and other licenses.
      *    original by Kelly Linden and reformatted by Kireji Haiku, 2011. 
      */
       
      key owner;
      string ownerName;
       
      string url;
       
      key currentRequestID;
      key backupID; // for double requests.
       
      integer responseStatus;
      string responseBody;
       
      list lastPath;
       
      string video_url;
       
      string header;
      string footer;
       
      string currentAnimation;
       
      integer isVisible;
       
      string exceptions;
       
      /*
      *  user-function: init
      *  - does not return anything
      *  - sets initial variable values
      *  - sets object's name and textures
      *  - request a url to use the HUD
      */
       
      init()
      {
          owner = llGetOwner();
          ownerName = llKey2Name(owner);
        //  llSetObjectName(ownerName+"'s HUD");
          currentRequestID=NULL_KEY;
          backupID=NULL_KEY;
          
          video_url = "http://www.youtube.com/embed/m7p9IEpPu-c?rel=0";
       
          //header set in set_link_media(url)
          //footer set in set_link_media(url)
       
          llSetLinkPrimitiveParamsFast(LINK_THIS, [
              PRIM_TEXTURE, ALL_SIDES, TEXTURE_BLANK, <1.0, 1.0, 1.0>, ZERO_VECTOR, (float)FALSE,
              PRIM_TEXTURE, 2, "0b815b79-c8f5-fc98-91fc-e77b53a468e2", <1.0, 1.0, 1.0>, ZERO_VECTOR, (float)FALSE]);
       
          toggle_visibility_of_HUD_button();
          request_url();
      }
       
      /*
      *  user-function: toggle_visibility_of_HUD_button
      *  - does not return anything
      *  - toggle the visibility of the prim
      *  - will rotate, position and scale the prim
      */
       
      toggle_visibility_of_HUD_button()
      {
          return;
          if (isVisible)
              llSetLinkPrimitiveParamsFast(LINK_THIS, [
                  PRIM_POS_LOCAL, <0.0, -0.13, -0.13>,
                  PRIM_ROT_LOCAL, <0.0, 0.0, 0.0, 1.0>,
                  PRIM_SIZE, <0.01, 0.5, 0.25>]);
          else
              llSetLinkPrimitiveParamsFast(LINK_THIS, [
       
                  PRIM_ROT_LOCAL, <0.0, 0.0, -1.0, 0.0>,
                  PRIM_SIZE, <0.05, 0.05, 0.05>]);
       //            PRIM_POS_LOCAL, <0.0, -0.04, -0.04>,
          isVisible = !isVisible;
      }
       
      /*
      *  user-function: request url
      *  - does not return anything
      *  - make sure we drop the old url before requesting a new one
      */
       
      request_url()
      {
          llReleaseURL(url);
          llRequestURL();
      }
       
      //-- expand media textures greater than 1024 pixel in a direction to fit the media face
      //-- does exactly what the "align" button does in the edit window
      //-- original by Edelman Linden (or Kate Linden), tweaked by Void Singer
      ExpandMediaTexture( integer vIntWidth, integer vIntHeight, integer vIntFace )
      {
          integer vIntTemp;
          vector  vSizScale;
          
          while (vIntWidth >> ++vIntTemp);
          vSizScale.x = vIntWidth / (float)(1 << vIntTemp);
          vIntTemp = 0;
          while (vIntHeight >> ++vIntTemp);
          vSizScale.y = vIntHeight / (float)(1 << vIntTemp);
          llSetLinkPrimitiveParamsFast( LINK_THIS,[PRIM_TEXTURE, vIntFace] +llListReplaceList( llGetLinkPrimitiveParams(LINK_THIS, [PRIM_TEXTURE, vIntFace] ),[vSizScale, ((vSizScale - <1.0, 1.0, 0.0>) / 2.0)],1,2));
          
      }
       
       
       
       /*
      *  user-function: set_link_media
      *  - does not return anything
      *  - set the values for the string variables 'header' and 'footer'
      *  - prepare face 4 for media on a prim
      */
      set_link_media(string scriptUrl)
      {
          url = scriptUrl;
       
          header = "<html><head><link href='https://d2mjw3k7q9u8rb.cloudfront.net/assets/common-294585465117107838557748825631511187866.css' media='all' rel='stylesheet' type='text/css' /><base href='" + scriptUrl + "/' /></head><body>";
       
          footer = "<div align='center' style='position:absolute;top:93%;left:8%;'><a href=''>Scan</a> | 
          <a href='anims'>Anims</a> | <a href='video'>Video</a> | <a href='config'>Config</a> | <a href='hide'>Hide</a></div><script src='https://d1979ns0fqtj19.cloudfront.net/assets/common-119678057620033795036051360211604465627.js' type='text/javascript'></script></body></html>";
       
          llSetLinkMedia(LINK_THIS, 4, [ PRIM_MEDIA_CONTROLS,PRIM_MEDIA_CONTROLS_STANDARD,
                                  PRIM_MEDIA_AUTO_PLAY, TRUE,
                                  PRIM_MEDIA_FIRST_CLICK_INTERACT,TRUE,
                                  PRIM_MEDIA_CURRENT_URL, url,
                                  PRIM_MEDIA_HOME_URL, url,
                                  PRIM_MEDIA_HEIGHT_PIXELS, 250,
                                  PRIM_MEDIA_WIDTH_PIXELS, 350,//]);
                                  PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE]);
       
          ExpandMediaTexture(350,250,4);
      }
       
      /*
      *  user-function:
      *  - does not return anything
      *  - used if there's not a request to the homepage of our website
      *  - check where on the site we are and prepare variables for the response
      */
       
      http_get(key requestID, list path)
      {
      //    llSay(0,"setting currentRequestID to "+(string)requestID);
          currentRequestID = requestID;
       
          integer numOfPathsParts = llGetListLength(path);
          string firstPathPart = llList2String(path, 0);
       
          if (firstPathPart == "hide")
          {
              toggle_visibility_of_HUD_button();
              http_get(requestID, lastPath);
              return;
          }
       
          lastPath = path;
       
          if (firstPathPart == "agent")
          {
              if (numOfPathsParts == 1)
              {
                  prepare_profile_overview_page(requestID, owner);
                  return;
              }
              else if (numOfPathsParts == 2)
              {
                  key id = (key)llList2String(path, 1);
                  prepare_profile_overview_page(requestID, id);
                  return;
              }
              else if (numOfPathsParts == 4)
              {
                  if (llList2String(path, 2) != "give")
                      return;
       
                  key id = (key)llList2String(path, 1);
                  string name = llKey2Name(id);
                  string itemName = llUnescapeURL(llList2String(path, 3));
       
                  llOwnerSay("Giving '" + itemName + "' to '" + name + "'.");
                  llGiveInventory(id, itemName);
                  prepare_profile_overview_page(requestID, id);
                  return;
              }
          }
          else if (firstPathPart == "anims")
          {
              if (numOfPathsParts == 1)
              {
                  anims_page();
                  return;
              }
              else if (numOfPathsParts == 2)
              {
                  play_anim(llList2String(path, 1));
                  anims_page();
                  return;
              }
          }
          else if (firstPathPart == "video")
          {
              responseStatus = 200;
              responseBody = header + "<br><iframe width='255' height='173' src='" + video_url + "' frameborder='0' allowfullscreen></iframe>" + footer;
              return;
          }
          else if (firstPathPart == "config")
          {
              if (numOfPathsParts == 1)
              {
                  config_page();
                  return;
              }
              else if (llList2String(path, 1) == "set")
              {
                  string queryString = llGetHTTPHeader(requestID, "x-query-string");
                  list args = llParseString2List(queryString, ["="], ["&"]);
                  integer index = -llGetListLength(args);
                  while (index)
                  {
                      string variable = llList2String(args, index);
                      string value = llUnescapeURL(llList2String(args, index + 1));
                      if (variable == "video")
                          video_url = value;
       
                      //because: var,val,&,var,val,&,...
                      index = index + 3;
                  }
                  config_page();
              }
          }
       
          responseStatus = 404;
          responseBody = header + "<h1>404 Page Not Found.</h1>" + footer;
          throw_exception("There has been a HTTP-request to a non-existant page on your HUD's website. Please check the path of the request for mistakes.");
      }
       
      /*
      *  user-function: prepare_profile_overview_page
      *  - does not return anything
      *  - prepares a response for a page with information about a certain avatar
      *  - includes profile thumbnail, name, script info, give menu
      */
       
      prepare_profile_overview_page(key requestID, key id)
      {
          list avatarDetails = llGetObjectDetails(id, [
                                      OBJECT_POS,
                                      OBJECT_TOTAL_SCRIPT_COUNT,
                                      OBJECT_SCRIPT_MEMORY,
                                      OBJECT_SCRIPT_TIME]);
       
          responseStatus = 200;
          responseBody = header + "<table border='0' cellspacing='1' cellpadding='1'><tr><td colspan='2' style='white-space:nowrap'><div class='profile_title'><h1 id='display_name'>" + html_body_with_formatted_avatar_name(id) + "</h1><h2 id='username'>" + llGetUsername(id) + "</h2></div></td><td><div align='right'>" + html_body_with_links_for_interaction_with_certain_avatar(id) + "<br>" + html_body_with_inventory_overview_for_give_menu(id) + "</div></td></tr><tr><td width='80'>" + html_body_avatar_profile_pic_thumbnail(id) + "</td><td colspan='2'><ul style='list-style-type:circle'><li>Scripts:<ul style='list-style-type:disc;margin-left:10px'><li>" + (string)llList2Integer(avatarDetails, 1) + " total</li><li>" + bytes2str(llList2Integer(avatarDetails, 2)) + "</li><li>" + (string)((integer)(llList2Float(avatarDetails, 3) * 1000000.0)) + "us</li></ul></li></ul></td></tr></table>" + footer;
      }
       
      /*
      *  user-function: html_body_with_formatted_avatar_name
      *  - returns the name of the avatar in html format
      *  - removes the lastname if it is Resident
      *  - adds a line-break for long names
      */
       
      string html_body_with_formatted_avatar_name(key id)
      {
          string stringToReturn = llKey2Name(id);
       
          if (llGetSubString(stringToReturn, -9, -1) == " Resident")
              stringToReturn = llDeleteSubString(stringToReturn, -9, -1);
       
          if (15 < llStringLength(stringToReturn))
              stringToReturn = llDumpList2String(llParseString2List(stringToReturn,[" "], []), "<br>");
       
          return stringToReturn;
      }
       
      /*
      *  user-function: html_body_with_links_for_interaction_with_certain_avatar
      *  - returns html text with links for an avatar to interact with who has a certain uuid
      */
       
      string html_body_with_links_for_interaction_with_certain_avatar(key id)
      {
          return "<div class='menu_button'><a class='icon_button actions dropdown'>Actions <b class='actions_dropdown'>&nbsp;</b></a><ul style='list-style-type:none;text-align:left' class='menu'><li><a href='secondlife:///app/agent/" + (string)id + "/im'>IM</a></li><li><a href='secondlife:///app/agent/" + (string)id + "/offerteleport'>Offer Teleport</a></li><li><a href='secondlife:///app/maptrackavatar/" + (string)id + "'>Map</a></li><li><a href='secondlife:///app/sharewithavatar/" + (string)id + "'>Share</a></li><li><a href='secondlife:///app/agent/" + (string)id + "/pay'>Pay</a></li></ul></div>";
      }
       
      /*
      *  user-function: html_body_with_inventory_overview_for_give_menu
      *  - returns html text with inventory item lists
      */
       
      string html_body_with_inventory_overview_for_give_menu(key id)
      {
          string stringToReturn = "<div class='menu_button'><a class='icon_button actions dropdown'>Give <b class='actions_dropdown'>&nbsp;</b></a><ul style='list-style-type:none;text-align:left;white-space:nowrap' class='menu'>";
       
          integer sizeOfStringBefore = llStringLength(stringToReturn);
       
          stringToReturn += inventory_list_in_html_format_for_give_menu(id, INVENTORY_NOTECARD);
          stringToReturn += inventory_list_in_html_format_for_give_menu(id, INVENTORY_OBJECT);
       
          if (llStringLength(stringToReturn) == sizeOfStringBefore)
              stringToReturn += "<li>(no objects or notecards found)</li>";
       
          stringToReturn += "</ul></div>";
          return stringToReturn;
      }
       
      /*
      *  user-function: bytes2str
      *  - returns a string with script memory info in readable format
      */
       
      string bytes2str(integer bytes)
      {
          // 1024² = 1048576
       
          if (bytes < 1048576)
              return (string)(bytes / 1024) + " KB";
          //else
              return (string)(bytes / 1048576) + " MB";
      }
       
      /*
      *  user-function: html_body_avatar_profile_pic_thumbnail
      *  - returns html text with urls to someone's profile pic
      */
       
      string html_body_avatar_profile_pic_thumbnail(key id)
      {
      //    return "<a href='secondlife:///app/agent/" + (string)id + "/about' class='avatar avatar_thumb' rel='#sl_image_zoom' title='Click to zoom'<img alt='Thumb_sl_image' class='avatar' src='https://my-secondlife.s3.amazonaws.com/users/" + llGetUsername(id) + "/thumb_sl_image.png' /></a>";
       
          return "<div class='simple_overlay' id='sl_image_zoom'></div><a href='javascript:void(0)' class='profile_avatar avatar_thumb tooltip n zoom' data-zoom-class='profile_picture_large' data-zoom-image='https://my-secondlife.s3.amazonaws.com/users/"+ llGetUsername(id)+"/sl_image.png' rel='#sl_image_zoom' title='Click to zoom'><img alt='Thumb_sl_image' class='avatar' src='https://my-secondlife.s3.amazonaws.com/users/"+ llGetUsername(id)+"/thumb_sl_image.png' /></a>";
      }
       
      /*
      *  user-function: inventory_list_in_html_format_for_give_menu
      *  - returns html text with inventory item list of given type
      */
       
      string inventory_list_in_html_format_for_give_menu(key id, integer type)
      {
          string stringToReturn;
       
          integer index = llGetInventoryNumber(type);
          while (index)
          {
              --index;
       
              string name = llGetInventoryName(type, index);
       
              stringToReturn += "<li><a href='agent/" + (string)id + "/give/" + name + "'>" + name + "</a></li>";
          }
       
          return stringToReturn;
      }
       
      /*
      *  user-function: anims_page
      *  - does not return anything
      *  - prepares an html text overview page of animations
      */
       
      anims_page()
      {
          responseStatus = 200;
          responseBody = header + "<h1>Animations</h1><h2>Choose an animation:</h2><div style='margin-left:60px'>" + html_body_animations_overview() + "</div><br><br>" + footer;
          
      // llOwnerSay(responseBody);
      }
       
      /*
      *  user-function: html_body_animations_overview
      *  - returns html text with a list of included animations
      */
       
      string html_body_animations_overview()
      {
          string stringToReturn = "<div class='menu_button' style='align:center'><a class='icon_button message dropdown'>Animate <b class='actions_dropdown'>&nbsp;</b></a><ul style='list-style-type:none;text-align:right' class='menu'>";
       
          integer index = llGetInventoryNumber(INVENTORY_ANIMATION);
       
          if (!index)
              stringToReturn += "<li>(no animations found)</li>";
       
          else while (index)
          {
              --index;
       
              string name = llGetInventoryName(INVENTORY_ANIMATION, index);
       
              stringToReturn += "<li><a href='anims/" + name + "'>" + name + "</a></li>";
          }
       
          stringToReturn += "</ul></div>";
       
          return stringToReturn;
      }
       
      /*
      *  user-function: play_anim
      *  - does not return anything
      *  - prompts a perms request to animate owner
      */
       
      play_anim(string anim)
      {
          llRequestPermissions(owner, PERMISSION_TRIGGER_ANIMATION);
          currentAnimation = llUnescapeURL(anim);
      }
       
      /*
      *  user-function: config_page
      *  - does not return anything
      *  - prepares page to configure youtube video link
      */
       
      config_page()
      {
          responseStatus = 200;
          responseBody = header + "<h1>Options:</h1><form action='config/set' method='get'>Video URL: <input type='text' name='video' value='" + video_url + "' /><input type='submit' value='Set' /></form>" + footer;
      }
       
      /*
      *  user-function: throw_exception
      *  - does not return anything
      *  - logs errors into cache for later viewing and debugging
      */
       
      throw_exception(string inputString)
      {
          if (exceptions == "")
              exceptions = "The following un-handled exception(s) occurred that are preventing this device's operation:\n";
       
          exceptions += "\t"+inputString+"\n";
      }
       
      default 
      { 
          on_rez(integer start_param)
          {
              llReleaseURL(url);
              llResetScript();
          }
       
          changed(integer change)
          {
              if (change & (CHANGED_OWNER | CHANGED_INVENTORY))
              {
                  llReleaseURL(url);
                  llResetScript();
              }
       
              if (change & (CHANGED_REGION | CHANGED_REGION_START | CHANGED_TELEPORT))
                  request_url();
          }
       
          state_entry()
          {
              init();
          }
       
          touch_start(integer num_detected)
          {
              toggle_visibility_of_HUD_button();
          }
       
          http_request(key id, string method, string body)
          {
              integer sendResponseNow = TRUE;
       
              responseStatus = 400;
              responseBody = "Unsupported method";
       
              if (method == URL_REQUEST_GRANTED)
              {
                  responseStatus = 200;
                  responseBody = "OK";
                  llMessageLinked(-4,0,"Got URL",NULL_KEY);          
                  set_link_media(body);
              }
              else if (method == URL_REQUEST_DENIED)
              {
                  responseStatus = 400;
                  responseBody = "Bad request";
       
                  throw_exception("The following error occurred while attempting to get a free URL for this device:\n \n" + body);
              }
              else if (method == "GET")
              {
           llMessageLinked(-4,0,"Got a Request+"+(string)id,NULL_KEY);            
                  responseStatus = 200;
                  responseBody = "GET";
       
                  string pathInfoHeader = llGetHTTPHeader(id, "x-path-info");
                  list path = llParseString2List(pathInfoHeader, ["/"], []);
                  llMessageLinked(-4,0,"path="+llList2CSV(path),NULL_KEY);
       
                  if (path == [])
                  {
                  llMessageLinked(-4,0,"Doing Sensor.",NULL_KEY);
                  if (currentRequestID!=NULL_KEY)
                  {
                      llMessageLinked(-4,0,"BackupID="+(string)id,NULL_KEY);
                      backupID=id;
                  }
                  else currentRequestID=id;
           
                      sendResponseNow = FALSE;
                      llSensor("", NULL_KEY, AGENT_BY_LEGACY_NAME, 96.0, PI);
                      lastPath = [];
                  }
                  else
                      http_get(id, path);
              }
       
      /*
      *  check if doing a sensor sweep and if so don't send a response
      *  but send the response in 'sensor event' or 'no_sensor event'
      *
      *  (time-out 30.0 seconds for response)
      */
       
              if (sendResponseNow)
              {
                  llSetContentType(id, CONTENT_TYPE_HTML);
                  llHTTPResponse(id, responseStatus, responseBody);
              }
       
              if (exceptions != "")
                  state error;
          }
       
          run_time_permissions(integer perm)
          {
              if (perm & PERMISSION_TRIGGER_ANIMATION)
              {
                  llStartAnimation(currentAnimation);
              }
              else
                  throw_exception("This HUD has tried to animate your avatar WITHOUT having the permissions to do so. You must grant this HUD permissions to animate your avatar for this feature to work.");
       
              if (exceptions != "")
                  state error;
          }
       
          sensor(integer num_detected)
          {
              responseStatus = 200;
              responseBody = header + "<h2>Scan Results:</h2><ul style='list-style-type:circle;margin-left:20px'>";
       
              if (14 < num_detected)
                  num_detected = 14;
       
              while (num_detected)
              {
                  --num_detected;
       
                  key id = llDetectedKey(num_detected);
       
                  responseBody += "<li><a href='agent/" + (string)id + "'>" + html_body_with_formatted_avatar_name(id) + "</a></li>";
              }
       
              responseBody += "</ul>" + footer;
       
              llMessageLinked(-4,0,"Sensor response+"+(string)currentRequestID,NULL_KEY);
       
              llSetContentType(currentRequestID, CONTENT_TYPE_HTML);
              llHTTPResponse(currentRequestID, responseStatus, responseBody);
              currentRequestID=NULL_KEY;
              if (backupID!=NULL_KEY) 
              {
              llSetContentType(backupID, CONTENT_TYPE_HTML);
              llHTTPResponse(backupID, responseStatus, responseBody);
              backupID=NULL_KEY;
              }
          }
       
          no_sensor()
          {
              responseStatus = 200;
              responseBody = header + "<h2>Scan Results:</h2><ul style='list-style-type:circle;margin-left:20px'><li>No one nearby.</li><li>Owner: <a href='agent/" + (string)owner + "'>" + ownerName + "</a></li></ul><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>Bottom" + footer;
       
       
                  llMessageLinked(-4,0,"No Sensor response+"+(string)currentRequestID,NULL_KEY);
                  llSetContentType(currentRequestID, CONTENT_TYPE_HTML);
                  llHTTPResponse(currentRequestID, responseStatus, responseBody);
                  currentRequestID=NULL_KEY;
       
              if (backupID!=NULL_KEY) 
              {
              llSetContentType(backupID, CONTENT_TYPE_HTML);
              llHTTPResponse(backupID, responseStatus, responseBody);
              backupID=NULL_KEY;
              }
          }
       
          state_exit()
          {
              llReleaseURL(url);
          }
      }
       
      state error
      {
          on_rez(integer start_param)
          {
              llResetScript();
          }
       
          changed(integer change)
          {
              if (change & (CHANGED_OWNER | CHANGED_INVENTORY))
                  llResetScript();
          }
       
          state_entry()
          {
              llOwnerSay("========== ERROR REPORT START ==========");
              llOwnerSay(exceptions);
              llOwnerSay("========== ERROR REPORT END ==========");
              llOwnerSay("Resetting now...");
              llResetScript();
          }
      }
      

      Attachments

        Activity

          People

            Unassigned Unassigned
            darien.caldwell Darien Caldwell
            jira-users
            Watchers:
            16 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved: