Tag colors, synced settings, and (stopgap) silent DB upgrades

- New tag colors support, with the ability to assign colors to up to 6
  tags per library. Tags with colors assigned will show up at the top of
  the tag selector and can be added to (and removed from) selected items
  by pressing the 1-6 keys on the keyboard. The tags will show up as
  color swatches before an item's title in the items list.
- Synced settings, with Notifier triggers when they change and
  accessible via the API (currently restricted on the server to
  'tagColors', but available for other things upon request)
- Silent DB upgrades for backwards-compatible changes. We'll do
  something fancier with async DB queries in 4.0, but this will work for
  changes that can be made without breaking compatibility with older
  clients, like the creation of new tables. The 'userdata' value is
  capped at 76, while further increments go to 'userdata2'.

TODO:

- Try to avoid jitter when redrawing swatches
- Optimize tag color images for retina displays
- Redo attachment dots in flat style?
- Clear all colors from an item with 0 (as in Thunderbird), but I don't
  think we can do this without undo
This commit is contained in:
Dan Stillman 2013-03-03 03:38:02 -05:00
parent 01c7c7f9e1
commit d2f028d797
26 changed files with 1706 additions and 616 deletions

View file

@ -0,0 +1,136 @@
<?xml version="1.0"?>
<!--
An extension of the Mozilla colorpicker that allows for a custom set of colors
-->
<bindings id="colorpickerBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="custom-colorpicker" extends="chrome://global/content/bindings/colorpicker.xml#colorpicker">
<resources>
<stylesheet src="chrome://zotero/skin/bindings/customcolorpicker.css"/>
</resources>
<content>
<vbox anonid="tiles" flex="1" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<hbox>
<spacer class="colorpickertile" style="background-color: #000000" color="#000000"/>
</hbox>
</vbox>
</content>
<implementation implements="nsIDOMEventListener">
<constructor><![CDATA[
this.initialize();
this._colors = this.getAttribute('colors');
if (this._colors) {
this._cols = this.getAttribute('cols');
}
this.redraw();
]]></constructor>
<!-- Defaults from the Mozilla colorpicker -->
<field name="_defaultColors">
[
'L#FFFFFF','L#FFCCCC','L#FFCC99','L#FFFF99','L#FFFFCC','L#99FF99','L#99FFFF','L#CCFFFF','L#CCCCFF','L#FFCCFF',
'#CCCCCC','#FF6666','#FF9966','L#FFFF66','L#FFFF33','L#66FF99','L#33FFFF','L#66FFFF','#9999FF','#FF99FF',
'#C0C0C0','#FF0000','#FF9900','#FFCC66','L#FFFF00','L#33FF33','#66CCCC','#33CCFF','#6666CC','#CC66CC',
'#999999','#CC0000','#FF6600','#FFCC33','#FFCC00','#33CC00','#00CCCC','#3366FF','#6633FF','#CC33CC',
'#666666','#990000','#CC6600','#CC9933','#999900','#009900','#339999','#3333FF','#6600CC','#993399',
'#333333','#660000','#993300','#996633','#666600','#006600','#336666','#000099','#333399','#663366',
'#000000','#330000','#663300','#663333','#333300','#003300','#003333','#000066','#330099','#330033'
]
</field>
<field name="_defaultCols">10</field>
<property name="colors" onget="return this._colors ? this._colors : []">
<setter><![CDATA[
if (typeof val == 'string') {
val = val ? val.split(',') : null;
}
this._colors = val;
this.redraw();
]]></setter>
</property>
<property name="cols" onget="return this.getAttribute('cols')">
<setter><![CDATA[
this.setAttribute('cols', val);
this.redraw();
]]></setter>
</property>
<method name="redraw">
<body><![CDATA[
//Zotero.debug("Redrawing color picker");
var tiles = document.getAnonymousNodes(this)[0];
var cols = this.getAttribute('cols') || this._defaultCols;
var colors = this._colors.concat() || this._defaultColors.concat();
while (tiles.hasChildNodes()) {
tiles.removeChild(tiles.firstChild);
}
var rows = Math.ceil(colors.length / cols);
var tileWidth = this.getAttribute('tileWidth');
var tileHeight = this.getAttribute('tileHeight');
for (let i=0; i<rows; i++) {
var hbox = document.createElement('hbox');
for (let j=0; j<cols; j++) {
let color = colors.shift();
if (!color) {
break;
}
let light = color.charAt(0) == 'L';
color = light ? color.substr(1) : color;
var spacer = document.createElement('spacer');
spacer.className = 'colorpickertile' + (light ? ' cp-light' : '');
spacer.setAttribute('style', 'background-color: ' + color);
spacer.setAttribute('color', color);
if (tileWidth) {
spacer.width = tileWidth;
}
if (tileHeight) {
spacer.height = tileHeight;
}
hbox.appendChild(spacer);
}
tiles.appendChild(hbox);
}
]]></body>
</method>
</implementation>
</binding>
<!-- The content of the Mozilla colorpicker-button, but with a customcolorpicker
with some extra inherited attributes instead -->
<binding id="custom-colorpicker-button" display="xul:menu"
extends="chrome://global/content/bindings/colorpicker.xml#colorpicker-button">
<resources>
<stylesheet src="chrome://zotero/skin/bindings/customcolorpicker.css"/>
</resources>
<content>
<xul:hbox class="colorpicker-button-colorbox" anonid="colorbox" flex="1" xbl:inherits="disabled"/>
<xul:panel class="colorpicker-button-menupopup"
anonid="colorpopup" noautofocus="true" level="top"
onmousedown="event.stopPropagation()"
onpopupshowing="this._colorPicker.onPopupShowing()"
onpopuphiding="this._colorPicker.onPopupHiding()"
onselect="this._colorPicker.pickerChange()">
<xul:customcolorpicker xbl:inherits="palettename,disabled,cols,columns,tileWidth,tileHeight" allowevents="true" anonid="colorpicker"/>
</xul:panel>
</content>
<implementation>
<property name="colors" onget="return this.mPicker.colors" onset="this.mPicker.colors = val"/>
</implementation>
</binding>
</bindings>

View file

@ -37,6 +37,7 @@
<implementation>
<field name="clickHandler"/>
<field name="_tagColors"/>
<field name="_notifierID"/>
<!-- Modes are predefined settings groups for particular tasks -->
<field name="_mode">"view"</field>
@ -109,38 +110,64 @@
if (this.hasAttribute('mode')) {
this.mode = this.getAttribute('mode');
}
this._notifierID = Zotero.Notifier.registerObserver(this, ['setting']);
]]>
</constructor>
<destructor>
<![CDATA[
Zotero.Notifier.unregisterObserver(this._notifierID);
]]>
</destructor>
<method name="notify">
<parameter name="event"/>
<parameter name="type"/>
<parameter name="ids"/>
<body>
<![CDATA[
if (type == 'setting') {
if (ids.some(function (val) val.split("/")[1] == 'tagColors') && this.item) {
this.reload();
}
return;
}
]]>
</body>
</method>
<method name="reload">
<body>
<![CDATA[
var self = this;
Zotero.debug('Reloading tags');
var addButton = self.id('addButton');
addButton.hidden = !self.editable;
var addButton = this.id('addButton');
addButton.hidden = !this.editable;
var rows = this.id('tagRows');
while(rows.hasChildNodes())
rows.removeChild(rows.firstChild);
var tags = this.item.getTags();
this._tagColors = Zotero.Tags.getColors();
if(tags)
{
for (var i=0; i<tags.length; i++) {
this.addDynamicRow(tags[i], i+1);
return Zotero.Tags.getColors(self.item.libraryIDInt)
.then(function (colors) {
self._tagColors = colors;
var rows = self.id('tagRows');
while(rows.hasChildNodes()) {
rows.removeChild(rows.firstChild);
}
var tags = self.item.getTags();
if (tags) {
for (var i=0; i<tags.length; i++) {
self.addDynamicRow(tags[i], i+1);
}
//self.fixPopup();
}
this.updateCount(tags.length);
//this.fixPopup();
return tags.length;
}
this.updateCount(0);
return 0;
self.updateCount(0);
})
.done();
]]>
</body>
</method>
@ -264,8 +291,13 @@
}
// Tag color
if (color = this._tagColors[valueText]) {
valueElement.setAttribute('style', 'color:' + this._tagColors[valueText]);
let color = this._tagColors[valueText];
if (color) {
valueElement.setAttribute(
'style',
'color:' + this._tagColors[valueText].color + '; '
+ 'font-weight: bold'
);
}
return valueElement;

View file

@ -27,8 +27,7 @@
<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd">
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="tag-selector">
<resources>
@ -77,6 +76,9 @@
<property name="libraryID" onget="return this._libraryID">
<setter>
<![CDATA[
// TEMP: libraryIDInt
val = val ? parseInt(val) : 0;
if (this._libraryID != val) {
this._dirty = true;
}
@ -169,7 +171,7 @@
<![CDATA[
this._initialized = true;
this.selection = {};
this._notifierID = Zotero.Notifier.registerObserver(this, ['collection-item', 'item-tag', 'tag']);
this._notifierID = Zotero.Notifier.registerObserver(this, ['collection-item', 'item-tag', 'tag', 'setting']);
]]>
</body>
</method>
@ -215,211 +217,211 @@
var empty = true;
var tagsToggleBox = this.id('tags-toggle');
if (fetch || this._dirty) {
this._tags = Zotero.Tags.getAll(this._types, this.libraryID);
// Remove children
tagsToggleBox.textContent = "";
var me = this,
onTagClick = function(event) { me.handleTagClick(event, this) },
lastTag;
for (var tagID in this._tags) {
var tagInfo = this._tags[tagID],
tagName = tagInfo.name;
// If the last tag was the same, add this tagID and tagType to it
if(lastTag && lastTag.value === tagName) {
lastTag.setAttribute('tagID', lastTag.getAttribute('tagID') + '-' + tagID);
lastTag.setAttribute('tagType', lastTag.getAttribute('tagType') + '-' + tagName.type);
continue;
}
var self = this;
Zotero.Tags.getColors(this.libraryID)
.then(function (tagColors) {
if (fetch || self._dirty) {
self._tags = Zotero.Tags.getAll(self._types, self.libraryID);
lastTag = document.createElement('label');
lastTag.addEventListener('click', onTagClick, false);
lastTag.className = 'zotero-clicky';
// Remove children
tagsToggleBox.textContent = "";
lastTag.setAttribute('value', tagName);
lastTag.setAttribute('tagID', tagID);
lastTag.setAttribute('tagType', tagInfo.type);
if (this.editable) {
lastTag.setAttribute('context', 'tag-menu');
lastTag.addEventListener('dragover', this.dragObserver.onDragOver, false);
lastTag.addEventListener('dragexit', this.dragObserver.onDragExit, false);
lastTag.addEventListener('drop', this.dragObserver.onDrop, true);
}
tagsToggleBox.appendChild(lastTag);
}
this._dirty = false;
}
// Set attributes
var labels = tagsToggleBox.getElementsByTagName('label');
var tagColors = Zotero.Tags.getColors();
for (var i=0; i<labels.length; i++){
var tagIDs = labels[i].getAttribute('tagID').split('-');
// Restore selection
if (this.selection[labels[i].value]){
labels[i].setAttribute('selected', 'true');
}
else {
labels[i].setAttribute('selected', 'false');
}
// Check tags against filter
if (this._hasFilter) {
var inFilter = false;
for each(var tagID in tagIDs) {
if (this._filter[tagID]) {
inFilter = true;
break;
var lastTag;
for (let tagID in self._tags) {
let tagButton = self._makeClickableTag(tagID, lastTag, self.editable);
if (tagButton) {
tagButton.addEventListener('click', function(event) {
self.handleTagClick(event, this);
});
if (self.editable) {
tagButton.addEventListener('dragover', self.dragObserver.onDragOver);
tagButton.addEventListener('dragexit', self.dragObserver.onDragExit);
tagButton.addEventListener('drop', self.dragObserver.onDrop, true);
}
lastTag = tagButton;
tagsToggleBox.appendChild(tagButton);
}
}
self._dirty = false;
}
// Check tags against scope
if (this._hasScope) {
var inScope = false;
for each(var tagID in tagIDs) {
if (this._scope[tagID]) {
inScope = true;
break;
// Set attributes
var colorTags = {};
var labels = tagsToggleBox.getElementsByTagName('label');
for (let i=0; i<labels.length; i++) {
var tagIDs = labels[i].getAttribute('tagID').split('-');
// Restore selection
if (self.selection[labels[i].value]){
labels[i].setAttribute('selected', 'true');
}
else {
labels[i].setAttribute('selected', 'false');
}
// Check tags against filter
if (self._hasFilter) {
var inFilter = false;
for each(var tagID in tagIDs) {
if (self._filter[tagID]) {
inFilter = true;
break;
}
}
}
}
// If not in filter, hide
if (this._hasFilter && !inFilter) {
//Zotero.debug(1);
labels[i].setAttribute('hidden', true);
}
else if (this.filterToScope) {
if (this._hasScope && inScope) {
//Zotero.debug(2);
labels[i].setAttribute('inScope', true);
// Check tags against scope
if (self._hasScope) {
var inScope = false;
for each(var tagID in tagIDs) {
if (self._scope[tagID]) {
inScope = true;
break;
}
}
}
// If not in filter, hide
if (self._hasFilter && !inFilter) {
labels[i].setAttribute('hidden', true);
}
else if (self.filterToScope) {
if (self._hasScope && inScope) {
labels[i].setAttribute('inScope', true);
labels[i].setAttribute('hidden', false);
empty = false;
}
else {
labels[i].setAttribute('hidden', true);
labels[i].setAttribute('inScope', false);
}
}
// Display all
else {
if (self._hasScope && inScope) {
labels[i].setAttribute('inScope', true);
}
else {
labels[i].setAttribute('inScope', false);
// If out of scope, make sure it's not selected (otherwise a tag
// stays selected after removing an item with that tag from the
// current collection)
if (self.selection[labels[i].value]) {
labels[i].setAttribute('selected', false);
delete self.selection[labels[i].value];
var doCommand = true;
}
}
labels[i].setAttribute('hidden', false);
empty = false;
}
let colorData = tagColors[labels[i].value];
if (colorData) {
labels[i].setAttribute(
'style', 'color:' + colorData.color + '; ' + 'font-weight: bold'
);
colorTags[colorData.position] = tagsToggleBox.removeChild(labels[i]);
// The HTMLCollection returned by getElementsByTagName() is live,
// so since we removed something we need to decrement the counter
i--;
}
else {
//Zotero.debug(3);
labels[i].setAttribute('hidden', true);
labels[i].setAttribute('inScope', false);
labels[i].removeAttribute('style');
}
}
// Display all
else {
if (this._hasScope && inScope) {
//Zotero.debug(4);
labels[i].setAttribute('inScope', true);
// Add color tags to beginning in order
var positions = Object.keys(colorTags);
positions.sort();
for (var i=positions.length-1; i>=0; i--) {
tagsToggleBox.insertBefore(colorTags[positions[i]], tagsToggleBox.firstChild);
}
//start tag cloud code
var tagCloud = Zotero.Prefs.get('tagCloud');
if(tagCloud) {
var labels = tagsToggleBox.getElementsByTagName('label');
//loop through displayed labels and find number of linked items
var numlinked= [];
for (var i=0; i<labels.length; i++){
if(labels[i].getAttribute("hidden") != 'true') {
var tagIDs = labels[i].getAttribute('tagID').split('-');
//replace getLinkedItems() with function that gets linked items within the current collection
var linked = self._tags[tagIDs[0]].getLinkedItems();
numlinked.push(parseInt(linked.length));
}
}
else {
//Zotero.debug(5);
labels[i].setAttribute('inScope', false);
// If out of scope, make sure it's not selected (otherwise a tag
// stays selected after removing an item with that tag from the
// current collection)
if (this.selection[labels[i].value]) {
labels[i].setAttribute('selected', false);
delete this.selection[labels[i].value];
var doCommand = true;
}
//
numlinked.sort();
//Get number of attached items from tag with fewest items
var min = numlinked[0];
//Get number of attached items from tag with most items
var max = numlinked.pop();
numlinked.push(max);
//Create array of possible tag text sizes
var sizes = ["11", "12", "13", "14", "15", "16", "17", "18", "19","20","21","22"];
//Number of possible tag sizes
var categories = sizes.length;
//inc is the size of each size category of tags, in terms of the number of attached items
var inc = Math.ceil((max-min)/categories);
if(inc<1) {
inc = 1;
}
labels[i].setAttribute('hidden', false);
empty = false;
}
if (color = tagColors[labels[i].value]) {
labels[i].setAttribute('style', 'color:' + color);
}
else {
labels[i].removeAttribute('style');
}
}
//start tag cloud code
var tagCloud = Zotero.Prefs.get('tagCloud');
if(tagCloud) {
var labels = tagsToggleBox.getElementsByTagName('label');
//loop through displayed labels and find number of linked items
var numlinked= [];
for (var i=0; i<labels.length; i++){
if(labels[i].getAttribute("hidden") != 'true') {
var tagIDs = labels[i].getAttribute('tagID').split('-');
//replace getLinkedItems() with function that gets linked items within the current collection
var linked = this._tags[tagIDs[0]].getLinkedItems();
numlinked.push(parseInt(linked.length));
}
}
//
numlinked.sort();
//Get number of attached items from tag with fewest items
var min = numlinked[0];
//Get number of attached items from tag with most items
var max = numlinked.pop();
numlinked.push(max);
//Create array of possible tag text sizes
var sizes = ["11", "12", "13", "14", "15", "16", "17", "18", "19","20","21","22"];
//Number of possible tag sizes
var categories = sizes.length;
//inc is the size of each size category of tags, in terms of the number of attached items
var inc = Math.ceil((max-min)/categories);
if(inc<1) {
inc = 1;
}
for (var i=0; i<labels.length; i++){
if(labels[i].getAttribute("hidden") != 'true') {
var tagIDs = labels[i].getAttribute('tagID').split('-');
//replace getLinkedItems() with function that gets linked items within the current collection
var linked = this._tags[tagIDs[0]].getLinkedItems();
numlink = linked.length;
//range is the difference between how many items this tag has and how many items the smallest tag has
var range=(numlink-min);
//Divide the range by the size of the categories
s=range/inc;
if(s==categories) {
s=categories-1;
for (var i=0; i<labels.length; i++){
if(labels[i].getAttribute("hidden") != 'true') {
var tagIDs = labels[i].getAttribute('tagID').split('-');
//replace getLinkedItems() with function that gets linked items within the current collection
var linked = self._tags[tagIDs[0]].getLinkedItems();
numlink = linked.length;
//range is the difference between how many items this tag has and how many items the smallest tag has
var range=(numlink-min);
//Divide the range by the size of the categories
s=range/inc;
if(s==categories) {
s=categories-1;
}
var stylestr = 'font-size:'+sizes[s]+'px;';
labels[i].setAttribute('style',stylestr);
}
var stylestr = 'font-size:'+sizes[s]+'px;';
labels[i].setAttribute('style',stylestr);
}
}
}
}
//end tag cloud code
this.updateNumSelected();
this._empty = empty;
this.id('tags-toggle').setAttribute('collapsed', empty);
this.id('no-tags-box').setAttribute('collapsed', !empty);
if (doCommand) {
Zotero.debug('A selected tag went out of scope -- deselecting');
this.doCommand();
}
//end tag cloud code
self.updateNumSelected();
self._empty = empty;
self.id('tags-toggle').setAttribute('collapsed', empty);
self.id('no-tags-box').setAttribute('collapsed', !empty);
if (doCommand) {
Zotero.debug('A selected tag went out of scope -- deselecting');
self.doCommand();
}
})
.done();
]]>
</body>
</method>
@ -437,6 +439,7 @@
</body>
</method>
<method name="updateNumSelected">
<body>
<![CDATA[
@ -466,6 +469,13 @@
<parameter name="ids"/>
<body>
<![CDATA[
if (type == 'setting') {
if (ids.some(function (val) val.split("/")[1] == 'tagColors')) {
this.refresh(true);
}
return;
}
var itemGroup = ZoteroPane_Local.getItemGroup();
// Ignore anything other than deletes in duplicates view
@ -663,16 +673,20 @@
delete this.selection[oldName];
}
// TODO: redo transaction for async DB
var promises = [];
Zotero.DB.beginTransaction();
for (var i=0; i<tagIDs.length; i++) {
Zotero.Tags.rename(tagIDs[i], newName.value);
promises.push(Zotero.Tags.rename(tagIDs[i], newName.value));
}
if (wasSelected) {
this.selection[newName.value] = true;
}
Zotero.DB.commitTransaction();
Q.all(promises)
.done();
}
]]>
</body>
@ -719,43 +733,84 @@
<![CDATA[
tagIDs = tagIDs.split('-');
var name = Zotero.Tags.getName(tagIDs[0]);
return Zotero.Tags.getColor(name);
return Zotero.Tags.getColor(this.libraryID, name)
.then(function (colorData) {
return colorData ? colorData.color : '#000000';
});
]]>
</body>
</method>
<method name="setColor">
<method name="_makeClickableTag">
<parameter name="tagID"/>
<parameter name="lastTag"/>
<parameter name="editable"/>
<body>
<![CDATA[
var tagInfo = this._tags[tagID], tagName = tagInfo.name;
// If the last tag was the same, add this tagID and tagType to it
if(lastTag && lastTag.value === tagName) {
lastTag.setAttribute('tagID', lastTag.getAttribute('tagID') + '-' + tagID);
lastTag.setAttribute('tagType', lastTag.getAttribute('tagType') + '-' + tagInfo.type);
return false;
}
var label = document.createElement('label');
label.className = 'zotero-clicky';
label.setAttribute('value', tagName);
label.setAttribute('tagID', tagID);
label.setAttribute('tagType', tagInfo.type);
if (editable) {
label.setAttribute('context', 'tag-menu');
}
return label;
]]>
</body>
</method>
<method name="_openColorPickerWindow">
<parameter name="tagIDs"/>
<parameter name="color"/>
<body>
<![CDATA[
tagIDs = tagIDs.split('-');
var name = Zotero.Tags.getName(tagIDs[0]);
Zotero.Tags.setColor(name, color);
// Iterate through all windows, updating tag selector and tags box
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
var win = enumerator.getNext();
if (!win.ZoteroPane) continue;
var tagSelector = win.ZoteroPane.document.getElementById('zotero-tag-selector');
var itemPane = win.ZoteroPane.document.getElementById('zotero-view-item');
tagSelector.refresh();
if (itemPane.selectedPanel.firstChild.getAttribute('id') == 'zotero-editpane-tags') {
var tagsBox = win.ZoteroPane.document.getElementById('zotero-editpane-tags');
tagsBox.reload();
var io = {
libraryID: this.libraryID,
name: Zotero.Tags.getName(tagIDs[0])
};
var self = this;
Zotero.Tags.getColors(this.libraryID)
.then(function (tagColors) {
if (Object.keys(tagColors).length >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors[io.name]) {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
ps.alert(null, "", Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS));
return;
}
}
window.openDialog(
'chrome://zotero/content/tagColorChooser.xul',
"zotero-tagSelector-colorChooser",
"chrome,modal,centerscreen", io
);
// Dialog cancel
if (typeof io.color == 'undefined') {
return;
}
return Zotero.Tags.setColor(self.libraryID, io.name, io.color, io.position);
})
.done();
]]>
</body>
</method>
<method name="focusTextbox">
<body>
<![CDATA[
@ -854,48 +909,41 @@
</implementation>
<content>
<xul:groupbox flex="1">
<xul:menupopup id="tag-menu">
<xul:menuitem label="&zotero.tagSelector.renameTag;" oncommand="document.getBindingParent(this).rename(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
<xul:menuitem label="&zotero.tagSelector.deleteTag;" oncommand="document.getBindingParent(this).delete(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
<!-- TODO: localized -->
<!--
<xul:menu label="Assign Color">
<xul:menupopup onpopupshowing="var color = document.getBindingParent(this).getColor(document.popupNode.getAttribute('tagID')); this.children[0].color = color">
<xul:colorpicker onclick="document.getBindingParent(this).setColor(document.popupNode.getAttribute('tagID'), event.originalTarget.getAttribute('color')); document.getBindingParent(this).id('tag-menu').hidePopup(); event.stopPropagation()"/>
</xul:menupopup>
</xul:menu>
-->
</xul:menupopup>
<groupbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" flex="1">
<menupopup id="tag-menu">
<menuitem label="&zotero.tagSelector.assignColor;" oncommand="_openColorPickerWindow(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
<menuitem label="&zotero.tagSelector.renameTag;" oncommand="document.getBindingParent(this).rename(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
<menuitem label="&zotero.tagSelector.deleteTag;" oncommand="document.getBindingParent(this).delete(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
</menupopup>
<xul:vbox id="no-tags-box" align="center" pack="center" flex="1">
<xul:label value="&zotero.tagSelector.noTagsToDisplay;"/>
</xul:vbox>
<vbox id="no-tags-box" align="center" pack="center" flex="1">
<label value="&zotero.tagSelector.noTagsToDisplay;"/>
</vbox>
<xul:vbox id="tags-toggle" flex="1"/>
<vbox id="tags-toggle" flex="1"/>
<xul:vbox id="tag-controls">
<xul:hbox>
<vbox id="tag-controls">
<hbox>
<!-- TODO: &zotero.tagSelector.filter; is now unused -->
<xul:textbox id="tags-search" flex="1" type="search" timeout="250" dir="reverse"
<textbox id="tags-search" flex="1" type="search" timeout="250" dir="reverse"
oncommand="document.getBindingParent(this).handleKeyPress(); event.stopPropagation()"
onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) { document.getBindingParent(this).handleKeyPress(true); }"/>
<xul:toolbarbutton id="view-settings-menu" tooltiptext="&zotero.toolbar.actions.label;"
<toolbarbutton id="view-settings-menu" tooltiptext="&zotero.toolbar.actions.label;"
image="chrome://zotero/skin/tag-selector-menu.png" type="menu">
<xul:menupopup id="view-settings-popup">
<xul:menuitem id="num-selected" disabled="true"/>
<xul:menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;"
<menupopup id="view-settings-popup">
<menuitem id="num-selected" disabled="true"/>
<menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;"
oncommand="document.getBindingParent(this).clearAll(); event.stopPropagation();"/>
<xul:menuseparator/>
<xul:menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" autocheck="true" type="checkbox"
<menuseparator/>
<menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" autocheck="true" type="checkbox"
oncommand="var ts = document.getBindingParent(this); ts._dirty = true; ts.setAttribute('showAutomatic', this.getAttribute('checked') == 'true')"/>
<xul:menuitem id="display-all-tags" label="&zotero.tagSelector.displayAllInLibrary;" autocheck="true" type="checkbox"
<menuitem id="display-all-tags" label="&zotero.tagSelector.displayAllInLibrary;" autocheck="true" type="checkbox"
oncommand="document.getBindingParent(this).filterToScope = !(this.getAttribute('checked') == 'true'); event.stopPropagation();"/>
</xul:menupopup>
</xul:toolbarbutton>
</xul:hbox>
</xul:vbox>
</xul:groupbox>
</menupopup>
</toolbarbutton>
</hbox>
</vbox>
</groupbox>
</content>
</binding>
</bindings>

View file

@ -0,0 +1,154 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2013 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
var _io;
var Zotero_Tag_Color_Chooser = new function() {
this.init = function () {
// Set font size from pref
Zotero.setFontSize(document.getElementById("tag-color-chooser-container"));
if (window.arguments && window.arguments.length) {
_io = window.arguments[0];
if (_io.wrappedJSObject) _io = _io.wrappedJSObject;
}
if (typeof _io.libraryID == 'undefined') throw new Error("libraryID not set");
if (typeof _io.name == 'undefined' || _io.name === "") throw new Error("name not set");
window.sizeToContent();
var dialog = document.getElementById('tag-color-chooser');
var colorPicker = document.getElementById('color-picker');
var tagPosition = document.getElementById('tag-position');
colorPicker.setAttribute('cols', 3);
colorPicker.setAttribute('tileWidth', 24);
colorPicker.setAttribute('tileHeight', 24);
colorPicker.colors = [
'#990000', '#CC9933', '#FF9900',
'#FFCC00', '#007439', '#1049A9',
'#9999FF', '#CC66CC', '#993399'
];
var maxTags = document.getElementById('max-tags');
maxTags.value = Zotero.getString('tagColorChooser.maxTags', Zotero.Tags.MAX_COLORED_TAGS);
var self = this;
Zotero.Tags.getColors(_io.libraryID)
.then(function (tagColors) {
var colorData = tagColors[_io.name];
// Color
if (colorData) {
colorPicker.color = colorData.color;
dialog.buttons = "extra1,cancel,accept";
}
else {
// Get unused color at random
var usedColors = [];
for (var i in tagColors) {
usedColors.push(tagColors[i].color);
}
var unusedColors = Zotero.Utilities.arrayDiff(
colorPicker.colors, usedColors
);
var color = unusedColors[Zotero.Utilities.rand(0, unusedColors.length - 1)];
colorPicker.color = color;
dialog.buttons = "cancel,accept";
}
colorPicker.setAttribute('disabled', 'false');
var numColors = Object.keys(tagColors).length;
var max = colorData ? numColors : numColors + 1;
// Position
for (let i=1; i<=max; i++) {
tagPosition.appendItem(i, i-1);
}
if (numColors) {
tagPosition.setAttribute('disabled', 'false');
if (colorData) {
tagPosition.selectedIndex = colorData.position;
}
// If no color currently, default to end
else {
tagPosition.selectedIndex = numColors;
}
}
// If no colors currently, only position "1" is available
else {
tagPosition.selectedIndex = 0;
}
self.onPositionChange();
window.sizeToContent();
})
.catch(function (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
dialog.cancelDialog();
})
.done();
};
this.onPositionChange = function () {
var tagPosition = document.getElementById('tag-position');
var instructions = document.getElementById('number-key-instructions');
while (instructions.hasChildNodes()) {
instructions.removeChild(instructions.firstChild);
}
var msg = Zotero.getString('tagColorChooser.numberKeyInstructions');
var matches = msg.match(/(.+)\$NUMBER(.+)/);
var num = document.createElement('label');
num.id = 'number-key';
num.setAttribute('value', parseInt(tagPosition.value) + 1);
instructions.appendChild(document.createTextNode(matches[1]));
instructions.appendChild(num);
instructions.appendChild(document.createTextNode(matches[2]));
};
this.onDialogAccept = function () {
var colorPicker = document.getElementById('color-picker');
var tagPosition = document.getElementById('tag-position');
_io.color = colorPicker.color;
_io.position = tagPosition.value;
};
this.onDialogCancel = function () {};
this.onDialogRemoveColor = function () {
_io.color = false;
window.close();
};
};

View file

@ -0,0 +1,63 @@
<?xml version="1.0"?>
<!--
***** BEGIN LICENSE BLOCK *****
Copyright © 2013 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/tagColorChooser.css" type="text/css"?>
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&zotero.tagColorChooser.title;" buttons="cancel,accept"
id="tag-color-chooser"
buttonlabelaccept="&zotero.tagColorChooser.setColor;"
buttonlabelextra1="&zotero.tagColorChooser.removeColor;"
ondialogaccept="Zotero_Tag_Color_Chooser.onDialogAccept()"
ondialogcancel="Zotero_Tag_Color_Chooser.onDialogCancel()"
ondialogextra1="Zotero_Tag_Color_Chooser.onDialogRemoveColor()"
onload="Zotero_Tag_Color_Chooser.init()"
width="300"
height="140">
<script src="include.js"/>
<script src="tagColorChooser.js" type="text/javascript;version=1.8"/>
<vbox id="tag-color-chooser-container">
<hbox align="center">
<label value="&zotero.tagColorChooser.color;" control="color-picker"/>
<customcolorpicker id="color-picker" type="button" disabled="true"/>
<separator width="20"/>
<label value="&zotero.tagColorChooser.position;" control="tag-position" disabled="true"/>
<menulist id="tag-position" disabled="true" sizetopopup="always"
oncommand="Zotero_Tag_Color_Chooser.onPositionChange()">
<menupopup/>
</menulist>
</hbox>
<separator/>
<description id="number-key-instructions"/>
<separator class="thin"/>
<description id="max-tags"/>
<separator class="thin"/>
</vbox>
</dialog>

View file

@ -355,6 +355,10 @@ Zotero.Group.prototype.erase = function() {
var prefix = "groups/" + this.id;
Zotero.Relations.eraseByURIPrefix(Zotero.URI.defaultPrefix + prefix);
// Delete settings
sql = "DELETE FROM syncedSettings WHERE libraryID=?";
Zotero.DB.query(sql, this.libraryID ? parseInt(this.libraryID) : 0);
// Delete group
sql = "DELETE FROM groups WHERE groupID=?";
Zotero.DB.query(sql, this.id)

View file

@ -113,6 +113,8 @@ Zotero.Item.prototype.__defineGetter__('itemID', function () {
});
Zotero.Item.prototype.__defineSetter__('id', function (val) { this.setField('id', val); });
Zotero.Item.prototype.__defineGetter__('libraryID', function () { return this.getField('libraryID'); });
// Temporary until everything expects an integer
Zotero.Item.prototype.__defineGetter__('libraryIDInt', function () { var libraryID = this.getField('libraryID'); return libraryID ? parseInt(libraryID) : 0; });
Zotero.Item.prototype.__defineSetter__('libraryID', function (val) { this.setField('libraryID', val); });
Zotero.Item.prototype.__defineGetter__('key', function () { return this.getField('key'); });
Zotero.Item.prototype.__defineSetter__('key', function (val) { this.setField('key', val) });

View file

@ -453,7 +453,7 @@ Zotero.Tag.prototype.save = function (full) {
pairs.push(itemID + "-" + tagID);
}
var tempRemoved = removed;
let tempRemoved = removed.concat();
do {
var chunk = tempRemoved.splice(0, maxItems);

View file

@ -31,8 +31,14 @@ Zotero.Tags = new function() {
Zotero.DataObjects.apply(this, ['tag']);
this.constructor.prototype = new Zotero.DataObjects();
this.MAX_COLORED_TAGS = 6;
var _tags = {}; // indexed by tag text
var _colorsByItem = {};
var _libraryColors = [];
var _libraryColorsByName = {};
var _itemsListImagePromises = {};
var _itemsListExtraImagePromises = {};
this.get = get;
this.getName = getName;
@ -258,7 +264,7 @@ Zotero.Tags = new function() {
*/
function getTagItems(tagID) {
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
return Zotero.DB.columnQuery(sql, tagID);
return Zotero.DB.columnQuery(sql, tagID) || [];
}
@ -287,179 +293,545 @@ Zotero.Tags = new function() {
}
function rename(tagID, name) {
Zotero.debug('Renaming tag', 4);
name = Zotero.Utilities.trim(name);
Zotero.DB.beginTransaction();
var tagObj = this.get(tagID);
var libraryID = tagObj.libraryID;
var oldName = tagObj.name;
var oldType = tagObj.type;
var notifierData = {};
notifierData[tagID] = { old: tagObj.serialize() };
if (oldName == name) {
Zotero.debug("Tag name hasn't changed", 2);
Zotero.DB.commitTransaction();
return;
}
var sql = "SELECT tagID, name FROM tags WHERE name=? AND type=0 AND libraryID=?";
var row = Zotero.DB.rowQuery(sql, [name, libraryID]);
if (row) {
var existingTagID = row.tagID;
var existingName = row.name;
}
// New tag already exists as manual tag
if (existingTagID
// Tag check is case-insensitive, so make sure we have a different tag
&& existingTagID != tagID) {
/**
* Rename a tag and update the tag colors setting accordingly if necessary
*
* @return {Promise}
*/
function rename(tagID, newName) {
var tagObj, libraryID, oldName, oldType, notifierData, self = this;
return Q.fcall(function () {
Zotero.debug('Renaming tag', 4);
var changed = false;
var itemsAdded = false;
newName = newName.trim();
// Change case of existing manual tag before switching automatic
if (oldName.toLowerCase() == name.toLowerCase() || existingName != name) {
var sql = "UPDATE tags SET name=? WHERE tagID=?";
Zotero.DB.query(sql, [name, existingTagID]);
changed = true;
tagObj = self.get(tagID);
libraryID = tagObj.libraryID;
oldName = tagObj.name;
oldType = tagObj.type;
notifierData = {};
notifierData[tagID] = { old: tagObj.serialize() };
if (oldName == newName) {
Zotero.debug("Tag name hasn't changed", 2);
return;
}
var itemIDs = this.getTagItems(tagID);
var existingItemIDs = this.getTagItems(existingTagID);
// We need to know if the old tag has a color assigned so that
// we can assign it to the new name
return self.getColor(libraryID ? parseInt(libraryID) : 0, oldName);
})
.then(function (oldColorData) {
Zotero.DB.beginTransaction();
// Would be easier to just call removeTag(tagID) and addTag(existingID)
// here, but this is considerably more efficient
var sql = "UPDATE OR REPLACE itemTags SET tagID=? WHERE tagID=?";
Zotero.DB.query(sql, [existingTagID, tagID]);
// Manual purge of old tag
sql = "DELETE FROM tags WHERE tagID=?";
Zotero.DB.query(sql, tagID);
if (_tags[libraryID] && _tags[libraryID][oldType]) {
delete _tags[libraryID][oldType]['_' + oldName];
}
delete this._objectCache[tagID];
Zotero.Notifier.trigger('delete', 'tag', tagID, notifierData);
// Simulate tag removal on items that used old tag
var itemTags = [];
for (var i in itemIDs) {
itemTags.push(itemIDs[i] + '-' + tagID);
}
Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
// And send tag add for new tag (except for those that already had it)
var itemTags = [];
for (var i in itemIDs) {
if (!existingItemIDs || existingItemIDs.indexOf(itemIDs[i]) == -1) {
itemTags.push(itemIDs[i] + '-' + existingTagID);
itemsAdded = true;
}
var sql = "SELECT tagID, name FROM tags WHERE name=? AND type=0 AND libraryID=?";
var row = Zotero.DB.rowQuery(sql, [newName, libraryID]);
if (row) {
var existingTagID = row.tagID;
var existingName = row.name;
}
if (changed) {
if (itemsAdded) {
Zotero.Notifier.trigger('add', 'item-tag', itemTags);
// New tag already exists as manual tag
if (existingTagID
// Tag check is case-insensitive, so make sure we have a different tag
&& existingTagID != tagID) {
var changed = false;
var itemsAdded = false;
// Change case of existing manual tag before switching automatic
if (oldName.toLowerCase() == newName.toLowerCase() || existingName != newName) {
var sql = "UPDATE tags SET name=? WHERE tagID=?";
Zotero.DB.query(sql, [newName, existingTagID]);
changed = true;
}
// Mark existing tag as updated
sql = "UPDATE tags SET dateModified=CURRENT_TIMESTAMP, "
+ "clientDateModified=CURRENT_TIMESTAMP WHERE tagID=?";
Zotero.DB.query(sql, existingTagID);
Zotero.Notifier.trigger('modify', 'tag', existingTagID);
Zotero.Tags.reload(existingTagID);
var itemIDs = self.getTagItems(tagID);
var existingItemIDs = self.getTagItems(existingTagID);
// Would be easier to just call removeTag(tagID) and addTag(existingID)
// here, but this is considerably more efficient
var sql = "UPDATE OR REPLACE itemTags SET tagID=? WHERE tagID=?";
Zotero.DB.query(sql, [existingTagID, tagID]);
// Manual purge of old tag
sql = "DELETE FROM tags WHERE tagID=?";
Zotero.DB.query(sql, tagID);
if (_tags[libraryID] && _tags[libraryID][oldType]) {
delete _tags[libraryID][oldType]['_' + oldName];
}
delete self._objectCache[tagID];
Zotero.Notifier.trigger('delete', 'tag', tagID, notifierData);
// Simulate tag removal on items that used old tag
var itemTags = [];
for (var i in itemIDs) {
itemTags.push(itemIDs[i] + '-' + tagID);
}
Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
// And send tag add for new tag (except for those that already had it)
var itemTags = [];
for (var i in itemIDs) {
if (existingItemIDs.indexOf(itemIDs[i]) == -1) {
itemTags.push(itemIDs[i] + '-' + existingTagID);
itemsAdded = true;
}
}
if (changed) {
if (itemsAdded) {
Zotero.Notifier.trigger('add', 'item-tag', itemTags);
}
// Mark existing tag as updated
sql = "UPDATE tags SET dateModified=CURRENT_TIMESTAMP, "
+ "clientDateModified=CURRENT_TIMESTAMP WHERE tagID=?";
Zotero.DB.query(sql, existingTagID);
Zotero.Notifier.trigger('modify', 'tag', existingTagID);
Zotero.Tags.reload(existingTagID);
}
// TODO: notify linked items?
//Zotero.Notifier.trigger('modify', 'item', itemIDs);
}
else {
tagObj.name = newName;
// Set all renamed tags to manual
tagObj.type = 0;
tagObj.save();
}
// TODO: notify linked items?
//Zotero.Notifier.trigger('modify', 'item', itemIDs);
Zotero.DB.commitTransaction();
if (oldColorData) {
var libraryIDInt = libraryID ? parseInt(libraryID) : 0
// Remove color from old tag
return self.setColor(libraryIDInt, oldName)
// Add color to new tag
.then(function () {
return self.setColor(
libraryIDInt,
newName,
oldColorData.color,
oldColorData.position
);
});
}
});
}
/**
*
* @param {Integer} libraryID
* @param {String} name Tag name
* @return {Promise} A Q promise for the tag color as a hex string (e.g., '#990000')
*/
this.getColor = function (libraryID, name) {
return this.getColors(libraryID)
.then(function () {
return _libraryColorsByName[libraryID][name]
? _libraryColorsByName[libraryID][name] : false;
});
}
/**
* Get color data by position (number key - 1)
*
* @param {Integer} libraryID
* @param {Integer} position The position of the tag, starting at 0
* @return {Promise} A Q promise for an object containing 'name' and 'color'
*/
this.getColorByPosition = function (libraryID, position) {
return this.getColors(libraryID)
.then(function () {
return _libraryColors[libraryID][position]
? _libraryColors[libraryID][position] : false;
});
}
/**
* @param {Integer} libraryID
* @return {Promise} A Q promise for an object with tag names as keys and
* objects containing 'color' and 'position' as values
*/
this.getColors = function (libraryID) {
var self = this;
return Q.fcall(function () {
if (_libraryColorsByName[libraryID]) {
return _libraryColorsByName[libraryID];
}
return Zotero.SyncedSettings.get(libraryID, 'tagColors')
.then(function (tagColors) {
// If the colors became available from another run
if (_libraryColorsByName[libraryID]) {
return _libraryColorsByName[libraryID];
}
tagColors = tagColors || [];
// Remove colors for tags that don't exist
tagColors = tagColors.filter(function (val) {
var tagIDs = self.getIDs(val.name, libraryID);
// TEMP: handle future getIDs return format change
return tagIDs && tagIDs.length;
});
_libraryColors[libraryID] = tagColors;
_libraryColorsByName[libraryID] = {};
// Also create object keyed by name for quick checking for individual tag colors
for (var i=0; i<tagColors.length; i++) {
_libraryColorsByName[libraryID][tagColors[i].name] = {
color: tagColors[i].color,
position: i
};
}
return _libraryColorsByName[libraryID];
});
});
}
/**
* Assign a color to a tag
*
* @return {Promise}
*/
this.setColor = function (libraryID, name, color, position) {
if (libraryID === null) {
throw new Error("libraryID must be an integer");
}
var self = this;
return this.getColors(libraryID)
.then(function () {
var tagColors = _libraryColors[libraryID];
var tagIDs = self.getIDs(name, libraryID);
// Just to be safe, remove colors for tags that don't exist
tagColors = tagColors.filter(function (val) {
// TEMP: handle future getIDs return format change
return tagIDs && tagIDs.length;
});
// Unset
if (!color) {
// Trying to clear color on tag that doesn't have one
if (!_libraryColorsByName[libraryID][name]) {
return;
}
tagColors = tagColors.filter(function (val) val.name != name);
}
else {
// Get current position if present
var currentPosition = -1;
for (var i=0; i<tagColors.length; i++) {
if (tagColors[i].name == name) {
currentPosition = i;
break;
}
}
// Remove if present
if (currentPosition != -1) {
// If no position was specified, we'll reinsert into the same place
if (typeof position == 'undefined') {
position = currentPosition;
}
tagColors.splice(currentPosition, 1);
}
var newObj = {
name: name,
color: color
};
// If no position or after end, add at end
if (typeof position == 'undefined' || position >= tagColors.length) {
tagColors.push(newObj);
}
// Otherwise insert into new position
else {
tagColors.splice(position, 0, newObj);
}
}
if (tagColors.length) {
return Zotero.SyncedSettings.set(libraryID, 'tagColors', tagColors);
}
else {
return Zotero.SyncedSettings.set(libraryID, 'tagColors');
}
});
};
/**
* Update caches and trigger redrawing of items in the items list
* when a 'tagColors' setting is modified
*/
this.notify = function (event, type, ids, extraData) {
if (type != 'setting') {
return;
}
tagObj.name = name;
// Set all renamed tags to manual
tagObj.type = 0;
tagObj.save();
var self = this;
Zotero.DB.commitTransaction();
}
this.getColor = function (name) {
var tagColors = this.getColors();
return tagColors[name] ? tagColors[name] : '#000000';
}
this.getColors = function (name) {
var tagColors = Zotero.Prefs.get('tagColors');
return tagColors ? JSON.parse(tagColors) : {};
}
this.getItemColor = function (itemID) {
var item = Zotero.Items.get(itemID);
if (!item) {
return false;
}
// Init library tag colors if not yet done
var libraryID = item.libraryID ? item.libraryID : 0;
if (!_colorsByItem[libraryID]) {
_colorsByItem[libraryID] = {};
var tagColors = this.getColors();
for (var name in tagColors) {
var color = tagColors[name];
var tagIDs = Zotero.Tags.getIDs(name, libraryID);
if (!tagIDs) {
continue;
for (let i=0; i<ids.length; i++) {
let libraryID, setting;
[libraryID, setting] = ids[i].split("/");
libraryID = parseInt(libraryID);
if (setting != 'tagColors') {
continue;
}
delete _libraryColors[libraryID];
delete _libraryColorsByName[libraryID];
// Get the tag colors for each library in which they were modified
Zotero.SyncedSettings.get(libraryID, 'tagColors')
.then(function (tagColors) {
if (!tagColors) {
tagColors = [];
}
for each(var tagID in tagIDs) {
var tag = Zotero.Tags.get(tagID);
var itemIDs = tag.getLinkedItems(true);
if (!itemIDs) {
continue;
let id = libraryID + "/" + setting;
if (event == 'modify' && extraData[id].changed) {
var previousTagColors = extraData[id].changed.value;
}
else {
var previousTagColors = [];
}
var affectedItems = [];
// Get all items linked to previous or current tag colors
var tagNames = tagColors.concat(previousTagColors).map(function (val) val.name);
tagNames = Zotero.Utilities.arrayUnique(tagNames);
for (let i=0; i<tagNames.length; i++) {
let tagIDs = self.getIDs(tagNames[i], libraryID) || [];
for (let i=0; i<tagIDs.length; i++) {
affectedItems = affectedItems.concat(self.getTagItems(tagIDs[i]));
}
for each(var id in itemIDs) {
_colorsByItem[libraryID][id] = color;
};
if (affectedItems.length) {
Zotero.Notifier.trigger('redraw', 'item', affectedItems, { column: 'title' });
}
})
.done();
}
};
this.toggleItemsListTags = function (libraryID, items, name) {
var self = this;
return Q.fcall(function () {
var tagIDs = self.getIDs(name, libraryID);
var tags = tagIDs.map(function (tagID) {
return Zotero.Tags.get(tagID, true);
});
if (!items.length) {
return;
}
Zotero.DB.beginTransaction();
// Base our action on the first item. If it has the tag,
// remove the tag from all items. If it doesn't, add it to all.
var firstItem = items[0];
// Remove from all items
if (firstItem.hasTags(tagIDs)) {
for (var i=0; i<items.length; i++) {
for (var j=0; j<tags.length; j++) {
tags[j].removeItem(items[i].id);
}
}
tags.forEach(function (tag) tag.save());
Zotero.Prefs.set('purge.tags', true);
}
// Add to all items
else {
for (var i=0; i<items.length; i++) {
items[i].addTag(name);
}
}
Zotero.DB.commitTransaction();
});
};
/**
* A tree cell can show only one image, and (as of Fx19) it can't be an SVG,
* so we need to generate a composite image containing the existing item type
* icon and one or more tag color swatches.
*
* @params {Array} colors Array of swatch colors
* @params {String} extraImage Chrome URL of image to add to final image
* @return {Q Promise} A Q promise for a data: URL for a PNG
*/
this.generateItemsListImage = function (colors, extraImage) {
var swatchWidth = 8;
var separator = 3;
var extraImageSeparator = 1;
var extraImageWidth = 16;
var canvasHeight = 16;
var swatchHeight = 8;
var prependExtraImage = true;
var hash = colors.join("") + (extraImage ? extraImage : "");
if (_itemsListImagePromises[hash]) {
return _itemsListImagePromises[hash];
}
return _colorsByItem[libraryID][itemID] ? _colorsByItem[libraryID][itemID] : false;
}
this.setColor = function (name, color) {
var tagColors = this.getColors();
var win = Components.classes["@mozilla.org/appshell/appShellService;1"]
.getService(Components.interfaces.nsIAppShellService)
.hiddenDOMWindow;
var doc = win.document;
var canvas = doc.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
// Unset
if (!color || color == '#000000') {
delete tagColors[name];
var width = colors.length * (swatchWidth + separator);
if (extraImage) {
width += (colors.length ? extraImageSeparator : 0) + extraImageWidth;
}
else if (colors.length) {
width -= separator;
}
canvas.width = width;
canvas.height = canvasHeight;
var swatchTop = Math.floor((canvasHeight - swatchHeight) / 2);
var ctx = canvas.getContext('2d');
var x = prependExtraImage ? extraImageWidth + separator + extraImageSeparator : 0;
for (let i=0, len=colors.length; i<len; i++) {
ctx.fillStyle = colors[i];
_canvasRoundRect(ctx, x, swatchTop + 1, swatchWidth, swatchHeight, 2, true, false)
x += swatchWidth + separator;
}
// If there's no extra iamge, resolve a promise now
if (!extraImage) {
var dataURI = canvas.toDataURL("image/png");
var dataURIPromise = Q(dataURI);
_itemsListImagePromises[hash] = dataURIPromise;
return dataURIPromise;
}
// Add an extra image to the beginning or end of the swatches
if (prependExtraImage) {
x = 0;
}
else {
tagColors[name] = color;
x += extraImageSeparator;
}
tagColors = JSON.stringify(tagColors);
Zotero.Prefs.set('tagColors', tagColors);
// If extra image hasn't started loading, start now
if (typeof _itemsListExtraImagePromises[extraImage] == 'undefined') {
let ios = Components.classes['@mozilla.org/network/io-service;1']
.getService(Components.interfaces["nsIIOService"]);
let uri = ios.newURI(extraImage, null, null);
uri = Components.classes['@mozilla.org/chrome/chrome-registry;1']
.getService(Components.interfaces["nsIChromeRegistry"])
.convertChromeURL(uri);
let file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
var img = new win.Image();
img.src = Zotero.File.generateDataURI(file, "image/png");
// Mark that we've started loading
var deferred = Q.defer();
var extraImageDeferred = Q.defer();
_itemsListExtraImagePromises[extraImage] = extraImageDeferred.promise;
// When extra image has loaded, draw it
img.onload = function () {
ctx.drawImage(img, x, 0);
var dataURI = canvas.toDataURL("image/png");
var dataURIPromise = Q(dataURI);
_itemsListImagePromises[hash] = dataURIPromise;
// Fulfill the promise for this call
deferred.resolve(dataURI);
// And resolve the extra image's promise to fulfill
// other promises waiting on it
extraImageDeferred.resolve(img);
}
return deferred.promise;
}
_reloadTagColors();
Zotero.Notifier.trigger('redraw', 'item', []);
// If extra image has already started loading, return a promise
// for the composite image once it's ready
return _itemsListExtraImagePromises[extraImage]
.then(function (img) {
ctx.drawImage(img, x, 0);
var dataURI = canvas.toDataURL("image/png");
var dataURIPromise = Q(dataURI);
_itemsListImagePromises[hash] = dataURIPromise;
return dataURIPromise;
});
}
function _reloadTagColors() {
_colorsByItem = {};
/**
* From http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html
*
* Draws a rounded rectangle using the current state of the canvas.
* If you omit the last three params, it will draw a rectangle
* outline with a 5 pixel border radius
*
* @param {CanvasRenderingContext2D} ctx
* @param {Number} x The top left x coordinate
* @param {Number} y The top left y coordinate
* @param {Number} width The width of the rectangle
* @param {Number} height The height of the rectangle
* @param {Number} radius The corner radius. Defaults to 5;
* @param {Boolean} fill Whether to fill the rectangle. Defaults to false.
* @param {Boolean} stroke Whether to stroke the rectangle. Defaults to true.
*/
function _canvasRoundRect(ctx, x, y, width, height, radius, fill, stroke) {
if (typeof stroke == "undefined" ) {
stroke = true;
}
if (typeof radius === "undefined") {
radius = 5;
}
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
if (stroke) {
ctx.stroke();
}
if (fill) {
ctx.fill();
}
}
/**
* @return {Promise}
*/
function erase(ids) {
ids = Zotero.flattenArguments(ids);
@ -563,7 +935,6 @@ Zotero.Tags = new function() {
*/
this._reload = function (ids) {
_tags = {};
_reloadTagColors();
}

View file

@ -231,6 +231,27 @@ Zotero.File = new function(){
return deferred.promise;
};
/**
* Generate a data: URI from an nsIFile
*
* From https://developer.mozilla.org/en-US/docs/data_URIs
*/
this.generateDataURI = function (file) {
var contentType = Components.classes["@mozilla.org/mime;1"]
.getService(Components.interfaces.nsIMIMEService)
.getTypeFromFile(file);
var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
.createInstance(Components.interfaces.nsIFileInputStream);
inputStream.init(file, 0x01, 0600, 0);
var stream = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);
stream.setInputStream(inputStream);
var encoded = btoa(stream.readBytes(stream.available()));
return "data:" + contentType + ";base64," + encoded;
}
function copyToUnique(file, newFile) {
newFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
var newName = newFile.leafName;

View file

@ -37,8 +37,10 @@
Zotero.ItemTreeView = function(itemGroup, sourcesOnly)
{
this.wrappedJSObject = this;
this.rowCount = 0;
this._initialized = false;
this._skipKeypress = false;
this._itemGroup = itemGroup;
this._sourcesOnly = sourcesOnly;
@ -50,7 +52,7 @@ Zotero.ItemTreeView = function(itemGroup, sourcesOnly)
this._needsSort = false;
this._dataItems = [];
this.rowCount = 0;
this._itemImages = {};
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item', 'collection-item', 'share-items', 'bucket']);
}
@ -136,44 +138,118 @@ Zotero.ItemTreeView.prototype._setTreeGenerator = function(treebox)
// Add a keypress listener for expand/collapse
var tree = this._treebox.treeBody.parentNode;
var me = this;
var self = this;
var coloredTagsRE = new RegExp("^[1-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1}$");
var listener = function(event) {
if (self._skipKeyPress) {
self._skipKeyPress = false;
return;
}
// Handle arrow keys specially on multiple selection, since
// otherwise the tree just applies it to the last-selected row
if (event.keyCode == 39 || event.keyCode == 37) {
if (me._treebox.view.selection.count > 1) {
if (self._treebox.view.selection.count > 1) {
switch (event.keyCode) {
case 39:
me.expandSelectedRows();
self.expandSelectedRows();
break;
case 37:
me.collapseSelectedRows();
self.collapseSelectedRows();
break;
}
event.preventDefault();
}
return;
}
var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
me.expandAllRows();
event.preventDefault();
return;
}
else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
me.collapseAllRows();
event.preventDefault();
// Ignore other non-character keypresses
if (!event.charCode) {
return;
}
event.preventDefault();
Q.fcall(function () {
var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
self.expandAllRows();
return false;
}
else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
self.collapseAllRows();
return false;
}
else if (coloredTagsRE.test(key)) {
let libraryID = self._itemGroup.libraryID;
libraryID = libraryID ? parseInt(libraryID) : 0;
let position = parseInt(key) - 1;
return Zotero.Tags.getColorByPosition(libraryID, position)
.then(function (colorData) {
// If a color isn't assigned to this number, allow key navigation,
// though I'm not sure this is a good idea.
if (!colorData) {
return true;
}
var items = self.getSelectedItems();
return Zotero.Tags.toggleItemsListTags(libraryID, items, colorData.name)
.then(function () {
return false;
});
});
}
return true;
})
// We have to disable key navigation on the tree in order to
// keep it from acting on the 1-6 keys used for colored tags.
// To allow navigation with other keys, we temporarily enable
// key navigation and recreate the keyboard event. Since
// that will trigger this listener again, we set a flag to
// ignore the event, and then clear the flag above when the
// event comes in. I see no way this could go wrong...
.then(function (resend) {
if (!resend) {
return;
}
tree.disableKeyNavigation = false;
self._skipKeyPress = true;
var nsIDWU = Components.interfaces.nsIDOMWindowUtils
var domWindowUtils = event.originalTarget.ownerDocument.defaultView
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(nsIDWU);
var modifiers = 0;
if (event.ctrlKey) {
modifiers |= nsIDWU.MODIFIER_CTRL;
}
if (event.shiftKey) {
modifiers |= nsIDWU.MODIFIER_SHIFT;
}
if (event.metaKey) {
modifiers |= nsIDWU.MODIFIER_META;
}
domWindowUtils.sendKeyEvent(
'keypress',
event.keyCode,
event.charCode,
modifiers
);
tree.disableKeyNavigation = true;
})
.catch(function (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
})
.done();
};
// Store listener so we can call removeEventListener()
// in overlay.js::onCollectionSelected()
this.listener = listener;
tree.addEventListener('keypress', listener, false);
tree.addEventListener('keypress', listener);
// This seems to be the only way to prevent Enter/Return
// from toggle row open/close. The event is handled by
// handleKeyPress() in zoteroPane.js.
@ -371,17 +447,22 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
'zotero-items-column-' + extraData.column
);
for each(var id in ids) {
if (extraData.column == 'title') {
delete this._itemImages[id];
}
this._treebox.invalidateCell(this._itemRowMap[id], col);
}
}
else {
for each(var id in ids) {
delete this._itemImages[id];
this._treebox.invalidateRow(this._itemRowMap[id]);
}
}
}
// Redraw the whole tree
else {
this._itemImages = {};
this._treebox.invalidate();
}
return;
@ -513,6 +594,8 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
this._refreshHashMap();
}
var row = this._itemRowMap[id];
// Clear item type icon and tag colors
delete this._itemImages[id];
// Deleted items get a modify that we have to ignore when
// not viewing the trash
@ -898,7 +981,51 @@ Zotero.ItemTreeView.prototype.getImageSrc = function(row, col)
{
if(col.id == 'zotero-items-column-title')
{
return this._getItemAtRow(row).ref.getImageSrc();
// Get item type icon and tag swatches
var item = this._getItemAtRow(row).ref;
var itemID = item.id;
if (this._itemImages[itemID]) {
return this._itemImages[itemID];
}
var uri = item.getImageSrc();
var tags = item.getTags();
if (!tags.length) {
this._itemImages[itemID] = uri;
return uri;
}
//Zotero.debug("Generating tree image for item " + itemID);
var colorData = [];
for (let i=0, len=tags.length; i<len; i++) {
let libraryIDInt = item.libraryIDInt; // TEMP
colorData.push(Zotero.Tags.getColor(libraryIDInt, tags[i].name));
}
var self = this;
Q.all(colorData)
.then(function (colorData) {
colorData = colorData.filter(function (val) val !== false);
if (!colorData.length) {
return false;
}
colorData.sort(function (a, b) {
return a.position - b.position;
});
var colors = colorData.map(function (val) val.color);
return Zotero.Tags.generateItemsListImage(colors, uri);
})
// When the promise is fulfilled, the data URL is ready, so invalidate
// the cell to force requesting it again
.then(function (dataURL) {
self._itemImages[itemID] = dataURL ? dataURL : uri;
if (dataURL) {
self._treebox.invalidateCell(row, col);
}
})
.done();
this._itemImages[itemID] = uri;
return uri;
}
else if (col.id == 'zotero-items-column-hasAttachment') {
if (this._itemGroup.isTrash()) return false;
@ -2830,37 +2957,12 @@ Zotero.ItemTreeView.prototype.onDragExit = function (event) {
////////////////////////////////////////////////////////////////////////////////
Zotero.ItemTreeView.prototype.isSeparator = function(row) { return false; }
Zotero.ItemTreeView.prototype.getRowProperties = function(row, prop) {
var treeRow = this._getItemAtRow(row);
var itemID = treeRow.ref.id;
// Set background color for selected items with colored tags
if (this.selection.isSelected(row)) {
if (color = Zotero.Tags.getItemColor(itemID)) {
var aServ = Components.classes["@mozilla.org/atom-service;1"].
getService(Components.interfaces.nsIAtomService);
prop.AppendElement(aServ.getAtom("color" + color.substr(1)));
}
}
}
Zotero.ItemTreeView.prototype.getColumnProperties = function(col, prop) { }
Zotero.ItemTreeView.prototype.getRowProperties = function(row, prop) {}
Zotero.ItemTreeView.prototype.getColumnProperties = function(col, prop) {}
Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) {
var treeRow = this._getItemAtRow(row);
var itemID = treeRow.ref.id;
// Set tag colors
//
// Don't set the text color if the row is selected, in which case the background
// color is set in getRowProperties() instead, unless the tree isn't focused,
// in which case it's not
if (!this.selection.isSelected(row) || !this._treebox.focused) {
if (color = Zotero.Tags.getItemColor(itemID)) {
var aServ = Components.classes["@mozilla.org/atom-service;1"].
getService(Components.interfaces.nsIAtomService);
prop.AppendElement(aServ.getAtom("color" + color.substr(1)));
}
}
// Mark items not matching search as context rows, displayed in gray
if (this._searchMode && !this._searchItemIDs[itemID]) {
var aServ = Components.classes["@mozilla.org/atom-service;1"].

View file

@ -28,7 +28,7 @@ Zotero.Notifier = new function(){
var _disabled = false;
var _types = [
'collection', 'creator', 'search', 'share', 'share-items', 'item',
'collection-item', 'item-tag', 'tag', 'group', 'trash', 'bucket', 'relation'
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'bucket', 'relation'
];
var _inTransaction;
var _locked = false;

View file

@ -115,6 +115,7 @@ Zotero.Schema = new function(){
*/
this.updateSchema = function () {
var dbVersion = this.getDBVersion('userdata');
var dbVersion2 = this.getDBVersion('userdata2');
// 'schema' check is for old (<= 1.0b1) schema system,
// 'user' is for pre-1.0b2 'user' table
@ -133,7 +134,7 @@ Zotero.Schema = new function(){
if (dbVersion < schemaVersion){
Zotero.DB.backupDatabase(dbVersion);
Zotero.wait(1000);
}
}
Zotero.DB.beginTransaction();
@ -154,19 +155,20 @@ Zotero.Schema = new function(){
}
}
var up2 = _updateSchema('system');
var up1 = _updateSchema('system');
// Update custom tables if they exist so that changes are in place before user data migration
if (Zotero.DB.tableExists('customItemTypes')) {
this.updateCustomTables(up2);
}
if(up2) Zotero.wait();
var up1 = _migrateUserDataSchema(dbVersion);
var up3 = _updateSchema('triggers');
// Update custom tables again in case custom fields were changed during user data migration
if (up1) {
this.updateCustomTables();
this.updateCustomTables(up1);
}
if(up1) Zotero.wait();
var up2 = _migrateUserDataSchema(dbVersion);
var up3 = _migrateUserDataSchemaSilent(dbVersion2);
var up4 = _updateSchema('triggers');
if (up2) {
// Update custom tables again in case custom fields were changed during user data migration
this.updateCustomTables();
Zotero.wait()
}
Zotero.DB.commitTransaction();
}
@ -176,7 +178,7 @@ Zotero.Schema = new function(){
throw(e);
}
if (up1) {
if (up2) {
// Upgrade seems to have been a success -- delete any previous backups
var maxPrevious = dbVersion - 1;
var file = Zotero.getZoteroDirectory();
@ -1353,10 +1355,21 @@ Zotero.Schema = new function(){
* Retrieve the version from the top line of the schema SQL file
*/
function _getSchemaSQLVersion(schema){
// TEMP
if (schema == 'userdata2') {
schema = 'userdata';
var newUserdata = true;
}
var sql = _getSchemaSQL(schema);
// Fetch the schema version from the first line of the file
var schemaVersion = sql.match(/^-- ([0-9]+)/)[1];
var schemaVersion = parseInt(sql.match(/^-- ([0-9]+)/)[1]);
// TEMP: For 'userdata', cap the version at 76
// For 'userdata2', versions > 76 are allowed.
if (schema == 'userdata' && !newUserdata) {
schemaVersion = Math.min(76, schemaVersion);
}
_schemaVersions[schema] = schemaVersion;
return schemaVersion;
@ -1416,6 +1429,7 @@ Zotero.Schema = new function(){
_updateDBVersion('system', _getSchemaSQLVersion('system'));
_updateDBVersion('userdata', _getSchemaSQLVersion('userdata'));
_updateDBVersion('userdata2', _getSchemaSQLVersion('userdata2'));
_updateDBVersion('triggers', _getSchemaSQLVersion('triggers'));
if (!Zotero.Schema.skipDefaultData) {
@ -1762,6 +1776,9 @@ Zotero.Schema = new function(){
function _migrateUserDataSchema(fromVersion){
var toVersion = _getSchemaSQLVersion('userdata');
// Only upgrades through version 76 are handled here
toVersion = Math.min(76, toVersion);
if (fromVersion==toVersion){
return false;
}
@ -3286,4 +3303,47 @@ Zotero.Schema = new function(){
return true;
}
// TEMP
//
// TODO: Make this asynchronous, and make it block other SQLite
function _migrateUserDataSchemaSilent(fromVersion) {
var toVersion = _getSchemaSQLVersion('userdata2');
if (!fromVersion) {
fromVersion = 76;
}
if (fromVersion == toVersion) {
return false;
}
Zotero.debug('Updating user data tables from version ' + fromVersion + ' to ' + toVersion);
Zotero.DB.beginTransaction();
try {
// Step through version changes until we reach the current version
//
// Each block performs the changes necessary to move from the
// previous revision to that one.
for (var i=fromVersion + 1; i<=toVersion; i++) {
if (i == 77) {
Zotero.DB.query("CREATE TABLE syncedSettings (\n setting TEXT NOT NULL,\n libraryID INT NOT NULL,\n value NOT NULL,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n PRIMARY KEY (setting, libraryID)\n)");
Zotero.DB.query("INSERT OR IGNORE INTO syncObjectTypes VALUES (7, 'setting')");
}
}
_updateDBVersion('userdata2', toVersion);
Zotero.DB.commitTransaction();
}
catch (e) {
Zotero.DB.rollbackTransaction();
throw(e);
}
return true;
}
}

View file

@ -52,6 +52,10 @@ Zotero.Sync = new function() {
singular: 'Relation',
plural: 'Relations'
},
setting: {
singular: 'Setting',
plural: 'Settings'
}
};
});
@ -128,6 +132,10 @@ Zotero.Sync = new function() {
}
for (var type in this.syncObjects) {
if (type == 'setting') {
continue;
}
var Types = this.syncObjects[type].plural; // 'Items'
var types = Types.toLowerCase(); // 'items'
@ -439,9 +447,15 @@ Zotero.Sync.EventListener = new function () {
continue;
}
var oldItem = extraData[ids[i]].old;
var libraryID = oldItem.primary.libraryID;
var key = oldItem.primary.key;
var libraryID, key;
if (type == 'setting') {
[libraryID, key] = ids[i].split("/");
}
else {
var oldItem = extraData[ids[i]].old;
libraryID = oldItem.primary.libraryID;
key = oldItem.primary.key;
}
if (!key) {
throw("Key not provided in notifier object in "
@ -2748,6 +2762,20 @@ Zotero.Sync.Server.Data = new function() {
typeloop:
for each(var objectNode in updatedNode.xpath(types + "/" + type)) {
var libraryID = _libID(objectNode.getAttribute('libraryID'));
// Process remote settings
if (type == 'setting') {
var name = objectNode.getAttribute('name');
if (!libraryID) {
libraryID = 0;
}
Zotero.debug("Processing remote setting " + libraryID + "/" + name);
var version = objectNode.getAttribute('version');
var value = JSON.parse(objectNode.textContent);
Zotero.SyncedSettings.setSynchronous(libraryID, name, value, version, true);
continue;
}
var key = objectNode.getAttribute('key');
var objLibraryKeyHash = Zotero[Types].makeLibraryKeyHash(libraryID, key);
@ -3117,6 +3145,18 @@ Zotero.Sync.Server.Data = new function() {
for each(var delNode in deletedObjectNodes) {
var libraryID = _libID(delNode.getAttribute('libraryID'));
var key = delNode.getAttribute('key');
// Process remote settings deletions
if (type == 'setting') {
if (!libraryID) {
libraryID = 0;
}
Zotero.debug("Processing remote setting " + libraryID + "/" + key);
Zotero.Sync.EventListener.ignoreDeletions('setting', [libraryID + "/" + key]);
Zotero.SyncedSettings.setSynchronous(libraryID, key);
continue;
}
var obj = Zotero[Types].getByLibraryAndKey(libraryID, key);
// Object can't be found
if (!obj) {
@ -3391,6 +3431,10 @@ Zotero.Sync.Server.Data = new function() {
var types = Types.toLowerCase(); // 'items'
var objectsNode = false;
if (type == 'setting') {
continue;
}
Zotero.debug("Processing locally changed " + types);
var libraryID, key;
@ -3427,6 +3471,21 @@ Zotero.Sync.Server.Data = new function() {
}
}
// Add unsynced settings
var sql = "SELECT libraryID, setting, value FROM syncedSettings WHERE synced=0";
var rows = Zotero.DB.query(sql);
if (rows) {
var settingsNode = doc.createElement('settings');
for (var i=0; i<rows.length; i++) {
var settingNode = doc.createElement('setting');
settingNode.setAttribute('libraryID', rows[i].libraryID ? rows[i].libraryID : Zotero.libraryID);
settingNode.setAttribute('name', rows[i].setting);
settingNode.appendChild(doc.createTextNode(_xmlize(rows[i].value)));
settingsNode.appendChild(settingNode);
}
docElem.appendChild(settingsNode);
}
// Deletions
var deletedNode = doc.createElement('deleted');
var inserted = false;

View file

@ -0,0 +1,115 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2013 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
/**
* @namespace
*/
Zotero.SyncedSettings = (function () {
//
// Public methods
//
var module = {
get: function (libraryID, setting) {
return Q.fcall(function () {
var sql = "SELECT value FROM syncedSettings WHERE setting=? AND libraryID=?";
return JSON.parse(Zotero.DB.valueQuery(sql, [setting, libraryID]));
});
},
set: function (libraryID, setting, value, version, synced) {
var self = this;
return Q.fcall(function () {
return self.setSynchronous(libraryID, setting, value, version, synced);
});
},
setSynchronous: function (libraryID, setting, value, version, synced) {
// TODO: get rid of this once we have proper affected rows handling
var sql = "SELECT value FROM syncedSettings WHERE setting=? AND libraryID=?";
var currentValue = Zotero.DB.valueQuery(sql, [setting, libraryID]);
// Make sure we can tell the difference between a
// missing setting (FALSE as returned by valueQuery())
// and a FALSE setting (FALSE as returned by JSON.parse())
var hasCurrentValue = currentValue !== false;
var hasValue = typeof value != 'undefined';
currentValue = JSON.parse(currentValue);
if ((!hasCurrentValue && !hasValue) || value === currentValue) {
return false;
}
var id = libraryID + '/' + setting;
if (hasCurrentValue) {
var extraData = {};
extraData[id] = {
changed: {}
};
extraData[id].changed = {
value: currentValue
};
}
// Clear
if (typeof value == 'undefined') {
var sql = "DELETE FROM syncedSettings WHERE setting=? AND libraryID=?";
Zotero.DB.query(sql, [setting, libraryID]);
Zotero.Notifier.trigger('delete', 'setting', [id], extraData);
return true;
}
// Set/update
if (currentValue === false) {
var event = 'add';
var extraData = {};
}
else {
var event = 'modify';
}
synced = synced ? 1 : 0;
if (hasCurrentValue) {
var sql = "UPDATE syncedSettings SET value=?, synced=? WHERE setting=? AND libraryID=?";
Zotero.DB.query(sql, [JSON.stringify(value), synced, setting, libraryID]);
}
else {
var sql = "INSERT INTO syncedSettings "
+ "(setting, libraryID, value, synced) VALUES (?, ?, ?, ?)";
Zotero.DB.query(sql, [setting, libraryID, JSON.stringify(value), synced]);
}
Zotero.Notifier.trigger(event, 'setting', [id], extraData);
return true;
}
};
return module;
}());

View file

@ -664,6 +664,8 @@ Components.utils.import("resource://gre/modules/Services.jsm");
Zotero.Server.init();
}
Zotero.Notifier.registerObserver(Zotero.Tags, 'setting');
Zotero.Sync.init();
Zotero.Sync.Runner.init();

View file

@ -86,7 +86,6 @@
</popup>
<stack id="zotero-pane-stack">
<!-- Barrier to prevent tabbing into Zotero pane when busy -->
<box id="zotero-pane-tab-catcher-top" hidden="true" align="center" pack="center" style="opacity: 0">
<checkbox/>
@ -324,9 +323,11 @@
<vbox id="zotero-items-pane" zotero-persist="width" flex="1">
<deck id="zotero-items-pane-content" selectedIndex="0" flex="1">
<!-- Key navigation is handled by listener in itemTreeView.js -->
<tree
id="zotero-items-tree" context="zotero-itemmenu"
enableColumnDrag="true"
disableKeyNavigation="true"
onfocus="if (ZoteroPane_Local.itemsView.rowCount &amp;&amp; !ZoteroPane_Local.itemsView.selection.count) { ZoteroPane_Local.itemsView.selection.select(0); }"
onkeydown="ZoteroPane_Local.handleKeyDown(event, this.id)"
onkeypress="ZoteroPane_Local.handleKeyPress(event, this.id)"

View file

@ -136,9 +136,16 @@
<!ENTITY zotero.tagSelector.selectVisible "Select Visible">
<!ENTITY zotero.tagSelector.clearVisible "Deselect Visible">
<!ENTITY zotero.tagSelector.clearAll "Deselect All">
<!ENTITY zotero.tagSelector.assignColor "Assign Color…">
<!ENTITY zotero.tagSelector.renameTag "Rename Tag…">
<!ENTITY zotero.tagSelector.deleteTag "Delete Tag…">
<!ENTITY zotero.tagColorChooser.title "Choose a Tag Color and Position">
<!ENTITY zotero.tagColorChooser.color "Color:">
<!ENTITY zotero.tagColorChooser.position "Position:">
<!ENTITY zotero.tagColorChooser.setColor "Set Color">
<!ENTITY zotero.tagColorChooser.removeColor "Remove Color">
<!ENTITY zotero.lookup.description "Enter the ISBN, DOI, or PMID to look up in the box below.">
<!ENTITY zotero.selectitems.title "Select Items">

View file

@ -150,6 +150,10 @@ pane.tagSelector.delete.message = Are you sure you want to delete this tag?\
pane.tagSelector.numSelected.none = 0 tags selected
pane.tagSelector.numSelected.singular = %S tag selected
pane.tagSelector.numSelected.plural = %S tags selected
pane.tagSelector.maxColoredTags = Only %S tags in each library can have colors assigned.
tagColorChooser.numberKeyInstructions = You can add this tag to selected items by pressing the $NUMBER key on the keyboard.
tagColorChooser.maxTags = Up to %S tags in each library can have colors assigned.
pane.items.loading = Loading items list…
pane.items.trash.title = Move to Trash

View file

@ -0,0 +1,24 @@
customcolorpicker[type="button"] {
width: 38px;
height: 24px;
border: 1px solid #a7a7a7;
background-color: ThreeDFace;
padding: 3px;
-moz-appearance: button-bevel;
}
.colorpickertile[hover="true"], .cp-light[hover="true"] {
border: 0;
}
.colorpickertile[selected="true"] {
border : 1px outset #C0C0C0;
}
.colorpickertile[hover="true"]:not([selected="true"]) {
border : 1px dotted #A7A7A7;
}
.cp-light[hover="true"]:not([selected="true"]) {
border : 1px dotted #000000;
}

View file

@ -200,149 +200,6 @@
#zotero-items-tree treechildren::-moz-tree-image(selected, hasAttachment, pie63) { -moz-image-region: rect(32px, 2016px, 64px, 1984px); }
#zotero-items-tree treechildren::-moz-tree-image(selected, hasAttachment, pie64) { -moz-image-region: rect(32px, 2048px, 64px, 2016px); }
/* Set tag colors */
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFFFFF) { color:#FFFFFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFCCCC) { color:#FFCCCC }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFCC99) { color:#FFCC99 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFFF99) { color:#FFFF99 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFFFCC) { color:#FFFFCC }
#zotero-items-tree treechildren::-moz-tree-cell-text(color99FF99) { color:#99FF99 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color99FFFF) { color:#99FFFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCCFFFF) { color:#CCFFFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCCCCFF) { color:#CCCCFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFCCFF) { color:#FFCCFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCCCCCC) { color:#CCCCCC }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFF6666) { color:#FF6666 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFF9966) { color:#FF9966 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFFF66) { color:#FFFF66 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFFF33) { color:#FFFF33 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color66FF99) { color:#66FF99 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color33FFFF) { color:#33FFFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(color66FFFF) { color:#66FFFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(color9999FF) { color:#9999FF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFF99FF) { color:#FF99FF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorC0C0C0) { color:#C0C0C0 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFF0000) { color:#FF0000 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFF9900) { color:#FF9900 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFCC66) { color:#FFCC66 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFFF00) { color:#FFFF00 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color33FF33) { color:#33FF33 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color66CCCC) { color:#66CCCC }
#zotero-items-tree treechildren::-moz-tree-cell-text(color33CCFF) { color:#33CCFF }
#zotero-items-tree treechildren::-moz-tree-cell-text(color6666CC) { color:#6666CC }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCC66CC) { color:#CC66CC }
#zotero-items-tree treechildren::-moz-tree-cell-text(color999999) { color:#999999 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCC0000) { color:#CC0000 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFF6600) { color:#FF6600 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFCC33) { color:#FFCC33 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorFFCC00) { color:#FFCC00 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color33CC00) { color:#33CC00 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color00CCCC) { color:#00CCCC }
#zotero-items-tree treechildren::-moz-tree-cell-text(color3366FF) { color:#3366FF }
#zotero-items-tree treechildren::-moz-tree-cell-text(color6633FF) { color:#6633FF }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCC33CC) { color:#CC33CC }
#zotero-items-tree treechildren::-moz-tree-cell-text(color666666) { color:#666666 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color990000) { color:#990000 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCC6600) { color:#CC6600 }
#zotero-items-tree treechildren::-moz-tree-cell-text(colorCC9933) { color:#CC9933 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color999900) { color:#999900 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color009900) { color:#009900 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color339999) { color:#339999 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color3333FF) { color:#3333FF }
#zotero-items-tree treechildren::-moz-tree-cell-text(color6600CC) { color:#6600CC }
#zotero-items-tree treechildren::-moz-tree-cell-text(color993399) { color:#993399 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color333333) { color:#333333 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color660000) { color:#660000 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color993300) { color:#993300 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color996633) { color:#996633 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color666600) { color:#666600 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color006600) { color:#006600 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color336666) { color:#336666 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color000099) { color:#000099 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color333399) { color:#333399 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color663366) { color:#663366 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color330000) { color:#330000 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color663300) { color:#663300 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color663333) { color:#663333 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color333300) { color:#333300 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color003300) { color:#003300 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color003333) { color:#003333 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color000066) { color:#000066 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color330099) { color:#330099 }
#zotero-items-tree treechildren::-moz-tree-cell-text(color330033) { color:#330033 }
/* For selected item with colored tags, set the background color instead */
#zotero-items-tree treechildren::-moz-tree-row(colorFFFFFF,focus) { background:#FFFFFF }
#zotero-items-tree treechildren::-moz-tree-row(colorFFCCCC,focus) { background:#FFCCCC }
#zotero-items-tree treechildren::-moz-tree-row(colorFFCC99,focus) { background:#FFCC99 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFFF99,focus) { background:#FFFF99 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFFFCC,focus) { background:#FFFFCC }
#zotero-items-tree treechildren::-moz-tree-row(color99FF99,focus) { background:#99FF99 }
#zotero-items-tree treechildren::-moz-tree-row(color99FFFF,focus) { background:#99FFFF }
#zotero-items-tree treechildren::-moz-tree-row(colorCCFFFF,focus) { background:#CCFFFF }
#zotero-items-tree treechildren::-moz-tree-row(colorCCCCFF,focus) { background:#CCCCFF }
#zotero-items-tree treechildren::-moz-tree-row(colorFFCCFF,focus) { background:#FFCCFF }
#zotero-items-tree treechildren::-moz-tree-row(colorCCCCCC,focus) { background:#CCCCCC }
#zotero-items-tree treechildren::-moz-tree-row(colorFF6666,focus) { background:#FF6666 }
#zotero-items-tree treechildren::-moz-tree-row(colorFF9966,focus) { background:#FF9966 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFFF66,focus) { background:#FFFF66 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFFF33,focus) { background:#FFFF33 }
#zotero-items-tree treechildren::-moz-tree-row(color66FF99,focus) { background:#66FF99 }
#zotero-items-tree treechildren::-moz-tree-row(color33FFFF,focus) { background:#33FFFF }
#zotero-items-tree treechildren::-moz-tree-row(color66FFFF,focus) { background:#66FFFF }
#zotero-items-tree treechildren::-moz-tree-row(color9999FF,focus) { background:#9999FF }
#zotero-items-tree treechildren::-moz-tree-row(colorFF99FF,focus) { background:#FF99FF }
#zotero-items-tree treechildren::-moz-tree-row(colorC0C0C0,focus) { background:#C0C0C0 }
#zotero-items-tree treechildren::-moz-tree-row(colorFF0000,focus) { background:#FF0000 }
#zotero-items-tree treechildren::-moz-tree-row(colorFF9900,focus) { background:#FF9900 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFCC66,focus) { background:#FFCC66 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFFF00,focus) { background:#FFFF00 }
#zotero-items-tree treechildren::-moz-tree-row(color33FF33,focus) { background:#33FF33 }
#zotero-items-tree treechildren::-moz-tree-row(color66CCCC,focus) { background:#66CCCC }
#zotero-items-tree treechildren::-moz-tree-row(color33CCFF,focus) { background:#33CCFF }
#zotero-items-tree treechildren::-moz-tree-row(color6666CC,focus) { background:#6666CC }
#zotero-items-tree treechildren::-moz-tree-row(colorCC66CC,focus) { background:#CC66CC }
#zotero-items-tree treechildren::-moz-tree-row(color999999,focus) { background:#999999 }
#zotero-items-tree treechildren::-moz-tree-row(colorCC0000,focus) { background:#CC0000 }
#zotero-items-tree treechildren::-moz-tree-row(colorFF6600,focus) { background:#FF6600 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFCC33,focus) { background:#FFCC33 }
#zotero-items-tree treechildren::-moz-tree-row(colorFFCC00,focus) { background:#FFCC00 }
#zotero-items-tree treechildren::-moz-tree-row(color33CC00,focus) { background:#33CC00 }
#zotero-items-tree treechildren::-moz-tree-row(color00CCCC,focus) { background:#00CCCC }
#zotero-items-tree treechildren::-moz-tree-row(color3366FF,focus) { background:#3366FF }
#zotero-items-tree treechildren::-moz-tree-row(color6633FF,focus) { background:#6633FF }
#zotero-items-tree treechildren::-moz-tree-row(colorCC33CC,focus) { background:#CC33CC }
#zotero-items-tree treechildren::-moz-tree-row(color666666,focus) { background:#666666 }
#zotero-items-tree treechildren::-moz-tree-row(color990000,focus) { background:#990000 }
#zotero-items-tree treechildren::-moz-tree-row(colorCC6600,focus) { background:#CC6600 }
#zotero-items-tree treechildren::-moz-tree-row(colorCC9933,focus) { background:#CC9933 }
#zotero-items-tree treechildren::-moz-tree-row(color999900,focus) { background:#999900 }
#zotero-items-tree treechildren::-moz-tree-row(color009900,focus) { background:#009900 }
#zotero-items-tree treechildren::-moz-tree-row(color339999,focus) { background:#339999 }
#zotero-items-tree treechildren::-moz-tree-row(color3333FF,focus) { background:#3333FF }
#zotero-items-tree treechildren::-moz-tree-row(color6600CC,focus) { background:#6600CC }
#zotero-items-tree treechildren::-moz-tree-row(color993399,focus) { background:#993399 }
#zotero-items-tree treechildren::-moz-tree-row(color333333,focus) { background:#333333 }
#zotero-items-tree treechildren::-moz-tree-row(color660000,focus) { background:#660000 }
#zotero-items-tree treechildren::-moz-tree-row(color993300,focus) { background:#993300 }
#zotero-items-tree treechildren::-moz-tree-row(color996633,focus) { background:#996633 }
#zotero-items-tree treechildren::-moz-tree-row(color666600,focus) { background:#666600 }
#zotero-items-tree treechildren::-moz-tree-row(color006600,focus) { background:#006600 }
#zotero-items-tree treechildren::-moz-tree-row(color336666,focus) { background:#336666 }
#zotero-items-tree treechildren::-moz-tree-row(color000099,focus) { background:#000099 }
#zotero-items-tree treechildren::-moz-tree-row(color333399,focus) { background:#333399 }
#zotero-items-tree treechildren::-moz-tree-row(color663366,focus) { background:#663366 }
#zotero-items-tree treechildren::-moz-tree-row(color330000,focus) { background:#330000 }
#zotero-items-tree treechildren::-moz-tree-row(color663300,focus) { background:#663300 }
#zotero-items-tree treechildren::-moz-tree-row(color663333,focus) { background:#663333 }
#zotero-items-tree treechildren::-moz-tree-row(color333300,focus) { background:#333300 }
#zotero-items-tree treechildren::-moz-tree-row(color003300,focus) { background:#003300 }
#zotero-items-tree treechildren::-moz-tree-row(color003333,focus) { background:#003333 }
#zotero-items-tree treechildren::-moz-tree-row(color000066,focus) { background:#000066 }
#zotero-items-tree treechildren::-moz-tree-row(color330099,focus) { background:#330099 }
#zotero-items-tree treechildren::-moz-tree-row(color330033,focus) { background:#330033 }
/* Style search results, display non-matches in gray */
#zotero-items-tree treechildren::-moz-tree-cell-text(contextRow) {
color: gray;

View file

@ -0,0 +1,8 @@
menulist {
font-size: 14px;
}
#number-key {
margin: 0 1px;
font-weight: bold;
}

View file

@ -96,6 +96,14 @@ tagsbox row
-moz-box-align:center;
}
customcolorpicker {
-moz-binding: url(chrome://zotero/content/bindings/customcolorpicker.xml#custom-colorpicker);
}
customcolorpicker[type=button] {
-moz-binding: url(chrome://zotero/content/bindings/customcolorpicker.xml#custom-colorpicker-button);
}
seealsobox
{
-moz-binding: url('chrome://zotero/content/bindings/relatedbox.xml#seealso-box');
@ -160,7 +168,6 @@ zoterofilesyncstatus {
-moz-binding: url('chrome://zotero/content/bindings/filesyncstatus.xml#file-sync-status');
}
label.zotero-text-link {
-moz-binding: url('chrome://zotero/content/bindings/text-link.xml#text-link');
-moz-user-focus: normal;

View file

@ -101,6 +101,7 @@ const xpcomFilesLocal = [
'storage/mode',
'storage/zfs',
'storage/webdav',
'syncedSettings',
'timeline',
'uri',
'translation/translate_item',

View file

@ -1360,3 +1360,4 @@ INSERT INTO "syncObjectTypes" VALUES(3, 'item');
INSERT INTO "syncObjectTypes" VALUES(4, 'search');
INSERT INTO "syncObjectTypes" VALUES(5, 'tag');
INSERT INTO "syncObjectTypes" VALUES(6, 'relation');
INSERT INTO "syncObjectTypes" VALUES(7, 'setting');

View file

@ -1,4 +1,4 @@
-- 76
-- 77
-- Copyright (c) 2009 Center for History and New Media
-- George Mason University, Fairfax, Virginia, USA
@ -30,6 +30,7 @@ CREATE TABLE version (
);
CREATE INDEX schema ON version(schema);
-- Settings that have to be tied to the local database rather than the profile directory
CREATE TABLE settings (
setting TEXT,
key TEXT,
@ -37,6 +38,16 @@ CREATE TABLE settings (
PRIMARY KEY (setting, key)
);
-- Settings that get synced between Zotero installations
CREATE TABLE syncedSettings (
setting TEXT NOT NULL,
libraryID INT NOT NULL,
value NOT NULL,
version INT NOT NULL DEFAULT 0,
synced INT NOT NULL DEFAULT 0,
PRIMARY KEY (setting, libraryID)
);
-- The foundational table; every item collected has a unique record here
CREATE TABLE items (
itemID INTEGER PRIMARY KEY,