diff --git a/ConsoleFramework/ConsoleApplication.cs b/ConsoleFramework/ConsoleApplication.cs index 1176e79..79bb175 100644 --- a/ConsoleFramework/ConsoleApplication.cs +++ b/ConsoleFramework/ConsoleApplication.cs @@ -191,12 +191,15 @@ public static Control LoadFromXaml( string xamlResourceName, object dataContext } using ( StreamReader reader = new StreamReader( stream ) ) { string result = reader.ReadToEnd( ); - return XamlParser.CreateFromXaml(result, dataContext, new List() + Control control = XamlParser.CreateFromXaml(result, dataContext, new List() { "clr-namespace:Xaml;assembly=Xaml", "clr-namespace:ConsoleFramework.Xaml;assembly=ConsoleFramework", "clr-namespace:ConsoleFramework.Controls;assembly=ConsoleFramework", }); + control.DataContext = dataContext; + control.Created( ); + return control; } } } @@ -720,10 +723,14 @@ private void processLinuxInput (TermKeyKey key) inputRecord.MouseEvent.dwMousePosition = new COORD((short) (col - 1), (short) (line - 1)); if (ev == TermKeyMouseEvent.TERMKEY_MOUSE_RELEASE) { inputRecord.MouseEvent.dwButtonState = 0; - } else if (ev == TermKeyMouseEvent.TERMKEY_MOUSE_DRAG) { - inputRecord.MouseEvent.dwButtonState = MOUSE_BUTTON_STATE.FROM_LEFT_1ST_BUTTON_PRESSED; - } else if (ev == TermKeyMouseEvent.TERMKEY_MOUSE_PRESS) { - inputRecord.MouseEvent.dwButtonState = MOUSE_BUTTON_STATE.FROM_LEFT_1ST_BUTTON_PRESSED; + } else if (ev == TermKeyMouseEvent.TERMKEY_MOUSE_DRAG || ev == TermKeyMouseEvent.TERMKEY_MOUSE_PRESS) { + if (1 == button) { + inputRecord.MouseEvent.dwButtonState = MOUSE_BUTTON_STATE.FROM_LEFT_1ST_BUTTON_PRESSED; + } else if (2 == button) { + inputRecord.MouseEvent.dwButtonState = MOUSE_BUTTON_STATE.FROM_LEFT_2ND_BUTTON_PRESSED; + } else if (3 == button) { + inputRecord.MouseEvent.dwButtonState = MOUSE_BUTTON_STATE.RIGHTMOST_BUTTON_PRESSED; + } } // processInputEvent(inputRecord); diff --git a/ConsoleFramework/ConsoleFramework.csproj b/ConsoleFramework/ConsoleFramework.csproj index eb51f4a..885c97a 100644 --- a/ConsoleFramework/ConsoleFramework.csproj +++ b/ConsoleFramework/ConsoleFramework.csproj @@ -61,6 +61,7 @@ + @@ -69,6 +70,7 @@ + diff --git a/ConsoleFramework/Controls/Button.cs b/ConsoleFramework/Controls/Button.cs index 7174a3f..8e6d3da 100644 --- a/ConsoleFramework/Controls/Button.cs +++ b/ConsoleFramework/Controls/Button.cs @@ -20,7 +20,7 @@ public string Caption { protected override Size MeasureOverride(Size availableSize) { if (!string.IsNullOrEmpty(caption)) { - Size minButtonSize = new Size(caption.Length + 14, 2); + Size minButtonSize = new Size(caption.Length + 10, 2); return minButtonSize; } else return new Size(8, 2); } diff --git a/ConsoleFramework/Controls/ComboBox.cs b/ConsoleFramework/Controls/ComboBox.cs index 0e6e0ac..37d3c89 100644 --- a/ConsoleFramework/Controls/ComboBox.cs +++ b/ConsoleFramework/Controls/ComboBox.cs @@ -208,7 +208,8 @@ private void OnKeyDown( object sender, KeyEventArgs args ) { } private void OnMouseDown( object sender, MouseButtonEventArgs mouseButtonEventArgs ) { - openPopup( ); + if ( !opened ) + openPopup( ); } private void OnPopupClosed( object o, EventArgs args ) { diff --git a/ConsoleFramework/Controls/ContextMenu.cs b/ConsoleFramework/Controls/ContextMenu.cs new file mode 100644 index 0000000..8a40b58 --- /dev/null +++ b/ConsoleFramework/Controls/ContextMenu.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Binding.Observables; +using ConsoleFramework.Core; +using ConsoleFramework.Events; +using ConsoleFramework.Native; +using Xaml; + +namespace ConsoleFramework.Controls +{ + [ContentProperty( "Items" )] + public class ContextMenu + { + private readonly ObservableList< MenuItemBase > items = new ObservableList< MenuItemBase >( + new List< MenuItemBase >( ) ); + + public IList< MenuItemBase > Items { + get { return items; } + } + + private MenuItem.Popup popup; + private bool expanded; + + private bool popupShadow = true; + public bool PopupShadow { + get { return popupShadow; } + set { popupShadow = value; } + } + + /// + /// Forces all open submenus to be closed. + /// + public void CloseAllSubmenus( ) { + List expandedSubmenus = new List< MenuItem >(); + MenuItem currentItem = ( MenuItem ) this.Items.SingleOrDefault( + item => item is MenuItem && ((MenuItem)item).expanded); + while ( null != currentItem ) { + expandedSubmenus.Add( currentItem ); + currentItem = (MenuItem)currentItem.Items.SingleOrDefault( + item => item is MenuItem && ((MenuItem)item).expanded); + } + expandedSubmenus.Reverse( ); + foreach ( MenuItem expandedSubmenu in expandedSubmenus ) { + expandedSubmenu.Close( ); + } + } + + private WindowsHost windowsHost; + private RoutedEventHandler windowsHostClick; + private KeyEventHandler windowsHostControlKeyPressed; + + public void OpenMenu( WindowsHost windowsHost, Point point ) { + if ( expanded ) return; + + // Вешаем на WindowsHost обработчик события MenuItem.ClickEvent, + // чтобы ловить момент выбора пункта меню в одном из модальных всплывающих окошек + // Дело в том, что эти окошки не являются дочерними элементами контрола Menu, + // а напрямую являются дочерними элементами WindowsHost (т.к. именно он создаёт + // окна). И событие выбора пункта меню из всплывающего окошка может быть поймано + // в WindowsHost, но не в Menu. А нам нужно повесить обработчик, который закроет + // все показанные попапы. + EventManager.AddHandler( windowsHost, MenuItem.ClickEvent, + windowsHostClick = ( sender, args ) => { + CloseAllSubmenus( ); + popup.Close( ); + }, true ); + + EventManager.AddHandler( windowsHost, MenuItem.Popup.ControlKeyPressedEvent, + windowsHostControlKeyPressed = ( sender, args ) => { + CloseAllSubmenus( ); + // + //ConsoleApplication.Instance.FocusManager.SetFocusScope(this); + if ( args.wVirtualKeyCode == VirtualKeys.Right ) + ConsoleApplication.Instance.FocusManager.MoveFocusNext( ); + else if ( args.wVirtualKeyCode == VirtualKeys.Left ) + ConsoleApplication.Instance.FocusManager.MoveFocusPrev( ); + MenuItem focusedItem = ( MenuItem ) this.Items.SingleOrDefault( + item => item is MenuItem && item.HasFocus ); + focusedItem.Expand( ); + } ); + + if ( null == popup ) { + popup = new MenuItem.Popup( this.Items, this.popupShadow, 0 ); + popup.AddHandler( Window.ClosedEvent, new EventHandler( onPopupClosed ) ); + } + popup.X = point.X; + popup.Y = point.Y; + windowsHost.ShowModal( popup, true ); + expanded = true; + this.windowsHost = windowsHost; + } + + private void onPopupClosed( object sender, EventArgs eventArgs ) { + if (!expanded) throw new InvalidOperationException("This shouldn't happen"); + expanded = false; + EventManager.RemoveHandler( windowsHost, MenuItem.ClickEvent, windowsHostClick ); + EventManager.RemoveHandler( windowsHost, MenuItem.Popup.ControlKeyPressedEvent, + windowsHostControlKeyPressed ); + } + } +} \ No newline at end of file diff --git a/ConsoleFramework/Controls/Control.cs b/ConsoleFramework/Controls/Control.cs index 54a6bc0..069a7b3 100644 --- a/ConsoleFramework/Controls/Control.cs +++ b/ConsoleFramework/Controls/Control.cs @@ -230,9 +230,16 @@ public T FindDirectChildByName< T >( string name ) where T:Control { internal LayoutInfo layoutInfo = new LayoutInfo(); internal LayoutInfo lastLayoutInfo = new LayoutInfo(); + private Visibility visibility; + public Visibility Visibility { - get; - set; + get { return visibility; } + set { + if ( visibility != value ) { + visibility = value; + Invalidate(); + } + } } /// @@ -1321,29 +1328,47 @@ protected static void assert( bool assertion ) { /// /// Определяет дочерний элемент, находящийся под курсором мыши, - /// и передаёт на него фокус, если он - Focusable и Visible. + /// и передаёт на него фокус, если он - Focusable. А если нажата правая кнопка мыши и у + /// контрола есть контекстное меню, активизирует его. /// protected void PassFocusToChildUnderPoint( MouseEventArgs args ) { - Control tofocus = null; - Control parent = this; - Control hitTested = null; - do - { - Point position = args.GetPosition(parent); - hitTested = parent.GetTopChildAtPoint(position); - if (null != hitTested) - { - parent = hitTested; - if (hitTested.Visibility == Visibility.Visible && hitTested.Focusable) - { - tofocus = hitTested; + Control topControl = VisualTreeHelper.FindTopControlUnderMouse( this, args.GetPosition( this ) ); + if ( topControl != null ) { + if ( topControl.Focusable ) { + ConsoleApplication.Instance.FocusManager.SetFocus(this, topControl); + } + if ( args.RightButton == MouseButtonState.Pressed ) { + if ( topControl.ContextMenu != null ) { + var windowsHost = VisualTreeHelper.FindClosestParent< WindowsHost >( this ); + topControl.ContextMenu.OpenMenu( windowsHost, args.GetPosition( windowsHost ) ); } } - } while (hitTested != null); - if (tofocus != null) - { - ConsoleApplication.Instance.FocusManager.SetFocus(this, tofocus); } } + + /// + /// This method is called after control has been created and filled with children. + /// todo : think about avoiding reentrant Created() calls + /// + public void Created( ) { + foreach ( var child in Children ) { + child.Created( ); + } + OnCreated( ); + } + + /// + /// This method is invoked after control has been created and all children + /// controls are created too (and children' OnCreated called). So, you can + /// find any child control in this method and subscribe for events. + /// + protected virtual void OnCreated( ) { + } + + private ContextMenu contextMenu; + public ContextMenu ContextMenu { + get { return contextMenu; } + set { contextMenu = value; } + } } } diff --git a/ConsoleFramework/Controls/ListBox.cs b/ConsoleFramework/Controls/ListBox.cs index dca3e38..bb6e0cf 100644 --- a/ConsoleFramework/Controls/ListBox.cs +++ b/ConsoleFramework/Controls/ListBox.cs @@ -21,7 +21,7 @@ public class ListBox : Control public int? PageSize { get; set; } private readonly ObservableList items = new ObservableList(new List()); - public IList Items { + public ObservableList Items { get { return items; } } diff --git a/ConsoleFramework/Controls/MessageBox.cs b/ConsoleFramework/Controls/MessageBox.cs index aa2cb15..87805d0 100644 --- a/ConsoleFramework/Controls/MessageBox.cs +++ b/ConsoleFramework/Controls/MessageBox.cs @@ -43,7 +43,9 @@ public static void Show( string title, string text, MessageBoxClosedEventHandler messageBox.Title = title; messageBox.Text = text; messageBox.AddHandler( ClosedEvent, new EventHandler(( sender, args ) => { - onClosed(MessageBoxResult.Button1); + if ( null != onClosed ) { + onClosed( MessageBoxResult.Button1 ); + } }) ); //messageBox.X = windowsHost.ShowModal( messageBox ); diff --git a/ConsoleFramework/Controls/Panel.cs b/ConsoleFramework/Controls/Panel.cs index b091ccd..b54050d 100644 --- a/ConsoleFramework/Controls/Panel.cs +++ b/ConsoleFramework/Controls/Panel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using ConsoleFramework.Core; using ConsoleFramework.Native; using ConsoleFramework.Rendering; @@ -131,6 +130,7 @@ public override void Render(RenderingBuffer buffer) { buffer.SetPixel(x, y, ' ', Attr.BACKGROUND_BLUE | Attr.BACKGROUND_GREEN | Attr.BACKGROUND_RED | Attr.FOREGROUND_BLUE | Attr.FOREGROUND_GREEN | Attr.FOREGROUND_RED | Attr.FOREGROUND_INTENSITY); + buffer.SetOpacity( x, y, 4 ); } } } diff --git a/ConsoleFramework/Controls/ProgressBar.cs b/ConsoleFramework/Controls/ProgressBar.cs index e59963d..654f783 100644 --- a/ConsoleFramework/Controls/ProgressBar.cs +++ b/ConsoleFramework/Controls/ProgressBar.cs @@ -1,4 +1,5 @@ -using ConsoleFramework.Core; +using System; +using ConsoleFramework.Core; using ConsoleFramework.Native; using ConsoleFramework.Rendering; @@ -25,7 +26,7 @@ public override void Render( RenderingBuffer buffer ) { Attr attr = Colors.Blend( Color.DarkCyan, Color.DarkBlue ); buffer.FillRectangle(0, 0, ActualWidth, ActualHeight, UnicodeTable.MediumShade, attr); int filled = ( int ) ( ActualWidth*( Percent*0.01 ) ); - buffer.FillRectangle(0, 0, filled, ActualHeight, UnicodeTable.DarkShade, attr); + buffer.FillRectangle(0, 0, Math.Min( filled, ActualWidth ), ActualHeight, UnicodeTable.DarkShade, attr); } } } diff --git a/ConsoleFramework/Controls/TabControl.cs b/ConsoleFramework/Controls/TabControl.cs new file mode 100644 index 0000000..87751ec --- /dev/null +++ b/ConsoleFramework/Controls/TabControl.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ConsoleFramework.Core; +using ConsoleFramework.Events; +using ConsoleFramework.Native; +using ConsoleFramework.Rendering; +using Xaml; + +namespace ConsoleFramework.Controls +{ + public class TabDefinition + { + public string Title { get; set; } + } + + /// + /// Control that presents a tabbed layout. + /// + [ContentProperty( "Controls" )] + public class TabControl : Control + { + private readonly List< TabDefinition > tabDefinitions = new List< TabDefinition >( ); + private readonly UIElementCollection controls; + + public TabControl( ) { + controls = new UIElementCollection( this ); + AddHandler(MouseDownEvent, new MouseButtonEventHandler(mouseDown)); + } + + private void mouseDown( object sender, MouseButtonEventArgs args ) { + Point pos = args.GetPosition( this ); + if ( pos.y > 2 ) return; + + int x = 0; + for ( int i = 0; i < tabDefinitions.Count; i++ ) { + TabDefinition tabDefinition = tabDefinitions[ i ]; + if ( pos.X > x && pos.X <= x + tabDefinition.Title.Length + 2 ) { + activeTabIndex = i; + Invalidate( ); + break; + } + x += tabDefinition.Title.Length + 2 + 1; // Two spaces around + one vertical border + } + args.Handled = true; + } + + public List< TabDefinition > TabDefinitions { + get { return tabDefinitions; } + } + + public UIElementCollection Controls { + get { return controls; } + } + + private int activeTabIndex; + + public int ActiveTabIndex { + get { return activeTabIndex; } + set { + if ( value != activeTabIndex ) { + if (value < 0 || value >= tabDefinitions.Count) + throw new ArgumentException("Tab index out of bounds"); + activeTabIndex = value; + Invalidate( ); + } + } + } + + protected override Size MeasureOverride( Size availableSize ) { + // Get children max desired size to determine the tab content desired size + Size childrenAvailableSize = new Size( + Math.Max( availableSize.Width - 2, 0 ), + Math.Max( availableSize.Height - 4, 0 ) ); + int maxDesiredWidth = 0; + int maxDesiredHeight = 0; + for ( int i = 0; i < Math.Min( Children.Count, tabDefinitions.Count ); i++ ) { + Control child = Children[ i ]; + child.Measure( childrenAvailableSize ); + if ( child.DesiredSize.Width > maxDesiredWidth ) maxDesiredWidth = child.DesiredSize.Width; + if ( child.DesiredSize.Height > maxDesiredHeight ) maxDesiredHeight = child.DesiredSize.Height; + } + + // Get tab header desired size + var tabHeaderWidth = getTabHeaderWidth( ); + + // Calculate final size = min(availableSize, controlWithChildrenDesiredSize) + Size controlWithChildrenDesiredSize = new Size( + Math.Max( maxDesiredWidth + 2, tabHeaderWidth ), + maxDesiredHeight + 4 + ); + Size finalAvailableSize = new Size( + Math.Min( availableSize.Width, controlWithChildrenDesiredSize.Width ), + Math.Min( availableSize.Height, controlWithChildrenDesiredSize.Height ) + ); + + // Invoke children.Measure() with final size + for ( int i = 0; i < Children.Count; i++ ) { + Control child = Children[ i ]; + if ( activeTabIndex == i ) { + child.Measure( new Size( + Math.Max( 0, finalAvailableSize.Width - 2 ), + Math.Max( 0, finalAvailableSize.Height - 4 ) + ) ); + } else { + child.Measure( Size.Empty ); + } + } + + return finalAvailableSize; + } + + private int getTabHeaderWidth( ) { + // Two spaces around + one vertical border per tab, plus extra one vertical border + return 1 + tabDefinitions.Sum( tabDefinition => tabDefinition.Title.Length + 2 + 1 ); + } + + protected override Size ArrangeOverride( Size finalSize ) { + for ( int i = 0; i < Children.Count; i++ ) { + Control child = Children[ i ]; + if ( activeTabIndex == i ) { + child.Arrange( new Rect( + new Point( 1, 3 ), + new Size( Math.Max( 0, finalSize.Width - 2 ), + Math.Max( 0, finalSize.Height - 4 ) + ) ) ); + } else { + child.Arrange( Rect.Empty ); + } + } + return finalSize; + } + + private void renderBorderSafe( RenderingBuffer buffer, int x, int y, int x2, int y2 ) { + if ( ActualWidth > x && ActualHeight > y ) + buffer.SetPixel( x, y, UnicodeTable.SingleFrameTopLeftCorner ); + if ( ActualWidth > x && ActualHeight > y2 && y2 > y ) + buffer.SetPixel( x, y2, UnicodeTable.SingleFrameBottomLeftCorner ); + if ( ActualHeight > y ) + for ( int i = x + 1; i <= Math.Min( x2 - 1, ActualWidth - 1 ); i++ ) + buffer.SetPixel( i, y, UnicodeTable.SingleFrameHorizontal ); + if ( ActualHeight > y2 ) + for ( int i = x + 1; i <= Math.Min( x2 - 1, ActualWidth - 1 ); i++ ) + buffer.SetPixel( i, y2, UnicodeTable.SingleFrameHorizontal ); + if ( ActualWidth > x ) + for ( int j = y + 1; j <= Math.Min( y2 - 1, ActualHeight - 1 ); j++ ) + buffer.SetPixel( x, j, UnicodeTable.SingleFrameVertical ); + if ( ActualWidth > x2 ) + for ( int j = y + 1; j <= Math.Min( y2 - 1, ActualHeight - 1 ); j++ ) + buffer.SetPixel( x2, j, UnicodeTable.SingleFrameVertical ); + if ( ActualWidth > x2 && ActualHeight > y && x2 > x ) + buffer.SetPixel( x2, y, UnicodeTable.SingleFrameTopRightCorner ); + if ( ActualWidth > x2 && ActualHeight > y2 && y2 > y && x2 > x ) + buffer.SetPixel( x2, y2, UnicodeTable.SingleFrameBottomRightCorner ); + } + + public override void Render( RenderingBuffer buffer ) { + Attr attr = Colors.Blend( Color.Black, Color.DarkGreen ); + Attr inactiveAttr = Colors.Blend( Color.DarkGray, Color.DarkGreen ); + + // Transparent background for borders + buffer.SetOpacityRect( 0, 0, ActualWidth, ActualHeight, 3 ); + + buffer.FillRectangle( 0, 0, ActualWidth, ActualHeight, ' ', attr ); + + // Transparent child content part + if ( ActualWidth > 2 && ActualHeight > 3 ) + buffer.SetOpacityRect( 1, 3, ActualWidth - 2, ActualHeight - 4, 2 ); + + renderBorderSafe( buffer, 0, 2, Math.Max( getTabHeaderWidth( ) - 1, ActualWidth - 1 ), ActualHeight - 1 ); + + // Start to render header + buffer.FillRectangle( 0, 0, ActualWidth, Math.Min( 2, ActualHeight ), ' ', attr ); + + int x = 0; + + // Render tabs before active tab + for ( int tab = 0; tab < tabDefinitions.Count; x += TabDefinitions[ tab++ ].Title.Length + 3 ) { + var tabDefinition = TabDefinitions[ tab ]; + if ( tab <= activeTabIndex ) { + buffer.SetPixelSafe( x, 0, UnicodeTable.SingleFrameTopLeftCorner ); + buffer.SetPixelSafe( x, 1, UnicodeTable.SingleFrameVertical ); + } + if ( tab == activeTabIndex ) { + buffer.SetPixelSafe( x, 2, + activeTabIndex == 0 ? UnicodeTable.SingleFrameVertical : UnicodeTable.SingleFrameBottomRightCorner ); + } + for ( int i = 0; i < tabDefinition.Title.Length + 2; i++ ) { + buffer.SetPixelSafe( x + 1 + i, 0, UnicodeTable.SingleFrameHorizontal ); + if ( tab == activeTabIndex ) + buffer.SetPixelSafe( x + 1 + i, 2, ' ' ); + } + buffer.RenderStringSafe( " " + tabDefinition.Title + " ", x + 1, 1, + activeTabIndex == tab ? attr : inactiveAttr ); + if ( tab >= activeTabIndex ) { + buffer.SetPixelSafe( x + tabDefinition.Title.Length + 3, 0, UnicodeTable.SingleFrameTopRightCorner ); + buffer.SetPixelSafe( x + tabDefinition.Title.Length + 3, 1, UnicodeTable.SingleFrameVertical ); + } + if ( tab == activeTabIndex ) { + buffer.SetPixelSafe( x + tabDefinition.Title.Length + 3, 2, + activeTabIndex == tabDefinitions.Count - 1 && ActualWidth - 1 == x + tabDefinition.Title.Length + 3 + ? UnicodeTable.SingleFrameVertical + : UnicodeTable.SingleFrameBottomLeftCorner ); + } + } + } + } +} \ No newline at end of file diff --git a/ConsoleFramework/Controls/TextBlock.cs b/ConsoleFramework/Controls/TextBlock.cs index 6a330c4..9e25336 100644 --- a/ConsoleFramework/Controls/TextBlock.cs +++ b/ConsoleFramework/Controls/TextBlock.cs @@ -1,9 +1,11 @@ using ConsoleFramework.Core; using ConsoleFramework.Native; using ConsoleFramework.Rendering; +using Xaml; namespace ConsoleFramework.Controls { + [ContentProperty("Text")] public class TextBlock : Control { private string text; @@ -14,6 +16,20 @@ public TextBlock() { initialize(); } + private Color color = Color.Black; + + public Color Color + { + get { return color; } + set + { + if ( color != value ) { + color = value; + Invalidate( ); + } + } + } + public string Text { get { return text; @@ -33,7 +49,7 @@ protected override Size MeasureOverride(Size availableSize) { } public override void Render(RenderingBuffer buffer) { - Attr attr = Colors.Blend(Color.Black, Color.DarkYellow); + Attr attr = Colors.Blend(color, Color.DarkYellow); buffer.FillRectangle( 0, 0, ActualWidth, ActualHeight, ' ', attr); for (int x = 0; x < ActualWidth; ++x) { for (int y = 0; y < ActualHeight; ++y) { diff --git a/ConsoleFramework/Controls/Window.cs b/ConsoleFramework/Controls/Window.cs index 02f9088..2e87276 100644 --- a/ConsoleFramework/Controls/Window.cs +++ b/ConsoleFramework/Controls/Window.cs @@ -43,8 +43,7 @@ protected virtual void initialize( ) { /// По клику мышки ищет конечный Focusable контрол, который размещён /// на месте нажатия и устанавливает на нём фокус. /// - private void Window_OnPreviewMouseDown(object sender, MouseButtonEventArgs e) - { + private void Window_OnPreviewMouseDown(object sender, MouseButtonEventArgs e) { PassFocusToChildUnderPoint( e ); } diff --git a/ConsoleFramework/Core/VisualTreeHelper.cs b/ConsoleFramework/Core/VisualTreeHelper.cs index aee5132..0b04f6c 100644 --- a/ConsoleFramework/Core/VisualTreeHelper.cs +++ b/ConsoleFramework/Core/VisualTreeHelper.cs @@ -85,5 +85,48 @@ public static T FindClosestParent< T >( Control control ) where T : Control { if (tmp is T) return (T) tmp; return null; } + + /// + /// Находит самый верхний элемент под указателем мыши с координатами rawPoint. + /// Учитывается прозрачность элементов - если пиксель, куда указывает мышь, отмечен как + /// прозрачный для событий мыши (opacity от 4 до 7), то они будут проходить насквозь, + /// к следующему контролу. Также учитывается видимость элементов - Hidden и Collapsed элементы + /// будут проигнорированы. + /// Так обрабатываются, например, тени окошек и прозрачные места контролов (первый столбец Combobox). + /// + /// Координаты относительно control + /// RootElement для проверки всего визуального дерева. + /// Элемент управления или null, если событие мыши было за границами всех контролов, или + /// если все контролы были прозрачны для событий мыши + public static Control FindTopControlUnderMouse(Control control, Point localPoint) { + if (null == control) throw new ArgumentNullException("control"); + + Point rawPoint = Control.TranslatePoint( control, localPoint, null ); + + if (control.Children.Count != 0) { + IList childrenOrderedByZIndex = control.GetChildrenOrderedByZIndex(); + for (int i = childrenOrderedByZIndex.Count - 1; i >= 0; i--) { + Control child = childrenOrderedByZIndex[i]; + if (Control.HitTest(rawPoint, control, child)) { + Control foundSource = FindTopControlUnderMouse(child, + Control.TranslatePoint( control, localPoint, child )); + if ( null != foundSource ) return foundSource; + } + } + } + Rect controlRect = new Rect(new Point(0, 0), control.RenderSize); + if ( !controlRect.Contains( localPoint ) ) { + return null; + } else { + if ( control.Visibility != Visibility.Visible ) + return null; + int _opacity = ConsoleApplication.Instance.Renderer + .getControlOpacityAt( control, localPoint.X, localPoint.Y ); + if ( _opacity >= 4 && _opacity <= 7 ) { + return null; + } + } + return control; + } } } \ No newline at end of file diff --git a/ConsoleFramework/Events/EventManager.cs b/ConsoleFramework/Events/EventManager.cs index 0495e18..40202f8 100644 --- a/ConsoleFramework/Events/EventManager.cs +++ b/ConsoleFramework/Events/EventManager.cs @@ -248,13 +248,18 @@ public void ParseInputEvent(INPUT_RECORD inputRecord, Control rootElement) { // вынуждены сохранять координаты, полученные при предыдущем событии мыши rawPosition = lastMousePosition; } - Control topMost = findSource(rawPosition, rootElement); + + Control topMost = VisualTreeHelper.FindTopControlUnderMouse(rootElement, + Control.TranslatePoint(null, rawPosition, rootElement)); // если мышь захвачена контролом, то события перемещения мыши доставляются только ему, // события, связанные с нажатием мыши - тоже доставляются только ему, вместо того // контрола, над которым событие было зарегистрировано. Такой механизм необходим, // например, для корректной обработки перемещений окон (вверх или в стороны) Control source = (inputCaptureStack.Count != 0) ? inputCaptureStack.Peek() : topMost; + + // No sense to further process event with no source control + if ( source == null ) return; if (mouseEvent.dwEventFlags == MouseEventFlags.MOUSE_MOVED) { MouseButtonState leftMouseButtonState = getLeftButtonState(mouseEvent.dwButtonState); @@ -593,34 +598,6 @@ private bool processRoutedEvent(RoutedEvent routedEvent, RoutedEventArgs args) { return args.Handled; } - /// - /// Находит самый верхний элемент под указателем мыши с координатами rawPoint. - /// Учитывается прозрачность элементов - если пиксель, куда указывает мышь, отмечен как - /// прозрачный для событий мыши (opacity от 4 до 7), то они будут проходить насквозь, - /// к следующему контролу. - /// Так обрабатываются, например, тени окошек и прозрачные места контролов (первый столбец Combobox). - /// - /// - /// RootElement для проверки всего визуального дерева. - /// - private Control findSource(Point rawPoint, Control control) { - if (control.Children.Count != 0) { - IList childrenOrderedByZIndex = control.GetChildrenOrderedByZIndex(); - for (int i = childrenOrderedByZIndex.Count - 1; i >= 0; i--) { - Control child = childrenOrderedByZIndex[i]; - if (Control.HitTest(rawPoint, control, child)) { - Point childPoint = Control.TranslatePoint( null, rawPoint, child ); - int opacity = ConsoleApplication.Instance.Renderer.getControlOpacityAt( child, childPoint.X, childPoint.Y ); - if ( opacity >= 4 && opacity <= 7 ) { - continue; - } - return findSource(rawPoint, child); - } - } - } - return control; - } - /// /// Adds specified routed event to event queue. This event will be processed in next pass. /// diff --git a/ConsoleFramework/Rendering/Renderer.cs b/ConsoleFramework/Rendering/Renderer.cs index 327e5b3..b51d2f9 100644 --- a/ConsoleFramework/Rendering/Renderer.cs +++ b/ConsoleFramework/Rendering/Renderer.cs @@ -508,6 +508,12 @@ private RenderingBuffer getOrCreateFullBufferForControl(Control control) { /// Это необходимо для определения контрола, который станет источником события мыши. /// internal int getControlOpacityAt( Control control, int x, int y ) { + // Если контрол, над которым водят мышью, имеет невидимых сыновей, которые ни разу + // не отрисовывались, то в словаре буферов для таких сыновей ничего не окажется. + // Возвращаем для таких детей 6 - как будто они полностью прозрачны + if ( !buffers.ContainsKey( control ) ) { + return 6; + } return buffers[ control ].GetOpacityAt( x, y ); } diff --git a/ConsoleFramework/Rendering/RenderingBuffer.cs b/ConsoleFramework/Rendering/RenderingBuffer.cs index ee6255a..8d67181 100644 --- a/ConsoleFramework/Rendering/RenderingBuffer.cs +++ b/ConsoleFramework/Rendering/RenderingBuffer.cs @@ -175,6 +175,21 @@ public void ApplyChild(RenderingBuffer childBuffer, Vector actualOffset, } } + public void SetPixelSafe( int x, int y, char c ) { + if (buffer.GetLength( 0 ) > x && buffer.GetLength( 1 ) > y) + SetPixel( x, y, c ); + } + + public void SetPixelSafe(int x, int y, Attr attr) { + if (buffer.GetLength(0) > x && buffer.GetLength(1) > y) + SetPixel(x, y, attr); + } + + public void SetPixelSafe(int x, int y, char c, Attr attr) { + if (buffer.GetLength(0) > x && buffer.GetLength(1) > y) + SetPixel(x, y, c, attr); + } + public void SetPixel(int x, int y, char c) { buffer[x, y].UnicodeChar = c; } @@ -285,5 +300,11 @@ public bool ContainsOpacity(Rect affectedRect) { public int GetOpacityAt( int x, int y ) { return opacityMatrix[ x, y ]; } + + public void RenderStringSafe( string s, int x, int y, Attr attr ) { + for ( int i = 0; i < s.Length; i++ ) { + SetPixelSafe( x + i, y, s[i], attr ); + } + } } } diff --git a/ConsoleFramework/Xaml/BindingMarkupExtension.cs b/ConsoleFramework/Xaml/BindingMarkupExtension.cs index f1f081a..a409961 100644 --- a/ConsoleFramework/Xaml/BindingMarkupExtension.cs +++ b/ConsoleFramework/Xaml/BindingMarkupExtension.cs @@ -29,7 +29,10 @@ public BindingMarkupExtension(string path) { public object ProvideValue(IMarkupExtensionContext context) { Object realSource = Source ?? context.DataContext; - if (null != realSource && realSource is INotifyPropertyChanged) { + if ( null != realSource && !( realSource is INotifyPropertyChanged ) ) { + throw new ArgumentException("Source must be INotifyPropertyChanged to use bindings"); + } + if (null != realSource) { BindingMode mode = BindingMode.Default; if ( Path != null ) { Type enumType = typeof ( BindingMode ); diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj index 6a93438..2a8a4dc 100644 --- a/Examples/Examples.csproj +++ b/Examples/Examples.csproj @@ -35,7 +35,7 @@ 4 - Examples.Program + Examples.TabControl.Program true @@ -68,12 +68,17 @@ + + + {e1cd529b-e3f1-4660-aa4f-1670e0992553} + Binding + {2A59C284-2995-4F37-8D65-411C25C85493} ConsoleFramework @@ -127,6 +132,9 @@ + + +