Import the gloda-facet patch with davida's quicksearch changes and CSS pulled in.

--HG--
branch : gloda-facet
This commit is contained in:
Andrew Sutherland 2009-09-01 11:26:31 -07:00
Родитель 36e13cf9f8
Коммит 559e0a1ab6
52 изменённых файлов: 5389 добавлений и 887 удалений

Просмотреть файл

@ -52,22 +52,33 @@
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<popupset>
<panel type="glodacomplete-richlistbox" chromedir="ltr"
id="PopupGlodaAutocomplete" noautofocus="true" />
</popupset>
<toolbarpalette id="MailToolbarPalette">
<!-- gloda search widget; provides global (message) searching. -->
<toolbaritem id="gloda-search" insertafter="search-container"
title="&glodaSearch.title;"
align="center"
class="chromeclass-toolbar-additional">
<textbox id="glodaSearchInput" flex="1" type="search"
searchbutton="true"
emptytext="&glodaSearchBar.emptyText;"/>
<textbox id="searchInput" flex="1"
chromedir="ltr"
searchCriteria="true"
type="glodacomplete"
searchbutton="true"
autocompletesearch="gloda"
autocompletepopup="PopupGlodaAutocomplete"
>
</textbox>
</toolbaritem>
<toolbaritem id="search-container" insertafter="button-stop"
title="&searchItem.title;"
align="center"
class="chromeclass-toolbar-additional">
<textbox id="searchInput" timeout="800" flex="1"
<textbox id="old_searchInput" timeout="800" flex="1"
onfocus="onSearchInputFocus(event);"
onclick="onSearchInputClick(event);"
onmousedown="onSearchInputMousedown(event);"

Просмотреть файл

@ -783,12 +783,12 @@ FolderDisplayWidget.prototype = {
* of searches and we will receive a notification for them.
*/
onSearching: function(aIsSearching) {
// getDocumentElements() sets gSearchBundle
getDocumentElements();
if (this._tabInfo)
if (this._tabInfo) {
let searchBundle = document.getElementById("bundle_search");
document.getElementById("tabmail").setTabThinking(
this._tabInfo,
aIsSearching && gSearchBundle.getString("searchingMessage"));
aIsSearching && searchBundle.getString("searchingMessage"));
}
},
/**
@ -892,8 +892,6 @@ FolderDisplayWidget.prototype = {
this._persistColumnStates(this._savedColumnStates);
}
// the quick-search gets nuked when we show a new folder
ClearQSIfNecessary();
// update the quick-search relative to whether it's incoming/outgoing
onSearchFolderTypeChanged(this.view.isOutgoingFolder);
@ -1438,9 +1436,6 @@ FolderDisplayWidget.prototype = {
searchInput.showingSearchCriteria = false;
searchInput.clearButtonHidden = false;
}
else {
searchInput.setSearchCriteriaText();
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
#query-explanation {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#query-explanation');
}
.facets {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facets');
}
.facetious[type="discrete"] {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-discrete');
}
.facetious[type="boolean"] {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-boolean');
}
.facetious[type="boolean-filtered"] {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-boolean-filtered');
}
.facetious[type="date"] {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-date');
}
.results[type="message"] {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#results-message');
}
.message {
-moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#result-message');
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,106 @@
Components.utils.import("resource://app/modules/StringBundle.js");
Components.utils.import("resource://app/modules/gloda/facet.js");
// needed by search.xml to use us
Components.utils.import("resource://app/modules/gloda/msg_search.js");
var glodaFacetTabType = {
name: "glodaFacet",
perTabPanel: "iframe",
strings:
new StringBundle("chrome://messenger/locale/glodaFacetView.properties"),
modes: {
glodaFacet: {
// this is what get exposed on the tab for icon purposes
type: "glodaSearch"
}
},
openTab: function glodaFacetTabType_openTab(aTab, aArgs) {
// we have no browser until our XUL document loads
aTab.browser = null;
if ("query" in aArgs) {
aTab.query = aArgs.query;
aTab.collection = aTab.query.getCollection();
aTab.title = this.strings.get("glodaFacetView.tab.query.label");
aTab.searchString = null;
}
else if ("searcher" in aArgs) {
aTab.searcher = aArgs.searcher;
aTab.collection = aTab.searcher.getCollection();
aTab.query = aTab.searcher.query;
let searchString = aTab.searcher.searchString;
aTab.title = aTab.glodaSearchInputValue = aTab.searchString =
searchString;
}
else if ("collection" in aArgs) {
aTab.collection = aArgs.collection;
aTab.title = this.strings.get("glodaFacetView.tab.query.label");
aTab.searchString = null;
}
function xulLoadHandler() {
aTab.panel.contentWindow.removeEventListener("load", xulLoadHandler,
false);
aTab.panel.contentWindow.tab = aTab;
aTab.browser = aTab.panel.contentDocument.getElementById("browser");
aTab.browser.setAttribute("src",
"chrome://messenger/content/glodaFacetView.xhtml");
}
aTab.panel.contentWindow.addEventListener("load", xulLoadHandler, false);
aTab.panel.setAttribute("src",
"chrome://messenger/content/glodaFacetViewWrapper.xul");
},
closeTab: function glodaFacetTabType_closeTab(aTab) {
},
saveTabState: function glodaFacetTabType_saveTabState(aTab) {
// nothing to do; we are not multiplexed
},
showTab: function glodaFacetTabType_showTab(aTab) {
// nothing to do; we are not multiplexed
},
getBrowser: function(aTab) {
return aTab.browser;
}
};
/**
* The glodaSearch tab mode has a UI widget outside of the mailTabType's
* display panel, the #glodaSearchInput textbox. This means we need to use a
* tab monitor so that we can appropriately update the contents of the textbox.
* Every time a tab is changed, we save the state of the text box and restore
* its previous value for the tab we are switching to, as well as whether this
* value is a change to the currently-used value (if it is a glodaSearch) tab.
* The behaviour rationale for this is that the glodaSearchInput is like the
* URL bar. When you are on a glodaSearch tab, we need to show you your
* current value, including any "uncommitted" (you haven't hit enter yet)
* changes. It's not entirely clear that imitating this behaviour on
* non-glodaSearch tabs makes a lot of sense, but it is consistent, so we do
* so. The counter-example to this choice is the search box in firefox, but
* it never updates when you switch tabs, so it is arguably less of a fit.
*/
var glodaFacetTabMonitor = {
onTabTitleChanged: function() {},
onTabSwitched: function glodaFacetTabMonitor_onTabSwitch(aTab, aOldTab) {
let inputNode = document.getElementById("glodaSearchInput");
if (!inputNode)
return;
// save the current search field value
if (aOldTab)
aOldTab.glodaSearchInputValue = inputNode.value;
// load (or clear if there is none) the persisted search field value
inputNode.value = aTab.glodaSearchInputValue || "";
// If the mode is glodaSearch and the search is unchanged, then we want to
// set the icon state of the input box to be the 'clear' icon.
if (aTab.mode.name == "glodaFacet") {
if (aTab.searchString == aTab.glodaSearchInputValue)
inputNode._searchIcons.selectedIndex = 1;
}
}
};

Просмотреть файл

@ -0,0 +1,491 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Thunderbird Global Database.
*
* The Initial Developer of the Original Code is
* Mozilla Messaging, Inc.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Andrew Sutherland <asutherland@asutherland.org>
* David Ascher <dascher@mozillamessaging.com>
* Bryan Clark <clarkbw@gnome.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
body {
background: #ffffff;
font-family: sans-serif;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
font-family: sans-serif;
}
/* ===== Query Explanation ===== */
#query-explanation {
display: block;
position: absolute;
top: 0;
left: 20.5em;
height: 2.5em;
font-size: 110%;
margin-left: 0;
padding: 2px;
padding-left: 0;
padding-top: 1em;
font-size: medium;
}
.explanation-fulltext-label {
font-size: 120%;
color: #3465a4;
margin: 0 0.1em;
}
.explanation-fulltext-term {
font-size: large;
color: black;
margin: 0 0.1em;
}
.explanation-fulltext-criteria {
font-size: medium;
color: #888;
margin: 0 0.1em;
}
.explanation-change-label {
font-size: 80%;
color: black;
margin: 0 0.1em;
}
.explanation-query-label {
margin-top: 1ex;
}
.explanation-query-label,
.explanation-query-involves,
.explanation-query-tagged {
color: #3465a4;
margin-right: 0.5ex;
}
/* ===== Facets ===== */
h1, h2, h3 {
color: #3465a4;
}
#filter-header-label {
font-size: 120%;
margin: 0;
margin-top: 1em;
}
.explanation-change-label {
margin-left: 1em;
font-size: 80%;
border: 1px solid grey;
-moz-border-radius: 4px;
padding: 3px;
}
.explanation-change-label:hover {
cursor: pointer;
background-color: #aed5ff;
}
.facetious[uninitialized] {
display: none;
}
.facets-sidebar {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 20em;
height: 100%;
background-color: #eeeeee;
padding: 4px;
padding-left: 1em;
font-size: 90%;
}
.facetious {
display: inline-block;
padding: 2px;
}
.facet > h2 {
display: inline;
margin: 0;
font-size: medium;
}
.facet-included[state="empty"],
.facet-excluded[state="empty"] {
display: none;
}
#facet-date {
position: fixed;
bottom: -4px;
left: 20em;
padding: 0px;
background-color: rgba(255,255,255,0.6);
display: block;
}
/* === Boolean Facet === */
.facetious[type="boolean"][disabled] {
color: #888888;
}
.facet-checkbox-bubble {
padding: 2px;
padding-right: 6px;
border: solid transparent 1px;
cursor: pointer;
color: #333;
font-size: 90%;
}
.facetious[type="boolean"][disabled] > .facet-checkbox-bubble,
.facetious[type="boolean-filtered"][disabled] > .facet-checkbox-bubble {
cursor: default;
color: #777;
}
.facetious[type="boolean"]:not([disabled]):hover > .facet-checkbox-bubble,
.facetious[type="boolean-filtered"]:not([disabled]):hover > .facet-checkbox-bubble {
background-color: #aed5ff;
border: solid #3465a4 1px;
-moz-border-radius: 4px;
}
.facet-checkbox-label {
}
.facet-checkbox-count {
}
.facet-checkbox-count:before {
content: " (";
}
.facet-checkbox-count:after {
content: ")";
}
/* === Boolean Filtered === */
.facetious[type="boolean-filtered"]:not([checked]) > .facet-filter-list {
display: none
}
.facet-filter-list {
display: block;
}
/* === Discrete Facet === */
/*
* Our basis for the bar-chart comes from:
* http://www.alistapart.com/articles/accessibledatavisualization/
* Thank you, Wilson Miner.
*/
.barry {
border-top: 1px solid #EEE;
margin: 0;
padding: 4px;
}
.facet-modes {
font-size: 80%;
cursor: pointer;
color: #888888;
}
.facet-modes:before {
font-weight: normal;
content: "( ";
}
.facet-mode:not(:first-child):before {
font-weight: normal;
content: " | ";
}
.facet-modes:after {
font-weight: normal;
content: " )";
}
.facet-mode[selected] {
font-weight: bold;
}
.bar {
position: relative;
display: block;
border-bottom: 1px solid #EEE;
_zoom: 1;
cursor: pointer;
font-size: 80%;
}
.bar[included] {
background-color: #efffef;
}
.bar[excluded] {
text-decoration: line-through;
}
.bar:hover {
background: #e8e8e8;
}
.bar-link {
color: #2D7BB2;
text-decoration: none;
display: block;
padding: 0.3em 2em 0.3em 0.5em;
position: relative;
z-index: 2;
}
.bar:hover > .bar-link {
color: #333;
}
.bar-percent {
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
background: #e9f2f5;
text-indent: -9999px;
overflow: hidden;
line-height: 1.6em;
}
.bar:hover > .bar-percent {
background: #ceeaf5;
}
.bar-exclude {
display: block;
visibility: hidden;
background-image: url("chrome://messenger/skin/icons/thread-ignored.png");
position: absolute;
top: 0.3em;
right: 2px;
width: 16px;
height: 16px;
z-index: 3;
}
.bar:hover > .bar-exclude {
visibility: visible;
}
.bar-exclude:hover {
background-color: #ffffcc;
}
/* ===== Results ===== */
.results {
margin-left: 20.5em;
margin-top: 3em;
}
.results-message-header {
background-color: #dcddde;
border-top: 2px solid #ccc;
padding: 2px;
}
.results-message-count {
display: inline;
margin: 0;
font-size: medium;
}
.results-message-showall {
margin-left: 1em;
cursor: pointer;
font-size: 80%;
}
/* ===== Messages ===== */
.message {
display: block;
font-family: sans-serif;
font-size: 80%;
padding-top: 3px;
padding-bottom: 3px;
margin-right: 1em;
border: 1px solid transparent;
-moz-border-radius: 3px;
border-bottom: 1px solid #ddd;
display: block;
color: #555;
background-color: #ffffff;
}
.message:hover {
border-color: grey;
background: #efefef;
cursor: pointer;
}
.message:focus {
border: 1px dotted #111;
padding: 1em 0px;
}
.message[unread="true"]:focus {
border: 1px dotted #111;
padding: 1em 0px;
}
.message:focus:last-child {
border: 1px dotted #111;
padding: 1em 0px;
}
.message:focus:first-child {
border: 1px dotted #111;
padding: 1em 0px;
}
.message:last-child {
border-bottom: 1px solid transparent;
}
.message:last-child:hover {
border-bottom: 1px solid;
}
.message {
display: block;
padding: 0.2em 0em;
padding-right: 1em;
}
.message-header,
.message-body {
margin-left: 24px;
font-size: 95%;
}
.message-header {
margin-bottom: 0.5em;
}
.message-meta {
float: right;
padding-left: 2em;
text-align: right;
color: #999;
font-size: 90%;
}
.message-attachments {
padding-right: 18px;
background: url("chrome://messenger/skin/icons/attachment.png") transparent no-repeat center right;
display: none;
}
.message-attachments[count] {
display: inline;
}
.message-attachments:before {
content: "(";
}
.message-attachments:after {
content: ")";
}
.message-writes {
font-size: 90%;
color: #777;
}
.message-date {
color: #999; font-size: 90%; }
.message-date:before {
content: "\2014 ";
}
.message-recipients {
display: inline;
color: #222;
}
.message-recipient:first-child:before {
content: "";
}
.message-recipient:after {
content: ", ";
}
.message-recipient:last-child:after {
content: "";
}
.message-subject {
white-space: nowrap;
overflow: hidden;
font-size: 115%;
font-weight: bold;
color: #555;
}
.message-body {
color: #555;
padding-left: 1em;
font-family: monospace;
font-size: 110%;
}
.message-tag {
-moz-margin-start: 0px;
background-image: url("chrome://messenger/skin/tagbg.png");
color: black;
-moz-border-radius: 2px;
padding: 1px 3px;
margin-right: 3px;
}
.message-folder {
background-color: #faf0b8;
border: 1px solid #ede4af;
}

Просмотреть файл

@ -0,0 +1,489 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Thunderbird Global Database.
*
* The Initial Developer of the Original Code is
* Mozilla Messaging, Inc.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Andrew Sutherland <asutherland@asutherland.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
* This file provides the global context for the faceting environment. In the
* Model View Controller (paradigm), we are the view and the XBL widgets are
* the the view and controller.
*
* Because much of the work related to faceting is not UI-specific, we try and
* push as much of it into mailnews/db/gloda/facet.js. In some cases we may
* get it wrong and it may eventually want to migrate.
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://app/modules/gloda/log4moz.js");
Cu.import("resource://app/modules/StringBundle.js");
Cu.import("resource://app/modules/PluralForm.jsm");
Cu.import("resource://app/modules/errUtils.js");
Cu.import("resource://app/modules/gloda/public.js");
Cu.import("resource://app/modules/gloda/facet.js");
const glodaFacetStrings =
new StringBundle("chrome://messenger/locale/glodaFacetView.properties");
/**
*
*/
function ActiveConstraint(aFaceter, aAttrDef, aInclusive, aGroupValues,
aRanged) {
this.faceter = aFaceter;
this.attrDef = aAttrDef;
this.inclusive = aInclusive;
this.ranged = Boolean(aRanged);
this.groupValues = aGroupValues;
this._makeQuery();
}
ActiveConstraint.prototype = {
_makeQuery: function() {
// have the faceter make the query and the invert decision for us if it
// implements the makeQuery method.
if ("makeQuery" in this.faceter) {
[this.query, this.invertQuery] = this.faceter.makeQuery(this.groupValues,
this.inclusive);
return;
}
let query = this.query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
let constraintFunc;
// If the facet definition references a queryHelper defined by the noun
// type, use that instead of the standard constraint function.
if ("queryHelper" in this.attrDef.facet)
constraintFunc = query[this.attrDef.boundName +
this.attrDef.facet.queryHelper];
else
constraintFunc = query[this.ranged ? (this.attrDef.boundName + "Range")
: this.attrDef.boundName];
constraintFunc.apply(query, this.groupValues);
this.invertQuery = !this.inclusive;
},
/**
* Adjust the constraint given the incoming faceting constraint desired.
* Mainly, if the inclusive flag is the same as what we already have, we
* just append the new values to the existing set of values. If it is not
* the same, we replace them.
*/
adjust: function(aInclusive, aGroupValues) {
if (aInclusive == this.inclusive) {
this.groupValues = this.groupValues.concat(aGroupValues);
this._makeQuery();
return;
}
this.inclusive = aInclusive;
this.groupValues = aGroupValues;
this._makeQuery();
},
/**
* Replace the existing constraints with the new constraint.
*/
replace: function(aInclusive, aGroupValues) {
this.inclusive = aInclusive;
this.groupValues = aGroupValues;
this._makeQuery();
},
/**
* Filter the items against our constraint.
*/
sieve: function(aItems) {
let query = this.query;
let expectedResult = !this.invertQuery;
let outItems = [];
for each (let [, item] in Iterator(aItems)) {
if (query.test(item) == expectedResult)
outItems.push(item);
}
return outItems;
}
};
var FacetContext = {
facetDriver: new FacetDriver(Gloda.lookupNounDef("message"),
window),
/**
* The root collection which our active set is a subset of. We hold onto this
* for garbage collection reasons, although the tab that owns us should also
* be holding on.
*/
_collection: null,
set collection(aCollection) {
this._collection = aCollection;
},
get collection() {
return this._collection;
},
/**
* List of the current working set
*/
_activeSet: null,
get activeSet() {
return this._activeSet;
},
initialBuild: function() {
let queryExplanation = document.getElementById("query-explanation");
if (this.searcher)
queryExplanation.setFulltext(this.searcher);
else
queryExplanation.setQuery(this.collection.query);
// we like to sort them so should clone the list
this.faceters = this.facetDriver.faceters.concat();
this.everFaceted = false;
this.build(this._collection.items);
},
build: function(aNewSet) {
this._activeSet = aNewSet;
this.facetDriver.go(this._activeSet, this.facetingCompleted, this);
},
/**
* Attempt to figure out a reasonable number of rows to limit each facet to
* display. While the number will ordinarily be dominated by the maximum
* number of rows we believe the user can easily scan, this may also be
* impacted by layout concerns (since we want to avoid scrolling).
*/
planLayout: function() {
// XXX arbitrary!
this.maxDisplayRows = 8;
this.maxMessagesToShow = 8;
},
_groupCountComparator: function(a, b) {
return b.groupCount - a.groupCount;
},
/**
* Tells the UI about all the facets when notified by the |facetDriver| when
* it is done faceting everything.
*/
facetingCompleted: function() {
this.planLayout();
let uiFacets = document.getElementById("facets");
if (!this.everFaceted) {
this.everFaceted = true;
this.faceters.sort(this._groupCountComparator);
for each (let [, faceter] in Iterator(this.faceters)) {
let attrName = faceter.attrDef.attributeName;
let explicitBinding = document.getElementById("facet-" + attrName);
if (explicitBinding) {
explicitBinding.faceter = faceter;
explicitBinding.attrDef = faceter.attrDef;
explicitBinding.nounDef = faceter.attrDef.objectNounDef;
explicitBinding.orderedGroups = faceter.orderedGroups;
// explicit booleans should always be displayed for consistency
if (faceter.groupCount >= 1 ||
faceter.type == "boolean") {
explicitBinding.build(true);
explicitBinding.removeAttribute("uninitialized");
}
faceter.xblNode = explicitBinding;
continue;
}
// ignore facets that do not vary!
if (faceter.groupCount <= 1) {
faceter.xblNode = null;
continue;
}
faceter.xblNode = uiFacets.addFacet(faceter.type, faceter.attrDef, {
faceter: faceter,
orderedGroups: faceter.orderedGroups,
maxDisplayRows: this.maxDisplayRows,
});
}
}
else {
for each (let [, faceter] in Iterator(this.faceters)) {
// Do not bother with un-displayed facets, or that are locked by a
// constraint. But do bother if the widget can be updated without
// losing important data.
if (!faceter.xblNode ||
(faceter.constraint && !faceter.xblNode.canUpdate))
continue;
// hide things that have 0/1 groups now and are not constrained and not
// boolean
if (faceter.groupCount <= 1 && !faceter.constraint &&
(faceter.type != "boolean"))
$(faceter.xblNode).hide();
// otherwise, update
else {
faceter.xblNode.orderedGroups = faceter.orderedGroups;
faceter.xblNode.build(false);
$(faceter.xblNode).show();
}
}
}
let results = document.getElementById("results");
let numMessageToShow = Math.min(this.maxMessagesToShow,
this._activeSet.length);
results.setMessages(this._activeSet.slice(0, numMessageToShow));
},
_HOVER_STABILITY_DURATION_MS: 100,
_brushedFacet: null,
_brushedGroup: null,
_brushedItems: null,
_brushTimeout: null,
hoverFacet: function(aFaceter, aAttrDef, aGroupValue, aGroupItems) {
// bail if we are already brushing this item
if (this._brushedFacet == aFaceter && this._brushedGroup == aGroupValue)
return;
this._brushedFacet = aFaceter;
this._brushedGroup = aGroupValue;
this._brushedItems = aGroupItems;
if (this._brushTimeout != null)
clearTimeout(this._brushTimeout);
this._brushTimeout = setTimeout(this._timeoutHoverWrapper,
this._HOVER_STABILITY_DURATION_MS, this);
},
_timeoutHover: function() {
this._brushTimeout = null;
for each (let [, faceter] in Iterator(this.faceters)) {
if (faceter == this._brushedFacet || !faceter.xblNode)
continue;
if (this._brushedItems != null)
faceter.xblNode.brushItems(this._brushedItems);
else
faceter.xblNode.clearBrushedItems();
}
},
_timeoutHoverWrapper: function(aThis) {
aThis._timeoutHover();
},
unhoverFacet: function(aFaceter, aAttrDef, aGroupValue, aGroupItems) {
// have we already brushed from some other source already? ignore then.
if (this._brushedFacet != aFaceter || this._brushedGroup != aGroupValue)
return;
// reuse hover facet to null everyone out
this.hoverFacet(null, null, null, null);
},
/**
* Maps attribute names to their corresponding |ActiveConstraint|, if they
* have one.
*/
_activeConstraints: {},
/**
* Called by facets when the user does some clicking and wants to impose a new
* constraint.
*
* @param aFaceter
* @param aAttrDef
* @param {Boolean} aInclusive
* @param aGroupValues
* @param aRanged Is it a ranged constraint? (Currently only for dates)
* @param aNukeExisting Do we need to replace the existing constraint and
* re-sieve everything? This currently only happens for dates, where
* our display allows a click to actually make our range more generic
* than it currently is. (But this only matters if we already have
* a date constraint applied.)
*/
addFacetConstraint: function(aFaceter, aAttrDef, aInclusive, aGroupValues,
aRanged, aNukeExisting) {
let attrName = aAttrDef.attributeName;
let constraint;
let needToSieveAll = false;
if (attrName in this._activeConstraints) {
constraint = this._activeConstraints[attrName];
needToSieveAll = true;
if (aNukeExisting)
constraint.replace(aInclusive, aGroupValues);
else
constraint.adjust(aInclusive, aGroupValues);
}
else {
constraint = this._activeConstraints[attrName] =
new ActiveConstraint(aFaceter, aAttrDef, aInclusive, aGroupValues,
aRanged);
}
aFaceter.constraint = constraint;
// Given our current implementation, we can only be further constraining our
// active set, so we can just sieve the existing active set with the
// (potentially updated) constraint. In some cases, it would be much
// cheaper to use the facet's knowledge about the items in the groups, but
// for now let's keep a single code-path for how we refine the active set.
this.build(needToSieveAll ? this._sieveAll()
: constraint.sieve(this.activeSet));
},
removeFacetConstraint: function(aFaceter) {
let attrName = aFaceter.attrDef.attributeName;
delete this._activeConstraints[attrName];
aFaceter.constraint = null;
// we definitely need to re-sieve everybody in this case...
this.build(this._sieveAll());
},
/**
* Sieve the items from the underlying collection against all constraints,
* returning the value.
*/
_sieveAll: function() {
let items = this.collection.items;
for each (let [, constraint] in Iterator(this._activeConstraints)) {
items = constraint.sieve(items);
}
return items;
},
toggleFulltextCriteria: function() {
this.tab.searcher.andTerms = !this.tab.searcher.andTerms;
this.collection = this.tab.searcher.getCollection(this);
},
/**
* Show the active message set in a glodaList tab, closing the current tab.
*/
showActiveSetInTab: function() {
let tabmail = this.rootWin.document.getElementById("tabmail");
tabmail.openTab("glodaList", {
collection: Gloda.explicitCollection(Gloda.NOUN_MESSAGE, this.activeSet),
title: this.tab.title
});
tabmail.closeTab(this.tab);
},
/**
* Show the conversation in a new glodaList tab.
*
* @param {GlodaConversation} aConversation The conversation to show.
* @param {Boolean} [aBackground] Whether it should be in the background.
*/
showConversationInTab: function(aMessage, aBackground) {
let tabmail = this.rootWin.document.getElementById("tabmail");
tabmail.openTab("glodaList", {
conversation: aMessage.conversation,
message: aMessage,
title: aMessage.conversation.subject,
background: aBackground
});
},
/**
* Show the message in a new tab.
*
* @param {GlodaMessage} aMessage The message to show.
* @param {Boolean} [aBackground] Whether it should be in the background.
*/
showMessageInTab: function(aMessage, aBackground) {
let tabmail = this.rootWin.document.getElementById("tabmail");
let msgHdr = aMessage.folderMessage;
if (!msgHdr)
throw new Error("Unable to translate gloda message to message header.");
tabmail.openTab("message", {
msgHdr: msgHdr,
background: aBackground
});
},
onItemsAdded: function(aItems, aCollection) {
},
onItemsModified: function(aItems, aCollection) {
},
onItemsRemoved: function(aItems, aCollection) {
},
onQueryCompleted: function(aCollection) {
this.initialBuild();
}
};
/**
* addEventListener betrayals compel us to establish our link with the
* outside world from inside. NeilAway suggests the problem might have
* been the registration of the listener prior to initiating the load. Which
* is odd considering it works for the XUL case, but I could see how that might
* differ. Anywho, this works for now and is a delightful reference to boot.
*/
function reachOutAndTouchFrame() {
let us = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem);
FacetContext.rootWin = us.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let parentWin = us.parent
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let aTab = FacetContext.tab = parentWin.tab;
parentWin.tab = null;
// we need to hook the context up as a listener in all cases since
// removal notifications are required.
if ("searcher" in aTab) {
FacetContext.searcher = aTab.searcher;
aTab.searcher.listener = FacetContext;
}
else {
FacetContext.searcher = null;
aTab.collection.listener = FacetContext;
}
FacetContext.collection = aTab.collection;
// if it has already completed, we need to prod things
if (aTab.query.completed)
FacetContext.initialBuild();
}

Просмотреть файл

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
%brandDTD;
<!ENTITY % aboutDTD SYSTEM "chrome://global/locale/about.dtd" >
%aboutDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % facetViewDTD SYSTEM "chrome://messenger/locale/glodaFacetView.dtd">
%facetViewDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
version="-//W3C//DTD XHTML 1.1//EN" xml:lang="en">
<head>
<!-- XBL bindings CSS -->
<link rel="stylesheet"
href="chrome://messenger/content/glodaFacetBindings.css"
type="text/css"></link>
<link rel="stylesheet" media="screen" type="text/css"
href="chrome://messenger/skin/tagColors.css"/>
<!-- Themes -->
<link rel="stylesheet"
href="chrome://messenger/content/glodaFacetView.css"
type="text/css"></link>
<!-- Global Context -->
<script type="application/javascript;version=1.8"
src="chrome://messenger/content/glodaFacetView.js"></script>
<!-- Libs -->
<script type="application/javascript;version=1.8"
src="chrome://messenger/content/jquery.js"></script>
<script type="application/javascript;version=1.8"
src="chrome://messenger/content/protovis-r2.6-modded.js"></script>
<!-- Facet Binding Stuff that doesn't belong in XBL -->
<script type="application/javascript;version=1.8"
src="chrome://messenger/content/glodaFacetVis.js"></script>
</head>
<body onload="reachOutAndTouchFrame()">
<div class="facets facets-sidebar" id="facets">
<h1 id="filter-header-label">&glodaFacetView.filters.label;</h1>
<div id="facet-fromMe" class="facetious" type="boolean" attr="fromMe"
uninitialized="true" />
<div id="facet-toMe" class="facetious" type="boolean" attr="toMe"
uninitialized="true" />
<div id="facet-star" class="facetious" type="boolean" attr="star"
uninitialized="true"/><br />
<div id="facet-attachmentTypes" class="facetious" type="boolean-filtered"
attr="attachmentTypes"
groupDisplayProperty="categoryLabel"
uninitialized="true"/>
</div>
<div id="query-explanation" />
<div class="results" id="results" type="message" />
<div id="facet-date" class="facetious" type="date" />
</body>
</html>

Просмотреть файл

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<window id="window" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript"
src="chrome://global/content/viewZoomOverlay.js"/>
<script type="application/javascript;version=1.8"><![CDATA[
function getBrowser() {
return document.getElementById('browser');
}
]]></script>
<commandset id="selectEditMenuItems">
<command id="cmd_fullZoomReduce" oncommand="ZoomManager.reduce();"/>
<command id="cmd_fullZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
<command id="cmd_fullZoomReset" oncommand="ZoomManager.reset();"/>
</commandset>
<keyset>
<!--move to locale-->
<key id="key_fullZoomEnlarge" key="+"
command="cmd_fullZoomEnlarge" modifiers="accel"/>
<key id="key_fullZoomEnlarge2" key="="
command="cmd_fullZoomEnlarge" modifiers="accel"/>
<key id="key_fullZoomReduce" key="-"
command="cmd_fullZoomReduce" modifiers="accel"/>
<key id="key_fullZoomReset" key="0"
command="cmd_fullZoomReset" modifiers="accel"/>
</keyset>
<browser id="browser"
flex="1" />
</window>

Просмотреть файл

@ -0,0 +1,407 @@
/****** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Thunderbird Global Database.
*
* The Initial Developer of the Original Code is
* Mozilla Messaging, Inc.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Andrew Sutherland <asutherland@asutherland.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
* Facet visualizations that would be awkward in XBL. Allegedly because the
* interaciton idiom of a protovis-based visualization is entirely different
* from XBL, but also a lot because of the lack of good syntax highlighting.
*/
/**
* A date facet visualization abstraction.
*/
function DateFacetVis(aBinding, aCanvasNode) {
this.binding = aBinding;
this.canvasNode = aCanvasNode;
this.faceter = aBinding.faceter;
this.attrDef = this.faceter.attrDef;
}
DateFacetVis.prototype = {
build: function() {
this.allowedSpace = document.documentElement.clientWidth -
this.canvasNode.getBoundingClientRect().left;
this.render();
},
rebuild: function() {
this.render();
},
_MIN_BAR_SIZE_PX: 9,
_BAR_SPACING_PX: 1,
_MAX_BAR_SIZE_PX: 15,
_AXIS_FONT: "10px sans-serif",
_AXIS_HEIGHT_NO_LABEL_PX: 6,
_AXIS_HEIGHT_WITH_LABEL_PX: 14,
_AXIS_VERT_SPACING_PX: 1,
_AXIS_HORIZ_MIN_SPACING_PX: 4,
_MAX_DAY_COUNT_LABEL_DISPLAY: 10,
/**
* Figure out how to chunk things given the linear space in pixels. In an
* ideal world we would not use pixels, avoiding tying ourselves to assumed
* pixel densities, but we do not live there. Reality wants crisp graphics
* and does not have enough pixels that you can ignore the pixel coordinate
* space and have things still look sharp (and good).
*
* Because of our love of sharpness, we will potentially under-use the space
* allocated to us.
*
* @param aPixels The number of linear content pixels we have to work with.
* You are in charge of the borders and such, so you subtract that off
* before you pass it in.
* @return An object with attributes:
*/
makeIdealScaleGivenSpace: function(aPixels) {
let facet = this.faceter;
// build a scale and have it grow the edges based on the span
let scale = pv.Scales.dateTime(facet.oldest, facet.newest);
const Span = pv.Scales.DateTimeScale.Span;
const MS_MIN = 60*1000, MS_HOUR = 60*MS_MIN, MS_DAY = 24*MS_HOUR,
MS_WEEK = 7*MS_DAY, MS_MONTHISH = 31*MS_DAY, MS_YEARISH = 366*MS_DAY;
const roughMap = {};
roughMap[Span.DAYS] = MS_DAY;
roughMap[Span.WEEKS] = MS_WEEK;
// we overestimate since we want to slightly underestimate pixel usage
// in enoughPix's rough estimate
roughMap[Span.MONTHS] = MS_MONTHISH;
roughMap[Span.YEARS] = MS_YEARISH;
const minBarPix = this._MIN_BAR_SIZE_PX + this._BAR_SPACING_PX;
let delta = facet.newest.valueOf() - facet.oldest.valueOf();
let span, rules, barPixBudget;
// evil side-effect land
function enoughPix(aSpan) {
span = aSpan;
// do a rough guestimate before doing something potentially expensive...
barPixBudget = Math.floor(aPixels / (delta / roughMap[span]));
if (barPixBudget < (minBarPix + 1))
return false;
rules = scale.ruleValues(span);
// + 0 because we want to over-estimate slightly for niceness rounding
// reasons
barPixBudget = Math.floor(aPixels / (rules.length + 0));
delta = scale.max().valueOf() - scale.min().valueOf();
return barPixBudget > minBarPix;
}
// day is our smallest unit
const ALLOWED_SPANS = [Span.DAYS, Span.WEEKS, Span.MONTHS, Span.YEARS];
for each (let [, trySpan] in Iterator(ALLOWED_SPANS)) {
if (enoughPix(trySpan)) {
// do the equivalent of nice() for our chosen span
scale.min(scale.round(scale.min(), trySpan, false));
scale.max(scale.round(scale.max(), trySpan, true));
// try again for paranoia, but mainly for the side-effect...
if (enoughPix(trySpan))
break;
}
}
// - Figure out our labeling strategy
// normalize the symbols into an explicit ordering
let spandex = ALLOWED_SPANS.indexOf(span);
// from least-specific to most-specific
let labelTiers = [];
// add year spans in all cases, although whether we draw bars depends on if
// we are in year mode or not
labelTiers.push({
rules: (span == Span.YEARS) ? rules : scale.ruleValues(Span.YEARS, true),
label: ["%Y", "%y", null], // we should not hit the null case...
boost: (span == Span.YEARS),
noFringe: (span == Span.YEARS)
});
// add month spans if we are days or weeks...
if (spandex < 2) {
labelTiers.push({
rules: scale.ruleValues(Span.MONTHS, true),
// try to use the full month, falling back to the short month
label: ["%B", "%b", null],
boost: false
});
}
// add week spans if our granularity is days...
if (span == Span.DAYS) {
let numDays = delta / MS_DAY;
// find out how many days we are talking about and add days if it's small
// enough, display both the date and the day of the week
if (numDays <= this._MAX_DAY_COUNT_LABEL_DISPLAY) {
labelTiers.push({
rules: rules,
label: ["%d", null],
boost: true, noFringe: true
});
labelTiers.push({
rules: rules,
label: ["%a", null],
boost: true, noFringe: true
});
}
// show the weeks since we're at greater than a day time-scale
else {
labelTiers.push({
rules: scale.ruleValues(Span.WEEKS, true),
// labeling weeks is nonsensical; no one understands ISO weeks
// numbers.
label: [null],
boost: false
});
}
}
return {
scale: scale, span: span, rules: rules, barPixBudget: barPixBudget,
labelTiers: labelTiers
};
},
render: function() {
let {scale: scale, span: span, rules: rules, barPixBudget: barPixBudget,
labelTiers: labelTiers} =
this.makeIdealScaleGivenSpace(this.allowedSpace);
barPixBudget = Math.floor(barPixBudget);
let minBarPix = this._MIN_BAR_SIZE_PX + this._BAR_SPACING_PX;
let maxBarPix = this._MAX_BAR_SIZE_PX + this._BAR_SPACING_PX;
let barPix = Math.max(minBarPix, Math.min(maxBarPix, barPixBudget));
let width = barPix * (rules.length - 1);
let totalAxisLabelHeight = 0;
// we need to do some font-metric calculations, so create a canvas...
let fontMetricCanvas = document.createElement("canvas");
let ctx = fontMetricCanvas.getContext("2d");
// do the labeling logic,
for each (let [, labelTier] in Iterator(labelTiers)) {
let labelRules = labelTier.rules;
let perLabelBudget = width / (labelRules.length - 1);
for each (let [, labelFormat] in Iterator(labelTier.label)) {
let maxWidth = 0;
let displayValues = [];
for (let iRule = 0; iRule < labelRules.length - 1; iRule++) {
// is this at the either edge of the display? in that case, it might
// be partial...
let fringe = (labelRules.length > 2) &&
((iRule == 0) || (iRule == labelRules.length - 2));
let labelStartDate = labelRules[iRule];
let labelEndDate = labelRules[iRule + 1];
let labelText = labelFormat ?
labelStartDate.toLocaleFormat(labelFormat) : null;
let labelStartNorm = Math.max(0, scale.normalize(labelStartDate));
let labelEndNorm = Math.min(1, scale.normalize(labelEndDate));
let labelBudget = (labelEndNorm - labelStartNorm) * width;
if (labelText) {
let labelWidth = ctx.measureText(labelText).width;
// discard labels at the fringe who don't fit in our budget
if (fringe && !labelTier.noFringe && labelWidth > labelBudget)
labelText = null;
else
maxWidth = Math.max(labelWidth, maxWidth);
}
displayValues.push([labelStartNorm, labelEndNorm, labelText,
labelStartDate, labelEndDate]);
}
// there needs to be space between the labels. (we may be over-padding
// here if there is only one label with the maximum width...)
maxWidth += this._AXIS_HORIZ_MIN_SPACING_PX;
if (labelTier.boost && (maxWidth > perLabelBudget)) {
// we only boost labels that are the same span as the bins, so rules
// === labelRules at this point. (and barPix === perLabelBudget)
barPix = perLabelBudget = maxWidth;
width = barPix * (labelRules.length - 1);
}
if (maxWidth <= perLabelBudget) {
labelTier.displayValues = displayValues;
labelTier.displayLabel = labelFormat != null;
labelTier.vertHeight = labelFormat ? this._AXIS_HEIGHT_WITH_LABEL_PX
: this._AXIS_HEIGHT_NO_LABEL_PX;
labelTier.vertOffset = totalAxisLabelHeight;
totalAxisLabelHeight += labelTier.vertHeight +
this._AXIS_VERT_SPACING_PX;
break;
}
}
}
let barWidth = barPix - this._BAR_SPACING_PX;
let barSpacing = this._BAR_SPACING_PX;
width = barPix * (rules.length - 1);
// we ideally want this to be the same size as the max rows translates to...
let height = 100;
let ch = height - totalAxisLabelHeight;
let [bins, maxBinSize] = this.binBySpan(scale, span, rules);
// build empty bins for our hot bins
this.emptyBins = [0 for each (bin in bins)];
let binScale = maxBinSize ? (ch / maxBinSize) : 1;
let vis = this.vis = new pv.Panel().canvas(this.canvasNode)
// dimensions
.width(width).height(height)
// margins
.bottom(totalAxisLabelHeight);
let faceter = this.faceter;
// bin bars...
vis.add(pv.Bar)
.data(bins)
.bottom(0)
.height(function (d) Math.floor(d.items.length * binScale))
.width(function() barWidth)
.left(function() this.index * barPix)
.fillStyle("#e9f2f5")
.event("mouseover", function(d) this.fillStyle("#ceeaf5"))
.event("mouseout", function(d) this.fillStyle("#e9f2f5"))
.event("click", function(d)
FacetContext.addFacetConstraint(faceter, faceter.attrDef, true,
[[d.startDate, d.endDate]],
true, true));
this.hotBars = vis.add(pv.Bar)
.data(this.emptyBins)
.bottom(0)
.height(function (d) Math.floor(d * binScale))
.width(function() barWidth)
.left(function() this.index * barPix)
.fillStyle("#ceeaf5");
for each (let [, labelTier] in Iterator(labelTiers)) {
let labelBar = vis.add(pv.Bar)
.data(labelTier.displayValues)
.bottom(-totalAxisLabelHeight + labelTier.vertOffset)
.height(labelTier.vertHeight)
.left(function(d) Math.floor(width * d[0]))
.width(function(d)
Math.floor(width * d[1]) - Math.floor(width * d[0]) - 1)
.fillStyle("#dddddd")
.event("mouseover", function(d) this.fillStyle("#ceeaf5"))
.event("mouseout", function(d) this.fillStyle("#dddddd"))
.event("click", function(d)
FacetContext.addFacetConstraint(faceter, faceter.attrDef, true,
[[d[3], d[4]]], true, true));
if (labelTier.displayLabel) {
labelBar.anchor("top").add(pv.Label)
.font(this._AXIS_FONT)
.textAlign("center")
.textBaseline("top")
.textStyle("#888888")
.text(function(d) d[2]);
}
}
vis.render();
},
hoverItems: function(aItems) {
let itemToBin = this.itemToBin;
let bins = this.emptyBins.concat();
for each (let [, item] in Iterator(aItems)) {
if (item.id in itemToBin)
bins[itemToBin[item.id]]++;
}
this.hotBars.data(bins);
this.vis.render();
},
clearHover: function() {
this.hotBars.data(this.emptyBins);
this.vis.render();
},
/**
* Bin items at the given span granularity with the set of rules generated
* for the given span. This could equally as well be done as a pre-built
* array of buckets with a linear scan of items and a calculation of what
* bucket they should be placed in.
*/
binBySpan: function(aScale, aSpan, aRules, aItems) {
let bins = [];
let maxBinSize = 0;
let binCount = aRules.length - 1;
let itemToBin = this.itemToBin = {};
// We used to break this out by case, but that was a lot of code, and it was
// somewhat ridiculous. So now we just do the simple, if somewhat more
// expensive thing. Reviewer, feel free to thank me.
// We do a pass through the rules, mapping each rounded rule to a bin. We
// then do a pass through all of the items, rounding them down and using
// that to perform a lookup against the map. We could special-case the
// rounding, but I doubt it's worth it.
let binMap = {};
for (let iRule = 0; iRule < binCount; iRule++) {
let binStartDate = aRules[iRule], binEndDate = aRules[iRule+1];
binMap[binStartDate.valueOf().toString()] = iRule;
bins.push({items: [],
startDate: binStartDate,
endDate: binEndDate});
}
let attrKey = this.attrDef.boundName;
for each (let [, item] in Iterator(this.faceter.validItems)) {
let val = item[attrKey];
// round it to the rule...
val = aScale.round(val, aSpan, false);
// which we can then map...
let itemBin = binMap[val.valueOf().toString()];
itemToBin[item.id] = itemBin;
bins[itemBin].items.push(item);
}
for each (let [, bin] in Iterator(bins)) {
maxBinSize = Math.max(bin.items.length, maxBinSize);
}
return [bins, maxBinSize];
}
};

Просмотреть файл

@ -1679,7 +1679,7 @@ function CreateToolbarTooltip(document, event)
}
/**
* Displays message "folder"s, mail "message"s, and "glodaSearch" results. The
* Displays message "folder"s, mail "message"s, and "glodaList" results. The
* commonality is that they all use the "mailContent" panel's folder tree,
* thread tree, and message pane objects. This happens for historical reasons,
* likely involving the fact that prior to the introduction of this
@ -1954,19 +1954,22 @@ let mailTabType = {
}
},
/**
* The glodaSearch view displays a gloda-backed nsMsgDBView with only the
* The glodaList view displays a gloda-backed nsMsgDBView with only the
* thread pane and (potentially) the message pane displayed; the folder
* pane is forced hidden.
*/
glodaSearch: {
glodaList: {
type: "glodaSearch",
/// The set of panes that are legal to be displayed in this mode
legalPanes: {
folder: false,
thread: true,
message: true,
glodaFacets: false,
},
/**
* The default set of columns to show. This really should just be for
* boot-strapping and should be persisted after that...
*/
desiredColumnStates: {
flaggedCol: {
visible: true,
@ -1982,37 +1985,42 @@ let mailTabType = {
},
},
/**
* Open a new tab whose view is backed by a gloda search.
* Open a new folder-display-style tab showing the contents of a gloda
* query/collection. You must pass one of 'query'/'collection'/
* 'conversation'
*
* @param searchString
* @param facetString
* - everything:
* - subject:
* - involves:
* - to:
* - from:
* - body:
* @param location Either a GlodaFolder or the string "everywhere".
* @param {GlodaQuery} [aArgs.query] An un-triggered gloda query to use.
* Alternatively, if you already have a collection, you can pass that
* instead as 'collection'.
* @param {GlodaCollection} [aArgs.collection] A gloda collection to
* display.
* @param {GlodaConversation} [aArgs.conversation] A conversation whose
* messages you want to display.
* @param {GlodaMessage} [aArgs.message] The message to select in the
* conversation, if provided.
* @param aArgs.title The title to give to the tab. If this is not user
* content (a search string, a message subject, etc.), make sure you
* are using a localized string.
*
* XXX This needs to handle opening in the background
*/
openTab: function(aTab, aArgs) {
// make sure the search string bundle is loaded
getDocumentElements();
aTab.title = gSearchBundle.getFormattedString("glodaSearchTabTitle",
[aArgs.searchString]);
aTab.glodaSearchInputValue = aArgs.searchString;
aTab.searchString = aArgs.searchString;
aTab.facetString = aArgs.facetString;
aTab.glodaSynView = new GlodaSyntheticSearchView(aArgs.searchString,
aArgs.facetString,
aArgs.location);
aTab.glodaSynView = new GlodaSyntheticView(aArgs);
aTab.title = aArgs.title;
this.openTab(aTab, false, new MessagePaneDisplayWidget());
aTab.folderDisplay.show(aTab.glodaSynView);
// XXX persist column states in preferences or session store or other
aTab.folderDisplay.setColumnStates(aTab.mode.desiredColumnStates);
aTab.folderDisplay.makeActive();
let background = ("background" in aArgs) && aArgs.background;
if (!background)
aTab.folderDisplay.makeActive();
if ("message" in aArgs) {
let hdr = aArgs.message.folderMessage;
if (hdr)
aTab.folderDisplay.selectMessage(hdr);
}
},
getBrowser: function(aTab) {
// If we are currently a thread summary, we want to select the multi
@ -2166,7 +2174,6 @@ let mailTabType = {
* is distinct from the thread pane because some other things depend
* on whether it's actually the thread pane we are showing.
* - message: The message pane. Required/assumed to be true for now.
* - glodaFacets: The gloda search facets pane.
* @param aVisibleStates A dictionary where each value indicates whether the
* pane should be 'visible' (not collapsed). Only panes that are governed
* by splitters are options here. Keys are:
@ -2253,10 +2260,6 @@ let mailTabType = {
messagePaneToggleKey.removeAttribute("disabled");
else
messagePaneToggleKey.setAttribute("disabled", "true");
// -- gloda facets
document.getElementById("glodaSearchFacets").hidden =
!aLegalStates.glodaFacets;
},
showTab: function(aTab) {
@ -2299,42 +2302,6 @@ let mailTabType = {
DefaultController.onEvent(aEvent);
}
};
/**
* The glodaSearch tab mode has a UI widget outside of the mailTabType's
* display panel, the #glodaSearchInput textbox. This means we need to use a
* tab monitor so that we can appropriately update the contents of the textbox.
* Every time a tab is changed, we save the state of the text box and restore
* its previous value for the tab we are switching to, as well as whether this
* value is a change to the currently-used value (if it is a glodaSearch) tab.
* The behaviour rationale for this is that the glodaSearchInput is like the
* URL bar. When you are on a glodaSearch tab, we need to show you your
* current value, including any "uncommitted" (you haven't hit enter yet)
* changes. It's not entirely clear that imitating this behaviour on
* non-glodaSearch tabs makes a lot of sense, but it is consistent, so we do
* so. The counter-example to this choice is the search box in firefox, but
* it never updates when you switch tabs, so it is arguably less of a fit.
*/
var glodaSearchTabMonitor = {
onTabTitleChanged: function() {},
onTabSwitched: function glodaSearchTabMonitor_onTabSwitch(aTab, aOldTab) {
let inputNode = document.getElementById("glodaSearchInput");
if (!inputNode)
return;
// save the current search field value
if (aOldTab)
aOldTab.glodaSearchInputValue = inputNode.value;
// load (or clear if there is none) the persisted search field value
inputNode.value = aTab.glodaSearchInputValue || "";
// If the mode is glodaSearch and the search is unchanged, then we want to
// set the icon state of the input box to be the 'clear' icon.
if (aTab.mode.name == "glodaSearch") {
if (aTab.searchString == aTab.glodaSearchInputValue)
inputNode._searchIcons.selectedIndex = 1;
}
}
};
function MsgOpenNewWindowForFolder(folderURI, msgKeyToSelect)

Просмотреть файл

@ -164,19 +164,15 @@ dummy.usesMailWidgets {
-moz-binding: url("chrome://messenger/content/mailWidgets.xml#dummy");
}
#glodaSearchInput {
#searchInput {
-moz-binding: url("chrome://messenger/content/search.xml#glodaSearch");
}
#glodaSearchFacets {
-moz-binding: url("chrome://messenger/content/search.xml#glodaFacets");
}
#searchInput {
#oldsearchInput {
-moz-binding: url("chrome://messenger/content/search.xml#searchbar");
}
#quick-search-button {
.quick-search-button {
-moz-binding: url("chrome://messenger/content/search.xml#searchBarDropMarker");
cursor: default;
-moz-user-focus: none;

Просмотреть файл

@ -40,6 +40,8 @@
# ***** END LICENSE BLOCK *****
<?xml-stylesheet href="chrome://messenger/skin/mailWindow1.css" type="text/css"?>
<?xml-stylesheet href="chrome://gloda/content/glodacomplete.css" type="text/css"?>
<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
<?xul-overlay href="chrome://messenger/content/msgHdrViewOverlay.xul"?>
@ -90,12 +92,13 @@
<script type="application/x-javascript" src="chrome://messenger/content/selectionsummaries.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/msgMail3PaneWindow.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/specialTabs.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/glodaFacetTab.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/searchBar.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/mail3PaneWindowCommands.js"/>
<script type="application/x-javascript" src="chrome://global/content/contentAreaUtils.js"/>
<script type="application/x-javascript" src="chrome://communicator/content/nsContextMenu.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/mailContextMenus.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/accountUtils.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/searchBar.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/folderPane.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/phishingDetector.js"/>
<script type="application/x-javascript" src="chrome://communicator/content/contentAreaClick.js"/>
@ -344,16 +347,6 @@
<splitter class="tree-splitter"/>
<treecol id="idCol" persist="width" flex="1" hidden="true"
label="&idColumn.label;" tooltiptext="&idColumn.tooltip;"/>
<splitter class="tree-splitter"/>
<treecol id="glodaWhyCol" persist="width" flex="1"
hidden="true" ignoreincolumnpicker="true"
label="&glodaWhyColumn.label;"
tooltiptext="&glodaWhyColumn.tooltip;"/>
<splitter class="tree-splitter"/>
<treecol id="glodaScoreCol" persist="width" flex="1"
hidden="true" ignoreincolumnpicker="true"
label="&glodaScoreColumn.label;"
tooltiptext="&glodaScoreColumn.tooltip;"/>
</treecols>
<treechildren ondraggesture="ThreadPaneOnDragStart(event);"
ondragover="ThreadPaneOnDragOver(event);"

Просмотреть файл

@ -279,8 +279,11 @@ function OnLoadMessenger()
let tabmail = document.getElementById('tabmail');
if (tabmail)
{
// mailTabType is defined in mailWindowOverlay.js
tabmail.registerTabType(mailTabType);
tabmail.registerTabMonitor(glodaSearchTabMonitor);
// glodaFacetTab* in glodaFacetTab.js
tabmail.registerTabType(glodaFacetTabType);
tabmail.registerTabMonitor(glodaFacetTabMonitor);
tabmail.registerTabMonitor(QuickSearchTabMonitor);
tabmail.registerTabMonitor(statusMessageCountsMonitor);
tabmail.openFirstTab();

Просмотреть файл

@ -23,6 +23,8 @@
-
- Contributor(s):
- Scott MacGregor <mscott@mozilla.org>
- Andrew Sutherland <asutherland@asutherland.org>
- David Ascher <dascher@mozillamessaging.com>
-
- Alternatively, the contents of this file may be used under the terms of
- either of the GNU General Public License Version 2 or later (the "GPL"),
@ -52,169 +54,297 @@
<!--
- The glodaSearch binding implements a gloda-backed search mechanism. The
- actual search logic comes from the glodaSearch tab mode in the
- mailTabType definition. This binding serves as a means to display and
- alter the current search query if a "glodaSearch" tab is displayed, or
- enter a search query and spawn a new "glodaSearch" tab if one is
- currently not displayed. The "glodaFacets" binding also is used to
- display/modify the parameters of the search when on a "glodaSearch" tab.
- actual search logic comes from the glodaFacet tab mode in the
- glodaFacetTabType definition. This binding serves as a means to display
- and alter the current search query if a "glodaFacet" tab is displayed,
- or enter a search query and spawn a new "glodaFacet" tab if one is
- currently not displayed.
-->
<binding id="glodaSearch" extends="chrome://global/content/bindings/textbox.xml#search-textbox">
<binding id="glodaSearch" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
<resources>
<stylesheet src="chrome://messenger/skin/searchBox.css"/>
</resources>
<handlers>
<handler event="command"><![CDATA[
if (this.value) {
let searchString = this.value;
let tabmail = document.getElementById("tabmail");
// If the current tab is a gloda search tab, reset the value
// to the initial search value. Otherwise, clear it. This
// is the value that is going to be saved with the current
// tab when we switch back to it next.
if (tabmail.currentTabInfo.mode.name == "glodaSearch")
this.value = tabmail.currentTabInfo.searchString;
else
this.value = "";
// open a new tab with our dude
tabmail.openTab("glodaSearch", {searchString: searchString,
facetString: "everything", locationString: "everywhere"});
}
]]></handler>
</handlers>
</binding>
<content>
<xul:button anonid="quick-search-button" class="quick-search-button" type="menu" chromedir="&locale.dir;">
<xul:menupopup anonid="quick-search-menupopup"
class="quick-search-menupopup"
persist="value"
onpopupshowing="this.parentNode.parentNode.updatePopup();"
popupalign="topleft"
popupanchor="bottomleft">
<xul:menuitem anonid="searchGlobalMenu"
class="quick-search-menu"
value="global"
label="&searchEverywhere.label;"
type="radio"
oncommand="this.parentNode.parentNode.parentNode.changeMode(this)"/>
<xul:menuseparator/>
</xul:menupopup>
</xul:button>
<xul:hbox class="quick-search-textbox textbox-input-box" flex="1">
<html:input class="textbox-input" flex="1" anonid="input" allowevents="true"
xbl:inherits="onfocus,onblur,oninput,value,type,maxlength,disabled,size,readonly,tabindex,accesskey"/>
</xul:hbox>
<xul:toolbarbutton anonid="quick-search-clearbutton" xbl:inherits=""
disabled="true" class="quick-search-clearbutton"
onclick="this.parentNode.value = ''; this.parentNode.select(); return false;"
chromedir="&locale.dir;"/>
<!--XXX update search if not global-->
<!--
- The glodaFacets binding is used to display additional search constraints
- on a "glodaSearch" tab's gloda-backed search. Because we live in the
- "mailContent" panel reused by the "glodaSearch" tab mode, we are always
- present, even when we should not be displayed (namely for "folder" and
- "message" tab modes). We leave it up to the mailTabType and glodaSearch
- tab mode to ensure that we are shown/hidden at the right times. (We
- could do this ourselves as a tabmail tab monitor, but it is more
- intuitive to have our behaviour/relationship made explicit.)
-->
<binding id="glodaFacets">
<resources>
<stylesheet src="chrome://messenger/skin/searchBox.css"/>
</resources>
<content orientation="horizontal" hidden="true">
<xul:label control="glodaFacetType" value="&glodaSearchBar.facet.label;"/>
<xul:menulist id="glodaFacetType">
<xul:menupopup>
<xul:menuitem label="&glodaSearchFacet.everything.label;"
value="everything"/>
<xul:menuitem label="&glodaSearchFacet.subject.label;"
value="subject"/>
<xul:menuitem label="&glodaSearchFacet.involves.label;"
value="involves"/>
<xul:menuitem label="&glodaSearchFacet.to.label;"
value="to"/>
<xul:menuitem label="&glodaSearchFacet.from.label;"
value="from"/>
<xul:menuitem label="&glodaSearchFacet.body.label;"
value="body"/>
</xul:menupopup>
</xul:menulist>
<xul:label control="glodaFacetLocation"
value="&glodaSearchBar.location.label;"/>
<xul:menulist id="glodaFacetLocation" anonid="glodaFacetLocation">
<xul:menupopup>
<xul:menuitem label="&glodaSearchFacet.everywhere.label;" value="everywhere"/>
<xul:menuitem anonid="currentFolder" label="" value="currentFolder" hidden="true"/>
<xul:menu label="&glodaSearchFacet.folder.label;">
<xul:menupopup type="folder"/>
</xul:menu>
</xul:menupopup>
</xul:menulist>
</content>
<implementation>
<constructor>
<![CDATA[
this._facetTypeNode =
document.getAnonymousElementByAttribute(this, "id",
"glodaFacetType");
this._facetLocationNode =
document.getAnonymousElementByAttribute(this, "id",
"glodaFacetLocation");
this._currentFolderNode =
document.getAnonymousElementByAttribute(this, "anonid",
"currentFolder");
]]>
</constructor>
<method name="updateStateFromCurrentTab">
<body><![CDATA[
/**
* Update our display state to match the state of the current tab.
*/
let tabmail = document.getElementById("tabmail");
let tabInfo = tabMail.currentTabInfo;
this._facetTypeNode.value = tabInfo.facetString;
if (typeof(tabInfo.location) == "string")
this._facetLocationNode.value = tabInfo.location;
]]></body>
</method>
<method name="setLocationFacetToFolder">
<body><![CDATA[
/**
* Update our display state to match the state of the current tab.
*/
let tabmail = document.getElementById("tabmail");
let tabInfo = tabMail.currentTabInfo;
this._facetTypeNode.value = tabInfo.facetString;
if (typeof(tabInfo.location) == "string")
this._facetLocationNode.value = tabInfo.location;
]]></body>
</method>
</implementation>
<handlers>
<handler event="command" phase="bubble"><![CDATA[
// Have widgetNode be our immediate child, with nodes being a list of
// the descendent nodes between eventNode and the actual target.
// This allows us to know which of our widgets actually got clicked on,
// plus makes subsequent processing easier. (Alternatively, we could
// register a command listener on each of our widgets, but that is
// arguably just as ugly.)
let nodes = [];
let widgetNode = event.originalTarget;
while (widgetNode.parentNode != this) {
nodes.unshift(widgetNode);
widgetNode = widgetNode.parentNode;
}
// -- Type Facet
if (widgetNode == this._facetTypeNode) {
}
// -- Location Facet
else if (widgetNode == this._facetLocationNode) {
if (event.originalTarget._folder) {
let folder = event.originalTarget._folder;
this._currentFolderNode.label =
[node._folder.prettiestName
for each ([, node] in Iterator(nodes))
if (node._folder)].join("/");
this._currentFolderNode.hidden = false;
this._facetLocationNode.selectedItem = this._currentFolderNode;
<handler event="input">
<![CDATA[
try {
if (this.searchMode != "global") { // it's a quick search
let dis = this;
clearTimeout(this.timeoutHandler);
this.timeoutHandler = setTimeout(this.onTimeout, this.timeout, dis);
}
} catch (e) {
logException(e);
}
dump("Selected folder: " + event.originalTarget._folder + "\n");
dump("event: " + event + "\n");
dump("target: " + event.originalTarget + "\n");
dump("event target id: " + event.originalTarget.id + "\n");
dump("event tag: " + event.originalTarget.tagName + "\n");
]]></handler>
]]>
</handler>
<!-- For the next two, we need to get in on the bubbling phase, as
otherwise we'll be doing searches when autocomplete results are
being selected. -->
<handler event="keypress" keycode="VK_ENTER"
phase="bubbling" action="return this.doSearch();"/>
<handler event="keypress" keycode="VK_RETURN"
phase="bubbling" action="try {this.doSearch();} catch (e) { logException(e);} return true;"/>
<handler event="input">
<![CDATA[
if (!this.value)
this.clearButtonHidden = true;
else
this.clearButtonHidden = false;
]]></handler>
</handlers>
</binding>
<implementation implements="nsIObserver">
<constructor><![CDATA[
this.build();
]]></constructor>
<method name="onTimeout">
<parameter name="dis"/>
<body><![CDATA[
try {
dis.doSearch();
} catch (e) {
logException(e);
}
]]>
</body>
</method>
<method name="updatePopup">
<body><![CDATA[
try {
// disable the create virtual folder menu item if the current radio
// value is set to Find in message since you can't really create a VF from find
// in message
if (this.searchMode == "global" || this.value == "")
this.saveAsVirtualFolder.setAttribute('disabled', 'true');
else
this.saveAsVirtualFolder.removeAttribute('disabled');
//let hideQuickSearchModes = this.searchMode == "global" ? "true" : "false";
//for each (let child in this.menupopup.childNodes) {
// if (child.hasAttribute("quicksearch")) {
// child.setAttribute("collapsed", hideQuickSearchModes)
// }
//}
} catch (e) {
logException(e);
}
]]>
</body>
</method>
<method name="build">
<body><![CDATA[
const Cu = Components.utils;
Cu.import("resource://gre/modules/errUtils.js");
try {
Cu.import("resource://app/modules/StringBundle.js");
Cu.import("resource://app/modules/quickSearchManager.js");
this.glodaCompleter =
Components.classes["@mozilla.org/autocomplete/search;1?name=gloda"].
getService(). //Components.interfaces.nsIAutoCompleteSearch)
wrappedJSObject;
var observerSvc = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observerSvc.addObserver(this, "autocomplete-did-enter-text", false);
this.quickSearchStrings = new StringBundle("chrome://messenger/locale/quickSearch.properties");
let quickSearchModes = QuickSearchManager.getSearchModes();
for (let i = 0; i < quickSearchModes.length; i++) {
let searchMode = quickSearchModes[i];
let value = searchMode["value"];
let label = searchMode["label"];
let menuitem = document.createElement("menuitem");
menuitem.setAttribute("value", String(value));
menuitem.setAttribute("label", label);
menuitem.setAttribute("type", "radio");
menuitem.setAttribute("quicksearch", "true");
menuitem.setAttribute("oncommand", "this.parentNode.parentNode.parentNode.changeMode(this)");
this.menupopup.appendChild(menuitem);
}
let separator = document.createElement("menuseparator");
this.menupopup.appendChild(separator);
let saveAsVF = document.createElement("menuitem");
saveAsVF.setAttribute("anonid", "quick-search-save-as-virtual-folder");
saveAsVF.setAttribute("label", this.quickSearchStrings.get("saveAsVirtualFolder.label"));
saveAsVF.setAttribute("oncommand",
"gFolderTreeController.newVirtualFolder(this.parentNode.parentNode.parentNode.value,\
gFolderDisplay.view.search.session.searchTerms);");
this.menupopup.appendChild(saveAsVF);
this.updateSaveItem();
} catch (e) {
logException(e);
}
]]></body>
</method>
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body><![CDATA[
try {
if (aTopic == "autocomplete-did-enter-text") {
let selectedIndex = this.autocompletePopup.selectedIndex;
let row = this.glodaCompleter.curResult.getObjectAt(selectedIndex);
if (row == null) {
this.applyConstraints();
return;
}
let theQuery = Gloda.newQuery(Gloda.NOUN_MESSAGE);
let tabmail = document.getElementById("tabmail");
if (row.fullText) {
tabmail.openTab("glodaFacet", {
searcher: new GlodaMsgSearcher(null, row.item, row.andTerms)
});
} else {
if (row.nounDef.name == "tag") {
theQuery = theQuery.tags(row.item);
} else if (row.nounDef.name == "identity") {
theQuery = theQuery.involves(row.item);
}
tabmail.openTab("glodaFacet", {
query: theQuery
});
}
this.applyConstraints();
}
} catch (e) {
logException(e);
}
]]></body>
</method>
<method name="applyConstraints">
<body><![CDATA[
]]>
</body>
</method>
<method name="updateEmptyText">
<body><![CDATA[
try {
// extract the label value from the menu item
let menuItem = this.menupopup.getElementsByAttribute('value',
this.searchMode)[0];
this.emptyText = menuItem.getAttribute('label');
} catch (e) {
logException(e);
}
]]></body>
</method>
<method name="updateSaveItem">
<body><![CDATA[
let disabled = true;
this.saveAsVirtualFolder.setAttribute("disabled", disabled)
]]></body>
</method>
<method name="doSearch">
<body><![CDATA[
try {
dump("doing search, value = " + this.value + "\n");
if (this.searchMode == 'global') // faceted search
{
if (this.value) {
let tabmail = document.getElementById("tabmail");
// If the current tab is a gloda search tab, reset the value
// to the initial search value. Otherwise, clear it. This
// is the value that is going to be saved with the current
// tab when we switch back to it next.
let searchString = this.value;
if (tabmail.currentTabInfo.mode.name == "glodaFacet")
this.value = tabmail.currentTabInfo.searchString;
else
this.value = "";
// open a new tab with our dude
tabmail.openTab("glodaFacet", {
searcher: new GlodaMsgSearcher(null, searchString)
});
}
} else { // quick search
if (! this.value)
gFolderDisplay.view.search.userTerms = null
else
gFolderDisplay.view.search.quickSearch(Number(this.searchMode), this.value);
}
} catch (e) {
logException(e);
}
]]>
</body>
</method>
<method name="changeMode">
<parameter name="aMenuItem" />
<body><![CDATA[
var oldSearchMode = this.searchMode;
this.searchMode = aMenuItem.value;
if (oldSearchMode != this.searchMode) // the search mode just changed so we need to redo the quick search
this.doSearch();
]]></body>
</method>
<field name="timeout">200</field>
<field name="glodaCompleter">null</field>
<field name="ignoreClick">false</field>
<field name="quickSearchStrings">null</field>
<field name="mQuickSearchMode">null</field>
<field name="timeoutHandler">null</field>
<property name="searchMode" onget="return this.mQuickSearchMode;"
onset="this.mQuickSearchMode = val;
dump('val = ' + val + ' \n');
dump('typeof val = ' + typeof(val) + ' \n');
this.menupopup.setAttribute('value', val);
this.updateEmptyText(); "/>
<property name="autocompletePopup" readonly="true" onget="return document.getElementById(this.getAttribute('autocompletepopup'))"/>
<property name="showingSearchCriteria" onget="return this.getAttribute('searchCriteria') == 'true';"
onset="this.setAttribute('searchCriteria', val); return val;"/>
<property name="menupopup" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-menupopup')"/>
<property name="saveAsVirtualFolder" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-save-as-virtual-folder')"/>
<property name="clearButtonHidden" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-clearbutton').getAttribute('clearButtonHidden') == 'true';"
onset="document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-clearbutton').setAttribute('clearButtonHidden', val); return val;"/>
</implementation>
</binding>
<binding id="searchbar" extends="chrome://global/content/bindings/textbox.xml#timed-textbox">
<resources>
@ -258,17 +388,11 @@
qsmenu.getElementsByAttribute('value', this.searchMode)[0];
}
selectedMenuItem.setAttribute('checked', 'true');
this.setSearchCriteriaText();
]]>
</constructor>
<property name="showingSearchCriteria" onget="return this.getAttribute('searchCriteria') == 'true';"
onset="this.setAttribute('searchCriteria', val); return val;"/>
<property name="clearButtonHidden" onget="return document.getElementById('quick-search-clearbutton').getAttribute('clearButtonHidden') == 'true';"
onset="document.getElementById('quick-search-clearbutton').setAttribute('clearButtonHidden', val); return val;"/>
<field name="mQuickSearchMode">null</field>
// DND Observer
@ -300,6 +424,7 @@
<method name="setSearchCriteriaText">
<body><![CDATA[
try {
this.showingSearchCriteria = true;
let qsmenu = document.getElementById('quick-search-menupopup');
@ -307,7 +432,7 @@
// extract the label value from the menu item
let menuItem = qsmenu.getElementsByAttribute('value',
this.searchMode)[0];
logElement(menuItem);
if (typeof menuItem == "undefined") {
// Error condition, something went wrong - try and recover from it.
this.mQuickSearchMode =
@ -320,10 +445,14 @@
this.inputField.value = selectedMenuItem.getAttribute('label');
}
else
else {
this.inputField.value = menuItem.getAttribute('label');
}
this.clearButtonHidden = true;
} catch (e) {
logException(e);
}
]]></body>
</method>

Просмотреть файл

@ -23,6 +23,8 @@
* Seth Spitzer <sspitzer@netscape.com>
* Scott MacGregor <mscott@mozilla.org>
* David Bienvenu <bienvenu@nventure.com>
* Andrew Sutherland <asutherland@asutherland.org>
* David Ascher <dascher@mozillamessaging.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
@ -40,27 +42,16 @@
Components.utils.import("resource://app/modules/quickSearchManager.js");
var gSearchBundle;
var gStatusBar = null;
var gIgnoreFocus = false;
var gIgnoreClick = false;
// change/add constants in QuickSearchConstants in quickSearchManager.js first
// ideally these should go away in favor of everyone using QuickSearchConstants
const kQuickSearchSubject = QuickSearchConstants.kQuickSearchSubject;
const kQuickSearchFrom = QuickSearchConstants.kQuickSearchFrom;
const kQuickSearchFromOrSubject =
QuickSearchConstants.kQuickSearchFromOrSubject;
const kQuickSearchBody = QuickSearchConstants.kQuickSearchBody;
const kQuickSearchRecipient = QuickSearchConstants.kQuickSearchRecipient;
const kQuickSearchRecipientOrSubject =
QuickSearchConstants.kQuickSearchRecipientOrSubject;
/**
* We are exclusively concerned with disabling the quick-search box when a
* tab is being displayed that lacks quick search abilities.
*/
var QuickSearchTabMonitor = {
onTabTitleChanged: function() {
},
@ -69,18 +60,21 @@ var QuickSearchTabMonitor = {
let searchInput = document.getElementById("searchInput");
if (searchInput) {
let newTabEligible = aTab.mode.tabType == mailTabType;
let newTabEligible = ((aTab.mode.tabType == mailTabType) ||
(aTab.mode.tabType == glodaFacetTabType));
searchInput.disabled = !newTabEligible;
if (!newTabEligible)
searchInput.value = "";
}
},
}
};
// XXX never called?
function SetQSStatusText(aNumHits)
{
var statusMsg;
gSearchBundle = document.getElementById("bundle_search");
// if there are no hits, it means no matches were found in the search.
if (aNumHits == 0)
statusMsg = gSearchBundle.getString("searchFailureMessage");
@ -95,159 +89,6 @@ function SetQSStatusText(aNumHits)
statusFeedback.showStatusString(statusMsg);
}
function getDocumentElements()
{
gSearchBundle = document.getElementById("bundle_search");
gStatusBar = document.getElementById('statusbar-icon');
GetSearchInput();
}
function onEnterInSearchBar()
{
if (!gSearchInput)
return;
// nothing changes while showing the criteria
if (gSearchInput.showingSearchCriteria)
return;
if (!gSearchInput || gSearchInput.value == "")
gFolderDisplay.view.search.userTerms = null;
else
gFolderDisplay.view.search.quickSearch(gSearchInput.searchMode,
gSearchInput.value);
}
function onSearchKeyPress()
{
if (gSearchInput.showingSearchCriteria)
gSearchInput.showingSearchCriteria = false;
}
function onSearchInputFocus(event)
{
GetSearchInput();
// search bar has focus, ...clear the showing search criteria flag
if (gSearchInput.showingSearchCriteria)
{
gSearchInput.value = "";
gSearchInput.showingSearchCriteria = false;
}
if (gIgnoreFocus) // got focus via mouse click, don't need to anything else
gIgnoreFocus = false;
else
gSearchInput.select();
}
function onSearchInputMousedown(event)
{
GetSearchInput();
if (gSearchInput.hasAttribute("focused"))
// If the search input is focused already, ignore the click so that
// onSearchInputBlur does nothing.
gIgnoreClick = true;
else
{
gIgnoreFocus = true;
gIgnoreClick = false;
}
}
function onSearchInputClick(event)
{
if (!gIgnoreClick)
// Triggers onSearchInputBlur(), but focus returns to field.
gSearchInput.select();
}
function onSearchInputBlur(event)
{
// If we're doing something else, don't process the blur.
if (gIgnoreClick)
return;
if (!gSearchInput.value)
gSearchInput.showingSearchCriteria = true;
if (gSearchInput.showingSearchCriteria)
gSearchInput.setSearchCriteriaText();
}
function onClearSearch()
{
// If we're not showing search criteria, then we need to clear up.
if (!gSearchInput.showingSearchCriteria)
{
Search("");
// Hide the clear button
gSearchInput.clearButtonHidden = true;
gIgnoreClick = true;
gSearchInput.select();
gIgnoreClick = false;
}
}
// called from commandglue.js in cases where the view is being changed and QS
// needs to be cleared.
function ClearQSIfNecessary()
{
if (!gSearchInput || gSearchInput.showingSearchCriteria)
return;
gSearchInput.setSearchCriteriaText();
}
function Search(str)
{
if (gSearchInput.showingSearchCriteria && str != "")
return;
gSearchInput.value = str; //on input does not get fired for some reason
onEnterInSearchBar();
}
// helper methods for the quick search drop down menu
function changeQuickSearchMode(aMenuItem)
{
// extract the label and set the search input to match it
var oldSearchMode = gSearchInput.searchMode;
gSearchInput.searchMode = aMenuItem.value;
if (gSearchInput.value == "" || gSearchInput.showingSearchCriteria)
{
gSearchInput.showingSearchCriteria = true;
if (gSearchInput.value) //
gSearchInput.setSearchCriteriaText();
}
// if the search box is empty, set showing search criteria to true so it shows up when focus moves out of the box
if (!gSearchInput.value)
gSearchInput.showingSearchCriteria = true;
else if (gSearchInput.showingSearchCriteria) // if we are showing criteria text and the box isn't empty, change the criteria text
gSearchInput.setSearchCriteriaText();
else if (oldSearchMode != gSearchInput.searchMode) // the search mode just changed so we need to redo the quick search
onEnterInSearchBar();
}
function saveViewAsVirtualFolder()
{
gFolderTreeController.newVirtualFolder(gSearchInput.value,
gFolderDisplay.view.search.session.searchTerms);
}
function InitQuickSearchPopup()
{
// disable the create virtual folder menu item if the current radio
// value is set to Find in message since you can't really create a VF from find
// in message
GetSearchInput();
if (!gSearchInput ||gSearchInput.value == "" || gSearchInput.showingSearchCriteria)
document.getElementById('quickSearchSaveAsVirtualFolder').setAttribute('disabled', 'true');
else
document.getElementById('quickSearchSaveAsVirtualFolder').removeAttribute('disabled');
}
/**
* If switching from an "incoming" (Inbox, etc.) type of mail folder,
@ -272,19 +113,19 @@ function onSearchFolderTypeChanged(isOutboundFolder)
if (isOutboundFolder)
{
if (gSearchInput.searchMode == kQuickSearchFromOrSubject)
newSearchType = kQuickSearchRecipientOrSubject;
else if (gSearchInput.searchMode == kQuickSearchFrom)
newSearchType = kQuickSearchRecipient;
if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchFromOrSubject)
newSearchType = QuickSearchConstants.kQuickSearchRecipientOrSubject;
else if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchFrom)
newSearchType = QuickSearchConstants.kQuickSearchRecipient;
else
return;
}
else
{
if (gSearchInput.searchMode == kQuickSearchRecipientOrSubject)
newSearchType = kQuickSearchFromOrSubject;
else if (gSearchInput.searchMode == kQuickSearchRecipient)
newSearchType = kQuickSearchFrom;
if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchRecipientOrSubject)
newSearchType = QuickSearchConstants.kQuickSearchFromOrSubject;
else if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchRecipient)
newSearchType = QuickSearchConstants.kQuickSearchFrom;
else
return;
}

Просмотреть файл

@ -417,6 +417,9 @@
<parameter name="aArgs"/>
<body>
<![CDATA[
try {
if (!(aTabModeName in this.tabModes))
throw new Error("No such tab mode: " + aTabModeName);
let tabMode = this.tabModes[aTabModeName];
// if we are already at our limit for this mode, show an existing one
if (tabMode.tabs.length == tabMode.maxTabs) {
@ -523,6 +526,7 @@
UpdateMailToolbar("tabmail");
return tab;
} catch (e) { logException(e);}
]]></body>
</method>
<method name="selectTabByMode">

Просмотреть файл

@ -60,7 +60,7 @@ messenger.jar:
content/messenger/aboutDialog.css (content/aboutDialog.css)
* content/messenger/credits.xhtml (content/credits.xhtml)
content/messenger/messenger.css (content/messenger.css)
* content/messenger/search.xml (content/search.xml)
content/messenger/search.xml (content/search.xml)
content/messenger/tabmail.xml (content/tabmail.xml)
content/messenger/tabmail.css (content/tabmail.css)
* content/messenger/newmailalert.xul (content/newmailalert.xul)
@ -80,6 +80,14 @@ messenger.jar:
content/messenger/multimessageview.css (content/multimessageview.css)
content/messenger/sharedsummary.css (content/sharedsummary.css)
content/messenger/multimessageview.xhtml (content/multimessageview.xhtml)
content/messenger/glodaFacetTab.js (content/glodaFacetTab.js)
content/messenger/glodaFacetViewWrapper.xul (content/glodaFacetViewWrapper.xul)
content/messenger/glodaFacetView.xhtml (content/glodaFacetView.xhtml)
content/messenger/glodaFacetView.js (content/glodaFacetView.js)
content/messenger/glodaFacetView.css (content/glodaFacetView.css)
content/messenger/glodaFacetBindings.css (content/glodaFacetBindings.css)
content/messenger/glodaFacetBindings.xml (content/glodaFacetBindings.xml)
content/messenger/glodaFacetVis.js (content/glodaFacetVis.js)
comm.jar:
% content communicator %content/communicator/ xpcnativewrappers=yes

Просмотреть файл

@ -0,0 +1,151 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Thunderbird Global Database
#
# The Initial Developer of the Original Code is
# Mozilla Messaging, Inc.
# Portions created by the Initial Developer are Copyright (C) 2009
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Andrew Sutherland <asutherland@asutherland.org>
#
# Alternatively, the contents of this file may be used under the terms of
# either of the GNU General Public License Version 2 or later (the "GPL"),
# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# LOCALIZATION NOTE (*.facetLabel): These are the labels used to label the facet
# displays in the global search facet display mechanism. Like thread pane
# column headers these should be relatively short but can take advantage of
# attributes with longer displays to be slightly longer. For example, the
# conversation attribute is larger than most attributes, so can have a slightly
# longer label. You can and should use the facetTooltip to provide a better
# and longer explanation of what the facet attribute is.
# LOCALIZATION NOTE (*.facetTooltip): These are the tooltips that will be
# displayed if you hover over the facet's label. These should be longer than
# the facetLabel and reduce confusion about what the facet is, but do not
# need to be a book on the subject.
# LOCALIZATION NOTE (gloda.message.attr.folder.*): Stores the message folder in
# which the message is stored.
gloda.message.attr.folder.facetLabel=Mail Folder
gloda.message.attr.folder.facetTooltip=The folder where the message is stored.
# LOCALIZATION NOTE (gloda.message.attr.conversation.*): Stores the conversation
# the message belongs to. The conversation is currently expressed using the
# subject of the message that initiated it.
gloda.message.attr.conversation.facetLabel=Conversation Subject
gloda.message.attr.conversation.facetTooltip=The subject of the conversation the message belongs to.
# LOCALIZATION NOTE (gloda.message.attr.fromMe.*): Stores everyone involved
# with the message. This means from/to/cc/bcc.
gloda.message.attr.fromMe.facetLabel=From Me
gloda.message.attr.fromMe.facetTooltip=Messages where I was the author.
# LOCALIZATION NOTE (gloda.message.attr.toMe.*): Stores everyone involved
# with the message. This means from/to/cc/bcc.
gloda.message.attr.toMe.facetLabel=To Me
gloda.message.attr.toMe.facetTooltip=Messages where I was on the To or Cc lines.
# LOCALIZATION NOTE (gloda.message.attr.involves.*): Stores everyone involved
# with the message. This means from/to/cc/bcc.
gloda.message.attr.involves.facetLabel=People
gloda.message.attr.involves.facetTooltip=People explicitly involved in the message by being listed on the From, To, Cc, or Bcc lines.
# LOCALIZATION NOTE (gloda.message.attr.date.*): Stores the date of the message.
# Thunderbird normally stores the date the message claims it was composed
# according to the "Date" header. This is not the same as when the message
# was sent or when it was eventually received by the user. In the future we
# may change this to be one of the other dates, but not anytime soon.
gloda.message.attr.date.facetLabel=Date
gloda.message.attr.date.facetTooltip=The date the message claims it was authored.
# LOCALIZATION NOTE (gloda.message.attr.attachmentTypes.*): Stores the list of
# MIME types (ex: image/png, text/plain) of real attachments (not just part of
# the message content but explicitly named attachments) on the message.
# Although we hope to be able to provide localized human-readable explanations
# of the MIME type (ex: "PowerPoint document"), I don't know if that is going
# to happen.
gloda.message.attr.attachmentTypes.facetLabel=Attachments
gloda.message.attr.attachmentTypes.facetTooltip=The type of attachments found on the message, if any.
# LOCALIZATION NOTE (gloda.message.attr.mailing-list.*): Stores the mailing
# lists detected in the message. This will normally be the e-mail address of
# the mailing list and only be detected in messages received from the mailing
# list. Extensions may contribute additional detected mailing-list-like
# things.
gloda.message.attr.mailing-list.facetLabel=Mail List Involved
gloda.message.attr.mailing-list.facetTooltip=If the message was sent via a mailing list, the e-mail address associated with the mailing list.
# LOCALIZATION NOTE (gloda.message.attr.tag.*): Stores the tags applied to the
# message. Notably, gmail's labels are not currently exposed via IMAP and we
# do not do anything clever with gmail, so this is indepdendent of gmail
# labels. This may change in the future, but it's a safe bet it's not
# happening on Thunderbird's side prior to 3.0.
gloda.message.attr.tag.facetLabel=Tags
gloda.message.attr.tag.facetTooltip=Tags applied to the message.
# LOCALIZATION NOTE (gloda.message.attr.tag.*): Stores whether the message is
# starred or not, as indicated by a pretty star icon. In the past, the icon
# used to be a flag. The IMAP terminology continues to be "flagged".
gloda.message.attr.star.facetLabel=Starred
gloda.message.attr.star.facetTooltip=Is the message starred / flagged?
# LOCALIZATION NOTE (gloda.message.attr.read.*): Stores whether the user has
# read the message or not.
gloda.message.attr.read.facetLabel=Read
gloda.message.attr.read.facetTooltip=Is the message marked read, or is it unread?
# LOCALIZATION NOTE (gloda.message.attr.repliedTo.*): Stores whether we believe
# the user has ever replied to the message. We normally show a little icon in
# the thread pane when this is the case.
gloda.message.attr.repliedTo.facetLabel=Replied To
gloda.message.attr.repliedTo.facetTooltip=Has this message been replied to?
# LOCALIZATION NOTE (gloda.message.attr.forwarded.*): Stores whether we believe
# the user has ever forwarded the message. We normally show a little icon in
# the thread pane when this is the case.
gloda.message.attr.forwarded.facetLabel=Forwarded
gloda.message.attr.forwarded.facetTooltip=Has this message been forwarded to anyone?
# LOCALIZATION NOTE (gloda.mimetype.category.*.label): Map categories of MIME
# types defined in mimeTypeCategories.js to labels.
# LOCALIZATION NOTE (gloda.mimetype.category.archives.label): Archive is
# referring to things like zip files, tar files, tar.gz files, etc.
gloda.mimetype.category.archives.label=Archives
gloda.mimetype.category.documents.label=Documents
gloda.mimetype.category.images.label=Images
# LOCALIZATION NOTE (gloda.mimetype.category.media.label): Media is meant to
# encompass both audio and video. This is because video and audio streams are
# frequently stored in the same type of container and we cannot rely on the
# sending e-mail client to have been clever enough to figure out what was
# really in the file. So we group them together.
gloda.mimetype.category.media.label=Media (Audio, Video)
gloda.mimetype.category.pdf.label=PDF Files
# LOCALIZATION NOTE (gloda.mimetype.category.other.label): Other is the category
# for MIME types that we don't really know what it is.
gloda.mimetype.category.other.label=Other

Просмотреть файл

@ -0,0 +1,4 @@
<!-- LOCALIZATION NOTE (glodaFacetView.filters.label): Label at the top of the
faceting sidebar. Serves as a header both for the checkboxes under it as
well for labeled facets with multiple options. -->
<!ENTITY glodaFacetView.filters.label "Filters">

Просмотреть файл

@ -0,0 +1,115 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Thunderbird Global Database
#
# The Initial Developer of the Original Code is
# Mozilla Messaging, Inc.
# Portions created by the Initial Developer are Copyright (C) 2009
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Andrew Sutherland <asutherland@asutherland.org>
#
# Alternatively, the contents of this file may be used under the terms of
# either of the GNU General Public License Version 2 or later (the "GPL"),
# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# LOCALIZATION NOTE (glodaFacetView.tab.query.label): The title to display for
# tabs that are based on a gloda (global database) query or collection rather
# than a user search. In the case of a user search, we just display the
# search string they entered. At some point we might try and explain what
# the query/collection is an automatic fashion, but not today.
glodaFacetView.tab.query.label=Search
# LOCALIZATION NOTE(glodaFacetView.constraints.query.fulltext.label):
# The label to display to describe when our base query was a fulltext search
# across messages. The value is displayed following the label.
glodaFacetView.constraints.query.fulltext.label=Searching for #1
glodaFacetView.constraints.query.fulltext.andJoinWord=and
glodaFacetView.constraints.query.fulltext.orJoinWord=or
glodaFacetView.constraints.query.fulltext.changeToAndLabel=require all terms instead
glodaFacetView.constraints.query.fulltext.changeToOrLabel=require any of the terms instead
# LOCALIZATION NOTE(glodaFacetView.constraints.query.initial):
# The label to display to describe when our base query is not a full-text
# search. Additional labels are appended describing each constraint.
glodaFacetView.constraints.query.initial=Searching for messages
# LOCALIZATION NOTE(glodaFacetView.constraints.query.involves.label):
# The label to display to describe when our base query was on messages
# involving a given contact from the address book. The value is displayed
# where the #1 is.
glodaFacetView.constraints.query.involves.label=involving #1
# LOCALIZATION NOTE(glodaFacetView.constraints.query.contact.label):
# The label to display to describe when our base query was on messages
# tagged with a specific tag. The tag is displayed following the label.
glodaFacetView.constraints.query.tagged.label=tagged:
# LOCALIZATION NOTE (glodaFacetView.facets.mode.top.otherLabel): The label to
# use for the "other" category where we lump everything that is not one of the
# top groups. Includes an argument which is the number of groups that are
# collapsed into this group.
glodaFacetView.facets.mode.top.otherLabel=Other (%S)
# LOCALIZATION NOTE (glodaFacetView.facets.noneLabel): The text to display when
# a facet needs to indicate that an attribute omitted a value or was otherwise
# empty.
glodaFacetView.facets.noneLabel=None
# LOCALIZATION NOTE (glodaFacetView.facets.filter.attachmentTypes.allLabel):
# The label to use when all types of attachments are being displayed.
glodaFacetView.facets.filter.attachmentTypes.allLabel=Any Kind
# LOCALIZATION NOTE (glodaFacetView.result.message.writesLabel): Used in the
# faceted search message display to delinate the author of a message and the
# recipients. An example usage is "Alice writes Bob, Chuck, Don".
glodaFacetView.result.message.writesLabel=writes
# LOCALIZATION NOTE(glodaFacetView.results.message.countLabel): Displays the
# number of messages displayed in the result area out of the number of
# messages in the active set (the set of messages remaining after the
# application of the facet constraints.) It takes the following arguments,
# you do not have to use all of them.
# #1: The number of messages displayed in the result area.
# #2: The pluralized form of "messages" (or whatever you provide in
# glodaFacetView.results.message.countLabelMessagePlurals) as it
# applies to #1 (the number of messages in the result area.)
# #3: The number of messages in the active set.
# #4: The pluralized form of "messages" as it applies to #3.
glodaFacetView.results.message.countLabel=Top #1 #2 out of #3
# LOCALIZATION NOTE(glodaFacetView.results.message.countLabelMessagePlurals):
# The plural forms of "messages" or whatever you choose. See
# https://developer.mozilla.org/en/Localization_and_Plurals for details on
# how this stuff works.
glodaFacetView.results.message.countLabelMessagePlurals=message;messages
# LOCALIZATION NOTE(glodaFacetView.results.message.showAllInList.label): The
# label for the button/link that causes us to display all of the messages in
# the active set in a new thread pane display tab, closing the current faceting
# tab.
glodaFacetView.results.message.showAllInList.label=Show all as list
# LOCALIZATION NOTE(glodaFacetView.results.message.showAllInList.tooltip): The
# tooltip to display when hovering over the showAllInList label.
glodaFacetView.results.message.showAllInList.tooltip=Show all of the messages in the active set in a new tab, closing this tab.

Просмотреть файл

@ -566,28 +566,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!-- Gloda Search Bar -->
<!ENTITY glodaSearchBar.emptyText "Search messages…">
<!ENTITY glodaSearchBar.facet.label "Search:">
<!-- LOCALIZATION NOTE (glodaSearchFacet.*) labels specify search constraints.
"everything" searches over subject, involves, body, and attachments.
"subject" searches only messages subjects.
"involves" searches only message to/from/cc/bcc.
"to" searches only message to/cc.
"from" searches only message using "from" (the message author).
"body" searches only message bodies, which does not include message
attachment names or the content of the attachments. Message body
basically means a message part with a content-type of text/*.
-->
<!ENTITY glodaSearchFacet.everything.label "Everything">
<!ENTITY glodaSearchFacet.subject.label "Subject">
<!ENTITY glodaSearchFacet.involves.label "Involves (from,to,cc,bcc)">
<!ENTITY glodaSearchFacet.to.label "To, CC">
<!ENTITY glodaSearchFacet.from.label "From">
<!ENTITY glodaSearchFacet.body.label "Message Text">
<!ENTITY glodaSearchFacet.attachmentNames.label "Attachment Names">
<!ENTITY glodaSearchBar.location.label "Location:">
<!ENTITY glodaSearchFacet.everywhere.label "All Folders">
<!ENTITY glodaSearchFacet.folder.label "In a specific folder…">
<!-- Quick Search Menu Bar -->
<!ENTITY searchSubjectMenu.label "Subject">
@ -618,15 +596,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY locationColumn.label "Location">
<!ENTITY idColumn.label "Order Received">
<!ENTITY attachmentColumn.label "Attachments">
<!-- LOCALIZATION NOTE (glodaWhyColumn.label): explains why a message is
present in the gloda search results. The values can be found in
messenger.properties with a prefix of "glodaSearch_results_why_". -->
<!ENTITY glodaWhyColumn.label "Why">
<!-- LOCALIZATION NOTE (glodaScoreColumn.label): provides the numerical
score assigned to the message as a result of the search. The column
primarily exists to be sorted by, and its contents will probably have
little meaning for most users. -->
<!ENTITY glodaScoreColumn.label "Score">
<!-- Thread Pane Tooltips -->
<!ENTITY columnChooser.tooltip "Click to select columns to display">
@ -649,8 +618,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY locationColumn.tooltip "Click to sort by location">
<!ENTITY idColumn.tooltip "Click to sort by order received">
<!ENTITY attachmentColumn.tooltip "Click to sort by attachments">
<!ENTITY glodaWhyColumn.tooltip "Click to sort by why the message is in your search results">
<!ENTITY glodaScoreColumn.tooltip "Click to sort by the search score">
<!-- Thread Pane Context Menu -->
<!ENTITY contextOpenNewWindow.label "Open Message in New Window">
@ -703,6 +670,7 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!-- Quick Search Bar -->
<!ENTITY quickSearchCmd.key "k">
<!ENTITY searchEverywhere.label "Search everywhere">
<!-- Message Header Context Menu -->
<!ENTITY AddToAddressBook.label "Add to Address Book…">

Просмотреть файл

@ -498,18 +498,6 @@ applyToCollapsedMsgs=Warning - this will delete messages in collapsed thread(s)
applyToCollapsedAlwaysAskCheckbox=Always ask me before deleting messages in collapsed threads
applyNowButton=Apply
#LOCALIZATION NOTE (glodaSearch_results_why_*): These strings populate the
# glodaWhyColumn when performing a gloda-backed search, explaining why a
# specific result is present in the search. Because of how the message grouping
# mechanism works, this value is both the value in the "Why" column as well as
# the header for the group (when sorting/grouping on the why column.) Short
# strings are preferable so the why column can be understood without taking up
# too much space.
glodaSearch_results_why_contact=Contact
glodaSearch_results_why_subject=Subject
glodaSearch_results_why_body=Body
glodaSearch_results_why_attachment=Attachment
mailServerLoginFailedTitle=Login Failed
# LOCALIZATION NOTE (mailServerLoginFailedTitle): Insert "%S" in your
# translation where you wish to display the hostname of the server to which

Просмотреть файл

@ -0,0 +1,8 @@
searchEverywhere.label=Search everywhere
searchSubject.label=Subject filter
searchFrom.label=From filter
searchFromOrSubject.label=Subject or From filter
searchRecipient.label=To or Cc filter
searchRecipientOrSubject.label=Subject, To, or Cc filter
searchBody.label=Entire message filter
saveAsVirtualFolder.label=Save search as virtual folder

Просмотреть файл

@ -21,7 +21,3 @@ labelForSearchButton.accesskey=S
moreButtonTooltipText=Add a new rule
lessButtonTooltipText=Remove this rule
# LOCALIZATION NOTE (glodaSearchTabTitle): The title to use for global database
# search tabs. Include "%S" where you want the search string to be inserted.
glodaSearchTabTitle=Search: %S

Просмотреть файл

@ -95,6 +95,10 @@
locale/@AB_CD@/messenger/outlookImportMsgs.properties (%chrome/messenger/outlookImportMsgs.properties)
locale/@AB_CD@/messenger/shutdownWindow.properties (%chrome/messenger/shutdownWindow.properties)
locale/@AB_CD@/messenger/configEditorOverlay.dtd (%chrome/messenger/configEditorOverlay.dtd)
locale/@AB_CD@/messenger/quickSearch.properties (%chrome/messenger/quickSearch.properties)
locale/@AB_CD@/messenger/gloda.properties (%chrome/messenger/gloda.properties)
locale/@AB_CD@/messenger/glodaFacetView.properties (%chrome/messenger/glodaFacetView.properties)
locale/@AB_CD@/messenger/glodaFacetView.dtd (%chrome/messenger/glodaFacetView.dtd)
locale/@AB_CD@/messenger/addressbook/abMainWindow.dtd (%chrome/messenger/addressbook/abMainWindow.dtd)
locale/@AB_CD@/messenger/addressbook/abNewCardDialog.dtd (%chrome/messenger/addressbook/abNewCardDialog.dtd)
locale/@AB_CD@/messenger/addressbook/abContactsPanel.dtd (%chrome/messenger/addressbook/abContactsPanel.dtd)

Просмотреть файл

@ -37,11 +37,7 @@
# ***** END LICENSE BLOCK *****
*/
#searchInput[searchCriteria="true"] {
color: grey;
}
#quick-search-button {
.quick-search-button {
-moz-margin-start: -10px;
-moz-margin-end: 4px;
margin-bottom: 0;

Просмотреть файл

@ -1820,7 +1820,7 @@ DBViewWrapper.prototype = {
aData);
// - persist the view to the folder.
if (!aDoNotPersist) {
if (!aDoNotPersist && this.displayedFolder) {
let msgDatabase = this.displayedFolder.msgDatabase;
if (msgDatabase) {
let dbFolderInfo = msgDatabase.dBFolderInfo;
@ -1901,6 +1901,8 @@ DBViewWrapper.prototype = {
*/
getMsgHdrForMessageID: function DBViewWrapper_getMsgHdrForMessageID(
aMessageId) {
if (this._syntheticView)
return this._syntheticView.getMsgHdrForMessageID(aMessageId);
if (!this._underlyingFolders)
return null;
for (let [, folder] in Iterator(this._underlyingFolders)) {

Просмотреть файл

@ -53,6 +53,14 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://app/modules/errUtils.js");
try {
Cu.import("resource://app/modules/StringBundle.js");
} catch (e) {
logException(e);
}
const nsMsgSearchScope = Ci.nsMsgSearchScope;
const nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
const nsMsgSearchOp = Ci.nsMsgSearchOp;
@ -69,11 +77,13 @@ var QuickSearchConstants = {
kQuickSearchSubject: 0,
kQuickSearchFrom: 1,
kQuickSearchFromOrSubject: 2,
kQuickSearchBody: 3,
// there used to be a kQuickSearchHighlight = 4, apparently removed
kQuickSearchRecipient: 5,
kQuickSearchRecipientOrSubject: 6,
kQuickSearchRecipient: 3,
kQuickSearchRecipientOrSubject: 4,
kQuickSearchBody: 5
};
const kQuickSearchCount = 6;
var QuickSearchLabels = null; // populated dynamically from properties files
/**
* All quick search logic that takes us from a search string (and search mode)
@ -82,6 +92,36 @@ var QuickSearchConstants = {
* actual nsIMsgDBView-related logic.
*/
var QuickSearchManager = {
_modeLabels: {},
/** populate an associative array containing the labels from a properties file
**/
loadLabels: function QuickSearchManager_loadLabels() {
const quickSearchStrings =
new StringBundle("chrome://messenger/locale/quickSearch.properties");
this._modeLabels[QuickSearchConstants.kQuickSearchSubject] = quickSearchStrings.get("searchSubject.label");
this._modeLabels[QuickSearchConstants.kQuickSearchFrom] = quickSearchStrings.get("searchFrom.label");
this._modeLabels[QuickSearchConstants.kQuickSearchFromOrSubject] = quickSearchStrings.get("searchFromOrSubject.label");
this._modeLabels[QuickSearchConstants.kQuickSearchRecipient] = quickSearchStrings.get("searchRecipient.label");
this._modeLabels[QuickSearchConstants.kQuickSearchRecipientOrSubject] = quickSearchStrings.get("searchRecipientOrSubject.label");
this._modeLabels[QuickSearchConstants.kQuickSearchBody] = quickSearchStrings.get("searchBody.label");
},
/** create the structure that the UI needs to fully describe a quick search
mode.
@return a list of array objects mapping 'value' to the constant specified in
QuickSearchConstants, and 'label' to a localized string.
**/
getSearchModes: function QuickSearchManager_getSearchModes() {
let modes =[];
for (let i = 0; i < kQuickSearchCount; i++)
modes.push({'value': i, 'label': this._modeLabels[i]});
return modes;
},
/**
* Create the search terms for the given quick-search configuration. This is
* intended to basically be directly used in the service of the UI without
@ -183,4 +223,6 @@ var QuickSearchManager = {
return searchTerms.length ? searchTerms : null;
}
};
};
QuickSearchManager.loadLabels();

Просмотреть файл

@ -91,6 +91,7 @@ Stringifier.prototype = {
},
dumpDOM: function(node, level, recursive) {
this._reset();
let s = this.DOMNodeAsString(node, level, recursive);
dump(s);
},
@ -201,7 +202,7 @@ Stringifier.prototype = {
s += pfx + tee + i + " (" + t + ") " + o[i] + "\n";
}
} catch (ex) {
s += pfx + tee + i + " (exception) " + ex + "\n";
s += pfx + tee + " (exception) " + ex + "\n";
}
if (!compress)
s += pfx + "|\n";
@ -219,12 +220,10 @@ Stringifier.prototype = {
},
DOMNodeAsString: function(node, level, recursive) {
this._reset();
if (level === undefined)
level = 0
if (recursive === undefined)
recursive = true;
this._append(this._repeatStr(" ", 2*level) + "<" + node.nodeName + "\n");
if (node.nodeType == 3) {
@ -244,7 +243,7 @@ Stringifier.prototype = {
else if (recursive) {
this._append(this._repeatStr(" ", (2*level)) + ">\n");
for (let i = 0; i < node.childNodes.length; i++) {
this.dumpDOM(node.childNodes[i], level + 1);
this._append(this.DOMNodeAsString(node.childNodes[i], level + 1));
}
this._append(this._repeatStr(" ", 2*level) + "</" + node.nodeName + ">\n");
}

Просмотреть файл

@ -42,8 +42,7 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var LOG = null;
Cu.import("resource://gre/modules/errUtils.js");
var Gloda = null;
var GlodaUtils = null;
@ -51,15 +50,28 @@ var MultiSuffixTree = null;
var TagNoun = null;
var FreeTagNoun = null;
function ResultRowFullText(aItem, words, typeForStyle, andTerms) {
this.item = aItem;
this.words = words;
this.andTerms = andTerms;
this.typeForStyle = "gloda-fulltext-" + typeForStyle;
}
ResultRowFullText.prototype = {
multi: false,
fullText: true
};
function ResultRowSingle(aItem, aCriteriaType, aCriteria, aExplicitNounID) {
this.nounID = aExplicitNounID || aItem.NOUN_ID;
this.nounDef = Gloda._nounIDToDef[this.nounID];
this.criteriaType = aCriteriaType;
this.criteria = aCriteria;
this.item = aItem;
this.typeForStyle = "gloda-single-" + this.nounDef.name;
}
ResultRowSingle.prototype = {
multi: false
multi: false,
fullText: false
};
function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
@ -73,12 +85,11 @@ function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
}
ResultRowMulti.prototype = {
multi: true,
typeForStyle: "gloda-multi",
fullText: false,
onItemsAdded: function(aItems) {
LOG.debug("RRM onItemsAdded: " + aItems.length + ": " + aItems);
if (this.renderer) {
LOG.debug("RRM rendering...");
for each (let [iItem, item] in Iterator(aItems)) {
LOG.debug("RRM ..." + item);
this.renderer.renderItem(item);
}
}
@ -110,15 +121,12 @@ nsAutoCompleteGlodaResult.prototype = {
},
markCompleted: function ACGR_markCompleted(aCompleter) {
if (--this._pendingCount == 0) {
LOG.debug("Notifying completion.");
this.listener.onSearchResult(this.completer, this);
}
},
addRows: function ACGR_addRows(aRows) {
if (!aRows.length)
return;
LOG.debug("Adding " + aRows.length + " rows (" + this._pendingCount +
" jobs still pending)");
this._results.push.apply(this._results, aRows);
this.listener.onSearchResult(this.completer, this);
},
@ -157,10 +165,7 @@ nsAutoCompleteGlodaResult.prototype = {
// rich uses this to be the "type"
getStyleAt: function(aIndex) {
let row = this._results[aIndex];
if (row.multi)
return "gloda-multi";
else
return "gloda-single-" + row.nounDef.name;
return row.typeForStyle;
},
// rich uses this to be the icon
getImageAt: function(aIndex) {
@ -176,7 +181,7 @@ nsAutoCompleteGlodaResult.prototype = {
removeValueAt: function() {},
_stop: function() {
},
}
};
const MAX_POPULAR_CONTACTS = 200;
@ -202,7 +207,6 @@ ContactIdentityCompleter.prototype = {
let matches;
if (this.suffixTree) {
matches = this.suffixTree.findMatches(aString.toLowerCase());
LOG.debug("CIC: Suffix Tree found " + matches.length + " matches.")
}
else
matches = [];
@ -231,13 +235,11 @@ ContactIdentityCompleter.prototype = {
// - match against database contacts / identities
let pending = {contactToThing: contactToThing, pendingCount: 2};
LOG.debug("CIC: issuing contact LIKE query");
let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
contactQuery.nameLike(contactQuery.WILD, aString, contactQuery.WILD);
pending.contactColl = contactQuery.getCollection(this, aResult);
pending.contactColl.becomeExplicit();
LOG.debug("CIC: issuing identity LIKE query");
let identityQuery = Gloda.newQuery(Gloda.NOUN_IDENTITY);
identityQuery.kind("email").valueLike(identityQuery.WILD, aString,
identityQuery.WILD);
@ -257,7 +259,6 @@ ContactIdentityCompleter.prototype = {
onQueryCompleted: function(aCollection) {
// handle the initial setup case...
if (aCollection.data == null) {
LOG.debug("CIC: Initial query found " + aCollection.items.length);
// cheat and explicitly add our own contact...
if (!(Gloda.myContact.id in this.contactCollection._idMap))
this.contactCollection._onItemsAdded([Gloda.myContact]);
@ -290,8 +291,6 @@ ContactIdentityCompleter.prototype = {
return;
}
LOG.debug("CIC: LIKE query found " + aCollection.items.length);
// handle the completion case
let result = aCollection.data;
let pending = result._contactCompleterPending;
@ -337,9 +336,6 @@ ContactIdentityCompleter.prototype = {
// the result object no longer needs us or our data
delete result._contactCompleterPending;
}
else {
LOG.debug("ignoring... pending is still: " + pending.pendingCount);
}
}
};
@ -357,7 +353,6 @@ ContactTagCompleter.prototype = {
for (let [tagName, tag] in Iterator(FreeTagNoun.knownFreeTags)) {
tagNames.push(tagName.toLowerCase());
tags.push(tag);
LOG.debug("contact tag: " + tagName);
}
this._suffixTree = new MultiSuffixTree(tagNames, tags);
this._suffixTreeDirty = false;
@ -373,13 +368,10 @@ ContactTagCompleter.prototype = {
if (aString.length < 2)
return false; // no async mechanism that will add new rows
LOG.debug("Completing on contact tags...");
tags = this._suffixTree.findMatches(aString.toLowerCase());
let rows = [];
for each (let [iTag, tag] in Iterator(tags)) {
let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
LOG.debug(" checking for contact tag: " + tag.name);
query.freeTags(tag);
let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
query);
@ -405,7 +397,6 @@ MessageTagCompleter.prototype = {
let tag = tagArray[iTag];
tagNames.push(tag.tag.toLowerCase());
tags.push(tag);
LOG.debug("message tag: " + tag.tag);
}
this._suffixTree = new MultiSuffixTree(tagNames, tags);
this._suffixTreeDirty = false;
@ -414,12 +405,9 @@ MessageTagCompleter.prototype = {
if (aString.length < 2)
return false;
LOG.debug("Completing on message tags...");
tags = this._suffixTree.findMatches(aString.toLowerCase());
let rows = [];
for each (let [, tag] in Iterator(tags)) {
LOG.debug(" found message tag: " + tag.tag);
let resRow = new ResultRowSingle(tag, "tag", tag.tag, TagNoun.id);
rows.push(resRow);
}
@ -429,76 +417,94 @@ MessageTagCompleter.prototype = {
}
};
/**
* Complete with helpful hints about full-text search
*/
function FullTextCompleter() {
}
FullTextCompleter.prototype = {
complete: function FullTextCompleter_complete(aResult, aString) {
if (aString.length < 2)
return false;
let rows = [];
let words = aString.trim().replace(/\s+/g, ' ').split(' ');
let numWords = words.length;
if (numWords == 1) {
let resRow = new ResultRowFullText(aString, words, "single", false);
rows.push(resRow);
} else {
let resRow = new ResultRowFullText(aString, words, "all", true);
rows.push(resRow);
resRow = new ResultRowFullText(aString, words, "any", false);
rows.push(resRow);
}
aResult.addRows(rows);
return false; // no async mechanism that will add new rows
}
};
function nsAutoCompleteGloda() {
this.wrappedJSObject = this;
try {
// set up our awesome globals!
if (Gloda === null) {
let loadNS = {};
Cu.import("resource://app/modules/gloda/public.js", loadNS);
Gloda = loadNS.Gloda;
Cu.import("resource://app/modules/gloda/utils.js", loadNS);
GlodaUtils = loadNS.GlodaUtils;
Cu.import("resource://app/modules/gloda/suffixtree.js", loadNS);
MultiSuffixTree = loadNS.MultiSuffixTree;
Cu.import("resource://app/modules/gloda/noun_tag.js", loadNS);
TagNoun = loadNS.TagNoun;
Cu.import("resource://app/modules/gloda/noun_freetag.js", loadNS);
FreeTagNoun = loadNS.FreeTagNoun;
Cu.import("resource://app/modules/gloda/log4moz.js", loadNS);
LOG = loadNS["Log4Moz"].repository.getLogger("gloda.autocomp");
}
// set up our awesome globals!
if (Gloda === null) {
let loadNS = {};
this.completers = [];
this.curResult = null;
Cu.import("resource://app/modules/gloda/public.js", loadNS);
Gloda = loadNS.Gloda;
Cu.import("resource://app/modules/gloda/utils.js", loadNS);
GlodaUtils = loadNS.GlodaUtils;
Cu.import("resource://app/modules/gloda/suffixtree.js", loadNS);
MultiSuffixTree = loadNS.MultiSuffixTree;
Cu.import("resource://app/modules/gloda/noun_tag.js", loadNS);
TagNoun = loadNS.TagNoun;
Cu.import("resource://app/modules/gloda/noun_freetag.js", loadNS);
FreeTagNoun = loadNS.FreeTagNoun;
Cu.import("resource://app/modules/gloda/log4moz.js", loadNS);
LOG = loadNS["Log4Moz"].repository.getLogger("gloda.autocomp");
this.completers.push(new FullTextCompleter());
this.completers.push(new ContactIdentityCompleter());
this.completers.push(new ContactTagCompleter());
this.completers.push(new MessageTagCompleter());
} catch (e) {
logException(e);
}
LOG.debug("initializing completers");
this.completers = [];
this.curResult = null;
dump("init CIC\n");
LOG.debug("initializing ContactIdentityCompleter");
try {
this.completers.push(new ContactIdentityCompleter());
} catch (ex) {dump("CICEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
dump("init CTC\n");
LOG.debug("initializing ContactTagCompleter");
this.completers.push(new ContactTagCompleter());
dump("init MTC\n");
LOG.debug("initializing MessageTagCompleter");
try {
this.completers.push(new MessageTagCompleter());
} catch (ex) {dump("MTCEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
LOG.debug("initialized completers");
}
nsAutoCompleteGloda.prototype = {
classDescription: "AutoCompleteGloda",
contractID: "@mozilla.org/autocomplete/search;1?name=gloda",
classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476c}"),
classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476d}"),
QueryInterface: XPCOMUtils.generateQI([
Components.interfaces.nsIAutoCompleteSearch]),
startSearch: function(aString, aParam, aResult, aListener) {
let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
// save this for hacky access to the search. I somewhat suspect we simply
// should not be using the formal autocomplete mechanism at all.
this.curResult = result;
for each (let [iCompleter, completer] in Iterator(this.completers)) {
// they will return true if they have something pending.
if (completer.complete(result, aString))
result.markPending(completer);
try {
let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
// save this for hacky access to the search. I somewhat suspect we simply
// should not be using the formal autocomplete mechanism at all.
this.curResult = result;
for each (let [iCompleter, completer] in Iterator(this.completers)) {
// they will return true if they have something pending.
if (completer.complete(result, aString))
result.markPending(completer);
}
aListener.onSearchResult(this, result);
} catch (e) {
logException(e);
}
aListener.onSearchResult(this, result);
},
stopSearch: function() {
},
}
};
function NSGetModule(compMgr, fileSpec) {

Просмотреть файл

@ -1,5 +1,6 @@
textbox[type="glodacomplete"] {
-moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete");
width: 400px;
}
panel[type="glodacomplete-richlistbox"] {
@ -16,6 +17,15 @@ panel[type="glodacomplete-richlistbox"] {
overflow-x: hidden !important;
}
.parameters {
font-style: italic;
margin-left: 1em;
}
.picture {
margin: 1ex;
margin-right: 1em;
}
.autocomplete-richlistitem[type="gloda-single-tag"] {
-moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-tag-item");
overflow: -moz-hidden-unscrollable;
@ -27,6 +37,21 @@ panel[type="glodacomplete-richlistbox"] {
overflow: -moz-hidden-unscrollable;
}
.autocomplete-richlistitem[type="gloda-fulltext-single"] {
-moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-fulltext-single-item");
overflow: -moz-hidden-unscrollable;
}
.autocomplete-richlistitem[type="gloda-fulltext-any"] {
-moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-fulltext-any-item");
overflow: -moz-hidden-unscrollable;
}
.autocomplete-richlistitem[type="gloda-fulltext-all"] {
-moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-fulltext-all-item");
overflow: -moz-hidden-unscrollable;
}
richlistitem[type="gloda-contact-chunk"] {
-moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-contact-chunk");
-moz-box-orient: vertical;

Просмотреть файл

@ -360,7 +360,114 @@
<method name="_adjustAcItem">
<body>
<![CDATA[
this._explanation.value = "messages tagged " + this.row.item.tag;
this._explanation.value = "messages tagged " + this.row.item.tag; // XXX l10n
]]>
</body>
</method>
</implementation>
</binding>
<binding id="gloda-fulltext-single-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
<content orient="vertical">
<xul:description anonid="explanation"/>
<xul:description anonid="parameters"/>
</content>
<implementation implements="nsIDOMXULSelectControlItemElement">
<constructor>
<![CDATA[
this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
this._adjustAcItem();
]]>
</constructor>
<property name="label" readonly="true">
<getter>
<![CDATA[
return "full text search: " + this.row.item;
]]>
</getter>
</property>
<method name="_adjustAcItem">
<body>
<![CDATA[
try {
this._explanation.value = "messages mentioning " + this.row.item; // XXX l10n
} catch (e) {
logException(e);
}
]]>
</body>
</method>
</implementation>
</binding>
<binding id="gloda-fulltext-any-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
<content orient="vertical">
<xul:description anonid="explanation"/>
<xul:description anonid="parameters" class="parameters"/>
</content>
<implementation implements="nsIDOMXULSelectControlItemElement">
<constructor>
<![CDATA[
this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
this._parameters = document.getAnonymousElementByAttribute(this, "anonid", "parameters");
this._adjustAcItem();
]]>
</constructor>
<property name="label" readonly="true">
<getter>
<![CDATA[
return "full text search: " + this.row.item;
]]>
</getter>
</property>
<method name="_adjustAcItem">
<body>
<![CDATA[
this._explanation.value = "messages with ANY of: "
this._parameters.value = this.row.words.join(", "); // XXX l10n
]]>
</body>
</method>
</implementation>
</binding>
<binding id="gloda-fulltext-all-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
<content orient="vertical">
<xul:description anonid="explanation"/>
<xul:description anonid="parameters" class="parameters"/>
</content>
<implementation implements="nsIDOMXULSelectControlItemElement">
<constructor>
<![CDATA[
this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
this._parameters = document.getAnonymousElementByAttribute(this, "anonid", "parameters");
this._adjustAcItem();
]]>
</constructor>
<property name="label" readonly="true">
<getter>
<![CDATA[
return "full text search: " + this.row.item;
]]>
</getter>
</property>
<method name="_adjustAcItem">
<body>
<![CDATA[
this._explanation.value = "messages with ALL of: "
this._parameters.value = this.row.words.join(", "); // XXX l10n
]]>
</body>
</method>
@ -370,7 +477,7 @@
<binding id="gloda-single-identity-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
<content>
<xul:hbox>
<xul:image anonid="picture"/>
<xul:image anonid="picture" class="picture"/>
<xul:vbox>
<xul:hbox>
<xul:hbox anonid="name-box" class="ac-title" flex="1"

Просмотреть файл

@ -597,6 +597,7 @@ GlodaCollection.prototype = {
},
_onQueryCompleted: function gloda_coll_onQueryCompleted() {
this.query.completed = true;
if (this._listener && this._listener.onQueryCompleted)
this._listener.onQueryCompleted(this);
}

Просмотреть файл

@ -206,6 +206,10 @@ GlodaConversation.prototype = {
toString: function gloda_conversation_toString() {
return "Conversation:" + this._id;
},
toLocaleString: function gloda_conversation_toLocaleString() {
return this._subject;
}
};
function GlodaFolder(aDatastore, aID, aURI, aDirtyStatus, aPrettyName,
@ -265,6 +269,14 @@ GlodaFolder.prototype = {
return "Folder:" + this._id;
},
toLocaleString: function gloda_folder_toLocaleString() {
let xpcomFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
if (!xpcomFolder)
return this._prettyName;
return xpcomFolder.prettiestName +
" (" + xpcomFolder.rootFolder.prettiestName + ")";
},
get indexingPriority() {
return this._indexingPriority;
},
@ -273,6 +285,9 @@ GlodaFolder.prototype = {
kActivityIndexing: 0,
/** Asking for the folder to perform header retrievals. */
kActivityHeaderRetrieval: 1,
/** We only want the folder for its metadata but are not going to open it. */
kActivityFolderOnlyNoData: 2,
/** Is this folder known to be actively used for indexing? */
_activeIndexing: false,
@ -324,6 +339,9 @@ GlodaFolder.prototype = {
this._datastore.markFolderLive(this);
this._activeHeaderRetrievalLastStamp = Date.now();
break;
case this.kActivityFolderOnlyNoData:
// we don't have to do anything here.
break;
}
return this._xpcomFolder;
@ -628,6 +646,12 @@ GlodaIdentity.prototype = {
return "Identity:" + this._kind + ":" + this._value;
},
toLocaleString: function gloda_identity_toLocaleString() {
if (this.contact.name == this.value)
return this.value;
return this.contact.name + " : " + this.value;
},
get abCard() {
// for our purposes, the address book only speaks email
if (this._kind != "email")

Просмотреть файл

@ -553,7 +553,7 @@ var GlodaDatastore = {
/* ******************* SCHEMA ******************* */
_schemaVersion: 12,
_schemaVersion: 13,
_schema: {
tables: {
@ -988,6 +988,9 @@ var GlodaDatastore = {
* Create a table for a noun, replete with data binding.
*/
createNounTable: function gloda_ds_createTableIfNotExists(aNounDef) {
// give it a _jsonText attribute if appropriate...
if (aNounDef.allowsArbitraryAttrs)
aNounDef.schema.columns.push(['jsonAttributes', 'STRING', '_jsonText']);
// check if the table exists
if (!this.asyncConnection.tableExists(aNounDef.tableName)) {
// it doesn't! create it (and its potentially many variants)
@ -1028,7 +1031,15 @@ var GlodaDatastore = {
// - notability column added
// version 13:
// - we are adding a new fulltext index column. blow away!
if (aCurVersion < 13) {
// - note that I screwed up and failed to mark the schema change; apparently
// no database will claim to be version 13...
// version 14:
// - new attributes: forwarded, repliedTo, bcc, recipients
// - altered fromMeTo and fromMeCc to fromMe
// - altered toMe and ccMe to just be toMe
// - exposes bcc to cc-related attributes
// - MIME type DB schema overhaul
if (aCurVersion < 14) {
aDBConnection.close();
aDBFile.remove(false);
this._log.warn("Global database has been purged due to schema change.");
@ -2099,7 +2110,7 @@ var GlodaDatastore = {
else
jsonText = aRow.getString(7);
// only queryFromQuery queries will have these columns
if (aRow.numEntries == 14) {
if (aRow.numEntries >= 14) {
if (aRow.getTypeOfIndex(9) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
subject = undefined;
else
@ -2980,7 +2991,7 @@ var GlodaDatastore = {
}
if (aListenerData) {
if (collection.dataStack)
collection.dataStack.push(aListenerData)
collection.dataStack.push(aListenerData);
else
collection.dataStack = [aListenerData];
}

Просмотреть файл

@ -40,7 +40,7 @@
* nsIMsgDBView.
*/
EXPORTED_SYMBOLS = ["GlodaSyntheticSearchView", "GlodaViewFactory"];
EXPORTED_SYMBOLS = ["GlodaSyntheticView"];
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -52,82 +52,41 @@ Cu.import("resource://app/modules/gloda/log4moz.js");
Cu.import("resource://app/modules/gloda/public.js");
Cu.import("resource://app/modules/gloda/msg_search.js");
function GlodaScoreColumn(aSearcher) {
this.searcher = aSearcher;
/**
* Create a synthetic view suitable for passing to |FolderDisplayWidget.show|.
* You must pass a query, collection, or conversation in.
*
* @param {GlodaQuery} [aArgs.query] A gloda query to run.
* @param {GlodaCollection} [aArgs.collection] An already-populated collection
* to display. Do not call getCollection on a query and hand us that. We
* will not register ourselves as a listener and things will not work.
* @param {GlodaConversation} [aArgs.conversation] A conversation whose messages
* you want to display.
*/
function GlodaSyntheticView(aArgs) {
if ("query" in aArgs) {
this.query = aArgs.query;
this.collection = this.query.getCollection(this);
this.completed = false;
}
else if ("collection" in aArgs) {
this.query = null;
this.collection = aArgs.collection;
this.completed = true;
}
else if ("conversation" in aArgs) {
this.collection = aArgs.conversation.getMessagesCollection(this);
this.query = this.collection.query;
this.completed = false;
}
else {
throw new Error("You need to pass a query or collection");
}
this.customColumns = [];
}
GlodaScoreColumn.prototype = {
id: "glodaScoreCol",
bindToView: function (aDBView) {
this.dbView = aDBView;
},
getCellText: function(row, col) {
let folder = this.dbView.getFolderForViewIndex(row);
let key = this.dbView.getKeyAt(row);
return "" + this.searcher.scoresByUriAndKey[folder.URI + "-" + key];
},
getSortLongForRow: function(hdr) {
return this.searcher.scoresByUriAndKey[
hdr.folder.URI + "-" + hdr.messageKey] || 0;
},
isString: function() {
return false;
},
getCellProperties: function(row, col, props){},
getRowProperties: function(row, props){},
getImageSrc: function(row, col) {return null;},
getSortStringForRow: function(hdr) {
return null;
},
};
function GlodaWhyColumn(aSearcher) {
this.searcher = aSearcher;
}
GlodaWhyColumn.prototype = {
id: "glodaWhyCol",
bindToView: function (aDBView) {
this.dbView = aDBView;
},
getCellText: function(row, col) {
let folder = this.dbView.getFolderForViewIndex(row);
let key = this.dbView.getKeyAt(row);
return this.searcher.whysByUriAndKey[folder.URI + "-" + key] || "";
},
getSortStringForRow: function(hdr) {
return this.searcher.whysByUriAndKey[hdr.folder.URI + "-" + hdr.messageKey]
|| "";
},
isString: function() {
return true;
},
getCellProperties: function(row, col, props){},
getRowProperties: function(row, props){},
getImageSrc: function(row, col) {return null;},
getSortLongForRow: function(hdr) {return 0;}
};
function GlodaSyntheticSearchView(aSearchString, aFacetString, aLocation) {
this.searcher = new GlodaMsgSearcher(this, aSearchString.split(" "));
this._whyColumn = new GlodaWhyColumn(this.searcher);
this._scoreColumn = new GlodaScoreColumn(this.searcher);
this.customColumns = [this._whyColumn, this._scoreColumn];
this.collection = null;
this._whyMap = {};
this._scoreMap = {};
this.searchString = aSearchString;
this.facetString = aFacetString;
this.location = aLocation;
}
GlodaSyntheticSearchView.prototype = {
defaultSort: [["glodaScoreCol", Ci.nsMsgViewSortOrder.descending]],
GlodaSyntheticView.prototype = {
defaultSort: [["dateCol", Ci.nsMsgViewSortOrder.descending]],
/**
* Request the search be performed and notification provided to
@ -139,14 +98,12 @@ GlodaSyntheticSearchView.prototype = {
this.completionCallback = aCompletionCallback;
this.searchListener.onNewSearch();
if (this.collection) {
if (this.completed) {
this.reportResults(this.collection.items);
// we're not really aborting, but it closes things out nicely
this.abortSearch();
return;
}
this.collection = this.searcher.go();
},
abortSearch: function() {
@ -161,10 +118,26 @@ GlodaSyntheticSearchView.prototype = {
reportResults: function(aItems) {
for each (let [, item] in Iterator(aItems)) {
let hdr = item.folderMessage;
this.searchListener.onSearchHit(hdr, hdr.folder);
if (hdr)
this.searchListener.onSearchHit(hdr, hdr.folder);
}
},
/**
* Helper function used by |DBViewWrapper.getMsgHdrForMessageID| since there
* are no actual backing folders for it to check.
*/
getMsgHdrForMessageID: function(aMessageId) {
for each (let [, item] in this.collection.items) {
if (item.messageId == aMessageId) {
let hdr = item.folderMessage;
if (hdr)
return hdr;
}
}
return null;
},
// --- collection listener
onItemsAdded: function(aItems, aCollection) {
if (this.searchListener)
@ -175,18 +148,9 @@ GlodaSyntheticSearchView.prototype = {
onItemsRemoved: function(aItems, aCollection) {
},
onQueryCompleted: function(aCollection) {
this.completed = true;
this.searchListener.onSearchDone(Cr.NS_OK);
if (this.completionCallback)
this.completionCallback();
},
};
var GlodaViewFactory = {
kFacetEverything: "everything",
kFacetSubject: "subject",
kFacetBody: "body",
kFacetAttachments: "attachments",
kFacetInvolves: "involves",
kFacetTo: "to",
kFacetFrom: "from",
};

Просмотреть файл

@ -50,16 +50,17 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://app/modules/gloda/log4moz.js");
Cu.import("resource://app/modules/StringBundle.js");
Cu.import("resource://app/modules/gloda/utils.js");
Cu.import("resource://app/modules/gloda/gloda.js");
Cu.import("resource://app/modules/gloda/noun_tag.js");
const nsMsgMessageFlags_Replied = Ci.nsMsgMessageFlags.Replied;
const nsMsgMessageFlags_Forwarded = Ci.nsMsgMessageFlags.Forwarded;
const EXT_BUILTIN = "built-in";
const FA_TAG = "TAG";
const FA_STAR = "STAR";
const FA_READ = "READ";
/**
* @namespace Explicit attribute provider. Indexes/defines attributes that are
@ -68,6 +69,7 @@ const FA_READ = "READ";
*/
var GlodaExplicitAttr = {
providerName: "gloda.explattr",
strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
_log: null,
_msgTagService: null,
@ -93,10 +95,6 @@ var GlodaExplicitAttr = {
/** Boost for tagged messages, each additional tag. */
NOTABILITY_TAGGED_ADDL: 1,
_attrTag: null,
_attrStar: null,
_attrRead: null,
defineAttributes: function() {
// Tag
this._attrTag = Gloda.defineAttribute({
@ -106,6 +104,7 @@ var GlodaExplicitAttr = {
attributeName: "tag",
bindName: "tags",
singular: false,
facet: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_TAG,
parameterNoun: null,
@ -121,6 +120,7 @@ var GlodaExplicitAttr = {
attributeName: "star",
bindName: "starred",
singular: true,
facet: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_BOOLEAN,
parameterNoun: null,
@ -137,6 +137,33 @@ var GlodaExplicitAttr = {
parameterNoun: null,
}); // tested-by: test_attributes_explicit
/**
* Has this message been replied to by the user.
*/
this._attrRepliedTo = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrExplicit,
attributeName: "repliedTo",
singular: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_BOOLEAN,
parameterNoun: null,
}); // tested-by: test_attributes_explicit
/**
* Has this user forwarded this message to someone.
*/
this._attrForwarded = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrExplicit,
attributeName: "forwarded",
singular: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_BOOLEAN,
parameterNoun: null,
}); // tested-by: test_attributes_explicit
},
process: function Gloda_explattr_process(aGlodaMessage, aRawReps, aIsNew,
@ -149,6 +176,10 @@ var GlodaExplicitAttr = {
aGlodaMessage.read = aMsgHdr.isRead;
let flags = aMsgHdr.flags;
aGlodaMessage.repliedTo = Boolean(flags & nsMsgMessageFlags_Replied);
aGlodaMessage.forwarded = Boolean(flags & nsMsgMessageFlags_Forwarded);
let tags = aGlodaMessage.tags = [];
// -- Tag

Просмотреть файл

@ -0,0 +1,595 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Thunderbird Global Database.
*
* The Initial Developer of the Original Code is
* Mozilla Messaging, Inc.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Andrew Sutherland <asutherland@asutherland.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
* This file provides faceting logic.
*/
let EXPORTED_SYMBOLS = ["FacetDriver", "FacetUtils"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://app/modules/gloda/public.js");
/**
* Decides the appropriate faceters for the noun type and drives the faceting
* process. This class and the faceters are intended to be reusable so that
* you only need one instance per faceting session. (Although each faceting
* pass is accordingly destructive to previous results.)
*
* Our strategy for faceting is to process one attribute at a time across all
* the items in the provided set. The alternative would be to iterate over
* the items and then iterate over the attributes on each item. While both
* approaches have caching downsides
*/
function FacetDriver(aNounDef, aWindow) {
this.nounDef = aNounDef;
this._window = aWindow;
this._makeFaceters();
}
FacetDriver.prototype = {
/**
* Populate |this.faceters| with a set of faceters appropriate to the noun
* definition associated with this instance.
*/
_makeFaceters: function() {
let faceters = this.faceters = [];
for each (let [, attrDef] in Iterator(this.nounDef.attribsByBoundName)) {
// ignore attributes that do not want to be faceted
if (!attrDef.facet)
continue;
let facetType = attrDef.facet.type;
if (attrDef.singular) {
if (facetType == "date")
faceters.push(new DateFaceter(attrDef));
else
faceters.push(new DiscreteFaceter(attrDef));
}
else {
if (facetType == "nonempty?")
faceters.push(new NonEmptySetFaceter(attrDef));
else
faceters.push(new DiscreteSetFaceter(attrDef));
}
}
},
/**
* Asynchronously facet the provided items, calling the provided callback when
* completed.
*/
go: function FacetDriver_go(aItems, aCallback, aCallbackThis) {
this.items = aItems;
this.callback = aCallback;
this.callbackThis = aCallbackThis;
this._nextFaceter = 0;
this._drive();
},
_MAX_FACETING_TIMESLICE_MS: 100,
_FACETING_YIELD_DURATION_MS: 0,
_driveWrapper: function(aThis) {
aThis._drive();
},
_drive: function() {
let start = Date.now();
while (this._nextFaceter < this.faceters.length) {
let faceter = this.faceters[this._nextFaceter++];
// for now we facet in one go, but the long-term plan allows for them to
// be generators.
faceter.facetItems(this.items);
let delta = Date.now() - start;
if (delta > this._MAX_FACETING_TIMESLICE_MS) {
this._window.setTimeout(this._driveWrapper,
this._FACETING_YIELD_DURATION_MS,
this);
return;
}
}
// we only get here once we are done with the faceters
this.callback.call(this.callbackThis);
}
};
var FacetUtils = {
_groupSizeComparator: function(a, b) {
return b[1].length - a[1].length;
},
/**
* Given a list where each entry is a tuple of [group object, list of items
* belonging to that group], produce a new list of the top grouped items plus
* an "other" category.
*
* @param aAttrDef The attribute for the facet we are working with.
* @param aGroups The list of groups built for the facet.
* @param aMaxCount The number of result rows you want back. We will provide
* the aMaxCount-1 rows plus an "other" row which we put last.
* @param aOtherSentinel An object to use as the other sentinel object and the
* count of the number of groups that went into the other group. The
* count is found in the 'count' attribute of this object.
*/
makeTopGroups: function FacetUtils_makeTopGroups(aAttrDef, aGroups,
aMaxCount, aOtherSentinel) {
let nounDef = aAttrDef.objectNounDef;
let realGroupsToUse = aMaxCount - 1;
let orderedBySize = aGroups.concat();
orderedBySize.sort(this._groupSizeComparator);
// - get the real groups to use and order them by the attribute comparator
let outGroups = orderedBySize.slice(0, realGroupsToUse);
let comparator = nounDef.comparator;
function comparatorHelper(a, b) {
return comparator(a[0], b[0]);
}
outGroups.sort(comparatorHelper);
// - build the 'other' group
let otherItems, otherGroupValues;
// If the attribute is singular, we can just concatenate everybody together
if (aAttrDef.singular) {
// Since concat can take multiple arrays, build a list of all of the items
// except for the first one who we will issue the concat call against.
// (We could also just use an empty array as the base.)
let iGroup = realGroupsToUse;
otherGroupValues = [orderedBySize[iGroup][0]];
let firstItemList = orderedBySize[iGroup++][1];
let otherItemLists = [];
for (; iGroup < orderedBySize.length; iGroup++) {
otherGroupValues.push(orderedBySize[iGroup][0]);
otherItemLists.push(orderedBySize[iGroup][1]);
}
otherItems = firstItemList.concat.apply(firstItemList, otherItemLists);
}
// For non-singular attributes, we need to uniqify the contents. If we
// naively concatenated all the items, we might end up with duplicates.
else {
let idsSeen = {};
otherItems = [];
otherGroupValues = [];
for (let iGroup = realGroupsToUse; iGroup < orderedBySize.length;
iGroup++) {
otherGroupValues.push(orderedBySize[iGroup][0]);
for each (let [, item] in Iterator(orderedBySize[iGroup][1])) {
if (!(item.id in idsSeen)) {
idsSeen[item.id] = true;
otherItems.push(item);
}
}
}
}
aOtherSentinel.count = orderedBySize.length - realGroupsToUse;
aOtherSentinel.groupValues = otherGroupValues;
outGroups.push([aOtherSentinel, otherItems]);
return outGroups;
}
};
/**
* Facet discrete things like message authors, boolean values, etc. Only
* appropriate for use on singular values. Use |DiscreteSetFaceter| for
* non-singular values.
*/
function DiscreteFaceter(aAttrDef) {
this.attrDef = aAttrDef;
}
DiscreteFaceter.prototype = {
type: "discrete",
/**
* Facet the given set of items, deferring to the appropriate helper method
*/
facetItems: function(aItems) {
if (this.attrDef.objectNounDef.isPrimitive)
return this.facetPrimitiveItems(aItems);
else
return this.facetComplexItems(aItems);
},
/**
* Facet an attribute whose value is primitive, meaning that it is a raw
* numeric value or string, rather than a complex object.
*/
facetPrimitiveItems: function(aItems) {
let attrKey = this.attrDef.boundName;
let nounDef = this.attrDef.objectNounDef;
let valStrToVal = {};
let groups = this.groups = {};
this.groupCount = 0;
for each (let [, item] in Iterator(aItems)) {
let val = (attrKey in item) ? item[attrKey] : null;
if (val in groups)
groups[val].push(item);
else {
groups[val] = [item];
valStrToVal[val] = val;
this.groupCount++;
}
}
let orderedGroups = [[valStrToVal[key], items] for each
([key, items] in Iterator(groups))];
let comparator = nounDef.comparator;
function comparatorHelper(a, b) {
return comparator(a[0], b[0]);
}
orderedGroups.sort(comparatorHelper);
this.orderedGroups = orderedGroups;
},
/**
* Facet an attribute whose value is a complex object that can be identified
* by its 'id' attribute. This is the case where the value is itself a noun
* instance.
*/
facetComplexItems: function(aItems) {
let attrKey = this.attrDef.boundName;
let nounDef = this.attrDef.objectNounDef;
let idAttr = this.attrDef.facet.groupIdAttr;
let groups = this.groups = {};
let groupMap = this.groupMap = {};
this.groupCount = 0;
for each (let [, item] in Iterator(aItems)) {
let val = (attrKey in item) ? item[attrKey] : null;
let valId = (val == null) ? null : val[idAttr];
if (valId in groupMap) {
groups[valId].push(item);
}
else {
groupMap[valId] = val;
groups[valId] = [item];
this.groupCount++;
}
}
let orderedGroups = [[groupMap[key], items] for each
([key, items] in Iterator(groups))];
let comparator = nounDef.comparator;
function comparatorHelper(a, b) {
return comparator(a[0], b[0]);
}
orderedGroups.sort(comparatorHelper);
this.orderedGroups = orderedGroups;
},
};
/**
* Facet sets of discrete items. For example, tags applied to messages.
*
* The main differences between us and |DiscreteFaceter| are:
* - The empty set is notable.
* - Specific set configurations could be interesting, but are not low-hanging
* fruit.
*/
function DiscreteSetFaceter(aAttrDef) {
this.attrDef = aAttrDef;
}
DiscreteSetFaceter.prototype = {
type: "discrete",
/**
* Facet the given set of items, deferring to the appropriate helper method
*/
facetItems: function(aItems) {
if (this.attrDef.objectNounDef.isPrimitive)
return this.facetPrimitiveItems(aItems);
else
return this.facetComplexItems(aItems);
},
/**
* Facet an attribute whose value is primitive, meaning that it is a raw
* numeric value or string, rather than a complex object.
*/
facetPrimitiveItems: function(aItems) {
let attrKey = this.attrDef.boundName;
let nounDef = this.attrDef.objectNounDef;
let groups = this.groups = {};
let valStrToVal = {};
this.groupCount = 0;
for each (let [, item] in Iterator(aItems)) {
let vals = (attrKey in item) ? item[attrKey] : null;
if (vals == null || vals.length == 0) {
vals = [null];
}
for each (let [, val] in Iterator(vals)) {
if (val in groups)
groups[val].push(item);
else {
groups[val] = [item];
valStrToVal[val] = val;
this.groupCount++;
}
}
}
let orderedGroups = [[valStrToVal[key], items] for each
([key, items] in Iterator(groups))];
let comparator = nounDef.comparator;
function comparatorHelper(a, b) {
return comparator(a[0], b[0]);
}
orderedGroups.sort(comparatorHelper);
this.orderedGroups = orderedGroups;
},
/**
* Facet an attribute whose value is a complex object that can be identified
* by its 'id' attribute. This is the case where the value is itself a noun
* instance.
*/
facetComplexItems: function(aItems) {
let attrKey = this.attrDef.boundName;
let nounDef = this.attrDef.objectNounDef;
let idAttr = this.attrDef.facet.groupIdAttr;
let groups = this.groups = {};
let groupMap = this.groupMap = {};
this.groupCount = 0;
for each (let [, item] in Iterator(aItems)) {
let vals = (attrKey in item) ? item[attrKey] : null;
if (vals == null || vals.length == 0) {
vals = [null];
}
for each (let [, val] in Iterator(vals)) {
let valId = (val == null) ? null : val[idAttr];
if (valId in groupMap) {
groups[valId].push(item);
}
else {
groupMap[valId] = val;
groups[valId] = [item];
this.groupCount++;
}
}
}
let orderedGroups = [[groupMap[key], items] for each
([key, items] in Iterator(groups))];
let comparator = nounDef.comparator;
function comparatorHelper(a, b) {
return comparator(a[0], b[0]);
}
orderedGroups.sort(comparatorHelper);
this.orderedGroups = orderedGroups;
},
};
/**
* Given a non-singular attribute, facet it as if it were a boolean based on
* whether there is anything in the list (set).
*/
function NonEmptySetFaceter(aAttrDef) {
this.attrDef = aAttrDef;
}
NonEmptySetFaceter.prototype = {
type: "boolean",
/**
* Facet the given set of items, deferring to the appropriate helper method
*/
facetItems: function(aItems) {
let attrKey = this.attrDef.boundName;
let nounDef = this.attrDef.objectNounDef;
let trueValues = [];
let falseValues = [];
let groups = this.groups = {};
this.groupCount = 0;
for each (let [, item] in Iterator(aItems)) {
let vals = (attrKey in item) ? item[attrKey] : null;
if (vals == null || vals.length == 0)
falseValues.push(item);
else
trueValues.push(item);
}
this.orderedGroups = [];
if (trueValues.length)
this.orderedGroups.push([true, trueValues]);
if (falseValues.length)
this.orderedGroups.push([false, falseValues]);
this.groupCount = this.orderedGroups.length;
},
makeQuery: function(aGroupValues, aInclusive) {
let query = this.query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
let constraintFunc = query[this.attrDef.boundName];
constraintFunc.call(query);
// Our query is always for non-empty lists (at this time), so we want to
// invert if they're excluding 'true' or including 'false', which means !=.
let invert = aGroupValues[0] != aInclusive;
return [query, invert];
}
};
/**
* Facet dates. We build a hierarchical nested structure of year, month, and
* day nesting levels. This decision was made speculatively in the hopes that
* it would allow us to do clustered analysis and that there might be a benefit
* for that. For example, if you search for "Christmas", we might notice
* clusters of messages around December of each year. We could then present
* these in a list as likely candidates, rather than a graphical timeline.
* Alternately, it could be used to inform a non-linear visualization. As it
* stands (as of this writing), it's just a complicating factor.
*/
function DateFaceter(aAttrDef) {
this.attrDef = aAttrDef;
}
DateFaceter.prototype = {
type: "date",
/**
*
*/
facetItems: function(aItems) {
let attrKey = this.attrDef.boundName;
let nounDef = this.attrDef.objectNounDef;
let years = this.years = {_subCount: 0};
// generally track the time range
let oldest = null, newest = null;
let validItems = this.validItems = [];
// just cheat and put us at the front...
this.groupCount = aItems.length ? 1000 : 0;
this.orderedGroups = null;
/** The number of items with a null/missing attribute. */
this.missing = 0;
/**
* The number of items with a date that is unreasonably far in the past or
* in the future. Old-wise, we are concerned about incorrectly formatted
* messages (spam) that end up placed around the UNIX epoch. New-wise,
* we are concerned about messages that can't be explained by users who
* don't know how to set their clocks (both the current user and people
* sending them mail), mainly meaning spam.
* We want to avoid having our clever time-scale logic being made useless by
* these unreasonable messages.
*/
this.unreasonable = 0;
// feb 1, 1970
let tooOld = new Date(1970, 1, 1);
// 3 days from now
let tooNew = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
for each (let [, item] in Iterator(aItems)) {
let val = (attrKey in item) ? item[attrKey] : null;
// -- missing
if (val == null) {
this.missing++;
continue;
}
// -- unreasonable
if (val < tooOld || val > tooNew) {
this.unreasonable++;
continue;
}
this.validItems.push(item);
// -- time range
if (oldest == null)
oldest = newest = val;
else if (val < oldest)
oldest = val;
else if (val > newest)
newest = val;
// -- bucket
// - year
let year, valYear = val.getYear();
if (valYear in years) {
year = years[valYear];
year._dateCount++;
}
else {
year = years[valYear] = {
_dateCount: 1,
_subCount: 0
};
years._subCount++;
}
// - month
let month, valMonth = val.getMonth();
if (valMonth in year) {
month = year[valMonth];
month._dateCount++;
}
else {
month = year[valMonth] = {
_dateCount: 1,
_subCount: 0
};
year._subCount++;
}
// - day
let valDate = val.getDate();
if (valDate in month) {
month[valDate].push(item);
}
else {
month[valDate] = [item];
}
}
this.oldest = oldest;
this.newest = newest;
},
_unionMonth: function(aMonthObj) {
let dayItemLists = [];
for each (let [key, dayItemList] in Iterator(aMonthObj)) {
if (typeof(key) == "string" && key[0] == '_')
continue;
dayItemLists.push(dayItemList);
}
return Array.concat.apply([], dayItemLists);
},
_unionYear: function(aYearObj) {
let monthItemLists = [];
for each (let [key, monthObj] in Iterator(aYearObj)) {
if (typeof(key) == "string" && key[0] == '_')
continue;
monthItemLists.push(this._unionMonth(monthObj));
}
return Array.concat.apply([], monthItemLists);
}
};

Просмотреть файл

@ -43,6 +43,7 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://app/modules/gloda/log4moz.js");
Cu.import("resource://app/modules/StringBundle.js");
Cu.import("resource://app/modules/gloda/utils.js");
Cu.import("resource://app/modules/gloda/gloda.js");
@ -60,6 +61,7 @@ Cu.import("resource://app/modules/gloda/noun_mimetype.js");
*/
var GlodaFundAttr = {
providerName: "gloda.fundattr",
strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
_log: null,
init: function gloda_explattr_init() {
@ -76,8 +78,10 @@ var GlodaFundAttr = {
POPULARITY_FROM_ME_TO: 10,
POPULARITY_FROM_ME_CC: 4,
POPULARITY_FROM_ME_BCC: 3,
POPULARITY_TO_ME: 5,
POPULARITY_CC_ME: 1,
POPULARITY_BCC_ME: 1,
/** Boost for messages 'I' sent */
NOTABILITY_FROM_ME: 10,
@ -90,17 +94,6 @@ var GlodaFundAttr = {
/** Boost for each additional person involved in my address book. */
NOTABILITY_INVOLVING_ADDR_BOOK_ADDL: 2,
_attrConvSubject: null,
_attrFolder: null,
_attrBody: null,
_attrFrom: null,
_attrFromMe: null,
_attrTo: null,
_attrToMe: null,
_attrCc: null,
_attrCcMe: null,
_attrDate: null,
defineAttributes: function() {
/* ***** Conversations ***** */
// conversation: subjectMatches
@ -124,6 +117,7 @@ var GlodaFundAttr = {
attributeType: Gloda.kAttrFundamental,
attributeName: "folder",
singular: true,
facet: true,
special: Gloda.kSpecialColumn,
specialColumnName: "folderID",
subjectNouns: [Gloda.NOUN_MESSAGE],
@ -272,6 +266,19 @@ var GlodaFundAttr = {
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_IDENTITY,
}); // not-tested
/**
* Bcc'ed recipients; only makes sense for sent messages.
*/
this._attrBcc = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "bcc",
singular: false,
facet: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_IDENTITY,
}); // not-tested
// Date. now lives on the row.
this._attrDate = Gloda.defineAttribute({
@ -280,6 +287,9 @@ var GlodaFundAttr = {
attributeType: Gloda.kAttrFundamental,
attributeName: "date",
singular: true,
facet: {
type: "date",
},
special: Gloda.kSpecialColumn,
specialColumnName: "date",
subjectNouns: [Gloda.NOUN_MESSAGE],
@ -293,61 +303,71 @@ var GlodaFundAttr = {
attributeType: Gloda.kAttrFundamental,
attributeName: "attachmentTypes",
singular: false,
facet: {
type: "default",
// This will group the MIME types by their category.
groupIdAttr: "category",
queryHelper: "Category",
},
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_MIME_TYPE,
});
// --- Optimization
// Involves. Means any of from/to/cc. The queries get ugly enough without
// this that it seems to justify the cost, especially given the frequent
// use case. (In fact, post-filtering for the specific from/to/cc is
// probably justifiable rather than losing this attribute...)
/**
* Involves means any of from/to/cc/bcc. The queries get ugly enough
* without this that it seems to justify the cost, especially given the
* frequent use case. (In fact, post-filtering for the specific from/to/cc
* is probably justifiable rather than losing this attribute...)
*/
this._attrInvolves = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrOptimization,
attributeName: "involves",
singular: false,
facet: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_IDENTITY,
}); // not-tested
// From Me To
this._attrFromMeTo = Gloda.defineAttribute({
/**
* Any of to/cc/bcc.
*/
this._attrRecipients = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrOptimization,
attributeName: "fromMeTo",
attributeName: "recipients",
singular: false,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_PARAM_IDENTITY,
objectNoun: Gloda.NOUN_IDENTITY,
}); // not-tested
// From Me Cc
this._attrFromMeCc = Gloda.defineAttribute({
// From Me (To/Cc/Bcc)
this._attrFromMe = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrOptimization,
attributeName: "fromMeCc",
attributeName: "fromMe",
singular: false,
// The interesting thing to a facet is whether the message is from me.
facet: {
type: "nonempty?"
},
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_PARAM_IDENTITY,
}); // not-tested
// To Me
// To/Cc/Bcc Me
this._attrToMe = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "toMe",
singular: false,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_PARAM_IDENTITY,
}); // not-tested
// Cc Me
this._attrCcMe = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "ccMe",
// The interesting thing to a facet is whether the message is to me.
facet: {
type: "nonempty?"
},
singular: false,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_PARAM_IDENTITY,
@ -377,11 +397,14 @@ var GlodaFundAttr = {
attributeName: "mailing-list",
bindName: "mailingLists",
singular: false,
facet: true,
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_IDENTITY,
}); // not-tested, not-implemented
},
RE_LIST_POST: /<mailto:([^>]+)>/,
/**
*
* Specializations:
@ -413,11 +436,20 @@ var GlodaFundAttr = {
if (author == null || author == "")
author = aMsgHdr.mime2DecodedAuthor;
let [authorIdentities, toIdentities, ccIdentities] =
let normalizedListPost = "";
if (aMimeMsg.has("list-post")) {
let match = this.RE_LIST_POST.exec(aMimeMsg.get("list-post"));
if (match)
normalizedListPost = "<" + match[1] + ">";
}
let [authorIdentities, toIdentities, ccIdentities, bccIdentities,
listIdentities] =
yield aCallbackHandle.pushAndGo(
Gloda.getOrCreateMailIdentities(aCallbackHandle,
author, aMsgHdr.mime2DecodedRecipients,
aMsgHdr.ccList));
aMsgHdr.ccList, aMsgHdr.bccList,
normalizedListPost));
if (authorIdentities.length == 0) {
this._log.error("Message with subject '" + aMsgHdr.mime2DecodedSubject +
@ -427,9 +459,14 @@ var GlodaFundAttr = {
let authorIdentity = authorIdentities[0];
aGlodaMessage.from = authorIdentity;
// -- To, Cc
// -- To, Cc, Bcc
aGlodaMessage.to = toIdentities;
aGlodaMessage.cc = ccIdentities;
aGlodaMessage.bcc = bccIdentities;
// -- Mailing List
if (listIdentities.length)
aGlodaMessage.mailingLists = listIdentities;
// -- Attachments
let attachmentTypes = [];
@ -460,14 +497,14 @@ var GlodaFundAttr = {
let aMsgHdr = aRawReps.header;
// for simplicity this is used for both involves and recipients
let involvesIdentities = {};
let involves = aGlodaMessage.involves || [];
let recipients = aGlodaMessage.recipients || [];
// me specialization optimizations
// 'me' specialization optimizations
let toMe = aGlodaMessage.toMe || [];
let fromMeTo = aGlodaMessage.fromMeTo || [];
let ccMe = aGlodaMessage.ccMe || [];
let fromMeCc = aGlodaMessage.fromMeCc || [];
let fromMe = aGlodaMessage.fromMe || [];
let myIdentities = Gloda.myIdentities; // needless optimization?
let authorIdentity = aGlodaMessage.from;
@ -500,6 +537,7 @@ var GlodaFundAttr = {
for each (let [,toIdentity] in Iterator(aGlodaMessage.to)) {
if (!(toIdentity.id in involvesIdentities)) {
involves.push(toIdentity);
recipients.push(toIdentity);
involvesIdentities[toIdentity.id] = true;
let toCard = toIdentity.abCard;
if (toCard) {
@ -517,7 +555,7 @@ var GlodaFundAttr = {
}
// optimization attribute from-me-to ('I' am the parameter)
if (isFromMe) {
fromMeTo.push([authorIdentity, toIdentity]);
fromMe.push([authorIdentity, toIdentity]);
// also, popularity
if (aIsNew)
toIdentity.contact.popularity += this.POPULARITY_FROM_ME_TO;
@ -526,6 +564,7 @@ var GlodaFundAttr = {
for each (let [,ccIdentity] in Iterator(aGlodaMessage.cc)) {
if (!(ccIdentity.id in involvesIdentities)) {
involves.push(ccIdentity);
recipients.push(ccIdentity);
involvesIdentities[ccIdentity.id] = true;
let ccCard = ccIdentity.abCard;
if (ccCard) {
@ -536,34 +575,59 @@ var GlodaFundAttr = {
}
// optimization attribute cc-me ('I' am the parameter)
if (ccIdentity.id in myIdentities) {
ccMe.push([ccIdentity, authorIdentity]);
toMe.push([ccIdentity, authorIdentity]);
if (aIsNew)
authorIdentity.contact.popularity += this.POPULARITY_CC_ME;
}
// optimization attribute from-me-to ('I' am the parameter)
if (isFromMe) {
fromMeCc.push([authorIdentity, ccIdentity]);
fromMe.push([authorIdentity, ccIdentity]);
// also, popularity
if (aIsNew)
ccIdentity.contact.popularity += this.POPULARITY_FROM_ME_CC;
}
}
// just treat bcc like cc; the intent is the same although the exact
// semantics differ.
for each (let [,bccIdentity] in Iterator(aGlodaMessage.bcc)) {
if (!(bccIdentity.id in involvesIdentities)) {
involves.push(bccIdentity);
recipients.push(bccIdentity);
involvesIdentities[bccIdentity.id] = true;
let bccCard = bccIdentity.abCard;
if (bccCard) {
involvedAddrBookCount++;
// @testpoint gloda.noun.message.attr.recipientsMatch
aGlodaMessage._indexRecipients += ' ' + bccCard.displayName;
}
}
// optimization attribute cc-me ('I' am the parameter)
if (bccIdentity.id in myIdentities) {
toMe.push([bccIdentity, authorIdentity]);
if (aIsNew)
authorIdentity.contact.popularity += this.POPULARITY_BCC_ME;
}
// optimization attribute from-me-to ('I' am the parameter)
if (isFromMe) {
fromMe.push([authorIdentity, bccIdentity]);
// also, popularity
if (aIsNew)
bccIdentity.contact.popularity += this.POPULARITY_FROM_ME_BCC;
}
}
if (involvedAddrBookCount)
aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ADDR_BOOK_FIRST +
(involvedAddrBookCount - 1) * this.NOTABILITY_INVOLVING_ADDR_BOOK_ADDL;
aGlodaMessage.involves = involves;
aGlodaMessage.recipients = recipients;
if (toMe.length) {
aGlodaMessage.toMe = toMe;
aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ME;
}
if (fromMeTo.length)
aGlodaMessage.fromMeTo = fromMeTo;
if (ccMe.length)
aGlodaMessage.ccMe = ccMe;
if (fromMeCc.length)
aGlodaMessage.fromMeCc = fromMeCc;
if (fromMe.length)
aGlodaMessage.fromMe = fromMe;
if (aRawReps.bodyLines &&
this.contentWhittle({}, aRawReps.bodyLines, aRawReps.content)) {

Просмотреть файл

@ -351,10 +351,16 @@ var Gloda = {
* This method uses the indexer's callback handle mechanism, and does not
* obey traditional return semantics.
*
* We normalize all e-mail addresses to be lowercase as a normative measure.
*
* @param aCallbackHandle The GlodaIndexer callback handle (or equivalent)
* that you are operating under.
* @param ... One or more strings. Each string can contain zero or more
* e-mail addresses with display name. If more than one address is given,
* they should be comma-delimited. For example
* '"Bob Smith" <bob@smith.com>' is an address with display name.
* '"Bob Smith" <bob@smith.com>' is an address with display name. Mime
* header decoding is performed, but is ignorant of any folder-level
* character set overrides.
* @returns via the callback handle mechanism, a list containing one sub-list
* for each string argument passed. Each sub-list containts zero or more
* GlodaIdentity instances corresponding to the addresses provided.
@ -374,7 +380,7 @@ var Gloda = {
let identities = [];
for (let iAddress = 0; iAddress < parsed.count; iAddress++) {
let address = parsed.addresses[iAddress];
let address = parsed.addresses[iAddress].toLowerCase();
if (address in addresses)
addresses[address].push(resultList);
else
@ -513,7 +519,7 @@ var Gloda = {
// find the identities if they exist, flag to create them if they don't
if (emailAddress) {
parsed = GlodaUtils.parseMailAddresses(emailAddress);
let parsed = GlodaUtils.parseMailAddresses(emailAddress);
if (!(parsed.addresses[0] in myEmailAddresses)) {
let identity = GlodaDatastore.getIdentity("email",
parsed.addresses[0]);
@ -525,7 +531,7 @@ var Gloda = {
}
}
if (replyTo) {
parsed = GlodaUtils.parseMailAddresses(replyTo);
let parsed = GlodaUtils.parseMailAddresses(replyTo);
if (!(parsed.addresses[0] in myEmailAddresses)) {
let identity = GlodaDatastore.getIdentity("email",
parsed.addresses[0]);
@ -780,40 +786,48 @@ var Gloda = {
/**
* Define a noun. Takes a dictionary with the following keys/values:
*
* @param name The name of the noun. This is not a display name (anything
* being displayed needs to be localized, after all), but simply the
* canonical name for debugging purposes and for people to pass to
* @param aNounDef.name The name of the noun. This is not a display name
* (anything being displayed needs to be localized, after all), but simply
* the canonical name for debugging purposes and for people to pass to
* lookupNoun. The suggested convention is lower-case-dash-delimited,
* with names being singular (since it's a single noun we are referring
* to.)
* @param class The 'class' to which an instance of the noun will belong (aka
* will pass an instanceof test).
* @param allowsArbitraryAttrs Is this a 'first class noun'/can it be a subject, AKA can
* this noun have attributes stored on it that relate it to other things?
* For example, a message is first-class; we store attributes of
* messages. A date is not first-class now, nor is it likely to be; we
* will not store attributes about a date, although dates will be the
* objects of other subjects. (For example: we might associate a date
* with a calendar event, but the date is an attribute of the calendar
* event and not vice versa.)
* @param usesParameter A boolean indicating whether this noun requires use
* of the 'parameter' BLOB storage field on the attribute bindings in the
* database to persist itself. Use of parameters should be limited
* to a reasonable number of values (16-32 is okay, more than that is
* pushing it and 256 should be considered an absolute upper bound)
* because of the database organization. When false, your toParamAndValue
* function is expected to return null for the parameter and likewise your
* fromParamAndValue should expect ignore and generally ignore the
* argument.
* @param toParamAndValue A function that takes an instantiated noun
* @param aNounDef.class The 'class' to which an instance of the noun will
* belong (aka will pass an instanceof test). You may also provide this
* as 'clazz' if the keyword makes your IDE angry.
* @param aNounDef.allowsArbitraryAttrs Is this a 'first class noun'/can it be
* a subject, AKA can this noun have attributes stored on it that relate
* it to other things? For example, a message is first-class; we store
* attributes of messages. A date is not first-class now, nor is it
* likely to be; we will not store attributes about a date, although dates
* will be the objects of other subjects. (For example: we might
* associate a date with a calendar event, but the date is an attribute of
* the calendar event and not vice versa.)
* @param aNounDef.usesParameter A boolean indicating whether this noun
* requires use of the 'parameter' BLOB storage field on the attribute
* bindings in the database to persist itself. Use of parameters should
* be limited to a reasonable number of values (16-32 is okay, more than
* that is pushing it and 256 should be considered an absolute upper
* bound) because of the database organization. When false, your
* toParamAndValue function is expected to return null for the parameter
* and likewise your fromParamAndValue should expect ignore and generally
* ignore the argument.
* @param aNounDef.toParamAndValue A function that takes an instantiated noun
* instance and returns a 2-element list of [parameter, value] where
* parameter may only be non-null if you passed a usesParameter of true.
* Parameter may be of any type (BLOB), and value must be numeric (pass
* 0 if you don't need the value).
*
* @param schema Unsupported mechanism by which you can define a table that
* corresponds to this noun. The table will be created if it does not
* exist.
* @param aNounDef.isPrimitive True when the noun instance is a raw numeric
* value/string/boolean. False when the instance is an object. When
* false, it is assumed the attribute that serves as a unique identifier
* for the value is "id" unless 'idAttr' is provided.
* @param [aNounDef.idAttr="id"] For non-primitive nouns, this is the
* attribute on the object that uniquely identifies it.
*
* @param aNounDef.schema Unsupported mechanism by which you can define a
* table that corresponds to this noun. The table will be created if it
* does not exist.
* - name The table name; don't conflict with other things!
* - columns A list of [column name, sqlite type] tuples. You should
* always include a definition like ["id", "INTEGER PRIMARY KEY"] for
@ -837,6 +851,15 @@ var Gloda = {
if (aNounDef.clazz)
aNounDef.class = aNounDef.clazz;
if (!("idAttr" in aNounDef))
aNounDef.idAttr = "id";
if (!("comparator" in aNounDef)) {
aNounDef.comparator = function() {
throw new Error("Noun type '" + aNounDef.name +
"' lacks a real comparator.");
};
}
// We allow nouns to have data tables associated with them where we do all
// the legwork. The schema attribute is the gateway to this magical world
// of functionality. Said door is officially unsupported.
@ -1007,30 +1030,87 @@ var Gloda = {
this.defineNoun({
name: "bool",
clazz: Boolean, allowsArbitraryAttrs: false,
isPrimitive: true,
// favor true before false
comparator: function gloda_bool_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return b - a;
},
toParamAndValue: function(aBool) {
return [null, aBool ? 1 : 0];
}}, this.NOUN_BOOLEAN);
this.defineNoun({
name: "number",
clazz: Number, allowsArbitraryAttrs: false, continuous: true,
isPrimitive: true,
comparator: function gloda_number_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a - b;
},
toParamAndValue: function(aNum) {
return [null, aNum];
}}, this.NOUN_NUMBER);
this.defineNoun({
name: "string",
clazz: String, allowsArbitraryAttrs: false,
isPrimitive: true,
comparator: function gloda_string_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.localeCompare(b);
},
toParamAndValue: function(aString) {
return [null, aString];
}}, this.NOUN_STRING);
this.defineNoun({
name: "date",
clazz: Date, allowsArbitraryAttrs: false, continuous: true,
isPrimitive: true,
comparator: function gloda_data_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a - b;
},
toParamAndValue: function(aDate) {
return [null, aDate.valueOf() * 1000];
}}, this.NOUN_DATE);
this.defineNoun({
name: "fulltext",
clazz: String, allowsArbitraryAttrs: false, continuous: false,
isPrimitive: true,
comparator: function gloda_fulltext_comparator(a, b) {
throw new Error("Fulltext nouns are not comparable!");
},
// as noted on NOUN_FULLTEXT, we just pass the string around. it never
// hits the database, so it's okay.
toParamAndValue: function(aString) {
@ -1041,6 +1121,19 @@ var Gloda = {
name: "folder",
clazz: GlodaFolder,
allowsArbitraryAttrs: false,
isPrimitive: false,
comparator: function gloda_folder_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.name.localeCompare(b.name);
},
toParamAndValue: function(aFolderOrGlodaFolder) {
if (aFolderOrGlodaFolder instanceof GlodaFolder)
return [null, aFolderOrGlodaFolder.id];
@ -1051,11 +1144,24 @@ var Gloda = {
name: "conversation",
clazz: GlodaConversation,
allowsArbitraryAttrs: false,
isPrimitive: false,
cache: true, cacheCost: 512,
tableName: "conversations",
attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
datastore: GlodaDatastore,
objFromRow: GlodaDatastore._conversationFromRow,
comparator: function gloda_conversation_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.subject.localeCompare(b.subject);
},
toParamAndValue: function(aConversation) {
if (aConversation instanceof GlodaConversation)
return [null, aConversation.id];
@ -1066,6 +1172,7 @@ var Gloda = {
name: "message",
clazz: GlodaMessage,
allowsArbitraryAttrs: true,
isPrimitive: false,
cache: true, cacheCost: 2048,
tableName: "messages",
// we will always have a fulltext row, even for messages where we don't
@ -1089,6 +1196,7 @@ var Gloda = {
name: "contact",
clazz: GlodaContact,
allowsArbitraryAttrs: true,
isPrimitive: false,
cache: true, cacheCost: 128,
tableName: "contacts",
attrTableName: "contactAttributes", attrIDColumnName: "contactID",
@ -1096,6 +1204,18 @@ var Gloda = {
dbAttribAdjuster: GlodaDatastore.adjustAttributes,
objInsert: GlodaDatastore.insertContact,
objUpdate: GlodaDatastore.updateContact,
comparator: function gloda_contact_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.name.localeCompare(b.name);
},
toParamAndValue: function(aContact) {
if (aContact instanceof GlodaContact)
return [null, aContact.id];
@ -1106,10 +1226,23 @@ var Gloda = {
name: "identity",
clazz: GlodaIdentity,
allowsArbitraryAttrs: false,
isPrimitive: false,
cache: true, cacheCost: 128,
usesUniqueValue: true,
tableName: "identities",
datastore: GlodaDatastore, objFromRow: GlodaDatastore._identityFromRow,
comparator: function gloda_identity_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.contact.name.localeCompare(b.contact.name);
},
toParamAndValue: function(aIdentity) {
if (aIdentity instanceof GlodaIdentity)
return [null, aIdentity.id];
@ -1125,6 +1258,26 @@ var Gloda = {
name: "parameterized-identity",
clazz: null,
allowsArbitraryAttrs: false,
comparator: function gloda_fulltext_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
// First sort by the first identity in the tuple
// Since our general use-case is for the first guy to be "me", we only
// compare the identity value, not the name.
let fic = a[0].value.localeCompare(b[0].value);
if (fic)
return fic;
// Next compare the second identity in the tuple, but use the contact
// this time to be consistent with our identity comparator.
return a[1].contact.name.localeCompare(b[1].contact.name);
},
computeDelta: function(aCurValues, aOldValues) {
let oldMap = {};
for each (let [, tupe] in Iterator(aOldValues)) {
@ -1269,43 +1422,66 @@ var Gloda = {
aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Like"] =
likeConstrainer;
}
// - Custom helpers provided by the noun type...
if ("queryHelpers" in objectNounDef) {
for each (let [name, helper] in Iterator(objectNounDef.queryHelpers)) {
// we need a new closure...
let helperFunc = helper;
aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + name] =
function() {
return helperFunc.call(this, aAttrDef, arguments);
};
}
}
}
},
/**
* Names of attribute-specific localized strings and the JS attribute they are
* exposed as in the attribute's "strings" attribute (if the provider has a
* string bundle exposed on its "strings" attribute). They are rooted at
* "gloda.SUBJECT-NOUN-NAME.attr.ATTR-NAME.*".
*/
_ATTR_LOCALIZED_STRINGS: {
facetLabel: "facetLabel",
facetTooltip: "facetTooltip",
},
/**
* Define an attribute and all its meta-data. Takes a single dictionary as
* its argument, with the following required properties:
*
* @param provider The object instance providing a 'process' method.
* @param extensionName The name of the extension providing these attributes.
* @param attributeType The type of attribute, one of the values from the
* kAttr* enumeration.
* @param attributeName The name of the attribute, which also doubles as the
* bound property name if you pass 'bind' a value of true. You are
* @param aAttrDef.provider The object instance providing a 'process' method.
* @param aAttrDef.extensionName The name of the extension providing these
* attributes.
* @param aAttrDef.attributeType The type of attribute, one of the values from
* the kAttr* enumeration.
* @param aAttrDef.attributeName The name of the attribute, which also doubles
* as the bound property name if you pass 'bind' a value of true. You are
* responsible for avoiding collisions, which presumably will mean
* checking/updating a wiki page in the future, or just prefixing your
* attribute name with your extension name or something like that.
* @param bind Should this attribute be 'bound' as a convenience attribute
* on the subject's object (true/false)? For example, with an
* @param aAttrDef.bind Should this attribute be 'bound' as a convenience
* attribute on the subject's object (true/false)? For example, with an
* attributeName of "foo" and passing true for 'bind' with a subject noun
* of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter
* that returns the value of the attribute. If 'singular' is true, this
* means an instance of the object class corresponding to the noun type or
* null if the attribute does not exist. If 'singular' is false, this
* means a list of instances of the object class corresponding to the noun
* type, where the list may be empty if no instances of the attribute are
* of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter that
* returns the value of the attribute. If 'singular' is true, this means
* an instance of the object class corresponding to the noun type or null
* if the attribute does not exist. If 'singular' is false, this means a
* list of instances of the object class corresponding to the noun type,
* where the list may be empty if no instances of the attribute are
* present.
* @param bindName Optional override of attributeName for purposes of the
* binding property's name.
* @param singular Is the attribute going to happen at most once (true),
* or potentially multiple times (false). This affects whether
* the binding returns a list or just a single item (which is null when
* @param aAttrDef.bindName Optional override of attributeName for purposes of
* the binding property's name.
* @param aAttrDef.singular Is the attribute going to happen at most once
* (true), or potentially multiple times (false). This affects whether
* the binding returns a list or just a single item (which is null when
* the attribute is not present).
* @param subjectNouns A list of object types (NOUNs) that this attribute can
* be set on. Each element in the list should be one of the NOUN_*
* constants or a dynamically registered noun type.
* @param objectNoun The object type (one of the NOUN_* constants or a
* dynamically registered noun types) that is the 'object' in the
* @param aAttrDef.subjectNouns A list of object types (NOUNs) that this
* attribute can be set on. Each element in the list should be one of the
* NOUN_* constants or a dynamically registered noun type.
* @param aAttrDef.objectNoun The object type (one of the NOUN_* constants or
* a dynamically registered noun types) that is the 'object' in the
* traditional RDF triple. More pragmatically, in the database row used
* to represent an attribute, we store the subject (ex: message ID),
* attribute ID, and an integer which is the integer representation of the
@ -1338,6 +1514,7 @@ var Gloda = {
}
let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
// -- Database Definition
let attrDBDef;
if (compoundName in GlodaDatastore._attributeDBDefs) {
// the existence of the GlodaAttributeDBDef means that either it has
@ -1373,6 +1550,52 @@ var Gloda = {
aAttrDef.objectNounDef = this._nounIDToDef[aAttrDef.objectNoun];
aAttrDef.objectNounDef.objectNounOfAttributes.push(aAttrDef);
// No facet attribute means no facet desired; set an explicit null so that
// code can check without doing an "in" check.
if (!("facet" in aAttrDef))
aAttrDef.facet = null;
// Promote "true" facet values to the defaults. Where attributes have
// specified values, make sure we fill in any missing defaults.
else {
if (aAttrDef.facet == true) {
aAttrDef.facet = {
type: "default",
groupIdAttr: aAttrDef.objectNounDef.idAttr
};
}
else {
if (!("groupIdAttr" in aAttrDef.facet))
aAttrDef.facet.groupIdAttr = aAttrDef.objectNounDef.idAttr;
}
}
// -- L10n.
// If the provider has a string bundle, populate a "strings" attribute with
// our standard attribute strings that can be UI exposed.
if ("strings" in aAttrDef.provider) {
let bundle = aAttrDef.provider.strings;
let attrStrings = aAttrDef.strings = {};
// we use the first subject the attribute applies to as the basis of
// where to get the string from. Mainly because we currently don't have
// any attributes with multiple subjects nor a use-case where we expose
// multiple noun types via the UI. (Just messages right now.)
let canonicalSubject = this._nounIDToDef[aAttrDef.subjectNouns[0]];
let propRoot = "gloda." + canonicalSubject.name + ".attr." +
aAttrDef.attributeName + ".";
for each (let [propName, attrName] in
Iterator(this._ATTR_LOCALIZED_STRINGS)) {
try {
attrStrings[attrName] = bundle.get(propRoot + propName);
}
catch (ex) {
// do nothing. nsIStringBundle throws exceptions because it is a
// standard nsresult type of API and our helper buddy does nothing
// to help us. (StringBundle.js, that is.)
}
}
}
// -- Subject Noun Binding
for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
iSubject++) {
let subjectType = aAttrDef.subjectNouns[iSubject];
@ -1756,8 +1979,12 @@ var Gloda = {
*/
scoreNounItems: function gloda_ns_grokNounItem(aItems, aContext,
aExtraScoreFuncs) {
let itemNounDef = aItems[0].NOUN_DEF;
let scores = [];
// bail if there is nothing to score
if (!aItems.length)
return scores;
let itemNounDef = aItems[0].NOUN_DEF;
if (aExtraScoreFuncs == null)
aExtraScoreFuncs = [];

Просмотреть файл

@ -5,7 +5,7 @@
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
@ -32,7 +32,7 @@
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
*
* ***** END LICENSE BLOCK ***** */
EXPORTED_SYMBOLS = ['GlodaABIndexer', 'GlodaABAttrs'];
@ -61,11 +61,11 @@ var GlodaABIndexer = {
enable: function() {
if (this._log == null)
this._log = Log4Moz.repository.getLogger("gloda.ab_indexer");
let abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager);
abManager.addAddressBookListener(this, Ci.nsIAbListener.itemChanged);
},
disable: function() {
let abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager);
abManager.removeAddressBookListener(this);
@ -74,21 +74,22 @@ var GlodaABIndexer = {
get workers() {
return [["ab-card", this._worker_index_card]];
},
_worker_index_card: function(aJob, aCallbackHandle) {
let card = aJob.id;
if (card.primaryEmail) {
// load the identity
let query = Gloda.newQuery(Gloda.NOUN_IDENTITY);
query.kind("email");
query.value(card.primaryEmail);
// we currently normalize all e-mail addresses to be lowercase
query.value(card.primaryEmail.toLowerCase());
let identityCollection = query.getCollection(aCallbackHandle);
yield Gloda.kWorkAsync;
if (identityCollection.items.length) {
let identity = identityCollection.items[0];
this._log.debug("Found identity, processing card.");
yield aCallbackHandle.pushAndGo(
Gloda.grokNounItem(identity.contact, card, false, false,
@ -96,13 +97,13 @@ var GlodaABIndexer = {
this._log.debug("Done processing card.");
}
}
yield GlodaIndexer.kWorkDone;
},
initialSweep: function() {
},
/* ------ nsIAbListener ------ */
onItemAdded: function ab_indexer_onItemAdded(aParentDir, aItem) {
},
@ -127,7 +128,7 @@ var GlodaABAttrs = {
init: function() {
this._log = Log4Moz.repository.getLogger("gloda.abattrs");
try {
this.defineAttributes();
}
@ -136,7 +137,7 @@ var GlodaABAttrs = {
throw ex;
}
},
defineAttributes: function() {
/* ***** Contacts ***** */
this._attrIdentityContact = Gloda.defineAttribute({
@ -195,7 +196,7 @@ var GlodaABAttrs = {
special: Gloda.kSpecialColumnParent,
specialColumnName: "contactID", // the column in the db
idStorageAttributeName: "_contactID",
valueStorageAttributeName: "_contact",
valueStorageAttributeName: "_contact",
subjectNouns: [Gloda.NOUN_IDENTITY],
objectNoun: Gloda.NOUN_CONTACT,
}); // tested-by: test_attributes_fundamental
@ -245,19 +246,19 @@ var GlodaABAttrs = {
FreeTagNoun.getFreeTag(freeTagName);
}
},
process: function(aContact, aCard, aIsNew, aCallbackHandle) {
if (aContact.NOUN_ID != Gloda.NOUN_CONTACT) {
this._log.warning("Somehow got a non-contact: " + aContact);
return; // this will produce an exception; we like.
}
// update the name
if (aCard.displayName && aCard.displayName != aContact.name)
aContact.name = aCard.displayName;
aContact.freeTags = [];
let tags = null;
try {
tags = aCard.getProperty("Categories", null);
@ -272,7 +273,7 @@ var GlodaABAttrs = {
}
}
}
yield Gloda.kWorkDone;
}
};

Просмотреть файл

@ -350,6 +350,12 @@ var GlodaIndexer = {
*/
_INITIAL_SWEEP_DELAY: 10000,
/**
* How many milliseconds in the future should we schedule indexing to start
* when turning on indexing (and it was not previously active).
*/
_INDEX_KICKOFF_DELAY: 200,
/**
* The time interval, in milliseconds, of pause between indexing batches. The
* maximum processor consumption is determined by this constant and the
@ -686,7 +692,7 @@ var GlodaIndexer = {
this._log.info("+++ Indexing Queue Processing Commencing");
this._indexingActive = true;
this._timer.initWithCallback(this._wrapCallbackDriver,
this._INDEX_INTERVAL,
this._INDEX_KICKOFF_DELAY,
Ci.nsITimer.TYPE_ONE_SHOT);
}
}
@ -710,7 +716,7 @@ var GlodaIndexer = {
this._log.info("+++ Indexing Queue Processing Resuming");
this._indexingActive = true;
this._timer.initWithCallback(this._wrapCallbackDriver,
this._INDEX_INTERVAL,
this._INDEX_KICKOFF_DELAY,
Ci.nsITimer.TYPE_ONE_SHOT);
}
},

Просмотреть файл

@ -0,0 +1,200 @@
/*
* This file wants to be a data file of some sort. It might do better as a real
* raw JSON file. It is trying to be one right now, but it obviously is not.
*/
let EXPORTED_SYMBOLS = ['MimeCategoryMapping'];
/**
* Input data structure to allow us to build a fast mapping from mime type to
* category name. The keys in MimeCategoryMapping are the top-level
* categories. Each value can either be a list of MIME types or a nested
* object which recursively defines sub-categories. We currently do not use
* the sub-categories. They are just there to try and organize the MIME types
* a little and open the door to future enhancements.
*
* Do _not_ add additional top-level categories unless you have added
* corresponding entries to gloda.properties under the
* "gloda.mimetype.category" branch and are making sure localizers are aware
* of the change and have time to localize it.
*
* Entries with wildcards in them are part of a fallback strategy by the
* |mimeTypeNoun| and do not actually use regular expressions or anything like
* that. Everything is a straight string lookup. Given "foo/bar" we look for
* "foo/bar", then "foo/*", and finally "*".
*/
let MimeCategoryMapping = {
archives: [
"application/java-archive",
"application/x-java-archive",
"application/x-jar",
"application/x-java-jnlp-file",
"application/mac-binhex40",
"application/vnd.ms-cab-compressed",
"application/x-arc",
"application/x-arj",
"application/x-compress",
"application/x-compressed-tar",
"application/x-cpio",
"application/x-cpio-compressed",
"application/x-deb",
"application/x-bittorrent",
"application/x-rar",
"application/x-rar-compressed",
"application/x-7z-compressed",
"application/zip",
"application/x-zip-compressed",
"application/x-zip",
"application/x-bzip",
"application/x-bzip-compressed-tar",
"application/x-bzip2",
"application/x-gzip",
"application/x-tar",
"application/x-tar-gz",
"application/x-tarz",
],
documents: {
database: [
"application/vnd.ms-access",
"application/x-msaccess",
"application/msaccess",
"application/vnd.msaccess",
"application/x-msaccess",
"application/mdb",
"application/x-mdb",
"application/vnd.oasis.opendocument.database",
],
graphics: [
"application/postscript",
"application/x-bzpostscript",
"application/x-dvi",
"application/x-gzdvi",
"application/illustrator",
"application/vnd.corel-draw",
"application/cdr",
"application/coreldraw",
"application/x-cdr",
"application/x-coreldraw",
"image/cdr",
"image/x-cdr",
"zz-application/zz-winassoc-cdr",
"application/vnd.oasis.opendocument.graphics",
"application/vnd.oasis.opendocument.graphics-template",
"application/vnd.oasis.opendocument.image",
"application/x-dia-diagram",
],
presentation: [
"application/vnd.ms-powerpoint.presentation.macroenabled.12",
"application/vnd.ms-powerpoint.template.macroenabled.12",
"application/vnd.ms-powerpoint",
"application/powerpoint",
"application/mspowerpoint",
"application/x-mspowerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.template",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.presentation-template"
],
spreadsheet: [
"application/vnd.lotus-1-2-3",
"application/x-lotus123",
"application/x-123",
"application/lotus123",
"application/wk1",
"application/x-quattropro",
"application/vnd.ms-excel.sheet.binary.macroenabled.12",
"application/vnd.ms-excel.sheet.macroenabled.12",
"application/vnd.ms-excel.template.macroenabled.12",
"application/vnd.ms-excel",
"application/msexcel",
"application/x-msexcel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"application/vnd.oasis.opendocument.formula",
"application/vnd.oasis.opendocument.formula-template",
"application/vnd.oasis.opendocument.chart",
"application/vnd.oasis.opendocument.chart-template",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.spreadsheet-template",
"application/x-gnumeric",
],
wordProcessor: [
"application/msword",
"application/vnd.ms-word",
"application/x-msword",
"application/msword-template",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.ms-word.document.macroenabled.12",
"application/vnd.ms-word.template.macroenabled.12",
"application/x-mswrite",
"application/x-pocket-word",
"application/rtf",
"text/rtf",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.text-master",
"application/vnd.oasis.opendocument.text-template",
"application/vnd.oasis.opendocument.text-web",
"application/vnd.wordperfect",
"application/x-abiword",
"application/x-amipro",
],
suite: [
"application/vnd.ms-works"
],
},
images: [
"image/*"
],
media: {
audio: [
"audio/*",
],
video: [
"video/*",
],
container: [
"application/ogg",
"application/smil",
"application/vnd.ms-asf",
"application/vnd.rn-realmedia",
"application/x-matroska",
"application/x-quicktime-media-link",
"application/x-quicktimeplayer",
]
},
other: [
"*"
],
pdf: [
"application/pdf",
"application/x-pdf",
"image/pdf",
"file/pdf",
"application/x-bzpdf",
"application/x-gzpdf",
],
}

Просмотреть файл

@ -88,7 +88,7 @@ const DASCORE_SQL_SNIPPET =
") + date)";
const FULLTEXT_QUERY_EXPLICIT_SQL =
"SELECT messages.*, offsets(messagesText) AS osets " +
"SELECT messages.*, messagesText.*, offsets(messagesText) AS osets " +
"FROM messages, messagesText WHERE messagesText MATCH ?" +
" AND messages.id == messagesText.docid";
@ -183,45 +183,96 @@ function scoreOffsets(aMessage, aContext) {
return score;
}
/**
* The searcher basically looks like a query, but is specialized for fulltext
* search against messages. Most of the explicit specialization involves
* crafting a SQL query that attempts to order the matches by likelihood that
* the user was looking for it. This is based on full-text matches combined
* with an explicit (generic) interest score value placed on the message at
* indexing time. This is followed by using the more generic gloda scoring
* mechanism to explicitly score the messages given the search context in
* addition to the more generic score adjusting rules.
*/
function GlodaMsgSearcher(aListener, aSearchString, aAndTerms) {
this.listener = aListener;
function GlodaMsgSearcher(aViewWrapper, aFulltextTerms) {
this.viewWrapper = aViewWrapper;
this.fulltextTerms = aFulltextTerms;
this.searchString = aSearchString;
this.fulltextTerms = this.parseSearchString(aSearchString);
this.andTerms = (aAndTerms != null) ? aAndTerms : true;
this.query = null;
this.collection = null;
this.scoresByUriAndKey = {};
this.whysByUriAndKey = {};
this.scoresById = {};
}
GlodaMsgSearcher.prototype = {
/**
* Number of messages to retrieve initially.
*/
retrievalLimit: 100,
retrievalLimit: 400,
parseSearchString: function GlodaMsgSearcher_parseSearchString(aSearchString) {
aSearchString = aSearchString.trim();
let terms = [];
while (aSearchString) {
if (aSearchString[0] == '"') {
let endIndex = aSearchString.indexOf(aSearchString[0], 1);
// eat the quote if it has no friend
if (endIndex == -1) {
aSearchString = aSearchString.substring(1);
continue;
}
terms.push(aSearchString.substring(1, endIndex).trim());
aSearchString = aSearchString.substring(endIndex + 1);
continue;
}
let spaceIndex = aSearchString.indexOf(" ");
if (spaceIndex == -1) {
terms.push(aSearchString);
break;
}
terms.push(aSearchString.substring(0, spaceIndex));
aSearchString = aSearchString.substring(spaceIndex+1);
}
return terms;
},
buildFulltextQuery: function GlodaMsgSearcher_buildFulltextQuery() {
let query = Gloda.newQuery(Gloda.NOUN_MESSAGE, {
noMagic: true,
explicitSQL: FULLTEXT_QUERY_EXPLICIT_SQL,
// osets is 0-based column number 9 (volatile to column changes)
// dascore becomes 0-based column number 10
// osets is 0-based column number 14 (volatile to column changes)
// dascore becomes 0-based column number 15
outerWrapColumns: [DASCORE_SQL_SNIPPET + " AS dascore"],
// save the offset column for extra analysis
stashColumns: [9]
stashColumns: [14]
});
query.fulltextMatches(this.fulltextTerms.join(" "));
let fulltextQueryString;
if (this.andTerms)
fulltextQueryString = '"' + this.fulltextTerms.join('" "') + '"';
else
fulltextQueryString = '"' + this.fulltextTerms.join('" OR "') + '"';
query.fulltextMatches(fulltextQueryString);
query.orderBy('-dascore');
query.limit(this.retrievalLimit);
return query;
},
go: function GlodaMsgSearcher_go() {
getCollection: function GlodaMsgSearcher_getCollection(
aListenerOverride, aData) {
if (aListenerOverride)
this.listener = aListenerOverride;
this.query = this.buildFulltextQuery();
this.collection = this.query.getCollection(this);
this.collection = this.query.getCollection(this, aData);
this.completed = false;
return this.collection;
},
@ -239,18 +290,25 @@ GlodaMsgSearcher.prototype = {
let item = aItems[i];
let score = scores[i];
let hdr = item.folderMessage;
if (hdr) {
this.scoresByUriAndKey[hdr.folder.URI + "-" + hdr.messageKey] = score;
actualItems.push(item);
}
this.scoresById[item.id] = score;
}
this.viewWrapper.onItemsAdded(actualItems, aCollection);
if (this.listener)
this.listener.onItemsAdded(actualItems, aCollection);
},
onItemsModified: function GlodaMsgSearcher_onItemsModified(aItems,
aCollection) {
if (this.listener)
this.listener.onItemsModified(aItems, aCollection);
},
onItemsRemoved: function GlodaMsgSearcher_onItemsRemoved(aItems,
aCollection) {
if (this.listener)
this.listener.onItemsRemoved(aItems, aCollection);
},
onItemsModified: function GlodaMsgSearcher_onItemsModified() {},
onItemsRemoved: function GlodaMsgSearcher_onItemsRemoved() {},
onQueryCompleted: function GlodaMsgSearcher_onQueryCompleted(aCollection) {
this.viewWrapper.onQueryCompleted(aCollection);
this.completed = true;
if (this.listener)
this.listener.onQueryCompleted(aCollection);
},
};
};

Просмотреть файл

@ -5,7 +5,7 @@
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
@ -32,7 +32,7 @@
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
*
* ***** END LICENSE BLOCK ***** */
EXPORTED_SYMBOLS = ['FreeTag', 'FreeTagNoun'];
@ -64,10 +64,10 @@ var FreeTagNoun = {
_log: Log4Moz.repository.getLogger("gloda.noun.freetag"),
name: "freetag",
class: FreeTag,
clazz: FreeTag,
allowsArbitraryAttrs: false,
usesParameter: true,
_listeners: [],
addListener: function(aListener) {
this._listeners.push(aListener);
@ -77,7 +77,7 @@ var FreeTagNoun = {
if (index >=0)
this._listeners.splice(index, 1);
},
populateKnownFreeTags: function() {
for each (let [,attr] in Iterator(this.objectNounOfAttributes)) {
let attrDB = attr.dbDef;
@ -86,7 +86,7 @@ var FreeTagNoun = {
}
}
},
knownFreeTags: {},
getFreeTag: function(aTagName) {
let tag = this.knownFreeTags[aTagName];
@ -98,10 +98,23 @@ var FreeTagNoun = {
return tag;
},
comparator: function gloda_noun_freetag_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.name.localeCompare(b.name);
},
toParamAndValue: function gloda_noun_freetag_toParamAndValue(aTag) {
return [aTag.name, null];
},
toJSON: function gloda_noun_freetag_toJSON(aTag) {
return aTag.name;
},

Просмотреть файл

@ -43,11 +43,14 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://app/modules/gloda/log4moz.js");
Cu.import("resource://app/modules/StringBundle.js");
const LOG = Log4Moz.repository.getLogger("gloda.noun.mimetype");
Cu.import("resource://app/modules/gloda/gloda.js");
let CategoryStringMap = {};
/**
* Mime type abstraction that exists primarily so we can map mime types to
* integer id's.
@ -55,11 +58,12 @@ Cu.import("resource://app/modules/gloda/gloda.js");
* Instances of this class should only be retrieved via |MimeTypeNoun|; no one
* should ever create an instance directly.
*/
function MimeType(aID, aType, aSubType, aFullType) {
function MimeType(aID, aType, aSubType, aFullType, aCategory) {
this._id = aID;
this._type = aType;
this._subType = aSubType;
this._fullType = aFullType;
this._category = aCategory;
}
MimeType.prototype = {
@ -78,6 +82,8 @@ MimeType.prototype = {
if (!this._fullType) {
this._fullType = aFullType;
[this._type, this._subType] = this._fullType.split("/");
this._category =
MimeTypeNoun._getCategoryForMimeType(aFullType, this._type);
}
},
/**
@ -90,11 +96,29 @@ MimeType.prototype = {
get fullType() { return this._fullType; },
toString: function () {
return this.fullType;
},
/**
* @return the category we believe this mime type belongs to. This category
* name should never be shown directly to the user. Instead, use
* |categoryLabel| to get the localized name for the category. The
* category mapping comes from mimeTypesCategories.js.
*/
get category() {
return this._category;
},
/**
* @return The localized label for the category from gloda.properties in the
* "gloda.mimetype.category.CATEGORY.label" definition using the value
* from |category|.
*/
get categoryLabel() {
return CategoryStringMap[this._category];
}
};
/**
* @namespace Mime type noun provider.
* Mime type noun provider.
*
* The set of MIME Types is sufficiently limited that we can keep them all in
* memory. In theory it is also sufficiently limited that we could use the
@ -110,13 +134,14 @@ var MimeTypeNoun = {
clazz: MimeType, // gloda supports clazz as well as class
allowsArbitraryAttrs: false,
_strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
// note! update test_noun_mimetype if you change our internals!
_mimeTypes: {},
_mimeTypesByID: {},
TYPE_BLOCK_SIZE: 8096, // bet you were expecting a power of 2!
// (we can fix this next time we bump the database schema version and have
// to resort to blowing the database away.)
TYPE_BLOCK_SIZE: 16384,
_mimeTypeHighID: {},
_mimeTypeRangeDummyObjects: {},
_highID: 0,
// we now use the exciting 'schema' mechanism of defineNoun to get our table
@ -129,22 +154,120 @@ var MimeTypeNoun = {
_init: function() {
LOG.debug("loading MIME types");
this._loadCategoryMapping();
this._loadMimeTypes();
},
_loadMimeTypes: function() {
/**
* A map from MIME type to category name.
*/
_mimeTypeToCategory: {},
/**
* Load the contents of mimeTypeCategories.js and populate
*/
_loadCategoryMapping: function MimeTypeNoun__loadCategoryMapping() {
let mimecatNS = {};
Cu.import("resource://app/modules/gloda/mimeTypeCategories.js",
mimecatNS);
let mcm = mimecatNS.MimeCategoryMapping;
let mimeTypeToCategory = this._mimeTypeToCategory;
function procMapObj(aSubTree, aCategories) {
for each (let [key, value] in Iterator(aSubTree)) {
// Add this category to our nested categories list. Use concat since
// the list will be long-lived and each list needs to be distinct.
let categories = aCategories.concat();
categories.push(key);
if (categories.length == 1) {
CategoryStringMap[key] =
MimeTypeNoun._strings.get(
"gloda.mimetype.category." + key + ".label");
}
// Is it an array? (We do not have isArray in 1.9.1 and since it comes
// from another JS module, it has its own Array global, so instanceof
// fails us.) If it is, just process this depth
if ("length" in value) {
for each (let [, mimeTypeStr] in Iterator(value)) {
mimeTypeToCategory[mimeTypeStr] = categories;
}
}
// it's yet another sub-tree branch
else {
procMapObj(value, categories);
}
}
}
procMapObj(mimecatNS.MimeCategoryMapping, []);
},
/**
* Lookup the category associated with a MIME type given its full type and
* type. (So, "foo/bar" and "foo" for "foo/bar".)
*/
_getCategoryForMimeType:
function MimeTypeNoun__getCategoryForMimeType(aFullType, aType) {
if (aFullType in this._mimeTypeToCategory)
return this._mimeTypeToCategory[aFullType][0];
let wildType = aType + "/*";
if (wildType in this._mimeTypeToCategory)
return this._mimeTypeToCategory[wildType][0];
return this._mimeTypeToCategory["*"][0];
},
/**
* In order to allow the gloda query mechanism to avoid hitting the database,
* we need to either define the noun type as cachable and have a super-large
* cache or simply have a collection with every MIME type in it that stays
* alive forever.
* This is that collection. It is initialized by |_loadMimeTypes|. As new
* MIME types are created, we add them to the collection.
*/
_universalCollection: null,
/**
* Kick off a query of all the mime types in our database, leaving
* |_processMimeTypes| to actually do the legwork.
*/
_loadMimeTypes: function MimeTypeNoun__loadMimeTypes() {
// get all the existing mime types!
let query = Gloda.newQuery(this.id);
let nullFunc = function() {};
query.getCollection({
onItemsAdded: nullFunc, onItemsModified: nullFunc, onItemsRemoved: null,
this._universalCollection = query.getCollection({
onItemsAdded: nullFunc, onItemsModified: nullFunc,
onItemsRemoved: nullFunc,
onQueryCompleted: function (aCollection) {
MimeTypeNoun._processMimeTypes(aCollection.items);
}
}, null).becomeExplicit();
}, null);
},
_processMimeTypes: function(aMimeTypes) {
/**
* For the benefit of our Category queryHelper, we need dummy ranged objects
* that cover the numerical address space allocated to the category. We
* can't use a real object for the upper-bound because the upper-bound is
* constantly growing and there is the chance the query might get persisted,
* which means these values need to be long-lived. Unfortunately, our
* solution to this problem (dummy objects) complicates the second case,
* should it ever occur. (Because the dummy objects cannot be persisted
* on their own... but there are other issues that will come up that we will
* just have to deal with then.)
*/
_createCategoryDummies: function (aId, aCategory) {
let blockBottom = aId - (aId % this.TYPE_BLOCK_SIZE);
let blockTop = blockBottom + this.TYPE_BLOCK_SIZE - 1;
this._mimeTypeRangeDummyObjects[aCategory] = [
new MimeType(blockBottom, "!category-dummy!", aCategory,
"!category-dummy!/" + aCategory, aCategory),
new MimeType(blockTop, "!category-dummy!", aCategory,
"!category-dummy!/" + aCategory, aCategory)
];
},
_processMimeTypes: function MimeTypeNoun__processMimeTypes(aMimeTypes) {
for each (let [, mimeType] in Iterator(aMimeTypes)) {
if (mimeType.id > this._highID)
this._highID = mimeType.id;
@ -152,34 +275,45 @@ var MimeTypeNoun = {
this._mimeTypesByID[mimeType.id] = mimeType;
let typeBlock = mimeType.id - (mimeType.id % this.TYPE_BLOCK_SIZE);
let blockHighID = this._mimeTypeHighID[mimeType.type];
let blockHighID = (mimeType.category in this._mimeTypeHighID) ?
this._mimeTypeHighID[mimeType.category] : undefined;
// create the dummy range objects
if (blockHighID === undefined)
this._createCategoryDummies(mimeType.id, mimeType.category);
if ((blockHighID === undefined) || mimeType.id > blockHighID)
this._mimeTypeHighID[mimeType.type] = mimeType.id;
this._mimeTypeHighID[mimeType.category] = mimeType.id;
}
},
_addNewMimeType: function(aMimeTypeName) {
_addNewMimeType: function MimeTypeNoun__addNewMimeType(aMimeTypeName) {
let [typeName, subTypeName] = aMimeTypeName.split("/");
let category = this._getCategoryForMimeType(aMimeTypeName, typeName);
if (!(typeName in this._mimeTypeHighID)) {
if (!(category in this._mimeTypeHighID)) {
let nextID = this._highID - (this._highID % this.TYPE_BLOCK_SIZE) +
this.TYPE_BLOCK_SIZE;
this._mimeTypeHighID[typeName] = nextID;
this._mimeTypeHighID[category] = nextID;
this._createCategoryDummies(nextID, category);
}
let nextID = ++this._mimeTypeHighID[typeName];
let nextID = ++this._mimeTypeHighID[category];
let mimeType = new MimeType(nextID, typeName, subTypeName, aMimeTypeName);
let mimeType = new MimeType(nextID, typeName, subTypeName, aMimeTypeName,
category);
if (mimeType.id > this._highID)
this._highID = mimeType.id;
this._mimeTypes[aMimeTypeName] = mimeType;
this._mimeTypesByID[nextID] = mimeType;
// as great as the gloda extension mechanisms are, we don't think it makes
// As great as the gloda extension mechanisms are, we don't think it makes
// a lot of sense to use them in this case. So we directly trigger object
// insertion without any of the grokNounItem stuff.
this.objInsert.call(this.datastore, mimeType);
// Since we bypass grokNounItem and its fun, we need to explicitly add the
// new MIME-type to _universalCollection ourselves. Don't try this at
// home, kids.
this._universalCollection._onItemsAdded([mimeType]);
return mimeType;
},
@ -191,7 +325,7 @@ var MimeTypeNoun = {
* (which will be ignored). A mime type is of the form "type/subtype".
* A type with parameters would look like 'type/subtype; param="value"'.
*/
getMimeType: function(aMimeTypeName) {
getMimeType: function MimeTypeNoun_getMimeType(aMimeTypeName) {
// first, lose any parameters
let semiIndex = aMimeTypeName.indexOf(";");
if (semiIndex >= 0)
@ -204,6 +338,49 @@ var MimeTypeNoun = {
return this._addNewMimeType(aMimeTypeName);
},
/**
* Query helpers contribute additional functions to the query object for the
* attributes that use the noun type. For example, we define Category, so
* for the "attachmentTypes" attribute, "attachmentTypesCategory" would be
* exposed.
*/
queryHelpers: {
/**
* Query for MIME type categories based on one or more MIME type objects
* passed in. We want the range to span the entire block allocated to the
* category.
*
* @param aAttrDef The attribute that is using us.
* @param aArguments The actual arguments object that
*/
Category: function(aAttrDef, aArguments) {
let rangePairs = [];
// If there are no arguments then we want to fall back to the 'in'
// constraint which matches on any attachment.
if (aArguments.length == 0)
return this._inConstraintHelper(aAttrDef, []);
for (let iArg = 0; iArg < aArguments.length; iArg++) {
let arg = aArguments[iArg];
rangePairs.push(MimeTypeNoun._mimeTypeRangeDummyObjects[arg.category]);
}
return this._rangedConstraintHelper(aAttrDef, rangePairs);
}
},
comparator: function gloda_noun_mimeType_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.fullType.localeCompare(b.fullType);
},
toParamAndValue: function gloda_noun_mimeType_toParamAndValue(aMimeType) {
return [null, aMimeType.id];
},

Просмотреть файл

@ -5,7 +5,7 @@
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
@ -32,7 +32,7 @@
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
*
* ***** END LICENSE BLOCK ***** */
EXPORTED_SYMBOLS = ['TagNoun'];
@ -49,22 +49,23 @@ Cu.import("resource://app/modules/gloda/gloda.js");
*/
var TagNoun = {
name: "tag",
class: Ci.nsIMsgTag,
clazz: Ci.nsIMsgTag,
usesParameter: true,
allowsArbitraryAttrs: false,
idAttr: "key",
_msgTagService: null,
_tagMap: null,
_init: function () {
this._msgTagService = Cc["@mozilla.org/messenger/tagservice;1"].
getService(Ci.nsIMsgTagService);
this._updateTagMap();
},
getAllTags: function gloda_noun_tag_getAllTags() {
return this._msgTagService.getAllTags({});
},
_updateTagMap: function gloda_noun_tag_updateTagMap() {
this._tagMap = {};
let tagArray = this._msgTagService.getAllTags({});
@ -73,9 +74,25 @@ var TagNoun = {
this._tagMap[tag.key] = tag;
}
},
comparator: function gloda_noun_tag_comparator(a, b) {
if (a == null) {
if (b == null)
return 0;
else
return 1;
}
else if (b == null) {
return -1;
}
return a.tag.localeCompare(b.tag);
},
userVisibleString: function gloda_noun_tag_userVisibleString(aTag) {
return aTag.tag;
},
// we cannot be an attribute value
toParamAndValue: function gloda_noun_tag_toParamAndValue(aTag) {
return [aTag.key, null];
},

Просмотреть файл

@ -135,6 +135,7 @@ GlodaQueryClass.prototype = {
* removed respectively.
*/
getCollection: function gloda_query_getCollection(aListener, aData) {
this.completed = false;
return this._nounDef.datastore.queryFromQuery(this, aListener, aData);
},
@ -157,6 +158,7 @@ GlodaQueryClass.prototype = {
iConstraint++) {
let constraint = curQuery._constraints[iConstraint];
let [constraintType, attrDef] = constraint;
let boundName = attrDef ? attrDef.boundName : "id";
let constraintValues = constraint.slice(2);
if (constraintType === GlodaDatastore.kConstraintIdIn) {
@ -176,10 +178,18 @@ GlodaQueryClass.prototype = {
// code complexity costs...)
if (objectNounDef.equals) {
let testValues;
if (attrDef.singular)
testValues = [aObj[attrDef.boundName]];
if (!(boundName in aObj))
testValues = [];
else if (attrDef.singular)
testValues = [aObj[boundName]];
else
testValues = aObj[attrDef.boundName];
testValues = aObj[boundName];
// If there are no constraints, then we are just testing for there
// being a value. Succeed (continue) in that case.
if (constraintValues.length == 0 && testValues.length &&
testValues[0] != null)
continue;
let foundMatch = false;
for each (let [,testValue] in Iterator(testValues)) {
@ -204,10 +214,18 @@ GlodaQueryClass.prototype = {
// what we did in the prior case but exploding values using
// toParamAndValue, and then comparing.
let testValues;
if (attrDef.singular)
testValues = [aObj[attrDef.boundName]];
if (!(boundName in aObj))
testValues = [];
else if (attrDef.singular)
testValues = [aObj[boundName]];
else
testValues = aObj[attrDef.boundName];
testValues = aObj[boundName];
// If there are no constraints, then we are just testing for there
// being a value. Succeed (continue) in that case.
if (constraintValues.length == 0 && testValues.length &&
testValues[0] != null)
continue;
let foundMatch = false;
for each (let [,testValue] in Iterator(testValues)) {
@ -233,10 +251,12 @@ GlodaQueryClass.prototype = {
let objectNounDef = attrDef.objectNounDef;
let testValues;
if (attrDef.singular)
testValues = [aObj[attrDef.boundName]];
if (!(boundName in aObj))
testValues = [];
else if (attrDef.singular)
testValues = [aObj[boundName]];
else
testValues = aObj[attrDef.boundName];
testValues = aObj[boundName];
let foundMatch = false;
for each (let [,testValue] in Iterator(testValues)) {
@ -282,7 +302,7 @@ GlodaQueryClass.prototype = {
// @testpoint gloda.query.test.kConstraintStringLike
else if (constraintType === GlodaDatastore.kConstraintStringLike) {
let curIndex = 0;
let value = aObj[attrDef.boundName];
let value = (boundName in aObj) ? aObj[boundName] : "";
// the attribute must be singular, we don't support arrays of strings.
for each (let [iValuePart, valuePart] in Iterator(constraintValues)) {
if (typeof valuePart == "string") {
@ -332,6 +352,37 @@ GlodaQueryClass.prototype = {
}
return false;
},
/**
* Helper code for noun definitions of queryHelpers that want to build a
* traditional in/equals constraint. The goal is to let them build a range
* without having to know how we structure |_constraints|.
*
* @protected
*/
_inConstraintHelper:
function gloda_query__discreteConstraintHelper(aAttrDef, aValues) {
let constraint =
[GlodaDatastore.kConstraintIn, aAttrDef].concat(aValues);
this._constraints.push(constraint);
return this;
},
/**
* Helper code for noun definitions of queryHelpers that want to build a
* range. The goal is to let them build a range without having to know how
* we structure |_constraints| or requiring them to mark themselves as
* continuous to get a "Range".
*
* @protected
*/
_rangedConstraintHelper:
function gloda_query__rangedConstraintHelper(aAttrDef, aRanges) {
let constraint =
[GlodaDatastore.kConstraintRanges, aAttrDef].concat(aRanges);
this._constraints.push(constraint);
return this;
}
};
/**

Просмотреть файл

@ -64,6 +64,9 @@ var GlodaUtils = {
* 4 attributes, as described below. We will use the example of the user
* passing an argument of '"Bob Smith" <bob@company.com>'.
*
* This method (by way of nsIMsgHeaderParser) takes care of decoding mime
* headers, but is not aware of folder-level character set overrides.
*
* count: the number of addresses parsed. (ex: 1)
* addresses: a list of e-mail addresses (ex: ["bob@company.com"])
* names: a list of names (ex: ["Bob Smith"])

Просмотреть файл

@ -38,6 +38,8 @@
// -- Pull in the POP3 fake-server / local account helper code
load("../../test_mailnewslocal/unit/head_maillocal.js");
Components.utils.import("resource://app/modules/errUtils.js");
/**
* Create a 'me' identity of "me@localhost" for the benefit of Gloda. At the
* time of this writing, Gloda only initializes Gloda.myIdentities and
@ -875,9 +877,11 @@ QueryExpectationListener.prototype = {
this.nextIndex++;
// make sure the query's test method agrees with the database about this
if (!aCollection.query.test(item))
if (!aCollection.query.test(item)) {
logObject(item);
do_throw("Query test returned false when it should have been true on " +
"extracted: " + glodaStringRep + " item: " + item);
}
}
},
onItemsModified: function query_expectation_onItemsModified(aItems,

Просмотреть файл

@ -59,7 +59,8 @@ function setup_test_noun_and_attributes() {
WidgetNoun = Gloda.defineNoun({
name: "widget",
clazz: Widget,
allowArbitraryAttrs: true,
allowsArbitraryAttrs: true,
//cache: true, cacheCost: 32,
schema: {
columns: [['id', 'INTEGER PRIMARY KEY'],
['intCol', 'NUMBER', 'inum'],
@ -82,7 +83,7 @@ function setup_test_noun_and_attributes() {
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "intCol",
attributeName: "inum",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "intCol",
@ -92,7 +93,7 @@ function setup_test_noun_and_attributes() {
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "dateCol",
attributeName: "date",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "dateCol",
@ -102,7 +103,7 @@ function setup_test_noun_and_attributes() {
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "strCol",
attributeName: "str",
singular: true,
special: Gloda.kSpecialString,
specialColumnName: "strCol",
@ -115,7 +116,7 @@ function setup_test_noun_and_attributes() {
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "fulltextOne",
attributeName: "text1",
singular: true,
special: Gloda.kSpecialFulltext,
specialColumnName: "fulltextOne",
@ -125,7 +126,7 @@ function setup_test_noun_and_attributes() {
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "fulltextTwo",
attributeName: "text2",
singular: true,
special: Gloda.kSpecialFulltext,
specialColumnName: "fulltextTwo",
@ -153,6 +154,15 @@ function setup_test_noun_and_attributes() {
objectNoun: Gloda.NOUN_NUMBER
});
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "multiIntAttr",
singular: false,
subjectNouns: [WidgetNoun.id],
objectNoun: Gloda.NOUN_NUMBER
});
next_test();
}
@ -174,11 +184,57 @@ function test_lots_of_string_constraints() {
}
let query = Gloda.newQuery(WidgetNoun.id);
query.strCol.apply(query, stringConstraints);
query.str.apply(query, stringConstraints);
queryExpect(query, []);
}
/* === Query === */
/**
* Use a counter so that each test can have its own unique value for intCol so
* that it can use that as a constraint. Otherwise we would need to purge
* between every test. That's not an unreasonable alternative, but this works.
* Every test should increment this before using it.
*/
var testUnique = 100;
/**
* Widgets with multiIntAttr populated with one or more values.
*/
var nonSingularWidgets;
/**
* Widgets with multiIntAttr unpopulated.
*/
var singularWidgets;
function setup_non_singular_values() {
testUnique++;
let origin = new Date("2007/01/01");
nonSingularWidgets = [
new Widget(testUnique, origin, "ns1", 0, "", ""),
new Widget(testUnique, origin, "ns2", 0, "", ""),
];
singularWidgets = [
new Widget(testUnique, origin, "s1", 0, "", ""),
new Widget(testUnique, origin, "s2", 0, "", ""),
];
nonSingularWidgets[0].multiIntAttr = [1, 2];
nonSingularWidgets[1].multiIntAttr = [3];
singularWidgets[0].multiIntAttr = [];
// and don't bother setting it on singularWidgets[1]
runOnIndexingComplete(next_test);
GenericIndexer.indexNewObjects(nonSingularWidgets.concat(singularWidgets));
}
function test_query_has_value_for_non_singular() {
let query = Gloda.newQuery(WidgetNoun.id);
query.inum(testUnique);
query.multiIntAttr();
queryExpect(query, nonSingularWidgets);
}
/* === Search === */
/*
* The conceit of our search is that more recent messages are better than older
@ -311,6 +367,8 @@ function test_search_ranking_idiom_score() {
var tests = [
setup_test_noun_and_attributes,
test_lots_of_string_constraints,
setup_non_singular_values,
test_query_has_value_for_non_singular,
setup_search_ranking_idiom,
test_search_ranking_idiom_offsets,
test_search_ranking_idiom_score,