public void ReloadCollections() { lock (_bookCollectionLock) { _bookCollections = null; GetBookCollections(); } _webSocketServer.SendEvent("editableCollectionList", "reload:" + _bookCollections[0].PathToDirectory); }
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); // To correct a weird SplitPane behavior in CollectionsTabPane, we need // a notification when our window changes state from minimized to something else. bool minimized = ParentForm?.WindowState == FormWindowState.Minimized; if (!minimized && _minimized) { _webSocketServer.SendEvent("window", "restored"); } _minimized = minimized; }
/// <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(); })); }
private void ShowBook(bool updatePreview = true) { if (_bookSelection.CurrentSelection == null || !_visible) { HidePreview(); } else { Debug.WriteLine("LibraryBookView.ShowBook() currentselection ok"); _readmeBrowser.Visible = false; _splitContainerForPreviewAndAboutBrowsers.Visible = true; if (updatePreview && !TroubleShooterDialog.SuppressBookPreview) { var previewDom = _bookSelection.CurrentSelection.GetPreviewHtmlFileForWholeBook(); XmlHtmlConverter.MakeXmlishTagsSafeForInterpretationAsHtml(previewDom.RawDom); var fakeTempFile = BloomServer.MakeSimulatedPageFileInBookFolder(previewDom, setAsCurrentPageForDebugging: false, source: BloomServer.SimulatedPageFileSource.Preview); _reactBookPreviewControl.Props = new { initialBookPreviewUrl = fakeTempFile.Key }; // need this for initial selection _webSocketServer.SendString("bookStatus", "changeBook", fakeTempFile.Key); // need this for changing selection display _webSocketServer.SendEvent("bookStatus", "reload"); // need this for changing selection's book info display if team collection _reactBookPreviewControl.Visible = true; RecordAndCleanupFakeFiles(fakeTempFile); } _splitContainerForPreviewAndAboutBrowsers.Panel2Collapsed = true; if (_bookSelection.CurrentSelection.HasAboutBookInformationToShow) { if (RobustFile.Exists(_bookSelection.CurrentSelection.AboutBookHtmlPath)) { _splitContainerForPreviewAndAboutBrowsers.Panel2Collapsed = false; _readmeBrowser.Navigate(_bookSelection.CurrentSelection.AboutBookHtmlPath, false); _readmeBrowser.Visible = true; } else if (RobustFile.Exists(_bookSelection.CurrentSelection.AboutBookMdPath)) { _splitContainerForPreviewAndAboutBrowsers.Panel2Collapsed = false; var md = new Markdown(); var contents = RobustFile.ReadAllText(_bookSelection.CurrentSelection.AboutBookMdPath); _readmeBrowser.NavigateRawHtml(string.Format("<html><head><meta charset=\"utf-8\"/></head><body>{0}</body></html>", md.Transform(contents))); _readmeBrowser.Visible = true; } } _reshowPending = false; } }
public AccessibilityCheckApi(BloomWebSocketServer webSocketServer, BookSelection bookSelection, BookRenamedEvent bookRenamedEvent, BookSavedEvent bookSavedEvent, EpubMaker.Factory epubMakerFactory, PublishEpubApi epubApi) { _webSocketServer = webSocketServer; var progress = new WebSocketProgress(_webSocketServer, kWebSocketContext); _webSocketProgress = progress.WithL10NPrefix("AccessibilityCheck."); _epubApi = epubApi; bookSelection.SelectionChanged += (unused1, unused2) => { _webSocketServer.SendEvent(kWebSocketContext, kBookSelectionChanged); }; // we get this when the book is renamed bookRenamedEvent.Subscribe((book) => { RefreshClient(); }); // we get this when the contents of the page might have changed bookSavedEvent.Subscribe((book) => { RefreshClient(); }); }
public void RegisterWithApiHandler(BloomApiHandler apiHandler) { // This is just for storing the user preference of method // If we had a couple of these, we could just have a generic preferences api // that browser-side code could use. apiHandler.RegisterEndpointLegacy(kApiUrlPart + "method", request => { if (request.HttpMethod == HttpMethods.Get) { var method = Settings.Default.PublishAndroidMethod; if (!new string[] { "wifi", "usb", "file" }.Contains(method)) { method = "wifi"; } request.ReplyWithText(method); } else // post { Settings.Default.PublishAndroidMethod = request.RequiredPostString(); #if __MonoCS__ if (Settings.Default.PublishAndroidMethod == "usb") { _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet."); } #endif request.PostSucceeded(); } }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "backColor", request => { if (request.HttpMethod == HttpMethods.Get) { if (request.CurrentBook != _coverColorSourceBook) { _coverColorSourceBook = request.CurrentBook; ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor); } request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor)); } else // post { // ignore invalid colors (very common while user is editing hex) Color newColor; var newColorAsString = request.RequiredPostString(); if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor)) { _thumbnailBackgroundColor = newColor; request.CurrentBook.SetCoverColor(newColorAsString); } request.PostSucceeded(); } }, true); apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode", readRequest => { // If the user has taken off all possible motion, force not having motion in the // Bloom Reader book. See https://issues.bloomlibrary.org/youtrack/issue/BL-7680. if (!readRequest.CurrentBook.HasMotionPages) { readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = false; } return(readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion); }, (writeRequest, value) => { writeRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = value; writeRequest.CurrentBook.BookInfo.SavePublishSettings(); _webSocketServer.SendEvent("publish", "motionChanged"); } , true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request => { MakeBloompubPreview(request, false); }, false); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "thumbnail", request => { var coverImage = request.CurrentBook.GetCoverImagePath(); if (coverImage == null) { request.Failed("no cover image"); } else { // We don't care as much about making it resized as making its background transparent. using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile()) { if (_thumbnailBackgroundColor == Color.Transparent) { ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor); } RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor); request.ReplyWithImage(thumbnail.Path); } } }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request => { #if !__MonoCS__ SetState("UsbStarted"); UpdatePreviewIfNeeded(request); _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings()); #endif request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request => { #if !__MonoCS__ _usbPublisher.Stop(disposing: false); SetState("stopped"); #endif request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/start", request => { SetState("ServingOnWifi"); UpdatePreviewIfNeeded(request); _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings()); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/stop", request => { _wifiPublisher.Stop(); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/save", request => { UpdatePreviewIfNeeded(request); FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings()); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubsParams", request => { request.ReplyWithJson(JsonConvert.SerializeObject(_collectionSettings.BulkPublishBloomPubSettings)); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubs", request => { // update what's in the collection so that we remember for next time _collectionSettings.BulkPublishBloomPubSettings = request.RequiredPostObject <BulkBloomPubPublishSettings>(); _collectionSettings.Save(); _bulkBloomPubCreator.PublishAllBooks(_collectionSettings.BulkPublishBloomPubSettings); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "textToClipboard", request => { PortableClipboard.SetText(request.RequiredPostString()); request.PostSucceeded(); }, true); apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode", request => { return(request.CurrentBook.HasMotionPages); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate", request => { return(request.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion && request.CurrentBook.HasMotionPages); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape", request => { return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterEndpointLegacy(kApiUrlPart + "languagesInBook", request => { try { InitializeLanguagesInBook(request); Dictionary <string, InclusionSetting> textLangsToPublish = request.CurrentBook.BookInfo.PublishSettings.BloomPub.TextLangs; Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.PublishSettings.BloomPub.AudioLangs; var result = "[" + string.Join(",", _allLanguages.Select(kvp => { string langCode = kvp.Key; bool includeText = false; if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting)) { includeText = includeTextSetting.IsIncluded(); } bool includeAudio = false; if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting)) { includeAudio = includeAudioSetting.IsIncluded(); } var value = new LanguagePublishInfo() { code = kvp.Key, name = request.CurrentBook.PrettyPrintLanguage(langCode), complete = kvp.Value, includeText = includeText, containsAnyAudio = _languagesWithAudio.Contains(langCode), includeAudio = includeAudio }; var json = JsonConvert.SerializeObject(value); return(json); })) + "]"; request.ReplyWithText(result); }
public bool UpdatePreview(EpubPublishUiSettings newSettings, bool force, WebSocketProgress progress = null) { _progress = progress ?? _standardProgress.WithL10NPrefix("PublishTab.Epub."); if (Program.RunningOnUiThread) { // There's some stuff inside this lock that has to run on the UI thread. // If we lock the UI thread here, we can deadlock the whole program. throw new ApplicationException(@"Must not attempt to make epubs on UI thread...will produce deadlocks"); } lock (_epubMakerLock) { if (EpubMaker != null) { EpubMaker.AbortRequested = false; } _stagingEpub = true; } // For some unknown reason, if the accessibility window is showing, some of the browser navigation // that is needed to accurately determine which content is visible simply doesn't happen. // It would be disconcerting if it popped to the top after we close it and reopen it. // So, we just close the window if it is showing when we do this. See BL-7807. // Except that opening the Ace Checker tab invokes this code path in a way that works without the // deadlock (or whatever causes the failure). This call can be detected by the progress argument not // being null. The Refresh button on the AccessibilityCheckWindow also uses this code path in the // same way, so the next two lines also allow that Refresh button to work. See BL-9341 for why // the original fix is inadequate. if (progress == null) { AccessibilityChecker.AccessibilityCheckWindow.StaticClose(); } try { _webSocketServer.SendString(kWebsocketContext, "startingEbookCreation", _previewSrc); var htmlPath = _bookSelection.CurrentSelection.GetPathHtmlFile(); var newVersion = Book.Book.ComputeHashForAllBookRelatedFiles(htmlPath); bool previewIsAlreadyCurrent; lock (_epubMakerLock) { previewIsAlreadyCurrent = _desiredEpubSettings == newSettings && EpubMaker != null && newVersion == _bookVersion && !EpubMaker.AbortRequested && !force; } if (previewIsAlreadyCurrent) { SaveAsEpub(); // just in case there's a race condition where we haven't already saved it. return(true); // preview is already up to date. } _desiredEpubSettings = newSettings; // clear the obsolete preview, if any; this also ensures that when the new one gets done, // we will really be changing the src attr in the preview iframe so the display will update. _webSocketServer.SendEvent(kWebsocketContext, kWebsocketEventId_epubReady); _bookVersion = newVersion; ReportProgress(LocalizationManager.GetString("PublishTab.Epub.PreparingPreview", "Preparing Preview")); // This three-tries loop is an attempt to recover from a weird state the system sometimes gets into // where a browser won't navigate to a temporary page that the EpubMaker uses. I'm not sure it actually // helps, once the system gets into this state even a brand new browser seems to have the same problem. // Usually there will be no exception, and the loop breaks at the end of the first iteration. for (int i = 0; i < 3; i++) { try { if (!PublishHelper.InPublishTab) { return(false); } _previewSrc = UpdateEpubControlContent(); } catch (ApplicationException ex) { if (i >= 2) { throw; } ReportProgress("Something went wrong, trying again"); continue; } break; // normal case, no exception } lock (_epubMakerLock) { if (EpubMaker.AbortRequested) { return(false); // the code that set the abort flag will request a new preview. } } } finally { lock (_epubMakerLock) { _stagingEpub = false; } } // Do pending save if the user requested it while the preview was still in progress. SaveAsEpub(); ReportProgress(LocalizationManager.GetString("PublishTab.Epub.Done", "Done")); return(true); }
public void RegisterWithApiHandler(BloomApiHandler apiHandler) { apiHandler.RegisterEndpointHandler(kApiUrlPart + "bookName", request => { request.ReplyWithText(request.CurrentBook.TitleBestForUserDisplay); }, false); apiHandler.RegisterEndpointHandler(kApiUrlPart + "showAccessibilityChecker", request => { AccessibilityCheckWindow.StaticShow(() => _webSocketServer.SendEvent(kWebSocketContext, kWindowActivated)); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "descriptionsForAllImages", request => { var problems = AccessibilityCheckers.CheckDescriptionsForAllImages(request.CurrentBook); var resultClass = problems.Any() ? "failed" : "passed"; request.ReplyWithJson(new { resultClass = resultClass, problems = problems }); }, false); apiHandler.RegisterEndpointHandler(kApiUrlPart + "audioForAllImageDescriptions", request => { var problems = AccessibilityCheckers.CheckAudioForAllImageDescriptions(request.CurrentBook); var resultClass = problems.Any() ? "failed" : "passed"; request.ReplyWithJson(new { resultClass = resultClass, problems = problems }); }, false); apiHandler.RegisterEndpointHandler(kApiUrlPart + "audioForAllText", request => { var problems = AccessibilityCheckers.CheckAudioForAllText(request.CurrentBook); var resultClass = problems.Any() ? "failed" : "passed"; request.ReplyWithJson(new { resultClass = resultClass, problems = problems }); }, false); // Just a checkbox that the user ticks to say "yes, I checked this" // At this point, we don't have a way to clear that when the book changes. apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "noEssentialInfoByColor", request => request.CurrentBook.BookInfo.MetaData.A11y_NoEssentialInfoByColor, (request, b) => { request.CurrentBook.BookInfo.MetaData.A11y_NoEssentialInfoByColor = b; request.CurrentBook.Save(); }, false); // Just a checkbox that the user ticks to say "yes, I checked this" // At this point, we don't have a way to clear that when the book changes. apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "noTextIncludedInAnyImages", request => request.CurrentBook.BookInfo.MetaData.A11y_NoTextIncludedInAnyImages, (request, b) => { request.CurrentBook.BookInfo.MetaData.A11y_NoTextIncludedInAnyImages = b; request.CurrentBook.Save(); }, false); //enhance: this might have to become async to work on large books on slow computers apiHandler.RegisterEndpointHandler(kApiUrlPart + "aceByDaisyReportUrl", request => { MakeAceByDaisyReport(request); }, false, false ); // A checkbox that the user ticks in the Accessible Image tool to request a preview // of how things might look with cataracts. // For now this doesn't seem worth persisting, except for the session so it sticks from page to page. apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "cataracts", request => _simulateCataracts, (request, b) => { _simulateCataracts = b; }, false); // A checkbox that the user ticks in the Accessible Image tool to request a preview // of how things might look with color-blindness, and a set of radio buttons // for choosing different kinds of color-blindness. // For now these doesn't seem worth persisting, except for the session so it sticks from page to page. apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "colorBlindness", request => _simulateColorBlindness, (request, b) => { _simulateColorBlindness = b; }, false); apiHandler.RegisterEnumEndpointHandler(kApiUrlPart + "kindOfColorBlindness", request => _kindOfColorBlindness, (request, kind) => _kindOfColorBlindness = kind, false); }
public static void DoWorkWithProgressDialog(BloomWebSocketServer socketServer, string socketContext, Func <Form> makeDialog, Func <IWebSocketProgress, bool> doWhat, Action <Form> doWhenMainActionFalse = null) { var progress = new WebSocketProgress(socketServer, socketContext); // NOTE: This (specifically ShowDialog) blocks the main thread until the dialog is closed. // Be careful to avoid deadlocks. using (var dlg = makeDialog()) { // For now let's not try to handle letting the user abort. dlg.ControlBox = false; var worker = new BackgroundWorker(); worker.DoWork += (sender, args) => { // A way of waiting until the dialog is ready to receive progress messages while (!socketServer.IsSocketOpen(socketContext)) { Thread.Sleep(50); } bool waitForUserToCloseDialogOrReportProblems; try { waitForUserToCloseDialogOrReportProblems = doWhat(progress); } catch (Exception ex) { // depending on the nature of the problem, we might want to do more or less than this. // But at least this lets the dialog reach one of the states where it can be closed, // and gives the user some idea things are not right. socketServer.SendEvent(socketContext, "finished"); waitForUserToCloseDialogOrReportProblems = true; progress.MessageWithoutLocalizing("Something went wrong: " + ex.Message, ProgressKind.Error); } // stop the spinner socketServer.SendEvent(socketContext, "finished"); if (waitForUserToCloseDialogOrReportProblems) { // Now the user is allowed to close the dialog or report problems. // (ProgressDialog in JS-land is watching for this message, which causes it to turn // on the buttons that allow the dialog to be manually closed (or a problem to be reported). socketServer.SendBundle(socketContext, "show-buttons", new DynamicJson()); } else { // Just close the dialog dlg.Invoke((Action)(() => { if (doWhenMainActionFalse != null) { doWhenMainActionFalse(dlg); } else { dlg.Close(); } })); } }; worker.RunWorkerAsync(); dlg.ShowDialog(); // returns when dialog closed } }
public bool UpdatePreview(EpubPublishUiSettings newSettings, bool force, WebSocketProgress progress = null) { _progress = progress ?? _standardProgress.WithL10NPrefix("PublishTab.Epub."); if (Program.RunningOnUiThread) { // There's some stuff inside this lock that has to run on the UI thread. // If we lock the UI thread here, we can deadlock the whole program. throw new ApplicationException(@"Must not attempt to make epubs on UI thread...will produce deadlocks"); } lock (_epubMakerLock) { if (EpubMaker != null) { EpubMaker.AbortRequested = false; } _stagingEpub = true; } try { _webSocketServer.SendString(kWebsocketContext, "startingEbookCreation", _previewSrc); var htmlPath = _bookSelection.CurrentSelection.GetPathHtmlFile(); var newVersion = Book.Book.MakeVersionCode(File.ReadAllText(htmlPath), htmlPath); bool previewIsAlreadyCurrent; lock (_epubMakerLock) { previewIsAlreadyCurrent = _desiredEpubSettings == newSettings && EpubMaker != null && newVersion == _bookVersion && !EpubMaker.AbortRequested && !force; } if (previewIsAlreadyCurrent) { SaveAsEpub(); // just in case there's a race condition where we haven't already saved it. return(true); // preview is already up to date. } _desiredEpubSettings = newSettings; // clear the obsolete preview, if any; this also ensures that when the new one gets done, // we will really be changing the src attr in the preview iframe so the display will update. _webSocketServer.SendEvent(kWebsocketContext, kWebsocketEventId_epubReady); _bookVersion = newVersion; ReportProgress(LocalizationManager.GetString("PublishTab.Epub.PreparingPreview", "Preparing Preview")); // This three-tries loop is an attempt to recover from a weird state the system sometimes gets into // where a browser won't navigate to a temporary page that the EpubMaker uses. I'm not sure it actually // helps, once the system gets into this state even a brand new browser seems to have the same problem. // Usually there will be no exception, and the loop breaks at the end of the first iteration. for (int i = 0; i < 3; i++) { try { if (!PublishHelper.InPublishTab) { return(false); } _previewSrc = UpdateEpubControlContent(); } catch (ApplicationException ex) { if (i >= 2) { throw; } ReportProgress("Something went wrong, trying again"); continue; } break; // normal case, no exception } lock (_epubMakerLock) { if (EpubMaker.AbortRequested) { return(false); // the code that set the abort flag will request a new preview. } } } finally { lock (_epubMakerLock) { _stagingEpub = false; } } // Do pending save if the user requested it while the preview was still in progress. SaveAsEpub(); ReportProgress(LocalizationManager.GetString("PublishTab.Epub.Done", "Done")); return(true); }