4 // Lukas Lipka <lukas@pmad.net>
5 // Raphael Slinckx <rslinckx@gmail.com>
7 // Copyright (C) 2005 Novell, Inc.
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.
29 using System
.Collections
;
33 using System
.Threading
;
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
)
67 if (Directory
.Exists (path
)) {
69 } else if (File
.Exists (path
)) {
70 log_path
= Path
.GetDirectoryName (path
);
71 initial_select_file
= new FileInfo (path
);
73 Console
.WriteLine ("ERROR: Log path doesn't exist - {0}", path
);
77 highlight_text
= highlight
;
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)
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
));
104 string nick
= speaker
;
106 if (buddy
!= null && buddy
.Alias
.Length
> 0)
109 imviewer
.Title
= String
.Format (Catalog
.GetString ("Conversations with {0}"), nick
);
112 speaking_to
= speaker
;
115 private void ShowWindow ()
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
);
159 index_thread_notify
= new ThreadNotify (new ReadyEvent (RepopulateTimeline
));
160 Thread t
= new Thread (new ThreadStart (IndexLogs
));
166 private void IndexLogs ()
168 foreach (string file
in Directory
.GetFiles (log_path
)) {
171 if (client
== ImClient
.Gaim
)
172 log
= new GaimLog (new FileInfo (file
), new StreamReader (file
));
173 else if (client
== ImClient
.Kopete
)
174 log
= new KopeteLog (new FileInfo (file
), new StreamReader (file
));
176 if (initial_select_file
!= null && log
.File
.FullName
== initial_select_file
.FullName
) {
177 initial_select
= log
;
178 initial_select_file
= null;
181 if (speaking_to
== null)
182 SetWindowTitle (log
.SpeakingTo
);
183 timeline
.Add (log
, log
.StartTime
);
186 index_thread_notify
.WakeupMain ();
189 private bool LogContainsString (ImLog log
, string text
)
191 string [] words
= text
.Split (null);
193 //FIXME: This is very crude and EXPENSIVE!
194 foreach (string word
in words
) {
197 foreach (ImLog
.Utterance utt
in log
.Utterances
) {
198 if (utt
.Text
.ToLower ().IndexOf (word
.ToLower ()) != -1) {
204 if (!match
) return false;
210 private string GetPreview (ImLog log
)
212 string preview
= null;
214 if (log
.Utterances
.Count
== 0)
217 foreach (ImLog
.Utterance utt
in log
.Utterances
) {
218 string snippet
= utt
.Text
;
219 int word_count
= StringFu
.CountWords (snippet
, 15);
220 if (word_count
> 3) {
227 return ((ImLog
.Utterance
) log
.Utterances
[0]).Text
;
229 if (preview
.Length
> 50)
230 return preview
.Substring (0, 50) + "...";
234 private void AddCategory (ArrayList list
, string name
, string date_format
)
239 TreeIter parent
= TreeIter
.Zero
;
241 foreach (ImLog log
in list
) {
242 if (search_text
!= null && search_text
.Length
> 0)
243 if (! LogContainsString (log
, search_text
))
246 if (parent
.Equals(TreeIter
.Zero
))
247 parent
= tree_store
.AppendValues (String
.Format ("<b>{0}</b>", Catalog
.GetString (name
)), String
.Empty
, null);
249 string date
= log
.StartTime
.ToString (Catalog
.GetString (date_format
));
250 tree_store
.AppendValues (parent
, date
, GetPreview (log
), log
);
254 private void SearchTimeline ()
256 // Remove all timeline entries that don't match the search results
258 ImLog selected
= GetSelectedLog ();
261 if (!tree_store
.GetIterFirst (out iter
))
264 ArrayList to_remove
= new ArrayList ();
267 if (tree_store
.IterHasChild (iter
)) {
269 tree_store
.IterNthChild (out child
, iter
, 0);
272 ImLog log
= tree_store
.GetValue (child
, 2) as ImLog
;
273 if (LogContainsString (log
, search_text
))
276 to_remove
.Add (tree_store
.GetPath (child
));
279 } while (tree_store
.IterNext (ref child
));
281 } while (tree_store
.IterNext (ref iter
));
283 for (int i
= to_remove
.Count
- 1; i
>= 0; i
--) {
284 if (!tree_store
.GetIter (out iter
, to_remove
[i
] as TreePath
))
286 tree_store
.Remove (ref iter
);
289 ScrollToLog (selected
);
290 RenderConversation (selected
);
293 private void RepopulateTimeline ()
295 RepopulateTimeline (true, 0);
298 private void RepopulateTimeline (bool reset
, double vadj
)
303 log
= GetSelectedLog ();
305 log
= initial_select
;
308 AddCategory (timeline
.Today
, "Today", "HH:mm");
309 AddCategory (timeline
.Yesterday
, "Yesterday", "HH:mm");
310 AddCategory (timeline
.ThisWeek
, "This Week", "dddd");
311 AddCategory (timeline
.LastWeek
, "Last Week", "dddd");
312 AddCategory (timeline
.ThisMonth
, "This Month", "MMM d");
313 AddCategory (timeline
.ThisYear
, "This Year", "MMM d");
314 AddCategory (timeline
.Older
, "Older", "yyy MMM d");
316 timelinetree
.ExpandAll();
318 RenderConversation (log
);
321 SetConversationScroll (vadj
);
324 private void RenderConversation (ImLog im_log
)
326 TextBuffer buffer
= conversation
.Buffer
;
329 if (im_log
== null) {
330 // Find the first (newest) conversation to render
331 TreeIter first_parent
;
332 if (!tree_store
.GetIterFirst (out first_parent
))
336 if (!tree_store
.IterChildren (out child
, first_parent
))
339 im_log
= tree_store
.GetValue (child
, 2) as ImLog
;
342 SetStatusTitle (im_log
.StartTime
);
344 TextTag bold
= buffer
.TagTable
.Lookup ("bold");
346 TextIter end
= buffer
.EndIter
;
348 foreach (ImLog
.Utterance utt
in im_log
.Utterances
) {
349 buffer
.InsertWithTags (ref end
, utt
.Who
+ ":", new TextTag
[] {bold}
);
350 buffer
.Insert (ref end
, String
.Format(" {0}\n", utt
.Text
));
353 if (highlight_text
!= null)
354 HighlightSearchTerms (highlight_text
);
356 if (search_text
!= null && search_text
.Length
> 0)
357 HighlightSearchTerms (search_text
);
360 private void HighlightSearchTerms (string highlight
)
362 TextBuffer buffer
= conversation
.Buffer
;
363 string text
= buffer
.GetText (buffer
.StartIter
, buffer
.EndIter
, false).ToLower ();
364 string [] words
= highlight
.Split (' ');
365 bool scrolled
= false;
367 foreach (string word
in words
) {
370 if (word
== String
.Empty
)
373 while ((idx
= text
.IndexOf (word
.ToLower (), idx
)) != -1) {
374 Gtk
.TextIter start
= buffer
.GetIterAtOffset (idx
);
375 Gtk
.TextIter end
= start
;
376 end
.ForwardChars (word
.Length
);
379 TextMark mark
= buffer
.CreateMark (null, start
, false);
380 conversation
.ScrollMarkOnscreen (mark
);
382 buffer
.ApplyTag ("highlight", start
, end
);
389 private void Search (string text
)
391 search_entry
.Text
= text
;
392 search_button
.Visible
= false;
393 clear_button
.Visible
= true;
394 search_entry
.Sensitive
= false;
397 highlight_text
= null;
400 private void OnConversationSelected (object o
, EventArgs args
)
405 if (((TreeSelection
)o
).GetSelected (out model
, out iter
)) {
406 ImLog log
= model
.GetValue (iter
, 2) as ImLog
;
411 RenderConversation (log
);
412 SetConversationScroll (0);
416 private void OnWindowClose (object o
, EventArgs args
)
421 private void OnWindowDelete (object o
, DeleteEventArgs args
)
426 private void OnSearchClicked (object o
, EventArgs args
)
428 if (search_entry
.Text
.Length
== 0)
431 Search (search_entry
.Text
);
435 private void SetConversationScroll (double vadj
)
437 scrolledwindow
.Vadjustment
.Value
= vadj
;
438 scrolledwindow
.Vadjustment
.ChangeValue ();
439 scrolledwindow
.Vadjustment
.Change ();
442 private void ScrollToFirstLog ()
444 SelectPath (new TreePath (new int [] {0, 0}
));
447 private void ScrollToLog (ImLog scroll_log
)
449 if (scroll_log
== null) {
455 if (!tree_store
.GetIterFirst (out root_iter
))
459 if (! tree_store
.IterHasChild (root_iter
))
463 tree_store
.IterNthChild (out child
, root_iter
, 0);
466 ImLog log
= tree_store
.GetValue (child
, 2) as ImLog
;
468 if (log
== scroll_log
) {
469 SelectPath (tree_store
.GetPath (child
));
472 } while (tree_store
.IterNext (ref child
));
473 } while (tree_store
.IterNext (ref root_iter
));
476 private void SelectPath (TreePath path
)
478 timelinetree
.Selection
.Changed
-= OnConversationSelected
;
479 timelinetree
.ExpandToPath (path
);
480 timelinetree
.Selection
.SelectPath (path
);
481 timelinetree
.ScrollToCell (path
, null, true, 0.5f
, 0.0f
);
482 timelinetree
.Selection
.Changed
+= OnConversationSelected
;
485 private ImLog
GetSelectedLog ()
487 TreeSelection selection
= timelinetree
.Selection
;
491 if (selection
.GetSelected (out model
, out iter
))
492 return (ImLog
) tree_store
.GetValue (iter
, 2);
496 private void OnClearClicked (object o
, EventArgs args
)
498 highlight_text
= search_text
= null;
499 search_button
.Visible
= true;
500 clear_button
.Visible
= false;
501 search_entry
.Sensitive
= true;
503 RepopulateTimeline (false, scrolledwindow
.Vadjustment
.Value
);
504 ScrollToLog (GetSelectedLog ());