From fff607e1a481b8a4dc8287d5c2ee13c5e9d6681d Mon Sep 17 00:00:00 2001 From: Nohemi Fernandez Date: Tue, 26 Jul 2011 13:33:57 -0400 Subject: [PATCH] Updated on-screen-keyboard patches (squashed) --- data/org.gnome.shell.gschema.xml.in | 33 ++ data/theme/gnome-shell.css | 54 +++ js/Makefile.am | 1 + js/ui/chrome.js | 4 + js/ui/keyboard.js | 609 ++++++++++++++++++++++++++++ js/ui/layout.js | 99 +++++ js/ui/main.js | 113 +++--- js/ui/messageTray.js | 53 ++- js/ui/notificationDaemon.js | 6 +- js/ui/overview.js | 40 +- js/ui/panel.js | 8 +- js/ui/status/accessibility.js | 8 +- js/ui/statusIconDispatcher.js | 7 +- js/ui/workspacesView.js | 1 - tools/build/gnome-shell.modules | 3 +- 15 files changed, 929 insertions(+), 110 deletions(-) create mode 100644 js/ui/keyboard.js diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in index 050c4130b..4822f3a72 100644 --- a/data/org.gnome.shell.gschema.xml.in +++ b/data/org.gnome.shell.gschema.xml.in @@ -62,6 +62,7 @@ + + + + false + <_summary>Show the onscreen keyboard + <_description> + If true, display onscreen keyboard. + + + + 'touch' + <_summary>Which keyboard to use + <_description> + The type of keyboard to use. + + + + false + <_summary>Enable keyboard dragging + <_description> + If true, enable keyboard dragging. + + + + false + <_summary>Enable floating keyboard + <_description> + If true, enable floating keyboard. + + + + diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index edbd02331..fe276311e 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -1839,3 +1839,57 @@ StTooltip StLabel { .magnifier-zoom-region.full-screen { border-width: 0px; } + +/* On-screen Keyboard */ + +#keyboard { + background: rgba(0,0,0,0.8); +} + +.keyboard-layout { + spacing: 10px; + padding: 10px; +} + +.keyboard-row { + spacing: 15px; +} + +.keyboard-key { + min-height: 30px; + min-width: 30px; + background-gradient-start: rgba(255,245,245,0.4); + background-gradient-end: rgba(105,105,105,0.1); + background-gradient-direction: vertical; + font-size: 14pt; + font-weight: bold; + border-radius: 10px; + border: 2px solid #a0a0a0; + color: white; +} + +.keyboard-key:grayed { + color: #808080; + border-color: #808080; +} + +.keyboard-key:checked, +.keyboard-key:hover { + background: #303030; + border: 3px solid white; +} + +.keyboard-key:active { + background: #808080; +} + +.keyboard-subkeys { + color: white; + padding: 5px; + -arrow-border-radius: 10px; + -arrow-background-color: #090909; + -arrow-border-width: 2px; + -arrow-border-color: white; + -arrow-base: 20px; + -arrow-rise: 10px; +} diff --git a/js/Makefile.am b/js/Makefile.am index fadab2d1c..50303f5ce 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -30,6 +30,7 @@ nobase_dist_js_DATA = \ ui/environment.js \ ui/extensionSystem.js \ ui/iconGrid.js \ + ui/keyboard.js \ ui/layout.js \ ui/lightbox.js \ ui/link.js \ diff --git a/js/ui/chrome.js b/js/ui/chrome.js index 5dd21e5e8..714ba0550 100644 --- a/js/ui/chrome.js +++ b/js/ui/chrome.js @@ -36,6 +36,10 @@ Chrome.prototype = { this._trackedActors = []; + Main.connect('initialized', Lang.bind(this, this._finishInit)); + }, + + _finishInit: function() { Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._relayout)); global.screen.connect('restacked', diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js new file mode 100644 index 000000000..60d8c09d7 --- /dev/null +++ b/js/ui/keyboard.js @@ -0,0 +1,609 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Caribou = imports.gi.Caribou; +const Clutter = imports.gi.Clutter; +const DBus = imports.dbus; +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const BoxPointer = imports.ui.boxpointer; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; + +const KEYBOARD_SCHEMA = 'org.gnome.shell.keyboard'; +const SHOW_KEYBOARD = 'show-keyboard'; +const KEYBOARD_TYPE = 'keyboard-type'; +const ENABLE_DRAGGABLE = 'enable-drag'; +const ENABLE_FLOAT = 'enable-float'; + +// Key constants taken from Antler +const PRETTY_KEYS = { + 'BackSpace': '\u232b', + 'space': ' ', + 'Return': '\u23ce', + 'Caribou_Prefs': '\u2328', + 'Caribou_ShiftUp': '\u2b06', + 'Caribou_ShiftDown': '\u2b07', + 'Caribou_Emoticons': '\u263a', + 'Caribou_Symbols': '123', + 'Caribou_Symbols_More': '{#*', + 'Caribou_Alpha': 'Abc', + 'Tab': 'Tab', + 'Escape': 'Esc', + 'Control_L': 'Ctrl', + 'Alt_L': 'Alt' +}; + +const CaribouKeyboardIface = { + name: 'org.gnome.Caribou.Keyboard', + methods: [ { name: 'Show', + inSignature: '', + outSignature: '' + }, + { name: 'Hide', + inSignature: '', + outSignature: '' + }, + { name: 'SetCursorLocation', + inSignature: 'iiii', + outSignature: '' + }, + { name: 'SetEntryLocation', + inSignature: 'iiii', + outSignature: '' + } ], + properties: [ { name: 'Name', + signature: 's', + access: 'read' } ] +}; + +function Key() { + this._init.apply(this, arguments); +} + +Key.prototype = { + _init : function(key, key_width, key_height) { + this._key = key; + + this._width = key_width; + this._height = key_height; + + this.actor = this._getKey(); + + this._extended_keys = this._key.get_extended_keys(); + this._extended_keyboard = {}; + + if (this._key.name == "Control_L" || this._key.name == "Alt_L") + this._key.latch = true; + + this._key.connect('key-pressed', Lang.bind(this, function () + { this.actor.checked = true })); + this._key.connect('key-released', Lang.bind(this, function () + { this.actor.checked = false; })); + + if (this._extended_keys.length > 0) { + this._grabbed = false; + this._eventCaptureId = 0; + this._key.connect('notify::show-subkeys', Lang.bind(this, this._onShowSubkeysChanged)); + this._boxPointer = new BoxPointer.BoxPointer(St.Side.BOTTOM, + { x_fill: true, + y_fill: true, + x_align: St.Align.START }); + // Adds style to existing keyboard style to avoid repetition + this._boxPointer.actor.add_style_class_name('keyboard-subkeys'); + this._getExtendedKeys(); + this.actor._extended_keys = this._extended_keyboard; + this._boxPointer.actor.hide(); + Main.chrome.addActor(this._boxPointer.actor, { visibleInFullscreen: true, + affectsStruts: false }); + } + }, + + _getKey: function () { + let label = this._key.name; + + if (label.length > 1) { + let pretty = PRETTY_KEYS[label]; + if (pretty) + label = pretty; + else + label = this._getUnichar(this._key); + } + + label = GLib.markup_escape_text(label, -1); + let button = new St.Button ({ label: label, style_class: 'keyboard-key' }); + + button.width = this._width; + button.key_width = this._key.width; + button.height = this._height; + button.draggable = false; + button.connect('button-press-event', Lang.bind(this, function () { this._key.press(); })); + button.connect('button-release-event', Lang.bind(this, function () { this._key.release(); })); + + return button; + }, + + _getUnichar: function(key) { + let keyval = key.keyval; + let unichar = Gdk.keyval_to_unicode(keyval); + if (unichar) { + return String.fromCharCode(unichar); + } else { + return key.name; + } + }, + + _getExtendedKeys: function () { + this._extended_keyboard = new St.BoxLayout({ style_class: 'keyboard-layout', + vertical: false }); + for (let i = 0; i < this._extended_keys.length; ++i) { + let extended_key = this._extended_keys[i]; + let label = this._getUnichar(extended_key); + let key = new St.Button({ label: label, style_class: 'keyboard-key' }); + key.extended_key = extended_key; + key.width = this._width; + key.height = this._height; + key.draggable = false; + key.connect('button-press-event', Lang.bind(this, function () { extended_key.press(); })); + key.connect('button-release-event', Lang.bind(this, function () { extended_key.release(); })); + this._extended_keyboard.add(key); + } + this._boxPointer.bin.add_actor(this._extended_keyboard); + }, + + _onEventCapture: function (actor, event) { + let source = event.get_source(); + if (event.type() == Clutter.EventType.BUTTON_PRESS || + (event.type() == Clutter.EventType.BUTTON_RELEASE && source.draggable)) { + if (this._extended_keyboard.contains(source)) { + if (source.draggable) { + source.extended_key.press(); + source.extended_key.release(); + } + this._ungrab(); + return false; + } + this._boxPointer.actor.hide(); + this._ungrab(); + return true; + } + return false; + }, + + _ungrab: function () { + global.stage.disconnect(this._eventCaptureId); + this._eventCaptureId = 0; + this._grabbed = false; + Main.popModal(this.actor); + }, + + _onShowSubkeysChanged: function () { + if (this._key.show_subkeys) { + this.actor.fake_release(); + this._boxPointer.actor.raise_top(); + this._boxPointer.setPosition(this.actor, 5, 0.5); + this._boxPointer.show(true); + this.actor.set_hover(false); + if (!this._grabbed) { + Main.pushModal(this.actor); + this._eventCaptureId = global.stage.connect('captured-event', Lang.bind(this, this._onEventCapture)); + this._grabbed = true; + } + this._key.release(); + } else { + if (this._grabbed) + this._ungrab(); + this._boxPointer.hide(true); + } + } +}; + +function Keyboard() { + this._init.apply(this, arguments); +} + +Keyboard.prototype = { + _init: function () { + DBus.session.exportObject('/org/gnome/Caribou/Keyboard', this); + DBus.session.acquire_name('org.gnome.Caribou.Keyboard', 0, null, null); + + this.actor = new St.BoxLayout({ name: 'keyboard', vertical: true, reactive: true }); + + this._keyboardSettings = new Gio.Settings({ schema: KEYBOARD_SCHEMA }); + this._keyboardSettings.connect('changed', Lang.bind(this, this._display)); + + this._setupKeyboard(); + + Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._redraw)); + + Main.layoutManager.bottomBox.add_actor(this.actor); + }, + + init: function () { + this._display(); + }, + + _setupKeyboard: function() { + if (this._keyboardNotifyId) + this._keyboard.disconnect(this._keyboardNotifyId); + let children = this.actor.get_children(); + for (let i = 0; i < children.length; i++) + children[i].destroy(); + + this._keyboard = new Caribou.KeyboardModel({ keyboard_type: this._keyboardSettings.get_string(KEYBOARD_TYPE) }); + this._groups = {}; + this._current_page = null; + + // Initialize keyboard key measurements + this._numOfHorizKeys = 0; + this._numOfVertKeys = 0; + + this._floatId = 0; + + this._addKeys(); + + this._keyboardNotifyId = this._keyboard.connect('notify::active-group', Lang.bind(this, this._onGroupChanged)); + }, + + _display: function () { + if (this._keyboard.keyboard_type != this._keyboardSettings.get_string(KEYBOARD_TYPE)) + this._setupKeyboard(); + + this._showKeyboard = this._keyboardSettings.get_boolean(SHOW_KEYBOARD); + this._draggable = this._keyboardSettings.get_boolean(ENABLE_DRAGGABLE); + this._floating = this._keyboardSettings.get_boolean(ENABLE_FLOAT); + if (this._floating) { + this._floatId = this.actor.connect('button-press-event', Lang.bind(this, this._startDragging)); + this._dragging = false; + } + else + this.actor.disconnect(this._floatId); + if (this._showKeyboard) + this.show(); + else { + this.hide(); + this.destroySource(); + } + }, + + _startDragging: function (actor, event) { + if (this._dragging) // don't allow two drags at the same time + return; + this._dragging = true; + this._preDragStageMode = global.stage_input_mode; + + Clutter.grab_pointer(this.actor); + global.set_stage_input_mode(Shell.StageInputMode.FULLSCREEN); + + this._releaseId = this.actor.connect('button-release-event', Lang.bind(this, this._endDragging)); + this._motionId = this.actor.connect('motion-event', Lang.bind(this, this._motionEvent)); + [this._dragStartX, this._dragStartY] = event.get_coords(); + [this._currentX, this._currentY] = this.actor.get_position(); + }, + + _endDragging: function () { + if (this._dragging) { + this.actor.disconnect(this._releaseId); + this.actor.disconnect(this._motionId); + + Clutter.ungrab_pointer(); + global.set_stage_input_mode(this._preDragStageMode); + global.unset_cursor(); + this._dragging = false; + } + return true; + }, + + _motionEvent: function(actor, event) { + let absX, absY; + [absX, absY] = event.get_coords(); + global.set_cursor(Shell.Cursor.DND_IN_DRAG); + this._moveHandle(absX, absY); + return true; + }, + + _moveHandle: function (stageX, stageY) { + let x, y; + x = stageX - this._dragStartX + this._currentX; + y = stageY - this._dragStartY + this._currentY; + this.actor.set_position(x,y); + + }, + + _addKeys: function () { + let groups = this._keyboard.get_groups(); + for (let i = 0; i < groups.length; ++i) { + let gname = groups[i]; + let group = this._keyboard.get_group(gname); + group.connect('notify::active-level', Lang.bind(this, this._onLevelChanged)); + let layers = {}; + let levels = group.get_levels(); + for (let j = 0; j < levels.length; ++j) { + let lname = levels[j]; + let level = group.get_level(lname); + let layout = new St.BoxLayout({ style_class: 'keyboard-layout', + vertical: true }); + this._loadRows(level, layout); + layers[lname] = layout; + this.actor.add(layout, { x_fill: false }); + + layout.hide(); + } + this._groups[gname] = layers; + } + + this._setActiveLayer(); + }, + + _getTrayIcon: function () { + let trayButton = new St.Button ({ label: "tray", style_class: 'keyboard-key' }); + trayButton.key_width = 1; + trayButton.connect('button-press-event', Lang.bind(this, function () { + Main.layoutManager.updateForTray(); + })); + + Main.overview.connect('showing', Lang.bind(this, function () { + trayButton.reactive = false; + trayButton.add_style_pseudo_class('grayed'); + })); + Main.overview.connect('hiding', Lang.bind(this, function () { + trayButton.reactive = true; + trayButton.remove_style_pseudo_class('grayed'); + })); + + return trayButton; + }, + + _addRows : function (keys, layout) { + let keyboard_row = new St.BoxLayout(); + for (let i = 0; i < keys.length; ++i) { + let children = keys[i].get_children(); + let right_box = new St.BoxLayout({ style_class: 'keyboard-row' }); + let left_box = new St.BoxLayout({ style_class: 'keyboard-row' }); + for (let j = 0; j < children.length; ++j) { + if (this._numOfHorizKeys == 0) + this._numOfHorizKeys = children.length; + let key = children[j]; + let button = new Key(key, 0, 0); + + if (key.align == 'right') + right_box.add(button.actor); + else + left_box.add(button.actor); + if (key.name == "Caribou_Prefs") { + key.connect('key-released', Lang.bind(this, this._onPrefsClick)); + + // Add new key for hiding message tray + right_box.add(this._getTrayIcon()); + } + } + keyboard_row.add(left_box, { expand: true, x_fill: false, x_align: St.Align.START }); + keyboard_row.add(right_box, { expand: true, x_fill: false, x_align: St.Align.END }); + } + layout.add(keyboard_row); + }, + + _manageTray: function () { + this.createSource(); + }, + + _onPrefsClick: function () { + this.hide(); + this._manageTray(); + }, + + _loadRows : function (level, layout) { + let rows = level.get_rows(); + for (let i = 0; i < rows.length; ++i) { + let row = rows[i]; + if (this._numOfVertKeys == 0) + this._numOfVertKeys = rows.length; + this._addRows(row.get_columns(), layout); + } + + }, + + _redraw: function () { + let monitor = Main.layoutManager.bottomMonitor; + let maxHeight = monitor.height / 3; + this.actor.width = monitor.width; + + let layout = this._current_page; + let verticalSpacing = layout.get_theme_node().get_length('spacing'); + let padding = layout.get_theme_node().get_length('padding'); + + let box = layout.get_children()[0].get_children()[0]; + let horizontalSpacing = box.get_theme_node().get_length('spacing'); + let allHorizontalSpacing = (this._numOfHorizKeys - 1) * horizontalSpacing; + let keyWidth = Math.floor((this.actor.width - allHorizontalSpacing - 2 * padding) / this._numOfHorizKeys); + + let allVerticalSpacing = (this._numOfVertKeys - 1) * verticalSpacing; + let keyHeight = Math.floor((maxHeight - allVerticalSpacing - 2 * padding) / this._numOfVertKeys); + + let keySize = Math.min(keyWidth, keyHeight); + this.actor.height = keySize * this._numOfVertKeys + allVerticalSpacing + 2 * padding; + + let rows = this._current_page.get_children(); + for (let i = 0; i < rows.length; ++i) { + let keyboard_row = rows[i]; + let boxes = keyboard_row.get_children(); + for (let j = 0; j < boxes.length; ++j) { + let keys = boxes[j].get_children(); + for (let k = 0; k < keys.length; ++k) { + let child = keys[k]; + child.width = keySize * child.key_width; + child.height = keySize; + child.draggable = this._draggable; + if (child._extended_keys) { + let extended_keys = child._extended_keys.get_children(); + for (let n = 0; n < extended_keys.length; ++n) { + let extended_key = extended_keys[n]; + extended_key.width = keySize; + extended_key.height = keySize; + extended_key.draggable = this._draggable; + } + } + } + } + } + }, + + _onLevelChanged: function () { + this._setActiveLayer(); + this._redraw(); + }, + + _onGroupChanged: function () { + this._setActiveLayer(); + this._redraw(); + }, + + _setActiveLayer: function () { + let active_group_name = this._keyboard.active_group; + let active_group = this._keyboard.get_group(active_group_name); + let active_level = active_group.active_level; + let layers = this._groups[active_group_name]; + + if (this._current_page != null) { + this._current_page.hide(); + } + + this._current_page = layers[active_level]; + this._current_page.show(); + }, + + createSource: function () { + if (this._source == null) { + this._source = new KeyboardSource(this); + Main.messageTray.add(this._source); + } + }, + + destroySource: function () { + if (this._source) { + this._source.destroy(); + this._source = null; + } + }, + + show: function () { + this._redraw(); + Main.layoutManager.showKeyboard(); + }, + + hide: function () { + Main.layoutManager.hideKeyboard(); + }, + + // Window placement method + _updatePosition: function (x, y) { + let primary = Main.layoutManager.primaryMonitor; + x -= this.actor.width / 2; + // Determines bottom/top centered + if (y <= primary.height / 2) + y += this.actor.height / 2; + else + y -= 3 * this.actor.height / 2; + + // Accounting for monitor boundaries + if (x < primary.x) + x = primary.x; + if (x + this.actor.width > primary.width) + x = primary.width - this.actor.width; + + this.actor.set_position(x, y); + }, + + _moveTemporarily: function () { + this._currentWindow = global.screen.get_display().focus_window; + let rect = this._currentWindow.get_outer_rect(); + this._currentWindow.x = rect.x; + this._currentWindow.y = rect.y; + + let newX = this._currentWindow.x; + let newY = 3 * this.actor.height / 2; + this._currentWindow.move_frame(true, newX, newY); + }, + + _setLocation: function (x, y) { + if (this._floating) + this._updatePosition(x, y); + else { + if (y >= 2 * this.actor.height) + this._moveTemporarily(); + } + }, + + // D-Bus methods + Show: function() { + this.destroySource(); + this.show(); + }, + + Hide: function() { + if (this._currentWindow) { + this._currentWindow.move_frame(true, this._currentWindow.x, this._currentWindow.y); + this._currentWindow = null; + } + this.hide(); + this._manageTray(); + }, + + SetCursorLocation: function(x, y, w, h) { + this._setLocation(x, y); + }, + + SetEntryLocation: function(x, y, w, h) { + this._setLocation(x, y); + }, + + get Name() { + return 'gnome-shell'; + } +}; +DBus.conformExport(Keyboard.prototype, CaribouKeyboardIface); + +function KeyboardSource() { + this._init.apply(this, arguments); +} + +KeyboardSource.prototype = { + __proto__: MessageTray.Source.prototype, + + _init: function(keyboard) { + this._keyboard = keyboard; + MessageTray.Source.prototype._init.call(this, _("Keyboard")); + + this._setSummaryIcon(this.createNotificationIcon()); + }, + + createNotificationIcon: function() { + return new St.Icon({ icon_name: 'input-keyboard', + icon_type: St.IconType.SYMBOLIC, + icon_size: this.ICON_SIZE }); + }, + + handleSummaryClick: function() { + let event = Clutter.get_current_event(); + if (event.type() != Clutter.EventType.BUTTON_RELEASE) + return false; + + if (event.get_button() != 1) + return false; + + this.open(); + return true; + }, + + open: function() { + this._keyboard.show(); + this._keyboard.destroySource(); + } +}; diff --git a/js/ui/layout.js b/js/ui/layout.js index e88450718..afa17f273 100644 --- a/js/ui/layout.js +++ b/js/ui/layout.js @@ -6,8 +6,17 @@ const Signals = imports.signals; const St = imports.gi.St; const Main = imports.ui.main; +const Meta = imports.gi.Meta; +const Panel = imports.ui.panel; const Tweener = imports.ui.tweener; +const State = { + HIDDEN: 0, + SHOWING: 1, + SHOWN: 2, + HIDING: 3 +}; + const HOT_CORNER_ACTIVATION_TIMEOUT = 0.5; function LayoutManager() { @@ -21,8 +30,95 @@ LayoutManager.prototype = { this.primaryMonitor = null; this.primaryIndex = -1; this._hotCorners = []; + this.bottomBox = new Clutter.Group(); + this.topBox = new Clutter.Group({ clip_to_allocation: true }); + this.bottomBox.add_actor(this.topBox); + global.screen.connect('monitors-changed', Lang.bind(this, this._monitorsChanged)); this._updateMonitors(); + + Main.connect('layout-initialized', Lang.bind(this, this._initChrome)); + + Main.connect('main-ui-initialized', Lang.bind(this, this._finishInit)); + }, + + _initChrome: function() { + Main.chrome.addActor(this.bottomBox, { affectsStruts: false, + visibleInFullscreen: true }); + }, + + // _updateHotCorners needs access to Main.panel + _finishInit: function() { + this._updateHotCorners(); + + this.topBox.height = Main.messageTray.actor.height; + this.topBox.y = - Main.messageTray.actor.height; + + this._keyboardState = Main.keyboard.actor.visible ? State.SHOWN : State.HIDDEN; + this._traySummoned = true; + + Main.keyboard.actor.connect('allocation-changed', Lang.bind(this, this._reposition)); + }, + + _reposition: function () { + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function () { this._updateForKeyboard(); })); + }, + + _updateForKeyboard: function () { + if (Tweener.isTweening(this.bottomBox)) + return; + + this.topBox.y = - Main.messageTray.actor.height; + let bottom = this.bottomMonitor.y + this.bottomMonitor.height; + if (this._keyboardState == State.SHOWN) + this.bottomBox.y = bottom - Main.keyboard.actor.height; + else { + this.bottomBox.y = bottom; + this._keyboardState = State.HIDDEN; + } + + }, + + updateForTray: function () { + if (this._keyboardState == State.SHOWN) { + if (this._traySummoned) { + Main.messageTray.lock(); + this._traySummoned = false; + } + else { + Main.messageTray.unlock(); + this._traySummoned = true; + } + } + else { + Main.messageTray.unlock(); + this._traySummoned = false; + } + }, + + showKeyboard: function () { + let bottom = this.bottomMonitor.y + this.bottomMonitor.height; + // Keyboard height cannot be found directly since the first + // call to this method may be when Keyboard.Keyboard() has + // not returned; therefore the keyboard would be null + Tweener.addTween(this.bottomBox, + { y: bottom - Main.keyboard.actor.height, + time: 0.5, + transition: 'easeOutQuad', + }); + this._keyboardState = State.SHOWN; + this.updateForTray(); + }, + + hideKeyboard: function () { + let bottom = this.bottomMonitor.y + this.bottomMonitor.height; + Tweener.addTween(this.bottomBox, + { y: bottom, + time: 0.5, + transition: 'easeOutQuad' + }); + this._keyboardState = State.HIDDEN; + this.updateForTray(); }, // This is called by Main after everything else is constructed; @@ -57,6 +153,9 @@ LayoutManager.prototype = { } this.primaryMonitor = this.monitors[this.primaryIndex]; this.bottomMonitor = this.monitors[this.bottomIndex]; + + this.bottomBox.set_position(0, this.bottomMonitor.y + this.bottomMonitor.height); + this.bottomBox.width = this.bottomMonitor.width; }, _updateHotCorners: function() { diff --git a/js/ui/main.js b/js/ui/main.js index 47f6af2cd..8564536cd 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -10,6 +10,7 @@ const Lang = imports.lang; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; +const Signals = imports.signals; const St = imports.gi.St; const AutomountManager = imports.ui.automountManager; @@ -20,6 +21,7 @@ const EndSessionDialog = imports.ui.endSessionDialog; const PolkitAuthenticationAgent = imports.ui.polkitAuthenticationAgent; const Environment = imports.ui.environment; const ExtensionSystem = imports.ui.extensionSystem; +const Keyboard = imports.ui.keyboard; const MessageTray = imports.ui.messageTray; const Overview = imports.ui.overview; const Panel = imports.ui.panel; @@ -45,32 +47,34 @@ let automountManager = null; let autorunManager = null; let chrome = null; let panel = null; -let hotCorners = []; let placesManager = null; let overview = null; -let runDialog = null; -let lookingGlass = null; let wm = null; let messageTray = null; let notificationDaemon = null; let windowAttentionHandler = null; let telepathyClient = null; let ctrlAltTabManager = null; -let recorder = null; let shellDBusService = null; -let modalCount = 0; -let modalActorFocusStack = []; let uiGroup = null; let magnifier = null; let xdndHandler = null; let statusIconDispatcher = null; +let keyboard = null; let layoutManager = null; + +let _runDialog = null; +let lookingGlass = null; +let _recorder = null; +let _modalCount = 0; +let _modalActorFocusStack = []; let _errorLogStack = []; let _startDate; let _defaultCssStylesheet = null; let _cssStylesheet = null; -let background = null; +const Main = this; +Signals.addSignalMethods(Main) function start() { // Monkey patch utility functions into the global proxy; @@ -132,50 +136,62 @@ function start() { global.overlay_group.reparent(uiGroup); global.stage.add_actor(uiGroup); + // Initialize JS modules. We do this in several steps, so that + // less-fundamental modules can depend on more-fundamental ones. + + // Overall layout management layoutManager = new Layout.LayoutManager(); - placesManager = new PlaceDisplay.PlacesManager(); - xdndHandler = new XdndHandler.XdndHandler(); - ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager(); - overview = new Overview.Overview(); chrome = new Chrome.Chrome(); - magnifier = new Magnifier.Magnifier(); - statusIconDispatcher = new StatusIconDispatcher.StatusIconDispatcher(); + ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager(); + Main.emit('layout-initialized'); + + // Major UI elements; initialize overview first since both panel + // and messageTray connect to its signals + overview = new Overview.Overview(); panel = new Panel.Panel(); - wm = new WindowManager.WindowManager(); messageTray = new MessageTray.MessageTray(); + keyboard = new Keyboard.Keyboard(); + Main.emit('main-ui-initialized'); + + // Now the rest of the JS modules (arbitrarily in alphabetical + // order). + keyboard.init(); + magnifier = new Magnifier.Magnifier(); notificationDaemon = new NotificationDaemon.NotificationDaemon(); - windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler(); + placesManager = new PlaceDisplay.PlacesManager(); + statusIconDispatcher = new StatusIconDispatcher.StatusIconDispatcher(); telepathyClient = new TelepathyClient.Client(); + windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler(); + wm = new WindowManager.WindowManager(); + xdndHandler = new XdndHandler.XdndHandler(); automountManager = new AutomountManager.AutomountManager(); autorunManager = new AutorunManager.AutorunManager(); - layoutManager.init(); - overview.init(); - statusIconDispatcher.start(messageTray.actor); + Main.emit('initialized'); _startDate = new Date(); let recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' }); global.screen.connect('toggle-recording', function() { - if (recorder == null) { - recorder = new Shell.Recorder({ stage: global.stage }); + if (_recorder == null) { + _recorder = new Shell.Recorder({ stage: global.stage }); } - if (recorder.is_recording()) { - recorder.pause(); + if (_recorder.is_recording()) { + _recorder.pause(); } else { // read the parameters from GSettings always in case they have changed - recorder.set_framerate(recorderSettings.get_int('framerate')); - recorder.set_filename('shell-%d%u-%c.' + recorderSettings.get_string('file-extension')); + _recorder.set_framerate(recorderSettings.get_int('framerate')); + _recorder.set_filename('shell-%d%u-%c.' + recorderSettings.get_string('file-extension')); let pipeline = recorderSettings.get_string('pipeline'); if (!pipeline.match(/^\s*$/)) - recorder.set_pipeline(pipeline); + _recorder.set_pipeline(pipeline); else - recorder.set_pipeline(null); + _recorder.set_pipeline(null); - recorder.record(); + _recorder.record(); } }); @@ -191,6 +207,7 @@ function start() { ExtensionSystem.init(); ExtensionSystem.loadExtensions(); + // Initialize the panel status area now that extensions are loaded panel.startStatusArea(); panel.startupAnimation(); @@ -515,7 +532,7 @@ function getWindowActorsForWorkspace(workspaceIndex) { // all key events will be delivered to the stage, so ::captured-event // on the stage can be used for global keybindings.) function _globalKeyPressHandler(actor, event) { - if (modalCount == 0) + if (_modalCount == 0) return false; if (event.type() != Clutter.EventType.KEY_PRESS) return false; @@ -540,7 +557,7 @@ function _globalKeyPressHandler(actor, event) { // Other bindings are only available when the overview is up and // no modal dialog is present. - if (!overview.visible || modalCount > 1) + if (!overview.visible || _modalCount > 1) return false; // This isn't a Meta.KeyBindingAction yet @@ -581,8 +598,8 @@ function _globalKeyPressHandler(actor, event) { } function _findModal(actor) { - for (let i = 0; i < modalActorFocusStack.length; i++) { - if (modalActorFocusStack[i].actor == actor) + for (let i = 0; i < _modalActorFocusStack.length; i++) { + if (_modalActorFocusStack[i].actor == actor) return i; } return -1; @@ -612,7 +629,7 @@ function pushModal(actor, timestamp) { if (timestamp == undefined) timestamp = global.get_current_time(); - if (modalCount == 0) { + if (_modalCount == 0) { if (!global.begin_modal(timestamp)) { log('pushModal: invocation of begin_modal failed'); return false; @@ -621,11 +638,11 @@ function pushModal(actor, timestamp) { global.set_stage_input_mode(Shell.StageInputMode.FULLSCREEN); - modalCount += 1; + _modalCount += 1; let actorDestroyId = actor.connect('destroy', function() { let index = _findModal(actor); if (index >= 0) - modalActorFocusStack.splice(index, 1); + _modalActorFocusStack.splice(index, 1); }); let curFocus = global.stage.get_key_focus(); let curFocusDestroyId; @@ -633,10 +650,10 @@ function pushModal(actor, timestamp) { curFocusDestroyId = curFocus.connect('destroy', function() { let index = _findModal(actor); if (index >= 0) - modalActorFocusStack[index].actor = null; + _modalActorFocusStack[index].actor = null; }); } - modalActorFocusStack.push({ actor: actor, + _modalActorFocusStack.push({ actor: actor, focus: curFocus, destroyId: actorDestroyId, focusDestroyId: curFocusDestroyId }); @@ -671,28 +688,28 @@ function popModal(actor, timestamp) { throw new Error('incorrect pop'); } - modalCount -= 1; + _modalCount -= 1; - let record = modalActorFocusStack[focusIndex]; + let record = _modalActorFocusStack[focusIndex]; record.actor.disconnect(record.destroyId); - if (focusIndex == modalActorFocusStack.length - 1) { + if (focusIndex == _modalActorFocusStack.length - 1) { if (record.focus) record.focus.disconnect(record.focusDestroyId); global.stage.set_key_focus(record.focus); } else { - let t = modalActorFocusStack[modalActorFocusStack.length - 1]; + let t = _modalActorFocusStack[_modalActorFocusStack.length - 1]; if (t.focus) t.focus.disconnect(t.focusDestroyId); // Remove from the middle, shift the focus chain up - for (let i = modalActorFocusStack.length - 1; i > focusIndex; i--) { - modalActorFocusStack[i].focus = modalActorFocusStack[i - 1].focus; - modalActorFocusStack[i].focusDestroyId = modalActorFocusStack[i - 1].focusDestroyId; + for (let i = _modalActorFocusStack.length - 1; i > focusIndex; i--) { + _modalActorFocusStack[i].focus = _modalActorFocusStack[i - 1].focus; + _modalActorFocusStack[i].focusDestroyId = _modalActorFocusStack[i - 1].focusDestroyId; } } - modalActorFocusStack.splice(focusIndex, 1); + _modalActorFocusStack.splice(focusIndex, 1); - if (modalCount > 0) + if (_modalCount > 0) return; global.end_modal(timestamp); @@ -708,10 +725,10 @@ function createLookingGlass() { } function getRunDialog() { - if (runDialog == null) { - runDialog = new RunDialog.RunDialog(); + if (_runDialog == null) { + _runDialog = new RunDialog.RunDialog(); } - return runDialog; + return _runDialog; } /** diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 41475c106..8bca949bc 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -1301,9 +1301,9 @@ MessageTray.prototype = { this._focusGrabber.connect('focus-grabbed', Lang.bind(this, function() { if (this._summaryBoxPointer.bin.child) - this._lock(); + this.lock(); })); - this._focusGrabber.connect('focus-ungrabbed', Lang.bind(this, this._unlock)); + this._focusGrabber.connect('focus-ungrabbed', Lang.bind(this, this.unlock)); this._focusGrabber.connect('button-pressed', Lang.bind(this, function(focusGrabber, source) { if (this._clickedSummaryItem && !this._clickedSummaryItem.actor.contains(source)) @@ -1313,7 +1313,7 @@ MessageTray.prototype = { this._focusGrabber.connect('escape-pressed', Lang.bind(this, this._escapeTray)); this._trayState = State.HIDDEN; - this._locked = false; + this._locked = 0; this._useLongerTrayLeftTimeout = false; this._trayLeftTimeoutId = 0; this._pointerInTray = false; @@ -1329,9 +1329,9 @@ MessageTray.prototype = { this._notificationRemoved = false; this._reNotifyAfterHideNotification = null; - Main.chrome.addActor(this.actor, { affectsStruts: false, - visibleInFullscreen: true }); + Main.layoutManager.topBox.add_actor(this.actor); Main.chrome.trackActor(this._notificationBin); + Main.chrome.trackActor(this._summaryBin); Main.chrome.trackActor(this._summaryBoxPointer.actor); Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._setSizePosition)); @@ -1341,9 +1341,9 @@ MessageTray.prototype = { Main.overview.connect('showing', Lang.bind(this, function() { this._overviewVisible = true; - if (this._locked) { + if (this._locked > 0) { this._unsetClickedSummaryItem(); - this._unlock(); + this.unlock(); } else { this._updateState(); } @@ -1351,9 +1351,9 @@ MessageTray.prototype = { Main.overview.connect('hiding', Lang.bind(this, function() { this._overviewVisible = false; - if (this._locked) { + if (this._locked > 0) { this._unsetClickedSummaryItem(); - this._unlock(); + this.unlock(); } else { this._updateState(); } @@ -1372,7 +1372,7 @@ MessageTray.prototype = { _setSizePosition: function() { let monitor = Main.layoutManager.bottomMonitor; this.actor.x = monitor.x; - this.actor.y = monitor.y + monitor.height - 1; + this.actor.y = -1; this.actor.width = monitor.width; this._notificationBin.x = 0; this._notificationBin.width = monitor.width; @@ -1524,15 +1524,16 @@ MessageTray.prototype = { this._notificationQueue.splice(index, 1); }, - _lock: function() { - this._locked = true; + lock: function() { + this._locked += 1; + this._updateState(); }, - _unlock: function() { - if (!this._locked) - return; - this._locked = false; - this._pointerInTray = this.actor.hover && !this._summaryBoxPointer.bin.hover; + unlock: function() { + if (this._locked > 0) + this._locked -= 1; + this._pointerInSummary = false; + this._pointerInTray = false; this._updateState(); }, @@ -1816,7 +1817,7 @@ MessageTray.prototype = { }, _escapeTray: function() { - this._unlock(); + this.unlock(); this._pointerInTray = false; this._pointerInSummary = false; this._updateNotificationTimeout(0); @@ -1834,7 +1835,7 @@ MessageTray.prototype = { let notificationsPending = this._notificationQueue.length > 0 && (!this._busy || notificationUrgent); let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved; let notificationExpanded = this._notificationBin.y < 0; - let notificationExpired = (this._notificationTimeoutId == 0 && !(this._notification && this._notification.urgency == Urgency.CRITICAL) && !this._pointerInTray && !this._locked) || this._notificationRemoved; + let notificationExpired = (this._notificationTimeoutId == 0 && !(this._notification && this._notification.urgency == Urgency.CRITICAL) && !this._pointerInTray && (this._locked == 0)) || this._notificationRemoved; let canShowNotification = notificationsPending && this._summaryState == State.HIDDEN; if (this._notificationState == State.HIDDEN) { @@ -1850,17 +1851,17 @@ MessageTray.prototype = { } // Summary - let summarySummoned = this._pointerInSummary || this._overviewVisible; - let summaryPinned = this._summaryTimeoutId != 0 || this._pointerInTray || summarySummoned || this._locked; + let summarySummoned = this._pointerInSummary || this._overviewVisible || (this._locked > 0); + let summaryPinned = this._summaryTimeoutId != 0 || this._pointerInTray || summarySummoned; let summaryHovered = this._pointerInTray || this._pointerInSummary; - let summaryVisibleWithNoHover = (this._overviewVisible || this._locked) && !summaryHovered; + let summaryVisibleWithNoHover = (this._overviewVisible || this._locked > 0) && !summaryHovered; let summaryNotificationIsForExpandedSummaryItem = (this._clickedSummaryItem == this._expandedSummaryItem); let notificationsVisible = (this._notificationState == State.SHOWING || this._notificationState == State.SHOWN); let notificationsDone = !notificationsVisible && !notificationsPending; - let summaryOptionalInOverview = this._overviewVisible && !this._locked && !summaryHovered; + let summaryOptionalInOverview = this._overviewVisible && (this._locked == 0) && !summaryHovered; let mustHideSummary = (notificationsPending && (notificationUrgent || summaryOptionalInOverview)) || notificationsVisible; @@ -1944,18 +1945,16 @@ MessageTray.prototype = { }, _showTray: function() { - let monitor = Main.layoutManager.bottomMonitor; this._tween(this.actor, '_trayState', State.SHOWN, - { y: monitor.y + monitor.height - this.actor.height, + { y: 0, time: ANIMATION_TIME, transition: 'easeOutQuad' }); }, _hideTray: function() { - let monitor = Main.layoutManager.bottomMonitor; this._tween(this.actor, '_trayState', State.HIDDEN, - { y: monitor.y + monitor.height - 1, + { y: Main.layoutManager.topBox.height - 1, time: ANIMATION_TIME, transition: 'easeOutQuad' }); diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index 5514ac171..a3ca9fd63 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -97,8 +97,10 @@ NotificationDaemon.prototype = { this._notifications = {}; this._busProxy = new Bus(); - Main.statusIconDispatcher.connect('message-icon-added', Lang.bind(this, this._onTrayIconAdded)); - Main.statusIconDispatcher.connect('message-icon-removed', Lang.bind(this, this._onTrayIconRemoved)); + Main.connect('initialized', Lang.bind(this, function() { + Main.statusIconDispatcher.connect('message-icon-added', Lang.bind(this, this._onTrayIconAdded)); + Main.statusIconDispatcher.connect('message-icon-removed', Lang.bind(this, this._onTrayIconRemoved)); + })); Shell.WindowTracker.get_default().connect('notify::focus-app', Lang.bind(this, this._onFocusAppChanged)); diff --git a/js/ui/overview.js b/js/ui/overview.js index 085f29224..98f865d7c 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -153,6 +153,18 @@ Overview.prototype = { this._coverPane.hide(); + this._windowSwitchTimeoutId = 0; + this._windowSwitchTimestamp = 0; + this._lastActiveWorkspaceIndex = -1; + this._lastHoveredWindow = null; + this._needsFakePointerEvent = false; + + this.workspaces = null; + + Main.connect('initialized', Lang.bind(this, this._finishInit)); + }, + + _finishInit: function() { // XDND this._dragMonitor = { dragMotion: Lang.bind(this, this._onDragMotion) @@ -161,22 +173,11 @@ Overview.prototype = { Main.xdndHandler.connect('drag-begin', Lang.bind(this, this._onDragBegin)); Main.xdndHandler.connect('drag-end', Lang.bind(this, this._onDragEnd)); - this._windowSwitchTimeoutId = 0; - this._windowSwitchTimestamp = 0; - this._lastActiveWorkspaceIndex = -1; - this._lastHoveredWindow = null; - this._needsFakePointerEvent = false; - - this.workspaces = null; - }, - - // The members we construct that are implemented in JS might - // want to access the overview as Main.overview to connect - // signal handlers and so forth. So we create them after - // construction in this init() method. - init: function() { this.shellInfo = new ShellInfo(); + // The members we construct that are implemented in JS might + // want to access the overview as Main.overview to connect + // signal handlers and so forth. So we create them here. this.viewSelector = new ViewSelector.ViewSelector(); this._group.add_actor(this.viewSelector.actor); @@ -449,19 +450,16 @@ Overview.prototype = { let primary = Main.layoutManager.primaryMonitor; let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL); - let contentY = Main.panel.actor.height; - let contentHeight = primary.height - contentY - Main.messageTray.actor.height; - this._group.set_position(primary.x, primary.y); this._group.set_size(primary.width, primary.height); - this._coverPane.set_position(0, contentY); - this._coverPane.set_size(primary.width, contentHeight); + this._coverPane.set_position(primary.x, primary.y); + this._coverPane.set_size(primary.width, primary.height); let dashWidth = Math.round(DASH_SPLIT_FRACTION * primary.width); let viewWidth = primary.width - dashWidth - this._spacing; - let viewHeight = contentHeight - 2 * this._spacing; - let viewY = contentY + this._spacing; + let viewHeight = primary.height - 2 * this._spacing; + let viewY = primary.y + this._spacing; let viewX = rtl ? 0 : dashWidth + this._spacing; // Set the dash's x position - y is handled by a constraint diff --git a/js/ui/panel.js b/js/ui/panel.js index 3fc8bd047..76b2127c7 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -939,9 +939,6 @@ Panel.prototype = { corner.actor.set_style_pseudo_class(pseudoClass); })); - Main.statusIconDispatcher.connect('status-icon-added', Lang.bind(this, this._onTrayIconAdded)); - Main.statusIconDispatcher.connect('status-icon-removed', Lang.bind(this, this._onTrayIconRemoved)); - Main.chrome.addActor(this.actor); Main.chrome.addActor(this._leftCorner.actor, { affectsStruts: false, affectsInputRegion: false }); @@ -970,8 +967,11 @@ Panel.prototype = { this._statusArea[role] = indicator; } + Main.statusIconDispatcher.connect('status-icon-added', Lang.bind(this, this._onTrayIconAdded)); + Main.statusIconDispatcher.connect('status-icon-removed', Lang.bind(this, this._onTrayIconRemoved)); + // PopupMenuManager depends on menus being added in order for - // keyboard navigation + // keyboard navigation, so we couldn't add this before this._menus.addMenu(this._userMenu.menu); }, diff --git a/js/ui/status/accessibility.js b/js/ui/status/accessibility.js index 560042c27..fb2a2d16f 100644 --- a/js/ui/status/accessibility.js +++ b/js/ui/status/accessibility.js @@ -40,6 +40,8 @@ const KEY_TEXT_SCALING_FACTOR = 'text-scaling-factor'; const HIGH_CONTRAST_THEME = 'HighContrast'; +const KEYBOARD_SCHEMA = 'org.gnome.shell.keyboard' + function ATIndicator() { this._init.apply(this, arguments); } @@ -68,9 +70,9 @@ ATIndicator.prototype = { // 'screen-reader-enabled'); // this.menu.addMenuItem(screenReader); -// let screenKeyboard = this._buildItem(_("Screen Keyboard"), APPLICATIONS_SCHEMA, -// 'screen-keyboard-enabled'); -// this.menu.addMenuItem(screenKeyboard); + let screenKeyboard = this._buildItem(_("Screen Keyboard"), KEYBOARD_SCHEMA, + 'show-keyboard'); + this.menu.addMenuItem(screenKeyboard); let visualBell = this._buildItemGConf(_("Visual Alerts"), client, KEY_VISUAL_BELL); this.menu.addMenuItem(visualBell); diff --git a/js/ui/statusIconDispatcher.js b/js/ui/statusIconDispatcher.js index 8b69e7dc3..030a35b58 100644 --- a/js/ui/statusIconDispatcher.js +++ b/js/ui/statusIconDispatcher.js @@ -4,6 +4,7 @@ const Lang = imports.lang; const Shell = imports.gi.Shell; const Signals = imports.signals; +const Main = imports.ui.main; const MessageTray = imports.ui.messageTray; const NotificationDaemon = imports.ui.notificationDaemon; const Util = imports.misc.util; @@ -38,10 +39,10 @@ StatusIconDispatcher.prototype = { // status icons // http://bugzilla.gnome.org/show_bug.cgi=id=621382 Util.killall('indicator-application-service'); - }, - start: function(themeWidget) { - this._traymanager.manage_stage(global.stage, themeWidget); + Main.connect('initialized', Lang.bind(this, function() { + this._traymanager.manage_stage(global.stage, Main.messageTray.actor); + })); }, _onTrayIconAdded: function(o, icon) { diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js index 08138f788..955aee0c6 100644 --- a/js/ui/workspacesView.js +++ b/js/ui/workspacesView.js @@ -551,7 +551,6 @@ WorkspacesDisplay.prototype = { Lang.bind(this, this._onScrollEvent)); this._monitorIndex = Main.layoutManager.primaryIndex; - this._monitor = Main.layoutManager.monitors[this._monitorIndex]; this._thumbnailsBox = new WorkspaceThumbnail.ThumbnailsBox(); controls.add_actor(this._thumbnailsBox.actor); diff --git a/tools/build/gnome-shell.modules b/tools/build/gnome-shell.modules index e5899600b..4fde5e7f0 100644 --- a/tools/build/gnome-shell.modules +++ b/tools/build/gnome-shell.modules @@ -317,6 +317,7 @@ + @@ -410,7 +411,7 @@ - +