def test_add_first(self):
        stack = ScreenStack()

        screen_data = ScreenData(None)
        stack.add_first(screen_data)
        self.assertEqual(stack.pop(False), screen_data)

        # Add new Screen data to the end
        new_screen_data = ScreenData(None)
        stack.add_first(new_screen_data)
        # First the old screen data should be there
        self.assertEqual(stack.pop(), screen_data)
        # Second should be the new screen data
        self.assertEqual(stack.pop(), new_screen_data)
    def test_size(self):
        stack = ScreenStack()
        self.assertEqual(stack.size(), 0)

        stack.append(ScreenData(None))
        self.assertEqual(stack.size(), 1)

        stack.append(ScreenData(None))
        self.assertEqual(stack.size(), 2)

        # Remove from stack
        stack.pop()
        self.assertEqual(stack.size(), 1)
        stack.pop()
        self.assertEqual(stack.size(), 0)

        # Add first when stack has items
        stack.append(ScreenData(None))
        stack.append(ScreenData(None))
        self.assertEqual(stack.size(), 2)
        stack.add_first(ScreenData(None))
        self.assertEqual(stack.size(), 3)
Example #3
0
class Scheduler_TestCase(unittest.TestCase):
    def setUp(self):
        self.stack = None
        self.scheduler = None

    def create_scheduler_with_stack(self):
        self.stack = ScreenStack()
        self.scheduler = ScreenScheduler(event_loop=mock.MagicMock(),
                                         scheduler_stack=self.stack)

    def pop_last_item(self, remove=True):
        return self.stack.pop(remove)

    def test_create_scheduler(self):
        scheduler = ScreenScheduler(MainLoop())
        self.assertTrue(type(scheduler._screen_stack) is ScreenStack)

    def test_scheduler_quit_screen(self):
        def test_callback():
            pass

        scheduler = ScreenScheduler(MainLoop())
        self.assertEqual(scheduler.quit_screen, None)
        scheduler.quit_screen = test_callback
        self.assertEqual(scheduler.quit_screen, test_callback)

    def test_nothing_to_render(self):
        self.create_scheduler_with_stack()

        self.assertTrue(self.scheduler.nothing_to_render)
        self.assertTrue(self.stack.empty())

        self.scheduler.schedule_screen(UIScreen())
        self.assertFalse(self.scheduler.nothing_to_render)
        self.assertFalse(self.stack.empty())

    def test_schedule_screen(self):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        self.scheduler.schedule_screen(screen)
        test_screen = self.pop_last_item(False)
        self.assertEqual(test_screen.ui_screen, screen)
        self.assertEqual(test_screen.args, None)  # empty field - no arguments
        self.assertFalse(test_screen.execute_new_loop)

        # Schedule another screen, new one will be added to the bottom of the stack
        new_screen = UIScreen()
        self.scheduler.schedule_screen(new_screen)
        # Here should still be the old screen
        self.assertEqual(self.pop_last_item().ui_screen, screen)
        # After removing the first we would find the second screen
        self.assertEqual(self.pop_last_item().ui_screen, new_screen)

    def test_replace_screen_with_empty_stack(self):
        self.create_scheduler_with_stack()

        with self.assertRaises(ScreenStackEmptyException):
            self.scheduler.replace_screen(UIScreen())

    def test_replace_screen(self):
        self.create_scheduler_with_stack()

        old_screen = UIScreen()
        screen = UIScreen()
        self.scheduler.schedule_screen(old_screen)
        self.scheduler.replace_screen(screen)
        self.assertEqual(self.pop_last_item(False).ui_screen, screen)

        new_screen = UIScreen()
        self.scheduler.replace_screen(new_screen)
        self.assertEqual(self.pop_last_item().ui_screen, new_screen)
        # The old_screen was replaced so the stack is empty now
        self.assertTrue(self.stack.empty())

    def test_replace_screen_with_args(self):
        self.create_scheduler_with_stack()

        old_screen = UIScreen()
        screen = UIScreen()
        self.scheduler.schedule_screen(old_screen)
        self.scheduler.replace_screen(screen, "test")
        test_screen = self.pop_last_item()
        self.assertEqual(test_screen.ui_screen, screen)
        self.assertEqual(test_screen.args, "test")
        # The old_screen was replaced so the stack is empty now
        self.assertTrue(self.stack.empty())

    def test_switch_screen_with_empty_stack(self):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        self.scheduler.push_screen(screen)
        self.assertEqual(self.pop_last_item().ui_screen, screen)

    def test_switch_screen(self):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        new_screen = UIScreen()

        self.scheduler.schedule_screen(screen)
        self.scheduler.push_screen(new_screen)

        test_screen = self.pop_last_item()
        self.assertEqual(test_screen.ui_screen, new_screen)
        self.assertEqual(test_screen.args, None)
        self.assertEqual(test_screen.execute_new_loop, False)

        # We popped the new_screen so the old screen should stay here
        self.assertEqual(self.pop_last_item().ui_screen, screen)
        self.assertTrue(self.stack.empty())

    def test_switch_screen_with_args(self):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        self.scheduler.push_screen(screen, args="test")
        self.assertEqual(self.pop_last_item(False).ui_screen, screen)
        self.assertEqual(self.pop_last_item().args, "test")

    @mock.patch(
        'simpleline.render.screen_scheduler.ScreenScheduler._draw_screen')
    def test_switch_screen_modal_empty_stack(self, _):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        self.scheduler.push_screen_modal(screen)
        self.assertEqual(self.pop_last_item().ui_screen, screen)

    @mock.patch(
        'simpleline.render.screen_scheduler.ScreenScheduler._draw_screen')
    def test_switch_screen_modal(self, _):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        new_screen = UIScreen()
        self.scheduler.schedule_screen(screen)
        self.scheduler.push_screen_modal(new_screen)

        test_screen = self.pop_last_item()
        self.assertEqual(test_screen.ui_screen, new_screen)
        self.assertEqual(test_screen.args, None)
        self.assertEqual(test_screen.execute_new_loop, True)

    @mock.patch(
        'simpleline.render.screen_scheduler.ScreenScheduler._draw_screen')
    def test_switch_screen_modal_with_args(self, _):
        self.create_scheduler_with_stack()

        screen = UIScreen()
        self.scheduler.push_screen_modal(screen, args="test")
        self.assertEqual(self.pop_last_item(False).ui_screen, screen)
    def test_pop(self):
        stack = ScreenStack()
        with self.assertRaises(ScreenStackEmptyException):
            stack.pop()

        with self.assertRaises(ScreenStackEmptyException):
            stack.pop(False)

        # stack.pop(True) will remove the item
        stack.append(ScreenData(None))
        stack.pop(True)
        with self.assertRaises(ScreenStackEmptyException):
            stack.pop()

        # stack.pop() should behave the same as stack.pop(True)
        stack.append(ScreenData(None))
        stack.pop()
        with self.assertRaises(ScreenStackEmptyException):
            stack.pop()

        stack.append(ScreenData(None))
        stack.pop(False)
        stack.pop(True)
class ScreenScheduler():
    def __init__(self, event_loop, scheduler_stack=None):
        """Constructor where you can pass your own scheduler stack.

        The ScreenStack will be used automatically if scheduler stack will be None.

        :param event_loop: Event loop used for the scheduler.
        :type event_loop: Class based on `simpleline.event_loop.AbstractEventLoop`.
        :param scheduler_stack: Use custom scheduler stack if you need to.
        :type scheduler_stack: `simpleline.screen_stack.ScreenStack` based class.
        """
        self._quit_screen = None
        self._event_loop = event_loop

        if scheduler_stack:
            self._screen_stack = scheduler_stack
        else:
            self._screen_stack = ScreenStack()
        self._register_handlers()

        self._first_screen_scheduled = False

    @staticmethod
    def _spacer():
        return "\n".join(2 * [App.get_configuration().width * "="])

    def _register_handlers(self):
        self._event_loop.register_signal_handler(RenderScreenSignal,
                                                 self._process_screen_callback)
        self._event_loop.register_signal_handler(CloseScreenSignal,
                                                 self._close_screen_callback)

    @property
    def quit_screen(self):
        """Return quit UIScreen."""
        return self._quit_screen

    @quit_screen.setter
    def quit_screen(self, quit_screen):
        """Set the UIScreen based instance which will be showed before the Application will quit.

        You can also use `simpleline.render.adv_widgets.YesNoDialog` or `UIScreen` based class
        with the `answer` property. Without the `answer` property the application will always
        close.
        """
        self._quit_screen = quit_screen

    @property
    def nothing_to_render(self):
        """Is something for rendering in the scheduler stack?

        :return: True if the rendering stack is empty
        :rtype: bool
        """
        return self._screen_stack.empty()

    def dump_stack(self):
        """Get string representation of actual screen stack."""
        return self._screen_stack.dump_stack()

    def schedule_screen(self, ui_screen, args=None):
        """Add screen to the bottom of the stack.

        This is mostly useful at the beginning to prepare the first screen hierarchy to display.

        :param ui_screen: screen to show
        :type ui_screen: UIScreen instance
        :param args: optional argument, please see switch_screen for details
        :type args: anything
        """
        log.debug("Scheduling screen %s", ui_screen)
        screen = ScreenData(ui_screen, args)
        self._screen_stack.add_first(screen)
        self._redraw_on_first_scheduled_screen()

    def _redraw_on_first_scheduled_screen(self):
        if not self._first_screen_scheduled:
            self.redraw()
            self._first_screen_scheduled = True

    def replace_screen(self, ui_screen, args=None):
        """Schedules a screen to replace the current one.

        :param ui_screen: screen to show
        :type ui_screen: instance of UIScreen
        :param args: optional argument to pass to ui's refresh and setup methods
                     (can be used to select what item should be displayed or so)
        :type args: anything
        """
        log.debug("Replacing screen %s", ui_screen)
        try:
            execute_new_loop = self._screen_stack.pop().execute_new_loop
        except ScreenStackEmptyException as e:
            raise ScreenStackEmptyException(
                "Switch screen is not possible when there is no "
                "screen scheduled!") from e

        # we have to keep the old_loop value so we stop
        # dialog's mainloop if it ever uses switch_screen
        screen = ScreenData(ui_screen, args, execute_new_loop)
        self._screen_stack.append(screen)
        self.redraw()

    def push_screen(self, ui_screen, args=None):
        """Schedules a screen to show, but keeps the current one in stack to
        return to, when the new one is closed.

        :param ui_screen: screen to show
        :type ui_screen: UIScreen instance
        :param args: optional argument
        :type args: anything
        """
        log.debug("Pushing screen %s to stack", ui_screen)
        screen = ScreenData(ui_screen, args, False)
        self._screen_stack.append(screen)
        self.redraw()

    def push_screen_modal(self, ui_screen, args=None):
        """Starts a new screen right away, so the caller can collect data back.

        When the new screen is closed, the caller is redisplayed.

        This method does not return until the new screen is closed.

        :param ui_screen: screen to show
        :type ui_screen: UIScreen instance
        :param args: optional argument, please see switch_screen for details
        :type args: anything
        """
        log.debug("Pushing modal screen %s to stack", ui_screen)
        screen = ScreenData(ui_screen, args, True)
        self._screen_stack.append(screen)
        # only new events will be processed now
        # the old one will wait after this event loop will be closed
        self._event_loop.execute_new_loop(RenderScreenSignal(self))

    def _close_screen_callback(self, signal, data):
        self.close_screen(signal.source)

    def close_screen(self, closed_from=None):
        """Close the currently displayed screen and exit it's main loop if necessary.

        Next screen from the stack is then displayed.
        """
        screen = self._screen_stack.pop()
        log.debug("Closing screen %s from %s", screen, closed_from)

        # User can react when screen is closing
        screen.ui_screen.closed()

        if closed_from is not None and closed_from is not screen.ui_screen:
            raise RenderUnexpectedError(
                "You are trying to close screen %s from screen %s! "
                "This is most probably not intentional." %
                (closed_from, screen.ui_screen))

        if screen.execute_new_loop:
            self._event_loop.close_loop()

        # redraw screen if there is what to redraw
        # and if it is not modal screen (modal screen parent is blocked)
        if not self._screen_stack.empty() and not screen.execute_new_loop:
            self.redraw()

        # we can't draw anything more. Kill the application.
        if self._screen_stack.empty():
            raise ExitMainLoop()

    def redraw(self):
        """Register rendering to the event loop for processing."""
        self._event_loop.enqueue_signal(RenderScreenSignal(self))

    def _process_screen_callback(self, signal, data):
        self._process_screen()

    def _process_screen(self):
        """Process the current screen.

        1) It will call setup if the screen is not already set.
        2a) If setup was success then draw the screen.
        2b) If setup wasn't successful then pop the screen and try to process next in the stack.
            Continue by (1).
        3)Ask for user input if requested.
        """
        top_screen = self._get_last_screen()

        log.debug("Processing screen %s", top_screen)

        # this screen is used first time (call setup() method)
        if not top_screen.ui_screen.screen_ready:
            if not top_screen.ui_screen.setup(top_screen.args):
                # remove the screen and skip if setup went wrong
                self._screen_stack.pop()
                self.redraw()
                log.warning("Screen %s setup wasn't successful", top_screen)
                return

        # get the widget tree from the screen and show it in the screen
        try:
            # refresh screen content
            top_screen.ui_screen.refresh(top_screen.args)

            # Screen was closed in the refresh method
            if top_screen != self._get_last_screen():
                return

            # draw screen to the console
            self._draw_screen(top_screen)

            if top_screen.ui_screen.input_required:
                log.debug("Input is required by %s screen", top_screen)
                top_screen.ui_screen.get_input_with_error_check(
                    top_screen.args)
        except ExitMainLoop:  # pylint: disable=try-except-raise
            raise
        except Exception:  # pylint: disable=broad-except
            self._event_loop.enqueue_signal(ExceptionSignal(self))
            return

    def _draw_screen(self, active_screen):
        """Draws the current `active_screen`.

        :param active_screen: Screen which should be draw to the console.
        :type active_screen: Classed based on `simpleline.render.screen.UIScreen`.
        """
        # get the widget tree from the screen and show it in the screen
        try:
            if not active_screen.ui_screen.no_separator:
                # separate the content on the screen from the stuff we are about to display now
                print(self._spacer())

            # print UIScreen content
            active_screen.ui_screen.show_all()
        except ExitMainLoop:  # pylint: disable=try-except-raise
            raise
        except Exception:  # pylint: disable=broad-except
            self._event_loop.enqueue_signal(ExceptionSignal(self))

    def _get_last_screen(self):
        if self._screen_stack.empty():
            raise ExitMainLoop()

        return self._screen_stack.pop(False)

    def process_input_result(self, input_result, should_redraw):
        active_screen = self._get_last_screen()

        if not input_result.was_successful():
            if should_redraw:
                self.redraw()
            else:
                log.debug("Input was not successful, ask for new input.")
                active_screen.ui_screen.get_input_with_error_check(
                    active_screen.args)
        else:
            if input_result == UserInputAction.NOOP:
                return

            if input_result == UserInputAction.REDRAW:
                self.redraw()
            elif input_result == UserInputAction.CLOSE:
                self.close_screen()
            elif input_result == UserInputAction.QUIT:
                if self.quit_screen:
                    self.push_screen_modal(self.quit_screen)
                    try:
                        if self.quit_screen.answer is True:
                            raise ExitMainLoop()

                        self.redraw()
                    except AttributeError as e:
                        raise ExitMainLoop() from e
                else:
                    raise ExitMainLoop()