Import the gloda-facet patch with davida's quicksearch changes and CSS pulled in.
--HG-- branch : gloda-facet
This commit is contained in:
Родитель
36e13cf9f8
Коммит
559e0a1ab6
|
@ -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,
|
||||
|
|
Загрузка…
Ссылка в новой задаче