private static void TryGetScreenshot(Control controlForScreenshotting)
        {
            SafeInvoke.InvokeIfPossible("Screen Shot", controlForScreenshotting, false, () =>
            {
                try
                {
                    var bounds     = controlForScreenshotting.Bounds;
                    var screenshot = new Bitmap(bounds.Width, bounds.Height);
                    using (var g = Graphics.FromImage(screenshot))
                    {
                        if (controlForScreenshotting.Parent == null)
                        {
                            g.CopyFromScreen(bounds.Left, bounds.Top, 0, 0, bounds.Size);                                       // bounds already in screen coords
                        }
                        else
                        {
                            g.CopyFromScreen(controlForScreenshotting.PointToScreen(new Point(bounds.Left, bounds.Top)), Point.Empty, bounds.Size);
                        }
                    }

                    _screenshotTempFile = TempFile.WithFilename(ScreenshotName);
                    RobustImageIO.SaveImage(screenshot, _screenshotTempFile.Path, ImageFormat.Png);
                }
                catch (Exception e)
                {
                    ResetScreenshotFile();
                    Logger.WriteError("Bloom was unable to create a screenshot.", e);
                }
            }
                                        );
        }
示例#2
0
        /// <summary>
        /// Called when a file system watcher notices a new book (or some similar change) in our downloaded books folder.
        /// This will happen on a thread-pool thread.
        /// Since we are updating the UI in response we want to deal with it on the main thread.
        /// That also has the effect of a lock, preventing multiple threads trying to respond to changes.
        /// The main purpose of this method is to debounce such changes, since lots of them may
        /// happen in succession while downloading a book, and also some that we don't want
        /// to process may happen while we are selecting one. Debounced changes result in a websocket message
        /// that acts as an event for Javascript, and also raising a C# event.
        /// </summary>
        private void DebounceFolderChanged(string fullPath)
        {
            var shell = Application.OpenForms.Cast <Form>().FirstOrDefault(f => f is Shell);

            SafeInvoke.InvokeIfPossible("update downloaded books", shell, true, (Action)(() =>
            {
                // We may notice a change to the downloaded books directory before the other Bloom instance has finished
                // copying the new book there. Finishing should not take long, because the download is done...at worst
                // we have to copy the book on our own filesystem. Typically we only have to move the directory.
                // As a safeguard, wait half a second before we update things.
                if (_folderChangeDebounceTimer != null)
                {
                    // Things changed again before we started the update! Forget the old update and wait until things
                    // are stable for the required interval.
                    _folderChangeDebounceTimer.Stop();
                    _folderChangeDebounceTimer.Dispose();
                }
                _folderChangeDebounceTimer = new Timer();
                _folderChangeDebounceTimer.Tick += (o, args) =>
                {
                    _folderChangeDebounceTimer.Stop();
                    _folderChangeDebounceTimer.Dispose();
                    _folderChangeDebounceTimer = null;

                    // Updating the books involves selecting the modified book, which might involve changing
                    // some files (e.g., adding a branding image, BL-4914), which could trigger this again.
                    // So don't allow it to be triggered by changes to a folder we're already sending
                    // notifications about.
                    // (It's PROBABLY overkill to maintain a set of these...don't expect a notification about
                    // one folder to trigger a change to another...but better safe than sorry.)
                    // (Note that we don't need synchronized access to this dictionary, because all this
                    // happens only on the UI thread.)
                    if (!_changingFolders.Contains(fullPath))
                    {
                        try
                        {
                            _changingFolders.Add(fullPath);
                            _webSocketServer.SendEvent("editableCollectionList", "reload:" + PathToDirectory);
                            if (FolderContentChanged != null)
                            {
                                FolderContentChanged(this, new ProjectChangedEventArgs()
                                {
                                    Path = fullPath
                                });
                            }
                        }
                        finally
                        {
                            // Now we need to arrange to remove it again. Not right now, because
                            // whatever changes might be made during event handling may get noticed slightly later.
                            // But we don't want to miss it if the book gets downloaded again.
                            RemoveFromChangingFoldersLater(fullPath);
                        }
                    }
                };
                _folderChangeDebounceTimer.Interval = 500;
                _folderChangeDebounceTimer.Start();
            }));
        }
示例#3
0
        public ErrorResult NotifyUserOfProblem(IRepeatNoticePolicy policy, string alternateButton1Label, ErrorResult resultIfAlternateButtonPressed, string message)
        {
            var returnResult = ErrorResult.OK;

            ReportButtonLabel = GetReportButtonLabel(alternateButton1Label);

            OnReportButtonPressed = (exceptionParam, messageParam) =>
            {
                returnResult = resultIfAlternateButtonPressed;
            };

            var control          = GetControlToUse();
            var forceSynchronous = true;

            SafeInvoke.InvokeIfPossible("Show Error Reporter", control, forceSynchronous, () =>
            {
                NotifyUserOfProblem(policy, null, message);
            });
            return(returnResult);
        }
示例#4
0
        private void ShowNotifyDialog(string severity, string messageText, Exception exception,
                                      string reportButtonLabel, string secondaryButtonLabel)
        {
            // Before we do anything that might be "risky", put the problem in the log.
            ProblemReportApi.LogProblem(exception, messageText, severity);

            // ENHANCE: Allow the caller to pass in the control, which would be at the front of this.
            //System.Windows.Forms.Control control = Form.ActiveForm ?? FatalExceptionHandler.ControlOnUIThread;
            var control        = GetControlToUse();
            var isSyncRequired = false;

            SafeInvoke.InvokeIfPossible("Show Error Reporter", control, isSyncRequired, () =>
            {
                // Uses a browser dialog to show the problem report
                try
                {
                    StartupScreenManager.CloseSplashScreen();                     // if it's still up, it'll be on top of the dialog

                    var message = GetMessage(messageText, exception);

                    if (!Api.BloomServer.ServerIsListening)
                    {
                        // There's no hope of using the HtmlErrorReporter dialog if our server is not yet running.
                        // We'll likely get errors, maybe Javascript alerts, that won't lead to a clean fallback to
                        // the exception handler below. Besides, failure of HtmlErrorReporter in these circumstances
                        // is expected; we just want to cleanly report the original problem, not to report a
                        // failure of error handling.

                        // Note: HtmlErrorReporter supports up to 3 buttons (OK, Report, and [Secondary action]), but the fallback reporter only supports a max of two.
                        // Well, just going to have to drop the secondary action.

                        ShowFallbackProblemDialog(severity, exception, messageText, null, false);
                        return;
                    }

                    object props = new { level = ProblemLevel.kNotify, reportLabel = reportButtonLabel, secondaryLabel = secondaryButtonLabel, message = message };

                    // Precondition: we must be on the UI thread for Gecko to work.
                    using (var dlg = BrowserDialogFactory.CreateReactDialog("problemReportBundle", props))
                    {
                        dlg.FormBorderStyle = FormBorderStyle.FixedToolWindow; // Allows the window to be dragged around
                        dlg.ControlBox      = true;                            // Add controls like the X button back to the top bar
                        dlg.Text            = "";                              // Remove the title from the WinForms top bar

                        dlg.Width = 620;

                        // 360px was experimentally determined as what was needed for the longest known text for NotifyUserOfProblem
                        // (which is "Before saving, Bloom did an integrity check of your book [...]" from BookStorage.cs)
                        // You can make this height taller if need be.
                        // A scrollbar will appear if the height is not tall enough for the text
                        dlg.Height = 360;

                        // ShowDialog will cause this thread to be blocked (because it spins up a modal) until the dialog is closed.
                        BloomServer.RegisterThreadBlocking();

                        try
                        {
                            dlg.ShowDialog();

                            // Take action if the user clicked a button other than Close
                            if (dlg.CloseSource == "closedByAlternateButton" && OnSecondaryActionPressed != null)
                            {
                                OnSecondaryActionPressed(exception, message);
                            }
                            else if (dlg.CloseSource == "closedByReportButton")
                            {
                                if (OnReportButtonPressed != null)
                                {
                                    OnReportButtonPressed(exception, message);
                                }
                                else
                                {
                                    DefaultOnReportPressed(exception, message);
                                }
                            }

                            // Note: With the way LibPalaso's ErrorReport is designed,
                            // its intention is that after OnShowDetails is invoked and closed, you will not come back to the Notify Dialog
                            // This code has been implemented to follow that model
                            //
                            // But now that we have more options, it might be nice to come back to this dialog.
                            // If so, you'd need to add/update some code in this section.
                        }
                        finally
                        {
                            ResetToDefaults();
                            BloomServer.RegisterThreadUnblocked();
                        }
                    }
                }
                catch (Exception errorReporterException)
                {
                    Logger.WriteError("*** HtmlErrorReporter threw an exception trying to display", errorReporterException);
                    // At this point our problem reporter has failed for some reason, so we want the old WinForms handler
                    // to report both the original error for which we tried to open our dialog and this new one where
                    // the dialog itself failed.
                    // In order to do that, we create a new exception with the original exception (if there was one) as the
                    // inner exception. We include the message of the exception we just caught. Then we call the
                    // old WinForms fatal exception report directly.
                    // In any case, both of the errors will be logged by now.
                    var message = "Bloom's error reporting failed: " + errorReporterException.Message;

                    // Fallback to Winforms in case of trouble getting the browser to work
                    var fallbackReporter = new WinFormsErrorReporter();
                    // Food for thought: is it really fatal of the Notify Dialog had an exception? Maybe NonFatal makes more sense
                    fallbackReporter.ReportFatalException(new ApplicationException(message, exception ?? errorReporterException));
                }
            });
        }
示例#5
0
        public LibraryView(LibraryModel model, LibraryListView.Factory libraryListViewFactory,
                           LibraryBookView.Factory templateBookViewFactory,
                           SelectedTabChangedEvent selectedTabChangedEvent,
                           SendReceiveCommand sendReceiveCommand,
                           TeamCollectionManager tcManager)
        {
            _model = model;
            InitializeComponent();
            splitContainer1.BackColor = Palette.BookListSplitterColor;             // controls the left vs. right splitter
            _toolStrip.Renderer       = new NoBorderToolStripRenderer();
            _toolStripLeft.Renderer   = new NoBorderToolStripRenderer();

            _collectionListView      = libraryListViewFactory();
            _collectionListView.Dock = DockStyle.Fill;
            splitContainer1.Panel1.Controls.Add(_collectionListView);

            _bookView = templateBookViewFactory();
            _bookView.TeamCollectionMgr = tcManager;
            _bookView.Dock = DockStyle.Fill;
            splitContainer1.Panel2.Controls.Add(_bookView);

            // When going down to Shrink Stage 3 (see WorkspaceView), we want the right-side toolstrip to take precedence
            // (Settings, Other Collection).
            // This essentially makes the TC Status button's zIndex less than the buttons on the right side.
            _toolStripLeft.SendToBack();

            splitContainer1.SplitterDistance = _collectionListView.PreferredWidth;
            _makeBloomPackButton.Visible     = model.IsShellProject;
            _sendReceiveButton.Visible       = Settings.Default.ShowSendReceive;

            if (sendReceiveCommand != null)
            {
#if Chorus
                _sendReceiveButton.Click  += (x, y) => sendReceiveCommand.Raise(this);
                _sendReceiveButton.Enabled = !SendReceiver.SendReceiveDisabled;
#endif
            }
            else
            {
                _sendReceiveButton.Enabled = false;
            }

            if (SIL.PlatformUtilities.Platform.IsMono)
            {
                BackgroundColorsForLinux();
            }

            selectedTabChangedEvent.Subscribe(c =>
            {
                if (c.To == this)
                {
                    Logger.WriteEvent("Entered Collections Tab");
                }
            });
            SetTeamCollectionStatus(tcManager);
            TeamCollectionManager.TeamCollectionStatusChanged += (sender, args) =>
            {
                if (IsHandleCreated && !IsDisposed)
                {
                    SafeInvoke.InvokeIfPossible("update TC status", this, false,
                                                () => SetTeamCollectionStatus(tcManager));
                }
            };
            _tcStatusButton.Click += (sender, args) =>
            {
                // Any messages for which reloading the collection is a useful action?
                var showReloadButton = tcManager.MessageLog.ShouldShowReloadButton;
                // Reinstate this to see messages from before we started up.
                // We think it might be too expensive to show a list as long as this might get.
                // Instead, in the short term we may add a button to show the file.
                // Later we may implement some efficient way to scroll through them.
                // tcManager.CurrentCollection?.MessageLog?.LoadSavedMessages();
                using (var dlg = new ReactDialog("teamCollectionDialogBundle", new
                {
                    showReloadButton
                }, "Team Collection"))
                {
                    dlg.ShowDialog(this);
                    tcManager.CurrentCollectionEvenIfDisconnected?.MessageLog.WriteMilestone(MessageAndMilestoneType
                                                                                             .LogDisplayed);
                }
            };
        }
        /// <summary>
        /// Shows a problem dialog.
        /// </summary>
        /// <param name="controlForScreenshotting"></param>
        /// <param name="exception"></param>
        /// <param name="detailedMessage"></param>
        /// <param name="levelOfProblem"></param>
        public static void ShowProblemDialog(Control controlForScreenshotting, Exception exception,
                                             string detailedMessage = "", string levelOfProblem = "user")
        {
            // Before we do anything that might be "risky", put the problem in the log.
            LogProblem(exception, detailedMessage, levelOfProblem);
            if (_showingProblemReport)
            {
                // If a problem is reported when already reporting a problem, that could
                // be an unbounded recursion that freezes the program and prevents the original
                // problem from being reported.  So minimally report the recursive problem and stop
                // the recursion in its tracks.
                //
                // Alternatively, can happen if multiple async BloomAPI calls go out and return errors.
                // It's probably not helpful to have multiple problem report dialogs at the same time
                // in this case either (even if there are theoretically a finite (not infinite) number of them)
                const string msg = "MULTIPLE CALLS to ShowProblemDialog. Suppressing the subsequent calls";
                Console.Write(msg);
                Logger.WriteEvent(msg);
                return;                 // Abort
            }

            _showingProblemReport = true;
            _currentException     = exception;
            _detailedMessage      = detailedMessage;
            if (controlForScreenshotting == null)
            {
                controlForScreenshotting = Form.ActiveForm;
            }
            if (controlForScreenshotting == null)             // still possible if we come from a "Details" button
            {
                controlForScreenshotting = FatalExceptionHandler.ControlOnUIThread;
            }
            ResetScreenshotFile();
            // Originally, we used SafeInvoke for both the screenshot and the new dialog display. SafeInvoke was great
            // for trying to get a screenshot, but having the actual dialog inside
            // of it was causing problems for handling any errors in showing the dialog.
            // Now we use SafeInvoke only inside of this extracted method.
            TryGetScreenshot(controlForScreenshotting);

            SafeInvoke.InvokeIfPossible("Show Problem Dialog", controlForScreenshotting, false, () =>
            {
                // Uses a browser dialog to show the problem report
                try
                {
                    var query = "?" + levelOfProblem;
                    var problemDialogRootPath = BloomFileLocator.GetBrowserFile(false, "problemDialog", "loader.html");
                    var url = problemDialogRootPath.ToLocalhost() + query;

                    // Precondition: we must be on the UI thread for Gecko to work.
                    using (var dlg = new BrowserDialog(url))
                    {
                        dlg.ShowDialog();
                    }
                }
                catch (Exception problemReportException)
                {
                    Logger.WriteError("*** ProblemReportApi threw an exception trying to display", problemReportException);
                    // At this point our problem reporter has failed for some reason, so we want the old WinForms handler
                    // to report both the original error for which we tried to open our dialog and this new one where
                    // the dialog itself failed.
                    // In order to do that, we create a new exception with the original exception (if there was one) as the
                    // inner exception. We include the message of the exception we just caught. Then we call the
                    // old WinForms fatal exception report directly.
                    // In any case, both of the errors will be logged by now.
                    var message = "Bloom's error reporting failed: " + problemReportException.Message;
                    ErrorReport.ReportFatalException(new ApplicationException(message, _currentException ?? problemReportException));
                }
                finally
                {
                    _showingProblemReport = false;
                }
            });
        }
示例#7
0
        // ENHANCE: Reduce duplication in HtmlErrorReporter and ProblemReportApi code. Some of the ProblemReportApi code can move to HtmlErrorReporter code.

        // ENHANCE: I think levelOfProblem would benefit from being required and being an enum.

        /// <summary>
        /// Shows a problem dialog.
        /// </summary>
        /// <param name="controlForScreenshotting"></param>
        /// <param name="exception"></param>
        /// <param name="detailedMessage"></param>
        /// <param name="levelOfProblem">"user", "nonfatal", or "fatal"</param>
        /// <param name="additionalPathsToInclude"></param>
        public static void ShowProblemDialog(Control controlForScreenshotting, Exception exception,
                                             string detailedMessage            = "", string levelOfProblem = "user", string shortUserLevelMessage = "", bool isShortMessagePreEncoded = false,
                                             string[] additionalPathsToInclude = null)
        {
            // Before we do anything that might be "risky", put the problem in the log.
            LogProblem(exception, detailedMessage, levelOfProblem);
            if (Program.RunningHarvesterMode)
            {
                Console.WriteLine(levelOfProblem + " Problem Detected: " + shortUserLevelMessage + "  " + detailedMessage + "  " + exception);
                return;
            }
            StartupScreenManager.CloseSplashScreen();             // if it's still up, it'll be on top of the dialog

            lock (_showingProblemReportLock)
            {
                if (_showingProblemReport)
                {
                    // If a problem is reported when already reporting a problem, that could
                    // be an unbounded recursion that freezes the program and prevents the original
                    // problem from being reported.  So minimally report the recursive problem and stop
                    // the recursion in its tracks.
                    //
                    // Alternatively, can happen if multiple async BloomAPI calls go out and return errors.
                    // It's probably not helpful to have multiple problem report dialogs at the same time
                    // in this case either (even if there are theoretically a finite (not infinite) number of them)
                    const string msg = "MULTIPLE CALLS to ShowProblemDialog. Suppressing the subsequent calls";
                    Console.Write(msg);
                    Logger.WriteEvent(msg);
                    return;                     // Abort
                }

                _showingProblemReport = true;
            }

            // We have a better UI for this problem
            // Note that this will trigger whether it's a plain 'ol System.IO.PathTooLongException, or our own enhanced subclass, Bloom.Utiles.PathTooLongException
            if (exception is System.IO.PathTooLongException)
            {
                Utils.LongPathAware.ReportLongPath((System.IO.PathTooLongException)exception);
                return;
            }

            GatherReportInfoExceptScreenshot(exception, detailedMessage, shortUserLevelMessage, isShortMessagePreEncoded);

            if (controlForScreenshotting == null)
            {
                controlForScreenshotting = Form.ActiveForm;
            }
            if (controlForScreenshotting == null)             // still possible if we come from a "Details" button
            {
                controlForScreenshotting = FatalExceptionHandler.ControlOnUIThread;
            }
            ResetScreenshotFile();
            // Originally, we used SafeInvoke for both the screenshot and the new dialog display. SafeInvoke was great
            // for trying to get a screenshot, but having the actual dialog inside
            // of it was causing problems for handling any errors in showing the dialog.
            // Now we use SafeInvoke only inside of this extracted method.
            TryGetScreenshot(controlForScreenshotting);

            if (BloomServer._theOneInstance == null)
            {
                // We got an error really early, before we can use HTML dialogs. Report using the old dialog.
                // Hopefully we're still on the one main thread.
                HtmlErrorReporter.ShowFallbackProblemDialog(levelOfProblem, exception, detailedMessage, shortUserLevelMessage, isShortMessagePreEncoded);
                return;
            }

            SafeInvoke.InvokeIfPossible("Show Problem Dialog", controlForScreenshotting, false, () =>
            {
                // Uses a browser ReactDialog (if possible) to show the problem report
                try
                {
                    // We call CloseSplashScreen() above too, where it might help in some cases, but
                    // this one, while apparently redundant might be wise to keep since closing the splash screen
                    // needs to be done on the UI thread.
                    StartupScreenManager.CloseSplashScreen();

                    if (!BloomServer.ServerIsListening)
                    {
                        // We can't use the react dialog!
                        HtmlErrorReporter.ShowFallbackProblemDialog(levelOfProblem, exception, detailedMessage, shortUserLevelMessage, isShortMessagePreEncoded);
                        return;
                    }

                    // Precondition: we must be on the UI thread for Gecko to work.
                    using (var dlg = new ReactDialog("problemReportBundle", new { level = levelOfProblem }, "Problem Report"))
                    {
                        _additionalPathsToInclude = additionalPathsToInclude;
                        dlg.FormBorderStyle       = FormBorderStyle.FixedToolWindow; // Allows the window to be dragged around
                        dlg.ControlBox            = true;                            // Add controls like the X button back to the top bar
                        dlg.Text = "";                                               // Remove the title from the WinForms top bar

                        dlg.Width  = 731;
                        dlg.Height = 616;

                        // ShowDialog will cause this thread to be blocked (because it spins up a modal) until the dialog is closed.
                        BloomServer._theOneInstance.RegisterThreadBlocking();
                        try
                        {
                            // Keep dialog on top of program window if possible.  See https://issues.bloomlibrary.org/youtrack/issue/BL-10292.
                            if (controlForScreenshotting is Bloom.Shell)
                            {
                                dlg.ShowDialog(controlForScreenshotting);
                            }
                            else
                            {
                                dlg.ShowDialog();
                            }
                        }
                        finally
                        {
                            BloomServer._theOneInstance.RegisterThreadUnblocked();
                            _additionalPathsToInclude = null;
                        }
                    }
                }
                catch (Exception problemReportException)
                {
                    Logger.WriteError("*** ProblemReportApi threw an exception trying to display", problemReportException);
                    // At this point our problem reporter has failed for some reason, so we want the old WinForms handler
                    // to report both the original error for which we tried to open our dialog and this new one where
                    // the dialog itself failed.
                    // In order to do that, we create a new exception with the original exception (if there was one) as the
                    // inner exception. We include the message of the exception we just caught. Then we call the
                    // old WinForms fatal exception report directly.
                    // In any case, both of the errors will be logged by now.
                    var message = "Bloom's error reporting failed: " + problemReportException.Message;

                    // Fallback to Winforms in case of trouble getting the browser up
                    var fallbackReporter = new WinFormsErrorReporter();
                    // ENHANCE?: If reporting a non-fatal problem failed, why is the program required to abort? It might be able to handle other tasks successfully
                    fallbackReporter.ReportFatalException(new ApplicationException(message, exception ?? problemReportException));
                }
                finally
                {
                    lock (_showingProblemReportLock)
                    {
                        _showingProblemReport = false;
                    }
                }
            });
        }
示例#8
0
        public delegate CollectionTabView Factory();      //autofac uses this

        public CollectionTabView(CollectionModel model,
                                 SelectedTabChangedEvent selectedTabChangedEvent,
                                 TeamCollectionManager tcManager, BookSelection bookSelection,
                                 WorkspaceTabSelection tabSelection, BloomWebSocketServer webSocketServer, LocalizationChangedEvent localizationChangedEvent)
        {
            _model           = model;
            _tabSelection    = tabSelection;
            _bookSelection   = bookSelection;
            _webSocketServer = webSocketServer;
            _tcManager       = tcManager;

            BookCollection.CollectionCreated += OnBookCollectionCreated;

            InitializeComponent();
            _reactControl.SetLocalizationChangedEvent(localizationChangedEvent);             // after InitializeComponent, which creates it.
            BackColor               = _reactControl.BackColor = Palette.GeneralBackground;
            _toolStrip.Renderer     = new NoBorderToolStripRenderer();
            _toolStripLeft.Renderer = new NoBorderToolStripRenderer();

            // When going down to Shrink Stage 3 (see WorkspaceView), we want the right-side toolstrip to take precedence
            // (Settings, Other Collection).
            // This essentially makes the TC Status button's zIndex less than the buttons on the right side.
            _toolStripLeft.SendToBack();

            //TODO splitContainer1.SplitterDistance = _collectionListView.PreferredWidth;

            if (SIL.PlatformUtilities.Platform.IsMono)
            {
                BackgroundColorsForLinux();
            }

            selectedTabChangedEvent.Subscribe(c =>
            {
                if (c.To == this)
                {
                    Logger.WriteEvent("Entered Collections Tab");
                    if (_bookChangesPending && _bookSelection.CurrentSelection != null)
                    {
                        UpdateForBookChanges(_bookSelection.CurrentSelection);
                    }
                }
            });
            SetTeamCollectionStatus(tcManager);
            TeamCollectionManager.TeamCollectionStatusChanged += (sender, args) =>
            {
                if (IsHandleCreated && !IsDisposed)
                {
                    SafeInvoke.InvokeIfPossible("update TC status", this, false,
                                                () => SetTeamCollectionStatus(tcManager));
                }
            };
            _tcStatusButton.Click += (sender, args) =>
            {
                // Reinstate this to see messages from before we started up.
                // We think it might be too expensive to show a list as long as this might get.
                // Instead, in the short term we may add a button to show the file.
                // Later we may implement some efficient way to scroll through them.
                // tcManager.CurrentCollection?.MessageLog?.LoadSavedMessages();

                dynamic messageBundle = new DynamicJson();
                messageBundle.showReloadButton = tcManager.MessageLog.ShouldShowReloadButton;
                _webSocketServer.LaunchDialog("TeamCollectionDialog", messageBundle);
                tcManager.CurrentCollectionEvenIfDisconnected?.MessageLog.WriteMilestone(MessageAndMilestoneType.LogDisplayed);
            };

            // We don't want this control initializing until team collections sync (if any) is done.
            // That could change, but for now we're not trying to handle async changes arriving from
            // the TC to the local collection, and as part of that, the collection tab doesn't expect
            // the local collection to change because of TC stuff once it starts loading.
            Controls.Remove(_reactControl);
            bookSelection.SelectionChanged += (sender, e) => BookSelectionChanged(bookSelection.CurrentSelection);
        }
        public ReactCollectionTabView(LibraryModel model, LibraryListView.Factory libraryListViewFactory,
                                      LibraryBookView.Factory templateBookViewFactory,
                                      SelectedTabChangedEvent selectedTabChangedEvent,
                                      SendReceiveCommand sendReceiveCommand,
                                      TeamCollectionManager tcManager)
        {
            _model = model;
            InitializeComponent();
            BackColor               = _reactControl.BackColor = Palette.GeneralBackground;
            _toolStrip.Renderer     = new NoBorderToolStripRenderer();
            _toolStripLeft.Renderer = new NoBorderToolStripRenderer();

            // When going down to Shrink Stage 3 (see WorkspaceView), we want the right-side toolstrip to take precedence
            // (Settings, Other Collection).
            // This essentially makes the TC Status button's zIndex less than the buttons on the right side.
            _toolStripLeft.SendToBack();

            //TODO splitContainer1.SplitterDistance = _collectionListView.PreferredWidth;
            _makeBloomPackButton.Visible = model.IsShellProject;
            _sendReceiveButton.Visible   = Settings.Default.ShowSendReceive;

            if (sendReceiveCommand != null)
            {
#if Chorus
                _sendReceiveButton.Click  += (x, y) => sendReceiveCommand.Raise(this);
                _sendReceiveButton.Enabled = !SendReceiver.SendReceiveDisabled;
#endif
            }
            else
            {
                _sendReceiveButton.Enabled = false;
            }

            if (SIL.PlatformUtilities.Platform.IsMono)
            {
                BackgroundColorsForLinux();
            }

            selectedTabChangedEvent.Subscribe(c =>
            {
                if (c.To == this)
                {
                    Logger.WriteEvent("Entered Collections Tab");
                }
            });
            SetTeamCollectionStatus(tcManager);
            TeamCollectionManager.TeamCollectionStatusChanged += (sender, args) =>
            {
                return;

                if (!IsDisposed)
                {
                    SafeInvoke.InvokeIfPossible("update TC status", this, false,
                                                () => SetTeamCollectionStatus(tcManager));
                }
            };
            _tcStatusButton.Click += (sender, args) =>
            {
                // Any messages for which reloading the collection is a useful action?
                var showReloadButton = tcManager.MessageLog.ShouldShowReloadButton;
                // Reinstate this to see messages from before we started up.
                // We think it might be too expensive to show a list as long as this might get.
                // Instead, in the short term we may add a button to show the file.
                // Later we may implement some efficient way to scroll through them.
                // tcManager.CurrentCollection?.MessageLog?.LoadSavedMessages();
                using (var dlg = new ReactDialog("teamCollectionDialog", new { showReloadButton }))
                {
                    dlg.ShowDialog(this);
                    tcManager.CurrentCollectionEvenIfDisconnected?.MessageLog.WriteMilestone(MessageAndMilestoneType.LogDisplayed);
                }
            };

            // We don't want this control initializing until team collections sync (if any) is done.
            // That could change, but for now we're not trying to handle async changes arriving from
            // the TC to the local collection, and as part of that, the collection tab doesn't expect
            // the local collection to change because of TC stuff once it starts loading.
            Controls.Remove(_reactControl);
        }