34a45f462f1b0e73015f60354ded50644bf629b7
braney
  Wed Apr 15 13:03:18 2026 -0700
Apply colorOverride to track labels; add right-click "Change Track Color" dialog, refs #20460

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index bc6c8e01b31..6813b8d7c4e 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -2483,30 +2483,34 @@
                         success: catchErrorOrDispatch,
                         error: errorHandler,
                         cache: true
                     });
                     });
         } else if (cmd === 'hgTrackUi_popup') {
 
             // Launches the popup but shields the ajax with a waitOnFunction
             popUp.hgTrackUi( rightClick.selectedMenuItem.id, false );  
 
         } else if (cmd === 'hgTrackUi_popup_description') {
 
             // Launches the popup but shields the ajax with a waitOnFunction
             popUp.hgTrackUi( rightClick.selectedMenuItem.id, true );
 
+        } else if (cmd === 'changeTrackColor') {
+
+            rightClick.showColorPicker(id);
+
         } else if (cmd === 'hgTrackUi_follow') {
 
             url = "hgTrackUi?hgsid=" + getHgsid() + "&g=";
             rec = hgTracks.trackDb[id];
             if (tdbHasParent(rec) && tdbIsLeaf(rec))
                 url += rec.parentTrack;
             else {
                 // The button already has the ref
                 var link = normed($( 'td#td_btn_'+ rightClick.selectedMenuItem.id ).children('a')); 
                 if (link)
                     url = $(link).attr('href');
                 else
                     url += rightClick.selectedMenuItem.id;
             }
             location.assign(url);
@@ -2748,30 +2752,105 @@
         if (rightClick.floatingMenuItem) {
             $('#img_data_' + rightClick.floatingMenuItem).parent().makeFloat(
                 {x:"current",y:"current", speed: 'fast', alwaysVisible: true, alwaysTop: true});
         }
     },
 
     makeImgTag: function (img)
     {   // Return img tag with explicit dimensions for img (dimensions are currently hardwired).
         // This fixes the "weird shadow problem when first loading the right-click menu"
         // seen in FireFox 3.X, which occurred b/c FF doesn't actually fetch the image until
         // the menu is being shown.
         return "<img style='width:16px; height:16px; border-style:none;' src='../images/" +
                 img + "' />";
     },
 
+    showColorPicker: function (trackName)
+    {   // Show a small dialog with a spectrum color picker for changing track color
+        var rec = hgTracks.trackDb[trackName];
+        if (!rec || !rec.defaultColor)
+            return;
+        var currentColor = (rec.colorOverrideOn && rec.colorOverride) ?
+                           rec.colorOverride : rec.defaultColor;
+        var dialogId = "trackColorDialog";
+        $("#" + dialogId).remove();
+        $("body").append(
+            "<div id='" + dialogId + "'>" +
+            "<p>Pick a new color for <b>" + rec.shortLabel + "</b>:</p>" +
+            "<input type='text' id='trackColorText' value='" + currentColor + "' size='8' />" +
+            "&nbsp;<input id='trackColorPicker' />" +
+            "<br><br><label><input type='checkbox' id='trackColorOn'" +
+            (rec.colorOverrideOn ? " checked" : "") +
+            " /> Enable color override</label>" +
+            "</div>");
+        $("#trackColorPicker").spectrum({
+            color: currentColor,
+            showPalette: true,
+            showSelectionPalette: true,
+            showInitial: true,
+            showInput: true,
+            preferredFormat: "hex",
+            localStorageKey: "genomebrowser",
+            hideAfterPaletteSelect: true,
+            change: function(color) {
+                $("#trackColorText").val(color.toHexString());
+                $("#trackColorOn").prop("checked", true);
+            }
+        });
+        $("#trackColorText").on("change", function() {
+            $("#trackColorPicker").spectrum("set", $(this).val());
+            $("#trackColorOn").prop("checked", true);
+        });
+        $("#" + dialogId).dialog({
+            modal: true,
+            title: "Change Track Color",
+            closeOnEscape: true,
+            resizable: false,
+            minWidth: 400,
+            buttons: {
+                "Apply": function() {
+                    var color = $("#trackColorText").val();
+                    var isOn = $("#trackColorOn").is(":checked") ? "1" : "0";
+                    rec.colorOverride = color;
+                    rec.colorOverrideOn = (isOn === "1");
+                    cart.setVars(
+                        [trackName + ".colorOverride", trackName + ".colorOverrideOn"],
+                        [color, isOn], null, false);
+                    imageV2.requestImgUpdate(trackName,
+                        trackName + ".colorOverride=" + encodeURIComponent(color) +
+                        "&" + trackName + ".colorOverrideOn=" + isOn);
+                },
+                "Ok": function() {
+                    var color = $("#trackColorText").val();
+                    var isOn = $("#trackColorOn").is(":checked") ? "1" : "0";
+                    rec.colorOverride = color;
+                    rec.colorOverrideOn = (isOn === "1");
+                    cart.setVars(
+                        [trackName + ".colorOverride", trackName + ".colorOverrideOn"],
+                        [color, isOn], null, false);
+                    imageV2.requestImgUpdate(trackName,
+                        trackName + ".colorOverride=" + encodeURIComponent(color) +
+                        "&" + trackName + ".colorOverrideOn=" + isOn);
+                    $(this).dialog("close");
+                }
+            },
+            close: function() {
+                $("#trackColorPicker").spectrum("destroy");
+                $(this).remove();
+            }
+        });
+    },
 
     // CGIs now use HTML tags, e.g. "<b>Transcript:</b> ENST00000297261.7<br><b>Strand:</b>"
     mouseOverToLabel: function(title)
     {
         if (title.search(/<b>Transcript: ?<[/]b>/) !== -1) {
             title = title.split("<br>")[0].split("</b>")[1];
         }
         // for older UCSC genes tracks, the protein name is forced onto the item name
         if (title.search(/&hgg_prot=/) !== -1) {
             title = title.split("&hgg_prot=")[0];
         }
         return title;
     },
 
     // when "exonNumbers on", the mouse over text is not a good item description for the right-click menu
@@ -3187,30 +3266,40 @@
                     } else {
                         titleStr += "Merge items that span the current region";
                     }
                     o[titleStr] = {onclick: function(menuItemClick, menuObject) {
                         rightClick.hit(menuItemClick, menuObject, "toggleMerge", rec);
                         return true; }
                     };
                 }
 
 		o[rightClick.makeImgTag("book.png")+" Track Description "+rec.shortLabel] = {
 		    onclick: function(menuItemClicked, menuObject) {
 			rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_popup_description");
 			return true; }
 		    };
 
+                if (rec.defaultColor) {
+                    menu.push(o);
+                    o = {};
+                    o[rightClick.makeImgTag("wrench.png")+" Change Track Color"] = {
+                        onclick: function(menuItemClicked, menuObject) {
+                            rightClick.hit(menuItemClicked, menuObject, "changeTrackColor");
+                            return true; }
+                    };
+                }
+
                 menu.push($.contextMenu.separator);
                 menu.push(o);
             }
 
             menu.push($.contextMenu.separator);
             if (hgTracks.highlight && rightClick.clickedHighlightIdx!==null) {
                 var currentlySeen = ($('#highlightItem').length > 0); 
                 o = {};
                 // Jumps to highlight when not currently seen in image
                 var text = (currentlySeen ? " Zoom" : " Jump") + " to highlight";
                 o[rightClick.makeImgTag("highlightZoom.png") + text] = {
                     onclick: rightClick.makeHitCallback('jumpToHighlight')
                 };
 
                 if ( currentlySeen ) {   // Remove only when seen