/*
* Copyright (C) 2006 Eskil Bylund
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
using System;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using Gtk;
using Glade;
using Mono.Unix;
using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Backend.Managers;
using DCSharp.Extras;
namespace DCSharp.GUI{
public class HubPage : Page
{
private VisibleColumnsWindow columnsWindow;
private ArrayList addQueue;
private ArrayList updateQueue;
private bool handlersAdded;
private TextMark helperStartMark;
private TextMark helperEndMark;
private Widget separator;
private TextTag nickTag;
private bool hovering;
private static Gdk.Cursor handCursor;
private static Gdk.Cursor normalCursor;
private UserStore userStore;
private TreeModelFilter filterModel;
private TreeModelSort sortModel;
private SortType sortOrder;
private int sortColumnId;
private uint searchTimeoutId;
[Widget]
private Box mainBox;
[Widget]
private HPaned hpaned;
[Widget]
private ChatView chatView;
[Widget]
private Entry messageEntry;
[Widget]
private Entry searchEntry;
[Widget]
private UserView userView;
#region Constructors
static HubPage()
{
handCursor = new Gdk.Cursor(Gdk.CursorType.Hand2);
normalCursor = new Gdk.Cursor(Gdk.CursorType.Xterm);
}
public HubPage(HubConnection hub) : base("HubPage.glade")
{
if (hub == null)
{
throw new ArgumentNullException("hub");
}
addQueue = ArrayList.Synchronized(new ArrayList());
updateQueue = ArrayList.Synchronized(new ArrayList());
// User interface
ActionEntry[] entries = new ActionEntry[] {
new ActionEntry("VisibleColumns", null, Catalog.GetString("_Visible Columns..."), null,
Catalog.GetString("Edit visible columns"),
OnVisibleColumns),
};
actionGroup = new ActionGroup("DownloadPageActions");
actionGroup.Add(entries);
// Widgets
nickTag = chatView.Buffer.TagTable.Lookup("nick");
TextTag tag = new TextTag("separator");
tag.Justification = Justification.Center;
chatView.Buffer.TagTable.Add(tag);
chatView.MotionNotifyEvent += OnChatViewMotionNotifyEvent;
chatView.WidgetEvent += OnChatViewWidgetEvent;
chatView.Show();
// User view
userStore = new UserStore();
filterModel = new TreeModelFilter(userStore, null);
filterModel.VisibleFunc = FilterUsers;
sortModel = new TreeModelSort(filterModel);
sortModel.SetSortColumnId((int)UserStore.Column.Nick,
SortType.Ascending);
sortModel.SetSortFunc((int)UserStore.Column.ShareSize, SortByShareSize);
// TODO: Enable custom sorting once the bug in GTK+ causing a
// segfault when sorting is fixed.
//sortModel.SetSortFunc((int)UserStore.Column.Nick, UserSortFunc);
userView.Model = sortModel;
userView.SetSizeRequest(140, 0);
userView.Show();
userView.GetColumn(1).Visible = false;
// Message entry
messageEntry = new UserEntry();
// Modifying a TreeModel connected to a EntryCompletion can be
// incredibly slow!
messageEntry.FocusInEvent += delegate
{
messageEntry.Completion.Model = userStore;
};
messageEntry.FocusOutEvent += delegate
{
messageEntry.Completion.Model = null;
};
messageEntry.Activated += OnMessageEntryActivated;
messageEntry.Show();
messageEntry.GrabFocus();
mainBox.PackEnd(messageEntry, false, false, 0);
// Search entry
searchEntry.Changed += OnSearchEntryChanged;
searchEntry.Show();
Hub = hub;
}
#endregion
#region Classes
private class UserEntry : Entry
{
public UserEntry()
{
Completion = new NickEntryCompletion();
Completion.TextColumn = (int)UserStore.Column.Nick;
}
// This is the same hack as the GtkFileChooserEntry uses to keep
// the focus in the entry when pressing tab.
protected override bool OnFocused(DirectionType direction)
{
bool controlPressed = false;
Gdk.ModifierType state;
if (Gtk.Global.GetCurrentEventState(out state))
{
controlPressed = (state & Gdk.ModifierType.ControlMask) ==
Gdk.ModifierType.ControlMask;
}
if (direction == DirectionType.TabForward && HasFocus &&
!controlPressed)
{
Position = -1;
return true;
}
return base.OnFocused(direction);
}
}
private class NickEntryCompletion : EntryCompletion
{
public NickEntryCompletion()
{
InlineCompletion = true;
PopupCompletion = true;
}
protected override bool OnMatchSelected(TreeModel model,
TreeIter iter)
{
Entry entry = Entry as Entry;
if (TextColumn >= 0)
{
string nick = model.GetValue(iter, TextColumn) as string;
entry.Text = nick + ": ";
entry.Position = -1;
return true;
}
return false;
}
}
#endregion
#region Properties
public override string Title
{
get { return Util.GetHubName(hub); }
}
public override Gdk.Pixbuf Icon
{
get { return RenderIcon(Stock.Network, IconSize.Menu, null); }
}
public override string Tooltip
{
get
{
string title = Util.GetHubName(hub);
if (title != hub.Hostname)
{
title += " (" + hub.Hostname + ")";
}
if (hub.Name != null)
{
title += "\n";
title += hub.Name;
}
return title;
}
}
public override ActionGroup Actions
{
get { return actionGroup; }
}
private ActionGroup actionGroup;
public int PanePosition
{
get { return hpaned.Position; }
set { hpaned.Position = value; }
}
public int VisibleColumns
{
get { return Util.GetVisibleColumns(userView.Columns); }
set { Util.SetVisibleColumns(userView.Columns, value | 1); }
}
/// <summary>
/// Gets or sets whether or not to show the new message separator on a
/// new message.
/// <summary>
public bool ShowSeparatorOnMessage
{
get { return showSeparator; }
set { showSeparator = value; }
}
private bool showSeparator;
public HubConnection Hub
{
get { return hub; }
set
{
if (hub != null)
{
hub.Message -= OnHubMessage;
hub.NameChanged -= OnHubNameChanged;
hub.StateChanged -= OnHubStateChanged;
hub.Error -= OnHubError;
}
hub = value;
if (hub != null)
{
hub.Message += OnHubMessage;
hub.NameChanged += OnHubNameChanged;
hub.StateChanged += OnHubStateChanged;
hub.Error += OnHubError;
}
}
}
private HubConnection hub;
#endregion
#region Methods
public override void Dispose()
{
HubConnection hub = this.hub;
if (hub != null)
{
// Remove the event handlers
Hub = null;
hub.Disconnect();
}
if (columnsWindow != null)
{
columnsWindow.Destroy();
columnsWindow = null;
}
base.Dispose();
}
/// <summary>
/// Updates the position of the new message separator, moving it to the
/// end of the chat view.
/// </summary>
public void UpdateNewMessageSeparator()
{
// TODO: Move code to the ChatView?
TextIter startIter;
TextIter endIter;
TextBuffer buffer = chatView.Buffer;
// Remove the previous anchor
if (helperStartMark != null && helperEndMark != null)
{
startIter = chatView.Buffer.GetIterAtMark(helperStartMark);
endIter = chatView.Buffer.GetIterAtMark(helperEndMark);
buffer.Delete(ref startIter, ref endIter);
buffer.DeleteMark(helperStartMark);
buffer.DeleteMark(helperEndMark);
}
// Create a new anchor
endIter = chatView.Buffer.EndIter;
helperStartMark = buffer.CreateMark(null, endIter, true);
TextChildAnchor anchor = buffer.CreateChildAnchor(ref endIter);
buffer.Insert(ref endIter, "\n");
startIter = buffer.GetIterAtMark(helperStartMark);
buffer.ApplyTag("separator", startIter, endIter);
helperEndMark = buffer.CreateMark(null, endIter, true);
// Attach the separator to the anchor
if (separator == null)
{
separator = new HSeparator();
separator.SetSizeRequest(200, -1);
}
chatView.AddChildAtAnchor(separator, anchor);
}
protected override bool OnKeyPressEvent(Gdk.EventKey evnt)
{
if (evnt.Key == Gdk.Key.Escape && hub != null &&
hub.State == ConnectionState.Connecting)
{
hub.Disconnect();
return true;
}
return base.OnKeyPressEvent(evnt);
}
protected void DisplayMessage(Identity user, string message)
{
// Display the new message separator
if (showSeparator && separator != null)
{
separator.Show();
}
if (user != null)
{
bool self = user.Nick == hub.LocalIdentity.Nick;
chatView.InsertMessage(user.Nick, message, self);
}
else
{
chatView.InsertText(message);
}
OnPageChanged();
}
protected void DisplayState(ConnectionState state)
{
string message;
switch (state)
{
case ConnectionState.Connected:
IsWorking = false;
message = Catalog.GetString("Connected");
break;
case ConnectionState.Disconnected:
IsWorking = false;
message = Catalog.GetString("Disconnected");
break;
default:
IsWorking = true;
message = String.Format(
Catalog.GetString("Connecting to {0}..."),
hub.Hostname);
break;
}
chatView.InsertStatus(message);
OnPageChanged();
Status = message;
}
protected void DisplayError(ConnectionError error)
{
string message;
switch (error)
{
case ConnectionError.Dns:
message = String.Format(
Catalog.GetString("Could not resolve the address {0}"),
hub.Hostname);
break;
case ConnectionError.Timeout:
message = Catalog.GetString("The connection timed out");
break;
default:
// FIXME: Should be "Could not connect to {0}".
message = Catalog.GetString("There was an error connecting");
break;
}
chatView.InsertError(message);
OnPageChanged();
}
protected void UpdateStatus()
{
if (hub != null && hub.State == ConnectionState.Connected)
{
int count = hub.Users.Count;
string status = Catalog.GetPluralString("{0} user online",
"{0} users online", count);
status = String.Format(status, count);
Status = Catalog.GetString("Connected") + ". " + status;
}
}
private bool FilterUsers(TreeModel model, TreeIter iter)
{
if (searchEntry.Text.Length == 0)
{
return true;
}
Identity user = model.GetValue(iter,
(int)UserStore.Column.User) as Identity;
if (user != null)
{
CultureInfo culture = CultureInfo.CurrentCulture;
return culture.CompareInfo.IndexOf(user.Nick, searchEntry.Text,
CompareOptions.IgnoreCase) >= 0;
}
return false;
}
private int UserSortFunc(TreeModel model, TreeIter tia, TreeIter tib)
{
Identity userA = model.GetValue(tia, (int)UserStore.Column.User) as Identity;
Identity userB = model.GetValue(tib, (int)UserStore.Column.User) as Identity;
if (userA != null && userB != null)
{
// Put operators before regular users
if (userA.Op && !userB.Op)
{
return -1;
}
if (!userA.Op && userB.Op)
{
return 1;
}
return String.Compare(userA.Nick, userB.Nick);
}
// Fallback
string namea = model.GetValue(tia, (int)UserStore.Column.Nick) as string;
string nameb = model.GetValue(tib, (int)UserStore.Column.Nick) as string;
return String.Compare(namea, nameb);
}
private void IdleAdd(Identity user)
{
bool addIdleHandler = addQueue.Count == 0;
addQueue.Add(user);
if (addIdleHandler)
{
GLib.Idle.Add(AddUsersInQueue);
}
}
private void IdleUpdate(Identity user)
{
bool addIdleHandler = updateQueue.Count == 0;
updateQueue.Add(user);
if (addIdleHandler)
{
GLib.Idle.Add(UpdateUsersInQueue);
}
}
private bool AddUsersInQueue()
{
long ticks = DateTime.Now.Ticks;
DisableSorting();
while (addQueue.Count > 0 &&
hub.State == ConnectionState.Connected)
{
userStore.AddUser((Identity)addQueue[0], hub);
addQueue.RemoveAt(0);
// Are we blocking the GUI?
if ((DateTime.Now.Ticks - ticks) >= 500000)
{
RestoreSorting();
UpdateStatus();
return true;
}
}
RestoreSorting();
UpdateStatus();
return false;
}
private bool UpdateUsersInQueue()
{
long ticks = DateTime.Now.Ticks;
DisableSorting();
while (updateQueue.Count > 0 &&
hub.State == ConnectionState.Connected)
{
Identity user = (Identity)updateQueue[0];
updateQueue.Remove(user);
// User disconnected
if (!hub.Users.Contains(user))
{
addQueue.Remove(user);
userStore.RemoveUser(user);
}
// Are we blocking the GUI?
if ((DateTime.Now.Ticks - ticks) >= 500000)
{
RestoreSorting();
UpdateStatus();
return true;
}
}
RestoreSorting();
UpdateStatus();
return false;
}
private void DisableSorting()
{
if (sortModel.GetSortColumnId(out sortColumnId, out sortOrder))
{
sortModel.SetSortColumnId(-1, SortType.Ascending);
}
}
private void RestoreSorting()
{
sortModel.SetSortColumnId(sortColumnId, sortOrder);
}
private int SortByShareSize(TreeModel model, TreeIter a, TreeIter b)
{
Identity userA = (Identity)model.GetValue(a,
(int)UserStore.Column.User);
Identity userB = (Identity)model.GetValue(b,
(int)UserStore.Column.User);
return userA.ShareSize.CompareTo(userB.ShareSize);
}
private void OnChatViewWidgetEvent(object obj, WidgetEventArgs args)
{
Gdk.EventButton evnt = args.Event as Gdk.EventButton;
if (evnt == null || evnt.Type != Gdk.EventType.ButtonPress ||
evnt.Button != 1)
{
return;
}
int x, y;
chatView.WindowToBufferCoords(TextWindowType.Text, (int)evnt.X,
(int)evnt.Y, out x, out y);
TextIter iter = chatView.GetIterAtLocation(x, y);
TextIter end = iter;
if (iter.HasTag(nickTag) && iter.BackwardToTagToggle(nickTag) &&
end.ForwardToTagToggle(nickTag))
{
string nick = iter.GetText(end);
nick = nick.Substring(0, nick.IndexOf(":"));
Identity user = GetUser(nick);
TreeIter treeIter;
if (user != null && userStore.GetIter(user, out treeIter))
{
treeIter = filterModel.ConvertChildIterToIter(treeIter);
treeIter = sortModel.ConvertChildIterToIter(treeIter);
userView.Selection.SelectIter(treeIter);
TreePath path = userView.Model.GetPath(treeIter);
userView.ScrollToCell(path, userView.Columns[0], true,
0.5f, 0f);
}
else
{
userView.Selection.UnselectAll();
}
}
}
private void OnChatViewMotionNotifyEvent(object obj,
MotionNotifyEventArgs args)
{
int pointerX, pointerY;
Gdk.ModifierType pointerMask;
Gdk.Window window = chatView.GetWindow(TextWindowType.Text);
window.GetPointer(out pointerX, out pointerY, out pointerMask);
int x, y;
chatView.WindowToBufferCoords(TextWindowType.Widget, pointerX,
pointerY, out x, out y);
TextIter iter = chatView.GetIterAtLocation(x, y);
bool hovering = iter.HasTag(nickTag);
if (hovering != this.hovering)
{
this.hovering = hovering;
if (this.hovering)
{
window.Cursor = handCursor;
}
else
{
window.Cursor = normalCursor;
}
}
}
private Identity GetUser(string nick)
{
if (hub == null)
{
return null;
}
return hub.Users.Find(delegate(Identity user)
{
return user.Nick == nick;
});
}
private void OnMessageEntryActivated(object obj, EventArgs args)
{
string message = messageEntry.Text.Trim();
if (hub != null && message.Length > 0)
{
messageEntry.Text = String.Empty;
hub.SendMessage(message);
}
}
private void OnSearchEntryChanged(object obj, EventArgs args)
{
if (searchTimeoutId > 0)
{
GLib.Source.Remove(searchTimeoutId);
}
searchTimeoutId = GLib.Timeout.Add(300, delegate
{
filterModel.Refilter();
return false;
});
}
private void OnVisibleColumns(object obj, EventArgs args)
{
if (columnsWindow == null)
{
columnsWindow = new VisibleColumnsWindow(userView.Columns,
Toplevel as Window, userView.Columns[0]);
columnsWindow.Window.Destroyed += delegate
{
columnsWindow = null;
};
}
columnsWindow.Show();
}
private void OnHubMessage(object obj, MessageEventArgs args)
{
Application.Invoke(delegate
{
DisplayMessage(args.From, args.Message);
});
}
private void OnHubNameChanged(object obj, EventArgs args)
{
string name = ((HubConnection)obj).Name;
Application.Invoke(delegate
{
string status = String.Format(
Catalog.GetString("The topic is: {0}"), name);
chatView.InsertStatus(status);
OnPageChanged();
});
}
private void OnHubStateChanged(object obj, EventArgs args)
{
ConnectionState state = hub.State;
// Handle the user events
if (state == ConnectionState.Connected)
{
if (!handlersAdded)
{
hub.Users.UserAdded += OnUserAdded;
hub.Users.UserRemoved += OnUserRemoved;
hub.Users.UserChanged += OnUserChanged;
handlersAdded = true;
}
}
else if (state == ConnectionState.Disconnected)
{
hub.Users.UserAdded -= OnUserAdded;
hub.Users.UserRemoved -= OnUserRemoved;
hub.Users.UserChanged -= OnUserChanged;
handlersAdded = false;
}
// Update the GUI
Application.Invoke(delegate
{
if (state == ConnectionState.Connected)
{
messageEntry.Sensitive = true;
}
else if (state == ConnectionState.Disconnected)
{
addQueue.Clear();
updateQueue.Clear();
userStore.Clear();
messageEntry.Sensitive = false;
}
DisplayState(state);
});
}
private void OnHubError(object obj, ConnectionErrorEventArgs args)
{
Application.Invoke(delegate
{
DisplayError(args.Error);
});
}
private void OnUserAdded(object obj, UserEventArgs args)
{
IdleAdd(args.User);
}
private void OnUserRemoved(object obj, UserEventArgs args)
{
IdleUpdate(args.User);
}
private void OnUserChanged(object obj, UserEventArgs args)
{
IdleUpdate(args.User);
}
#endregion
}
}
|