2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System.Collections.Generic ;
2018-12-20 20:06:40 +08:00
using System.Linq ;
2018-11-20 15:51:59 +08:00
using osuTK ;
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
using osu.Framework.Graphics.UserInterface ;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events ;
2018-04-13 17:19:50 +08:00
using osu.Game.Configuration ;
using osu.Game.Graphics ;
using osu.Game.Graphics.Containers ;
using osu.Game.Graphics.UserInterface ;
using osu.Game.Online.Chat ;
using osu.Game.Overlays.Chat ;
2018-07-10 01:42:57 +08:00
using osu.Game.Overlays.Chat.Selection ;
2018-07-30 03:18:37 +08:00
using osu.Game.Overlays.Chat.Tabs ;
2018-12-20 20:06:40 +08:00
using osuTK.Input ;
2019-10-07 01:22:55 +08:00
using osu.Framework.Graphics.Sprites ;
2019-12-26 10:32:40 +08:00
using System ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Overlays
{
2018-04-14 19:31:03 +08:00
public class ChatOverlay : OsuFocusedOverlayContainer
2018-04-13 17:19:50 +08:00
{
private const float textbox_height = 60 ;
private const float channel_selection_min_height = 0.3f ;
2018-04-14 19:31:03 +08:00
private ChannelManager channelManager ;
2018-04-13 17:19:50 +08:00
2019-06-20 22:02:02 +08:00
private Container < DrawableChannel > currentChannelContainer ;
2018-07-10 00:23:40 +08:00
private readonly List < DrawableChannel > loadedChannels = new List < DrawableChannel > ( ) ;
2018-04-13 17:19:50 +08:00
2019-06-20 22:02:02 +08:00
private LoadingAnimation loading ;
2018-04-13 17:19:50 +08:00
2019-06-20 22:02:02 +08:00
private FocusedTextBox textbox ;
2018-04-13 17:19:50 +08:00
private const int transition_length = 500 ;
public const float DEFAULT_HEIGHT = 0.4f ;
public const float TAB_AREA_HEIGHT = 50 ;
2019-06-25 18:52:31 +08:00
protected ChannelTabControl ChannelTabControl ;
protected virtual ChannelTabControl CreateChannelTabControl ( ) = > new ChannelTabControl ( ) ;
2018-04-13 17:19:50 +08:00
2019-06-20 22:02:02 +08:00
private Container chatContainer ;
private TabsArea tabsArea ;
private Box chatBackground ;
private Box tabBackground ;
2018-04-13 17:19:50 +08:00
2019-09-03 06:50:52 +08:00
public Bindable < float > ChatHeight { get ; set ; }
2018-04-13 17:19:50 +08:00
2019-06-20 22:02:02 +08:00
private Container channelSelectionContainer ;
2019-06-25 18:52:31 +08:00
protected ChannelSelectionOverlay ChannelSelectionOverlay ;
2018-04-13 17:19:50 +08:00
2019-12-26 10:32:40 +08:00
private Message highlightingMessage { get ; set ; }
2019-06-25 18:52:31 +08:00
public override bool Contains ( Vector2 screenSpacePos ) = > chatContainer . ReceivePositionalInputAt ( screenSpacePos )
| | ( ChannelSelectionOverlay . State . Value = = Visibility . Visible & & ChannelSelectionOverlay . ReceivePositionalInputAt ( screenSpacePos ) ) ;
2018-04-13 17:19:50 +08:00
public ChatOverlay ( )
{
RelativeSizeAxes = Axes . Both ;
RelativePositionAxes = Axes . Both ;
Anchor = Anchor . BottomLeft ;
Origin = Anchor . BottomLeft ;
2019-06-20 22:02:02 +08:00
}
2018-04-13 17:19:50 +08:00
2019-06-20 22:02:02 +08:00
[BackgroundDependencyLoader]
private void load ( OsuConfigManager config , OsuColour colours , ChannelManager channelManager )
{
2018-04-13 17:19:50 +08:00
const float padding = 5 ;
Children = new Drawable [ ]
{
channelSelectionContainer = new Container
{
RelativeSizeAxes = Axes . Both ,
Height = 1f - DEFAULT_HEIGHT ,
Masking = true ,
Children = new [ ]
{
2019-06-25 18:52:31 +08:00
ChannelSelectionOverlay = new ChannelSelectionOverlay
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes . Both ,
} ,
} ,
} ,
chatContainer = new Container
{
Name = @"chat container" ,
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
RelativeSizeAxes = Axes . Both ,
Height = DEFAULT_HEIGHT ,
Children = new [ ]
{
new Container
{
Name = @"chat area" ,
RelativeSizeAxes = Axes . Both ,
Padding = new MarginPadding { Top = TAB_AREA_HEIGHT } ,
Children = new Drawable [ ]
{
chatBackground = new Box
{
RelativeSizeAxes = Axes . Both ,
} ,
2018-07-10 02:13:34 +08:00
currentChannelContainer = new Container < DrawableChannel >
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes . Both ,
Padding = new MarginPadding
{
Bottom = textbox_height
} ,
} ,
new Container
{
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
RelativeSizeAxes = Axes . X ,
Height = textbox_height ,
Padding = new MarginPadding
{
Top = padding * 2 ,
Bottom = padding * 2 ,
Left = ChatLine . LEFT_PADDING + padding * 2 ,
Right = padding * 2 ,
} ,
Children = new Drawable [ ]
{
textbox = new FocusedTextBox
{
RelativeSizeAxes = Axes . Both ,
Height = 1 ,
PlaceholderText = "type your message" ,
OnCommit = postMessage ,
ReleaseFocusOnCommit = false ,
HoldFocus = true ,
}
}
} ,
loading = new LoadingAnimation ( ) ,
}
} ,
2018-10-02 13:41:18 +08:00
tabsArea = new TabsArea
2018-04-13 17:19:50 +08:00
{
Children = new Drawable [ ]
{
tabBackground = new Box
{
RelativeSizeAxes = Axes . Both ,
Colour = Color4 . Black ,
} ,
2019-10-07 01:22:55 +08:00
new SpriteIcon
{
Icon = FontAwesome . Solid . Comments ,
Anchor = Anchor . CentreLeft ,
Origin = Anchor . CentreLeft ,
Size = new Vector2 ( 20 ) ,
Margin = new MarginPadding ( 10 ) ,
} ,
2019-06-25 18:52:31 +08:00
ChannelTabControl = CreateChannelTabControl ( ) . With ( d = >
2019-06-26 18:25:54 +08:00
{
d . Anchor = Anchor . BottomLeft ;
d . Origin = Anchor . BottomLeft ;
d . RelativeSizeAxes = Axes . Both ;
d . OnRequestLeave = channelManager . LeaveChannel ;
} ) ,
2018-04-13 17:19:50 +08:00
}
} ,
} ,
} ,
} ;
2019-06-25 18:52:31 +08:00
ChannelTabControl . Current . ValueChanged + = current = > channelManager . CurrentChannel . Value = current . NewValue ;
ChannelTabControl . ChannelSelectorActive . ValueChanged + = active = > ChannelSelectionOverlay . State . Value = active . NewValue ? Visibility . Visible : Visibility . Hidden ;
ChannelSelectionOverlay . State . ValueChanged + = state = >
2018-04-13 17:19:50 +08:00
{
2019-06-25 18:52:31 +08:00
// Propagate the visibility state to ChannelSelectorActive
ChannelTabControl . ChannelSelectorActive . Value = state . NewValue = = Visibility . Visible ;
2018-04-13 17:19:50 +08:00
2019-06-11 13:28:52 +08:00
if ( state . NewValue = = Visibility . Visible )
2018-04-13 17:19:50 +08:00
{
textbox . HoldFocus = false ;
if ( 1f - ChatHeight . Value < channel_selection_min_height )
2018-05-02 17:26:23 +08:00
this . TransformBindableTo ( ChatHeight , 1f - channel_selection_min_height , 800 , Easing . OutQuint ) ;
2018-04-13 17:19:50 +08:00
}
else
textbox . HoldFocus = true ;
} ;
2018-11-13 14:20:40 +08:00
2019-06-25 18:52:31 +08:00
ChannelSelectionOverlay . OnRequestJoin = channel = > channelManager . JoinChannel ( channel ) ;
ChannelSelectionOverlay . OnRequestLeave = channelManager . LeaveChannel ;
2019-06-20 22:02:02 +08:00
2019-09-03 06:50:52 +08:00
ChatHeight = config . GetBindable < float > ( OsuSetting . ChatDisplayHeight ) ;
ChatHeight . BindValueChanged ( height = >
2019-06-20 22:02:02 +08:00
{
2019-09-03 06:50:52 +08:00
chatContainer . Height = height . NewValue ;
channelSelectionContainer . Height = 1f - height . NewValue ;
tabBackground . FadeTo ( height . NewValue = = 1f ? 1f : 0.8f , 200 ) ;
} , true ) ;
2019-06-20 22:02:02 +08:00
chatBackground . Colour = colours . ChatBlue ;
this . channelManager = channelManager ;
loading . Show ( ) ;
// This is a relatively expensive (and blocking) operation.
// Scheduling it ensures that it won't be performed unless the user decides to open chat.
// TODO: Refactor OsuFocusedOverlayContainer / OverlayContainer to support delayed content loading.
Schedule ( ( ) = >
{
// TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
channelManager . JoinedChannels . ItemsAdded + = onChannelAddedToJoinedChannels ;
channelManager . JoinedChannels . ItemsRemoved + = onChannelRemovedFromJoinedChannels ;
foreach ( Channel channel in channelManager . JoinedChannels )
2019-06-25 18:52:31 +08:00
ChannelTabControl . AddChannel ( channel ) ;
2019-06-20 22:02:02 +08:00
channelManager . AvailableChannels . ItemsAdded + = availableChannelsChanged ;
channelManager . AvailableChannels . ItemsRemoved + = availableChannelsChanged ;
2019-06-25 18:52:31 +08:00
ChannelSelectionOverlay . UpdateAvailableChannels ( channelManager . AvailableChannels ) ;
2019-06-20 22:02:02 +08:00
currentChannel = channelManager . CurrentChannel . GetBoundCopy ( ) ;
currentChannel . BindValueChanged ( currentChannelChanged , true ) ;
} ) ;
2018-04-14 19:31:03 +08:00
}
2019-06-20 22:02:02 +08:00
private Bindable < Channel > currentChannel ;
2019-02-21 17:56:34 +08:00
private void currentChannelChanged ( ValueChangedEvent < Channel > e )
2018-04-14 19:31:03 +08:00
{
2019-02-21 17:56:34 +08:00
if ( e . NewValue = = null )
2018-04-14 19:31:03 +08:00
{
textbox . Current . Disabled = true ;
2018-07-10 02:13:34 +08:00
currentChannelContainer . Clear ( false ) ;
2019-06-25 18:52:31 +08:00
ChannelSelectionOverlay . Show ( ) ;
2018-04-14 19:31:03 +08:00
return ;
}
2019-05-12 07:21:12 +08:00
2019-05-12 18:31:11 +08:00
if ( e . NewValue is ChannelSelectorTabItem . ChannelSelectorTabChannel )
2019-05-12 07:16:15 +08:00
return ;
2018-04-14 19:31:03 +08:00
2019-02-21 17:56:34 +08:00
textbox . Current . Disabled = e . NewValue . ReadOnly ;
2018-04-14 19:31:03 +08:00
2019-06-25 18:52:31 +08:00
if ( ChannelTabControl . Current . Value ! = e . NewValue )
Scheduler . Add ( ( ) = > ChannelTabControl . Current . Value = e . NewValue ) ;
2018-04-14 19:31:03 +08:00
2019-12-26 10:32:40 +08:00
var loaded = GetChannelDrawable ( e . NewValue ) ;
2019-04-01 11:16:05 +08:00
2018-04-14 19:31:03 +08:00
if ( loaded = = null )
{
2018-07-10 02:13:34 +08:00
currentChannelContainer . FadeOut ( 500 , Easing . OutQuint ) ;
2018-04-14 19:31:03 +08:00
loading . Show ( ) ;
2019-12-26 10:32:40 +08:00
loaded = loadChannelDrawable ( e . NewValue ) ;
2018-04-14 19:31:03 +08:00
LoadComponentAsync ( loaded , l = >
{
2019-08-05 18:23:13 +08:00
if ( currentChannel . Value ! = e . NewValue )
return ;
2018-04-14 19:31:03 +08:00
loading . Hide ( ) ;
2018-07-10 02:13:34 +08:00
currentChannelContainer . Clear ( false ) ;
currentChannelContainer . Add ( loaded ) ;
currentChannelContainer . FadeIn ( 500 , Easing . OutQuint ) ;
2019-12-26 10:32:40 +08:00
if ( highlightingMessage ! = null & & highlightingMessage . ChannelId = = e . NewValue . Id )
{
loaded . ScrollToAndHighlightMessage ( highlightingMessage ) ;
highlightingMessage = null ;
}
2018-04-14 19:31:03 +08:00
} ) ;
}
else
{
2018-07-10 02:13:34 +08:00
currentChannelContainer . Clear ( false ) ;
2018-12-20 20:06:40 +08:00
currentChannelContainer . Add ( loaded ) ;
2018-04-14 19:31:03 +08:00
}
2018-04-13 17:19:50 +08:00
}
2019-09-03 06:50:52 +08:00
private float startDragChatHeight ;
2018-04-13 17:19:50 +08:00
private bool isDragging ;
2018-10-02 11:02:47 +08:00
protected override bool OnDragStart ( DragStartEvent e )
2018-04-13 17:19:50 +08:00
{
isDragging = tabsArea . IsHovered ;
if ( ! isDragging )
2018-10-02 11:02:47 +08:00
return base . OnDragStart ( e ) ;
2018-04-13 17:19:50 +08:00
startDragChatHeight = ChatHeight . Value ;
return true ;
}
2018-10-02 11:02:47 +08:00
protected override bool OnDrag ( DragEvent e )
2018-04-13 17:19:50 +08:00
{
if ( isDragging )
{
2019-09-03 06:50:52 +08:00
float targetChatHeight = startDragChatHeight - ( e . MousePosition . Y - e . MouseDownPosition . Y ) / Parent . DrawSize . Y ;
2018-04-13 17:19:50 +08:00
// If the channel selection screen is shown, mind its minimum height
2019-06-25 18:52:31 +08:00
if ( ChannelSelectionOverlay . State . Value = = Visibility . Visible & & targetChatHeight > 1f - channel_selection_min_height )
2018-04-13 17:19:50 +08:00
targetChatHeight = 1f - channel_selection_min_height ;
ChatHeight . Value = targetChatHeight ;
}
return true ;
}
2018-10-02 11:02:47 +08:00
protected override bool OnDragEnd ( DragEndEvent e )
2018-04-13 17:19:50 +08:00
{
isDragging = false ;
2018-10-02 11:02:47 +08:00
return base . OnDragEnd ( e ) ;
2018-04-13 17:19:50 +08:00
}
2018-12-20 20:06:40 +08:00
private void selectTab ( int index )
{
2019-06-25 18:52:31 +08:00
var channel = ChannelTabControl . Items . Skip ( index ) . FirstOrDefault ( ) ;
2019-05-12 18:31:11 +08:00
if ( channel ! = null & & ! ( channel is ChannelSelectorTabItem . ChannelSelectorTabChannel ) )
2019-06-25 18:52:31 +08:00
ChannelTabControl . Current . Value = channel ;
2018-12-20 20:06:40 +08:00
}
protected override bool OnKeyDown ( KeyDownEvent e )
{
if ( e . AltPressed )
{
switch ( e . Key )
{
case Key . Number1 :
case Key . Number2 :
case Key . Number3 :
case Key . Number4 :
case Key . Number5 :
case Key . Number6 :
case Key . Number7 :
case Key . Number8 :
case Key . Number9 :
selectTab ( ( int ) e . Key - ( int ) Key . Number1 ) ;
return true ;
2019-04-01 11:16:05 +08:00
2018-12-20 20:06:40 +08:00
case Key . Number0 :
selectTab ( 9 ) ;
return true ;
}
}
return base . OnKeyDown ( e ) ;
}
2018-04-13 17:19:50 +08:00
public override bool AcceptsFocus = > true ;
2018-10-02 11:02:47 +08:00
protected override void OnFocus ( FocusEvent e )
2018-04-13 17:19:50 +08:00
{
//this is necessary as textbox is masked away and therefore can't get focus :(
2019-01-25 18:20:08 +08:00
textbox . TakeFocus ( ) ;
2018-10-02 11:02:47 +08:00
base . OnFocus ( e ) ;
2018-04-13 17:19:50 +08:00
}
protected override void PopIn ( )
{
this . MoveToY ( 0 , transition_length , Easing . OutQuint ) ;
this . FadeIn ( transition_length , Easing . OutQuint ) ;
textbox . HoldFocus = true ;
2019-06-25 18:52:31 +08:00
2018-04-13 17:19:50 +08:00
base . PopIn ( ) ;
}
protected override void PopOut ( )
{
this . MoveToY ( Height , transition_length , Easing . InSine ) ;
this . FadeOut ( transition_length , Easing . InSine ) ;
2019-06-25 18:52:31 +08:00
ChannelSelectionOverlay . Hide ( ) ;
2019-03-12 11:10:59 +08:00
2018-04-13 17:19:50 +08:00
textbox . HoldFocus = false ;
base . PopOut ( ) ;
}
2018-11-22 02:15:55 +08:00
private void onChannelAddedToJoinedChannels ( IEnumerable < Channel > channels )
2018-09-06 14:56:04 +08:00
{
2018-11-22 02:15:55 +08:00
foreach ( Channel channel in channels )
2019-06-25 18:52:31 +08:00
ChannelTabControl . AddChannel ( channel ) ;
2018-11-22 02:15:55 +08:00
}
private void onChannelRemovedFromJoinedChannels ( IEnumerable < Channel > channels )
{
foreach ( Channel channel in channels )
{
2019-06-25 18:52:31 +08:00
ChannelTabControl . RemoveChannel ( channel ) ;
2019-08-05 18:23:13 +08:00
var loaded = loadedChannels . Find ( c = > c . Channel = = channel ) ;
if ( loaded ! = null )
{
loadedChannels . Remove ( loaded ) ;
// Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared
// to ensure that the previous channel doesn't get updated after it's disposed
currentChannelContainer . Remove ( loaded ) ;
loaded . Dispose ( ) ;
}
2018-11-22 02:15:55 +08:00
}
2018-09-06 14:56:04 +08:00
}
2018-11-22 02:15:55 +08:00
private void availableChannelsChanged ( IEnumerable < Channel > channels )
2019-06-25 18:52:31 +08:00
= > ChannelSelectionOverlay . UpdateAvailableChannels ( channelManager . AvailableChannels ) ;
2018-11-22 02:15:55 +08:00
2018-09-06 14:56:04 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2018-09-06 16:11:23 +08:00
if ( channelManager ! = null )
{
channelManager . CurrentChannel . ValueChanged - = currentChannelChanged ;
2018-11-22 02:15:55 +08:00
channelManager . JoinedChannels . ItemsAdded - = onChannelAddedToJoinedChannels ;
channelManager . JoinedChannels . ItemsRemoved - = onChannelRemovedFromJoinedChannels ;
channelManager . AvailableChannels . ItemsAdded - = availableChannelsChanged ;
channelManager . AvailableChannels . ItemsRemoved - = availableChannelsChanged ;
2018-09-06 16:11:23 +08:00
}
2018-09-06 14:56:04 +08:00
}
2018-04-13 17:19:50 +08:00
private void postMessage ( TextBox textbox , bool newText )
{
2018-04-14 19:31:03 +08:00
var text = textbox . Text . Trim ( ) ;
2018-04-13 17:19:50 +08:00
2018-04-14 19:31:03 +08:00
if ( string . IsNullOrWhiteSpace ( text ) )
2018-04-13 17:19:50 +08:00
return ;
2018-04-14 19:31:03 +08:00
if ( text [ 0 ] = = '/' )
channelManager . PostCommand ( text . Substring ( 1 ) ) ;
else
channelManager . PostMessage ( text ) ;
2018-04-13 17:19:50 +08:00
2018-04-14 19:31:03 +08:00
textbox . Text = string . Empty ;
2018-04-13 17:19:50 +08:00
}
2018-10-02 13:41:18 +08:00
2019-12-17 13:56:05 +08:00
/// <summary>
2019-12-26 10:32:40 +08:00
/// Returns the loaded drawable for a channel. Creates new instance if <paramref name="createIfUnloaded"/> is true. Otherwise returns null if not found.
2019-12-17 13:56:05 +08:00
/// </summary>
2019-12-26 10:32:40 +08:00
public DrawableChannel GetChannelDrawable ( Channel channel , bool createIfUnloaded = false )
{
var result = loadedChannels . Find ( drawable = > drawable . Channel = = channel ) ;
if ( createIfUnloaded & & result = = null )
{
result = loadChannelDrawable ( channel ) ;
}
return result ;
}
private DrawableChannel loadChannelDrawable ( Channel channel )
{
var loaded = new DrawableChannel ( channel ) ;
loadedChannels . Add ( loaded ) ;
return loaded ;
}
public void ScrollToAndHighlightMessage ( Channel channel , Message message )
{
highlightingMessage = message ;
channelManager . CurrentChannel . Value = channel ;
}
2019-12-17 13:56:05 +08:00
2018-10-02 13:41:18 +08:00
private class TabsArea : Container
{
// IsHovered is used
public override bool HandlePositionalInput = > true ;
public TabsArea ( )
{
Name = @"tabs area" ;
RelativeSizeAxes = Axes . X ;
Height = TAB_AREA_HEIGHT ;
}
}
2018-04-13 17:19:50 +08:00
}
}