15 December 2007

Creating the Clippings Toolbar

Finally I've got the Clippings toolbar to work reasonably well. It can be invoked by right-clicking on the Clippings icon on the host application's status bar and choosing the Clippings Toolbar command. It is a floating palette toolbar (or "windoid" as the Mac folks say), so it appears above all windows.


You can try it out now by going to http://downloads.mozdev.org/clippings/experimental/ and downloading and installing the XPI file named clippings-2.99.3+_20071215.xpi (there is another XPI file in the same directory with the word "toolbar" at the end of its file name; it is an older, buggy build, so ignore it). Note that it hasn't yet been tested on Firefox or Thunderbird 1.5 series, and thus it isn't guaranteed to work on those versions.

A few challenges had to be overcome in order to implement the Clippings toolbar:
  1. The features string in the window.open() call which invokes the floating toolbar window includes the unknown string "popup" - this makes the window appear as a popup window which UI elements like menus and tooltips render as. However, this results in a chromeless window, lacking the title bar and borders. I had to style my own window border and title bar, resulting in a floating toolbar window that, as you can see from the above screen shot, won't necessarily match the appearance of floating toolbar windows in the user's desktop environment.

    The toolbar was invoked in this fashion to satisfy the criteria of a floating toolbar palette -- specifically, that it floats above all windows and that a taskbar button (in Windows) should not appear for the floating toolbar. Invoking the floating toolbar as a modeless dialog would satisfy the criteria, but on the Mac it may be rendered as a dialog sheet.

  2. The toolbar needs a reference to the most recent host application window so that it will perform its operations (insert clipping, new from selection) in the context of that window. This was made possible through the very useful XPCOM interface nsIWindowMediator.

  3. Moving the floating toolbar window by dragging its title bar was painful to implement. After over a week of trying to get it to work I gave up and searched online for drag-and-drop DHTML code, hoping that I could somehow adapt it to work with dragging an XUL window to move it. In the end, I figured it out on my own.

    In a nutshell, the drag start and stop can be initiated from the onmousedown and onmouseup events, respectively, on the toolbar palette's title bar (actually an XUL <vbox> element) -- but the actual move event handling needs to be invoked from the onmousemove event from the XUL <window> element. Handling the mouse move from the title bar (the <vbox> element) works only as long as the mouse stays within that element during the move operation; if the mouse is moved at a great distance on the screen, the mouse cursor jumps out of the title bar rather than moving the floating toolbar window. I still haven't figured out exactly why, although I suspect it has something to do with the DOM event flow as explained in the W3C's DOM 3 Events specification.
There's another problem: if the Clippings toolbar is invoked from both host applications (Firefox and Thunderbird), and both are open at the same time, two Clippings toolbars appear on the screen, and it's nearly impossible to distinguish which of the two is from which host app. More about this in another post.

3 comments:

Chris said...

I found a way to improve the mousemove, add the move event to mouseout as well...

var gWindoidMove = {
isMoving: false,
startCoords: null,

start: function (aEvent)
{
this.isMoving = true;
this.startCoords = { x: aEvent.clientX, y: aEvent.clientY };
},

stop: function (aEvent)
{
this.isMoving = false;
this.startCoords = null;
},

lost: function (aEvent)
{
if (this.isMoving && this.startCoords) {
var dx = aEvent.clientX - this.startCoords.x;
var dy = aEvent.clientY - this.startCoords.y;
window.moveBy(dx, dy)
}
},

move: function (aEvent)
{
if (this.isMoving && this.startCoords) {
var dx = aEvent.clientX - this.startCoords.x;
var dy = aEvent.clientY - this.startCoords.y;
window.moveBy(dx, dy)
}
}
};

window.addEventListener('mousemove', gWindoidMove.move, true)
window.addEventListener('mousedown', gWindoidMove.start, true)
window.addEventListener('mouseup', gWindoidMove.stop, true)
window.addEventListener('mouseout', gWindoidMove.lost, true)

Chris said...

Even better, connect the move function to BOTH the mouseover and mouseout events, works like a charm.

Chris said...

OK, last comment on the issue. Here is the implimentation I have settled on for Music Player Minion 1.4:
http://code.google.com/p/musicpm/

/* windowMove method inspired by Alex Eng's [ateng@users.sourceforge.net]
* implimentation in his Clippings extension:
* clippings-2.99.3+_20071215.xpi, content/hostappToolbar.js,
* released under MPL 1.1, http://www.mozilla.org/MPL/ for original license.
* This version has been adapted to fit MPM and improved.
*/
var windowMove = {
isMoving: false,
x: null,
y: null,

start: function (aEvent)
{
this.isMoving = true;
this.x = aEvent.clientX;
this.y = aEvent.clientY;
// MPD polling loop interferes with smooth movement of window.
mpd_stop = true
},

stop: function (aEvent)
{
this.isMoving = false;
// Restore MPD polling loop.
mpd_stop = false
checkStatus()
},

moveTo: function (aEvent)
{
if (this.isMoving) {
var dx = aEvent.screenX - this.x
var dy = aEvent.screenY - this.y
window.moveTo(dx, dy)
}
}
};

window.addEventListener('mousedown', windowMove.start, true)
window.addEventListener('mouseup', windowMove.stop, true)
window.addEventListener('mousemove', windowMove.moveTo, true)
window.addEventListener('mouseout', windowMove.moveTo, true)
window.addEventListener('mouseover', windowMove.moveTo, true)

I believe the moveTo method works better than moveBy, but I'm not positive.

Let me know if you feel the attribution comment needs to be changed. Thanks for posting about this, it was a big help.