Allow / in the regexes for search/groups
[debian/pino.git] / src / re_tweet.vala
1 /* re_tweet.vala
2  *
3  * Copyright (C) 2009-2010  troorl
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Author:
19  *      troorl <troorl@gmail.com>
20  */
21
22 using Gtk;
23 using Gee;
24 using Auth;
25 using RestAPI;
26
27 public class ReTweet : VBox {
28         
29         public static enum Style { CLASSIC, UNI, VIA }
30         
31         private static enum State { NEW, REPLY, DIRECT_REPLY, RETWEET }
32         
33         private State state;
34         
35         private RestAPIRe api;
36         
37         private TextView entry;
38         public TextView text_entry {
39                 public get { return entry; }
40         }
41         
42         public string text {
43                 public owned get
44                 { return entry.buffer.text; }
45                 set
46                 { entry.buffer.set_text(value, (int)value.size()); }
47         }
48         
49         private Accounts accounts;
50         
51         private Image status_icon;
52         private Label label;
53         public Label user_label;
54         
55         public Action shortAct;
56         
57         private DmEntry direct_entry;
58         private ToolButton close_btn;
59         private Window parent;
60         private Prefs prefs;
61         private UrlShort url_short;
62         private string reply_id = "";
63         
64         Regex nicks;
65         Regex urls;
66         Regex tags;
67         
68         SystemStyle gtk_style;
69         
70         public signal void start_update(string req_url);
71         public signal void sending_data(string message);
72         public signal void data_sent(string message);
73         public signal void data_error_sent(string message);
74         public signal void status_updated(Status status);
75         
76         public ReTweet(Window _parent, Prefs _prefs, Accounts _accounts, Cache cache, SystemStyle _gtk_style) {
77                 parent = _parent;
78                 prefs = _prefs;
79                 accounts = _accounts;
80                 gtk_style = _gtk_style;
81                 
82                 var acc = accounts.get_current_account();
83                 api = new RestAPIRe(acc);
84                 api.request.connect((req) => start_update(req));
85                 
86                 url_short = new UrlShort(prefs, api);
87                 
88                 shortAct = new Action("UrlShort", _("Shorten URLs..."), null, null);
89                 shortAct.set_sensitive(false);
90                 shortAct.activate.connect(() => {
91                         shortAct.set_sensitive(false);
92                         set_sensitive(false);
93                         sending_data(_("Shortening URLs...")); //signal
94                         
95                         string reply = url_short.shortit(text);
96                         
97                         if(reply != text)
98                                 data_sent(_("URLs was shortened successfully")); //signal
99                         else
100                                 data_sent(_("Nothing to shorten")); //signal
101                         
102                         text = reply;
103                         
104                         shortAct.set_sensitive(true);
105                         set_sensitive(true);
106                         
107                         parent.set_focus(entry);
108                 });
109                 
110                 //enable or disable shrten url action
111                 show.connect((event) => {
112                         shortAct.set_sensitive(true);
113                 });
114                 hide.connect((event) => {
115                         shortAct.set_sensitive(false);
116                 });
117                 
118                 //gui setup
119                 border_width = 0;
120                 set_homogeneous(false);
121                 set_spacing(2);
122                 
123                 var l_box = new HBox(false, 2);
124                 status_icon = new Image();
125                 user_label = new Label(_("New status:"));
126                 
127                 direct_entry = new DmEntry(api);
128                 
129                 label = new Label("<b>140</b>");
130                 label.set_use_markup(true);
131                 
132                 Image close_img = new Image();
133                 close_img.set_from_stock("gtk-close", IconSize.MENU);
134                 close_img.set_tooltip_text(_("Hide"));
135                 var event_close = new EventBox();
136                 event_close.add(close_img);
137                 event_close.set_events(Gdk.EventMask.BUTTON_PRESS_MASK);
138                 event_close.button_press_event.connect((event) => {
139                         hide();
140                         return false;
141                 });
142                 
143                 l_box.pack_start(status_icon, false, false, 2);
144                 l_box.pack_start(user_label, false, false, 2);
145                 l_box.pack_start(direct_entry, false, false, 2);
146                 l_box.pack_end(event_close, false, false, 2);
147                 l_box.pack_end(label, false, false, 2);
148                 
149                 entry = new TextView();
150                 entry.set_size_request(-1, 60);
151                 entry.set_wrap_mode(Gtk.WrapMode.WORD);
152                 entry.key_press_event.connect(hide_or_send);
153                 entry.buffer.changed.connect(change);
154                 
155                 string tag_link_color;
156                 if(prefs.nativeLinkColor)
157                         tag_link_color = gtk_style.lk_color;
158                 else
159                         tag_link_color = gtk_style.sl_color;
160                 
161                 entry.buffer.create_tag("red_bg", "background", "#ffa4a4");
162                 entry.buffer.create_tag("url", "foreground", tag_link_color);
163                 entry.buffer.create_tag("nick", "weight", Pango.Weight.BOLD);
164                 entry.buffer.create_tag("tag", "foreground", tag_link_color,
165                         "weight", Pango.Weight.BOLD);
166                 
167                 prefs.enableSpellChanged.connect(spellEnabling);
168                 spellEnabling();
169                 
170                 
171                 nicks = new Regex("(^|\\s)@([A-Za-z0-9_]+)");
172                 urls = new Regex("((http|https|ftp)://([\\S]+)\\.([\\S]+))");
173                 tags = new Regex("((^|\\s)\\#[A-Za-z0-9_\\p{Latin}\\p{Greek}]+)");
174                 
175                 var scroll = new ScrolledWindow(null, null);
176         scroll.set_policy(PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);
177         scroll.add(entry);
178                 
179                 var hbox = new HBox(false, 1);
180                 hbox.pack_start(scroll, true, true, 0);
181                 
182                 var frame = new Frame(null);
183                 frame.set_size_request(-1, 60);
184                 frame.add(hbox);
185                 
186                 var sep = new HSeparator();
187                 
188                 pack_start(sep, false, false, 0);
189                 pack_start(l_box, false, false, 0);
190                 pack_start(frame, false, true, 0);
191         }
192         
193         private void spellEnabling() {
194                 if(prefs.enableSpell) {
195                         if(prefs.enableSpell) {
196                                 try {
197                                         Spell spell = new Spell.attach(entry, null);
198                                 } catch(Error e) {
199                                         try {
200                                                 Spell spell = new Spell.attach(entry, "en_US");
201                                         } catch(Error e) {
202                                                 warning(e.message);
203                                         }
204                                         warning(e.message);
205                                 }
206                         }
207                 } else {
208                         Spell spell = Spell.get_from_text_view(entry);
209                         if(spell != null)
210                                 spell.detach();
211                 }
212         }
213         
214         public void update_auth() {
215                 var acc = accounts.get_current_account();
216                 
217                 api.set_auth(acc);
218         }
219         
220         public void set_screen_name(string user_name) {
221                 user_label.set_text(_("Reply to <b>%s</b>:").printf(user_name));
222                 user_label.set_use_markup(true);
223         }
224         
225         private void clear() {
226                 text = "";
227         }
228         
229         public void insert(string str) {
230                 entry.buffer.insert_at_cursor(str, (int)str.length);
231         }
232         
233         /* when user clicked on a nick */
234         public void set_nickto(string screen_name) {
235                 if(!visible)
236                         set_state_new();
237                         
238                 insert("@%s ".printf(screen_name));
239         }
240         
241         public void set_state_new() {
242                 state = State.NEW;
243                 
244                 direct_entry.hide();
245                 clear();
246                 show();
247                 status_icon.set_from_stock(STOCK_EDIT, IconSize.SMALL_TOOLBAR);
248                 user_label.set_text(_("New status:"));
249                 parent.set_focus(text_entry);
250         }
251         
252         public void set_state_reply(Status status) {
253                 state = State.REPLY;
254                 reply_id = status.id;
255                 
256                 direct_entry.hide();
257                 clear();
258                 show();
259                 
260                 status_icon.set_from_file(Config.REPLY_PATH);
261                 user_label.set_text(_("Reply to") + " <b>%s</b>:".printf(status.user_screen_name));
262                 user_label.set_use_markup(true);
263                 text = "@%s ".printf(status.user_screen_name);
264                 parent.set_focus(text_entry);
265         }
266         
267         public void set_state_directreply(string screen_name) {
268                 state = State.DIRECT_REPLY;
269                 
270                 direct_entry.show();
271                 direct_entry.set_text(screen_name);
272                 
273                 status_icon.set_from_file(Config.DIRECT_REPLY_PATH);
274                 user_label.set_text(_("Direct message to"));
275                 user_label.set_use_markup(true);
276                 
277                 clear();
278                 show();
279                 
280                 if(screen_name != "") { // if new DM, not a "reply"
281                         direct_entry.check();
282                         parent.set_focus(text_entry);
283                 } else parent.set_focus(direct_entry);
284         }
285         
286         public void set_state_retweet(Status status) {
287                 state = State.RETWEET;
288                 
289                 direct_entry.hide();
290                 clear();
291                 show();
292                 
293                 status_icon.set_from_file(Config.RETWEET_PATH);
294                 user_label.set_text(_("Retweet:"));
295                 
296                 switch(prefs.retweetStyle) {
297                         case ReTweet.Style.UNI:
298                                 text = "♺ @%s: %s".printf(status.user_screen_name, status.text);
299                                 break;
300                         
301                         case ReTweet.Style.CLASSIC:
302                                 text = "RT @%s: %s".printf(status.user_screen_name, status.text);
303                                 break;
304                         
305                         case ReTweet.Style.VIA:
306                                 var msg = status.text;
307                                 var via = " (via @%s)".printf(status.user_screen_name);
308                                 
309                                 if(msg.length > (140 - via.length))
310                                         msg = msg.substring(0, 140 - via.length);
311                                 
312                                 text = msg + via;
313                                 break;
314                 }
315                 
316                 parent.set_focus(text_entry);
317         }
318         
319         private bool too_long() {
320                 if(text.length > 140) {
321                         var message_dialog = new MessageDialog(parent,
322                                 Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL,
323                                 Gtk.MessageType.INFO, Gtk.ButtonsType.OK, (_("Your status is too long")));
324                 
325                         message_dialog.run();
326                         message_dialog.destroy();
327                         
328                         return true;
329                 }
330                 return false;
331                 
332         }
333         
334         private bool hide_or_send(Gdk.EventKey event) {
335                 switch(event.hardware_keycode) {
336                         case 36: //return key
337                                 if(event.state == 1) { //shift + enter
338                                         entry.buffer.insert_at_cursor("\n", (int)"\n".length);
339                                         return true;
340                                 }
341                                 if(text.length > 0) {
342                                         enter_pressed();
343                                 } else { // if nothing to send
344                                         var message_dialog = new MessageDialog(parent,
345                                         Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL,
346                                         Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
347                                         (_("Type something first")));
348                                         
349                                         message_dialog.run();
350                                         message_dialog.destroy();
351                                 }
352                                 return true;
353                 
354                 case 9: //esc key
355                         clear();
356                         hide();
357                         break;
358                 }
359                 
360                 return false;
361         }
362         
363         private void enter_pressed() {
364                 switch(state) {
365                         case State.NEW:
366                                 if(!too_long())
367                                         send_new();
368                                 break;
369                         
370                         case State.REPLY:
371                                 if(!too_long())
372                                         send_new(reply_id);
373                                 break;
374                         
375                         case State.RETWEET:
376                                 if(!too_long())
377                                         send_new();
378                                 break;
379                         
380                         case State.DIRECT_REPLY:
381                                 send_dm();
382                                 break;
383                 }
384         }
385         
386         private void send_new(string reply_id = "") {
387                 set_sensitive(false);
388                 
389                 sending_data(_("Sending status...")); //signal
390                 
391                 Status status = null;
392                 try {
393                         status = api.update_status(text, reply_id);
394                 } catch(RestError e) {
395                         set_sensitive(true);
396                         
397                         data_error_sent(e.message);
398                         return;
399                 } catch(ParseError e) {
400                         set_sensitive(true);
401                         
402                         data_error_sent(e.message);
403                         return;
404                 }
405                 
406                 status.tmp = true;
407                 status_updated(status); //signal
408                 
409                 hide();
410                 set_sensitive(true);
411                 
412                 data_sent(_("Your status has been sent successfully")); //signal
413         }
414         
415         private void send_dm() {
416                 set_sensitive(false);
417                 
418                 sending_data(_("Sending direct message...")); //signal
419                 
420                 try {
421                         api.send_dm(direct_entry.text, text);
422                 } catch(RestError e) {
423                         set_sensitive(true);
424                         
425                         data_error_sent(e.message);
426                         return;
427                 }
428                 
429                 hide();
430                 set_sensitive(true);
431                 
432                 data_sent(_("Your direct message has been sent successfully")); //signal
433         }
434         
435         /* styling for nicks in the buffer */
436         private void tagging(TextIter start_pos, string text, Regex regex, string tag_name) {
437                 ArrayList<string> lst = new ArrayList<string>();
438                 int pos = 0;
439                 
440                 while(true) {
441                         MatchInfo match_info;
442                         bool bingo = regex.match_all_full(text, -1, pos, GLib.RegexMatchFlags.NEWLINE_ANY, out match_info);
443                         
444                         if(bingo) {
445                                 foreach(string s in match_info.fetch_all()) {
446                                         lst.add(s);
447                                         
448                                         match_info.fetch_pos(0, null, out pos);
449                                         break;
450                                 }
451                         } else break;
452                 }
453                 
454                 foreach(string item in lst) {
455                         TextIter start_pos_regex;
456                         TextIter end_pos_regex;
457                         
458                         start_pos.forward_search(item, TextSearchFlags.TEXT_ONLY,
459                                 out start_pos_regex, out end_pos_regex, null);
460                         
461                         entry.buffer.apply_tag_by_name(tag_name, start_pos_regex, end_pos_regex);
462                 }
463         }
464         
465         private void change() {
466                 int length = (int)text.len();
467                 
468                 TextIter start_pos;
469                 TextIter end_pos;
470                 
471                 entry.buffer.get_bounds(out start_pos, out end_pos);
472                 
473                 //entry.buffer.remove_all_tags(start_pos, end_pos);
474                 entry.buffer.remove_tag_by_name("red_bg", start_pos, end_pos);
475                 entry.buffer.remove_tag_by_name("url", start_pos, end_pos);
476                 entry.buffer.remove_tag_by_name("nick", start_pos, end_pos);
477                 entry.buffer.remove_tag_by_name("tag", start_pos, end_pos);
478                 
479                 tagging(start_pos, text, urls, "url");
480                 tagging(start_pos, text, nicks, "nick");
481                 tagging(start_pos, text, tags, "tag");
482                 
483                 if(length > 140) {
484                         TextIter start_red_pos;
485                         
486                         entry.buffer.get_iter_at_offset(out start_red_pos, 140);
487                         entry.buffer.get_iter_at_offset(out end_pos, length);
488                         
489                         entry.buffer.apply_tag_by_name("red_bg", start_red_pos, end_pos);
490                 }
491                 
492                 label.set_text("<b>%s</b>".printf((140 - text.len()).to_string()));
493                 label.set_use_markup(true);
494         }
495 }