/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Reflection; using System.Windows.Forms; using JetBrains.Omea.Containers; using JetBrains.Omea.GUIControls; using JetBrains.Omea.GUIControls.CommandBar; using JetBrains.Omea.OpenAPI; using JetBrains.UI.Components.ImageListButton; using JetBrains.UI.Interop; namespace JetBrains.Omea { /// /// A composite control for a bar that is displayed above the newspaper and provides for narrowing the view and paging. /// public class NewspaperBar : UserControl, IBackgroundBrushProvider { #region Data /// /// "Show" text label. /// protected JetLinkLabel _labelShow; /// /// Combobox with the list of the filtering views available. /// protected ResourceComboBox _comboView; /// /// "with items" text label. /// protected JetLinkLabel _labelWithItems; /// /// An editable combobox for specifying number of items per page. /// protected ResourceComboBox _comboItemsPerPage; /// /// "Items per page" text label.7 /// protected JetLinkLabel _labelItemsPerPage; /// /// Button that goes to the previous page. /// protected ImageListButton _btnPrevPage; /// /// Button that goes to the next page. /// protected ImageListButton _btnNextPage; /// /// An object that manages the newspaper state. /// protected NewspaperManager _man = null; /// /// Required designer variable. /// protected Container components = null; /// /// A list of the available page button controls. /// protected ArrayList _btnPaging = new ArrayList(); /// /// Number of page buttons that are currently visible, and the index of the first unused button in the array. /// protected int _nUsedPagingButtons = 0; /// /// Base tab-index for the paging buttons. /// The first paging button has this index, the following ones — base plus the internal (zero-based) number of its page. /// The prev-page-button has a tab index of base minus 1, and the next-page-button — base plus total number of pages. /// protected static readonly int c_nPagingButtonsTabIndexBase = 1000; /// /// A list of separators that indicate the places where there's a jump in numbering of the paging buttons. /// Populated by -> , used by to implement it on the screen. /// protected Rectangle[] _separators = new Rectangle[0]; /// /// Scale of the form. Affects sizing of the child controls. /// protected SizeF _sizeScale = new SizeF(1, 1); #endregion #region Types /// /// A delegate for a bool-param function. /// public delegate void BoolDelegate(bool param); #endregion #region Construction public NewspaperBar(NewspaperManager man) { // This call is required by the Windows.Forms Form Designer. InitializeComponentSelf(); _man = man; // Wire up the events _man.FilteringViewAdded += new ResourceIndexEventHandler(OnViewAdded); _man.FilteringViewChanged += new ResourcePropIndexEventHandler(OnViewChanged); _man.FilteringViewDeleted += new ResourceIndexEventHandler(OnViewDeleted); _man.Initializing += new EventHandler(OnManInitializing); _man.Deinitializing += new EventHandler(OnManDeinitializing); _man.ItemsPerPageChanged += new EventHandler(OnManItemsPerPageChanged); _man.CurrentFilteringViewChanged += new EventHandler(OnManCurrentViewChanged); _man.PagingChanged += new EventHandler(OnManPagingChanged); } /// /// Clean up any resources being used. /// protected override void Dispose(bool disposing) { if(disposing) { if(components != null) { components.Dispose(); } // Unwire from the events _man.FilteringViewAdded -= new ResourceIndexEventHandler(OnViewAdded); _man.FilteringViewChanged -= new ResourcePropIndexEventHandler(OnViewChanged); _man.FilteringViewDeleted -= new ResourceIndexEventHandler(OnViewDeleted); _man.Initializing -= new EventHandler(OnManInitializing); _man.Deinitializing -= new EventHandler(OnManDeinitializing); _man.ItemsPerPageChanged -= new EventHandler(OnManItemsPerPageChanged); _man.CurrentFilteringViewChanged -= new EventHandler(OnManCurrentViewChanged); _man.PagingChanged -= new EventHandler(OnManPagingChanged); _man = null; } base.Dispose(disposing); } #endregion #region Visual Init /// /// Visual Init. /// protected void InitializeComponentSelf() { SuspendLayout(); // // _labelShow // _labelShow = new JetLinkLabel(); _labelShow.BackColor = Color.Transparent; _labelShow.Name = "_labelShow"; _labelShow.TabIndex = 0; _labelShow.Text = "Show"; _labelShow.ClickableLink = false; _labelShow.Visible = false; _labelShow.AutoSize = true; // // _comboView // _comboView = new ResourceComboBox(); _comboView.DropDownStyle = ComboBoxStyle.DropDownList; _comboView.Name = "_comboView"; _comboView.TabIndex = 1; _comboView.SelectedIndexChanged += new EventHandler(OnViewsSelectionChange); _comboView.EnterPressed += new KeyEventHandler(OnViewsEnterPressed); _comboView.EscapePressed += new KeyEventHandler(OnViewsEscapePressed); _comboView.CloseUp += new EventHandler(OnViewsCloseUp); _comboView.Visible = false; _comboView.Width = 150; // // _labelWithItems // _labelWithItems = new JetLinkLabel(); _labelWithItems.BackColor = Color.Transparent; _labelWithItems.Name = "_labelWithItems"; _labelWithItems.TabIndex = 0; _labelWithItems.Text = "with"; _labelWithItems.ClickableLink = false; _labelWithItems.Visible = false; _labelWithItems.AutoSize = true; // // _comboItemsPerPage // _comboItemsPerPage = new ResourceComboBox(); _comboItemsPerPage.Name = "_comboItemsPerPage"; _comboItemsPerPage.TabIndex = 2; _comboItemsPerPage.TextChanged += new EventHandler(OnItemsPerPageChange); _comboItemsPerPage.SelectedValueChanged += new EventHandler(OnItemsPerPageChange); _comboItemsPerPage.EnterPressed += new KeyEventHandler(OnItemsPerPageEnterPressed); _comboItemsPerPage.EscapePressed += new KeyEventHandler(OnItemsPerPageEscapePressed); _comboItemsPerPage.CloseUp += new EventHandler(OnItemsPerPageCloseUp); _comboItemsPerPage.Leave += new EventHandler(OnItemsPerPageLeave); _comboItemsPerPage.Visible = false; _comboItemsPerPage.Width = 75; // // _labelItemsPerPage // _labelItemsPerPage = new JetLinkLabel(); _labelItemsPerPage.BackColor = Color.Transparent; _labelItemsPerPage.Name = "_labelItemsPerPage"; _labelItemsPerPage.TabIndex = 0; _labelItemsPerPage.Text = "items per page"; _labelItemsPerPage.ClickableLink = false; _labelItemsPerPage.Visible = false; _labelItemsPerPage.AutoSize = true; // Prev Button _btnPrevPage = new ImageListButton(); _btnPrevPage.Text = "< Prev"; _btnPrevPage.Click += new EventHandler(OnPrevPageClick); _btnPrevPage.Visible = false; _btnPrevPage.Cursor = Cursors.Hand; _btnPrevPage.AddIcon(new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream("OmniaMea.Icons.Newspaper.PreviousPage.Normal.ico")), ImageListButton.ButtonState.Normal); _btnPrevPage.AddIcon(new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream("OmniaMea.Icons.Newspaper.PreviousPage.Hot.ico")), ImageListButton.ButtonState.Hot); _btnPrevPage.AddIcon(new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream("OmniaMea.Icons.Newspaper.PreviousPage.Disabled.ico")), ImageListButton.ButtonState.Disabled); // Next Button _btnNextPage = new ImageListButton(); _btnNextPage.Text = "Next >"; _btnNextPage.Click += new EventHandler(OnNextPageClick); _btnNextPage.Visible = false; _btnNextPage.Cursor = Cursors.Hand; _btnNextPage.AddIcon(new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream("OmniaMea.Icons.Newspaper.NextPage.Normal.ico")), ImageListButton.ButtonState.Normal); _btnNextPage.AddIcon(new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream("OmniaMea.Icons.Newspaper.NextPage.Hot.ico")), ImageListButton.ButtonState.Hot); _btnNextPage.AddIcon(new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream("OmniaMea.Icons.Newspaper.NextPage.Disabled.ico")), ImageListButton.ButtonState.Disabled); // // NewspaperBar // Controls.Add(_labelItemsPerPage); Controls.Add(_comboItemsPerPage); Controls.Add(_labelWithItems); Controls.Add(_comboView); Controls.Add(_labelShow); Controls.Add(_btnPrevPage); Controls.Add(_btnNextPage); SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.CacheText | ControlStyles.ContainerControl | ControlStyles.Opaque | ControlStyles.ResizeRedraw | ControlStyles.UserPaint | ControlStyles.EnableNotifyMessage , true); SetStyle(ControlStyles.StandardClick | ControlStyles.StandardDoubleClick | ControlStyles.Selectable , false); UpdateStyles(); Name = "NewspaperBar"; Height = 24; Enabled = false; Dock = DockStyle.Bottom; // Sets the newspaper bar location: either above the newspaper or below it Font = new Font("Tahoma", 8.25F, FontStyle.Regular, GraphicsUnit.Point, ((Byte)(204))); ResumeLayout(false); } #endregion #region Global Events /// /// Causes the newspaper bar to lose focus and passes it to the newspaper viewer itself. /// protected void LoseFocus() { Parent.SelectNextControl(this, true, true, true, true); } /// /// Processes the native Windows messages. /// /// This is a hack which is needed in here because the combobox cannot catch its own notifications :( protected override void WndProc(ref Message m) { switch(m.Msg) { case Win32Declarations.WM_COMMAND: switch(Win32Declarations.HIWORD((UInt32)m.WParam)) { case (UInt16)ComboBoxNotification.CBN_CLOSEUP: OnCloseUp(Control.FromChildHandle(m.LParam)); break; } break; } base.WndProc(ref m); } /// /// Invokes when either of the child comboboxes is dropped down and then closed back. /// /// This is a hack which is needed in here because the combobox cannot catch its own notifications :( protected void OnCloseUp(Control source) { if(source == _comboItemsPerPage) OnItemsPerPageCloseUp(source, EventArgs.Empty); else if(source == _comboView) OnViewsCloseUp(source, EventArgs.Empty); } /// /// Handles the Newspaper Manager's event and defers its processing to a bit later time. /// protected void OnManInitializing(object sender, EventArgs args) { Core.UserInterfaceAP.QueueJobAt(DateTime.Now.AddMilliseconds(100), new MethodInvoker(OnManInitializingDeferred), new object[0]); } /// /// Initializes the newspaper bar controls. /// protected void OnManInitializingDeferred() { if((_man != null) && (_man.IsInitialized)) { using(new LayoutSuspender(this)) { Enabled = true; PopulateViewsCombo(); PopulateItemsPerPageCombo(); _labelItemsPerPage.Visible = true; _labelShow.Visible = true; _labelWithItems.Visible = true; } } } protected void OnManDeinitializing(object sender, EventArgs e) { using(new LayoutSuspender(this)) { _comboItemsPerPage.Items.Clear(); _comboView.Items.Clear(); Enabled = false; } } #endregion #region Views Combobox protected void OnViewAdded(object sender, ResourceIndexEventArgs e) { _comboView.Items.Add(e.Resource); } protected void OnViewChanged(object sender, ResourcePropIndexEventArgs e) { // If the changed item is being displayed, update its text rep _comboView.Update(); // This re-retrieves the view name } protected void OnViewDeleted(object sender, ResourceIndexEventArgs e) { // Find the item that is being deleted int nItem = _comboView.Items.IndexOf(e.Resource); if(nItem != -1) { // If this view is selected, select another one instead if((nItem == _comboView.SelectedIndex) && (_comboView.Items.Count > 1)) { if(nItem < _comboView.Items.Count - 1) _comboView.SelectedIndex = nItem + 1; // Select the next view, if possible else if(nItem > 0) _comboView.SelectedIndex = nItem - 1; // Select the prev view otherwise } // Drop this item from the combobox _comboView.Items.RemoveAt(nItem); } else Debug.Assert(false, "The updated view is not present in the combo box."); } /// /// Populate the Views combobox. /// protected void PopulateViewsCombo() { // Remove the old views _comboView.Items.Clear(); // Add the "All" view _comboView.Items.Add("All"); // Add the "Unread" view explicitly (if it exists) IResourceList listUnreads = Core.ResourceStore.FindResources(SelectionType.Normal, "SearchView", "DeepName", "Unread"); IResourceList listViewsButUnread = _man.FilteringViews; if(listUnreads.Count == 1) { _comboView.Items.Add(listUnreads[0]); listViewsButUnread = listViewsButUnread.Minus(listUnreads); // Exclude from the further adding } // Add all the other items, as a tree _comboView.AddFolderedResourceTree(Core.ResourceTreeManager.ResourceTreeRoot, "SearchView", "ViewFolder", Core.Props.Parent, 0, _man.FilteringViews, false, true); _comboView.Enabled = _comboView.Visible = true; // Assign selection if(_man.CurrentFilteringView == null) _comboView.SelectedIndex = 0; else _comboView.SelectedIndex = _comboView.Items.IndexOf(_man.CurrentFilteringView); } protected void OnManCurrentViewChanged(object sender, EventArgs e) { UpdateCurrentViewData(false); } protected void OnViewsCloseUp(object sender, EventArgs e) { OnViewsEnterPressed(sender, new KeyEventArgs(Keys.Enter)); } protected void OnViewsEnterPressed(object sender, KeyEventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; // Not quite ready // Check IResource resUI = (_comboView.Items.Count != 0 ? _comboView.SelectedItem as IResource : null); // A filtering view currently selected in the UI, or Null for the "All" view if((resUI != null) && (!_man.FilteringViews.Contains(resUI))) { MessageBox.Show(Parent, String.Format("\"{0}\" is not a valid filtering view for the newspaper.\n\nNote that you cannot select the view folders as filtering views.", resUI), "Newspaper View — " + Core.ProductFullName, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Apply! UpdateCurrentViewData(true); LoseFocus(); } protected void OnViewsEscapePressed(object sender, KeyEventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; // Not quite ready // Update with the actual background data UpdateCurrentViewData(false); LoseFocus(); } protected void OnViewsSelectionChange(object sender, EventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; // Not quite ready // Defer application of the new value Core.UserInterfaceAP.QueueJobAt(DateTime.Now.AddMilliseconds(1000), "Apply User Input for Current View.", new BoolDelegate(UpdateCurrentViewData), true); } /// /// Sends the current view data from UI to Manager ( is True), or vice versa. /// protected void UpdateCurrentViewData(bool bFromUI) { if((_man == null) || (!_man.IsInitialized)) return; IResource resUI = (_comboView.Items.Count != 0 ? _comboView.SelectedItem as IResource : null); // A filtering view currently selected in the UI, or Null for the "All" view if(resUI != _man.CurrentFilteringView) { // UI selection differs from the codebehind if(bFromUI) // UI -> Manager { if((resUI == null) || (_man.FilteringViews.Contains(resUI))) // Validate (there are also view folders in the list) _man.CurrentFilteringView = resUI; } else { // Manager -> UI // Visualize the new setting (if necessary) int nNewIndex = 0; // Default — "All" selection for the Null value if(_man.CurrentFilteringView != null) { nNewIndex = _comboView.Items.IndexOf(_man.CurrentFilteringView); // Find this item in the combobox if(nNewIndex == -1) throw new InvalidOperationException("Inconsistency: a valid view is not present in the combobox."); } if(_comboView.SelectedIndex != nNewIndex) // Apply selection only if it's not the same; avoid re-switching _comboView.SelectedIndex = nNewIndex; } } } #endregion #region ItemsPerPage Combo /// /// Invokes when focus leaves the items-per-page combobox. /// protected void OnItemsPerPageLeave(object sender, EventArgs e) { UpdateItemsPerPageData(false); // Display the actual value } /// /// The Newspaper Manager's value for items on page has changed. /// protected void OnManItemsPerPageChanged(object sender, EventArgs e) { // Visualize the new value (if necessary) UpdateItemsPerPageData(false); } /// /// Fills in the items-per-page combobox. /// protected void PopulateItemsPerPageCombo() { // Clear _comboItemsPerPage.Items.Clear(); // Load the new settings string sDefault = "10,25,50,100"; string sValues = Core.SettingStore.ReadString(_man.GetSettingsKey(true), "ItemsPerPageValues", sDefault); // Fill in string[] arValues = sValues.Split(','); // String rep of individual proposed values foreach(string sValue in arValues) { try { // Convert to int and then back to string to canonicize and avoid illegal values in the default-values-combo int nValue = int.Parse(sValue); // This also checks if the string is a valid number if((nValue <= 0) || (nValue >= NewspaperManager.c_nMaxItemsOnPage)) // Check the constraints { Trace.WriteLine("Warning: the \"{0}\" value for the ItemsPerPage combobox does not fall into the allowed range.", sValue); continue; } _comboItemsPerPage.Items.Add(nValue.ToString()); } catch(Exception) { Trace.WriteLine("Warning: the \"{0}\" value for the ItemsPerPage combobox is not coercible to an integer.", sValue); } } _comboItemsPerPage.Visible = _comboItemsPerPage.Enabled = true; // Set selection or explicit text to the proper item UpdateItemsPerPageData(false); // Reapply the new value after some time; this helps to prevent the combobox from setting the value whose prefix is currently typed in Core.UserInterfaceAP.QueueJobAt(DateTime.Now.AddMilliseconds(100), "Apply Initial Value for Items per Page.", new BoolDelegate(UpdateItemsPerPageData), false); } /// /// The combobox text has changed. /// protected void OnItemsPerPageChange(object sender, EventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; // Not quite ready // Defer application of the new value Core.UserInterfaceAP.QueueJobAt(DateTime.Now.AddMilliseconds(1000), "Apply User Input for Items per Page.", new BoolDelegate(UpdateItemsPerPageData), true); } /// /// Applies a value from the combobox to the Newspaper Manager (if is True) /// or vice versa. /// Deferred-invoked when text in the combobox changes, or immediately-invoked when the Enter/ESC key is pressed. /// protected void UpdateItemsPerPageData(bool bFromUI) { if((_man == null) || (!_man.IsInitialized)) return; Trace.WriteLine( String.Format("Updating items-per-page data in direction {0}.", bFromUI), "[NPB]" ); if(_man.ItemsPerPage.ToString() != _comboItemsPerPage.Text) { // UI value differs from the codebehind value if(bFromUI) { // UI -> Manager try { _man.ItemsPerPage = int.Parse(_comboItemsPerPage.Text); } catch(Exception ex) { // Means that the value entered into the control is not a valid number, do nothing Trace.WriteLine("Failed to apply the Items per Page combobox value. " + ex.Message, "[NPB]"); } } else { // Manager -> UI int nIndex = _comboItemsPerPage.FindStringExact(_man.ItemsPerPage.ToString()); _comboItemsPerPage.SelectedIndex = nIndex; if(nIndex < 0) // Select the existing item with such a value, or remove list item selection if there is none available _comboItemsPerPage.Text = _man.ItemsPerPage.ToString(); // There's no stock item with this value, type it by hand } } } protected void OnItemsPerPageEnterPressed(object sender, KeyEventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; // Not quite ready e.Handled = true; ///////////// // Validate // Check for an empty value if(_comboItemsPerPage.Text.Length == 0) { MessageBox.Show("The number of items per page cannot be empty.", Core.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Try coercing to a number int nItemsPerPage; try { nItemsPerPage = int.Parse(_comboItemsPerPage.Text); } catch(Exception) { MessageBox.Show(String.Format("\"{0}\" cannot be coerced to an integer value and is not a valid setting for number of items per page.", _comboItemsPerPage.Text), Core.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Check if non-positive if((nItemsPerPage <= 0) || (nItemsPerPage >= NewspaperManager.c_nMaxItemsOnPage)) { MessageBox.Show(String.Format("The number of items per page must be a positive integer below {0}.", NewspaperManager.c_nMaxItemsOnPage), Core.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } /////////// // Apply! // Apply the new setting UpdateItemsPerPageData(true); // Lose the focus LoseFocus(); } protected void OnItemsPerPageEscapePressed(object sender, KeyEventArgs e) { UpdateItemsPerPageData(false); // In case the input is invalid, cause the previous valid value to appear in the editbox LoseFocus(); } protected void OnItemsPerPageCloseUp(object sender, EventArgs e) { OnItemsPerPageEnterPressed(sender, new KeyEventArgs(Keys.Enter)); } #endregion #region Paging Toolbar /// /// The set of pages or current page has changed. /// protected void OnManPagingChanged(object sender, EventArgs e) { PerformLayout(); } /// /// The prev-page button has been clicked. /// private void OnPrevPageClick(object sender, EventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; if(_man.CurrentPage > 0) _man.CurrentPage--; LoseFocus(); } /// /// The next-page button has been clicked. /// private void OnNextPageClick(object sender, EventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; if(_man.CurrentPage < _man.PagesCount - 1) _man.CurrentPage++; LoseFocus(); } /// /// Creates and returns a disposable brush for painting a background of a child control that resides on this one. /// If a brush is a gradient brush, it has to be specifically adjusted to fit the background. /// /// Control for which the brush is being requested. /// This may be needed for calculating the rect for gradient brushes. /// Background brush. public Brush GetBackgroundBrush(Control sender) { // Perform a safety check to ensure that the rectangle falls within the parent control Rectangle rectBrush = sender.RectangleToClient(RectangleToScreen(ClientRectangle)); if((rectBrush.Width != 0) && (rectBrush.Height != 0)) return new LinearGradientBrush(rectBrush, SystemColors.ControlLight, SystemColors.Control, LinearGradientMode.Vertical); else return new SolidBrush(SystemColors.Control); } #endregion #region Layouting #region Layouting Constants /// /// Layouting Constants Class. /// public class Const { /// /// Horizontal margin of the control. /// public static readonly int HorMargin = 5; /// /// Vertical margin of the control. /// public static readonly int VerMargin = 1; /// /// Gep between the adjacent controls. /// public static readonly int Gap = 5; /// /// Horizontal spacing between the last of the filtering controls and the first paging button (including the prev/next buttons). /// public static readonly int HorSpacingBeforePagingButtons = 20; /// /// Distance from the paging button text (displayed within) to the button edge. /// public static readonly int PagingButtonTextHorPadding = 7; /// /// Distance from the paging button text (displayed within) to the button edge. /// public static readonly int PagingButtonTextVerPadding = 2; /// /// Width of the separator between the paging buttons that is inserted in case there's a jump in numbers of the adjacent buttons (instead of the cut-out buttons). /// public static readonly int PagingJumpSeparatorHorSpacing = 4; /// /// Padding between the ends of the hover-underline of the paging button and the button edges. /// public static readonly int PagingButtonHoverUnderlinePadding = 2; } #endregion /// /// Layouts the controls. /// protected override void OnLayout(LayoutEventArgs levent) { // Calculate the client area Rectangle client = ClientArea; // If the newspaper manager has not been initialized, hide the controls and avoid layouting them if((_man == null) || ((_man == null) || (!_man.IsInitialized))) { foreach(Control control in Controls) // Hide all the controls available control.Visible = false; return; } // Layout the fixed controls int nPos = client.Left; // A list of the controls that belong to the fixed set and are located at the left of the bar Control[] controlsLeftFixed = new Control[] {_labelShow, _comboView, _labelWithItems, _comboItemsPerPage, _labelItemsPerPage}; bool bFirst = true; foreach(Control control in controlsLeftFixed) { if(nPos + control.Width <= client.Right) { if(bFirst) // Do not add a gap before the first control bFirst = false; else nPos += Const.Gap; control.Location = new Point(nPos, client.Top + (client.Height - control.Height) / 2); // V-center control.Visible = true; nPos += control.Width; } else control.Visible = false; } // Add the horizontal spacing before the paging buttons nPos += Const.HorSpacingBeforePagingButtons; // Place the "Prev" button if(nPos + _btnPrevPage.Width <= client.Right) { _btnPrevPage.Location = new Point(nPos, client.Top + (client.Height - _btnPrevPage.Height) / 2); // V-center _btnPrevPage.Visible = true; nPos += _btnPrevPage.Width; nPos += Const.Gap; } else _btnPrevPage.Visible = false; // Calculate the space left for the paging controls (exclude the Next button that is about to appear on the right) Rectangle rectPaging = Rectangle.FromLTRB(nPos, client.Top, client.Right - _btnNextPage.Width - Const.Gap, client.Bottom); // Layout the paging controls OnLayout_Paging(ref rectPaging); // Place the "Next" button if(rectPaging.Right + Const.Gap + _btnNextPage.Width <= client.Right) { _btnNextPage.Location = new Point(rectPaging.Right + Const.Gap, client.Top + (client.Height - _btnNextPage.Height) / 2); // V-center _btnNextPage.Visible = true; } else _btnNextPage.Visible = false; // Repaint the separators Invalidate(false); } protected override void ScaleCore(float dx, float dy) { _sizeScale = new SizeF(dx, dy); // Apply scaling to the bar (the controls will be scaled automatically) Height = (int)(24 * dy); } /// /// Calculates layout for the paging buttons, and updates the rectangle to indicate the actually-consumed space. /// private void OnLayout_Paging(ref Rectangle paging) { // No place for the controls? _nUsedPagingButtons = 0; // Mark all the buttons as unused if((paging.Width <= 0) || (paging.Height <= 0)) { // Not enough place for layouing a single button foreach(Control control in _btnPaging) control.Visible = false; // Validate the rectangle to zero width paging = Rectangle.FromLTRB(paging.Left, paging.Top, paging.Left, paging.Bottom); return; } int nCurPage = _man.CurrentPage; int nNumPages = _man.PagesCount; int nAvailWidth = paging.Width; // Width available for all the buttons nAvailWidth -= Const.PagingJumpSeparatorHorSpacing * 2; // Reserve some place for the two possible cuttings RedBlackTree treePresent = new RedBlackTree(); InsertPagingButton(treePresent, ref nAvailWidth, 0); // First page button that is never cut InsertPagingButton(treePresent, ref nAvailWidth, nNumPages - 1); // Last page button that is never cut InsertPagingButton(treePresent, ref nAvailWidth, nCurPage); // The current page button // Now go on inserting the buttons on both sides of the current page int nMaxOffs = Math.Max(nCurPage - 0 - 1, (nNumPages - 1) - nCurPage - 1); // Maximum distance from the current page to the ends for(int nOffs = 1; nOffs <= nMaxOffs; nOffs++) { // Left side: if within the scope, try inserting; break if space is thru if((nCurPage - nOffs > 0) && (InsertPagingButton(treePresent, ref nAvailWidth, nCurPage - nOffs) == InsertPagingButtonResult.NoRoom)) break; // Right side: if within the scope, try inserting; break if space is thru if((nCurPage + nOffs < nNumPages - 1) && (InsertPagingButton(treePresent, ref nAvailWidth, nCurPage + nOffs) == InsertPagingButtonResult.NoRoom)) break; } // Implement the results in the layouting RBNodeBase node = treePresent.GetMinimumNode(); // The first page button int nPrevPageNumber = -1; // Check for cuttings (jumps in numbering) int nCurPos = paging.Left; ArrayList separators = new ArrayList(); // Collect the separators that indicate the jumps in page numbers here while(node != null) { PagingButton button = (PagingButton)node.Key; // If there's a numbering jump, add a spacing (don't check for the very first button) if((nPrevPageNumber >= 0) && (button.PageNumber != nPrevPageNumber + 1)) { // Add the separator-drawing info separators.Add(new Rectangle(nCurPos, paging.Top, Const.PagingJumpSeparatorHorSpacing, paging.Height)); nCurPos += Const.PagingJumpSeparatorHorSpacing; } // Place the button button.Size = button.OptimalSize; button.Location = new Point(nCurPos, paging.Top + (paging.Height - button.Height) / 2); button.Visible = true; nCurPos += button.Width; nPrevPageNumber = button.PageNumber; // Advance to the next button node = treePresent.GetNext(node); } _separators = (Rectangle[])separators.ToArray(typeof(Rectangle)); // Hide the buttons that have been created but are unused HideUnusedPagingButtons(); // Update the prev-next page buttons state _btnNextPage.Enabled = nCurPage < nNumPages - 1; _btnNextPage.TabIndex = c_nPagingButtonsTabIndexBase + nNumPages; _btnPrevPage.Enabled = nCurPage > 0; _btnPrevPage.TabIndex = c_nPagingButtonsTabIndexBase - 1; // Return the actually-used rectangle paging = Rectangle.FromLTRB(paging.Left, paging.Top, nCurPos, paging.Bottom); } /// /// Inserts a paging button to the pending list /// /// Tree that holds the buttons. /// The available width that is decreased in case of a successful insertion. /// Page number for the button. /// Whether the insertion had success. private InsertPagingButtonResult InsertPagingButton(RedBlackTree tree, ref int nAvailWidth, int nPageNumber) { // Get the button and measure its wannabe-size PagingButton button = GetPagingButton(nPageNumber, false); Size sizeButton = button.OptimalSize; // Is there room for it left? // Note: even if there's no room, we have to check if it's present already if(sizeButton.Width > nAvailWidth) return tree.Search(button) != null ? InsertPagingButtonResult.AlreadyThere : InsertPagingButtonResult.NoRoom; // Has it been inserted already? RBNodeBase foundOrNew; if(tree.SearchOrInsert(button, out foundOrNew)) // Try inserting a new button, true retval indicates a failute (the item is already there) return InsertPagingButtonResult.AlreadyThere; // Allow it to be inserted button = GetPagingButton(nPageNumber, true); nAvailWidth -= button.OptimalSize.Width; return InsertPagingButtonResult.OK; // Inserted a new one } /// /// Possible result of the function execution. /// private enum InsertPagingButtonResult { OK, NoRoom, AlreadyThere } /// /// Returns the next free paging button without marking it as used. /// /// Number of the page that should be assigned to the button. /// Either marks the button as used or not. private PagingButton GetPagingButton(int nPageNumber, bool bMarkAsUsed) { PagingButton button; if(_nUsedPagingButtons == _btnPaging.Count) { // If no more free buttons, create one button = new PagingButton(nPageNumber, this, _man); button.Visible = false; button.Font = Font; _btnPaging.Add(button); Controls.Add(button); } else // Reuse some free button button = (PagingButton)_btnPaging[_nUsedPagingButtons]; button.PageNumber = nPageNumber; if(bMarkAsUsed) _nUsedPagingButtons++; return button; } /// /// Makes the unused buttons invisible. /// private void HideUnusedPagingButtons() { for(int a = _nUsedPagingButtons; a < _btnPaging.Count; a++) ((PagingButton)_btnPaging[a]).Visible = false; } #endregion #region Painting protected override void OnPaint(PaintEventArgs e) { Rectangle rectNoBorder = ClientRectangle; rectNoBorder = new Rectangle(rectNoBorder.Left, rectNoBorder.Top + (Dock == DockStyle.Bottom ? 1 : 0), rectNoBorder.Width, rectNoBorder.Height - 1); // Leave some place for the border, either at the top (docked at bottom) or at the bottom (docked at top) // Background using(Brush brush = GetBackgroundBrush(this)) e.Graphics.FillRectangle(brush, rectNoBorder); // Border using(Brush brush = new SolidBrush(NewspaperViewer.c_colorBorder)) { Rectangle client = ClientRectangle; e.Graphics.FillRectangle(brush, new Rectangle(client.Left, (Dock == DockStyle.Bottom ? client.Top : client.Bottom - 1), client.Width, 1)); } // Paint the control, if it's initialized and ready, otherwise, show the banner if((_man != null) && (_man.IsInitialized)) { // Paint the separators at the places where the jumps in page numbering are located (collected by the layouting) foreach(Rectangle rectangle in _separators) { int nLeft = (rectangle.Left + rectangle.Right) / 2 - 1; e.Graphics.FillRectangle(SystemBrushes.ControlDark, new Rectangle(nLeft, rectangle.Top, 1, rectangle.Height)); e.Graphics.FillRectangle(SystemBrushes.ControlLightLight, new Rectangle(nLeft + 1, rectangle.Top, 1, rectangle.Height)); } } else // The "initializing …" banner JetLinkLabel.DrawText(e.Graphics, "Newspaper is not ready, please wait …", rectNoBorder, Font, SystemColors.Control, DrawTextFormatFlags.DT_CENTER | DrawTextFormatFlags.DT_VCENTER | DrawTextFormatFlags.DT_SINGLELINE | DrawTextFormatFlags.DT_END_ELLIPSIS); } #endregion #region Attributes /// /// Controls the docking of the Newspaper Bar: either at top or at bottom. /// Note that this does not actually affect the parent newspaper layouting, but controls the way Newspaper bar draws its borders, to avoid the double borders. /// public override DockStyle Dock { get { return base.Dock; } set { if(!((value == DockStyle.Top) || (value == DockStyle.Bottom))) throw new ArgumentException("The bar can be docked at either top or bottom. Other values are prohibited."); if(value != base.Dock) base.Dock = value; PerformLayout(); // Update the borders Invalidate(); // Repaint } } /// /// Gets the client are of the Newspaper bar, which is the minus the border(s) and margins. /// public Rectangle ClientArea { get { Rectangle client = ClientRectangle; // Leave some place for the border, either at the top (docked at bottom) or at the bottom (docked at top) client = new Rectangle(client.Left, client.Top + (Dock == DockStyle.Bottom ? 1 : 0), client.Width, client.Height - 1); // Account for the margins client.Inflate(-Const.HorMargin, -Const.VerMargin); return client; } } #endregion #region Class PagingButton — A class that represents a paging button. /// /// A class that represents a paging button. /// internal class PagingButton : UserControl, IComparable, ICommandBar { #region Data /// /// Number of the page represented by this button. /// protected int _nPageNumber; /// /// Newspaper bar that owns the control. /// protected readonly NewspaperBar _bar; /// /// The newspaper manager. /// protected readonly NewspaperManager _man; /// /// Bounding rectangle of the button text. /// protected Rectangle _rectText = Rectangle.Empty; /// /// True if the button is currently hovered with mouse. /// protected bool _bHovered = false; #endregion #region Construction internal PagingButton(int nPageNumber, NewspaperBar bar, NewspaperManager man) { _nPageNumber = nPageNumber; _bar = bar; _man = man; // Set the control styles SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.CacheText | ControlStyles.FixedHeight | ControlStyles.FixedWidth | ControlStyles.Opaque | ControlStyles.ResizeRedraw | ControlStyles.StandardClick | ControlStyles.UserPaint , true); // Set the back color (it applies to an active button only, otherwise, transparency is emulated) BackColor = ColorManagement.Mix(SystemColors.Control, SystemColors.ControlDark, 0.66); } #endregion #region IComparable Members /// /// Provides for comparing the page buttons by their page number. /// public int CompareTo(object obj) { return PageNumber.CompareTo(((PagingButton)obj).PageNumber); } #endregion #region Attributes /// /// Number of the page represented by this button. /// Internal rep, zero-based. /// public int PageNumber { get { return _nPageNumber; } set { // Store & update the related properties _nPageNumber = value; Text = DisplayPageNumber.ToString(); Enabled = !Active; TabIndex = NewspaperBar.c_nPagingButtonsTabIndexBase + _nPageNumber; Cursor = Active ? Cursors.Default : Cursors.Hand; // The first, last, and current pages are painted in bold bool bBold = false; if((_man != null) && (_man.IsInitialized)) bBold = (_nPageNumber == 0) || (_nPageNumber == _man.PagesCount - 1) || (_nPageNumber == _man.CurrentPage); Font = bBold ? new Font(_bar.Font, FontStyle.Bold) : _bar.Font; // Calculate the text bounds _rectText = new Rectangle(new Point(0, 0), JetLinkLabel.GetTextSize(this, Text, Font)); PerformLayout(); // Adjust placement of the text rect Invalidate(); // Repaint the button } } /// /// Page number as it should be displayed to user (one-based). /// public int DisplayPageNumber { get { return _nPageNumber + 1; } } /// /// Gets whether this button represents an active page. /// public bool Active { get { return (_man.IsInitialized) && (_man.CurrentPage == _nPageNumber); } } #endregion #region ICommandBar Members public void SetSite(ICommandBarSite site) { } public Size MinSize { get { return new Size(10, 10); } // Just let it be } public Size MaxSize { get { return new Size(int.MaxValue, int.MaxValue); } // Just let it be } /// /// This is the destiation size of the button. The real size is not changed until it is shown in the layout. /// public Size OptimalSize { get { return _rectText.Size + new Size(Const.PagingButtonTextHorPadding * 2, Const.PagingButtonTextVerPadding * 2); } } public Size Integral { get { return new Size(1, 1); } } #endregion #region Overrides /// /// The paging button has been clicked. Try switching to the page. /// protected override void OnClick(EventArgs e) { if((_man == null) || (!_man.IsInitialized)) return; if(PageNumber < _man.PagesCount) _man.CurrentPage = PageNumber; _bar.LoseFocus(); // Return focus to the reading pane } protected override void OnLayout(LayoutEventArgs levent) { if(_rectText == Rectangle.Empty) // The button has not been assigned the page number, no layouting possible return; // Update the text rectangle placement Rectangle client = ClientRectangle; _rectText = new Rectangle(new Point(client.Left + (client.Width - _rectText.Width) / 2, client.Top + (client.Height - _rectText.Height) / 2), _rectText.Size); } protected override void OnPaint(PaintEventArgs e) { Rectangle client = ClientRectangle; // Background using(Brush brush = Active ? new SolidBrush(BackColor) : _bar.GetBackgroundBrush(this)) e.Graphics.FillRectangle(brush, client); // Button text JetLinkLabel.DrawText(e.Graphics, Text, _rectText, Font, (Active ? Color.Black : Color.Blue), DrawTextFormatFlags.DT_NOPREFIX | DrawTextFormatFlags.DT_SINGLELINE); // Underline the hovered button if((_bHovered) && (!Active)) { using(Brush brush = new SolidBrush(Color.Blue)) e.Graphics.FillRectangle(brush, Rectangle.FromLTRB(client.Left + Const.PagingButtonHoverUnderlinePadding, _rectText.Bottom - 1, client.Right - Const.PagingButtonHoverUnderlinePadding, _rectText.Bottom)); } } protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _bHovered = true; Invalidate(new Rectangle(0, _rectText.Bottom - 1, ClientRectangle.Width, 1)); } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _bHovered = false; Invalidate(new Rectangle(0, _rectText.Bottom - 1, ClientRectangle.Width, 1)); } #endregion } #endregion } }