10
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
12
Lesser General Public License for more details.
14
14
You should have received a copy of the GNU Lesser General
15
15
Public License along with this library; if not, write to the
16
16
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17
17
Boston, MA 02110-1301 USA.
20
public enum Cable.Widgets.UserType {
20
public enum Cable.UserType {
26
public enum Cable.Widgets.MessageType {
26
public enum Cable.State {
36
34
* This class represents a chat room, i.e, the topic, the message display,
37
35
* a list of users and is associated to a channel.
38
36
* This class is always accessed through its related channel.
39
* TODO : Use Gtk.Grid instead of Gtk.Box, Gtk.Box will be deprecated in the future.
41
public class Cable.Widgets.Room : Gtk.Box {
38
public class Cable.Widgets.Room : Gtk.Grid {
43
40
Granite.Widgets.SourceList.ExpandableItem operators;
44
41
Granite.Widgets.SourceList.ExpandableItem voiced;
45
42
Granite.Widgets.SourceList.ExpandableItem regular;
47
44
Granite.Widgets.SourceList users;
50
Gtk.ScrolledWindow left_scrolled;
45
public ChatDisplay chat;
46
Granite.Widgets.EmbeddedAlert not_joined_alert;
47
Granite.Widgets.EmbeddedAlert no_such_alert;
48
Granite.Widgets.EmbeddedAlert loading_screen;
49
Granite.Widgets.ThinPaned paned;
51
internal Gtk.Entry text_entry;
52
Utils.AutoScrolled left_scrolled;
55
Gee.HashMap<string, string> color_map;
57
private int unity_badge_count = 0;
59
public Gtk.InfoBar topic;
60
public Gtk.Label topic_name;
61
private Gtk.Button identity_button;
56
public Widgets.Topic topic_bar;
57
Gtk.Action action_join;
58
public Gtk.Label pseudo_label;
63
60
public signal void send_message (string message);
65
public signal bool kick_user (string nick_name);
66
public signal bool ban_user (string nick_name);
67
public signal void ignore (string nick_name, bool ignore = true);
69
public Server server { get; private set; }
70
public string channel;
72
public Room (string channel, Server server) {
61
// public signal void request_complete ();
62
public signal void rejoin ();
64
public string channel { get; set; }
67
get { return pseudo_label.label; }
68
set { pseudo_label.label = value; }
72
get { return text_entry.text; }
73
set { text_entry.text = value; }
78
get { return _state; }
82
var widget = get_child_at (0, 0);
88
attach (paned, 0, 0, 1, 1);
91
attach (loading_screen, 0, 0, 1, 1);
92
loading_screen.working = true;
94
case State.NOT_JOINED:
95
attach (not_joined_alert, 0, 0, 1, 1);
98
attach (no_such_alert, 0, 0, 1, 1);
101
assert_not_reached ();
108
private Gee.Set <string> nicks {
110
var _nicks = new Gee.HashSet <string> ();
111
Granite.Widgets.SourceList.ExpandableItem[] lists = {voiced, operators, regular};
112
foreach (var list in lists)
113
foreach (var child in list.children)
114
_nicks.add (child.name);
115
return _nicks as Gee.Set;
119
public Room (string channel, string nick="") {
73
120
this.channel = channel;
76
color_map = new Gee.HashMap<string, string> (null, null);
78
this.orientation = Gtk.Orientation.VERTICAL;
80
var paned = new Granite.Widgets.ThinPaned ();
123
connect_callbacks ();
126
void init_layout () {
127
paned = new Granite.Widgets.ThinPaned ();
81
128
var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
83
/* Create basic SourceList with items */
84
130
operators = new Granite.Widgets.SourceList.ExpandableItem (_("Operators"));
85
131
operators.expanded = true;
87
regular = new Granite.Widgets.SourceList.ExpandableItem ("Regular");
133
regular = new Granite.Widgets.SourceList.ExpandableItem (_("Regular"));
88
134
regular.expanded = true;
90
voiced = new Granite.Widgets.SourceList.ExpandableItem ("Voiced");
136
voiced = new Granite.Widgets.SourceList.ExpandableItem (_("Voiced"));
91
137
voiced.expanded = true;
93
139
users = new Granite.Widgets.SourceList ();
94
140
users.hscrollbar_policy = Gtk.PolicyType.NEVER;
95
141
users.width_request = 150;
96
142
users.set_sort_func ((a,b) => {return a.name.collate (b.name);});
97
var css = new Gtk.CssProvider ();
99
css.load_from_data ("*{
100
background-color: #fff;
103
critical (e.message);
105
users.get_style_context ().add_provider (css, 40000);
107
144
users.root.add (operators);
108
145
users.root.add (voiced);
109
146
users.root.add (regular);
111
148
user_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
112
149
user_box.pack_start (users);
114
150
paned.pack1 (box, true, false);
115
151
paned.pack2 (user_box, false, false);
117
/* Create inline toolbar */
118
misc_bar = new Gtk.Toolbar ();
119
misc_bar.set_icon_size (Gtk.IconSize.SMALL_TOOLBAR);
121
var misc_bar_style = misc_bar.get_style_context ();
122
misc_bar_style.add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
124
var aligner = new Gtk.SeparatorToolItem ();
125
aligner.set_draw (false);
126
aligner.set_expand (true);
128
var about_button = main_actions.get_action ("About").create_tool_item() as Gtk.ToolButton;
129
var settings_button = main_actions.get_action ("Preferences").create_tool_item() as Gtk.ToolButton;
131
misc_bar.add (aligner);
132
misc_bar.add (settings_button);
133
misc_bar.add (about_button);
135
/* Create the Topic InfoBar */
136
var topic = new Gtk.InfoBar ();
137
topic_name = new Gtk.Label (_("Loading Topic…"));
138
topic_name.use_markup = true;
139
topic_name.wrap = true;
140
topic_name.halign = Gtk.Align.FILL;
141
topic_name.valign = Gtk.Align.FILL;
142
var content = topic.get_content_area () as Gtk.Container;
143
content.add (topic_name);
145
entry = new Gtk.Entry ();
146
entry.hexpand = true;
148
left_scrolled = new Gtk.ScrolledWindow (null, null);
153
text_entry = new Gtk.Entry ();
154
text_entry.hexpand = true;
156
left_scrolled = new Utils.AutoScrolled ();
149
157
left_scrolled.hscrollbar_policy = Gtk.PolicyType.NEVER;
151
159
chat = new ChatDisplay ();
153
var pseudo_label = new Gtk.Label (server.identity.nick_name);
161
topic_bar = new Widgets.Topic ();
163
pseudo_label = new Gtk.Label ("");
154
164
pseudo_label.set_alignment (1, (float)0.5);
155
165
pseudo_label.width_request = 80;
157
167
var entry_eventbox = new Gtk.EventBox ();
158
168
entry_eventbox.get_style_context ().add_class (Granite.StyleClass.CONTENT_VIEW);
159
169
var entry_grid = new Gtk.Grid ();
160
170
entry_grid.margin = 12;
161
171
entry_grid.column_spacing = 12;
162
172
entry_eventbox.add (entry_grid);
164
174
entry_grid.attach (pseudo_label, 0, 0, 1, 1);
165
entry_grid.attach (entry, 1, 0, 1, 1);
175
entry_grid.attach (text_entry, 1, 0, 1, 1);
166
176
left_scrolled.add_with_viewport(chat);
168
box.pack_start (topic, false);
178
box.pack_start (topic_bar, false);
169
179
box.pack_start (left_scrolled);
170
180
box.pack_start (entry_eventbox, false);
172
this.pack_start (paned);
174
entry.focus_in_event.connect (() => {
175
for (int n = 0; n < unity_badge_count; n++)
176
Cable.Services.unity.remove_job ();
177
unity_badge_count = 0;
181
entry.activate.connect (() => {
182
send_message (entry.text);
187
entry.key_press_event.connect ((ev) => {
182
loading_screen = new Granite.Widgets.EmbeddedAlert ();
183
loading_screen.primary_text = _("Joining channel %s").printf(channel);
184
loading_screen.working = true;
186
action_join = new Gtk.Action ("join", "Join Channel", "Join this channel", null);
188
not_joined_alert = new Granite.Widgets.EmbeddedAlert ();
189
not_joined_alert.primary_text = _("Channel is not joined");
190
not_joined_alert.secondary_text = _("Join the channel to receive and send messages.");
191
not_joined_alert.message_type = Gtk.MessageType.WARNING;
192
not_joined_alert.actions = {action_join};
194
no_such_alert = new Granite.Widgets.EmbeddedAlert ();
195
no_such_alert.primary_text = _("Cannot join channel");
196
no_such_alert.secondary_text = _("An error has occurred, this channel does not exist.");
197
no_such_alert.message_type = Gtk.MessageType.WARNING;
198
no_such_alert.show_icon= true;
203
void connect_callbacks () {
204
text_entry.key_press_event.connect ((ev) => {
188
205
if (ev.keyval == Gdk.Key.Tab) {
189
if (entry.text == null || entry.text == "")
206
var text = text_entry.text;
208
if (text == null || text == "")
192
var txt = entry.text.split (" ");
193
var word = txt[txt.length-1];
211
var words = text.split (" ");
212
var word = words[words.length-1];
195
214
if (word == null || word == "")
198
var collection = new Gee.LinkedList<Granite.Widgets.SourceList.Item> ();
199
collection.add_all (operators.children);
200
collection.add_all (voiced.children);
201
collection.add_all (regular.children);
202
var user_array = collection.to_array ();
204
string[]? completion = null;
205
for (int n = 0; n < user_array.length; n++)
206
if (user_array[n].name != null)
207
if (word.down () == user_array[n].name[0:word.length].down ())
208
completion += user_array[n].name;
217
string[] completion = null;
218
foreach (var nick in nicks)
219
if (word.down () == nick[0:word.length].down ())
210
222
if (completion == null)
213
225
if (completion.length > 1) {
216
for (int i = 0; i < completion.length; i++) {
217
if (i != (completion.length -1))
218
msg += (completion[i] + sep);
220
msg += completion[i];
222
this.message ("", msg, MessageType.SUGGESTION);
226
txt = entry.text.split (" ");
227
word = txt[txt.length-1];
228
entry.buffer.delete_text (entry.cursor_position - word.length, word.length);
229
entry.insert_at_cursor (completion[0] + ": ");
233
for (int n = 0; n < user_array.length; n++)
234
if (user_array[n].name != null)
235
if (word.casefold () == user_array[n].name[0:word.length].casefold ()) {
236
entry.delete_from_cursor (Gtk.DeleteType.WORDS, 1);
237
entry.insert_at_cursor (user_array[n].name + ": ");
226
chat.message_suggestions (completion);
229
text_entry.buffer.delete_text (text_entry.cursor_position - word.length, word.length);
230
text_entry.insert_at_cursor (completion[0] + ": ");
245
/* Load nickname's color from settings */
246
string[] colors = Settings.settings.get_strv ("colors");
247
foreach (var color in colors) {
248
var array = color.split(":", 4);
249
if (array.length == 4) {
251
if (array[0] == server.network.address && array[1] == channel) {
252
color_map.set (array[2], array[3]);
237
text_entry.activate.connect (() => {
238
if (text_entry.text != "")
239
send_message (text_entry.text); // signal
242
action_join.activate.connect (() => {
247
public bool has_user (string nick) {
248
Granite.Widgets.SourceList.ExpandableItem[] lists = {voiced, operators, regular};
249
foreach (var list in lists)
250
foreach (var child in list.children)
251
if (child.name == nick)
256
public void list_users (string[] nicks, string[] prefixes)
257
requires (nicks.length == prefixes.length) {
259
for (int i = 0; i < nicks.length; i++) {
260
switch (prefixes[i]) {
262
add_user (nicks[i], UserType.OPERATOR, true);
265
add_user (nicks[i], UserType.VOICED, true);
268
add_user (nicks[i], UserType.REGULAR, true);
259
public void rename_user (string old_name, string new_name, UserType type, bool silent = false) {
260
remove_user (old_name, true);
261
add_user (new_name, type, true);
274
public void rename_user (string? old_name, string new_name, UserType type, bool silent = false)
275
requires (has_user (old_name))
276
requires (!has_user (new_name))
277
ensures (!has_user (old_name))
278
ensures (has_user (new_name)) {
264
message (new_name, "", MessageType.NICK);
280
if (old_name == null) {
281
add_user (new_name, type, true);
285
if (remove_user (old_name, "", true)) {
286
add_user (new_name, type, true);
288
chat.message_nick (old_name, new_name);
267
public void add_user (string name, UserType type, bool silent = false) {
292
public void add_user (string name, UserType type, bool silent = false)
293
requires (!has_user (name))
294
ensures (has_user (name)) {
268
296
var user = new User (name);
269
if (name == server.identity.nick_name)
271
user.color_changed.connect (user_color_changed);
272
297
user.selectable = false;
273
if (color_map.has_key (name)) {
274
user.set_color_from_string (color_map.get (name));
300
case UserType.OPERATOR:
301
operators.add (user);
303
case UserType.VOICED:
277
if (type == UserType.OPERATOR)
278
operators.add (user);
279
else if (type == UserType.VOICED)
285
message (name, "", MessageType.ENTER);
312
chat.message_join (name);
287
314
regular.name = _("Regular") + " (" + regular.n_children.to_string () + ")";
288
315
operators.name = _("Operators") + " (" + operators.n_children.to_string () + ")";
289
316
voiced.name = _("Voiced") + " (" + voiced.n_children.to_string () + ")";
292
public void remove_user (string name, bool silent = false) {
319
public bool remove_user (string name, string message, bool silent = false)
320
requires (has_user (name))
321
ensures (!has_user (name)) {
293
323
Granite.Widgets.SourceList.ExpandableItem[] lists = {voiced, operators, regular};
294
324
foreach (var list in lists) {
295
325
foreach (var child in list.children) {
296
326
if (child.name == name) {
297
327
list.remove (child);
300
message (child.name, "", MessageType.LEAVE);
330
chat.message_part (child.name, message);
302
332
regular.name = _("Regular") + " (" + regular.n_children.to_string () + ")";
303
333
operators.name = _("Operators") + " (" + operators.n_children.to_string () + ")";
304
334
voiced.name = _("Voiced") + " (" + voiced.n_children.to_string () + ")";
313
private void user_color_changed (string user, string color) {
314
if (!color_map.has_key (user)) {
315
color_map.set (user, color);
317
color_map.unset (user, null);
318
color_map.set (user, color);
320
string[] colors = Settings.settings.get_strv ("colors");
321
var colors_modified = new string[colors.length+1];
322
bool value_exists = false;
323
for (int i = 0; i<colors.length; i++) {
324
colors_modified[i] = colors[i];
325
var array = colors[i].split(":", 4);
326
if (array.length == 4) {
327
if (array[0] == server.network.address && array[1] == channel && array[2]==user) {
328
colors_modified[i] = server.network.address + ":" + channel + ":" + user + ":" + color;
335
colors_modified[colors.length] = server.network.address + ":" + channel + ":" + user + ":" + color;
337
Settings.settings.set_strv ("colors", colors_modified);
340
public void message (owned string from, string message, MessageType type = MessageType.NORMAL) {
341
bool do_scroll = left_scrolled.vadjustment.value + 10 > left_scrolled.vadjustment.upper;
344
case MessageType.NORMAL:
345
var new_message = message;
346
foreach (var entry in color_map.entries) {
347
new_message = new_message.replace (entry.key, "<span color='"+entry.value+"'>"+entry.key+"</span>");
349
if (message.contains (server.identity.nick_name)) {
350
chat.add_message (@"<span color='#DA4D45'><b>$from</b></span>", @"<span color='#DA4D45'><b>$new_message</b></span>");
351
// Send a notification...
352
var notify = new Notify.Notification (from, new_message, "internet-chat");
355
} catch (Error e) { warning (e.message); }
357
if (!entry.has_focus) {
358
Cable.Services.unity.append_job ();
363
if (color_map.has_key (from)) {
364
chat.add_message ("<span color='"+color_map.get (from)+@"'>$from</span>", new_message);
366
chat.add_message (from, new_message);
370
case MessageType.ENTER:
371
chat.add_message ("", @"<span color='#9a9'>"+_("%s joined").printf (from)+"</span>");
373
case MessageType.LEAVE:
374
chat.add_message ("", @"<span color='#a99'>"+_("%s left").printf (from)+"</span>");
376
case MessageType.AWAY:
377
chat.add_message ("", @"<span color='#999'>"+_("%s is away").printf (from)+"</span>");
379
case MessageType.NICK:
380
chat.add_message ("", @"<span color='#999'>$from</span>");
382
case MessageType.SUGGESTION:
383
chat.add_message ("", @"<span color='#9a9'>"+ ("%s").printf (message) + "</span>");
387
Timeout.add (1, () => {
389
left_scrolled.vadjustment.value = left_scrolled.vadjustment.upper;