From f0398f2331c72feda0613c031c8651a97a67a596 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 15:52:21 -0700 Subject: [PATCH] Ensure modal dialogs are presented in a coherent order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modal dialogs can be presented while other modal dialogs are already being displayed. Previously, dialogs were always displayed in the order they were requested. But say you have two untitled buffers in a pane and you close all items… You'll display prompt dialogs for both buffers asking the user if they want to save. If the user answers yes to the first dialog, they should see the path selection dialog before they see the save prompt for the second buffer. This commit uses a stack of queues to store deferred dialogs and allow dialogs presented by the dismissal of another dialog to take precedence over other pending dialogs. --- spec/app/atom-spec.coffee | 69 +++++++++++++++++++++++++++++++++++++++ spec/spec-helper.coffee | 2 ++ src/app/atom.coffee | 50 ++++++++++++++++++++++++---- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 261c86d4f..675443a19 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -141,3 +141,72 @@ describe "the `atom` global", -> runs -> expect(versionHandler.argsForCall[0][0]).toMatch /^\d+\.\d+(\.\d+)?$/ + + describe "modal native dialogs", -> + beforeEach -> + spyOn(atom, 'sendMessageToBrowserProcess') + atom.sendMessageToBrowserProcess.simulateConfirmation = (buttonText) -> + labels = @argsForCall[0][1][2...] + callbacks = @argsForCall[0][2] + @reset() + callbacks[labels.indexOf(buttonText)]() + + atom.sendMessageToBrowserProcess.simulatePathSelection = (path) -> + callback = @argsForCall[0][2] + @reset() + callback(path) + + it "only presents one native dialog at a time", -> + confirmHandler = jasmine.createSpy("confirmHandler") + selectPathHandler = jasmine.createSpy("selectPathHandler") + + atom.confirm "Are you happy?", "really, truly happy?", "Yes", confirmHandler, "No" + atom.confirm "Are you happy?", "really, truly happy?", "Yes", confirmHandler, "No" + atom.showSaveDialog(selectPathHandler) + atom.showSaveDialog(selectPathHandler) + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + atom.sendMessageToBrowserProcess.simulateConfirmation("Yes") + expect(confirmHandler).toHaveBeenCalled() + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + atom.sendMessageToBrowserProcess.simulateConfirmation("No") + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + atom.sendMessageToBrowserProcess.simulatePathSelection('/selected/path') + expect(selectPathHandler).toHaveBeenCalledWith('/selected/path') + selectPathHandler.reset() + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + + it "prioritizes dialogs presented as the result of dismissing other dialogs before any previously deferred dialogs", -> + atom.confirm "A1", "", "Next", -> + atom.confirm "B1", "", "Next", -> + atom.confirm "C1", "", "Next", -> + atom.confirm "C2", "", "Next", -> + atom.confirm "B2", "", "Next", -> + atom.confirm "A2", "", "Next", -> + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A1" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "B1" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "C1" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "C2" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "B2" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A2" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index fc93ecc61..bbe236034 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -83,6 +83,8 @@ afterEach -> window.git = null $('#jasmine-content').empty() ensureNoPathSubscriptions() + atom.pendingModals = [[]] + atom.presentingModal = false waits(0) # yield to ui thread to make screen update more frequently window.loadPackage = (name, options) -> diff --git a/src/app/atom.coffee b/src/app/atom.coffee index 6939a071f..597fb60cb 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -16,6 +16,8 @@ _.extend atom, loadedPackages: [] activatedAtomPackages: [] atomPackageStates: {} + presentingModal: false + pendingModals: [[]] getPathToOpen: -> @getWindowState('pathToOpen') ? window.location.params.pathToOpen @@ -102,15 +104,49 @@ _.extend atom, @sendMessageToBrowserProcess('newWindow', args) confirm: (message, detailedMessage, buttonLabelsAndCallbacks...) -> - args = [message, detailedMessage] - callbacks = [] - while buttonLabelsAndCallbacks.length - args.push(buttonLabelsAndCallbacks.shift()) - callbacks.push(buttonLabelsAndCallbacks.shift()) - @sendMessageToBrowserProcess('confirm', args, callbacks) + wrapCallback = (callback) => => @dismissModal(callback) + @presentModal => + args = [message, detailedMessage] + callbacks = [] + while buttonLabelsAndCallbacks.length + do => + buttonLabel = buttonLabelsAndCallbacks.shift() + buttonCallback = buttonLabelsAndCallbacks.shift() + args.push(buttonLabel) + callbacks.push(=> @dismissModal(buttonCallback)) + @sendMessageToBrowserProcess('confirm', args, callbacks) showSaveDialog: (callback) -> - @sendMessageToBrowserProcess('showSaveDialog', [], callback) + @presentModal => + @sendMessageToBrowserProcess('showSaveDialog', [], (path) => @dismissModal(callback, path)) + + presentModal: (fn) -> + if @presentingModal + @pushPendingModal(fn) + else + @presentingModal = true + fn() + + dismissModal: (fn, args...) -> + @pendingModals.push([]) # prioritize any modals presented during dismiss callback + fn?(args...) + @presentingModal = false + @presentModal(fn) if fn = @shiftPendingModal() + + pushPendingModal: (fn) -> + # pendingModals is a stack of queues. enqueue to top of stack. + stackSize = @pendingModals.length + @pendingModals[stackSize - 1].push(fn) + + shiftPendingModal: -> + # pop pendingModals stack if its top queue is empty, otherwise shift off the topmost queue + stackSize = @pendingModals.length + currentQueueSize = @pendingModals[stackSize - 1].length + if stackSize > 1 and currentQueueSize == 0 + @pendingModals.pop() + @shiftPendingModal() + else + @pendingModals[stackSize - 1].shift() toggleDevTools: -> @sendMessageToBrowserProcess('toggleDevTools')