Bug 1817682 - Functions for extensions to add custom column handlers. r=darktrojan

Differential Revision: https://phabricator.services.mozilla.com/D179632

--HG--
extra : amend_source : 5e8c2382b2823f0cbd581254cda378aa68722e17
This commit is contained in:
John Bieling 2024-01-30 09:28:00 +02:00
Родитель 6904135ab1
Коммит 861a5ccec8
10 изменённых файлов: 575 добавлений и 91 удалений

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

@ -58,10 +58,9 @@ const messengerBundle = Services.strings.createBundle(
"chrome://messenger/locale/messenger.properties"
);
const { getDefaultColumns, getDefaultColumnsForCardsView, isOutgoing } =
ChromeUtils.importESModule(
"chrome://messenger/content/thread-pane-columns.mjs"
);
const { ThreadPaneColumns } = ChromeUtils.importESModule(
"chrome://messenger/content/thread-pane-columns.mjs"
);
// As defined in nsMsgDBView.h.
const MSG_VIEW_FLAG_DUMMY = 0x20000000;
@ -3955,9 +3954,9 @@ var threadPane = {
*/
isFirstScroll: true,
columns: getDefaultColumns(gFolder),
columns: ThreadPaneColumns.getDefaultColumns(gFolder),
cardColumns: getDefaultColumnsForCardsView(gFolder),
cardColumns: ThreadPaneColumns.getDefaultColumnsForCardsView(gFolder),
async init() {
await quickFilterBar.init();
@ -3966,6 +3965,9 @@ var threadPane = {
Services.prefs.addObserver("mailnews.tags.", this);
Services.obs.addObserver(this, "addrbook-displayname-changed");
Services.obs.addObserver(this, "custom-column-added");
Services.obs.addObserver(this, "custom-column-removed");
Services.obs.addObserver(this, "custom-column-refreshed");
threadTree = document.getElementById("threadTree");
this.treeTable = threadTree.table;
@ -4079,6 +4081,9 @@ var threadPane = {
uninit() {
Services.prefs.removeObserver("mailnews.tags.", this);
Services.obs.removeObserver(this, "addrbook-displayname-changed");
Services.obs.removeObserver(this, "custom-column-added");
Services.obs.removeObserver(this, "custom-column-removed");
Services.obs.removeObserver(this, "custom-column-refreshed");
},
handleEvent(event) {
@ -4132,12 +4137,25 @@ var threadPane = {
}
},
observe(subject, topic, data) {
if (topic == "nsPref:changed") {
this.setUpTagStyles();
} else if (topic == "addrbook-displayname-changed") {
// This runs the when mail.displayname.version preference observer is
// notified/the mail.displayname.version number has been updated.
threadTree.invalidate();
switch (topic) {
case "nsPref:changed":
this.setUpTagStyles();
break;
case "addrbook-displayname-changed":
// This runs the when mail.displayname.version preference observer is
// notified/the mail.displayname.version number has been updated.
threadTree.invalidate();
break;
case "custom-column-refreshed":
// TODO: Invalidate only the column identified by data.
threadTree.invalidate();
break;
case "custom-column-added":
this.addCustomColumn(data);
break;
case "custom-column-removed":
this.removeCustomColumn(data);
break;
}
},
@ -4150,7 +4168,10 @@ var threadPane = {
return;
}
threadTree.classList.toggle("is-outgoing", isOutgoing(gFolder));
threadTree.classList.toggle(
"is-outgoing",
ThreadPaneColumns.isOutgoing(gFolder)
);
},
/**
@ -4830,7 +4851,7 @@ var threadPane = {
restoreColumnsState() {
// Always fetch a fresh array of columns for the cards view even if we don't
// have a folder defined.
this.cardColumns = getDefaultColumnsForCardsView(gFolder);
this.cardColumns = ThreadPaneColumns.getDefaultColumnsForCardsView(gFolder);
this.updateClassList();
// Avoid doing anything if no folder has been loaded yet.
@ -4853,7 +4874,7 @@ var threadPane = {
// default columns for the currently visible folder, otherwise the table
// layout will maintain whatever state is currently set from the previous
// folder, which it doesn't reflect reality.
this.columns = getDefaultColumns(gFolder);
this.columns = ThreadPaneColumns.getDefaultColumns(gFolder);
return;
}
@ -4876,6 +4897,34 @@ var threadPane = {
});
},
makeCustomColumnCell(column) {
if (!column?.custom) {
throw new Error(`Not a custom column: ${column?.id}`);
}
const cell = document.createElement("td");
const columnName = column.id.toLowerCase();
cell.classList.add(`${columnName}-column`);
// Default columns have this hardcoded in about3Pane.xhtml.
cell.dataset.columnName = columnName;
if (column.icon && column.iconCellDefinitions) {
cell.classList.add("button-column");
// Add predefined icons for custom icon columns.
for (const { id, url, title, alt } of column.iconCellDefinitions) {
const img = document.createElement("img");
img.dataset.cellIconId = id;
img.src = url;
img.alt = alt || "";
img.title = title || "";
img.hidden = true;
cell.appendChild(img);
}
}
return cell;
},
/**
* Force an update of the thread tree to reflect the columns change.
*
@ -4885,6 +4934,11 @@ var threadPane = {
updateColumns(isSimple = false) {
if (!this.rowTemplate) {
this.rowTemplate = document.getElementById("threadPaneRowTemplate");
this.rowTemplate.content.append(
...ThreadPaneColumns.getCustomColumns().map(column =>
this.makeCustomColumnCell(column)
)
);
}
// Update the row template to match the column properties.
@ -4910,14 +4964,60 @@ var threadPane = {
* Restore the default columns visibility and order and save the change.
*/
restoreDefaultColumns() {
this.columns = getDefaultColumns(gFolder, gViewWrapper?.isSynthetic);
this.cardColumns = getDefaultColumnsForCardsView(gFolder);
this.columns = ThreadPaneColumns.getDefaultColumns(
gFolder,
gViewWrapper?.isSynthetic
);
this.cardColumns = ThreadPaneColumns.getDefaultColumnsForCardsView(gFolder);
this.updateClassList();
this.updateColumns();
threadTree.reset();
this.persistColumnStates();
},
/**
* Adds a custom column to the thread pane.
*
* @param {string} columnID - uniqe id of the custom column
*/
addCustomColumn(columnID) {
const column = ThreadPaneColumns.getColumn(columnID);
if (this.rowTemplate) {
this.rowTemplate.content.appendChild(this.makeCustomColumnCell(column));
}
this.columns.push(column);
const columnStates =
gFolder.msgDatabase.dBFolderInfo.getCharProperty("columnStates");
if (columnStates) {
this.applyPersistedColumnsState(JSON.parse(columnStates));
}
gViewWrapper?.dbView.addColumnHandler(column.id, column.handler);
this.updateColumns();
this.restoreSortIndicator();
threadTree.reset();
},
/**
* Removes a custom column from the thread pane.
*
* @param {string} columnID - uniqe id of the custom column
*/
removeCustomColumn(columnID) {
if (this.rowTemplate) {
this.rowTemplate.content
.querySelector(`td.${columnID.toLowerCase()}-column`)
?.remove();
}
this.columns = this.columns.filter(column => column.id != columnID);
this.updateColumns();
gViewWrapper?.dbView.removeColumnHandler(columnID);
threadTree.reset();
},
/**
* Shift the ordinal of a column by one based on the visible columns.
*
@ -4972,7 +5072,7 @@ var threadPane = {
this.columns = data.columns;
this.persistColumnStates();
this.updateColumns(true);
this.updateColumns();
threadTree.reset();
},
@ -5038,25 +5138,30 @@ var threadPane = {
* @param {object} data - The detail of the custom event.
*/
onSortChanged(data) {
const sortColumn = sortController.convertSortTypeToColumnID(
const curSortColumnId = sortController.convertSortTypeToColumnID(
gViewWrapper.primarySortType
);
const column = data.column;
const newSortColumnId = data.column;
// A click happened on the column that is already used to sort the list.
if (sortColumn == column) {
if (curSortColumnId == newSortColumnId) {
if (gViewWrapper.isSortedAscending) {
sortController.sortDescending();
} else {
sortController.sortAscending();
}
this.updateSortIndicator(column);
this.updateSortIndicator(newSortColumnId);
return;
}
const sortName = this.columns.find(c => c.id == data.column).sortKey;
// If the new sort column is a custom column, we need to pass the id of the
// custom column, instead of the the general "byCustom" sortName.
const newSortColumnData = this.columns.find(c => c.id == newSortColumnId);
const sortName = newSortColumnData.custom
? newSortColumnId
: newSortColumnData.sortKey;
sortController.sortThreadPane(sortName);
this.updateSortIndicator(column);
this.updateSortIndicator(newSortColumnId);
},
/**
@ -5068,6 +5173,10 @@ var threadPane = {
this.treeTable
.querySelector(".sorting")
?.classList.remove("sorting", "ascending", "descending");
// The column could be a removed custom column.
if (!column) {
return;
}
this.treeTable
.querySelector(`#${column} button`)
?.classList.add(
@ -5134,7 +5243,7 @@ var threadPane = {
swappedColumnStateString = columStateString;
}
const currentFolderIsOutgoing = isOutgoing(gFolder);
const currentFolderIsOutgoing = ThreadPaneColumns.isOutgoing(gFolder);
/**
* Update the columnStates property of the folder database and forget the
@ -5149,7 +5258,7 @@ var threadPane = {
// Check if the destination folder we're trying to update matches the same
// special state of the folder we're getting the column state from.
const colStateString =
isOutgoing(folder) == currentFolderIsOutgoing
ThreadPaneColumns.isOutgoing(folder) == currentFolderIsOutgoing
? columStateString
: swappedColumnStateString;
@ -5984,6 +6093,15 @@ customElements.whenDefined("tree-view-table-row").then(() => {
continue;
}
if (column.custom && column.icon) {
const images = cell.querySelectorAll("img");
for (const image of images) {
const cellIconProperty = `${column.id}-${image.dataset.cellIconId}`;
image.hidden = !propertiesSet.has(cellIconProperty);
}
continue;
}
if (textIndex >= 0) {
if (isDummyRow) {
cell.textContent = "";
@ -6400,7 +6518,12 @@ var sortController = {
this.sortThreadPane("byDate");
},
sortThreadPane(sortName) {
const sortType = Ci.nsMsgViewSortType[sortName];
// The sortName is either a sortKey (see Ci.nsMsgViewSortType and
// DEFAULT_COLUMNS in thread-pane-columns.js), or the id of a custom column.
// TODO: Using the columnId as input whould be a lot more obvious, here and
// in gViewWrapper.sort(), because now sortType could be a string or an int.
const sortType = Ci.nsMsgViewSortType[sortName] ?? sortName;
const grouped = gViewWrapper.showGroupedBySort;
gViewWrapper._threadExpandAll = Boolean(
gViewWrapper._viewFlags & Ci.nsMsgViewFlagsType.kExpandAll
@ -6594,9 +6717,9 @@ var sortController = {
break;
case Ci.nsMsgViewSortType.byCustom:
// TODO: either change try() catch to if (property exists) or restore
// the getColumnHandler() check.
// the ThreadPaneColumns.getColumnHandler() check.
try {
// getColumnHandler throws an error when the ID is not handled
// ThreadPaneColumns.getColumnHandler throws an error when the ID is not handled
columnID = gDBView.curCustomColumn;
} catch (e) {
// error - means no handler

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

@ -8,6 +8,9 @@
// msgViewNavigation.js
/* globals CrossFolderNavigation */
// about3pane.js
/* globals ThreadPaneColumns */
var { MailServices } = ChromeUtils.import(
"resource:///modules/MailServices.jsm"
);
@ -980,6 +983,9 @@ var dbViewWrapperListener = {
onSearching(isSearching) {},
onCreatedView() {
if (window.threadTree) {
for (const col of ThreadPaneColumns.getCustomColumns()) {
gViewWrapper.dbView.addColumnHandler(col.id, col.handler);
}
window.threadPane.setTreeView(gViewWrapper.dbView);
window.threadPane.isFirstScroll = true;
window.threadPane.scrollDetected = false;

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

@ -266,7 +266,7 @@ const DEFAULT_COLUMNS = [
* @param {nsIMsgFolder} folder - The message folder.
* @returns {boolean} True if the folder is Outgoing.
*/
export const isOutgoing = folder => {
const isOutgoing = folder => {
return folder.isSpecialFolder(
lazy.DBViewWrapper.prototype.OUTGOING_FOLDER_FLAGS,
true
@ -283,7 +283,7 @@ export const isOutgoing = folder => {
* the gloda results list.
* @returns {object[]}
*/
export function getDefaultColumns(folder, isSynthetic = false) {
function getDefaultColumns(folder, isSynthetic = false) {
// Create a clone we can edit.
const updatedColumns = DEFAULT_COLUMNS.map(column => ({ ...column }));
@ -379,7 +379,206 @@ function getProperSenderForCardsView(folder) {
* @param {?nsIMsgFolder} folder - The currently viewed folder if available.
* @returns {string[]}
*/
export function getDefaultColumnsForCardsView(folder) {
function getDefaultColumnsForCardsView(folder) {
const sender = getProperSenderForCardsView(folder);
return ["subjectCol", sender, "dateCol", "tagsCol", "totalCol"];
}
/**
* @typedef CustomColumnProperties
* @property {string} name - Name of the column as displayed in the column
* header of text columns, and in the column picker menu.
* @property {boolean} [hidden] - Whether the column should be initially hidden.
* @property {boolean} [icon] - Whether the column is an icon column.
* @property {IconCellDefinition[]} [iconCellDefinitions] - Cell icon definitions
* for the column. Required if the icon property is set.
* @property {string} [iconHeaderUrl] - Header icon url for the column.
* Required if the icon property is set.
* @property {boolean} [resizable] - Whether the column should be resizable.
* @property {boolean} [sortable] - Whether the column should be sortable.
*
* @property {TextCallback} textCallback - Callback function to retrieve the
* text to be used for a given msgHdr. Used for sorting if no dedicated
* sortCallback function given. Also used as display name if columns are
* grouped by the sorted column.
* @property {IconCallback} [iconCallback] - Callback function to retrieve the
* icon id to be used for a given msgHdr. Required if icon property is set.
* @property {SortCallback} [sortCallback] - Callback function to retrieve a
* numeric sort key for a given msgHdr. If not given, column will be sorted
* by the value returned by specified textCallback.
*/
/**
* @typedef IconCellDefinition
* @property {string} id - The id of the icon. Must be alphanumeric only.
* @property {string} url - The url of the icon.
* @property {string} [title] - Optional value for the icon's title attribute.
* @property {string} [alt] - Optional value for the icon's alt attribute.
*/
/**
* Callback function to retrieve the icon to be used for the given msgHdr.
* @callback IconCallback
* @param {nsIMsgDBHdr} msgHdr
*
* @returns {string} The id of the icon to be used, as specified in the
* iconCellDefinitions property.
*/
/**
* Callback function to retrieve a numeric sort key for the given msgHdr.
* @callback SortCallback
* @param {nsIMsgDBHdr} msgHdr
*
* @returns {integer} A numeric sort key.
*/
/**
* Callback function to retrieve the text to be used for the given msgHdr.
* @callback TextCallback
* @param {nsIMsgDBHdr} msgHdr
*
* @returns {string} The text content.
*/
/**
* Register a custom column.
*
* @param {string} id - uniqe id of the custom column
* @param {CustomColumnProperties} properties
*/
function addCustomColumn(id, properties) {
const {
name: columnName,
resizable = true,
hidden = false,
icon = false,
sortable = false,
iconCellDefinitions = [],
iconHeaderUrl = "",
iconCallback = null,
sortCallback = null,
textCallback = null,
} = properties;
if (DEFAULT_COLUMNS.some(column => column.id == id)) {
throw new Error(`Cannot add custom column, id is already used: ${id}`);
}
if (!columnName) {
throw new Error(`Missing name property for custom column: ${id}`);
}
if (icon) {
if (!iconCellDefinitions || !iconHeaderUrl || !iconCallback) {
throw new Error(`Invalid icon properties for custom icon column: ${id}`);
}
if (iconCellDefinitions.some(e => !e.id || !e.url || /\W/g.test(e.id))) {
throw new Error(
`Invalid icon definition: ${JSON.stringify(iconCellDefinitions)}`
);
}
}
if (!textCallback) {
throw new Error(`Missing textCallback property for custom column: ${id}`);
}
const columnDef = {
id,
name: columnName,
ordinal: DEFAULT_COLUMNS.length + 1,
resizable,
hidden,
icon,
sortable,
custom: true,
iconCellDefinitions,
iconHeaderUrl,
handler: {
QueryInterface: ChromeUtils.generateQI(["nsIMsgCustomColumnHandler"]),
getCellText: textCallback,
// With Bug 1192696, Grouped By Sort was implemented for custom columns.
// Implementers should consider that the value returned by GetSortStringForRow
// will be displayed in the grouped header row, as well as be used as the
// sort string.
getSortStringForRow: textCallback,
// Allow to provide a dedicated numerical sort function.
getSortLongForRow: sortCallback,
isString() {
return !sortCallback;
},
getRowProperties(msgHdr) {
// Row properties are used for icons.
if (icon) {
const iconId = iconCallback(msgHdr);
if (/\W/g.test(iconId)) {
throw new Error(`Invalid icon value used: ${iconId}`);
}
return `${id}-${iconId}`;
}
return "";
},
},
};
DEFAULT_COLUMNS.push(columnDef);
Services.obs.notifyObservers(null, "custom-column-added", id);
}
/**
* Unregister a custom column.
*
* @param {string} id - uniqe id of the custom column
*/
function removeCustomColumn(id) {
const index = DEFAULT_COLUMNS.findIndex(column => column.id == id);
if (index >= 0) {
DEFAULT_COLUMNS.splice(index, 1);
}
Services.obs.notifyObservers(null, "custom-column-removed", id);
}
/**
* Refresh display of a custom column.
*
* @param {string} id - uniqe id of the custom column
*/
function refreshCustomColumn(id) {
Services.obs.notifyObservers(null, "custom-column-refreshed", id);
}
/**
* Retrieve the registered column information for the column with the given id.
*
* @param {string} id - uniqe id of the custom column
* @returns {object} Entry of the DEFAULT_COLUMNS array with the given id, or null.
*/
function getColumn(id) {
const columnDef = DEFAULT_COLUMNS.find(column => column.id == id);
if (!columnDef) {
console.warn(`Found unknown column ${id}`);
return null;
}
return { ...columnDef };
}
/**
* Retrieve the registered column information of all custom columns.
*
* @returns {object} Entries of the DEFAULT_COLUMNS array of custom columns.
*/
function getCustomColumns() {
return DEFAULT_COLUMNS.filter(column => column.custom).map(column => ({
...column,
}));
}
export const ThreadPaneColumns = {
isOutgoing,
getDefaultColumns,
getDefaultColumnsForCardsView,
addCustomColumn,
removeCustomColumn,
refreshCustomColumn,
getColumn,
getCustomColumns,
};

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

@ -2228,6 +2228,8 @@ class TreeViewTableHeaderCell extends HTMLTableCellElement {
if (column.l10n?.header) {
document.l10n.setAttributes(this.#button, column.l10n.header);
} else if (column.name && !column.icon) {
this.#button.textContent = column.name;
}
// Add an image if this is a table header that needs to display an icon,
@ -2235,7 +2237,7 @@ class TreeViewTableHeaderCell extends HTMLTableCellElement {
if (column.icon) {
this.dataset.type = "icon";
const img = document.createElement("img");
img.src = "";
img.src = column.custom && column.icon ? column.iconHeaderUrl : "";
img.alt = "";
this.#button.appendChild(img);
}
@ -2464,6 +2466,8 @@ class TreeViewTableColumnPicker extends HTMLTableCellElement {
menuitem.setAttribute("closemenu", "none");
if (column.l10n?.menuitem) {
document.l10n.setAttributes(menuitem, column.l10n.menuitem);
} else if (column.name) {
menuitem.label = column.name;
}
menuitem.addEventListener("command", () => {

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

@ -0,0 +1,110 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
const { ThreadPaneColumns } = ChromeUtils.importESModule(
"chrome://messenger/content/thread-pane-columns.mjs"
);
add_task(function testGetters() {
const defaultColumns = ThreadPaneColumns.getDefaultColumns();
const defaultSubjectColumn = defaultColumns.find(
column => column.id == "subjectCol"
);
Assert.notEqual(
ThreadPaneColumns.getDefaultColumns().find(
column => column.id == "subjectCol"
),
defaultSubjectColumn,
"ThreadPaneColumns.getDefaultColumns should return different objects for each call"
);
const subjectColumn1 = ThreadPaneColumns.getColumn("subjectCol");
Assert.notEqual(
subjectColumn1,
defaultSubjectColumn,
"ThreadPaneColumns.getColumn and ThreadPaneColumns.getDefaultColumns should return different objects"
);
const subjectColumn2 = ThreadPaneColumns.getColumn("subjectCol");
Assert.notEqual(
subjectColumn1,
subjectColumn2,
"ThreadPaneColumns.getColumn should return different objects for each call"
);
subjectColumn1.testProperty = "hello!";
Assert.ok(
!defaultSubjectColumn.testProperty,
"property changes should not affect other instances of the same column"
);
Assert.ok(
!subjectColumn2.testProperty,
"property changes should not affect other instances of the same column"
);
});
add_task(function testCustomColumns() {
Assert.deepEqual(
ThreadPaneColumns.getCustomColumns(),
[],
"should be no custom columns"
);
Assert.equal(
ThreadPaneColumns.getColumn("testCol"),
null,
"ThreadPaneColumns.getColumn should return null"
);
ThreadPaneColumns.addCustomColumn("testCol", {
name: "Test Column",
textCallback: () => {
"static value";
},
});
const testColumn = ThreadPaneColumns.getColumn("testCol");
Assert.equal(testColumn.id, "testCol", "Column should have the correct id");
Assert.equal(
testColumn.name,
"Test Column",
"Column should have the correct name"
);
Assert.ok(testColumn.custom, "Column should be a custom column");
Assert.ok(!testColumn.sortable, "Column should be a sortable");
Assert.ok(
testColumn.handler.QueryInterface(Ci.nsIMsgCustomColumnHandler),
"Column handler should be a custom column handler"
);
const customColumns = ThreadPaneColumns.getCustomColumns();
Assert.equal(
customColumns.length,
1,
"should return a single custom column object"
);
Assert.notEqual(
customColumns[0],
testColumn,
"should return a complex test column object, a simple equal compare should fail"
);
Assert.deepEqual(
customColumns[0],
testColumn,
"should return the correct test column object"
);
ThreadPaneColumns.removeCustomColumn("testCol");
Assert.deepEqual(
ThreadPaneColumns.getCustomColumns(),
[],
"should find no custom columns"
);
Assert.equal(
ThreadPaneColumns.getColumn("testCol"),
null,
"ThreadPaneColumns.getColumn should return null"
);
});

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

@ -5,6 +5,7 @@ support-files = distribution.ini resources/*
[test_alertHook.js]
[test_attachmentChecker.js]
[test_columns.js]
[test_bug1086527.js]
[test_devtools_url.js]
[test_emptyTrash_dbViewWrapper.js]

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

@ -1728,6 +1728,8 @@ DBViewWrapper.prototype = {
if (sortTypeType != "number") {
sortCustomColumn = sortTypeType == "string" ? sortType : sortType.id;
sortType = Ci.nsMsgViewSortType.byCustom;
// Set correct sortType.
this._sort[aIndex][0] = sortType;
}
return [sortType, sortOrder, sortCustomColumn];

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

@ -36,6 +36,10 @@ var { GlodaSyntheticView } = ChromeUtils.import(
"resource:///modules/gloda/GlodaSyntheticView.jsm"
);
var { ThreadPaneColumns } = ChromeUtils.importESModule(
"chrome://messenger/content/thread-pane-columns.mjs"
);
var folderInbox, folderSent, folderVirtual, folderA, folderB;
// INBOX_DEFAULTS sans 'dateCol' but gains 'tagsCol'
var columnsB;
@ -100,7 +104,6 @@ add_setup(async function () {
useCorrespondent ? "correspondentCol" : "senderCol",
"junkStatusCol",
"dateCol",
"locationCol",
];
GLODA_DEFAULTS = [
"threadCol",
@ -147,12 +150,18 @@ function assert_visible_columns(desiredColumns) {
const visibleColumns = columns
.filter(column => !column.hidden)
.map(column => column.id);
const failCol = visibleColumns.filter(x => !desiredColumns.includes(x));
let failCol = visibleColumns.filter(x => !desiredColumns.includes(x));
if (failCol.length) {
throw new Error(
`Found unexpected visible columns: '${failCol}'!\ndesired list: ${desiredColumns}\nactual list: ${visibleColumns}`
);
}
failCol = desiredColumns.filter(x => !visibleColumns.includes(x));
if (failCol.length) {
throw new Error(
`Found unexpected hidden columns: '${failCol}'!\ndesired list: ${desiredColumns}\nactual list: ${visibleColumns}`
);
}
}
/**
@ -530,7 +539,7 @@ add_task(async function test_column_reordering_persists() {
close_tab(tabB);
});
async function invoke_column_picker_option(aActions) {
async function open_column_picker() {
const tabmail = document.getElementById("tabmail");
const about3Pane = tabmail.currentAbout3Pane;
@ -547,6 +556,12 @@ async function invoke_column_picker_option(aActions) {
);
EventUtils.synthesizeMouseAtCenter(colPicker, {}, about3Pane);
await shownPromise;
return colPickerPopup;
}
async function invoke_column_picker_option(aActions) {
const colPickerPopup = await open_column_picker();
await click_menus_in_sequence(colPickerPopup, aActions);
}
@ -569,6 +584,64 @@ add_task(async function test_reset_to_inbox() {
assert_visible_columns(INBOX_DEFAULTS);
});
/**
* Registers a custom column and verifies it is added to the thread pane.
*/
add_task(async function test_custom_columns() {
await enter_folder(inboxFolder);
assert_visible_columns(INBOX_DEFAULTS);
ThreadPaneColumns.addCustomColumn("testCol", {
name: "Test",
sortCallback(header) {
return header.subject.length;
},
textCallback(header) {
return header.subject.length;
},
});
await new Promise(setTimeout);
assert_visible_columns(INBOX_DEFAULTS);
let colPickerPopup = await open_column_picker();
let columnItem = colPickerPopup.querySelector(
`menuitem[type="checkbox"][value="testCol"]`
);
Assert.ok(columnItem, "Column item should exist");
Assert.ok(
!columnItem.hasAttribute("checked"),
"Column item should not be checked"
);
colPickerPopup.hidePopup();
await toggleColumn("testCol");
assert_visible_columns([...INBOX_DEFAULTS, "testCol"]);
colPickerPopup = await open_column_picker();
columnItem = colPickerPopup.querySelector(
`menuitem[type="checkbox"][value="testCol"]`
);
Assert.ok(columnItem, "Column item should exist");
Assert.equal(
columnItem.getAttribute("checked"),
"true",
"Column item should be checked"
);
colPickerPopup.hidePopup();
ThreadPaneColumns.removeCustomColumn("testCol");
assert_visible_columns(INBOX_DEFAULTS);
colPickerPopup = await open_column_picker();
columnItem = colPickerPopup.querySelector(
`menuitem[type="checkbox"][value="testCol"]`
);
Assert.ok(!columnItem, "Column item should not exist");
colPickerPopup.hidePopup();
});
async function _apply_to_folder_common(aChildrenToo, folder) {
let notificatonPromise;
if (aChildrenToo) {

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

@ -3,18 +3,21 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsITreeView.idl"
#include "nsISupports.idl"
interface nsIMsgDBHdr;
/* //TODO JavaDoc
When implementing a js custom column handler (of type nsITreeView) you must implement the following
functions:
When implementing a custom column handler, the following methods are
currently not supported:
1. isEditable
2. GetCellProperties
3. GetImageSrc
4. GetCellText
5. CycleCell
4. CycleCell
The following methods of the nsIMsgCustomColumnHandler must be
implemented:
5. GetCellText
6. GetSortStringForRow
7. GetSortLongForRow
8. isString
@ -26,14 +29,12 @@ interface nsIMsgDBHdr;
Implementers should consider that the value returned by GetSortStringForRow
will be displayed in the grouped header row, as well as be used as the
sort string.
If implementing a c++ custom column handler, you must define all
nsITreeView and nsIMsgCustomColumnHandler methods.
*/
[scriptable, uuid(00f75b13-3ac4-4a17-a8b9-c6e4dd1b3f32)]
interface nsIMsgCustomColumnHandler : nsITreeView
{
interface nsIMsgCustomColumnHandler : nsISupports {
AString getRowProperties(in nsIMsgDBHdr aHdr);
AString getCellText(in nsIMsgDBHdr aHdr);
AString getSortStringForRow(in nsIMsgDBHdr aHdr);
unsigned long getSortLongForRow(in nsIMsgDBHdr aHdr);
boolean isString();

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

@ -943,16 +943,6 @@ NS_IMETHODIMP
nsMsgDBView::IsEditable(int32_t row, nsTreeColumn* col, bool* _retval) {
NS_ENSURE_ARG_POINTER(col);
NS_ENSURE_ARG_POINTER(_retval);
// Attempt to retrieve a custom column handler. If it exists call it and
// return.
const nsAString& colID = col->GetId();
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
if (colHandler) {
colHandler->IsEditable(row, col, _retval);
return NS_OK;
}
*_retval = false;
return NS_OK;
}
@ -1161,7 +1151,7 @@ nsMsgDBView::GetRowProperties(int32_t index, nsAString& properties) {
// Give the custom column handlers a chance to style the row.
for (int i = 0; i < m_customColumnHandlers.Count(); i++) {
nsString extra;
m_customColumnHandlers[i]->GetRowProperties(index, extra);
m_customColumnHandlers[i]->GetRowProperties(msgHdr, extra);
if (!extra.IsEmpty()) {
properties.Append(' ');
properties.Append(extra);
@ -1254,11 +1244,7 @@ nsMsgDBView::GetCellProperties(int32_t aRow, nsTreeColumn* col,
}
const nsAString& colID = col->GetId();
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
if (colHandler != nullptr) {
colHandler->GetCellProperties(aRow, col, properties);
} else if (colID[0] == 'c') {
// Correspondent.
if (colID.First() == 'c' && colID.EqualsLiteral("correspondentCol")) {
if (IsOutgoingMsg(msgHdr))
properties.AssignLiteral("outgoing");
else
@ -1574,16 +1560,6 @@ nsresult nsMsgDBView::GetDBForViewIndex(nsMsgViewIndex index,
NS_IMETHODIMP
nsMsgDBView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
NS_ENSURE_ARG_POINTER(aCol);
// Attempt to retrieve a custom column handler. If it exists call it and
// return.
const nsAString& colID = aCol->GetId();
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
if (colHandler) {
colHandler->GetImageSrc(aRow, aCol, aValue);
return NS_OK;
}
return NS_OK;
}
@ -1839,16 +1815,6 @@ nsMsgDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
aValue.Truncate();
// Attempt to retrieve a custom column handler. If it exists call it and
// return.
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
if (colHandler) {
colHandler->GetCellText(aRow, aCol, aValue);
return NS_OK;
}
return CellTextForColumn(aRow, colID, aValue);
}
@ -1868,6 +1834,14 @@ nsMsgDBView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
return NS_MSG_INVALID_DBVIEW_INDEX;
}
// Attempt to retrieve a custom column handler. If it exists call it and
// return.
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(aColumnName);
if (colHandler) {
colHandler->GetCellText(msgHdr, aValue);
return NS_OK;
}
nsCOMPtr<nsIMsgThread> thread;
switch (aColumnName.First()) {
@ -2049,15 +2023,6 @@ nsMsgDBView::CycleCell(int32_t row, nsTreeColumn* col) {
const nsAString& colID = col->GetId();
// Attempt to retrieve a custom column handler. If it exists call it and
// return.
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
if (colHandler) {
colHandler->CycleCell(row, col);
return NS_OK;
}
// The cyclers below don't work for the grouped header dummy row, currently.
// A future implementation should consider both collapsed and expanded state.
if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort &&