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)
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()