[Pkg-mozext-commits] [nosquint] 12/47: Import of 2.0.1b1 release into git

David Prévot taffit at moszumanska.debian.org
Tue Apr 28 01:41:17 UTC 2015


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to annotated tag 2.1.6
in repository nosquint.

commit fbb2801c89e19f13c72e20ccf69817dd12768198
Author: Jason Tackaberry <tack at urandom.ca>
Date:   Fri Jan 13 19:41:13 2012 -0500

    Import of 2.0.1b1 release into git
---
 src/chrome.manifest                    |   2 +
 src/chrome.manifest.in                 |   2 +
 src/content/browser.js                 | 472 ++++++++++++++++
 src/content/cmd.js                     | 184 +++++++
 src/content/dlg-global.js              | 241 ++++++++
 src/content/dlg-global.xul             | 218 ++++++++
 src/content/dlg-help.js                |  10 +
 src/content/dlg-help.xul               |  23 +
 src/content/dlg-site.js                | 176 ++++++
 src/content/dlg-site.xul               | 118 ++++
 src/content/dlg-style.css              |  35 ++
 src/content/init.js                    |  80 ++-
 src/content/interfaces.js              | 211 +++++++
 src/content/lib.js                     | 220 ++++++++
 src/content/overlay.xul                |  89 ++-
 src/content/overlay_sanitize.xul       |   8 +
 src/content/prefs.js                   | 980 ++++++++++++++++++++++++++-------
 src/content/sanitize.js                |  81 +++
 src/content/two-level-tlds             |  91 +++
 src/content/zoommanager.js             |  62 +++
 src/defaults/preferences/nosquint.js   |  16 +-
 src/install.rdf                        |  12 +-
 src/locale/en-US/dlg-global.dtd        |  53 ++
 src/locale/en-US/dlg-global.properties |   3 +
 src/locale/en-US/dlg-help.dtd          |   2 +
 src/locale/en-US/dlg-site.dtd          |   7 +
 src/locale/en-US/dlg-site.properties   |   4 +
 src/locale/en-US/help.html             | 211 +++++--
 src/locale/en-US/overlay.dtd           |   7 +
 src/locale/en-US/overlay.properties    |  10 +
 src/skin/icon-enlarge-16.png           | Bin 652 -> 3316 bytes
 src/skin/icon-enlarge-24.png           | Bin 1281 -> 3794 bytes
 src/skin/icon-reduce-16.png            | Bin 654 -> 3313 bytes
 src/skin/icon-reduce-24.png            | Bin 1233 -> 3736 bytes
 src/skin/icon-reset-16.png             | Bin 0 -> 3335 bytes
 src/skin/icon-reset-24.png             | Bin 0 -> 3758 bytes
 src/skin/icon-statusbar-16.png         | Bin 0 -> 854 bytes
 src/skin/logo-32.png                   | Bin 0 -> 2418 bytes
 src/skin/toolbar.css                   |  16 +
 39 files changed, 3313 insertions(+), 331 deletions(-)

diff --git a/src/chrome.manifest b/src/chrome.manifest
index e87d7a0..8fe1d1e 100644
--- a/src/chrome.manifest
+++ b/src/chrome.manifest
@@ -1,5 +1,7 @@
 content nosquint content/
 overlay chrome://browser/content/browser.xul chrome://nosquint/content/overlay.xul
+overlay chrome://browser/content/preferences/sanitize.xul  chrome://nosquint/content/overlay_sanitize.xul
+overlay chrome://browser/content/sanitize.xul chrome://nosquint/content/overlay_sanitize.xul
 
 locale nosquint en-US locale/en-US/
 
diff --git a/src/chrome.manifest.in b/src/chrome.manifest.in
index c576e6c..bc2ae92 100644
--- a/src/chrome.manifest.in
+++ b/src/chrome.manifest.in
@@ -1,5 +1,7 @@
 content nosquint ${JAR}content/
 overlay chrome://browser/content/browser.xul chrome://nosquint/content/overlay.xul
+overlay chrome://browser/content/preferences/sanitize.xul  chrome://nosquint/content/overlay_sanitize.xul
+overlay chrome://browser/content/sanitize.xul chrome://nosquint/content/overlay_sanitize.xul
 
 locale nosquint en-US ${JAR}locale/en-US/
 
diff --git a/src/content/browser.js b/src/content/browser.js
new file mode 100644
index 0000000..70af012
--- /dev/null
+++ b/src/content/browser.js
@@ -0,0 +1,472 @@
+// chrome://browser/content/browser.xul
+
+/******************************************************************************
+ * Browser
+ *
+ */
+NoSquint.browser = NoSquint.ns(function() { with (NoSquint) {
+    const CI = Components.interfaces;
+    this.id = 'NoSquint.browser';
+    var zoomAllTimer = null;             // Timer for queueZoomAll()
+    var styleAllTimer = null;            // Timer for queueStyleAll()
+    var updateStatusTimer = null;        // Timer for queueUpdateStatus()
+
+    this.init = function() {
+        this.gBrowser = gBrowser;
+        this.updateZoomMenu();
+
+        this.observer = new NSQ.interfaces.Observer();
+        this.observer.watcher = {
+            onEnterPrivateBrowsing: function() {
+                this.closeSiteSettings();
+                // Switching the private browsing mode.  Store any current pending
+                // changes now.
+                NSQ.prefs.saveSiteList(true);
+                // Save current (non-private) site data for when we exit private
+                // browsing.
+                this.origSites = NSQ.prefs.cloneSites();
+            },
+
+            onExitPrivateBrowsing: function() {
+                this.closeSiteSettings();
+                // Restore previously saved site data and rezoom/style all tabs.
+                NSQ.prefs.sites = this.origSites;
+                this.origSites = null;
+                NSQ.browser.zoomAll();
+                NSQ.browser.styleAll();
+            },
+
+            closeSiteSettings: function() {
+                if (NSQ.storage.dialogs.site)
+                    NSQ.storage.dialogs.site.die();
+            }
+        };
+
+        if (this.observer.inPrivateBrowsing)
+            this.observer.watcher.onEnterPrivateBrowsing();
+
+        window.addEventListener('DOMMouseScroll', this.handleMouseScroll, false); 
+        // XXX: used for image zoom, which feature is currently removed.
+        //window.addEventListener("resize", this.handleResize, false);
+        gBrowser.tabContainer.addEventListener('TabOpen', this.handleTabOpen, false);
+        gBrowser.tabContainer.addEventListener('TabSelect', this.handleTabSelect, false);
+        gBrowser.tabContainer.addEventListener('TabClose', this.handleTabClose, false);
+
+        this.zoomAll(null, true);
+    };
+
+    this.destroy = function() {
+    };
+
+
+    /* Event handlers.  Reminder: 'this' will not be NSQ.browser
+     */
+
+    this.handleMouseScroll = function(event) {
+        if (!event.ctrlKey)
+            return;
+        if (NSQ.prefs.wheelZoomEnabled) {
+            var browser = gBrowser.selectedBrowser;
+            var text = full = false;
+            var increment = NSQ.prefs.zoomIncrement * (event.detail < 0 ? 1 : -1);
+            //var img = isImage(browser);
+            var img = false;
+                
+            if (NSQ.prefs.wheelZoomInvert)
+                increment *= -1;
+
+            if (NSQ.prefs.fullZoomPrimary && !event.shiftKey || !NSQ.prefs.fullZoomPrimary && event.shiftKey || img)
+                full = Math.round((browser.markupDocumentViewer.fullZoom * 100) + increment);
+            else
+                text = Math.round((browser.markupDocumentViewer.textZoom * 100) + increment);
+
+            //if (!img || !browser.getUserData('nosquint').site) {
+            if (!img) {
+                NSQ.browser.zoom(browser, text, full);
+                NSQ.browser.saveCurrentZoom();
+            }
+        }
+        event.stopPropagation();
+        event.preventDefault();
+    };
+
+
+    // Would be used for image zoom, but currently not implemented.
+    this.handleResize = function(event) {
+    };
+
+    this.handleTabOpen = function(event) {
+        var browser = event.target.linkedBrowser;
+        NSQ.browser.attach(browser);
+        NSQ.browser.zoom(browser);
+    };
+
+    this.handleTabSelect = function(event) {
+        NSQ.browser.updateStatus();
+    };
+
+    this.handleTabClose = function(event) {
+        var browser = event.target.linkedBrowser;
+        browser.removeProgressListener(browser.getUserData('nosquint').listener);
+        browser.setUserData('nosquint', null, null);
+    };
+
+
+    /* Updates View | Zoom menu to replace the default Zoom In/Out menu
+     * items with Primary Zoom In/Out and Secondary Zoom In/Out.  Also the
+     * "Zoom Text Only" menuitem is replaced with an option to open the NS
+     * Global prefs.
+     */
+    this.updateZoomMenu = function() {
+        var popup = $('viewFullZoomMenu').childNodes[0];
+        var full_zoom_primary = NSQ.prefs.fullZoomPrimary;
+
+        if (!$('nosquint-view-menu-settings')) {
+            for (let [i, child] in enumerate(popup.childNodes)) {
+                if (child.id == 'toggle_zoom')
+                    child.style.display = 'none';
+                if (child.nodeName != 'menuitem' || (child.command != 'cmd_fullZoomEnlarge' && 
+                    child.command != 'cmd_fullZoomReduce'))
+                    continue;
+
+                var icon = document.defaultView.getComputedStyle(child, null).getPropertyValue('list-style-image');
+                var enlarge = child.command == 'cmd_fullZoomEnlarge';
+                var item = document.createElement('menuitem');
+                var suffix = "noSquint" + (enlarge ? "Enlarge" : "Reduce") + "Secondary";
+                item.setAttribute("command",  "cmd_" + suffix);
+                item.setAttribute("key",  "key_" + suffix);
+                item.style.listStyleImage = icon;
+                popup.insertBefore(item, popup.childNodes[i + 2]);
+            }
+
+            var item = document.createElement('menuitem');
+            item.id = 'nosquint-view-menu-settings';
+            item.setAttribute('command', 'cmd_noSquintPrefs');
+            item.setAttribute('label', NSQ.strings.zoomMenuSettings);
+            popup.appendChild(item);
+        }
+
+        for (let child in iter(popup.childNodes)) {
+            if (child.nodeName != 'menuitem')
+                continue;
+            var command = child.getAttribute('command');
+            if (command == "cmd_fullZoomEnlarge")
+                child.setAttribute('label', NSQ.strings['zoomMenuIn' + (full_zoom_primary ? "Full" : "Text")]);
+            else if (command == "cmd_noSquintEnlargeSecondary")
+                child.setAttribute('label', NSQ.strings['zoomMenuIn' + (full_zoom_primary ? "Text" : "Full")]);
+            if (command == "cmd_fullZoomReduce")
+                child.setAttribute('label', NSQ.strings['zoomMenuOut' + (full_zoom_primary ? "Full" : "Text")]);
+            else if (command == "cmd_noSquintReduceSecondary")
+                child.setAttribute('label', NSQ.strings['zoomMenuOut' + (full_zoom_primary ? "Text" : "Full")]);
+        }
+    };
+
+
+    /* Updates the status panel and tooltip to reflect current site name
+     * and zoom levels.
+     */
+    this.updateStatus = function() {
+        var browser = gBrowser.selectedBrowser;
+        var site = browser.getUserData('nosquint').site;
+        // Disable/enable context menu item.
+        $('nosquint-menu-settings').disabled = (site === null);
+
+        if (updateStatusTimer) {
+            clearTimeout(updateStatusTimer);
+            updateStatusTimer = null;
+        }
+
+        if (NSQ.prefs.hideStatus)
+            // Pref indicates we're hiding status panel, no sense in updating.
+            return;
+
+        var text = Math.round(browser.markupDocumentViewer.textZoom * 100);
+        var full = Math.round(browser.markupDocumentViewer.fullZoom * 100);
+        var [text_default, full_default] = NSQ.prefs.getZoomDefaults();
+
+        var e = $('nosquint-status');
+        if (site) {
+            if (NSQ.prefs.fullZoomPrimary)
+                e.label = full + '%' + (text == 100 ? '' : (' / ' + text + '%'));
+            else
+                e.label = text + '%' + (full == 100 ? '' : (' / ' + full + '%'));
+            $('nosquint-status-tooltip-site').value = site.replace(/%20/g, ' ');
+            $('nosquint-status-tooltip-full').value = full + '%';
+            $('nosquint-status-tooltip-text').value = text + '%';
+
+            var style = this.getStyleForBrowser(browser);
+            var label = $('nosquint-status-tooltip-textcolor');
+            label.style.color = style.colorText || 'inherit';
+            label.style.backgroundColor = style.colorBackground || 'inherit';
+            label.value = (style.colorText || style.colorBackground) ? 'Sample' : 'Site Controlled';
+
+            var vis = $('nosquint-status-tooltip-vis-link');
+            var unvis = $('nosquint-status-tooltip-unvis-link');
+            unvis.value = vis.value = '';
+            vis.style.color = vis.style.textDecoration = 'inherit';
+            unvis.style.color = unvis.style.textDecoration = 'inherit';
+
+            if (!style.linksUnvisited && !style.linksVisited)
+                unvis.value = 'Site Controlled';
+            else {
+                for (let [attr, elem] in items({'linksUnvisited': unvis, 'linksVisited': vis})) {
+                    if (style[attr]) {
+                        elem.value = attr.replace('links', '');
+                        elem.style.color = style[attr];
+                        elem.style.textDecoration = style.linksUnderline ? 'underline' : 'inherit';
+                    }
+                }
+            }
+            $('nosquint-status-tooltip').style.display = '';
+            e.style.fontStyle = e.style.opacity = 'inherit';
+        } else {
+            $('nosquint-status-tooltip').style.display = 'none';
+            e.label = 'N/A';
+            /* Lame: the documentation for statusbarpanel says there is a
+             * disabled attribute.  The DOM Inspector says otherwise.  So we
+             * must simulate the disabled look.
+             */
+            e.style.opacity = 0.5;
+            e.style.fontStyle = 'italic';
+        }
+    };
+
+    /* Queues an updateStatus().
+     */
+    this.queueUpdateStatus = function() {
+        if (!updateStatusTimer)
+            updateStatusTimer = setTimeout(function() NSQ.browser.updateStatus(), 1);
+    };
+
+    /* Given a browser, returns the site name.  Does not use the cached
+     * site name user data attached to the browser.
+     */
+    this.getSiteFromBrowser = function(browser) {
+        if (isChrome(browser))
+            return null;
+        return NSQ.prefs.getSiteFromURI(browser.currentURI);
+    };
+
+    /* Returns a 2-tuple [text, full] zoom levels for the given browser.
+     * Defaults are applied.
+     */
+    this.getZoomForBrowser = function(browser) {
+        var site = browser.getUserData('nosquint').site;
+        if (site === null) {
+            site = this.getSiteFromBrowser(browser);
+            browser.getUserData('nosquint').site = site;
+        }
+
+        var [text, full] = NSQ.prefs.getZoomForSite(site);
+        var [text_default, full_default] = NSQ.prefs.getZoomDefaults();
+        return [text || text_default, full || full_default];
+    };
+
+
+    /* Saves the current tab's zoom level in the site list.
+     */
+    this.saveCurrentZoom = function() {
+        var browser = gBrowser.selectedBrowser;
+        var site = browser.getUserData('nosquint').site;
+        if (!site)
+            // Nothing to save.  Chrome maybe.
+            return;
+
+        var text = Math.round(browser.markupDocumentViewer.textZoom * 100);
+        var full = Math.round(browser.markupDocumentViewer.fullZoom * 100);
+        debug("saveCurrentZoom(): site=" + site);
+        NSQ.prefs.updateSiteList(site, [text, full]);
+    };
+
+    this.attach = function(browser) {
+        var listener = new NSQ.interfaces.ProgressListener(browser);
+        browser.addProgressListener(listener, CI.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+        var userData = {
+            listener: listener,
+            stylers: []
+        };
+        browser.setUserData('nosquint', userData, null);
+
+        browser.addEventListener('DOMFrameContentLoaded', function(event) {
+            var styler = NSQ.browser.getDocumentStyler(browser, event.target.contentWindow.document);
+            styler();
+            browser.getUserData('nosquint').stylers.push(styler);
+        }, true);
+
+    };
+
+
+    /* Zooms text and/or full zoom to the specified level.  If text or full is
+     * null, the default for browser is used.  If it is false, it is
+     * untouched.  Status bar is updated, but new level is NOT saved.
+     */
+    this.zoom = function(browser, text, full) {
+        if (!browser || (text == false && full == false))
+            return false;
+
+        var t0 = new Date().getTime();
+        if (text == null || full == null) {
+            var [site_text, site_full] = this.getZoomForBrowser(browser);
+            if (text == null)
+                text = text || site_text;
+            if (full == null)
+                full = full || site_full;
+            // Only zoom web content, not chrome or plugins (e.g. PDF)
+            if (!browser.getUserData('nosquint').site)
+                [text, full] = [100, 100];
+        }
+
+        debug("zoom(): text=" + text + ", full=" + full);
+        if (text !== false)
+            browser.markupDocumentViewer.textZoom = text / 100.0;
+        if (full !== false)
+            browser.markupDocumentViewer.fullZoom = full / 100.0;
+        if (browser == gBrowser.selectedBrowser)
+            this.queueUpdateStatus();
+        var t1 = new Date().getTime();
+        debug('zoom(): took ' + (t1-t0));
+        return true;
+    };
+
+    /* Updates the zoom levels for all tabs; each tab is set to the levels
+     * for the current URIs of each browser.  If 'attach' is true, then
+     * ProgressListeners are attached to each browser as well.  This is
+     * useful on initialization, where we can hook into any tabs that may
+     * have been opened prior to initialization.
+     */
+    this.zoomAll = function(site, attach) {
+        debug("zoomAll(): site=" + site + ", attach=" + attach);
+        for (let browser in iter(gBrowser.browsers)) {
+            if (site && site != browser.getUserData('nosquint').site)
+                continue;
+            if (attach)
+                this.attach(browser);
+            this.zoom(browser);
+        }
+        clearTimeout(zoomAllTimer);
+        zoomAllTimer = null;
+    };
+
+    /* Queues a zoomAll.  Useful when we might otherwise call zoomAll() 
+     * multiple times, such as in the case of multiple preferences being
+     * updated at once.
+     */
+    this.queueZoomAll = function(site, delay) {
+        if (delay === undefined)
+            delay = 1;
+        if (!zoomAllTimer)
+            zoomAllTimer = setTimeout(function() NSQ.browser.zoomAll(site), delay);
+    };
+
+
+    /* Returns a style object for the given browser.  Defaults are applied.
+     */
+    this.getStyleForBrowser = function(browser) {
+        var site = browser.getUserData('nosquint').site;
+        var style = NSQ.prefs.getStyleForSite(site);
+        return NSQ.prefs.applyStyleGlobals(style);
+    };
+
+    /* Returns CSS string for the given style object.
+     */
+    this.getCSSFromStyle = function(style) {
+        var css = '';
+        if (style.colorText || style.colorBackground || style.colorBackgroundImages) {
+            css += 'body,p,div,span,center,blockquote,h1,h2,h3,h4,h5,table,tr,th,td,iframe,a,b,i {';
+            if (style.colorText)
+                css += 'color: ' + style.colorText + ' !important;';
+            if (style.colorBackground)
+                css += 'background-color: ' + style.colorBackground + ' !important;';
+            if (style.colorBackgroundImages)
+                css += 'background-image: none !important;';
+            css += '}\n';
+        };
+
+        if (style.linksUnvisited)
+            css += 'a:link { color: ' + style.linksUnvisited + ' !important; }\n';
+        if (style.linksVisited)
+            css += 'a:visited { color: ' + style.linksVisited + ' !important; }\n';
+        if (style.linksUnderline)
+            css += 'a { text-decoration: underline !important; }\n';
+
+        return css;
+    };
+
+    /* Returns a function that, when invoked, will style the given document
+     * from the given browser.  The styler function can be explicitly passed
+     * a style attributes object to override the calculated one for the site.
+     */
+    this.getDocumentStyler = function(browser, doc) {
+        var styleobj = null;
+        function styler(style) {
+            if (!style)
+                style = NSQ.browser.getStyleForBrowser(browser);
+
+            debug("styler(): enabled=" + style.enabled + ", obj=" + styleobj);
+            if (style.enabled) {
+                if (!styleobj) {
+                    styleobj = doc.createElementNS('http://www.w3.org/1999/xhtml', 'style');
+                    // This doesn't appear to be necessary, and in any case seems
+                    // to not work when there are CSS problems on the site (like google
+                    // sometimes has).
+                    //var head = doc.getElementsByTagName('head');
+                    //var node = (head ? head[0] : doc.documentElement);
+                    //node.insertBefore(styleobj, node.childNodes[0]);
+                    doc.documentElement.appendChild(styleobj);
+                }
+                var css = NSQ.browser.getCSSFromStyle(style);
+                styleobj.textContent = css;
+            } else if (styleobj) {
+                styleobj.parentNode.removeChild(styleobj);
+                // Must recreate style object if we want to attach later.
+                styleobj = null;
+            }
+        }
+        return styler;
+    };
+
+    /* Adds a styler to the given document if none exist, and invokes all
+     * attached stylers with the given style.
+     *
+     * If the document cannot be styled, false is returned.  Otherwise, true.
+     */
+    this.style = function(browser, style) {
+        var doc = browser.docShell.document;
+        if (!doc.documentElement)
+            // Nothing to style; chrome?
+            return false;
+
+        var stylers = browser.getUserData('nosquint').stylers;
+        if (stylers.length == 0)
+            // Initial styling; attach styler for document (or frameset).
+            stylers.push(this.getDocumentStyler(browser, doc));
+
+        debug("style(): num stylers=" + stylers.length);
+        for (let styler in iter(stylers))
+            styler(style);
+
+        if (browser == gBrowser.selectedBrowser)
+            this.queueUpdateStatus();
+
+        return true;
+    };
+
+    this.styleAll = function(site) {
+        for (let browser in iter(gBrowser.browsers)) {
+            if (site && site != browser.getUserData('nosquint').site)
+                continue;
+            this.style(browser);
+        }
+        clearTimeout(styleAllTimer);
+        styleAllTimer = null;
+    };
+
+    /* Queues a styleAll.
+     */
+    this.queueStyleAll = function(site, delay) {
+        if (delay === undefined)
+            delay = 1;
+        if (!styleAllTimer)
+            styleAllTimer = setTimeout(function() NSQ.browser.styleAll(site), delay);
+    };
+}});
diff --git a/src/content/cmd.js b/src/content/cmd.js
new file mode 100644
index 0000000..eba119a
--- /dev/null
+++ b/src/content/cmd.js
@@ -0,0 +1,184 @@
+/******************************************************************************
+ * Commands
+ *
+ * Functions that are invoked as a result of some UI action.
+ *
+ */
+NoSquint.cmd = NoSquint.ns(function() { with (NoSquint) {
+
+    /* Handlers for toolar buttons */
+    this.buttonEnlarge = function(event) {
+        event.shiftKey ? NSQ.cmd.enlargeSecondary() : NSQ.cmd.enlargePrimary();
+    };
+
+    this.buttonReduce = function(event) {
+        event.shiftKey ? NSQ.cmd.reduceSecondary() : NoSquint.cmd.reducePrimary();
+    };
+
+    this.buttonReset = function(event) {
+        NSQ.cmd.reset();
+    };
+
+    /* Handlers for commands defined in overlay.xul */
+    this.enlargePrimary = function() {
+        NSQ.prefs.fullZoomPrimary ? NSQ.cmd.enlargeFullZoom() : NSQ.cmd.enlargeTextZoom();
+    };
+
+    this.reducePrimary = function() {
+        NSQ.prefs.fullZoomPrimary ? NSQ.cmd.reduceFullZoom() : NSQ.cmd.reduceTextZoom();
+    };
+
+    this.enlargeSecondary = function() {
+        NSQ.prefs.fullZoomPrimary ? NSQ.cmd.enlargeTextZoom() : NSQ.cmd.enlargeFullZoom();
+    };
+
+    this.reduceSecondary = function() {
+        NSQ.prefs.fullZoomPrimary ? NSQ.cmd.reduceTextZoom() : NSQ.cmd.reduceFullZoom();
+    };
+
+    this.reset = function() {
+        var [text, full] = NSQ.prefs.getZoomDefaults();
+        var viewer = getBrowser().mCurrentBrowser.markupDocumentViewer;
+        var updated = false;
+
+        if (Math.round(viewer.textZoom * 100.0) != text)
+            updated = viewer.textZoom = text / 100.0;
+        if (Math.round(viewer.fullZoom * 100.0) != full)
+            updated = viewer.fullZoom = full / 100.0;
+        
+        if (updated != false) {
+            NSQ.browser.saveCurrentZoom();
+            NSQ.browser.updateStatus();
+        }
+    };
+
+    this.enlargeTextZoom = function() {
+        var browser = getBrowser().mCurrentBrowser;
+        if (isImage(browser))
+            return NSQ.cmd.enlargeFullZoom();
+        var mdv = browser.markupDocumentViewer;
+        mdv.textZoom = Math.round(mdv.textZoom * 100.0 + NSQ.prefs.zoomIncrement) / 100.0;
+        NSQ.browser.saveCurrentZoom();
+        NSQ.browser.updateStatus();
+    };
+
+    this.reduceTextZoom = function() {
+        var browser = getBrowser().mCurrentBrowser;
+        if (isImage(browser))
+            return NSQ.cmd.reduceFullZoom();
+        var mdv = browser.markupDocumentViewer;
+        mdv.textZoom = Math.round(mdv.textZoom * 100.0 - NSQ.prefs.zoomIncrement) / 100.0;
+        NSQ.browser.saveCurrentZoom();
+        NSQ.browser.updateStatus();
+    };
+
+    this.enlargeFullZoom = function() {
+        var browser = getBrowser().mCurrentBrowser;
+        if (isImage(browser) && browser.getUserData('nosquint').fit)
+            return;
+        var mdv = browser.markupDocumentViewer;
+        mdv.fullZoom = Math.round(mdv.fullZoom * 100.0 + NSQ.prefs.zoomIncrement) / 100.0;
+        NSQ.browser.saveCurrentZoom();
+        NSQ.browser.updateStatus();
+    };
+
+    this.reduceFullZoom = function() {
+        var browser = getBrowser().mCurrentBrowser;
+        if (isImage(browser) && browser.getUserData('nosquint').fit)
+            return;
+        var mdv = browser.markupDocumentViewer;
+        mdv.fullZoom = Math.round(mdv.fullZoom * 100.0 - NSQ.prefs.zoomIncrement) / 100.0;
+        NSQ.browser.saveCurrentZoom();
+        NSQ.browser.updateStatus();
+    };
+
+    /* Called when a menuitem from the status panel context menu is selected. */
+    this.popupItemSelect = function(event) {
+        var item = event.target;
+        var label = item.label;
+        if (label.search(/%$/) != -1) {
+            /* One of the radio menuitems for zoom level was selected (label 
+             * ends in %).  Set the zoom level based on the radio's group
+             * name.
+             */
+            var level = parseInt(label.replace(/%/, ''));
+            var browser = gBrowser.selectedBrowser;
+            if (item.getAttribute('name') == 'text')
+                NSQ.browser.zoom(browser, level, false);
+            else
+                NSQ.browser.zoom(browser, false, level);
+            NSQ.browser.saveCurrentZoom();
+        }
+    };
+
+    /* Handle left/middle/right click on the status panel. */
+    this.statusPanelClick = function(event) {
+        if (event.button == 0)
+            // Left click, open site prefs.
+            return NSQ.cmd.openSiteSettings();
+        else if (event.button == 1)
+            // Middle click, open global prefs.
+            return NSQ.cmd.openGlobalSettings();
+
+        /* Right click.  Setup the context menu according to the current
+         * browser tab: the site name is set, and the appropriate radio 
+         * menuitems get selected.
+         */
+        var popup = $('nosquint-status-popup');
+        var browser = gBrowser.selectedBrowser;
+        var site = browser.getUserData('nosquint').site;
+
+        // Hide all but the last menuitem if there is no site
+        for (let [n, child] in enumerate(popup.childNodes))
+            child.style.display = (site || n == popup.childNodes.length-1) ? '' : 'none';
+
+        var popup_text = $('nosquint-status-popup-text');
+        var popup_full = $('nosquint-status-popup-full');
+
+        var current_text = Math.round(browser.markupDocumentViewer.textZoom * 100);
+        var current_full = Math.round(browser.markupDocumentViewer.fullZoom * 100);
+
+        popup.childNodes[0].label = site;
+
+        for (let child in iter(popup_text.childNodes))
+            child.setAttribute('checked', child.label.replace(/%/, '') == current_text);
+        for (let child in iter(popup_full.childNodes))
+            child.setAttribute('checked', child.label.replace(/%/, '') == current_full);
+
+        popup.openPopupAtScreen(event.screenX, event.screenY, true);
+    };
+
+
+    /* Opens the site prefs dialog, or focuses it if it's already open.
+     * In either case, the values of the dialog are updated to reflect the
+     * current browser tab.
+     */
+    this.openSiteSettings = function() {
+        var browser = gBrowser.selectedBrowser;
+        if (!browser.getUserData('nosquint').site)
+            // Chrome
+            return;
+        var dlg = NSQ.storage.dialogs.site;
+        if (dlg)
+            return dlg.setBrowser(NSQ.browser, browser);
+        window.openDialog('chrome://nosquint/content/dlg-site.xul', null, 'chrome', NSQ.browser, browser);
+    };
+
+
+    /* Opens global prefs dialog or focuses it if it's already open. */
+    this.openGlobalSettings = function(browser) {
+        var dlg = NSQ.storage.dialogs.global;
+        if (dlg)
+            return dlg.focus();
+
+        browser = browser || gBrowser.selectedBrowser;
+        var host = browser.currentURI.asciiHost;
+        try {
+            if (browser.currentURI.port > 0)
+                host += ':' + browser.currentURI.port;
+        } catch (err) {};
+        var url = host + browser.currentURI.path;
+        window.openDialog('chrome://nosquint/content/dlg-global.xul', null, 'chrome', url);
+    };
+
+}});
diff --git a/src/content/dlg-global.js b/src/content/dlg-global.js
new file mode 100644
index 0000000..c40b2e2
--- /dev/null
+++ b/src/content/dlg-global.js
@@ -0,0 +1,241 @@
+NoSquint.dialogs.global = NoSquint.ns(function() { with (NoSquint) {
+    this.strings = getStringBundle('dlg-global');
+    var branchPI = NSQ.prefs.svc.getBranch('privacy.' + (is30() ? 'item.' : 'cpd.'));
+
+    this.init = function() {
+        NSQ.storage.dialogs.global = this;
+        this.dlg = $('nosquint-dialog-global');
+        this.url = window.arguments ? window.arguments[0] : null;
+
+        // General tab
+        $('rememberSites').selectedIndex = Number(!NSQ.prefs.rememberSites);
+        $('siteForget').checked = (NSQ.prefs.forgetMonths != 0);
+        $('siteForget-menu').value = NSQ.prefs.forgetMonths;
+        $('siteForget').addEventListener('CheckboxStateChange', 
+                                         function() NSQ.dialogs.global.forgetMonthsChecked(), false);
+        $('siteSanitize').checked = branchPI.getBoolPref('extensions-nosquint');
+
+        // Zooming tab
+        $('fullZoomLevel').value = NSQ.prefs.fullZoomLevel;
+        $('textZoomLevel').value = NSQ.prefs.textZoomLevel;
+        $('zoomIncrement').value = NSQ.prefs.zoomIncrement;
+        // XXX: image zoom feature disabled for now.
+        //$('zoomImages').checked  = NSQ.prefs.zoomImages;
+        $('showStatus').checked  = !NSQ.prefs.hideStatus;
+        $('wheelZoomEnabled').checked  = NSQ.prefs.wheelZoomEnabled;
+        $('primaryZoomMethod-menu').value = NSQ.prefs.fullZoomPrimary ? 'full' : 'text';
+        this.rememberSelect();
+
+        // Color tab
+        for (let [id, defcolor] in items(NSQ.prefs.defaultColors)) {
+            var color = NSQ.prefs[id];
+            $(id).parentNode.childNodes[1].color = (color == '0' ? defcolor : color);
+            $(id).addEventListener('CheckboxStateChange', this.colorChecked, false);
+            $(id).checked = (color == '0' ? false : true);
+            this.colorChecked.apply($(id));
+        }
+        $('colorBackgroundImages').checked = NSQ.prefs.colorBackgroundImages;
+        $('linksUnderline').checked = NSQ.prefs.linksUnderline;
+
+        // Exceptions tab
+        $('copyURL-button').style.display = this.url ? '' : 'none';
+        for (let exc in iter(NSQ.prefs.exceptions))
+            this.exceptionsListAdd(exc[0].replace(/%20/g, ' '), false);
+        $('exceptionsList').setUserData('nosquint.changed', false, null);
+    };
+
+    this.focus = function() {
+        window.focus();
+    };
+
+    this.cancel = function() {
+        this.finalize();
+    };
+
+    this.finalize = function() {
+        NSQ.storage.dialogs.global = null;
+    };
+
+    this.help = function() {
+        var tab = $('tabs').selectedPanel.id.replace(/tab$/, '');
+        window.openDialog('chrome://nosquint/content/dlg-help.xul', null, 'chrome', tab);
+    };
+
+    this.close = function() {
+        if ($('pattern').value != '')
+            /* User entered stuff in exception input but OK'd dialog without
+             * adding the exception.  We assume here the user actually _wanted_
+             * the exception to be added, so add it automatically.  This is
+             * a bit of do-what-I-mean behaviour.
+             */
+            this.buttonAddException();
+
+        // General tab
+        NSQ.prefs.rememberSites = !Boolean($('rememberSites').selectedIndex);
+        NSQ.prefs.forgetMonths = $('siteForget').checked ? $('siteForget-menu').value : 0;
+        branchPI.setBoolPref('extensions-nosquint', $('siteSanitize').checked);
+
+        // Zooming tab
+        NSQ.prefs.fullZoomLevel = parseInt($('fullZoomLevel').value);
+        NSQ.prefs.textZoomLevel = parseInt($('textZoomLevel').value);
+        NSQ.prefs.zoomIncrement = parseInt($('zoomIncrement').value);
+        // XXX: image zoom feature disabled for now.
+        //NSQ.prefs.zoomImages = $('zoomImages').checked;
+        NSQ.prefs.hideStatus = !$('showStatus').checked;
+        NSQ.prefs.wheelZoomEnabled = $('wheelZoomEnabled').checked;
+        NSQ.prefs.fullZoomPrimary = $('primaryZoomMethod-menu').value == 'full';
+
+        // Color tab
+        for (let [id, defcolor] in items(NSQ.prefs.defaultColors))
+            NSQ.prefs[id] = $(id).checked ? $(id).parentNode.childNodes[1].color : '0';
+        NSQ.prefs.colorBackgroundImages = $('colorBackgroundImages').checked;
+        NSQ.prefs.linksUnderline = $('linksUnderline').checked;
+
+        // Exceptions tab
+        var listbox = $('exceptionsList');
+        var exceptions = null;
+        if (listbox.getUserData('nosquint.changed')) {
+            exceptions = [];
+            for (let i = 0; i < listbox.getRowCount(); i++) {
+                var item = listbox.getItemAtIndex(i);
+                var pattern = item.childNodes[0].getAttribute('label');
+                exceptions.push(pattern.replace(/ /g, '%20'));
+            }
+        }
+        NSQ.prefs.saveAll(exceptions);
+        this.finalize();
+    };
+
+
+    /*********************************************
+     * General tab functions
+     */
+    this.forgetMonthsChecked = function() {
+        // Months optionlist is disabled if "Forget settings" checkbox isn't checked.
+        $('siteForget-menu').disabled = !$('siteForget').checked;
+    };
+
+
+    /*********************************************
+     * Zooming tab functions
+     */
+    // Called when the "Remember zoom and color settings per site" radio button
+    // is clicked.
+    this.rememberSelect = function() {
+        if (this.dlg === undefined)
+            // Happens on initial dialog open before init()
+            return;
+        // Enable nested options under "Remember zoom" radiobutton if the radio is active.
+        var disabled = $('rememberSites').selectedIndex == 1;
+        this.enableTree($('siteForget-box'), disabled);
+    };
+
+    // Enables or disables all elements in the given hierarchy
+    this.enableTree = function(node, state) {
+        for (let child in iter(node.childNodes)) {
+            if (state && child.disabled == false || child.disabled == true)
+                child.disabled = state;
+            if (child.childNodes.length)
+                this.enableTree(child, state);
+        }
+    };
+
+
+
+    /*********************************************
+     * Color tab functions
+     */
+
+    this.colorChecked = function(event) {
+        // Color picker button is enabled if the checkbox beside is is on.
+        var picker = this.parentNode.childNodes[1];
+        picker.disabled = !this.checked;
+        picker.style.opacity = this.checked ? 1.0 : 0.2;
+    };
+
+
+    /*********************************************
+     * Exceptions tab functions
+     */
+
+    this.exceptionsListAdd = function(pattern, check_dupe) {
+        var listbox = $('exceptionsList');
+        // Strip URI scheme from pattern (if it exists)
+        pattern = pattern.replace(/^\w+:\/\//, '');
+
+        if (check_dupe) {
+            for (let node in iter(listbox.childNodes)) {
+                if (node.childNodes[0].getAttribute('label') == pattern)
+                    return;
+            }
+        }
+
+        // Append new exceptions pattern to the list.
+        var node = document.createElement("listitem");
+        var li1 = document.createElement("listcell");
+        li1.setAttribute('label', pattern);
+        node.appendChild(li1);
+        listbox.appendChild(node);
+        node.addEventListener('dblclick', function() NSQ.dialogs.global.buttonEditException(), false);
+        // Mark the listbox as having been changed from stored prefs.
+        listbox.setUserData('nosquint.changed', true, null);
+    };
+
+    this.textPatternKeyPress = function(event) {
+        if (event.keyCode == 13) {
+            // Pressed enter in the pattern input box.
+            this.buttonAddException();
+            return false;
+        }
+    };
+
+    this.textPatternChange = function() {
+        // Enable 'Add' button if the pattern input box isn't empty.
+        $('exceptionAdd-button').disabled = ($('pattern').value == '');
+    };
+
+    this.excListKeyPress = function(event) {
+        if (event.keyCode == 13) {
+            // Pressed enter on one of the listitems.
+            this.buttonEditException();
+            return false;
+        }
+    };
+
+    this.excListSelect = function() {
+        // Edit/Remove buttons enabled when one of the listitems is selected.
+        $('exceptionRemove-button').disabled = ($('exceptionsList').selectedItems.length == 0);
+        $('exceptionEdit-button').disabled = ($('exceptionsList').selectedItems.length != 1);
+    };
+
+    this.buttonCopyFromURL = function() {
+        // Copy button is hidden unless this.url is set.
+        $('pattern').value = this.url;
+        this.textPatternChange();
+    };
+
+    this.buttonAddException = function() {
+        this.exceptionsListAdd($('pattern').value, true);
+        $('pattern').value = '';
+        this.textPatternChange();
+    };
+
+    this.buttonEditException = function() {
+        var listcell = $('exceptionsList').selectedItem.childNodes[0];
+        var oldPattern = listcell.getAttribute('label');
+        var newPattern = popup('prompt', this.strings.editTitle, this.strings.editPrompt, oldPattern);
+        if (newPattern != null && newPattern != oldPattern) {
+            listcell.setAttribute('label', newPattern);
+            $('exceptionsList').setUserData('nosquint.changed', true, null);
+        }
+    };
+
+    this.buttonRemoveException = function() {
+        // Listbox is multi-select capable; remove all selected items.
+        var listbox = $('exceptionsList');
+        while (listbox.selectedItems.length)
+            listbox.removeChild(listbox.selectedItems[0]);
+        listbox.setUserData('nosquint.changed', true, null);
+    };
+
+}});
diff --git a/src/content/dlg-global.xul b/src/content/dlg-global.xul
new file mode 100644
index 0000000..86f0e78
--- /dev/null
+++ b/src/content/dlg-global.xul
@@ -0,0 +1,218 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"  type="text/css"?>
+<?xml-stylesheet href="chrome://nosquint/content/dlg-style.css"  type="text/css"?>
+<!DOCTYPE window SYSTEM "chrome://nosquint/locale/dlg-global.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
+        title="&ns.pref.title;"
+        buttons="help,accept,cancel" 
+        ondialogaccept="NoSquint.dialogs.global.close()"
+        ondialogcancel="NoSquint.dialogs.global.cancel()"
+        ondialoghelp="NoSquint.dialogs.global.help()"
+        id="nosquint-dialog-global"
+        persist="screenX screenY">
+
+    <script type="application/x-javascript" src="chrome://nosquint/content/init.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/lib.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/prefs.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/dlg-global.js" />
+
+    <tabbox id='tabs' flex="1">
+        <tabs>
+            <tab label="&ns.pref.tab.general.label;" />
+            <tab label="&ns.pref.tab.zooming.label;" />
+            <tab label="&ns.pref.tab.colors.label;" />
+            <tab label="&ns.pref.tab.exceptions.label;" />
+        </tabs>
+
+        <tabpanels flex="1">
+            <!-- General Tab -->
+            <tabpanel id="generaltab" flex="1">
+                <vbox flex="1">
+                    <groupbox id="persistence">
+                        <caption label="&ns.pref.persistence.caption;" />
+                        <radiogroup id="rememberSites" onselect="NoSquint.dialogs.global.rememberSelect()">
+                            <radio label="&ns.pref.persistence.remember.label;" />
+                            <vbox class="indent" id='siteForget-box'>
+                                <hbox align="center">
+                                    <checkbox id="siteForget" label="&ns.pref.persistence.forget.label;" 
+                                              checked="false" />
+                                    <menulist id="siteForget-menu">
+                                        <menupopup>
+                                            <menuitem value="12" label="&ns.pref.persistence.forget.year;"/>
+                                            <menuitem value="6" label="&ns.pref.persistence.forget.6months;" 
+                                                      selected="true" />
+                                            <menuitem value="3" label="&ns.pref.persistence.forget.3months;" />
+                                            <menuitem value="1" label="&ns.pref.persistence.forget.month;"/>
+                                        </menupopup>
+                                    </menulist>
+                                </hbox>
+                                <checkbox id="siteSanitize" label="&ns.pref.persistence.sanitize.label;" 
+                                          checked="false" />
+                            </vbox>
+                            <radio label="&ns.pref.persistence.noRemember.label;" />
+                        </radiogroup>
+                    </groupbox>
+                </vbox>
+            </tabpanel>
+
+            <!-- Zooming Tab -->
+            <tabpanel id="zoomingtab" flex="1">
+                <vbox flex="1">
+                    <groupbox>
+                        <caption label="&ns.pref.zooming.caption;" />
+                        <grid>
+                            <columns>
+                                <column />
+                                <column />
+                            </columns>
+                            <rows>
+                                <row align="center">
+                                    <hbox>
+                                        <spacer flex="1" />
+                                        <label>&ns.pref.zooming.primaryMethod.label;:</label>
+                                    </hbox>
+                                    <menulist id="primaryZoomMethod-menu">
+                                        <menupopup>
+                                            <menuitem value="full" label="&ns.pref.zooming.primaryMethod.full;" 
+                                                      selected="true"/>
+                                            <menuitem value="text" label="&ns.pref.zooming.primaryMethod.text;"/>
+                                        </menupopup>
+                                    </menulist>
+                                </row>
+
+                                <row align="center">
+                                    <hbox>
+                                        <spacer flex="1" />
+                                        <label>&ns.pref.zooming.fullLevel.label;:</label>
+                                    </hbox>
+                                    <hbox align="center">
+                                        <textbox id="fullZoomLevel" size="2" type="number" min="40" 
+                                                 max="300" increment="5" />
+                                        <label class="percent">%</label>
+                                    </hbox>
+                                </row>
+
+                                <row align="center">
+                                    <hbox>
+                                        <spacer flex="1" />
+                                        <label>&ns.pref.zooming.textLevel.label;:</label>
+                                    </hbox>
+                                    <hbox align="center">
+                                        <textbox id="textZoomLevel" size="2" type="number" min="40" 
+                                                 max="300" increment="5" />
+                                        <label class="percent">%</label>
+                                    </hbox>
+                                </row>
+
+                                <row align="center">
+                                    <hbox>
+                                        <spacer flex="1" />
+                                        <label>&ns.pref.zooming.increment.label;:</label>
+                                    </hbox>
+                                    <hbox align="center">
+                                        <textbox id="zoomIncrement" size="2" type="number" min="1" 
+                                                 max="100" increment="1" />
+                                        <label class="percent">%</label>
+                                    </hbox>
+                                </row>
+                            </rows>
+                        </grid>
+                        <vbox>
+                            <!-- XXX: image zoom feature disabled for now.
+                            <checkbox id="zoomImages" label="&ns.pref.zooming.images.label;" checked="false" />
+                            -->
+                            <checkbox id="wheelZoomEnabled" label="&ns.pref.zooming.mousewheel.label;" 
+                                      checked="false" />
+                            <checkbox id="showStatus" label="&ns.pref.zooming.showstatus.label;" checked="false" />
+                        </vbox>
+                    </groupbox>
+                </vbox>
+            </tabpanel>
+
+            <!-- Colors Tab -->
+            <tabpanel id="colorstab" flex="1">
+                <vbox flex="1">
+                    <html:p style="margin: 0 7px 0.5em 7px; padding: 0;">&ns.pref.colors.info;</html:p>
+                    <hbox>
+                        <groupbox flex='1'>
+                            <caption label="&ns.pref.colors.colors.caption;" />
+                            <vbox>
+                                <hbox>
+                                    <checkbox id="colorText" label="&ns.pref.colors.colors.text.label;"
+                                              checked="false" flex="1" />
+                                    <colorpicker type='button' />
+                                </hbox>
+                                <hbox>
+                                    <checkbox id="colorBackground" label="&ns.pref.colors.colors.background.label;"
+                                              checked="false" flex="1" />
+                                    <colorpicker type='button' />
+                                </hbox>
+                                <checkbox id="colorBackgroundImages" label="&ns.pref.colors.colors.images.label;"
+                                          checked="false" flex="1" />
+                            </vbox>
+                        </groupbox>
+                        <groupbox flex='1'>
+                            <caption label="&ns.pref.colors.links.caption;" />
+                            <vbox>
+                                <hbox>
+                                    <checkbox id="linksUnvisited" label="&ns.pref.colors.links.unvisited.label;" 
+                                              checked="false" flex="1" />
+                                    <colorpicker type='button' />
+                                </hbox>
+                                <hbox>
+                                    <checkbox id="linksVisited" label="&ns.pref.colors.links.visited.label;"
+                                              checked="false" flex="1" />
+                                    <colorpicker type='button' />
+                                </hbox>
+                                <checkbox id="linksUnderline" label="&ns.pref.colors.links.underline.label;"
+                                          checked="false" flex="1" />
+                            </vbox>
+                        </groupbox>
+                    </hbox>
+                </vbox>
+            </tabpanel>
+
+            <!-- Exceptions Tab -->
+            <tabpanel id="exceptionstab" flex="1">
+                <vbox flex="1">
+                    <html:p style="margin: 0 7px 0.5em 7px; padding: 0;">&ns.pref.exceptions.info;</html:p>
+
+                    <label value="&ns.pref.exceptions.pattern.label;:" />
+                    <textbox id="pattern" width="100%" oninput="NoSquint.dialogs.global.textPatternChange()" 
+                             onkeypress="return NoSquint.dialogs.global.textPatternKeyPress(event)" />
+                    <hbox>
+                        <button label="&ns.pref.exceptions.copyButton.label;" id="copyURL-button"
+                                accesskey="&ns.pref.exceptions.copyButton.accesskey;"
+                                oncommand="NoSquint.dialogs.global.buttonCopyFromURL()" />
+                        <spacer flex="1" />
+                        <button label="&ns.pref.exceptions.addButton.label;" icon="add" id="exceptionAdd-button" 
+                                accesskey="&ns.pref.exceptions.addButton.accesskey;"
+                                disabled="true" oncommand="NoSquint.dialogs.global.buttonAddException()" />
+                    </hbox>
+
+                    <separator />
+
+                <listbox id="exceptionsList" flex="1" seltype="multiple" rows="5" 
+                         onkeypress="return NoSquint.dialogs.global.excListKeyPress(event)" 
+                         onselect="NoSquint.dialogs.global.excListSelect()">
+                    <listhead>
+                        <listheader label="&ns.pref.exceptions.list.col1.label;" />
+                    </listhead>
+                </listbox>
+                <hbox>
+                    <button label="&ns.pref.exceptions.editButton.label;" id="exceptionEdit-button" 
+                            accesskey="&ns.pref.exceptions.editButton.accesskey;"
+                            oncommand="NoSquint.dialogs.global.buttonEditException()" />
+                    <button label="&ns.pref.exceptions.removeButton.label;" 
+                            icon="remove" id="exceptionRemove-button"
+                            accesskey="&ns.pref.exceptions.removeButton.accesskey;"
+                            oncommand="NoSquint.dialogs.global.buttonRemoveException()" />
+                </hbox>
+                </vbox>
+            </tabpanel>
+
+        </tabpanels>
+    </tabbox>
+</dialog>
diff --git a/src/content/dlg-help.js b/src/content/dlg-help.js
new file mode 100644
index 0000000..121548b
--- /dev/null
+++ b/src/content/dlg-help.js
@@ -0,0 +1,10 @@
+NoSquint.dialogs.help = NoSquint.ns(function() { with (NoSquint) {
+    this.init = function() {
+        var browser = $('nosquint-help-browser');
+        var uri = 'chrome://nosquint/locale/help.html';
+        if (window.arguments)
+            uri += '#' + window.arguments[0];
+        browser.loadURI(uri, null, null);
+    };
+
+}});
diff --git a/src/content/dlg-help.xul b/src/content/dlg-help.xul
new file mode 100644
index 0000000..d8f15e6
--- /dev/null
+++ b/src/content/dlg-help.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"  type="text/css"?>
+<!DOCTYPE window SYSTEM "chrome://nosquint/locale/dlg-help.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
+        title="&ns.help.title;"
+        buttons="accept" 
+        ondialogaccept="return"
+        id="nosquint-dialog-help"
+        persist="width height"
+        width="800"
+        height="700">
+
+    <script type="application/x-javascript" src="chrome://nosquint/content/init.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/lib.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/dlg-help.js" />
+
+    <dialogheader title="&ns.help.subtitle;" description="&ns.help.title;" />
+    <groupbox flex="1">
+        <browser type="content" id="nosquint-help-browser" flex="1" />
+    </groupbox>
+</dialog>
diff --git a/src/content/dlg-site.js b/src/content/dlg-site.js
new file mode 100644
index 0000000..b314cc1
--- /dev/null
+++ b/src/content/dlg-site.js
@@ -0,0 +1,176 @@
+NoSquint.dialogs.site = NoSquint.ns(function() { with (NoSquint) {
+    this.strings = getStringBundle('dlg-site');
+
+    var updateTimer = null;
+
+    this.init = function() {
+        NSQ.storage.dialogs.site = this;
+        this.dlg = $('nosquint-dialog-site');
+
+        this.setBrowser(window.arguments[0], window.arguments[1]);
+
+        $('full-zoom-level').onchange = function() NSQ.dialogs.site.valueChange(this);
+        $('text-zoom-level').onchange = function() NSQ.dialogs.site.valueChange(this);
+
+        var restyle = function() NSQ.dialogs.site.style(true, false);
+        for (let id in NSQ.prefs.defaultColors) {
+            $(id).addEventListener('CheckboxStateChange', this.colorChecked, false);
+            $(id).parentNode.childNodes[1].onchange = restyle;
+        }
+        $('colorBackgroundImages').addEventListener('CheckboxStateChange', restyle, false);
+        $('linksUnderline').addEventListener('CheckboxStateChange', restyle, false);
+    };
+
+    
+    // Immediately dismiss window.  Used when transitioning from Private Browsing mode.
+    this.die = function() {
+        this.finalize();
+        window.close();
+    };
+
+    this.cancel = function() {
+        this.revert();
+        this.finalize();
+    };
+
+    this.close = function() {
+        this.zoom(true, true);
+        this.style(true, true);
+        this.finalize();
+    };
+
+    this.finalize = function() {
+        NSQ.storage.dialogs.site = null;
+    };
+
+
+    this.setBrowser = function(nsqBrowser, mozBrowser) {
+        NSQ.browser = nsqBrowser;
+        var site = mozBrowser.getUserData('nosquint').site;
+        if (this.site) {
+            if (this.browser != mozBrowser || this.site != site)
+                // Settings opened for new site, revert any changes for last site.
+                this.revert();
+            else
+                // Everything is the same.
+                return window.focus();
+        }
+        this.browser = mozBrowser;
+        this.site = site;
+
+        var [text, full] = NSQ.browser.getZoomForBrowser(this.browser);
+        var style = NSQ.prefs.getStyleForSite(this.site);
+
+        this.updateWarning();
+
+        $('caption').label = this.site;
+        $('text-zoom-slider').value = text;
+        $('full-zoom-slider').value = full;
+
+        for (let [id, defcolor] in items(NSQ.prefs.defaultColors)) {
+            $(id).parentNode.childNodes[1].color = (!style || style[id] == '0' ? defcolor : style[id]);
+            $(id).checked = (!style || style[id] == '0' ? false : true);
+            this.colorChecked.apply($(id));
+        }
+        window.focus();
+        window.sizeToContent();
+    };
+
+    this.updateWarning = function() {
+        var content = null;
+        if (NSQ.browser.observer.inPrivateBrowsing)
+            content = this.strings.warningPrivateBrowsing;
+        else if (!NSQ.prefs.rememberSites)
+            content = this.strings.warningForgetSites;
+
+        $('warning-box-content').innerHTML = content;
+        $('warning-box').style.display = content ? '' : 'none';
+        window.sizeToContent();
+    };
+
+    this.revert = function() {
+        this.zoom(false, false);
+        this.style(false, false);
+    };
+
+
+    this.openGlobalSettings = function() {
+        NSQ.cmd.openGlobalSettings(this.browser);
+    };
+
+
+    // Callback when text/full zoom text input is changed.
+    this.valueChange = function(target) {
+        $(target.id.replace('level', 'slider')).value = target.value;
+        this.queueUpdateZoom();
+    };
+
+    // Callback when text/full zoom slider is changed.
+    this.sliderChange = function(target) {
+        // Snap to increments of 5.
+        target.value = parseInt(target.value / 5) * 5;
+        // Sync slider value to text input field.
+        $(target.id.replace('slider', 'level')).value = target.value;
+        this.queueUpdateZoom();
+    };
+
+    this.buttonUseDefault = function(target) {
+        var [text, full] = NSQ.prefs.getZoomDefaults();
+        var input = $(target.id.replace('button', 'level'));
+        input.value = (input.id == 'text-zoom-level' ? text : full);
+        input.onchange();
+    };
+
+    this.queueUpdateZoom = function() {
+        if (updateTimer)
+            return;
+        updateTimer = setTimeout(function() {
+            clearTimeout(updateTimer);
+            updateTimer = null;
+            NSQ.dialogs.site.zoom(true, false);
+        }, 400);
+    };
+
+    this.zoom = function(fromForm, save) {
+        var text = fromForm ? $('text-zoom-level').value : null;
+        var full = fromForm ? $('full-zoom-level').value : null;
+        NSQ.browser.zoom(this.browser, text, full);
+        if (save)
+            NSQ.prefs.updateSiteList(this.site, [text, full]);
+    };
+
+
+    this.colorChecked = function(event) {
+        // Color picker button is enabled if the checkbox beside is is on.
+        var picker = this.parentNode.childNodes[1];
+        picker.disabled = !this.checked;
+        picker.style.opacity = this.checked ? 1.0 : 0.2;
+        if (event)
+            // Only style() if we've been triggered by user checking the checkbox,
+            // not a call from elsewhere in this file.
+            NSQ.dialogs.site.style(true, false);
+    };
+
+    this.style = function(fromForm, save) {
+        var style = null;
+        if (fromForm) {
+            var style = {enabled: false};
+            for (let attr in iter(NSQ.prefs.defaultColors)) {
+                style[attr] = $(attr).checked ? $(attr).parentNode.childNodes[1].color : null;
+                style.enabled = style.enabled || Boolean(style[attr]);
+            }
+            for (let attr in iter(['colorBackgroundImages', 'linksUnderline'])) {
+                style[attr] = $(attr).checked;
+                style.enabled = style.enabled || Boolean(style[attr]);
+            }
+        }
+        if (save)
+            NSQ.prefs.updateSiteList(this.site, null, style);
+        if (style)
+            style = NSQ.prefs.applyStyleGlobals(style);
+
+        NSQ.browser.style(this.browser, style);
+    };
+
+
+}});
diff --git a/src/content/dlg-site.xul b/src/content/dlg-site.xul
new file mode 100644
index 0000000..1f55415
--- /dev/null
+++ b/src/content/dlg-site.xul
@@ -0,0 +1,118 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"  type="text/css"?>
+<?xml-stylesheet href="chrome://nosquint/content/dlg-style.css"  type="text/css"?>
+<!DOCTYPE window [
+    <!ENTITY % siteDTD SYSTEM "chrome://nosquint/locale/dlg-site.dtd" >
+    %siteDTD;
+    <!ENTITY % globalDTD SYSTEM "chrome://nosquint/locale/dlg-global.dtd" >
+    %globalDTD;
+]>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
+        title="&ns.pref.title;"
+        buttons="extra1,accept,cancel" 
+        ondialogaccept="NoSquint.dialogs.site.close()"
+        ondialogcancel="NoSquint.dialogs.site.cancel()"
+        ondialogextra1="NoSquint.dialogs.site.openGlobalSettings()"
+        buttonlabelextra1="&ns.pref.button.global.label;"
+        buttonaccesskeyextra1="&ns.pref.button.global.accesskey;"
+        id="nosquint-dialog-site"
+        persist="screenX screenY">
+
+    <script type="application/x-javascript" src="chrome://nosquint/content/init.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/lib.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/prefs.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/cmd.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/dlg-site.js" />
+
+    <groupbox id="site">
+        <caption id='caption' />
+        <vbox flex='1'>
+            <hbox id="warning-box" align="center">
+                <image class="alert-icon" />
+                <html:div id='warning-box-content'></html:div>
+            </hbox>
+
+            <grid id="siteZoom-box" flex="1">
+                <columns>
+                    <column />
+                    <column />
+                    <column />
+                    <column flex="1" />
+                    <column />
+                </columns>
+                <rows>
+                    <row align="center">
+                        <hbox>
+                            <spacer flex="1" />
+                            <label value="&ns.pref.fullZoom.label;:" />
+                        </hbox>
+                        <scale id="full-zoom-slider" min="40" increment="1" max="300" style="width:200px"
+                               onchange="NoSquint.dialogs.site.sliderChange(this)"/>
+                        <hbox align="center">
+                            <textbox id="full-zoom-level" size="2" type="number" min="40" max="300" increment="5" />
+                            <label class="percent">%</label>
+                        </hbox>
+                        <spacer flex="1" />
+                        <button  label="&ns.pref.button.useDefault.label;"
+                                 id="full-zoom-button" oncommand="NoSquint.dialogs.site.buttonUseDefault(this)" />
+                    </row>
+                    <row align="center">
+                        <hbox>
+                            <spacer flex="1" />
+                            <label value="&ns.pref.textZoom.label;:" />
+                        </hbox>
+                        <scale id="text-zoom-slider" min="40" increment="1" max="300" 
+                               onchange="NoSquint.dialogs.site.sliderChange(this)"/>
+                        <hbox align="center">
+                            <textbox id="text-zoom-level" size="2" type="number" min="40" max="300" increment="5"  />
+                            <label class="percent">%</label>
+                        </hbox>
+                        <spacer flex="1" />
+                        <button  label="&ns.pref.button.useDefault.label;"
+                                 id="text-zoom-button" oncommand="NoSquint.dialogs.site.buttonUseDefault(this)" />
+                    </row>
+                </rows>
+            </grid>
+
+            <hbox flex='1' style='padding-top: 1em'>
+                <groupbox flex='1'>
+                    <caption label="&ns.pref.colors.colors.caption;" />
+                    <vbox>
+                        <hbox>
+                            <checkbox id="colorText" label="&ns.pref.colors.colors.text.label;"
+                                      checked="false" flex="1" />
+                            <colorpicker type='button' />
+                        </hbox>
+                        <hbox>
+                            <checkbox id="colorBackground" label="&ns.pref.colors.colors.background.label;"
+                                      checked="false" flex="1" />
+                            <colorpicker type='button' />
+                        </hbox>
+                        <checkbox id="colorBackgroundImages" label="&ns.pref.colors.colors.images.label;"
+                                  checked="false" flex="1" />
+                    </vbox>
+                </groupbox>
+                <groupbox flex='1'>
+                    <caption label="&ns.pref.colors.links.caption;" />
+                    <vbox>
+                        <hbox>
+                            <checkbox id="linksUnvisited" label="&ns.pref.colors.links.unvisited.label;" 
+                                      checked="false" flex="1" />
+                            <colorpicker type='button' />
+                        </hbox>
+                        <hbox>
+                            <checkbox id="linksVisited" label="&ns.pref.colors.links.visited.label;"
+                                      checked="false" flex="1" />
+                            <colorpicker type='button' />
+                        </hbox>
+                        <checkbox id="linksUnderline" label="&ns.pref.colors.links.underline.label;"
+                                  checked="false" flex="1" />
+                    </vbox>
+                </groupbox>
+            </hbox>
+
+        </vbox>
+    </groupbox>
+</dialog>
diff --git a/src/content/dlg-style.css b/src/content/dlg-style.css
new file mode 100644
index 0000000..428bb52
--- /dev/null
+++ b/src/content/dlg-style.css
@@ -0,0 +1,35 @@
+p {
+    max-width: 40em; 
+}
+
+label.percent {
+    margin-left: -0.18em;
+}
+
+.indent {
+    margin-left: 2.5em;
+}
+
+button[dlgtype="extra1"] {
+      list-style-image: url("chrome://nosquint/skin/icon-statusbar-16.png");
+}
+
+#warning-box {
+    border: 1px solid #e0cd64;
+    background-color: #fffac4;
+    padding: 3px 15px 3px 15px;
+    margin: 0.5em 2em;
+    vertical-align: middle;
+    font-weight: bold;
+    -moz-border-radius: 20px;
+}
+
+#warning-box div {
+    padding-left: 0.7em;
+    width: 400px;
+    font-weight: normal;
+}
+
+#warning-box image {
+}
+
diff --git a/src/content/init.js b/src/content/init.js
index 52099ce..c571967 100644
--- a/src/content/init.js
+++ b/src/content/init.js
@@ -1,46 +1,44 @@
-window.addEventListener("load", NoSquint.init, false); 
-window.addEventListener("unload", NoSquint.destroy, false); 
-
-// Hook ZoomManager in order to override Firefox's internal per-site
-// zoom memory feature.
+// Global object for NoSquint.  'NoSquint' is the only name added to the global
+// namespace by this addon.
+NoSquint = {
+    id: 'NoSquint',
+    namespaces: [],
+    _initialized: false,
+    dialogs: {},            // dialogs namespace
 
-ZoomManager._nosquintPendingZoom = null;
-ZoomManager._nosquintOrigZoomGetter = ZoomManager.__lookupGetter__('zoom');
-ZoomManager._nosquintOrigZoomSetter = ZoomManager.__lookupSetter__('zoom');
+    ns: function(fn) {
+        var scope = {
+            extend: function(o) {
+                for (var key in o)
+                    this[key] = o[key];
+                }
+        };
+        scope = fn.apply(scope) || scope;
+        NoSquint.namespaces.push(scope);
+        return scope;
+    },
 
-ZoomManager.__defineSetter__('zoom', function(value) {
-    /* XXX: Horrid hack, makes baby Jesus cry.
-     *
-     * Problem: on location change and tab change, some internal FF mechanism
-     * sets zoom to some stored value (on a per site basis).  NoSquint
-     * must fully override this mechanism, as we implement our own approach.
-     *
-     * Solution: rather than update zoom on the current browser immediately,
-     * we queue it with a timer, and give the location/tab change handlers
-     * in nosquint.js a chance to abort the queued zoom via
-     * NoSquint.abortPendingZoomManager()
-     */
-    ZoomManager._nosquintPendingZoom = value;
-    if (NoSquint.zoomManagerTimeout == false) {
-        dump("[nosquint] EATING ZOOM REQUEST: "+ value + "\n");
-        NoSquint.zoomManagerTimeout = null;
-        return;
-    }
-    NoSquint.zoomManagerTimeout = setTimeout(function() { 
-        dump("[nosquint] setting zoom through ZoomManager: " + value + "\n");
-        ZoomManager._nosquintOrigZoomSetter(value);
-        NoSquint.zoomManagerTimeout = null;
-        ZoomManager._nosquintPendingZoom = null;
-    }, 0);
-});
+    init: function() {
+        if (NoSquint._initialized)
+            return;
+        NoSquint._initialized = true;
 
+        for (let i = 0; i < NoSquint.namespaces.length; i++) {
+            var scope = NoSquint.namespaces[i];
+            if (scope.init !== undefined)
+                scope.init();
+        }
+    },
 
-ZoomManager.__defineGetter__('zoom', function() {
-    if (ZoomManager._nosquintPendingZoom != null)
-        return ZoomManager._nosquintPendingZoom;
-    return ZoomManager._nosquintOrigZoomGetter();
-});
+    destroy: function() {
+        // Invoke destroy functions in all registered namespaces
+        for (let i = 0; i < NoSquint.namespaces.length; i++) {
+            var scope = NoSquint.namespaces[i];
+            if (scope.destroy !== undefined)
+                scope.destroy();
+        }
+    }
+};
 
-ZoomManager.enlarge = NoSquint.cmdEnlargePrimary;
-ZoomManager.reduce = NoSquint.cmdReducePrimary;
-ZoomManager.reset = NoSquint.cmdReset;
+window.addEventListener("load", NoSquint.init, false); 
+window.addEventListener("unload", NoSquint.destroy, false);
diff --git a/src/content/interfaces.js b/src/content/interfaces.js
new file mode 100644
index 0000000..c999383
--- /dev/null
+++ b/src/content/interfaces.js
@@ -0,0 +1,211 @@
+NoSquint.interfaces = NoSquint.ns(function() { with (NoSquint) {
+    const CI = Components.interfaces;
+
+    this.id = 'NoSquint.interfaces';
+
+    /* Specifies at which state we will try to zoom and style the page.  With
+     * 3.5+, we can style early with STATE_TRANSFERRING.  With 3.0, we seem to
+     * have style later at STATE_STOP in order to get reliable results. (In 3.0
+     * using STATE_TRANSFERRING, on e.g. youtube.com the search bar is
+     * improperly rendered.  [And this, quite perplexingly, is caused by
+     * accessing doc.documentElement in NSQ.browser.style()])
+     */
+    var stateFlag = is30() ? Components.interfaces.nsIWebProgressListener.STATE_STOP
+                           : Components.interfaces.nsIWebProgressListener.STATE_TRANSFERRING;
+
+    /* Listener used to receive notifications when a new URI is about to be loaded.
+     * TODO: when support for Firefox 3.0 is dropped, use:
+     *          https://developer.mozilla.org/En/Listening_to_events_on_all_tabs
+     */
+    this.ProgressListener = function(browser) {
+        this.id = 'NoSquint.interfaces.ProgressListener';
+        this.browser = browser;
+    }
+
+    this.ProgressListener.prototype = {
+        QueryInterface: function(aIID) {
+            if (aIID.equals(CI.nsIWebProgressListener) ||
+                aIID.equals(CI.nsISupportsWeakReference) ||
+                aIID.equals(CI.nsISupports))
+                return this;
+            throw Components.results.NS_NOINTERFACE;
+        },
+
+        onLocationChange: function(progress, request, uri) {
+            // Ignore url#foo -> url#bar location changes
+            if (!request)
+                return;
+
+            // If we're here, a new document will be loaded next.
+            this.contentType = this.browser.docShell.document.contentType;
+            this.styleApplied = false;
+            this.zoomApplied = false;
+
+            // Remove any stylers from the last document.
+            var userData = this.browser.getUserData('nosquint');
+            userData.stylers = [];
+
+            var site = NSQ.browser.getSiteFromBrowser(this.browser);
+            if (site == userData.site)
+                // New document on the same site.
+                return;
+
+            debug("onLocationChange(): old=" + userData.site + "new=" + site + ", uri=" + uri.spec);
+            /* Update timestamp for site.  This isn't _quite_ perfect because
+             * the timestamp is only updated for the first page load on that site
+             * rather than the last.  But it should be good enough in practice, and
+             * avoids updating the site list on _every_ page load.
+             */
+            NSQ.prefs.updateSiteTimestamp(site);
+            userData.site = site;
+
+            /* Now zoom the current browser for the proper zoom level for this site.
+             * It's expected that this zoom level will not get modified from under us.
+             * However, this has happened with a Firefox 3.6 nightly -- see bug
+             * #516513.  That bug got fixed, so it seems to be safe to zoom here.
+             * If the problem resurfaces, we will need to move the zooming into
+             * onStateChange the way styling is currently hooked.
+             * XXX: 3.6 private browsing mode exhibits some problems, so zooming
+             * is back in onStateChange.
+             */
+            NSQ.browser.zoom(this.browser);
+
+            // If the site settings dialog was open from this browser, sync it.
+            var dlg = NSQ.storage.dialogs.site;
+            if (dlg && dlg.browser == this.browser)
+                dlg.setBrowser(NSQ.browser, this.browser);
+        },
+
+        onStateChange: function(progress, request, state, astatus) {
+            //debug("LISTENER: request=" + request + ", state=" + state + ", status=" + 
+            //      astatus + ", type=" + this.browser.docShell.document.contentType);
+
+            /* Check the current content type against the content type we initially got.
+             * This changes in the case when there's an error page (e.g. dns failure),
+             * which we treat as chrome and do not adjust.
+             */
+            var contentType = this.browser.docShell.document.contentType;
+            if (this.contentType != contentType) {
+                this.contentType = contentType;
+                if (isChrome(this.browser)) {
+                    this.browser.getUserData('nosquint').site = null;
+                    NSQ.browser.zoom(this.browser, 100, 100);
+                }
+            } else if (state & stateFlag) {
+                if (!this.zoomApplied) {
+                    this.zoomApplied = true;
+                    NSQ.browser.zoom(this.browser);
+                }
+                if (!this.styleApplied) {
+                    if (!isChrome(this.browser) || isImage(this.browser))
+                        this.styleApplied = NSQ.browser.style(this.browser);
+                    else
+                        this.styleApplied = true;
+                }
+            }
+        },
+
+        onProgressChange: function() 0,
+        onStatusChange: function() 0,
+        onSecurityChange: function() 0,
+        onLinkIconAvailable: function() 0,
+    };
+
+
+
+    /* Custom observer attached to nsIObserverService.  Used to detect changes
+     * to private browsing state, and addon disable/uninstall.  Some code
+     * borrowed from https://developer.mozilla.org/En/Supporting_private_browsing_mode
+     */
+    this.Observer = function() {  
+        this.id = 'NoSquint.interfaces.Observer';
+        this.init();
+    };
+
+    this.Observer.prototype = {  
+        _os: null,  
+        _inPrivateBrowsing: false, // whether we are in private browsing mode  
+        watcher: {}, // the watcher object  
+        _hooked: false,
+       
+        init: function () {  
+            this._inited = true;  
+            this._os = Components.classes["@mozilla.org/observer-service;1"]  
+                                 .getService(Components.interfaces.nsIObserverService);  
+            this._hook();
+        },
+
+        _hook: function() {
+            this._os.addObserver(this, "private-browsing", false);  
+            this._os.addObserver(this, "quit-application-granted", false);  
+            this._os.addObserver(this, "em-action-requested", false);  
+            try {  
+                var pbs = Components.classes["@mozilla.org/privatebrowsing;1"]  
+                                  .getService(Components.interfaces.nsIPrivateBrowsingService);  
+                this._inPrivateBrowsing = pbs.privateBrowsingEnabled;  
+            } catch(ex) {  
+                // ignore exceptions in older versions of Firefox  
+            }
+            this._hooked = true;
+        },
+
+        _unhook: function() {
+            this._os.removeObserver(this, "quit-application-granted");  
+            this._os.removeObserver(this, "private-browsing");  
+            this._hooked = false;
+        },
+
+        observe: function (subject, topic, data) {  
+            switch (topic) {
+                case "private-browsing":
+                    switch (data) {
+                        case "enter":
+                            this._inPrivateBrowsing = true;  
+                            if ("onEnterPrivateBrowsing" in this.watcher)
+                                this.watcher.onEnterPrivateBrowsing();  
+                            break;
+
+                        case "exit":
+                            this._inPrivateBrowsing = false;  
+                            if ("onExitPrivateBrowsing" in this.watcher)
+                                this.watcher.onExitPrivateBrowsing();  
+                            break;
+                    }
+                    break;
+
+                case "quit-application-granted":
+                    NSQ.storage.quitting = true;
+                    this._unhook();
+                    break;
+
+                case "em-action-requested":
+                    switch (data) {
+                        case "item-disabled":
+                        case "item-uninstalled":
+                            var item = subject.QueryInterface(Components.interfaces.nsIUpdateItem);
+                            if (item.id != 'nosquint at urandom.ca' || NSQ.storage.disabled)
+                                break;
+
+                            NSQ.storage.disabled = true;
+                            if (popup('confirm', NSQ.strings.disableTitle, NSQ.strings.disablePrompt) == 1) {
+                                // Clicked no
+                            } else
+                                NSQ.prefs.setSiteSpecific(true);
+                            break;
+                        
+                        case "item-cancel-action":
+                            var item = subject.QueryInterface(Components.interfaces.nsIUpdateItem);
+                            if (item.id != 'nosquint at urandom.ca' || NSQ.storage.disabled != true)
+                                break;
+                            NSQ.prefs.setSiteSpecific(false);
+                            NSQ.storage.disabled = false;
+                    }
+                    break;
+            }
+        },  
+       
+        get inPrivateBrowsing() {  
+            return this._inPrivateBrowsing;  
+        }
+    }; 
+}});
diff --git a/src/content/lib.js b/src/content/lib.js
new file mode 100644
index 0000000..8669c6b
--- /dev/null
+++ b/src/content/lib.js
@@ -0,0 +1,220 @@
+(function() {
+    // Shorter alias
+    this.NSQ = NoSquint;
+
+    /* Setup global (spans all windows) storage object.  The storage object
+     * exists once, and is referenced for each window.  (In contrast, doing
+     * Application.storage.set('foo', [1,2]) will store a copy of the list.)
+     */
+    var extstorage = Application.extensions.get('nosquint at urandom.ca').storage;
+    this.storage = extstorage.get('global', null);
+    if (this.storage === null) {
+        // Initialize global defaults.
+        this.storage = {
+            disabled: false,
+            quitting: false,
+            origSiteSpecific: null,
+            dialogs: {}
+        };
+        extstorage.set('global', this.storage);
+    }
+
+
+    this.is30 = function() {
+        return Application.version.substr(0, 4) == '3.0.';
+    };
+
+    this.is36 = function() {
+        return Application.version.substr(0, 4) >=  '3.6.';
+    };
+
+    this.$ = function(id, doc) {
+        if (doc === undefined)
+            doc = document;
+        return doc.getElementById(id);
+    };
+
+    // Loads a string bundle and returns a key -> value map.
+    this.getStringBundle = function(name) {
+        var bundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
+                         .getService(Components.interfaces.nsIStringBundleService)
+                         .createBundle('chrome://nosquint/locale/' + name + '.properties');
+        var strings = {}
+        var enum = bundle.getSimpleEnumeration();
+        while (enum.hasMoreElements()) {
+            var str = enum.getNext().QueryInterface(Components.interfaces.nsIPropertyElement);
+            strings[str.key] = str.value;
+        }
+        return strings;
+    }
+
+    this.strings = this.getStringBundle('overlay');
+
+    /* Returns a list of lines from a URL (such as chrome://).
+     */
+    this.readLines = function(aURL) {
+      var ioService = Components.classes["@mozilla.org/network/io-service;1"]
+                      .getService(Components.interfaces.nsIIOService);
+      var scriptableStream = Components.classes["@mozilla.org/scriptableinputstream;1"]
+                             .getService(Components.interfaces.nsIScriptableInputStream);
+
+      var channel = ioService.newChannel(aURL, null, null);
+      var input = channel.open();
+      scriptableStream.init(input);
+      var str = scriptableStream.read(input.available());
+      scriptableStream.close();
+      input.close();
+      return str.split("\n");
+    };
+
+
+    /* Given a FQDN, returns only the base domain, and honors two-level TLDs.
+     * So for example, www.foo.bar.com returns bar.com, or www.foo.bar.co.uk
+     * returns bar.co.uk.
+     */
+    this.getBaseDomainFromHost = function(host) {
+        if (this.storage.TLDs === undefined) {
+            // First window opened, so parse from stored list, which is
+            // borrowed from http://www.surbl.org/two-level-tlds
+            this.storage.TLDs = {};
+            for each (let line in this.readLines('chrome://nosquint/content/two-level-tlds'))
+                this.storage.TLDs[line] = true;
+        }
+        if (host.match(/^[\d.]+$/) != null)
+            // IP address.
+            return host;
+
+        var parts = host.split('.');
+        var level2 = parts.slice(-2).join('.');
+        var level3 = parts.slice(-3).join('.');
+        if (this.storage.TLDs[level3])
+            return parts.slice(-4).join('.');
+        else if (this.storage.TLDs[level2])
+            return level3;
+        return level2;
+    };
+
+
+    // XXX: don't forget to disable this for releases.
+    this.debug = function(msg) {
+        dump("[nosquint] " + msg + "\n");
+    };
+
+    /* This function is called a lot, so we take some care to optimize for the
+     * common cases.
+     */
+    this.isChrome = function(browser) {
+        var document = browser.docShell.document;
+        
+        if (document.URL == undefined)
+            return true;
+
+        /* In the common case, document.URL == browser.currentURI.spec, so we test
+         * this simple equality first before resorting to the probably unnecessary
+         * regexp call.
+         */
+        if (document.URL !=  browser.currentURI.spec &&
+            document.URL.replace(/#.*$/, '') != browser.currentURI.spec.replace(/#.*$/, ''))
+            /* Kludge: doc.URL doesn't match browser currentURI during host lookup failure,
+             * SSL cert errors, or other scenarios that result in an internal page being
+             * displayed that we consider chrome.
+             */
+            return true;
+
+        // A couple other common cases.
+        if (document.URL == undefined || document.URL.substr(0, 6) == 'about:')
+            return true;
+        if (document.contentType == 'text/html' || document.contentType == 'application/xhtml+xml')
+            return false;
+
+        // Less common cases that we'll cover with the more expensive regexp.
+        return document.contentType.search(/^text\/(plain|css|xml|javascript)/) != 0;
+    };
+
+    this.isImage = function(browser) {
+        return browser.docShell.document.contentType.search(/^image\//) == 0;
+    };
+
+    this.getImage = function(doc) {
+        // Not yet.
+        /*
+        var svg = doc.getElementsByTagName('svg');
+        if (svg.length > 0)
+            return svg[0];
+        */
+        return doc.body ? doc.body.firstChild : null;
+    };
+
+    this.foreachNSQ = (function() {
+        var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+                           .getService(Components.interfaces.nsIWindowMediator);
+        return function(callback) {
+            var enumerator = wm.getEnumerator("navigator:browser");
+            var win;
+            while (win = enumerator.getNext())
+                if (win.NoSquint && callback(win.NoSquint) === false)
+                    break;
+            return win;
+        };
+    })();
+
+
+    this.popup = function(type, title, text, value) {
+        var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+                      .getService(Components.interfaces.nsIPromptService);
+        if (type == 'confirm') 
+            return prompts.confirmEx(window, title, text,
+                                     prompts.STD_YES_NO_BUTTONS, null, null, null, 
+                                     null, {value: null});
+        else if (type == 'alert')
+            return prompts.alert(window, title, text);
+        else if (type == 'prompt') {
+            var data = {value: value};
+            prompts.prompt(window, title, text, data, null, {});
+            return data.value;
+        }
+        return null;
+    };
+
+
+    // Pythonic general purpose iterators.
+    this.iter = function() {
+        for (let i = 0; i < arguments.length; i++) {
+            var arg = arguments[i];
+            // duck typing
+            if (arg.length !== undefined) { 
+                for (let idx = 0; idx < arg.length; idx++)
+                    yield arg[idx];
+            } else {
+                for (let key in arg)
+                    yield key
+            }
+        }
+    };
+
+    this.items = function() {
+        for (let i = 0; i < arguments.length; i++) {
+            var arg = arguments[i];
+            for each (let [key, value] in Iterator(arg))
+                yield [key, value];
+        }
+    };
+
+    this.values = function() {
+        for (let i = 0; i < arguments.length; i++) {
+            var arg = arguments[i];
+            for each (let [key, value] in Iterator(arg))
+                yield value;
+        }
+    };
+
+    this.enumerate = function(o) {
+        var n = 0;
+        for (let i = 0; i < arguments.length; i++) {
+            var arg = arguments[i];
+            for (let value in this.iter(arg))
+                yield [n++, value];
+        }
+    };
+
+}).apply(NoSquint);
diff --git a/src/content/overlay.xul b/src/content/overlay.xul
index fb0b1e7..cc12c52 100644
--- a/src/content/overlay.xul
+++ b/src/content/overlay.xul
@@ -2,43 +2,55 @@
 <!DOCTYPE overlay SYSTEM "chrome://nosquint/locale/overlay.dtd">
 <?xml-stylesheet href="chrome://nosquint/skin/toolbar.css"  type="text/css"?>
 
-<overlay id="nosquint-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-    <script src="nosquint.js" />
-    <script src="init.js" />
+<overlay id="nosquint-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
+    <script type="application/x-javascript" src="chrome://nosquint/content/init.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/lib.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/interfaces.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/prefs.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/browser.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/cmd.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/zoommanager.js" />
 
     <stringbundleset id="stringbundleset">
         <stringbundle id="nosquint-overlay-bundle" src="chrome://nosquint/locale/overlay.properties" />
     </stringbundleset>
 
     <toolbarpalette id="BrowserToolbarPalette">
-        <toolbaritem id="nosquint-toolbar">
-            <toolbarbutton id="nosquint-button-reduce" class="toolbarbutton-1"
-                           label="Zoom Out" tooltiptext="Zoom out"
-                           oncommand="NoSquint.buttonReduce(event);" />
-            <toolbarbutton id="nosquint-button-enlarge" class="toolbarbutton-1"
-                           label="Zoom In" tooltiptext="Zoom in"
-                           oncommand="NoSquint.buttonEnlarge(event);" />
-        </toolbaritem>
+        <toolbarbutton id="nosquint-button-reduce" class="toolbarbutton-1"
+                       label="Zoom Out" tooltiptext="Zoom out"
+                       oncommand="NoSquint.cmd.buttonReduce(event);" />
+        <toolbarbutton id="nosquint-button-enlarge" class="toolbarbutton-1"
+                       label="Zoom In" tooltiptext="Zoom in"
+                       oncommand="NoSquint.cmd.buttonEnlarge(event);" />
+        <toolbarbutton id="nosquint-button-reset" class="toolbarbutton-1"
+                       label="Reset Zoom" tooltiptext="Reset Zoom"
+                       oncommand="NoSquint.cmd.buttonReset(event);" />
     </toolbarpalette>
 
     <keyset id="mainKeyset">
-        <key id="key_noSquintEnlargeSecondary" key="+" modifiers="control shift" 
+        <key id="key_noSquintEnlargeSecondary" key="+" modifiers="accel shift" 
              command="cmd_noSquintEnlargeSecondary" />
-        <key id="key_noSquintReduceSecondary" key="_" modifiers="control shift" 
+        <key id="key_noSquintReduceSecondary" key="_" modifiers="accel shift" 
              command="cmd_noSquintReduceSecondary" keytext="-" />
     </keyset>
 
     <commandset id="mainCommandSet">
-        <command id="cmd_noSquintPrefs" oncommand="NoSquint.openGlobalPrefsDialog()" />
-        <command id="cmd_fullZoomEnlarge" oncommand="NoSquint.cmdEnlargePrimary()" />
-        <command id="cmd_fullZoomReduce" oncommand="NoSquint.cmdReducePrimary()" />
-        <command id="cmd_fullZoomReset" oncommand="NoSquint.cmdReset()" />
-        <command id="cmd_noSquintEnlargeSecondary" oncommand="NoSquint.cmdEnlargeSecondary()" />
-        <command id="cmd_noSquintReduceSecondary" oncommand="NoSquint.cmdReduceSecondary()" />
+        <command id="cmd_noSquintPrefs" oncommand="NoSquint.cmd.openGlobalSettings()" />
+        <command id="cmd_fullZoomEnlarge" oncommand="NoSquint.cmd.enlargePrimary()" />
+        <command id="cmd_fullZoomReduce" oncommand="NoSquint.cmd.reducePrimary()" />
+        <command id="cmd_fullZoomReset" oncommand="NoSquint.cmd.reset()" />
+        <command id="cmd_noSquintEnlargeSecondary" oncommand="NoSquint.cmd.enlargeSecondary()" />
+        <command id="cmd_noSquintReduceSecondary" oncommand="NoSquint.cmd.reduceSecondary()" />
     </commandset>
 
+    <popup id="contentAreaContextMenu">
+      <menuitem id="nosquint-menu-settings" label="&ns.menu.context.label;"
+                accesskey="&ns.menu.context.accesskey;" oncommand="NoSquint.cmd.openSiteSettings();"/>
+    </popup>
+
     <statusbar id="status-bar">
-        <tooltip id="nosquint-status-tooltip" orient="vertical" style="background-color: #33DD00;">
+        <tooltip id="nosquint-status-tooltip" orient="vertical">
             <grid>
                 <columns>
                     <column />
@@ -52,6 +64,7 @@
                         </hbox>
                         <label value="" id="nosquint-status-tooltip-site" />
                     </row>
+                    <row style='border-top: 1px solid black; margin: 5px; opacity: 0.15' />
                     <row>
                         <hbox>
                             <spacer flex="1" />
@@ -66,11 +79,35 @@
                         </hbox>
                         <label value="" id="nosquint-status-tooltip-text" />
                     </row>
+
+                    <row style='border-top: 1px solid black; margin: 5px; opacity: 0.15' />
+
+                    <row>
+                        <hbox>
+                            <spacer flex="1" />
+                            <label value="&ns.tooltip.textColor.label;:" style="font-weight: bold" />
+                        </hbox>
+                        <hbox>
+                            <label id="nosquint-status-tooltip-textcolor" 
+                                   style='padding: 2px 10px; border: 1px solid black' />
+                        </hbox>
+                    </row>
+
+                    <row>
+                        <hbox>
+                            <spacer flex="1" />
+                            <label value="&ns.tooltip.linkColor.label;:" style="font-weight: bold" />
+                        </hbox>
+                        <hbox>
+                            <label id="nosquint-status-tooltip-unvis-link" />
+                            <label id="nosquint-status-tooltip-vis-link" />
+                        </hbox>
+                    </row>
                 </rows>
             </grid>
         </tooltip>
 
-        <menupopup id="nosquint-status-popup" oncommand="NoSquint.popupItemSelect(event)">
+        <menupopup id="nosquint-status-popup" oncommand="NoSquint.cmd.popupItemSelect(event)">
             <menuitem id="nosquint-popup-site" label="Site" disabled="true" style="font-style: italic" />
             <menu label="&ns.menu.fullZoom.label;">
                 <menupopup id="nosquint-status-popup-full">
@@ -94,15 +131,15 @@
                     <menuitem type="radio" name="text" label="150%" />
                 </menupopup>
             </menu>
-            <menuitem label="&ns.menu.reset.label;" onclick="NoSquint.cmdReset()" />
-            <menuitem label="&ns.menu.siteSettings.label;" onclick="NoSquint.openSitePrefsDialog()" />
+            <menuitem id="nosquint-status-reset" label="&ns.menu.reset.label;" onclick="NoSquint.cmd.reset()" />
+            <menuitem label="&ns.menu.siteSettings.label;" onclick="NoSquint.cmd.openSiteSettings()" />
             <menuseparator />
-            <menuitem label="&ns.menu.globalSettings.label;" onclick="NoSquint.openGlobalPrefsDialog()" />
+            <menuitem label="&ns.menu.globalSettings.label;" onclick="NoSquint.cmd.openGlobalSettings()" />
         </menupopup>
 
         <statusbarpanel class="statusbarpanel-iconic-text" id="nosquint-status" label="100%" 
-                        onclick="NoSquint.statusPanelClick(event)" 
-                        src="chrome://nosquint/skin/icon-enlarge-16.png" 
+                        onclick="NoSquint.cmd.statusPanelClick(event)" 
+                        src="chrome://nosquint/skin/icon-statusbar-16.png" 
                         tooltip="nosquint-status-tooltip" />
     </statusbar>
 </overlay> 
diff --git a/src/content/overlay_sanitize.xul b/src/content/overlay_sanitize.xul
new file mode 100644
index 0000000..9615a56
--- /dev/null
+++ b/src/content/overlay_sanitize.xul
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!DOCTYPE overlay SYSTEM "chrome://nosquint/locale/overlay.dtd">
+<overlay id="nosquint-sanitize" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+    <script type="application/x-javascript" src="chrome://nosquint/content/init.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/lib.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/prefs.js" />
+    <script type="application/x-javascript" src="chrome://nosquint/content/sanitize.js" />
+</overlay>
diff --git a/src/content/prefs.js b/src/content/prefs.js
index e381883..579eb74 100644
--- a/src/content/prefs.js
+++ b/src/content/prefs.js
@@ -1,216 +1,796 @@
-var NoSquintPrefs = {
-    prefs: null,
-    site: null,
-    level: null,
-    NoSquint: null,
-
-    init: function(doc) {
-        NoSquintPrefs.doc = doc;
-
-        if (window.arguments) {
-            NoSquintPrefs.site = window.arguments[0];
-            NoSquintPrefs.level = window.arguments[1];
-            NoSquintPrefs.url = window.arguments[2];
-            NoSquintPrefs.NoSquint = window.arguments[3];
-            NoSquintPrefs.prefs = NoSquintPrefs.NoSquint.prefs;
-        } else {
-            var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(
-                                      Components.interfaces.nsIPrefService);
-            NoSquintPrefs.prefs = prefs.getBranch("extensions.nosquint.")
-        }
-        doc.getElementById("defaultZoomLevel").value = NoSquintPrefs.prefs.getIntPref("zoomlevel");
-        doc.getElementById("zoomIncrement").value = NoSquintPrefs.prefs.getIntPref("zoomIncrement");
-        doc.getElementById("rememberSites").selectedIndex = NoSquintPrefs.prefs.getBoolPref("rememberSites") ? 1 : 0;
-        doc.getElementById("showStatus").checked = !NoSquintPrefs.prefs.getBoolPref("hideStatus");
-        doc.getElementById("wheelZoomEnabled").checked = NoSquintPrefs.prefs.getBoolPref("wheelZoomEnabled");
-
-        var forget_cb = doc.getElementById("siteForget");
-        var months = NoSquintPrefs.prefs.getIntPref("forgetMonths");
-        //if (months < 0 || months > 12 || months % 3 != 0)
-        //    months = 6;
-        forget_cb.checked = (months != 0);
-        if (months)
-            doc.getElementById("siteForget-menu").value = months;
-        forget_cb.addEventListener("CheckboxStateChange", NoSquintPrefs.forgetMonthsChecked, false);
-        NoSquintPrefs.forgetMonthsChecked();
-
-        NoSquintPrefs.sitesRadioSelect();
-        NoSquintPrefs.parseExceptions();
-        NoSquintPrefs.excListSelect();
-    },
-
-    parseExceptions: function() {
-        var exstr = NoSquintPrefs.prefs.getCharPref("exceptions");
+// chrome://browser/content/browser.xul
+
+/******************************************************************************
+ * Preferences (Singleton)
+ *
+ * Namespace for anything pref related, including service objects, any
+ * currently cached values, routines for parsing, or convenience functions
+ * for accessing preferences.
+ */
+
+NoSquint.prefs = NoSquint.ns(function() { with(NoSquint) {
+    // Namespace is a singleton, so return any previously instantiated prefs object.
+    if (NSQ.storage.prefs)
+        return NSQ.storage.prefs;
+    NSQ.storage.prefs = this;
+
+    this.id = 'NoSquint.prefs';
+    this.defaultColors = {
+        colorText: '#000000',
+        colorBackground: '#ffffff',
+        linksUnvisited: '#0000ee',
+        linksVisited: '#551a8b'
+    };
+
+    /* Active window we can use for window methods (e.g. setTimeout).  Because
+     * NSQ.prefs is a singleton, it could be that the window we initialized
+     * with has been closed.  In that case, setTimeout will fail with 
+     * NS_ERROR_NOT_INITIALIZED.  So we keep a reference to an available
+     * window here we can call window.* methods with, and if the window
+     * goes away, we find a new one using foreachNSQ().
+     */
+    this.window = window;
+
+    // Pref service.
+    var svc = Components.classes["@mozilla.org/preferences-service;1"].getService(
+                          Components.interfaces.nsIPrefService);
+    svc.QueryInterface(Components.interfaces.nsIPrefBranch);
+    this.svc = svc;
+
+    // Pref Branches we're interested in.
+    var branchNS = svc.getBranch('extensions.nosquint.');
+    var branchBZ = svc.getBranch('browser.zoom.');
+
+    var saveTimer = null;                // Timer for saveSiteList
+    var pruneTimer = null;               // Timer for pruneSites
+    var ignoreNextSitesChange = false;   // Ignore next update to sites pref
+    var origSiteSpecific = null;         // Original value of browser.zoom.siteSpecific
+    var initialized = false;
+
+
+    this.init = function() {
+        if (initialized)
+            return;
+        initialized = true;
+
+        // Backward compatibility: convert old prefs.
+        if (branchNS.getCharPref('prefsVersion') < '2.0') {
+            try {
+                // In 2.0, zoomlevel was split into fullZoomLevel and textZoomLevel
+                var zoomlevel = branchNS.getIntPref('zoomlevel');
+                branchNS.clearUserPref('zoomlevel');
+            } catch (err) {
+                // this was the default zoomlevel for < 2.0.
+                var zoomlevel = 120;
+            }
+
+            /* Previous versions of NoSquint set mousewheel.withcontrolkey.action=0
+             * under the assumption that we won't see DOMMouseScroll events otherwise.
+             * This was true with Firefox < 3, but apparently no longer the case with
+             * 3 and later.  So we restore the pref to its default value during this
+             * initial migration.  (The user might not want it restored, but this is
+             * the best we can do given what we know, and the correct thing to do
+             * in the common case.)
+             */
+            svc.setIntPref('mousewheel.withcontrolkey.action', 3);
+
+            var fullZoomPrimary = branchNS.getBoolPref('fullZoomPrimary');
+            if (fullZoomPrimary) {
+                branchNS.setIntPref('fullZoomLevel', zoomlevel);
+                branchNS.setIntPref('textZoomLevel', 100);
+            } else {
+                branchNS.setIntPref('fullZoomLevel', 100);
+                branchNS.setIntPref('textZoomLevel', zoomlevel);
+            }
+            branchNS.setCharPref('prefsVersion', '2.0');
+        }
+
+        /* Disable browser.zoom.siteSpecific, which prevents Firefox from
+         * automatically applying zoom levels, as that is now NoSquint's job.
+         */
+        if (origSiteSpecific === null)
+            origSiteSpecific = branchBZ.getBoolPref('siteSpecific');
+        this.setSiteSpecific(false);
+
+        // Pull prefs from prefs branch into object attributes
+        this.preload();
+
+        // Attach observers to both branches.
+        branchNS.QueryInterface(Components.interfaces.nsIPrefBranch2);
+        branchNS.addObserver('', this, false);
+        branchBZ.QueryInterface(Components.interfaces.nsIPrefBranch2);
+        branchBZ.addObserver('', this, false);
+    };
+
+    this.destroy = function() {
+        if (this.rememberSites)
+            // In case the window shutting down is the one whose saveTimer is
+            // associated with, we should finish any pending save now.
+            this.finishPendingSaveSiteList();
+
+        if (!NSQ.storage.quitting)
+            // NSQ.prefs is a singleton so we only ever truly destroy on app
+            // shutdown.
+            return;
+
+        branchNS.removeObserver('', this);
+        branchBZ.removeObserver('', this);
+
+        if (!this.rememberSites)
+            // Per-site setting storage disabled.
+            branchNS.setCharPref('sites', '');
+
+        this.setSiteSpecific(origSiteSpecific);
+    };
+
+
+    /* Invoke a window method, such as setTimeout.  We need to do this indirectly
+     * because NSQ.prefs is a singleton, and the window NSQ.prefs initialized with
+     * may not actually still be alive.
+     */
+    this.winFunc = function(func) {
+        var args = Array.prototype.slice.call(arguments, 1); 
+        try {
+            return this.window[func].apply(this.window, args);
+        } catch (e) {
+            // Presumably NS_ERROR_NOT_INITIALIZED.  TODO: verify.
+            this.window = foreachNSQ(function() false);
+            return this.window[func].apply(this.window, args);
+        }
+    };
+
+    this.setSiteSpecific = function(value) {
+        branchBZ.setBoolPref('siteSpecific', value);
+        this.save();
+    };
+
+    this.preload = function() {
+        // Initialize preferences in this order; some of them require other prefs
+        // have been loaded.  (e.g. forgetMonths needs rememberSites)
+        var prefs = [
+            'fullZoomLevel', 'textZoomLevel', 'zoomIncrement', 'wheelZoomEnabled', 'hideStatus',
+            'action', 'sitesSaveDelay', 'rememberSites', 'exceptions', 'sites', 'forgetMonths',
+            'fullZoomPrimary', 'wheelZoomInvert', 'zoomImages', 'colorText', 'colorBackground', 
+            'colorBackgroundImages', 'linksUnvisited', 'linksVisited', 'linksUnderline'
+        ];
+        for (let pref in iter(prefs))
+            // Simulate pref change for each pref to populate attributes
+            this.observe(null, "nsPref:changed", pref);
+    };
+
+
+    this.observe = function(subject, topic, data) {
+        if (topic != "nsPref:changed")
+            // Not a pref change.
+            return;
+
+        debug('observe(): data=' + data);
+        switch (data) {
+            case 'siteSpecific':
+                if (branchBZ.getBoolPref('siteSpecific') == false || NSQ.storage.disabled || NSQ.storage.quitting)
+                    // disabled, which is fine with us, so ignore.
+                    break;
+
+                // yes == 0, no or close == 1
+                if (popup('confirm', NSQ.strings.siteSpecificTitle, NSQ.strings.siteSpecificPrompt) == 1)
+                    popup('alert', NSQ.strings.siteSpecificBrokenTitle, NSQ.strings.siteSpecificBrokenPrompt);
+                else
+                    this.setSiteSpecific(false);
+                break;
+
+            case 'fullZoomLevel':
+                this.fullZoomLevel = branchNS.getIntPref('fullZoomLevel');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueZoomAll() : null);
+                break;
+
+            case 'textZoomLevel':
+                this.textZoomLevel = branchNS.getIntPref('textZoomLevel');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueZoomAll() : null);
+                break;
+
+            case 'wheelZoomEnabled':
+                this.wheelZoomEnabled = branchNS.getBoolPref('wheelZoomEnabled');
+                break;
+
+            case 'wheelZoomInvert':
+                this.wheelZoomInvert = branchNS.getBoolPref('wheelZoomInvert');
+                break;
+
+            case 'zoomIncrement':
+                this.zoomIncrement = branchNS.getIntPref('zoomIncrement');
+                break;
+
+            case 'forgetMonths':
+                this.forgetMonths = branchNS.getIntPref('forgetMonths');
+                this.pruneSites();
+                break;
+
+            case 'fullZoomPrimary':
+                this.fullZoomPrimary = branchNS.getBoolPref('fullZoomPrimary');
+                foreachNSQ(function(NSQ) {
+                    if (NSQ.browser) {
+                        NSQ.browser.updateZoomMenu();
+                        NSQ.browser.queueZoomAll();
+                    }
+                });
+                break;
+
+            case 'zoomImages':
+                this.zoomImages = branchNS.getBoolPref('zoomImages');
+                break;
+
+            case 'hideStatus':
+                var hideStatus = branchNS.getBoolPref('hideStatus');
+                this.hideStatus = hideStatus;
+                foreachNSQ(function(NSQ) {
+                    if (NSQ.browser) {
+                        $('nosquint-status').hidden = hideStatus;
+                        if (!hideStatus)
+                            // Status now being shown; update it to reflect current values.
+                            NSQ.browser.queueUpdateStatus();
+                    }
+                });
+                break;
+
+            case 'rememberSites':
+                this.rememberSites = branchNS.getBoolPref('rememberSites');
+                if (NSQ.storage.dialogs.site)
+                    // Toggle the warning in sites dialog.
+                    NSQ.storage.dialogs.site.updateWarning();
+                // TODO: if false, remove stored sites settings immediately, but keep
+                // in memory until end of session.
+                break;
+
+            case 'sitesSaveDelay':
+                this.saveDelay = branchNS.getIntPref('sitesSaveDelay');
+                break;
+
+            case 'exceptions':
+                // Parse exceptions list from prefs
+                this.exceptions = this.parseExceptions(branchNS.getCharPref('exceptions'));
+                foreachNSQ(function(NSQ) {
+                    if (NSQ.browser) {
+                        NSQ.browser.updateZoomMenu();
+                        NSQ.browser.queueZoomAll();
+                    }
+                });
+                break;
+
+            case 'sites':
+                if (ignoreNextSitesChange) {
+                    ignoreNextSitesChange = false;
+                    break;
+                }
+                this.sites = this.parseSites(branchNS.getCharPref('sites'));
+                if (saveTimer) {
+                    /* FIXME: looks like the sites list pref was updated (possibly by
+                     * another browser window) before we got a chance to write out our
+                     * changes.  We have lost them now; we should try to merge only
+                     * newer changes based on timestamp.
+                     */
+                     this.stopQueueSaveSiteList();
+                }
+                foreachNSQ(function(NSQ) {
+                    if (NSQ.browser) {
+                        NSQ.browser.queueZoomAll();
+                        NSQ.browser.queueStyleAll();
+                    }
+                });
+                break;
+
+            case 'colorText':
+                this.colorText = branchNS.getCharPref('colorText');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll() : null);
+                break;
+
+            case 'colorBackground':
+                this.colorBackground = branchNS.getCharPref('colorBackground');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll() : null);
+                break;
+
+            case 'colorBackgroundImages':
+                this.colorBackgroundImages = branchNS.getBoolPref('colorBackgroundImages');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll() : null);
+                break;
+
+            case 'linksUnvisited':
+                this.linksUnvisited = branchNS.getCharPref('linksUnvisited');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll() : null);
+                break;
+
+            case 'linksVisited':
+                this.linksVisited = branchNS.getCharPref('linksVisited');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll() : null);
+                break;
+
+            case 'linksUnderline':
+                this.linksUnderline = branchNS.getBoolPref('linksUnderline');
+                foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll() : null);
+                break;
+        }
+    };
+
+    this.save = function() {
+        return svc.savePrefFile(null);
+    };
+
+
+    /* Parses a extensions.nosquint.sites pref into sites array.
+     */
+    this.parseSites = function(sitesStr) {
+        /* Parse site list from prefs.  The prefs string a list of site specs,
+         * delimited by a space, in the form: 
+         *
+         *     sitename=text_level,timestamp,visits,full_level,textcolor,bgcolor,
+         *              nobgimages,linkunvis,linkvis,linkunderline
+         *
+         * Spaces are not allowed in any value; sitename is a string, all other
+         * values are integers.  The parsing code tries to be robust and handle
+         * malformed entries gracefully (in case the user edits them manually
+         * and screws up).  Consequently it is ugly.
+         */
+        var sites = {};
         // Trim whitespace and split on space.
-        exlist = exstr.replace(/(^\s+|\s+$)/g, "").split(" ");
-        for (var i = 0; i < exlist.length; i++) {
-            if (exlist[i])
-                NoSquintPrefs.exceptionsListAdd(exlist[i], false);
-        }
-        NoSquintPrefs.doc.getElementById("exceptionsList")._changed = false;
-    },
-
-    exceptionsListAdd: function(pattern, check_dupe) {
-        // Strip URI scheme from pattern (if it exists)
-        pattern = pattern.replace(/^\w+:\/\//, '');
-
-        var listbox = NoSquintPrefs.doc.getElementById("exceptionsList");
-        if (check_dupe) {
-            for (var i = 0; i < listbox.childNodes.length; i++) {
-                var node = listbox.childNodes[i];
-                if (node.childNodes[0].getAttribute("label") == pattern) {
-                    var bundle = NoSquintPrefs.doc.getElementById("nosquint-prefs-bundle");
-                    alert(bundle.getString('patternExists'));
-                    return;
+        var sitesList = sitesStr.replace(/(^\s+|\s+$)/g, '').split(' ');
+        var now = new Date().getTime();
+
+        for (let defn in iter(sitesList)) {
+            var parts = defn.split('=');
+            if (parts.length != 2)
+                continue; // malformed
+            var [site, info] = parts;
+            var parts = info.split(',');
+            sites[site] = [parseInt(parts[0]) || 0, now, 1, 0, '0', '0', false, '0', '0', false];
+            if (parts.length > 1) // last visited timestamp
+                sites[site][1] = parseInt(parts[1]) || now;
+            if (parts.length > 2) // visit count
+                sites[site][2] = parseInt(parts[2]) || 1;
+            if (parts.length > 3) // full page zoom level
+                sites[site][3] = parseInt(parts[3]) || 0;
+            if (parts.length > 4) // text color
+                sites[site][4] = parts[4] || '0';
+            if (parts.length > 5) // bg color
+                sites[site][5] = parts[5] || '0';
+            if (parts.length > 6) // disable bg images
+                sites[site][6] = parts[6] == 'true' ? true : false;
+            if (parts.length > 7) // unvisited link color
+                sites[site][7] = parts[7] || '0';
+            if (parts.length > 8) // visited link color
+                sites[site][8] = parts[8] || '0';
+            if (parts.length > 9) // force underline links
+                sites[site][9] = parts[9] == 'true' ? true : false;
+
+        }
+        return sites;
+    };
+
+
+    /* Takes an array of exceptions as stored in prefs, and returns a sorted
+     * list, where each exception is converted to a regexp grammar.  The list
+     * is sorted such that exceptions with the most literal (non-wildcard)
+     * characters are first.
+     */
+    this.parseExceptions = function(exStr) {
+        // Trim the space-delimited exceptions string and convert to array.
+        var exlist = exStr.replace(/(^\s+|\s+$)/g, '').split(' ');
+
+        /* This ugly function takes an exception, with our custom
+         * grammar, and converts it to a regular expression that we can
+         * match later.  Hostname and path components are processed in
+         * separate calls; re_star and re_dblstar define the regexp syntax
+         * for * and ** wildcards for this pattern.  (This is because
+         * wildcards have different semantics for host vs path.)
+         *
+         * Function returns a list of [length, pattern, sub] where length
+         * is the number of literal (non-wildcard) characters, pattern is
+         * the regexp that will be used to match against the URI, and sub is
+         * used (via regexp.replace) to create the site name based on the
+         * URI.
+         */
+        function regexpify(pattern, re_star, re_dblstar) {
+            var parts = pattern.split(/(\[\*+\]|\*+)/);
+            var pattern = [];
+            var sub = [];
+            var length = 0;
+
+            // Maps wildcards in custom grammar to regexp equivalent.
+            var wildcards = {
+                '*': '(' + re_star + ')',
+                '**': '(' + re_dblstar + ')',
+                '[*]': re_star,
+                '[**]': re_dblstar
+            };
+
+            var group = 1;
+            for (let part in iter(parts)) {
+                if (part == '')
+                    continue;
+                if (wildcards[part]) 
+                    pattern.push(wildcards[part]);
+                else {
+                    length += part.length;
+                    pattern.push('(' + part + ')');
                 }
+
+                if (part[0] == '[')
+                    sub.push(part.slice(1, -1));
+                else
+                    sub.push('$' + group++);
             }
+            return [length, pattern.join(''), sub.join('')];
         }
 
-        var node = NoSquintPrefs.doc.createElement("listitem");
-        var li1 = NoSquintPrefs.doc.createElement("listcell");
-        li1.setAttribute("label", pattern);
-        node.appendChild(li1);
-        listbox.appendChild(node);
-        node.addEventListener("dblclick", NoSquintPrefs.buttonEditException, false);
-        listbox._changed = true;
-    },
+        var exceptions = [];
+        for (var origexc in iter(exlist)) {
+            if (!origexc)
+                continue;
+            // Escape metacharacters except *
+            exc = origexc.replace(/([^\w:*\[\]])/g, '\\$1');
+            // Split into host and path parts, and regexpify separately.
+            var [_, exc_host, exc_path] = exc.match(/([^\/]*)(\\\/.*|$)/);
+            var [len_host, re_host, sub_host] = regexpify(exc_host, '[^.:/]+', '.*');
+            var [len_path, re_path, sub_path] = regexpify(exc_path, '[^/]+', '.*');
+            if (exc_host.search(':') == -1)
+                re_host += '(:\\d+)';
 
-    textPatternKeyPress: function(event) {
-        if (event.keyCode == 13) {
-            NoSquintPrefs.buttonAddException();
-            return false;
+            debug("regexpify(): exc_host=" + exc_host + ", re_host=" + re_host + ", sub_host=" + sub_host + ", exc_path=" + exc_path + ", re_path=" + re_path + ", sub_path=" + sub_path);
+            exceptions.push([origexc, len_host * 1000 + len_path, exc_host, re_host, sub_host, re_path, sub_path]);
+        }
+        // Sort the exceptions such that the ones with the highest weights
+        // (that is, the longest literal lengths) appear first.
+        exceptions.sort(function(a, b) b[1] - a[1]);
+        return exceptions;
+    };
+
+
+    /* Called periodically (on startup, and once a day after that) in order to
+     * remove remembered values for sites we haven't visited in forgetMonths.
+     */
+    this.pruneSites = function()  {
+        if (!this.rememberSites || this.forgetMonths == 0)
+            return;
+    
+        var remove = [];
+        var now = new Date();
+        for (let [site, settings] in items(this.sites)) {
+            if (!settings)
+                continue
+            var [text, timestamp, counter, full] = settings;
+            var age = now - new Date(timestamp);
+            var prune = (age > this.forgetMonths * 30*24*60*60*1000);
+            if (prune)
+                remove.push(site);
+            debug("pruneSites(): site=" + site + ", age=" + Math.round(age/1000/60/60/24) + " days, prune=" + prune);
+        }
+        if (remove.length) {
+            for (let site in iter(remove))
+                delete this.sites[site];
+            this.queueSaveSiteList();
+        }
+
+        // Fire timer once a day.
+        if (pruneTimer == null)
+            pruneTimer = this.winFunc('setTimeout', function() { pruneTimer = null; NSQ.prefs.pruneSites(); }, 24*60*60*1000);
+    };
+
+
+    /* Updates the site list for the given site name to set the given levels
+     * (2-tuple of [text, full]), and then queues a site list save.
+     */
+    this.updateSiteList = function(site, levels, style, update_timestamp) {
+        if (!site)
+            return;
+
+        if (!this.sites[site])
+            // new site record, initialize to defaults.
+            this.sites[site] = [0, new Date().getTime(), 1, 0, '0', '0', false, '0', '0', false];
+        var record = this.sites[site];
+
+        if (levels) {
+            // Update record with specified levels.
+            var [text_default, full_default] = this.getZoomDefaults();
+            var [text, full] = levels;
+            // Default zooms are stored as 0.
+            record[0] = text == text_default ? 0 : text;
+            record[3] = full == full_default ? 0 : full;
+            // Update all other tabs for this site.
+            foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueZoomAll(site, 1000) : null);
+        }
+        if (style) {
+            record[4] = style.colorText || '0';
+            record[5] = style.colorBackground || '0';
+            record[6] = style.colorBackgroundImages || '0';
+            record[7] = style.linksUnvisited || '0';
+            record[8] = style.linksVisited || '0';
+            record[9] = style.linksUnderline || '0';
+            // Update all other tabs for this site.
+            foreachNSQ(function(NSQ) NSQ.browser ? NSQ.browser.queueStyleAll(site, 1000) : null);
         }
-    },
 
-    textPatternChange: function() {
-        var pattern = NoSquintPrefs.doc.getElementById("pattern").value;
-        var exc_button = NoSquintPrefs.doc.getElementById("exceptionAdd-button");
-        exc_button.disabled = (pattern == '');
-        //var grp_button = NoSquintPrefs.doc.getElementById("groupAdd-button");
-        //exc_button.disabled = grp_button.disabled = (pattern == '');
-    },
+        // Check newly updated record against defaults.  If all values are default, we
+        // remove the record.
+        if ([record[0]].concat(record.slice(3)).toString() == [0, 0, '0', '0', false, '0', '0', false].toString())
+            // All defaults.
+            delete this.sites[site];
 
-    excListKeyPress: function(event) {
-        if (event.keyCode == 13) {
-            NoSquintPrefs.buttonEditException();
+        debug('updateSiteList(): site=' + site + ', record=' + record);
+
+        if (this.rememberSites)
+            this.queueSaveSiteList();
+    };
+
+
+    /* Updates the last-accessed timestamp for the given site, and then
+     * queues a site list save.
+     */
+    this.updateSiteTimestamp = function(site) {
+        if (!site || !this.sites[site])
+            return;
+
+        this.sites[site][1] = new Date().getTime();
+        this.sites[site][2] += 1;
+        if (this.rememberSites)
+            // Save updated timestamp.  Timestamps are only updated on
+            // the first page accessed for a given visit to that site,
+            // so this shouldn't be too bad.
+            this.queueSaveSiteList();
+    };
+
+
+    /* Queues a save of the site list in the prefs service.
+     *
+     * NOTE: This must only be called when the list has actually changed, or
+     * else the next time a change is made in the Settings dialog, it will
+     * be ignored.
+     */
+    this.queueSaveSiteList = function() {
+        this.stopQueueSaveSiteList();
+
+        /* The list is actually saved (by default) 5s later, so if the user
+         * changes the zoom several times in a short period of time, we aren't
+         * needlessly iterating over the sites array.
+         */
+        debug("queueSaveSiteList(): delay=" + this.saveDelay + ', window=' + window);
+        saveTimer = this.winFunc('setTimeout', function() NSQ.prefs.saveSiteList(), this.saveDelay);
+    };
+
+
+    /* Store the sites list right now. */
+    this.saveSiteList = function(force) {
+        if (!this.rememberSites || (NSQ.browser && NSQ.browser.observer.inPrivateBrowsing && !force))
+            /* Private Browsing mode is enabled or rememberSites disabled; do
+             * not save site list.
+             */
+            return;
+        var t0 = new Date().getTime();
+        var sites = [];
+        for (let [site, settings] in items(this.sites)) {
+            if (!settings)
+                continue;
+            sites.push(site + "=" + settings.join(','));
+        }
+
+        /* We're modifying the sites pref here.  Setting ignoreNextSitesChange=true
+         * causes the observer (in our current state) to not bother reparsing the
+         * sites pref because we know it's current.  In other words, we needn't
+         * respond to our own changes.
+         */
+        ignoreNextSitesChange = true;
+        branchNS.setCharPref('sites', sites.join(' '));
+        this.save();
+        debug("saveSiteList(): took: " + (new Date().getTime() - t0) + "ms");
+        this.winFunc('clearTimeout', saveTimer);
+        saveTimer = null;
+    };
+
+
+    /* Stops a previously queued site list save.  Returns true if a save was
+     * queued and aborted, or false if no save was queued.
+     */
+    this.stopQueueSaveSiteList = function() {
+        if (saveTimer === null)
             return false;
+
+        this.winFunc('clearTimeout', saveTimer);
+        saveTimer = null;
+        return true;
+    };
+
+    /* If a site list save is queued, force it to happen now.
+     */
+    this.finishPendingSaveSiteList = function() {
+        if (saveTimer)
+            this.saveSiteList();
+    };
+
+    this.cloneSites = function() {
+        var sites = {};
+        for (let [site, values] in Iterator(this.sites))
+            sites[site] = values.slice();
+        return sites;
+    };
+
+    /* Returns a 2-tuple [text_default, full_default] representing the default
+     * zoom levels.
+     */
+    this.getZoomDefaults = function() {
+        return [this.textZoomLevel, this.fullZoomLevel];
+    };
+
+
+    /* Given a URI, returns the site name, as computed based on user-defined
+     * exceptions.  If no exception matches the URI, we fall back to the base
+     * domain name.
+     */
+    this.getSiteFromURI = function(URI) {
+        var t0 = new Date().getTime();
+        if (!URI)
+            return null;
+
+        var uri_host = URI.asciiHost;
+        var uri_path = URI.path;
+
+        try {
+            var uri_port = URI.port < 0 ? 0 : URI.port;
+        } catch (err) {
+            var uri_port = '0';
         }
-    },
-
-    excListSelect: function() {
-        var btn = NoSquintPrefs.doc.getElementById("exceptionRemove-button");
-        var listbox = NoSquintPrefs.doc.getElementById("exceptionsList");
-        btn.disabled = (listbox.selectedItems.length == 0);
-
-        var btn = NoSquintPrefs.doc.getElementById("exceptionEdit-button");
-        btn.disabled = listbox.selectedItems.length != 1;
-    },
-
-    buttonCopyFromURL: function() {
-        var pattern = NoSquintPrefs.doc.getElementById("pattern");
-        pattern.value = NoSquintPrefs.url;
-        NoSquintPrefs.textPatternChange();
-    },
-
-    buttonAddException: function() {
-        var pattern = NoSquintPrefs.doc.getElementById("pattern");
-        NoSquintPrefs.exceptionsListAdd(pattern.value, true);
-        pattern.value = '';
-        NoSquintPrefs.textPatternChange();
-    },
-
-
-    buttonEditException: function() {
-        var listbox = NoSquintPrefs.doc.getElementById("exceptionsList");
-        var item = listbox.selectedItem;
-        var pattern = item.childNodes[0].getAttribute('label');
-        var bundle = NoSquintPrefs.doc.getElementById("nosquint-prefs-bundle");
-        var new_pattern = prompt(bundle.getString('editPrompt'),  pattern, bundle.getString('editTitle'));
-        if (new_pattern != null && new_pattern != pattern) {
-            item.childNodes[0].setAttribute('label', new_pattern);
-            listbox._changed = true;
-        }
-    },
-
-    buttonRemoveException: function() {
-        var listbox = NoSquintPrefs.doc.getElementById("exceptionsList");
-        while (listbox.selectedItems.length)
-            listbox.removeChild(listbox.selectedItems[0]);
-        listbox._changed = true;
-    },
-
-    forgetMonthsChecked: function() {
-        var checked = NoSquintPrefs.doc.getElementById('siteForget').checked;
-        NoSquintPrefs.doc.getElementById('siteForget-menu').disabled = !checked;
-    },
-
-    sitesRadioSelect: function() {
-        var doc = NoSquintPrefs.doc;
-        var label = doc.getElementById("siteZoom-label");
-        if (NoSquintPrefs.site) {
-            if (label.value.search("\\(") == -1) {
-                label.value = label.value.replace(":", " (" + NoSquintPrefs.site + "):");
-                doc.getElementById("siteZoom").value = NoSquintPrefs.level;
-            }
+
+        var base = getBaseDomainFromHost(uri_host);
+        if (!base && !uri_host)
+            // file:// url, use base as /
+            base = '/';
+
+        uri_host += ':' + uri_port;
+
+        var match = null;
+        
+        /* Iterate over each exception, trying to match it with the URI.
+         * We break the loop on the first match, because exceptions are
+         * sorted with highest weights first.
+         */
+        for (let exc in iter(this.exceptions)) {
+            var [_, weight, exc_host, re_host, sub_host, re_path, sub_path] = exc;
+            if (re_host.substr(0, 11) == '([^.:/]+)(:') // exc_host == *[:...]
+                // Single star is base name, so match just that, plus any port spec
+                // that's part of the exception.
+                re_host = '(' + base + ')' + re_host.substr(9);
+
+            var m1 = uri_host.match(new RegExp('(' + re_host + ')$'));
+            var m2 = uri_path.match(new RegExp('^(' + re_path + ')'));
+
+            //debug("getSiteFromURI(): host=" + uri_host + ", port=" + uri_port+ ", path=" + uri_path + ", base=" + base + " === exception info: re_host=" + re_host + ", sub_host=" + sub_host + ", re_path=" + re_path + ", sub_path=" + sub_path + " === results: m1=" + m1 + ", m2=" + m2);
+
+            if (!m1 || !m2)
+                // No match
+                continue;
+
+            var site_host = m1[1].replace(new RegExp(re_host), sub_host);
+            var site_path = m2[1].replace(new RegExp(re_path), sub_path);
+            match = site_host + site_path;
+            break;
         }
-        else
-            doc.getElementById("siteZoom-box").style.display = "none";
-        if (!NoSquintPrefs.url)
-            doc.getElementById("copyURL-button").style.display = "none";
-
-        var disabled = doc.getElementById("rememberSites").selectedIndex == 0;
-        NoSquintPrefs.enableTree(doc.getElementById("siteZoom-box"), disabled);
-        NoSquintPrefs.enableTree(doc.getElementById("siteForget-box"), disabled);
-    },
-
-    enableTree: function(node, state) {
-        for (var i = 0; i < node.childNodes.length; i++) {
-            var child = node.childNodes[i];
-            if (state && child.disabled == false || child.disabled == true)
-                child.disabled = state;
-            if (child.childNodes.length)
-                NoSquintPrefs.enableTree(child, state);
-        }
-    },
-
-    buttonSitesUseDefault: function() {
-        NoSquintPrefs.doc.getElementById("siteZoom").value = "default";
-    },
-
-    help: function() {
-        window.openDialog("chrome://nosquint/content/help.xul", "NoSquint Help", "chrome");
-    },
-
-    close: function() {
-        var doc = NoSquintPrefs.doc;
-        NoSquintPrefs.prefs.setBoolPref("hideStatus", !doc.getElementById("showStatus").checked);
-        NoSquintPrefs.prefs.setBoolPref("wheelZoomEnabled", doc.getElementById("wheelZoomEnabled").checked);
-        NoSquintPrefs.prefs.setIntPref("zoomlevel", doc.getElementById("defaultZoomLevel").value);
-        NoSquintPrefs.prefs.setIntPref("zoomIncrement", doc.getElementById("zoomIncrement").value);
-        var val = doc.getElementById("rememberSites").selectedIndex == 0 ? false : true;
-        NoSquintPrefs.prefs.setBoolPref("rememberSites", val);
-
-        var listbox = NoSquintPrefs.doc.getElementById("exceptionsList");
-        if (listbox._changed) {
-            var exceptions = [];
-            for (var i = 0; i < listbox.getRowCount(); i++) {
-                var item = listbox.getItemAtIndex(i);
-                var pattern = item.childNodes[0].getAttribute('label');
-                exceptions.push(pattern);
+        var t1 = new Date().getTime();
+        debug("getSiteFromURI(): took " + (t1-t0) + " ms: " + (match ? match : base) + ", uri=" + URI.spec);
+
+        return match ? match : base;
+    };
+
+
+    /* Gets the zoom levels for the given site name.  (Note, this is the site
+     * name as gotten from getSiteFromURI(), not the URI itself.)  Returns a
+     * 2-tuple [text_size, full_size], or [null, null] if the site is not
+     * found.  (This signifies to the caller to use the default zoom.)
+     */
+    this.getZoomForSite = function(site) {
+        if (site && this.sites[site])
+            return [this.sites[site][0], this.sites[site][3]];
+        return [null, null];
+    };
+
+    /* Gets the style parameters for the given site name.  Returns null if
+     * the site has no settings.
+     */
+    this.getStyleForSite = function(site) {
+       if (site && this.sites[site]) {
+            var s = this.sites[site];
+            return {
+                colorText: s[4],
+                colorBackground: s[5],
+                colorBackgroundImages: s[6],
+                linksUnvisited: s[7],
+                linksVisited: s[8],
+                linksUnderline: s[9]
+            };
+        }
+        return null;
+    };
+
+    /* Applies global styles to the given style object.  Attributes that have
+     * no site-local or global value are null.
+     */
+    this.applyStyleGlobals = function(style) {
+        var newstyle = { enabled: false };
+        var boolDefaults = {colorBackgroundImages: false, linksUnderline: false};
+        var isDefault = function(o, attr) !o || !o[attr] || o[attr] in ['0', false];
+        for (let [key, value] in items(this.defaultColors, boolDefaults)) {
+            newstyle[key] = isDefault(style, key) ? (isDefault(this, key) ? null : this[key]) : style[key];
+            newstyle.enabled = newstyle.enabled || Boolean(newstyle[key]);
+        }
+        return newstyle;
+    };
+
+
+    // Saves all preferences, including exceptions BUT NOT sites.
+    this.saveAll = function(exceptions) {
+        const intPrefs = [
+            'fullZoomLevel', 'textZoomLevel', 'zoomIncrement', 'forgetMonths'
+        ];
+        const boolPrefs = [
+            'wheelZoomEnabled', 'wheelZoomInvert', 'fullZoomPrimary', 'zoomImages',
+            'hideStatus', 'rememberSites', 'colorBackgroundImages', 'linksUnderline'
+        ];
+        const charPrefs = [
+            'colorText', 'colorBackground', 'linksUnvisited', 'linksVisited'
+        ];
+
+        for (let pref in iter(intPrefs))
+            branchNS.setIntPref(pref, this[pref]);
+
+        for (let pref in iter(boolPrefs))
+            branchNS.setBoolPref(pref, this[pref]);
+
+        for (let pref in iter(charPrefs))
+            branchNS.setCharPref(pref, this[pref]);
+
+        var exChanged = false;
+        if (exceptions) {
+            // TODO: if there is a new exception that matches any currently open
+            // tab, copy site settings for that tab into the new site name.  Also,
+            // any open site prefs dialog should be updated.
+            var exStr = exceptions.join(' ');
+            if (exStr != branchNS.getCharPref('exceptions')) {
+                branchNS.setCharPref('exceptions', exStr);
+                this.exceptions = this.parseExceptions(exStr);
+                exChanged = true;
             }
-            NoSquintPrefs.prefs.setCharPref("exceptions", exceptions.join(' '));
         }
-        if (!NoSquintPrefs.doc.getElementById("siteForget").checked)
-            NoSquintPrefs.prefs.setIntPref("forgetMonths", 0);
-        else
-            NoSquintPrefs.prefs.setIntPref("forgetMonths", NoSquintPrefs.doc.getElementById("siteForget-menu").value);
 
-        if (!NoSquintPrefs.NoSquint)
-            return;
+        foreachNSQ(function(NSQ) {
+            if (!NSQ.browser)
+                return;
+            if (exChanged) {
+                // exceptions changed, site names may have changed, so regenerate
+                // site names for all browsers.
+                for (let browser in iter(NSQ.browser.gBrowser.browsers))
+                    browser.getUserData('nosquint').site = NSQ.browser.getSiteFromBrowser(browser);
+            }
+            NSQ.browser.queueZoomAll();
+            NSQ.browser.queueStyleAll();
+        });
+    };
 
-        var level = doc.getElementById("siteZoom").value;
-        if (NoSquintPrefs.NoSquint.updateSiteList(NoSquintPrefs.site, level))
-            NoSquintPrefs.NoSquint.queueZoomAll();
-            //NoSquintPrefs.NoSquint.zoomAll(false);
-    }
-};
+    /* Removes all site settings for sites that were modified within the given
+     * range.  range is a 2-tuple (start, stop) where each are timestamps in
+     * milliseconds.  The newly sanitized site list is then immediately stored.
+     * All browsers are updated to reflect any changes.
+     */
+    this.sanitize = function(range) {
+        if (range == undefined || !range) {
+            this.sites = {}
+        } else {
+            for (var site in this.sites) {
+                var timestamp = this.sites[site][1] * 1000;
+                if (timestamp >= range[0] && timestamp <= range[1])
+                    delete this.sites[site];
+            }
+        }
+        this.saveSiteList();
+        foreachNSQ(function(NSQ) {
+            if (NSQ.browser) {
+                NSQ.browser.queueZoomAll();
+                NSQ.browser.queueStyleAll();
+            }
+        });
+    };
+}});
diff --git a/src/content/sanitize.js b/src/content/sanitize.js
new file mode 100644
index 0000000..cb375c5
--- /dev/null
+++ b/src/content/sanitize.js
@@ -0,0 +1,81 @@
+// chrome://browser/content/sanitize.xul 
+// chrome://browser/content/preferences/sanitize.xul 
+
+NoSquint.sanitizer = NoSquint.ns(function() { with (NoSquint) {
+    this.init = function() {
+        // Adds nosquint option to sanitizer UI
+        this.attachOption(NSQ.strings.sanitizeLabel)
+        if (typeof Sanitizer != 'undefined')
+            // Installs NoSquint hooks into the sanitizer
+            this.hookSanitizer();
+    };
+
+    this.attachOption = function(label) {
+        var inSanitizeDialog = typeof(gSanitizePromptDialog) == 'object';
+        // TODO: put this into a convenience function in lib.js
+        var prefService = Components.classes["@mozilla.org/preferences-service;1"]
+                            .getService(Components.interfaces.nsIPrefBranch);
+
+        // pref domain is privacy.cpd. for Firefox 3.1+, and privacy.item. for 3.0
+        // and earlier.
+        var domain = 'privacy.cpd.';
+        if ($('privacy.item.cache'))
+            domain = 'privacy.item.';
+        var prefs = document.getElementsByTagName('preferences')[0];
+        var pref = document.createElement('preference');
+        pref.setAttribute('id', domain + 'extensions-nosquint');
+        pref.setAttribute('name', domain + 'extensions-nosquint');
+        pref.setAttribute('type', 'bool');
+        var value = prefService.getBoolPref(domain + 'extensions-nosquint');
+        pref.setAttribute('value', value);
+        prefService.setBoolPref(domain + 'extensions-nosquint', value);
+        prefs.appendChild(pref);
+
+        if ($('itemList')) {
+            // In Clear Recent History dialog in Firefox 3.0
+            var check = $('itemList').appendItem(label);
+            check.setAttribute('type', 'checkbox');
+        } else {
+            // Firefox 3.0, or Firefox 3.5 in Settings, where the user sets which to enable/disable.
+            var check = document.createElement('checkbox');
+            check.setAttribute('label', label);
+            var rows = document.getElementsByTagName('rows');
+            if (rows.length) {
+                // Firefox 3.5
+                // Add new row to to rows.  TODO: append to last row if only has one column
+                var row = document.createElement('row');
+                row.appendChild(check);
+                rows[0].appendChild(row);
+            } else
+                // Firefox 3.0
+                document.getElementsByTagName('checkbox')[0].parentNode.appendChild(check);
+        }    
+        check.setAttribute('preference', domain + 'extensions-nosquint');
+        check.setAttribute('checked', value);
+
+        if (inSanitizeDialog) {
+            pref.setAttribute('readonly', 'true');
+            check.setAttribute('onsyncfrompreference', 'return gSanitizePromptDialog.onReadGeneric();');
+            if (prefService.getCharPref('extensions.nosquint.sites') == '') {
+                /* FIXME: a minor race condition: if user made first zoom change
+                 * and immediately opened sanitizer (before 5s timeout to store sites)
+                 * we will disable the checkbox when we shouldn't.
+                 */
+                check.setAttribute('disabled', true);
+                check.setAttribute('checked', false);
+            }
+        }
+    };
+
+    this.hookSanitizer = function() {
+        Sanitizer.prototype.items['extensions-nosquint'] = {
+            clear: function() {
+                NSQ.prefs.sanitize(this.range);
+            },
+            get canClear() {
+                return true;
+            }
+        };
+    };
+
+}});
diff --git a/src/content/two-level-tlds b/src/content/two-level-tlds
index 52428b6..379f194 100644
--- a/src/content/two-level-tlds
+++ b/src/content/two-level-tlds
@@ -3,6 +3,7 @@ ab.ca
 ab.se
 abo.pa
 ac.ae
+ac.am
 ac.at
 ac.bd
 ac.be
@@ -14,6 +15,7 @@ ac.fj
 ac.fk
 ac.gg
 ac.gn
+ac.hu
 ac.id
 ac.il
 ac.im
@@ -23,6 +25,7 @@ ac.je
 ac.jp
 ac.ke
 ac.kr
+ac.lk
 ac.ma
 ac.mw
 ac.ng
@@ -30,6 +33,7 @@ ac.nz
 ac.om
 ac.pa
 ac.pg
+ac.rs
 ac.ru
 ac.rw
 ac.se
@@ -94,6 +98,7 @@ asso.ht
 asso.mc
 asso.re
 astrakhan.ru
+at.tf
 at.tt
 atm.pl
 ato.br
@@ -114,13 +119,17 @@ be.tt
 bel.tr
 belgie.be
 belgorod.ru
+bg.tf
+bialystok.pl
 bib.ve
 bio.br
 bir.ru
 biz.az
+biz.bh
 biz.cy
 biz.et
 biz.fj
+biz.ly
 biz.mv
 biz.nr
 biz.om
@@ -143,11 +152,14 @@ bryansk.ru
 buryatia.ru
 busan.kr
 c.se
+ca.tf
 ca.tt
 ca.us
 casino.hu
 cbg.ru
+cc.bh
 cci.fr
+ch.tf
 ch.vu
 chambagri.fr
 chel.ru
@@ -174,12 +186,15 @@ cng.br
 cnt.br
 co.ae
 co.ag
+co.am
 co.ao
 co.at
+co.ba
 co.bw
 co.ck
 co.cr
 co.dk
+co.ee
 co.fk
 co.gg
 co.hu
@@ -196,8 +211,10 @@ co.ls
 co.ma
 co.mu
 co.mw
+co.mz
 co.nz
 co.om
+co.rs
 co.rw
 co.st
 co.th
@@ -205,9 +222,11 @@ co.tj
 co.tt
 co.tv
 co.tz
+co.ua
 co.ug
 co.uk
 co.us
+co.uz
 co.ve
 co.vi
 co.yu
@@ -220,11 +239,13 @@ com.af
 com.ag
 com.ai
 com.al
+com.am
 com.an
 com.ar
 com.au
 com.aw
 com.az
+com.ba
 com.bb
 com.bd
 com.bh
@@ -270,6 +291,7 @@ com.jm
 com.jo
 com.kg
 com.kh
+com.ki
 com.kw
 com.ky
 com.kz
@@ -294,6 +316,7 @@ com.mx
 com.my
 com.na
 com.nc
+com.nf
 com.ng
 com.ni
 com.np
@@ -331,6 +354,8 @@ com.tt
 com.tw
 com.ua
 com.uy
+com.uz
+com.vc
 com.ve
 com.vi
 com.vn
@@ -353,6 +378,7 @@ csiro.au
 ct.us
 cul.na
 cv.ua
+cz.tf
 d.se
 daegu.kr
 daejeon.kr
@@ -360,6 +386,7 @@ dagestan.ru
 dc.us
 de.com
 de.net
+de.tf
 de.tt
 de.us
 de.vu
@@ -386,10 +413,12 @@ edu.ac
 edu.af
 edu.ai
 edu.al
+edu.am
 edu.an
 edu.ar
 edu.au
 edu.az
+edu.ba
 edu.bb
 edu.bd
 edu.bh
@@ -406,6 +435,7 @@ edu.dm
 edu.do
 edu.dz
 edu.ec
+edu.ee
 edu.eg
 edu.er
 edu.es
@@ -420,7 +450,9 @@ edu.gu
 edu.hk
 edu.hn
 edu.ht
+edu.hu
 edu.in
+edu.it
 edu.jm
 edu.jo
 edu.kg
@@ -460,6 +492,7 @@ edu.ps
 edu.pt
 edu.py
 edu.qa
+edu.rs
 edu.ru
 edu.rw
 edu.sa
@@ -471,6 +504,7 @@ edu.sh
 edu.sk
 edu.st
 edu.sv
+edu.tf
 edu.tj
 edu.tr
 edu.tt
@@ -502,6 +536,7 @@ etc.br
 eti.br
 eu.com
 eu.org
+eu.tf
 eu.tt
 eun.eg
 experts-comptables.fr
@@ -551,6 +586,8 @@ gb.com
 gb.net
 gc.ca
 gd.cn
+gda.pl
+gdansk.pl
 geek.nz
 gen.in
 gen.nz
@@ -591,9 +628,11 @@ gov.ae
 gov.af
 gov.ai
 gov.al
+gov.am
 gov.ar
 gov.au
 gov.az
+gov.ba
 gov.bb
 gov.bd
 gov.bf
@@ -627,6 +666,7 @@ gov.gn
 gov.gr
 gov.gu
 gov.hk
+gov.hu
 gov.ie
 gov.il
 gov.im
@@ -673,6 +713,7 @@ gov.ps
 gov.pt
 gov.py
 gov.qa
+gov.rs
 gov.ru
 gov.rw
 gov.sa
@@ -758,6 +799,7 @@ if.ua
 il.us
 imb.br
 in-addr.arpa
+in.rs
 in.th
 in.ua
 in.us
@@ -773,7 +815,9 @@ inf.br
 inf.cu
 info.au
 info.az
+info.bh
 info.co
+info.cu
 info.cy
 info.ec
 info.et
@@ -794,6 +838,7 @@ info.vn
 ing.pa
 ingatlan.hu
 inima.al
+int.am
 int.ar
 int.az
 int.bo
@@ -804,6 +849,7 @@ int.mw
 int.pt
 int.ru
 int.rw
+int.tf
 int.tj
 int.tt
 int.ve
@@ -850,6 +896,7 @@ kamchatka.ru
 kanagawa.jp
 kanazawa.jp
 karelia.ru
+katowice.pl
 kawasaki.jp
 kazan.ru
 kchr.ru
@@ -880,6 +927,7 @@ komvux.se
 konyvelo.hu
 kostroma.ru
 kr.ua
+krakow.pl
 krasnoyarsk.ru
 ks.ua
 ks.us
@@ -905,6 +953,7 @@ lg.ua
 lipetsk.ru
 lkd.co.im
 ln.cn
+lodz.pl
 ltd.co.im
 ltd.cy
 ltd.gg
@@ -912,6 +961,7 @@ ltd.gi
 ltd.je
 ltd.lk
 ltd.uk
+lublin.pl
 lugansk.ua
 lutsk.ua
 lviv.ua
@@ -949,8 +999,10 @@ miasta.pl
 mie.jp
 mil.ac
 mil.ae
+mil.am
 mil.ar
 mil.az
+mil.ba
 mil.bd
 mil.bo
 mil.br
@@ -1073,10 +1125,12 @@ net.af
 net.ag
 net.ai
 net.al
+net.am
 net.an
 net.ar
 net.au
 net.az
+net.ba
 net.bb
 net.bd
 net.bh
@@ -1125,6 +1179,7 @@ net.jo
 net.jp
 net.kg
 net.kh
+net.ki
 net.kw
 net.ky
 net.kz
@@ -1141,12 +1196,14 @@ net.ma
 net.mm
 net.mo
 net.mt
+net.mu
 net.mv
 net.mw
 net.mx
 net.my
 net.na
 net.nc
+net.nf
 net.ng
 net.ni
 net.np
@@ -1174,6 +1231,7 @@ net.sg
 net.sh
 net.st
 net.sy
+net.tf
 net.th
 net.tj
 net.tn
@@ -1183,6 +1241,8 @@ net.tw
 net.ua
 net.uk
 net.uy
+net.uz
+net.vc
 net.ve
 net.vi
 net.vn
@@ -1190,6 +1250,7 @@ net.vu
 net.ws
 net.ye
 net.za
+new.ke
 news.hu
 nf.ca
 ngo.lk
@@ -1244,6 +1305,7 @@ nt.ca
 nt.ro
 ntr.br
 nu.ca
+nui.hu
 nv.us
 nx.cn
 ny.us
@@ -1258,8 +1320,10 @@ oita.jp
 ok.us
 okayama.jp
 okinawa.jp
+olsztyn.pl
 omsk.ru
 on.ca
+opole.pl
 or.at
 or.cr
 or.id
@@ -1276,10 +1340,12 @@ org.ae
 org.ag
 org.ai
 org.al
+org.am
 org.an
 org.ar
 org.au
 org.az
+org.ba
 org.bb
 org.bd
 org.bh
@@ -1333,6 +1399,7 @@ org.jo
 org.jp
 org.kg
 org.kh
+org.ki
 org.kw
 org.ky
 org.kz
@@ -1353,6 +1420,7 @@ org.mm
 org.mn
 org.mo
 org.mt
+org.mu
 org.mv
 org.mw
 org.mx
@@ -1377,6 +1445,7 @@ org.pt
 org.py
 org.qa
 org.ro
+org.rs
 org.ru
 org.sa
 org.sb
@@ -1396,6 +1465,8 @@ org.tw
 org.ua
 org.uk
 org.uy
+org.uz
+org.vc
 org.ve
 org.vi
 org.vn
@@ -1426,6 +1497,7 @@ per.sg
 perm.ru
 perso.ht
 pharmacien.fr
+pl.tf
 pl.ua
 plc.co.im
 plc.ly
@@ -1438,6 +1510,7 @@ police.uk
 poltava.ua
 port.fr
 powiat.pl
+poznan.pl
 pp.az
 pp.ru
 pp.se
@@ -1495,7 +1568,9 @@ rnrt.tn
 rns.tn
 rnu.tn
 rovno.ua
+rs.ba
 ru.com
+ru.tf
 rubtsovsk.ru
 rv.ua
 ryazan.ru
@@ -1518,6 +1593,7 @@ sc.ug
 sc.us
 sch.ae
 sch.gg
+sch.id
 sch.ir
 sch.je
 sch.lk
@@ -1542,6 +1618,7 @@ sendai.jp
 seoul.kr
 sex.hu
 sex.pl
+sg.tf
 sh.cn
 shiga.jp
 shimane.jp
@@ -1555,6 +1632,7 @@ sklep.pl
 sld.do
 sld.pa
 slg.br
+slupsk.pl
 smolensk.ru
 sn.cn
 snz.ru
@@ -1577,6 +1655,7 @@ sumy.ua
 surgut.ru
 sx.cn
 syzran.ru
+szczecin.pl
 szex.hu
 szkola.pl
 t.se
@@ -1596,6 +1675,7 @@ ternopil.ua
 test.ru
 tirana.al
 tj.cn
+tld.am
 tlf.nr
 tm.cy
 tm.fr
@@ -1614,6 +1694,7 @@ tokushima.jp
 tokyo.jp
 tom.ru
 tomsk.ru
+torun.pl
 tottori.jp
 tourism.pl
 tourism.tn
@@ -1644,11 +1725,14 @@ uk.tt
 ulan-ude.ru
 ulsan.kr
 unam.na
+unbi.ba
 uniti.al
+unsa.ba
 upt.al
 uri.arpa
 urn.arpa
 us.com
+us.tf
 us.tt
 ut.us
 utazas.hu
@@ -1679,9 +1763,12 @@ w.se
 wa.au
 wa.us
 wakayama.jp
+warszawa.pl
+waw.pl
 weather.mobi
 web.co
 web.do
+web.id
 web.lk
 web.pk
 web.tj
@@ -1689,6 +1776,8 @@ web.tr
 web.ve
 web.za
 wi.us
+wroc.pl
+wroclaw.pl
 wv.us
 www.ro
 wy.us
@@ -1709,7 +1798,9 @@ yokohama.jp
 yuzhno-sakhalinsk.ru
 z.se
 za.com
+za.pl
 zaporizhzhe.ua
+zgora.pl
 zgrad.ru
 zhitomir.ua
 zj.cn
diff --git a/src/content/zoommanager.js b/src/content/zoommanager.js
new file mode 100644
index 0000000..ed2365c
--- /dev/null
+++ b/src/content/zoommanager.js
@@ -0,0 +1,62 @@
+/* NoSquint hooks the ZoomManager, overriding its default functionality.
+ *
+ * The logic below should be well-behaved when the user uses exclusively
+ * full page or text-only zooms.
+ *
+ * If both zooms are in use (i.e. full and text != 100%), things can get
+ * a little dubious.  In general, if full zoom is not 100%, then we pretend
+ * as if full zoom is the primary method, regardless of whether it actually
+ * is.  The rationale is that full page zoom is more likely to affect logic
+ * used by people interfacing with ZoomManager.
+ *
+ * More details in ZoomManager.useFullZoom getter.
+ */
+
+// ZoomManager._nosquintOrigZoomGetter = ZoomManager.__lookupGetter__('zoom');
+// ZoomManager._nosquintOrigZoomSetter = ZoomManager.__lookupSetter__('zoom');
+
+ZoomManager.__defineSetter__('zoom', function(value) {
+    var viewer = getBrowser().mCurrentBrowser.markupDocumentViewer;
+    var updated = false;
+
+    if (ZoomManager.useFullZoom && viewer.fullZoom != value)
+        updated = viewer.fullZoom = value;
+    else if (!ZoomManager.useFullZoom && viewer.textZoom != value)
+        updated = viewer.textZoom = value;
+
+    if (updated != false) {
+        NoSquint.browser.saveCurrentZoom();
+        NoSquint.browser.updateStatus();
+    }
+});
+
+ZoomManager.__defineGetter__('zoom', function() {
+    var viewer = getBrowser().mCurrentBrowser.markupDocumentViewer;
+    return ZoomManager.useFullZoom ? viewer.fullZoom : viewer.textZoom;
+});
+
+ZoomManager.__defineGetter__('useFullZoom', function() {
+    /* Extensions (like all-in-one gestures) assume that zoom is either all
+     * full page or all text-only, which is of course quite reasonable given
+     * that the ZoomManager interface assumes this too.
+     *
+     * So, regardless of what the primary zoom method is set to, if the
+     * current page has a full zoom level != 100%, then we always return
+     * true here.
+     * 
+     * This is to handle the uncommon case where the user has modified
+     * both text and full page zoom.  Extensions like AIO need to base
+     * decisions on whether or not the page is full-zoomed, not whether
+     * or not the user prefers full or text zoom.
+     */
+    var viewer = getBrowser().mCurrentBrowser.markupDocumentViewer;
+    return viewer.fullZoom != 1.0 ? true : NoSquint.prefs.fullZoomPrimary;
+});
+
+ZoomManager.enlarge = NoSquint.cmd.enlargePrimary;
+ZoomManager.reduce = NoSquint.cmd.reducePrimary;
+ZoomManager.reset = NoSquint.cmd.reset;
+
+FullZoom.enlarge = NoSquint.cmd.enlargeFullZoom;
+FullZoom.reduce = NoSquint.cmd.reduceFullZoom;
+FullZoom.reset = NoSquint.cmd.reset;
diff --git a/src/defaults/preferences/nosquint.js b/src/defaults/preferences/nosquint.js
index ad31683..a67cf00 100644
--- a/src/defaults/preferences/nosquint.js
+++ b/src/defaults/preferences/nosquint.js
@@ -1,10 +1,22 @@
-pref("extensions.nosquint.zoomlevel", 120);
+pref("extensions.nosquint.fullZoomLevel", 120);
+pref("extensions.nosquint.textZoomLevel", 100);
 pref("extensions.nosquint.zoomIncrement", 10);
 pref("extensions.nosquint.rememberSites", true);
 pref("extensions.nosquint.sites", "");
 pref("extensions.nosquint.sitesSaveDelay", 5000);
 pref("extensions.nosquint.exceptions", "*/~* *.sourceforge.net *.google.[*]");
-pref("extensions.nosquint.wheelZoomEnabled", false);
+pref("extensions.nosquint.zoomImages", true);
+pref("extensions.nosquint.wheelZoomEnabled", true);
+pref("extensions.nosquint.wheelZoomInvert", false);
 pref("extensions.nosquint.hideStatus", false);
 pref("extensions.nosquint.forgetMonths", 6);
 pref("extensions.nosquint.fullZoomPrimary", true);
+pref("extensions.nosquint.prefsVersion", '0');
+pref("extensions.nosquint.colorText", '0');
+pref("extensions.nosquint.colorBackground", '0');
+pref("extensions.nosquint.colorBackgroundImages", false);
+pref("extensions.nosquint.linksUnvisited", '0');
+pref("extensions.nosquint.linksVisited", '0');
+pref("extensions.nosquint.linksUnderline", false);
+pref("privacy.cpd.extensions-nosquint", true);
+pref("privacy.item.extensions-nosquint", true);
diff --git a/src/install.rdf b/src/install.rdf
index 9250cce..0ce1b26 100644
--- a/src/install.rdf
+++ b/src/install.rdf
@@ -5,14 +5,14 @@
     <Description about="urn:mozilla:install-manifest">
     
         <em:id>nosquint at urandom.ca</em:id>
-        <em:name>No Squint</em:name>
-        <em:version>1.93.2.1</em:version>
-        <em:description>Manage site-specific full page and text zoom levels</em:description>
+        <em:name>NoSquint</em:name>
+        <em:version>2.0.1b1</em:version>
+        <em:description>Manage site-specific zoom levels and color settings</em:description>
         <em:creator>Jason Tackaberry</em:creator>
         <!-- optional items -->
         <em:homepageURL>http://urandom.ca/nosquint/</em:homepageURL>
-        <em:optionsURL>chrome://nosquint/content/globalprefs.xul</em:optionsURL>
-        <em:iconURL>chrome://nosquint/content/icon-32.png</em:iconURL>
+        <em:optionsURL>chrome://nosquint/content/dlg-global.xul</em:optionsURL>
+        <em:iconURL>chrome://nosquint/skin/logo-32.png</em:iconURL>
         <em:type>2</em:type> <!-- type=extension --> 
 
         <!-- Firefox -->
@@ -20,7 +20,7 @@
             <Description>
                 <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
                 <em:minVersion>3.0b4pre</em:minVersion>
-                <em:maxVersion>3.0.*</em:maxVersion>
+                <em:maxVersion>3.7a1pre</em:maxVersion>
             </Description>
         </em:targetApplication>
     </Description>
diff --git a/src/locale/en-US/dlg-global.dtd b/src/locale/en-US/dlg-global.dtd
new file mode 100644
index 0000000..e430929
--- /dev/null
+++ b/src/locale/en-US/dlg-global.dtd
@@ -0,0 +1,53 @@
+<!ENTITY ns.pref.title "NoSquint Global Settings">
+<!ENTITY ns.pref.tab.general.label "General">
+<!ENTITY ns.pref.tab.zooming.label "Zooming">
+<!ENTITY ns.pref.tab.colors.label "Colors">
+<!ENTITY ns.pref.tab.exceptions.label "Exceptions">
+
+<!ENTITY ns.pref.zooming.caption "Default Zoom Options">
+
+<!ENTITY ns.pref.zooming.primaryMethod.label "Primary zoom method">
+<!ENTITY ns.pref.zooming.primaryMethod.full "Full Page Zoom (images and text)">
+<!ENTITY ns.pref.zooming.primaryMethod.text "Text Zoom (text only)">
+
+<!ENTITY ns.pref.zooming.fullLevel.label "Default full page zoom level">
+<!ENTITY ns.pref.zooming.textLevel.label "Default text-only zoom level">
+<!ENTITY ns.pref.zooming.increment.label "Zoom increment">
+<!ENTITY ns.pref.zooming.images.label "Zoom standalone images to fit browser window">
+<!ENTITY ns.pref.zooming.mousewheel.label "Enable zoom with ctrl-mousewheel">
+<!ENTITY ns.pref.zooming.showstatus.label "Show current zoom levels in status bar">
+
+<!ENTITY ns.pref.persistence.caption "Per-site settings (color and zoom)">
+<!ENTITY ns.pref.persistence.remember.label "Remember per-site settings between Firefox restarts">
+<!ENTITY ns.pref.persistence.noRemember.label "Forget all per-site settings when Firefox closes">
+<!ENTITY ns.pref.persistence.forget.label "Forget settings for sites not visited in the last">
+<!ENTITY ns.pref.persistence.forget.year "Year">
+<!ENTITY ns.pref.persistence.forget.6months "Six Months">
+<!ENTITY ns.pref.persistence.forget.3months "Three Months">
+<!ENTITY ns.pref.persistence.forget.month "Month">
+<!ENTITY ns.pref.persistence.sanitize.label "Erase NoSquint site history when Firefox clears private data">
+
+<!ENTITY ns.pref.colors.info "Enabling any of the color options below overrides the default for all pages.">
+<!ENTITY ns.pref.colors.colors.caption "Text and Background">
+<!ENTITY ns.pref.colors.colors.text.label "Text">
+<!ENTITY ns.pref.colors.colors.background.label "Background">
+<!ENTITY ns.pref.colors.colors.images.label "Disable background images">
+<!ENTITY ns.pref.colors.links.caption "Link Colors">
+<!ENTITY ns.pref.colors.links.unvisited.label "Unvisited">
+<!ENTITY ns.pref.colors.links.visited.label "Visited">
+<!ENTITY ns.pref.colors.links.underline.label "Always underline links">
+
+
+<!ENTITY ns.pref.exceptions.info "Exceptions are an advanced feature that controls how NoSquint distinguishes separate sites.  Click the Help button below for full details.">
+
+<!ENTITY ns.pref.exceptions.pattern.label "Pattern for new exception">
+<!ENTITY ns.pref.exceptions.copyButton.label "Copy from URL">
+<!ENTITY ns.pref.exceptions.copyButton.accesskey "C">
+<!ENTITY ns.pref.exceptions.addButton.label "Add Exception">
+<!ENTITY ns.pref.exceptions.addButton.accesskey "A">
+<!ENTITY ns.pref.exceptions.list.col1.label "Exception Pattern">
+<!ENTITY ns.pref.exceptions.editButton.label "Edit Exception">
+<!ENTITY ns.pref.exceptions.editButton.accesskey "E">
+<!ENTITY ns.pref.exceptions.removeButton.label "Remove Exception">
+<!ENTITY ns.pref.exceptions.removeButton.accesskey "R">
+
diff --git a/src/locale/en-US/dlg-global.properties b/src/locale/en-US/dlg-global.properties
new file mode 100644
index 0000000..1608767
--- /dev/null
+++ b/src/locale/en-US/dlg-global.properties
@@ -0,0 +1,3 @@
+editPrompt=Specify pattern for exception
+editTitle=Edit Pattern
+patternExists=This pattern already exists in the Exceptions list.
diff --git a/src/locale/en-US/dlg-help.dtd b/src/locale/en-US/dlg-help.dtd
new file mode 100644
index 0000000..6e6f28a
--- /dev/null
+++ b/src/locale/en-US/dlg-help.dtd
@@ -0,0 +1,2 @@
+<!ENTITY ns.help.title "NoSquint Help">
+<!ENTITY ns.help.subtitle "Help">
diff --git a/src/locale/en-US/dlg-site.dtd b/src/locale/en-US/dlg-site.dtd
new file mode 100644
index 0000000..e41f2aa
--- /dev/null
+++ b/src/locale/en-US/dlg-site.dtd
@@ -0,0 +1,7 @@
+<!ENTITY ns.pref.title "NoSquint Site Settings">
+<!ENTITY ns.pref.button.global.label "Global Settings">
+<!ENTITY ns.pref.button.global.accesskey "G">
+
+<!ENTITY ns.pref.fullZoom.label "Full zoom level">
+<!ENTITY ns.pref.textZoom.label "Text zoom level">
+<!ENTITY ns.pref.button.useDefault.label "Use Default">
diff --git a/src/locale/en-US/dlg-site.properties b/src/locale/en-US/dlg-site.properties
new file mode 100644
index 0000000..9e7f125
--- /dev/null
+++ b/src/locale/en-US/dlg-site.properties
@@ -0,0 +1,4 @@
+settingsFor=Settings for
+warningForgetSites=Values set here will be discarded when you quit Firefox.  This behavior can be changed in Global Settings.
+warningPrivateBrowsing=Values set here will be discarded once you quit Firefox or exit Private Browsing mode.
+
diff --git a/src/locale/en-US/help.html b/src/locale/en-US/help.html
index bbc6660..7db3377 100644
--- a/src/locale/en-US/help.html
+++ b/src/locale/en-US/help.html
@@ -52,8 +52,77 @@
 </style>
 
 <body>
-<h2>Options Tab</h2>
-<h3>General Options</h3>
+<h2 id='general'>General Tab</h2>
+
+<p>To NoSquint, a site is a web location where all pages have the same zoom
+level and color settings, and the site name is derived from the page's URL.</p>
+
+<p>In most cases, the site is the domain.  For example, if the current page is
+<code>www2.ibm.com/index.php</code>, NoSquint will consider the site name to be
+<code>ibm.com</code>.  NoSquint will also take into account common country-specific
+second-level domains.  For example, if you're visiting
+<code>www.bbc.co.uk</code>, NoSquint will consider the site name to be
+<code>bbc.co.uk</code>.</p>
+
+<p>The default behaviour should work almost all the time.  When it doesn't, you
+can control how NoSquint determines site names in the 
+<a href='#exceptions'>Exceptions Tab</a>.</p>
+
+<h3>Per-site settings (color and zoom)</h3>
+<ul>
+    <li>
+        <b>Remember per-site settings between Firefox restarts</b>
+            <p>With this option selected, NoSquint will remember any changes
+            you make to the zoom levels and color choices for a given site, and
+            the changes are stored on disk so they can be applied even if you
+            restart Firefox.</p>
+            <p>Both full page zoom and text zoom levels are remembered
+            independently.  Next time you visit that site, NoSquint will use
+            the zoom and colors previously used on that site.</p>
+    </li>
+    <li>
+        <b>Forget settings for sites not visited in the last ...</b>
+            <p>With the "Remember per-site settings between Firefox restarts"
+            option enabled, NoSquint keeps track of all zoom level and color
+            changes for sites, even sites you only visit once.  This option is
+            house cleaning: if you haven't visited a site (for which you've set
+            a non-default zoom or color setting) for the specified number of
+            months, NoSquint will forget the setting.</p>
+    </li>
+    <li>
+        <b>Erase NoSquint site history when Firefox clears private data</b>
+            <p>When you change the default zoom levels or colors for a given
+            site, NoSquint remembers that change, which means it must keep a 
+            record of those sites.  From a privacy perspective, this is similar
+            to your browsing history.  If this option is selected, when Firefox
+            clears your private data (via Tools | Clear Private Data for Firefox
+            3.0, and Tools | Clear Recent History for Firefox 3.5), it will
+            also purge all site-specific data associated with NoSquint.</p>
+            <p>Note that user-added <a href='#exceptions'>exceptions</a> are
+            not cleared.</p>
+    </li>
+    <li>
+        <b>Forget all per-site settings when Firefox closes</b>
+            <p>Per-site settings will be retained only for the current Firefox
+            session.  They are never written to disk, and will therefore not
+            be remembered if you restart Firefox.</p>
+            <p>An alternative would be to use Firefox's Private Browsing mode
+            (Firefox 3.5 and later), which NoSquint supports.  When Private
+            Browsing is activated, even if "Remember per-site settings between
+            Firefox restarts" is selected instead of this option, NoSquint will
+            never store any per-site settings to disk which have changed while
+            in that mode.</p>
+    </li>
+</ul>
+
+<h2 id='zooming'>Zooming Tab</h2>
+
+<p>The options in this tab let you control the default zooming behavior as they
+apply to all sites.  You can override the zoom levels for individual sites using
+the Site Settings, which can be managed by left-clicking on NoSquint's status bar
+icon, or selecting NoSquint Site Settings from the page's context menu.</p>
+
+<h3>Default Zoom Options</h3>
 <ul>
     <li>
         <b>Primary zoom method</b>
@@ -67,17 +136,30 @@
             secondary zoom method.  For example, if the primary zoom method is
             set to Full Page Zoom, and ctrl-shift-plus is pressed, only text size will
             be increased.</p>
+    </li>
             
+    <li>
+        <b>Default full page zoom level</b>
+            <p>This is the full page zoom level (affecting both images and
+            text) which will be applied to all pages by default.  A value of
+            100% is the standard Firefox zoom level without NoSquint.  With
+            NoSquint, you can override this value to be larger or smaller.</p>
+
+            <p>Modifying the full page zoom level when visiting a web page will
+            override this value for that site.</p>
+    </li>
 
-        <b>Default primary zoom level</b>
-            <p>This is the zoom level of the primary zoom method as applied to
-            all pages by default.  A value of 100% is the standard Firefox zoom
-            level without NoSquint.  With NoSquint, you can override this value
-            to be larger or smaller.</p>
-
-            <p>Modifying the zoom when visiting a web page will override this
-            value for that site.</p>
+    <li>
+        <b>Default text-only zoom level</b>
+            <p>This is the text zoom level (affecting <i>only</i> text) which
+            will be applied to all pages by default.  A value of 100% is the
+            standard Firefox zoom level without NoSquint.  With NoSquint, you
+            can override this value to be larger or smaller.</p>
+
+            <p>Modifying the text zoom level when visiting a web page will
+            override this value for that site.</p>
     </li>
+
     <li>
         <b>Zoom increment</b>
             <p>You can change the zoom level for a page from the View menu, by
@@ -102,61 +184,63 @@
     </li>
 </ul>
 
-<h3>Site Options</h3>
-
-<p>To NoSquint, a site is a web location where all pages under that location
-have the same zoom level, and the site name is derived from the page's
-URL.</p>
-
-<p>In most cases, the site is the domain.  For example, if the current page is
-<code>www2.ibm.com/index.php</code>, NoSquint will consider the site name to be
-<code>ibm.com</code>.  NoSquint will also take into account common
-second-level domains.  For example, if you're visiting
-<code>www.bbc.co.uk</code>, NoSquint will consider the site name to be
-<code>bbc.co.uk</code>.</p>
+<h2 id='colors'>Colors Tab</h2>
+<p>Sometimes website creators choose to use rather questionable color choices
+which can significantly hinder readability.  NoSquint lets you override the
+standard text colors for <i>all</i> sites here.  Or (probably more usefully),
+you can modify per-site colors via the Site Settings, which can be managed by
+left-clicking on NoSquint's status bar icon, or selecting NoSquint Site
+Settings from the page's context menu.</p>
 
-<p>The default behaviour should work almost all the time.  When it doesn't, you
-can control how NoSquint determines site names in the Exceptions Tab.</p>
+<h3>Text and Background</h3>
+<ul>
+    <li>
+        <b>Text</b>
+            <p>Overrides the foreground color for all text on the page.</p>
+    </li>
+    <li>
+        <b>Background</b>
+            <p>Overrides the background color for the page.</p>
+    </li>
+    <li>
+        <b>Disable background images</b>
+            <p>Some sites may overlay text on background images, rendering the
+            user-selected background color (above) ineffective.  You can disable
+            those images by selecting this options.</p>
+    </li>
+</ul>
 
+<h3>Link Colors</h3>
 <ul>
     <li>
-        <b>Remember zoom level per site</b>
-            <p>With this option selected, NoSquint will remember any changes
-            you make to the zoom levels for a given site.  Both full page zoom
-            and text zoom levels are remembered independently.  Next time you
-            visit that site, NoSquint will change the zoom to the levels
-            previously used on that site.</p>
+        <b>Unvisited</b>
+            <p>The foreground text color to use for all links that have not been visited before.</p>
     </li>
     <li>
-        <b>Forget zoom settings for sites not visited in the last ...</b>
-            <p>With the "remember zoom levels per site" option enabled,
-            NoSquint keeps track of all zoom level changes for sites, even
-            sites you only visit once.  This option is house cleaning: if you
-            haven't visited a site (for which you've set a non-default zoom
-            level) for the specified number of months, NoSquint will forget the
-            setting.</p>
+        <b>Visited</b>
+            <p>The foreground text color to use for all links that <i>have</i> been visited before.</p>
     </li>
     <li>
-        <b>Use the default zoom level for all sites</b>
-            <p>One of NoSquint's features is the ability to remember custom
-            zoom levels for individual sites.  If you're not interested this
-            and want to use the same level for all sites, or you just don't
-            want NoSquint to remember any manual changes, select this
-            option.</p>
+        <b>Always underline links</b>
+            <p>Some websites make it difficult to tell the difference between links and regular text.
+            Selecting this option will force hyperlinks to always be underlined.</p>
     </li>
 </ul>
 
-<h2>Exceptions Tab</h2>
+
+<h2 id='exceptions'>Exceptions Tab</h2>
 
 <p>Because not all web sites are structured the same, sometimes the default
 logic NoSquint uses to determine the site name doesn't work the way you want it
-to.  By way of exceptions, you can control how NoSquint determines what
-constitutes a separate site.</p>
+to.  Using exceptions, you can control how NoSquint determines what constitutes
+a separate site.</p>
 
 <h3>Use Cases</h3>
 <p>Exceptions are powerful and expressive, and unfortunately can be confusing.
-Before going into a detailed explanation, let's first examine some common
-use-cases.  Hopefully one of these examples applies to your case.</p>
+They are specified using a simple custom grammar, <i>not</i> regular
+expressions.  Before going into a detailed explanation, let's first examine
+some common use-cases.  Hopefully one of these examples applies to your
+case.</p>
 
 <ol>
     <li>
@@ -221,7 +305,7 @@ use-cases.  Hopefully one of these examples applies to your case.</p>
         <code>example.com/[*]/apps/*</code>
     </li>
     <li>
-        <b>Problem:</b> same scenario the previous one, but sometimes the
+        <b>Problem:</b> same scenario as the previous one, but sometimes the
         server isn't in the URL, so <code>example.com/apps/app1</code> is
         the same site as <code>example.com/server1/apps/app1</code>.<br/>
 
@@ -255,8 +339,9 @@ computed by NoSquint based on the current page's URL and the user-defined list
 of exceptions.  For instance, both <code>foo.example.com</code> and
 <code>myapp.*.example.com</code> could be site names, depending on the
 exceptions defined.  NoSquint looks up zoom levels based on the site name.  The
-site name, as determined by NoSquint, is by default displayed in the status bar
-beside the current zoom level.</p>
+site name, as determined by NoSquint, can be viewed in the tooltip by hovering
+over the magnifying glass in the status bar, or by opening the Site Settings
+dialog by left clicking the magnifying glass in the status bar.</p>
 
 <p>When a wildcard is enclosed in square brackets (i.e. <code>[*]</code> or
 <code>[**]</code>), the literal wildcard (<code>*</code> or <code>**</code>)
@@ -278,10 +363,24 @@ For example, for <code>www.google.com</code>, a single <code>*</code> matches
 <code>google.com</code>; for <code>www.bbc.co.uk</code> it matches
 <code>bbc.co.uk</code>.</p>
 
-<p>When multiple exceptions match a page's URL, NoSquint will use the exception
-that matches the most non-wildcard characters in the host name.  If there are
-still multiple exceptions in that narrowed list, the exception that matches the
-most non-wildcard characters in the path is then chosen.  If still there are
-multiple exceptions, the first one that matched is chosen.</p>
+<p>Hostname parts can optionally contain a port number, which is delimited from
+the hostname with a <code>:</code> (colon).  For example, <code>*:8080</code> will
+cause port 8080 for all domains to be treated separately, so that
+<code>www.example.com</code> is considered a different site than
+<code>www.example.com:8080</code> (but, assuming no other exceptions than
+this, <code>www.example.com:8001</code> is still considered the same site
+as <code>www.example.com</code> or <code>www.example.com:4242</code>).</p>
+
+<p>Any exception whose host part is the null string – in other words, the
+exception begins with a <code>/</code> (front slash) – is applied only to
+<code>file://</code> URLs.  So the exception <code>/home/*</code> for Linux or
+<code>/C:/Users/*</code> for Windows Vista causes all home directories to be
+treated distinctly from one another.</p>
+
+<p>When multiple exception patterns match a page's URL, NoSquint will use the
+exception that matches the most non-wildcard characters in the host name.  If
+there are still multiple exceptions in that narrowed list, the exception that
+matches the most non-wildcard characters in the path is then chosen.  If still
+there are multiple exceptions, the first one that matched is chosen.</p>
 </body>
 </html>
diff --git a/src/locale/en-US/overlay.dtd b/src/locale/en-US/overlay.dtd
index cdd380c..df25813 100644
--- a/src/locale/en-US/overlay.dtd
+++ b/src/locale/en-US/overlay.dtd
@@ -1,9 +1,16 @@
 <!ENTITY ns.tooltip.site.label "Site Name">
 <!ENTITY ns.tooltip.fullZoom.label "Full Zoom">
 <!ENTITY ns.tooltip.textZoom.label "Text Zoom">
+<!ENTITY ns.tooltip.textColor.label "Text Color">
+<!ENTITY ns.tooltip.linkColor.label "Link Color">
 
 <!ENTITY ns.menu.fullZoom.label "Full Zoom">
 <!ENTITY ns.menu.textZoom.label "Text Zoom">
 <!ENTITY ns.menu.reset.label "Reset Levels">
 <!ENTITY ns.menu.siteSettings.label "Site Settings">
 <!ENTITY ns.menu.globalSettings.label "Global Settings">
+
+<!ENTITY ns.menu.context.label "NoSquint Site Settings">
+<!ENTITY ns.menu.context.accesskey "q">
+
+<!ENTITY ns.sanitize.label "NoSquint Site History">
diff --git a/src/locale/en-US/overlay.properties b/src/locale/en-US/overlay.properties
index 3dfe0ec..98fa412 100644
--- a/src/locale/en-US/overlay.properties
+++ b/src/locale/en-US/overlay.properties
@@ -3,3 +3,13 @@ zoomMenuOutText=Text Zoom Out
 zoomMenuInFull=Full Zoom In
 zoomMenuOutFull=Full Zoom Out
 zoomMenuSettings=Zoom Settings (NoSquint)
+
+siteSpecificTitle=Override Native Per-Site Zooming?
+siteSpecificPrompt=Either another extension or a user action caused Firefox's native per-site zooming to be enabled.  This will cause NoSquint to stop working properly.  Would you like to revert this action so that NoSquint will continue working?
+siteSpecificBrokenTitle=NoSquint Is Broken!
+siteSpecificBrokenPrompt=NoSquint will now no longer work correctly until the browser is restarted.
+
+disableTitle=Restore Native Firefox Behavior?
+disablePrompt=Firefox natively provides less sophisticated per-site zooming, which NoSquint replaces.  Having disabled NoSquint, would you now like to restore Firefox's native behavior?
+
+sanitizeLabel=NoSquint Site History
diff --git a/src/skin/icon-enlarge-16.png b/src/skin/icon-enlarge-16.png
index e1bf549..9a48f6b 100644
Binary files a/src/skin/icon-enlarge-16.png and b/src/skin/icon-enlarge-16.png differ
diff --git a/src/skin/icon-enlarge-24.png b/src/skin/icon-enlarge-24.png
index 54937bd..4a7bad6 100644
Binary files a/src/skin/icon-enlarge-24.png and b/src/skin/icon-enlarge-24.png differ
diff --git a/src/skin/icon-reduce-16.png b/src/skin/icon-reduce-16.png
index 385fec0..34a14c7 100644
Binary files a/src/skin/icon-reduce-16.png and b/src/skin/icon-reduce-16.png differ
diff --git a/src/skin/icon-reduce-24.png b/src/skin/icon-reduce-24.png
index 7543ac4..5b92e15 100644
Binary files a/src/skin/icon-reduce-24.png and b/src/skin/icon-reduce-24.png differ
diff --git a/src/skin/icon-reset-16.png b/src/skin/icon-reset-16.png
new file mode 100644
index 0000000..34eff11
Binary files /dev/null and b/src/skin/icon-reset-16.png differ
diff --git a/src/skin/icon-reset-24.png b/src/skin/icon-reset-24.png
new file mode 100644
index 0000000..8cfc2a1
Binary files /dev/null and b/src/skin/icon-reset-24.png differ
diff --git a/src/skin/icon-statusbar-16.png b/src/skin/icon-statusbar-16.png
new file mode 100644
index 0000000..26a0661
Binary files /dev/null and b/src/skin/icon-statusbar-16.png differ
diff --git a/src/skin/logo-32.png b/src/skin/logo-32.png
new file mode 100644
index 0000000..b465a31
Binary files /dev/null and b/src/skin/logo-32.png differ
diff --git a/src/skin/toolbar.css b/src/skin/toolbar.css
index f9e05fc..c1a8337 100644
--- a/src/skin/toolbar.css
+++ b/src/skin/toolbar.css
@@ -13,3 +13,19 @@ toolbar[iconsize="small"] #nosquint-button-reduce  {
 toolbar[iconsize="small"] #nosquint-button-enlarge {
       list-style-image: url("chrome://nosquint/skin/icon-enlarge-16.png");
 }
+
+#nosquint-button-reset {
+      list-style-image: url("chrome://nosquint/skin/icon-reset-24.png");
+}
+
+toolbar[iconsize="small"] #nosquint-button-reset {
+      list-style-image: url("chrome://nosquint/skin/icon-reset-16.png");
+}
+
+#nosquint-menu-settings, #nosquint-view-menu-settings {
+      list-style-image: url("chrome://nosquint/skin/icon-statusbar-16.png");
+}
+
+#nosquint-status-reset {
+      list-style-image: url("chrome://nosquint/skin/icon-reset-16.png");
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/nosquint.git



More information about the Pkg-mozext-commits mailing list