function Listbox() {
  var scrollPos=0;
  var firstvisible=0;
  var itemcount=0;
  var totalsizeneeded=0;
  var skip=0;
  var sdelta=0;
  var areaht=30;
  var extraspace=0;
  var divs=[];
  var indtop=0;
  var indht=0;
  var mouseY=0;
  var diff=0;
  var eThHt=0;
  var mousedn=false;
  var usepress=false;
  var getLimitCB=null;
  var putItemInCB=null;
  var divht=ShSettings.lbItemHeight;
  var divwd=ShSettings.lbItemWidth;
  var divpad=ShSettings.lbItemPadding;
  var rowcount=ShSettings.lbRowCount;
  var colcount=2;
  var divcount=rowcount*colcount;
  var o=DOMhelp.createElement;
  var highlightdiv=o('div#hilite');
  var matches=o('div#matches', highlightdiv);
  var scup=o('div#scup');
  var scthmid=o('div#sctmid');
  var scthumb=o('div#scthumb', o('div#scttop'), scthmid, o('div#sctbot'));
  var scdn=o('div#scdn');
  var scbar=o('div#scbar', scup, scthumb, scdn);
  var keyCB = function() {return true;};
  var highlighted = -1;

  this.DOMelements = function() {return [matches, scbar];};
  this.setKeyCB = function(fn) { keyCB=fn; };
  this.setLimitCB = function(fn) { getLimitCB=fn; };
  this.setItemInCB = function(fn) { putItemInCB=fn; };
  function endScroll() {
    sdelta=0;
    document.body.onmousemove=null;
    mousedn=false;
  }
  function trMouseFn(e) {
    if (e && (e.pageX || e.pageY)) {
      return function(x) {mouseY=x.pageY;};
    }
    if (e && (e.clientX || e.clientY)) {
      return function(x) {mouseY=x.clientY+document.body.scrollTop+document.documentElement.scrollTop;};
    }
    if (window.event.pageY) {
      return function() {mouseY=window.event.pageY;};
    }
    return function() {mouseY=window.event.clientY+document.body.scrollTop+document.documentElement.scrollTop;};
  }
  function trackThumb() {
    if (!mousedn) {
      return;
    }
    var d = mouseY-diff-indtop;
    if (d!==0) {
      scrollBy(Math.round(d*totalsizeneeded/(areaht-eThHt-34)));
    }
    setTimeout(trackThumb,50);
  }
  function trackUp() {
    if (!mousedn) {
      return;
    }
    if (skip%6===0 && mouseY<findTop(scthumb)) {
      scrollBy(-283);
    }
    if (skip) {
      skip--;
    }
    setTimeout(trackUp,50);
  }
  function trackDn() {
    if (!mousedn) {
      return;
    }
    if (skip%6===0 && mouseY>findTop(scthumb)+indht) {
      scrollBy(283);
    }
    if (skip) {
      skip--;
    }
    setTimeout(trackDn,50);
  }
  function placeItemsTop(offset) {
    for (var i=0; i<rowcount; i++) {
      for (var j=0; j<colcount; j++) {
        divs[i*colcount+j].style.top = (divht*i-offset+1+divpad)+'px';
      }
    }
    fixhighlight();
  }
  function placeItemsLeft() {
    var extra = Math.floor(extraspace/(colcount+1));
    for (var i=0; i<rowcount; i++) {
      for (var j=0; j<colcount; j++) {
        divs[i*colcount+j].style.left = (extra+(divwd+extra)*j+divpad)+'px';
      }
    }
    fixhighlight();
  }
  function scrollUp() {sdelta=-34; skip=6; doAScroll(); return false;}
  function scrollDn() {sdelta=34; skip=6; doAScroll(); return false;}
  function mouseDnBar(e) {
    mousedn=true;
    (document.body.onmousemove=trMouseFn(e))(e);
    skip=6;
    if (mouseY<=findTop(scthumb)) {
      trackUp();
    } else {
      trackDn();
    }
    return false;
  }
  function mouseDnThumb(e) {
    mousedn=true;
    (document.body.onmousemove=trMouseFn(e))(e);
    diff=mouseY-indtop;
    trackThumb();
    return false;
  }

  scup.onmousedown=scrollUp;
  scdn.onmousedown=scrollDn;
  scbar.onmousedown=mouseDnBar;
  scthumb.onmousedown=mouseDnThumb;
  function addItemDiv(idx) {
    var n=divs.length;
    divs[n]=o('div.item');
    divs[n].style.width=(divwd-2*divpad)+"px";
    matches.appendChild(divs[n]);
    if (putItemInCB && idx<itemcount) {
      putItemInCB(idx, divs[n]);
    }
  }

  while (divs.length<divcount) {
    addItemDiv(divs.length);
  }
  placeItemsTop(0);
  placeItemsLeft();
  this.clearItems=function(newcount) {
    firstvisible=0;
    itemcount=newcount;
    totalsizeneeded=Math.max(Math.ceil(itemcount/colcount)*divht,1);
    scrollPos=0;
    placeItemsTop(0);
    sdelta=0;
    var i=0;
    if (putItemInCB) {
      for (; i<newcount && i<divcount; ++i) {
        putItemInCB(i,divs[i]);
      }
    }
    for (; i<divcount; ++i) {
      divs[i].innerHTML='';
    }
    doind();
  };
  function fixhighlight() {
    var idx=highlighted-firstvisible;
    if (idx>=0 && idx<divcount) {
      var extra=(window.ieLT7 ? 8 : 2);
      highlightdiv.style.display='block';
      highlightdiv.style.width=(divs[idx].offsetWidth+extra)+"px";
      highlightdiv.style.height=(divs[idx].offsetHeight+extra)+"px";
      highlightdiv.style.top=(divs[idx].offsetTop-4)+"px";
      highlightdiv.style.left=(divs[idx].offsetLeft-4)+"px";
    } else {
      highlightdiv.style.display='none';
     }
  }
  this.highlight=function(n) {
    highlighted=n;
    fixhighlight();
  };
  this.itemReceived=function(itemno) {
    if (itemno>=firstvisible && itemno<firstvisible+divcount && putItemInCB) {
      putItemInCB(itemno, divs[itemno-firstvisible]);
    }
  };
  function movebar() {
    indtop=Math.floor(scrollPos*(areaht-eThHt-34)/totalsizeneeded+17);
    scthumb.style.top=indtop+'px';
  }
  function doind() {
    if (areaht>=totalsizeneeded) {
      scbar.style.visibility='hidden';
    } else {
      scbar.style.visibility='visible';
      indht = Math.floor(areaht*(areaht-34)/totalsizeneeded);
      if (indht>20) {
        eThHt = 0;
      } else {
        eThHt = 20-indht;
        indht = 20;
      }
      scthumb.style.height=(indht-2)+'px';
      scthmid.style.top=(indht/2-5)+'px';
      movebar();
    }
  }
  function changeFirstVisible(newfv) {
    var delta=newfv-firstvisible;
    firstvisible=newfv;
    var beginreplace=0, endreplace=divcount;

    /* Salvage any existing data */
    var i;
    if (delta<0 && -delta<divcount) {
      for (i=0; i<-delta; ++i) {
        divs.unshift(divs.pop());
      }
      endreplace=-delta;
    } else if (delta>0 && delta<divcount) {
      for (i=0; i<delta; ++i) {
        divs.push(divs.shift());
      }
      beginreplace=divcount-delta;
    }
    if (delta!==0) {
      i=beginreplace;
      if (putItemInCB) {
        for (; i<endreplace && firstvisible+i<itemcount; ++i) {
          putItemInCB(firstvisible+i, divs[i]);
        }
      }
      for (; i<endreplace; ++i) {
        divs[i].innerHTML='';
      }
    }
  }
  this.setWidth=function(wd, center) {
    matches.style.width=(wd+(window.ieLT7 ? 2 : 0))+"px";
    scbar.style.left=(wd-8)+"px";
    var newcolcount = Math.max(Math.floor(wd/divwd),1);
    if (colcount!=newcolcount) {
      colcount=newcolcount;
      var oldtsn = totalsizeneeded;
      totalsizeneeded=Math.max(Math.ceil(itemcount/colcount)*divht,1);
      doind();

      while (divcount>rowcount*colcount) {
        matches.removeChild(divs.pop());
        --divcount;
      }
      while (divcount<rowcount*colcount) {
        addItemDiv(firstvisible+divcount);
        ++divcount;
      }
      if (center==-1) {
        scrollTo((scrollPos+areaht*0.5)*totalsizeneeded/oldtsn-areaht*0.5, true);
      } else {
        scrollTo(Math.floor(center/colcount)*divht-(areaht-divht)/2, true);
      }
    }
    extraspace = wd-colcount*divwd;
    if (areaht<totalsizeneeded) {
      extraspace-=16;
    }
    placeItemsLeft();
  };
  this.setHeight=function(ht) {
    if (areaht!=ht) {
      areaht=ht;

      matches.style.height=(areaht-(window.ieLT7 ? 1 : 3))+"px";
      scbar.style.height=(areaht-3)+"px";
      scdn.style.top=(areaht-20)+"px";
      doind();
      scrollBy(0);
    }
  };
  function scrollBy(delta) {
    return scrollTo(scrollPos+delta, false);
  }
  function scrollTo(dest, force) {
    dest=Math.round(dest);
    var limit=(getLimitCB) ? getLimitCB() : itemcount;
    var pixellimit=Math.ceil(limit/colcount)*divht;
    if (dest+areaht>pixellimit) {
      dest=pixellimit-areaht;
    }
    if (dest<0) {
      dest=0;
    }
    if (dest==scrollPos && !force) {
      return false;
    }

    scrollPos=dest;
    var firstvisiblerow = Math.floor(scrollPos/divht);
    changeFirstVisible(firstvisiblerow*colcount);
    placeItemsTop(scrollPos-firstvisiblerow*divht);
    movebar();    
    return true;
  }
  function doAScroll() {
    if (sdelta && (skip%6!==0 || scrollBy(sdelta))) {
      setTimeout(doAScroll,50);
    } else {
      sdelta=0;
    }
    if (skip) {
      skip--;
    }
  }
  function findTop(obj) {
    var curtop = 0;
    if (obj.offsetParent) {
      curtop = obj.offsetTop;
      while ((obj = obj.offsetParent)) {
        curtop += obj.offsetTop;
      }
    }
    return curtop;
  }
  function mouseOut(e) {
    var ev=e || window.event;
    if (!ev.relatedTarget && !ev.toElement) {
      endScroll();
    }
  }
  function mwheelhandlerbody(e) {
    if (e.cancelable) {
      e.preventDefault();
    }
  }
  function mwheelhandlerbody2() {
    return false;
  }
  if (matches.addEventListener) {
    matches.addEventListener( 'DOMMouseScroll', function(e) {scrollBy(e.detail/3*51);}, false);
  } else {
    matches.onmousewheel=function() {scrollBy(-event.wheelDelta/120*51);};
  }
  function keyhandler(e) {
    var ev=e || window.event;
    var k = ev.keyCode ? ev.keyCode : ev.which;
    if (k>40 || k<33 || k==39 || (k>34 && k<38)) {
      return keyCB(k);
    }
    var press=(ev.type=='keypress');
    if (press==usepress) {
      scrollBy(k>=38 ? (k==38 ? -51 : 51) : (k==33 ? -231 : 231));
    }
    if (press) {
      usepress=true;
    }
    return false;
  }
  this.turnOn=function() {
    document.onmouseup=endScroll;
    document.body.onmouseout=mouseOut;
    if (document.body.addEventListener) {
      document.body.addEventListener( 'DOMMouseScroll', mwheelhandlerbody, false);
    } else {
      document.body.onmousewheel=mwheelhandlerbody2;
    }
    document.onkeypress = document.onkeydown= keyhandler;
    scbar.style.display="block";
    matches.style.display="block";
  };
  this.turnOff=function() {
    document.onmouseup=null;
    document.onmousemove=null;
    document.body.onmouseout = null;
    if (document.body.addEventListener) {
      document.body.removeEventListener( 'DOMMouseScroll', mwheelhandlerbody, false);
    } else {
      document.body.onmousewheel = null;
    }
    document.onkeypress = document.onkeydown = null;
    scbar.style.display="none";
    matches.style.display="none";
  };
}
