[polari/nick-completion: 2/2] mainWindow: Implement nick completion



commit 8aef812f2d4720d674148205d039df7c7e133a4e
Author: Florian Müllner <fmuellner gnome org>
Date:   Thu Aug 8 17:06:56 2013 +0200

    mainWindow: Implement nick completion
    
    Add simple tab completion that cycles through matches on each
    Tab press while displaying possible completions in a popup
    (except in case of a single match). The Escape key may be used
    to cancel the operation.
    We should add some smarts about the order in which matches are
    presented in the future, but for now having basic functionality
    in place is still better than nothing ...

 src/Makefile.am      |    1 +
 src/mainWindow.js    |   18 +++++
 src/tabCompletion.js |  192 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 211 insertions(+), 0 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 13de457..ec8845e 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -40,6 +40,7 @@ dist_js_DATA = \
        notify.js \
        pasteManager.js \
        roomList.js \
+       tabCompletion.js \
        userList.js \
        utils.js \
        $(NULL)
diff --git a/src/mainWindow.js b/src/mainWindow.js
index d196e02..5ae6792 100644
--- a/src/mainWindow.js
+++ b/src/mainWindow.js
@@ -13,6 +13,7 @@ const JoinDialog = imports.joinDialog;
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
 const RoomList = imports.roomList;
+const TabCompletion = imports.tabCompletion;
 const UserList = imports.userList;
 
 const MAX_NICK_UPDATE_TIME = 5; /* s */
@@ -71,6 +72,7 @@ const MainWindow = new Lang.Class({
         this._entry = builder.get_object('message_entry');
 
         this._nickEntry.width_chars = ChatView.MAX_NICK_CHARS
+        this._completion = new TabCompletion.TabCompletion(this._entry);
 
         let scroll = builder.get_object('room_list_scrollview');
         this._roomList = new RoomList.RoomList();
@@ -259,6 +261,7 @@ const MainWindow = new Lang.Class({
         if (this._room) {
             this._room.disconnect(this._displayNameChangedId);
             this._room.disconnect(this._topicChangedId);
+            this._room.disconnect(this._membersChangedId);
             this._room.channel.connection.disconnect(this._nicknameChangedId);
         }
         this._displayNameChangedId = 0;
@@ -271,6 +274,7 @@ const MainWindow = new Lang.Class({
         this._updateTitlebar();
         this._updateNick();
         this._updateSensitivity();
+        this._updateCompletions();
 
         if (!this._room)
             return; // finished
@@ -281,6 +285,9 @@ const MainWindow = new Lang.Class({
         this._topicChangedId =
             this._room.connect('notify::topic',
                                Lang.bind(this, this._updateTitlebar));
+        this._membersChangedId =
+            this._room.connect('members-changed',
+                               Lang.bind(this, this._updateCompletions));
         this._nicknameChangedId =
             this._room.channel.connection.connect('notify::self-contact',
                                                   Lang.bind(this,
@@ -331,6 +338,17 @@ const MainWindow = new Lang.Class({
             });
     },
 
+    _updateCompletions: function() {
+        let nicks = [];
+
+        if (this._room &&
+            this._room.channel.has_interface(Tp.IFACE_CHANNEL_INTERFACE_GROUP)) {
+            let members = this._room.channel.group_dup_members_contacts();
+            nicks = members.map(function(member) { return member.alias; });
+        }
+        this._completion.setCompletions(nicks);
+    },
+
     _updateTitlebar: function() {
         this._titlebarRight.title = this._room ? this._room.display_name : null;
         this._titlebarRight.subtitle = this._room ? this._room.topic : null;
diff --git a/src/tabCompletion.js b/src/tabCompletion.js
new file mode 100644
index 0000000..2523c96
--- /dev/null
+++ b/src/tabCompletion.js
@@ -0,0 +1,192 @@
+const Gdk = imports.gi.Gdk;
+const GLib = imports.gi.GLib;
+const Gtk = imports.gi.Gtk;
+const Pango = imports.gi.Pango;
+
+const Lang = imports.lang;
+
+const TabCompletion = new Lang.Class({
+    Name: 'TabCompletion',
+
+    _init: function(entry) {
+        this._entry = entry;
+        this._canComplete = false;
+        this._key = '';
+
+        this._entry.connect('key-press-event', Lang.bind(this, this._onKeyPress));
+        this._entry.connect('focus-out-event', Lang.bind(this, this._cancel));
+        this._entry.connect('unmap', Lang.bind(this, this._cancel));
+
+        this._popup = new Gtk.Window({ type: Gtk.WindowType.POPUP });
+
+        this._list = new Gtk.ListBox({ selection_mode: Gtk.SelectionMode.SINGLE });
+        this._list.set_filter_func(Lang.bind(this, this._filter));
+        this._list.connect('row-selected', Lang.bind(this, this._onRowSelected));
+        this._list.connect('row-activated', Lang.bind(this, this._stop));
+        this._list.connect('keynav-failed', Lang.bind(this, this._onKeynavFailed));
+        this._popup.add(this._list);
+    },
+
+    _showPopup: function() {
+        this._list.show_all();
+
+        let [, height] = this._list.get_preferred_height();
+        let [, width] = this._list.get_preferred_width();
+        this._popup.resize(width, height);
+
+        let win = this._entry.get_window();
+
+        let layout = this._entry.get_layout();
+        let text = this._entry.text.substr(0, this._entry.get_position());
+        let wordIndex = text.lastIndexOf(' ') + 1;
+        let layoutIndex = this._entry.text_index_to_layout_index(wordIndex);
+        let wordPos = layout.index_to_pos(layoutIndex);
+        let [layoutX,] = this._entry.get_layout_offsets();
+
+        let allocation = this._entry.get_allocation();
+        let [ret, x, y] = win.get_origin();
+        x += allocation.x + Math.min((layoutX + wordPos.x) / Pango.SCALE,
+                                     allocation.width - width);
+        y += allocation.y - height;
+        this._popup.move(x, y);
+        this._popup.show();
+    },
+
+    setCompletions: function(completions) {
+        if (this._popup.visible) {
+            let id = this._popup.connect('unmap', Lang.bind(this,
+                function() {
+                    this._popup.disconnect(id);
+                    this.setCompletions(completions);
+                }));
+            return;
+        }
+
+        this._list.foreach(function(r) { r.destroy(); });
+
+        for (let i = 0; i < completions.length; i++) {
+            let row = new Gtk.ListBoxRow();
+            row._text = completions[i];
+            row._casefoldedText = row._text.toLowerCase();
+            row.add(new Gtk.Label({ label: row._text,
+                                    halign: Gtk.Align.START,
+                                    margin_left: 6,
+                                    margin_right: 6 }));
+            this._list.add(row);
+        }
+        this._canComplete = completions.length > 0;
+    },
+
+    _onKeyPress: function(w, event) {
+        let [, keyval] = event.get_keyval();
+
+        if (this._key.length == 0) {
+            if (keyval == Gdk.KEY_Tab) {
+                this._start();
+                return true;
+            }
+            return false;
+        }
+
+        switch (keyval) {
+            case Gdk.KEY_Tab:
+            case Gdk.KEY_Down:
+                this._moveSelection(Gtk.MovementStep.DISPLAY_LINES, 1);
+                return true;
+            case Gdk.KEY_ISO_Left_Tab:
+            case Gdk.KEY_Up:
+                this._moveSelection(Gtk.MovementStep.DISPLAY_LINES, -1);
+                return true;
+            case Gdk.KEY_Escape:
+                this._cancel();
+                return true;
+        }
+
+        if (Gdk.keyval_to_unicode(keyval) != 0) {
+            let popupShown = this._popup.visible;
+            this._stop();
+            // eat keys that would active the entry
+            // when showing the popup
+            return popupShown &&
+                   (keyval == Gdk.KEY_Return ||
+                    keyval == Gdk.KEY_KP_Enter ||
+                    keyval == Gdk.KEY_ISO_Enter);
+        }
+        return false;
+    },
+
+    _onRowSelected: function(w, row) {
+        if (row)
+            this._insertCompletion(row._text);
+    },
+
+    _filter: function(row) {
+        if (this._key.length == 0)
+            return false;
+        return row._casefoldedText.startsWith(this._key);
+    },
+
+    _insertCompletion: function(completion) {
+        let pos = this._entry.get_position();
+        let text = this._entry.text.substr(0, pos);
+        let wordPos = text.lastIndexOf(' ') + 1;
+        this._entry.delete_text(wordPos, pos);
+        this._entry.insert_text(completion, -1, wordPos);
+        this._entry.set_position(wordPos + completion.length);
+    },
+
+    _start: function() {
+        if (!this._canComplete)
+            return;
+
+        let text = this._entry.text.substr(0, this._entry.get_position());
+        let wordPos = text.lastIndexOf(' ') + 1;
+        this._key = text.toLowerCase().substr(wordPos);
+
+        this._list.invalidate_filter();
+
+        let visibleRows = this._list.get_children().filter(function(c) {
+            return c.get_child_visible();
+        });
+        let nVisibleRows = visibleRows.length;
+
+        if (nVisibleRows == 0)
+            return;
+
+        this._insertCompletion(visibleRows[0]._text);
+        if (visibleRows.length > 1) {
+            this._list.select_row(visibleRows[0]);
+            this._showPopup()
+        }
+    },
+
+    _onKeynavFailed: function(w, dir) {
+        if (this._inHandler)
+            return false;
+        let count = dir == Gtk.DirectionType.DOWN ? -1 : 1;
+        this._inHandler = true;
+        this._moveSelection(Gtk.MovementStep.BUFFER_ENDS, count);
+        this._inHandler = false;
+        return true;
+    },
+
+    _moveSelection: function(movement, count) {
+        this._list.emit('move-cursor', movement, count);
+        let row = this._list.get_focus_child();
+        this._list.select_row(row);
+    },
+
+    _stop: function() {
+        this._popup.hide();
+        this._popup.set_size_request(-1, -1);
+
+        this._key = '';
+
+        this._list.invalidate_filter();
+    },
+
+    _cancel: function() {
+        this._insertCompletion('');
+        this._stop();
+    },
+});


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]