Commit 995f4f0d authored by alekseys's avatar alekseys Committed by Commit bot

Add DPI option selection to Print Preview.

BUG=429393

Review URL: https://codereview.chromium.org/692303002

Cr-Commit-Position: refs/heads/master@{#302497}
parent 121e8899
...@@ -8450,6 +8450,12 @@ Keep your key file in a safe place. You will need it to create new versions of y ...@@ -8450,6 +8450,12 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_PRINT_PREVIEW_MEDIA_SIZE_LABEL" desc="Media size option label. Provides user the option to change the size of the printed page."> <message name="IDS_PRINT_PREVIEW_MEDIA_SIZE_LABEL" desc="Media size option label. Provides user the option to change the size of the printed page.">
Paper size Paper size
</message> </message>
<message name="IDS_PRINT_PREVIEW_DPI_LABEL" desc="Print quality option label. Provides user the option to select one of the print quality options defined for the printer.">
Quality
</message>
<message name="IDS_PRINT_PREVIEW_DPI_ITEM_LABEL" desc="Print quality option value text.">
<ph name="DPI">$1<ex>300</ex></ph> dpi
</message>
<message name="IDS_PRINT_PREVIEW_NATIVE_DIALOG" desc="Option offering the user to open the native print dialog, displayed when the PDF viewer is missing or when print preview fails."> <message name="IDS_PRINT_PREVIEW_NATIVE_DIALOG" desc="Option offering the user to open the native print dialog, displayed when the PDF viewer is missing or when print preview fails.">
Print using system dialog… Print using system dialog…
</message> </message>
......
...@@ -40,6 +40,7 @@ cr.define('print_preview', function() { ...@@ -40,6 +40,7 @@ cr.define('print_preview', function() {
SELECTED_DESTINATION_CAPABILITIES: 'selectedDestinationCapabilities', SELECTED_DESTINATION_CAPABILITIES: 'selectedDestinationCapabilities',
SELECTED_DESTINATION_NAME: 'selectedDestinationName', SELECTED_DESTINATION_NAME: 'selectedDestinationName',
IS_GCP_PROMO_DISMISSED: 'isGcpPromoDismissed', IS_GCP_PROMO_DISMISSED: 'isGcpPromoDismissed',
DPI: 'dpi',
MEDIA_SIZE: 'mediaSize', MEDIA_SIZE: 'mediaSize',
MARGINS_TYPE: 'marginsType', MARGINS_TYPE: 'marginsType',
CUSTOM_MARGINS: 'customMargins', CUSTOM_MARGINS: 'customMargins',
......
...@@ -85,6 +85,14 @@ cr.define('print_preview', function() { ...@@ -85,6 +85,14 @@ cr.define('print_preview', function() {
this.copies_ = this.copies_ =
new print_preview.ticket_items.Copies(this.destinationStore_); new print_preview.ticket_items.Copies(this.destinationStore_);
/**
* DPI ticket item.
* @type {!print_preview.ticket_items.Dpi}
* @private
*/
this.dpi_ = new print_preview.ticket_items.Dpi(
this.appState_, this.destinationStore_);
/** /**
* Duplex ticket item. * Duplex ticket item.
* @type {!print_preview.ticket_items.Duplex} * @type {!print_preview.ticket_items.Duplex}
...@@ -243,6 +251,10 @@ cr.define('print_preview', function() { ...@@ -243,6 +251,10 @@ cr.define('print_preview', function() {
return this.customMargins_; return this.customMargins_;
}, },
get dpi() {
return this.dpi_;
},
get duplex() { get duplex() {
return this.duplex_; return this.duplex_;
}, },
...@@ -309,6 +321,11 @@ cr.define('print_preview', function() { ...@@ -309,6 +321,11 @@ cr.define('print_preview', function() {
/** @type {!Object} */(this.appState_.getField( /** @type {!Object} */(this.appState_.getField(
print_preview.AppState.Field.IS_COLOR_ENABLED))); print_preview.AppState.Field.IS_COLOR_ENABLED)));
} }
if (this.appState_.hasField(print_preview.AppState.Field.DPI)) {
this.dpi_.updateValue(
/** @type {!Object} */(this.appState_.getField(
print_preview.AppState.Field.DPI)));
}
if (this.appState_.hasField( if (this.appState_.hasField(
print_preview.AppState.Field.IS_DUPLEX_ENABLED)) { print_preview.AppState.Field.IS_DUPLEX_ENABLED)) {
this.duplex_.updateValue( this.duplex_.updateValue(
...@@ -440,6 +457,14 @@ cr.define('print_preview', function() { ...@@ -440,6 +457,14 @@ cr.define('print_preview', function() {
cjt.print.page_orientation = cjt.print.page_orientation =
{type: this.landscape.getValue() ? 'LANDSCAPE' : 'PORTRAIT'}; {type: this.landscape.getValue() ? 'LANDSCAPE' : 'PORTRAIT'};
} }
if (this.dpi.isCapabilityAvailable()) {
var value = this.dpi.getValue();
cjt.print.dpi = {
horizontal_dpi: value.horizontal_dpi,
vertical_dpi: value.vertical_dpi,
vendor_id: value.vendor_id
};
}
if (this.vendorItems.isCapabilityAvailable() && if (this.vendorItems.isCapabilityAvailable() &&
this.vendorItems.isUserEdited()) { this.vendorItems.isUserEdited()) {
var items = this.vendorItems.ticketItems; var items = this.vendorItems.ticketItems;
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('print_preview.ticket_items', function() {
'use strict';
/**
* DPI ticket item.
* @param {!print_preview.AppState} appState App state used to persist DPI
* selection.
* @param {!print_preview.DestinationStore} destinationStore Destination store
* used to determine if a destination has the DPI capability.
* @constructor
* @extends {print_preview.ticket_items.TicketItem}
*/
function Dpi(appState, destinationStore) {
print_preview.ticket_items.TicketItem.call(
this,
appState,
print_preview.AppState.Field.DPI,
destinationStore);
};
Dpi.prototype = {
__proto__: print_preview.ticket_items.TicketItem.prototype,
/** @override */
wouldValueBeValid: function(value) {
if (!this.isCapabilityAvailable())
return false;
return this.capability.option.some(function(option) {
return option.horizontal_dpi == value.horizontal_dpi &&
option.vertical_dpi == value.vertical_dpi &&
option.vendor_id == value.vendor_id;
});
},
/** @override */
isCapabilityAvailable: function() {
return !!this.capability &&
!!this.capability.option &&
this.capability.option.length > 1;
},
/** @override */
isValueEqual: function(value) {
var myValue = this.getValue();
return myValue.horizontal_dpi == value.horizontal_dpi &&
myValue.vertical_dpi == value.vertical_dpi &&
myValue.vendor_id == value.vendor_id;
},
/** @return {Object} DPI capability of the selected destination. */
get capability() {
var destination = this.getSelectedDestInternal();
return (destination &&
destination.capabilities &&
destination.capabilities.printer &&
destination.capabilities.printer.dpi) ||
null;
},
/** @override */
getDefaultValueInternal: function() {
var defaultOptions = this.capability.option.filter(function(option) {
return option.is_default;
});
return defaultOptions.length > 0 ? defaultOptions[0] : null;
},
/** @override */
getCapabilityNotAvailableValueInternal: function() {
return {};
}
};
// Export
return {
Dpi: Dpi
};
});
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
<link rel="stylesheet" href="settings/margin_settings.css"> <link rel="stylesheet" href="settings/margin_settings.css">
<link rel="stylesheet" href="settings/media_size_settings.css"> <link rel="stylesheet" href="settings/media_size_settings.css">
<link rel="stylesheet" href="settings/layout_settings.css"> <link rel="stylesheet" href="settings/layout_settings.css">
<link rel="stylesheet" href="settings/dpi_settings.css">
<link rel="stylesheet" href="settings/other_options_settings.css"> <link rel="stylesheet" href="settings/other_options_settings.css">
<link rel="stylesheet" href="settings/advanced_options_settings.css"> <link rel="stylesheet" href="settings/advanced_options_settings.css">
<link rel="stylesheet" href="settings/advanced_settings/advanced_settings.css"> <link rel="stylesheet" href="settings/advanced_settings/advanced_settings.css">
...@@ -64,6 +65,7 @@ ...@@ -64,6 +65,7 @@
<include src="settings/color_settings.html"> <include src="settings/color_settings.html">
<include src="settings/media_size_settings.html"> <include src="settings/media_size_settings.html">
<include src="settings/margin_settings.html"> <include src="settings/margin_settings.html">
<include src="settings/dpi_settings.html">
<include src="settings/other_options_settings.html"> <include src="settings/other_options_settings.html">
<include src="settings/advanced_options_settings.html"> <include src="settings/advanced_options_settings.html">
<include src="settings/more_settings.html"> <include src="settings/more_settings.html">
......
...@@ -150,6 +150,15 @@ cr.define('print_preview', function() { ...@@ -150,6 +150,15 @@ cr.define('print_preview', function() {
new print_preview.MarginSettings(this.printTicketStore_.marginsType); new print_preview.MarginSettings(this.printTicketStore_.marginsType);
this.addChild(this.marginSettings_); this.addChild(this.marginSettings_);
/**
* Component that renders the DPI settings.
* @type {!print_preview.DpiSettings}
* @private
*/
this.dpiSettings_ =
new print_preview.DpiSettings(this.printTicketStore_.dpi);
this.addChild(this.dpiSettings_);
/** /**
* Component that renders miscellaneous print options. * Component that renders miscellaneous print options.
* @type {!print_preview.OtherOptionsSettings} * @type {!print_preview.OtherOptionsSettings}
...@@ -189,6 +198,7 @@ cr.define('print_preview', function() { ...@@ -189,6 +198,7 @@ cr.define('print_preview', function() {
this.layoutSettings_, this.layoutSettings_,
this.marginSettings_, this.marginSettings_,
this.colorSettings_, this.colorSettings_,
this.dpiSettings_,
this.otherOptionsSettings_, this.otherOptionsSettings_,
this.advancedOptionsSettings_]; this.advancedOptionsSettings_];
/** /**
...@@ -461,6 +471,7 @@ cr.define('print_preview', function() { ...@@ -461,6 +471,7 @@ cr.define('print_preview', function() {
this.layoutSettings_.decorate($('layout-settings')); this.layoutSettings_.decorate($('layout-settings'));
this.colorSettings_.decorate($('color-settings')); this.colorSettings_.decorate($('color-settings'));
this.marginSettings_.decorate($('margin-settings')); this.marginSettings_.decorate($('margin-settings'));
this.dpiSettings_.decorate($('dpi-settings'));
this.otherOptionsSettings_.decorate($('other-options-settings')); this.otherOptionsSettings_.decorate($('other-options-settings'));
this.advancedOptionsSettings_.decorate($('advanced-options-settings')); this.advancedOptionsSettings_.decorate($('advanced-options-settings'));
this.advancedSettings_.decorate($('advanced-settings')); this.advancedSettings_.decorate($('advanced-settings'));
...@@ -487,6 +498,7 @@ cr.define('print_preview', function() { ...@@ -487,6 +498,7 @@ cr.define('print_preview', function() {
this.layoutSettings_.isEnabled = isEnabled; this.layoutSettings_.isEnabled = isEnabled;
this.colorSettings_.isEnabled = isEnabled; this.colorSettings_.isEnabled = isEnabled;
this.marginSettings_.isEnabled = isEnabled; this.marginSettings_.isEnabled = isEnabled;
this.dpiSettings_.isEnabled = isEnabled;
this.otherOptionsSettings_.isEnabled = isEnabled; this.otherOptionsSettings_.isEnabled = isEnabled;
this.advancedOptionsSettings_.isEnabled = isEnabled; this.advancedOptionsSettings_.isEnabled = isEnabled;
}, },
...@@ -1242,6 +1254,7 @@ cr.define('print_preview', function() { ...@@ -1242,6 +1254,7 @@ cr.define('print_preview', function() {
<include src="data/ticket_items/collate.js"> <include src="data/ticket_items/collate.js">
<include src="data/ticket_items/color.js"> <include src="data/ticket_items/color.js">
<include src="data/ticket_items/copies.js"> <include src="data/ticket_items/copies.js">
<include src="data/ticket_items/dpi.js">
<include src="data/ticket_items/duplex.js"> <include src="data/ticket_items/duplex.js">
<include src="data/ticket_items/header_footer.js"> <include src="data/ticket_items/header_footer.js">
<include src="data/ticket_items/media_size.js"> <include src="data/ticket_items/media_size.js">
...@@ -1261,8 +1274,10 @@ cr.define('print_preview', function() { ...@@ -1261,8 +1274,10 @@ cr.define('print_preview', function() {
<include src="metrics.js"> <include src="metrics.js">
<include src="settings/settings_section.js"> <include src="settings/settings_section.js">
<include src="settings/settings_section_select.js">
<include src="settings/page_settings.js"> <include src="settings/page_settings.js">
<include src="settings/copies_settings.js"> <include src="settings/copies_settings.js">
<include src="settings/dpi_settings.js">
<include src="settings/media_size_settings.js"> <include src="settings/media_size_settings.js">
<include src="settings/layout_settings.js"> <include src="settings/layout_settings.js">
<include src="settings/color_settings.js"> <include src="settings/color_settings.js">
......
/* Copyright 2014 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
#dpi-settings .dpi-settings-select {
height: 28px;
margin: 10px 0;
}
<div id="dpi-settings" class="two-column dpi-settings" hidden>
<div class="left-column">
<h1 id="dpi-label" i18n-content="dpiLabel"></h1>
</div>
<div class="right-column">
<select class="settings-select" aria-labelledby="dpi-label"></select>
</div>
</div>
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('print_preview', function() {
'use strict';
/**
* Encapsulates all settings and logic related to the DPI selection UI.
* @param {!print_preview.ticket_items.Dpi} ticketItem Used to read and write
* the DPI ticket item.
* @constructor
* @extends {print_preview.SettingsSectionSelect}
*/
function DpiSettings(ticketItem) {
print_preview.SettingsSectionSelect.call(this, ticketItem);
};
DpiSettings.prototype = {
__proto__: print_preview.SettingsSectionSelect.prototype,
/** @override */
getDefaultDisplayName_: function(option) {
return loadTimeData.getStringF('dpiItemLabel', option.horizontal_dpi);
}
};
// Export
return {
DpiSettings: DpiSettings
};
});
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
<h1 id="media-size-label" i18n-content="mediaSizeLabel"></h1> <h1 id="media-size-label" i18n-content="mediaSizeLabel"></h1>
</div> </div>
<div class="right-column"> <div class="right-column">
<select class="media-size-settings-select" <select class="settings-select" aria-labelledby="media-size-label"></select>
aria-labelledby="media-size-label">
</select>
</div> </div>
</div> </div>
...@@ -10,134 +10,18 @@ cr.define('print_preview', function() { ...@@ -10,134 +10,18 @@ cr.define('print_preview', function() {
* @param {!print_preview.ticket_items.MediaSize} ticketItem Used to read and * @param {!print_preview.ticket_items.MediaSize} ticketItem Used to read and
* write the media size ticket item. * write the media size ticket item.
* @constructor * @constructor
* @extends {print_preview.SettingsSection} * @extends {print_preview.SettingsSectionSelect}
*/ */
function MediaSizeSettings(ticketItem) { function MediaSizeSettings(ticketItem) {
print_preview.SettingsSection.call(this); print_preview.SettingsSectionSelect.call(this, ticketItem);
/** @private {!print_preview.ticket_items.MediaSize} */
this.ticketItem_ = ticketItem;
}; };
MediaSizeSettings.prototype = { MediaSizeSettings.prototype = {
__proto__: print_preview.SettingsSection.prototype, __proto__: print_preview.SettingsSectionSelect.prototype,
/** @override */
isAvailable: function() {
return this.ticketItem_.isCapabilityAvailable();
},
/** @override */ /** @override */
hasCollapsibleContent: function() { getDefaultDisplayName_: function(option) {
return this.isAvailable(); return option.name;
},
/** @override */
set isEnabled(isEnabled) {
this.select_.disabled = !isEnabled;
},
/** @override */
enterDocument: function() {
print_preview.SettingsSection.prototype.enterDocument.call(this);
this.tracker.add(this.select_, 'change', this.onSelectChange_.bind(this));
this.tracker.add(
this.ticketItem_,
print_preview.ticket_items.TicketItem.EventType.CHANGE,
this.onTicketItemChange_.bind(this));
},
/**
* @return {HTMLSelectElement} Select element containing media size options.
* @private
*/
get select_() {
return this.getElement().getElementsByClassName(
'media-size-settings-select')[0];
},
/**
* Makes sure the content of the select element matches the capabilities of
* the destination.
* @private
*/
updateSelect_: function() {
var select = this.select_;
if (!this.isAvailable()) {
select.innerHTML = '';
return;
}
// Should the select content be updated?
var sameContent =
this.ticketItem_.capability.option.length == select.length &&
this.ticketItem_.capability.option.every(function(option, index) {
return select.options[index].value == JSON.stringify(option);
});
var indexToSelect = select.selectedIndex;
if (!sameContent) {
select.innerHTML = '';
// TODO: Better heuristics for the display name and options grouping.
this.ticketItem_.capability.option.forEach(function(option, index) {
var selectOption = document.createElement('option');
var displayName = option.custom_display_name;
if (!displayName && option.custom_display_name_localized) {
var getLocaleToCompare =
/** @type {function(string, boolean=): string} */
(function(locale, opt_languageOnly) {
var code = opt_languageOnly ? locale.split('-')[0] : locale;
return code.toLowerCase();
});
var getItemForLocale = function(items, locale, languageOnly) {
locale = getLocaleToCompare(locale, languageOnly);
for (var i = 0; i < items.length; i++) {
if (getLocaleToCompare(items[i].locale) == locale)
return items[i].value;
}
return '';
};
var items = option.custom_display_name_localized;
displayName =
getItemForLocale(items, navigator.language, false) ||
getItemForLocale(items, navigator.language, true);
}
selectOption.text = displayName || option.name;
selectOption.value = JSON.stringify(option);
select.appendChild(selectOption);
if (option.is_default) {
indexToSelect = index;
}
});
}
// Try to select current ticket item.
var valueToSelect = JSON.stringify(this.ticketItem_.getValue());
for (var i = 0, option; option = select.options[i]; i++) {
if (option.value == valueToSelect) {
indexToSelect = i;
break;
}
}
select.selectedIndex = indexToSelect;
this.onSelectChange_();
},
/**
* Called when the select element is changed. Updates the print ticket.
* @private
*/
onSelectChange_: function() {
var select = this.select_;
var mediaSize = JSON.parse(select.options[select.selectedIndex].value);
this.ticketItem_.updateValue(mediaSize);
},
/**
* Called when the print ticket store changes. Selects the corresponding
* select option.
* @private
*/
onTicketItemChange_: function() {
this.updateSelect_();
this.updateUiStateInternal();
} }
}; };
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('print_preview', function() {
'use strict';
/**
* Base class for the printer option element visualizing the generic selection
* based option.
* @param {!print_preview.ticket_items.TicketItem} ticketItem Ticket item
* visualized by this component.
* @constructor
* @extends {print_preview.SettingsSection}
*/
function SettingsSectionSelect(ticketItem) {
print_preview.SettingsSection.call(this);
/** @private {!print_preview.ticket_items.TicketItem} */
this.ticketItem_ = ticketItem;
};
SettingsSectionSelect.prototype = {
__proto__: print_preview.SettingsSection.prototype,
/** @override */
isAvailable: function() {
return this.ticketItem_.isCapabilityAvailable();
},
/** @override */
hasCollapsibleContent: function() {
return this.isAvailable();
},
/** @override */
set isEnabled(isEnabled) {
this.select_.disabled = !isEnabled;
},
/** @override */
enterDocument: function() {
print_preview.SettingsSection.prototype.enterDocument.call(this);
this.tracker.add(assert(this.select_),
'change',
this.onSelectChange_.bind(this));
this.tracker.add(this.ticketItem_,
print_preview.ticket_items.TicketItem.EventType.CHANGE,
this.onTicketItemChange_.bind(this));
},
/**
* @return {HTMLSelectElement} Select element containing option items.
* @private
*/
get select_() {
return this.getElement().querySelector('.settings-select');
},
/**
* Makes sure the content of the select element matches the capabilities of
* the destination.
* @private
*/
updateSelect_: function() {
var select = this.select_;
if (!this.isAvailable()) {
select.innerHTML = '';
return;
}
// Should the select content be updated?
var sameContent =
this.ticketItem_.capability.option.length == select.length &&
this.ticketItem_.capability.option.every(function(option, index) {
return select.options[index].value == JSON.stringify(option);
});
var indexToSelect = select.selectedIndex;
if (!sameContent) {
select.innerHTML = '';
this.ticketItem_.capability.option.forEach(function(option, index) {
var selectOption = document.createElement('option');
var displayName = option.custom_display_name;
if (!displayName && option.custom_display_name_localized) {
var getLocaleToCompare =
/** @type {function(string, boolean=): string} */
(function(locale, opt_languageOnly) {
var code = opt_languageOnly ? locale.split('-')[0] : locale;
return code.toLowerCase();
});
var getItemForLocale = function(items, locale, languageOnly) {
locale = getLocaleToCompare(locale, languageOnly);
for (var i = 0; i < items.length; i++) {
if (getLocaleToCompare(items[i].locale) == locale)
return items[i].value;
}
return '';
};
var items = option.custom_display_name_localized;
displayName =
getItemForLocale(items, navigator.language, false) ||
getItemForLocale(items, navigator.language, true);
}
selectOption.text = displayName ||
this.getDefaultDisplayName_(option);
selectOption.value = JSON.stringify(option);
select.appendChild(selectOption);
if (option.is_default)
indexToSelect = index;
});
}
// Try to select current ticket item.
var valueToSelect = JSON.stringify(this.ticketItem_.getValue());
for (var i = 0, option; option = select.options[i]; i++) {
if (option.value == valueToSelect) {
indexToSelect = i;
break;
}
}
select.selectedIndex = indexToSelect;
this.onSelectChange_();
},
/**
* @param {!Object} option Option to get the default display name for.
* @return {string} Default option display name.
* @private
*/
getDefaultDisplayName_: function(option) {
throw Error('Abstract method not overridden');
},
/**
* Called when the select element is changed. Updates the print ticket.
* @private
*/
onSelectChange_: function() {
var select = this.select_;
this.ticketItem_.updateValue(
JSON.parse(select.options[select.selectedIndex].value));
},
/**
* Called when the print ticket store changes. Selects the corresponding
* select option.
* @private
*/
onTicketItemChange_: function() {
this.updateSelect_();
this.updateUiStateInternal();
}
};
// Export
return {
SettingsSectionSelect: SettingsSectionSelect
};
});
...@@ -264,6 +264,8 @@ content::WebUIDataSource* CreatePrintPreviewUISource() { ...@@ -264,6 +264,8 @@ content::WebUIDataSource* CreatePrintPreviewUISource() {
source->AddLocalizedString("right", IDS_PRINT_PREVIEW_RIGHT_MARGIN_LABEL); source->AddLocalizedString("right", IDS_PRINT_PREVIEW_RIGHT_MARGIN_LABEL);
source->AddLocalizedString("mediaSizeLabel", source->AddLocalizedString("mediaSizeLabel",
IDS_PRINT_PREVIEW_MEDIA_SIZE_LABEL); IDS_PRINT_PREVIEW_MEDIA_SIZE_LABEL);
source->AddLocalizedString("dpiLabel", IDS_PRINT_PREVIEW_DPI_LABEL);
source->AddLocalizedString("dpiItemLabel", IDS_PRINT_PREVIEW_DPI_ITEM_LABEL);
source->AddLocalizedString("destinationSearchTitle", source->AddLocalizedString("destinationSearchTitle",
IDS_PRINT_PREVIEW_DESTINATION_SEARCH_TITLE); IDS_PRINT_PREVIEW_DESTINATION_SEARCH_TITLE);
source->AddLocalizedString("accountSelectTitle", source->AddLocalizedString("accountSelectTitle",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment