Revision control

Copy as Markdown

Other Tools

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var { MailServices } = ChromeUtils.import(
);
// Each tag entry in our list looks like this:
// <listitem>
// <listcell>
// <textbox/>
// </listcell>
// <listcell>
// <colorpicker type='button'/>
// </listcell>
// </listitem>
// For ease of handling, all tag data is stored in <listitem>.tagInfo also.
const kOrdinalCharLow = "a";
const kOrdinalCharHigh = "z";
const kOrdinalPadding = String.fromCharCode(kOrdinalCharLow.charCodeAt(0) - 1);
var gInstantApply = document.documentElement.instantApply; // read only once
var gTagList = null; // tagList root element
var gAddButton = null;
var gDeleteButton = null;
var gRaiseButton = null;
var gLowerButton = null;
var gDeletedTags = {}; // tags marked for deletion in non-instant apply mode
function Startup()
{
gTagList = document.getElementById('tagList');
gAddButton = document.getElementById('addTagButton');
gDeleteButton = document.getElementById('deleteTagButton');
gRaiseButton = document.getElementById('raiseTagButton');
gLowerButton = document.getElementById('lowerTagButton');
InitTagList();
if (!gInstantApply)
window.addEventListener("dialogaccept", this.OnOK, true);
UpdateButtonStates();
}
function InitTagList()
{
// Read the tags from preferences via the tag service.
var tagArray = MailServices.tags.getAllTags();
for (var i = 0; i < tagArray.length; ++i)
{
var t = tagArray[i];
var tagInfo = {tag: t.tag,
key: t.key,
color: t.color,
ordinal: t.ordinal,
new: false, // not added in this run
changed: false}; // not changed (yet)
AppendTagEntry(tagInfo, null);
}
}
// read text and color from the listitem
function UpdateTagInfo(aTagInfo, aEntry)
{
var tag = aEntry.firstChild.firstChild.value;
var color = aEntry.lastChild.lastChild.color;
if (tag != aTagInfo.tag || color != aTagInfo.color)
{
aTagInfo.changed = true; // never unset changed flag here!
aTagInfo.tag = tag;
aTagInfo.color = color;
}
}
// set text and color of the listitem
function UpdateTagEntry(aTagInfo, aEntry)
{
aEntry.firstChild.firstChild.value = aTagInfo.tag;
aEntry.lastChild.lastChild.color = aTagInfo.color || 'inherit';
}
function AppendTagEntry(aTagInfo, aRefChild)
{
// Creating a colorpicker dynamically in an onload handler is really sucky.
// You MUST first set its type attribute (to select the correct binding), then
// add the element to the DOM (to bind the binding) and finally set the color
// property(!) afterwards. Try in any other order and fail... :-(
var tagCell = document.createElement('listcell');
var textbox = document.createElement('textbox');
textbox.setAttribute('flex', 1);
textbox.setAttribute('value', aTagInfo.tag);
tagCell.appendChild(textbox);
var colorCell = document.createElement('listcell');
var colorpicker = document.createElement('colorpicker');
colorpicker.setAttribute('type', 'button');
colorpicker.setAttribute('color', aTagInfo.color || 'inherit')
colorCell.appendChild(colorpicker);
var entry = document.createElement('listitem');
entry.addEventListener('focus', OnFocus, true);
entry.addEventListener('change', OnChange);
entry.setAttribute('allowevents', 'true'); // activate textbox and colorpicker
entry.tagInfo = aTagInfo;
entry.appendChild(tagCell);
entry.appendChild(colorCell);
gTagList.insertBefore(entry, aRefChild);
return entry;
}
function OnFocus(aEvent)
{
gTagList.selectedItem = this;
UpdateButtonStates();
}
function FocusTagEntry(aEntry)
{
// focus the entry's textbox
gTagList.ensureElementIsVisible(aEntry);
aEntry.firstChild.firstChild.focus();
}
function GetTagOrdinal(aTagInfo)
{
if (aTagInfo.ordinal)
return aTagInfo.ordinal;
return aTagInfo.key;
}
function SetTagOrdinal(aTagInfo, aOrdinal)
{
var ordinal = aTagInfo.ordinal;
aTagInfo.ordinal = (aTagInfo.key != aOrdinal) ? aOrdinal : '';
if (aTagInfo.ordinal != ordinal)
aTagInfo.changed = true;
}
function BisectString(aPrev, aNext)
{
// find a string which is lexically greater than aPrev and lesser than aNext:
// - copy leading parts common to aPrev and aNext into the result
// - find the first position where aPrev and aNext differ:
// - if we can squeeze a character in between there: fine, done!
// - if not:
// - if the rest of aNext is longer than one character, we can squeeze
// in just the first aNext rest-character and be done!
// - else we try to "increment" aPrev a bit to fit in
if ((aPrev >= aNext) || (aPrev + kOrdinalCharLow >= aNext))
return ''; // no such string exists
// pad the shorter string
var lenPrev = aPrev.length;
var lenNext = aNext.length;
var lenMax = Math.max(lenPrev, lenNext);
// loop over both strings at once, padding if necessary
var constructing = false;
var result = '';
for (var i = 0; i < lenMax; ++i)
{
var prevChar = (i < lenPrev) ? aPrev[i] : kOrdinalPadding;
var nextChar = constructing ? kOrdinalCharHigh
: (i < lenNext) ? aNext[i]
: kOrdinalPadding;
var prevCode = prevChar.charCodeAt(0);
var nextCode = nextChar.charCodeAt(0);
if (prevCode == nextCode)
{
// copy common characters
result += prevChar;
}
else if (prevCode + 1 < nextCode)
{
// found a real bisecting string
result += String.fromCharCode((prevCode + nextCode) / 2);
return result;
}
else
{
// nextCode is greater than prevCode, but there's no place in between.
// But if aNext[i+1] exists, then nextChar will suffice and we're done!
// ("x" < "xsomething")
if (i + 1 < lenNext)
{
// found a real bisecting string
return result + nextChar;
}
// just copy over prevChar and enter construction mode
result += prevChar;
constructing = true;
}
}
return ''; // nothing found
}
function RecalculateOrdinal(aEntry)
{
// Calculate a new ordinal for the given entry, assuming that both its
// predecessor's and successor's are correct, i.e. ord(p) < ord(s)!
var tagInfo = aEntry.tagInfo;
var ordinal = tagInfo.key;
// get neighbouring ordinals
var prevOrdinal = '', nextOrdinal = '';
var prev = aEntry.previousSibling;
if (prev && prev.nodeName == 'listitem') // first.prev == listhead
prevOrdinal = GetTagOrdinal(prev.tagInfo);
var next = aEntry.nextSibling;
if (next)
{
nextOrdinal = GetTagOrdinal(next.tagInfo);
}
else
{
// ensure key < nextOrdinal if entry is the last/only entry
nextOrdinal = prevOrdinal || ordinal;
nextOrdinal = String.fromCharCode(nextOrdinal.charCodeAt(0) + 2);
}
if (prevOrdinal < ordinal && ordinal < nextOrdinal)
{
// no ordinal needed, just clear it
SetTagOrdinal(tagInfo, '')
return;
}
// so we need a new ordinal, because key <= prevOrdinal or key >= nextOrdinal
ordinal = BisectString(prevOrdinal, nextOrdinal);
if (ordinal)
{
// found a new ordinal
SetTagOrdinal(tagInfo, ordinal)
return;
}
// couldn't find an ordinal before the nextOrdinal, so take that instead
// and recalculate a new one for the next entry
SetTagOrdinal(tagInfo, nextOrdinal);
if (next)
ApplyChange(next);
}
function OnChange(aEvent)
{
ApplyChange(aEvent.currentTarget);
}
function ApplyChange(aEntry)
{
if (!aEntry)
{
dump('ApplyChange: aEntry is null! (called by ' + ApplyChange.caller.name + ')\n');
return;
}
// the tag data got changed, so write it back to the system
var tagInfo = aEntry.tagInfo;
UpdateTagInfo(tagInfo, aEntry);
// ensure unique tag name
var dupeList = ReadTagListFromUI(aEntry);
var uniqueTag = DisambiguateTag(tagInfo.tag, dupeList);
if (tagInfo.tag != uniqueTag)
{
tagInfo.tag = uniqueTag;
tagInfo.changed = true;
UpdateTagEntry(tagInfo, aEntry);
}
if (gInstantApply)
{
// If the item was newly added, we still can rename the key,
// so that it's in sync with the actual tag.
if (tagInfo.new && tagInfo.key)
{
// Do not clear the "new" flag!
// The key will only stick after closing the dialog.
MailServices.tags.deleteKey(tagInfo.key);
tagInfo.key = '';
}
if (!tagInfo.key)
{
// create a new key, based upon the new tag
MailServices.tags.addTag(tagInfo.tag, '', '');
tagInfo.key = MailServices.tags.getKeyForTag(tagInfo.tag);
}
// Recalculate the sort ordinal, if necessary.
// We assume that the neighbour's ordinals are correct,
// i.e. that ordinal(pos - 1) < ordinal(pos + 1)!
RecalculateOrdinal(aEntry);
WriteTag(tagInfo);
}
}
function WriteTag(aTagInfo)
{
//dump('********** WriteTag: ' + aTagInfo.toSource() + '\n');
try
{
MailServices.tags.addTagForKey(aTagInfo.key, aTagInfo.tag, aTagInfo.color,
aTagInfo.ordinal);
aTagInfo.changed = false;
}
catch (e)
{
dump('WriteTag: update exception:\n' + e);
}
}
function UpdateButtonStates()
{
var entry = gTagList.selectedItem;
// disable Delete if no selection
gDeleteButton.disabled = !entry;
// disable Raise if no selection or first entry
gRaiseButton.disabled = !entry || !gTagList.getPreviousItem(entry, 1);
// disable Lower if no selection or last entry
gLowerButton.disabled = !entry || !gTagList.getNextItem(entry, 1);
}
function ReadTagListFromUI(aIgnoreEntry)
{
// reads distinct tag names from the UI
var dupeList = {}; // indexed by tag
for (var entry = gTagList.firstChild; entry; entry = entry.nextSibling)
if ((entry != aIgnoreEntry) && (entry.localName == 'listitem'))
dupeList[entry.firstChild.firstChild.value] = true;
return dupeList;
}
function DisambiguateTag(aTag, aTagList)
{
if (aTag in aTagList)
{
var suffix = 2;
while (aTag + ' ' + suffix in aTagList)
++suffix;
aTag += ' ' + suffix;
}
return aTag;
}
function AddTag()
{
// Add a new tag to the UI here.
// It will be be written to the preference system
// (a) directly on each change for instant apply, or
// (b) only if the dialogaccept handler is executed.
// create new unique tag name
var dupeList = ReadTagListFromUI();
var tag = DisambiguateTag(gAddButton.getAttribute('defaulttagname'), dupeList);
// create new tag list entry
var tagInfo = {tag: tag,
key: '',
color: 'inherit',
ordinal: '',
new: true,
changed: true};
var refChild = gTagList.getNextItem(gTagList.selectedItem, 1);
var newEntry = AppendTagEntry(tagInfo, refChild);
ApplyChange(newEntry);
FocusTagEntry(newEntry);
}
function DeleteTag()
{
// Delete the selected tag from the UI here. If it was added during this
// preference dialog session, we can drop it at once; if it was read from
// the preferences system, we may need to remember killing it in OnOK.
var entry = gTagList.selectedItem;
var key = entry.tagInfo.key;
if (key)
{
if (gInstantApply)
MailServices.tags.deleteKey(key);
else
gDeletedTags[key] = true; // dummy value
}
// after removing, move focus to next entry, if it exist, else try previous
var newFocusItem = gTagList.getNextItem(entry, 1) ||
gTagList.getPreviousItem(entry, 1);
gTagList.removeItemAt(gTagList.getIndexOfItem(entry));
if (newFocusItem)
FocusTagEntry(newFocusItem);
else
UpdateButtonStates();
}
function MoveTag(aMoveUp)
{
// Move the selected tag one position up or down in the tagList's child order.
// This reordering may require changing ordinal strings.
var entry = gTagList.selectedItem;
var tagInfo = entry.tagInfo;
UpdateTagInfo(tagInfo, entry); // remember changed values
var successor = aMoveUp ? gTagList.getPreviousItem(entry, 1)
: gTagList.getNextItem(entry, 2);
entry.parentNode.insertBefore(entry, successor);
FocusTagEntry(entry);
tagInfo.changed = true;
UpdateTagEntry(tagInfo, entry); // needs to be visible
ApplyChange(entry);
}
function Restore()
{
// clear pref panel tag list
// Remember any known keys for deletion in the OKHandler.
while (gTagList.getRowCount())
{
var key = gTagList.removeItemAt(0).tagInfo.key;
if (key)
{
if (gInstantApply)
MailServices.tags.deleteKey(key);
else
gDeletedTags[key] = true; // dummy value
}
}
// add default items (no ordinal strings for those)
for (var i = 1; i <= 5; ++i)
{
// create default tags from the former label defaults
var key = "$label" + i;
var tag = GetLocalizedStringPref("mailnews.labels.description." + i);
var color = Services.prefs.getDefaultBranch("mailnews.labels.color.").getCharPref(i);
var tagInfo = {tag: tag,
key: key,
color: color,
ordinal: '',
new: false,
changed: true};
var newEntry = AppendTagEntry(tagInfo, null);
ApplyChange(newEntry);
}
FocusTagEntry(gTagList.getItemAtIndex(0));
}
function OnOK()
{
// remove all deleted tags from the preferences system
for (var key in gDeletedTags)
MailServices.tags.deleteKey(key);
// Write tags to the preferences system, creating keys and ordinal strings.
for (var entry = gTagList.firstChild; entry; entry = entry.nextSibling)
{
if (entry.localName == 'listitem')
{
// only write listitems which have changed (this includes new ones)
var tagInfo = entry.tagInfo;
if (tagInfo.changed)
{
if (!tagInfo.key)
{
// newly added tag, need to create a key and read it
MailServices.tags.addTag(tagInfo.tag, '', '');
tagInfo.key = MailServices.tags.getKeyForTag(tagInfo.tag);
}
if (tagInfo.key)
{
// Recalculate the sort ordinal, if necessary.
// We assume that the neighbour's ordinals are correct,
// i.e. that ordinal(pos - 1) < ordinal(pos + 1)!
RecalculateOrdinal(entry);
// update the tag definition
WriteTag(tagInfo);
}
}
}
}
}