URI hostname is case-insensitive but path is case-sensitive. Be careful when generati...
[beagle.git] / ImLogViewer / ImLogWindow.cs
blob9b44bdb743ea77da14bc638d7af4be57ed6f7488
1 //
2 // ImLogWindow.cs
3 //
4 // Lukas Lipka <lukas@pmad.net>
5 // Raphael Slinckx <rslinckx@gmail.com>
6 //
7 // Copyright (C) 2005 Novell, Inc.
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a
10 // copy of this software and associated documentation files (the "Software"),
11 // to deal in the Software without restriction, including without limitation
12 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 // and/or sell copies of the Software, and to permit persons to whom the
14 // Software is furnished to do so, subject to the following conditions:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 // DEALINGS IN THE SOFTWARE.
28 using System;
29 using System.Collections;
30 using System.IO;
31 using Gtk;
32 using Glade;
33 using System.Threading;
34 using Beagle.Util;
35 using Mono.Unix;
37 namespace ImLogViewer {
39 public class ImLogWindow {
40 [Widget] Window imviewer;
41 [Widget] TreeView timelinetree;
42 [Widget] Label time_title;
43 [Widget] Entry search_entry;
44 [Widget] Button search_button;
45 [Widget] Button clear_button;
46 [Widget] TextView conversation;
47 [Widget] ScrolledWindow scrolledwindow;
49 private string speaking_to;
50 private string log_path;
51 private string highlight_text;
52 private string search_text;
54 private TreeStore tree_store;
55 private ThreadNotify index_thread_notify;
56 private Timeline timeline = new Timeline ();
58 private FileInfo initial_select_file;
59 private ImLog initial_select;
61 private ImClient client;
63 public ImLogWindow (ImClient client, string path, string search, string highlight)
65 this.client = client;
67 if (Directory.Exists (path)) {
68 log_path = path;
69 } else if (File.Exists (path)) {
70 log_path = Path.GetDirectoryName (path);
71 initial_select_file = new FileInfo (path);
72 } else {
73 Console.WriteLine ("ERROR: Log path doesn't exist - {0}", path);
74 return;
77 highlight_text = highlight;
78 search_text = search;
80 ShowWindow ();
83 private void SetStatusTitle (DateTime dt)
85 time_title.Markup = String.Format ("<b>{0}</b>", StringFu.DateTimeToPrettyString (dt));
88 private void SetWindowTitle (string speaker)
90 if (speaker == null || speaker.Length == 0)
91 return;
93 // Find the buddy
94 ImBuddy buddy = null;
96 if (client == ImClient.Gaim)
97 buddy = new GaimBuddyListReader ().Search (speaker);
98 else if (client == ImClient.Kopete)
99 buddy = new KopeteBuddyListReader ().Search (speaker);
101 if (speaker.EndsWith (".chat")) {
102 imviewer.Title = String.Format (Catalog.GetString ("Conversations in {0}"), speaker.Replace (".chat", String.Empty));
103 } else {
104 string nick = speaker;
106 if (buddy != null && buddy.Alias.Length > 0)
107 nick = buddy.Alias;
109 imviewer.Title = String.Format (Catalog.GetString ("Conversations with {0}"), nick);
112 speaking_to = speaker;
115 private void ShowWindow ()
117 Application.Init ();
119 Glade.XML gxml = new Glade.XML (null, "ImLogViewer.glade", "imviewer", null);
120 gxml.Autoconnect (this);
121 imviewer.Icon = Beagle.Images.GetPixbuf ("system-search.png");
123 conversation.PixelsAboveLines = 3;
124 conversation.LeftMargin = 4;
125 conversation.RightMargin = 4;
127 TextTag boldtag = new TextTag ("bold");
128 boldtag.Weight = Pango.Weight.Bold;
129 conversation.Buffer.TagTable.Add (boldtag);
131 TextTag highlight = new TextTag ("highlight");
132 highlight.Background = "yellow";
133 conversation.Buffer.TagTable.Add (highlight);
135 tree_store = new TreeStore (new Type[] {typeof (string), typeof (string), typeof (object)});
137 timelinetree.Model = tree_store;
138 timelinetree.AppendColumn ("Date", new CellRendererText(), "markup", 0);
139 timelinetree.AppendColumn ("Snippet", new CellRendererText(), "text", 1);
140 timelinetree.Selection.Changed += OnConversationSelected;
142 if (highlight_text != null)
143 search_entry.Text = highlight_text;
145 if (search_text != null)
146 Search (search_text);
148 search_entry.Activated += OnSearchClicked;
149 search_button.Clicked += OnSearchClicked;
150 clear_button.Clicked += OnClearClicked;
151 imviewer.DeleteEvent += new DeleteEventHandler (OnWindowDelete);
153 AccelGroup accel_group = new AccelGroup ();
154 GlobalKeybinder global_keys = new GlobalKeybinder (accel_group);
155 global_keys.AddAccelerator (OnWindowClose, (uint) Gdk.Key.Escape, 0, Gtk.AccelFlags.Visible);
156 imviewer.AddAccelGroup (accel_group);
158 // Index the logs
159 index_thread_notify = new ThreadNotify (new ReadyEvent (RepopulateTimeline));
160 Thread t = new Thread (new ThreadStart (IndexLogs));
161 t.Start ();
163 Application.Run();
166 private void IndexLogs ()
168 foreach (string file in Directory.GetFiles (log_path)) {
169 ImLog log = null;
170 StreamReader reader = new StreamReader (file);
172 if (client == ImClient.Gaim)
173 log = new GaimLog (new FileInfo (file), reader);
174 else if (client == ImClient.Kopete)
175 log = new KopeteLog (new FileInfo (file), reader);
177 reader.Close ();
179 if (initial_select_file != null && log.File.FullName == initial_select_file.FullName) {
180 initial_select = log;
181 initial_select_file = null;
184 if (speaking_to == null)
185 SetWindowTitle (log.SpeakingTo);
186 timeline.Add (log, log.StartTime);
189 index_thread_notify.WakeupMain ();
192 private bool LogContainsString (ImLog log, string text)
194 string [] words = text.Split (null);
196 //FIXME: This is very crude and EXPENSIVE!
197 foreach (string word in words) {
198 bool match = false;
200 foreach (ImLog.Utterance utt in log.Utterances) {
201 if (utt.Text.ToLower ().IndexOf (word.ToLower ()) != -1) {
202 match = true;
203 break;
207 if (!match) return false;
210 return true;
213 private string GetPreview (ImLog log)
215 string preview = null;
217 if (log.Utterances.Count == 0)
218 return String.Empty;
220 foreach (ImLog.Utterance utt in log.Utterances) {
221 string snippet = utt.Text;
222 int word_count = StringFu.CountWords (snippet, 15);
223 if (word_count > 3) {
224 preview = snippet;
225 break;
229 if (preview == null)
230 return ((ImLog.Utterance) log.Utterances [0]).Text;
232 if (preview.Length > 50)
233 return preview.Substring (0, 50) + "...";
234 return preview;
237 private void AddCategory (ArrayList list, string name, string date_format)
239 if (list.Count == 0)
240 return;
242 TreeIter parent = TreeIter.Zero;
244 foreach (ImLog log in list) {
245 if (search_text != null && search_text.Length > 0)
246 if (! LogContainsString (log, search_text))
247 continue;
249 if (parent.Equals(TreeIter.Zero))
250 parent = tree_store.AppendValues (String.Format ("<b>{0}</b>", Catalog.GetString (name)), String.Empty, null);
252 string date = log.StartTime.ToString (Catalog.GetString (date_format));
253 tree_store.AppendValues (parent, date, GetPreview (log), log);
257 private void SearchTimeline ()
259 // Remove all timeline entries that don't match the search results
261 ImLog selected = GetSelectedLog ();
263 TreeIter iter;
264 if (!tree_store.GetIterFirst (out iter))
265 return;
267 ArrayList to_remove = new ArrayList ();
269 do {
270 if (tree_store.IterHasChild (iter)) {
271 TreeIter child;
272 tree_store.IterNthChild (out child, iter, 0);
274 do {
275 ImLog log = tree_store.GetValue (child, 2) as ImLog;
276 if (LogContainsString (log, search_text))
277 continue;
279 to_remove.Add (tree_store.GetPath (child));
280 if (log == selected)
281 selected = null;
282 } while (tree_store.IterNext (ref child));
284 } while (tree_store.IterNext (ref iter));
286 for (int i = to_remove.Count - 1; i >= 0; i--) {
287 if (!tree_store.GetIter (out iter, to_remove [i] as TreePath))
288 break;
289 tree_store.Remove (ref iter);
292 // Remove all the categories that dont have any matches
293 tree_store.GetIterFirst (out iter);
295 do {
296 if (tree_store.IterNChildren (iter) < 1)
297 tree_store.Remove (ref iter);
298 } while (tree_store.IterNext (ref iter));
300 ScrollToLog (selected);
301 RenderConversation (selected);
304 private void RepopulateTimeline ()
306 RepopulateTimeline (true, 0);
309 private void RepopulateTimeline (bool reset, double vadj)
311 ImLog log;
313 if (!reset)
314 log = GetSelectedLog ();
315 else
316 log = initial_select;
318 tree_store.Clear ();
319 AddCategory (timeline.Today, "Today", "HH:mm");
320 AddCategory (timeline.Yesterday, "Yesterday", "HH:mm");
321 AddCategory (timeline.ThisWeek, "This Week", "dddd");
322 AddCategory (timeline.LastWeek, "Last Week", "dddd");
323 AddCategory (timeline.ThisMonth, "This Month", "MMM d");
324 AddCategory (timeline.ThisYear, "This Year", "MMM d");
325 AddCategory (timeline.Older, "Older", "yyy MMM d");
327 timelinetree.ExpandAll();
328 ScrollToLog (log);
329 RenderConversation (log);
331 if (!reset)
332 SetConversationScroll (vadj);
335 private void RenderConversation (ImLog im_log)
337 TextBuffer buffer = conversation.Buffer;
338 buffer.Clear ();
340 if (im_log == null) {
341 // Find the first (newest) conversation to render
342 TreeIter first_parent;
343 if (!tree_store.GetIterFirst (out first_parent))
344 return;
346 TreeIter child;
347 if (!tree_store.IterChildren (out child, first_parent))
348 return;
350 im_log = tree_store.GetValue (child, 2) as ImLog;
353 SetStatusTitle (im_log.StartTime);
355 TextTag bold = buffer.TagTable.Lookup ("bold");
357 TextIter end = buffer.EndIter;
359 foreach (ImLog.Utterance utt in im_log.Utterances) {
360 buffer.InsertWithTags (ref end, utt.Who + ":", new TextTag[] {bold});
361 buffer.Insert (ref end, String.Format(" {0}\n", utt.Text));
364 if (highlight_text != null)
365 HighlightSearchTerms (highlight_text);
367 if (search_text != null && search_text.Length > 0)
368 HighlightSearchTerms (search_text);
371 private void HighlightSearchTerms (string highlight)
373 TextBuffer buffer = conversation.Buffer;
374 string text = buffer.GetText (buffer.StartIter, buffer.EndIter, false).ToLower ();
375 string [] words = highlight.Split (' ');
376 bool scrolled = false;
378 foreach (string word in words) {
379 int idx = 0;
381 if (word == String.Empty)
382 continue;
384 while ((idx = text.IndexOf (word.ToLower (), idx)) != -1) {
385 Gtk.TextIter start = buffer.GetIterAtOffset (idx);
386 Gtk.TextIter end = start;
387 end.ForwardChars (word.Length);
388 if (!scrolled) {
389 scrolled = true;
390 TextMark mark = buffer.CreateMark (null, start, false);
391 conversation.ScrollMarkOnscreen (mark);
393 buffer.ApplyTag ("highlight", start, end);
395 idx += word.Length;
400 private void Search (string text)
402 search_entry.Text = text;
403 search_button.Visible = false;
404 clear_button.Visible = true;
405 search_entry.Sensitive = false;
407 search_text = text;
408 highlight_text = null;
411 private void OnConversationSelected (object o, EventArgs args)
413 TreeIter iter;
414 TreeModel model;
416 if (((TreeSelection)o).GetSelected (out model, out iter)) {
417 ImLog log = model.GetValue (iter, 2) as ImLog;
419 if (log == null)
420 return;
422 RenderConversation (log);
423 SetConversationScroll (0);
427 private void OnWindowClose (object o, EventArgs args)
429 Application.Quit ();
432 private void OnWindowDelete (object o, DeleteEventArgs args)
434 Application.Quit ();
437 private void OnSearchClicked (object o, EventArgs args)
439 if (search_entry.Text.Length == 0)
440 return;
442 Search (search_entry.Text);
443 SearchTimeline ();
446 private void SetConversationScroll (double vadj)
448 scrolledwindow.Vadjustment.Value = vadj;
449 scrolledwindow.Vadjustment.ChangeValue ();
450 scrolledwindow.Vadjustment.Change ();
453 private void ScrollToFirstLog ()
455 SelectPath (new TreePath (new int [] {0, 0}));
458 private void ScrollToLog (ImLog scroll_log)
460 if (scroll_log == null) {
461 ScrollToFirstLog ();
462 return;
465 TreeIter root_iter;
466 if (!tree_store.GetIterFirst (out root_iter))
467 return;
469 do {
470 if (! tree_store.IterHasChild (root_iter))
471 continue;
473 TreeIter child;
474 tree_store.IterNthChild (out child, root_iter, 0);
476 do {
477 ImLog log = tree_store.GetValue (child, 2) as ImLog;
479 if (log == scroll_log) {
480 SelectPath (tree_store.GetPath (child));
481 return;
483 } while (tree_store.IterNext (ref child));
484 } while (tree_store.IterNext (ref root_iter));
487 private void SelectPath (TreePath path)
489 timelinetree.Selection.Changed -= OnConversationSelected;
490 timelinetree.ExpandToPath (path);
491 timelinetree.Selection.SelectPath (path);
492 timelinetree.ScrollToCell (path, null, true, 0.5f, 0.0f);
493 timelinetree.Selection.Changed += OnConversationSelected;
496 private ImLog GetSelectedLog ()
498 TreeSelection selection = timelinetree.Selection;
499 TreeModel model;
500 TreeIter iter;
502 if (selection.GetSelected (out model, out iter))
503 return (ImLog) tree_store.GetValue (iter, 2);
504 return null;
507 private void OnClearClicked (object o, EventArgs args)
509 highlight_text = search_text = null;
510 search_button.Visible = true;
511 clear_button.Visible = false;
512 search_entry.Sensitive = true;
514 RepopulateTimeline (false, scrolledwindow.Vadjustment.Value);
515 ScrollToLog (GetSelectedLog ());