Esempio n. 1
0
 def test_item_to_page(self):
     p = Paginator(3, 3, 19)
     self.assertEqual(p.item_to_page(4), 0)
     self.assertEqual(p.item_to_page(8), 0)
     self.assertEqual(p.item_to_page(9), 1)
     self.assertEqual(p.item_to_page(17), 1)
     self.assertEqual(p.item_to_page(18), 2)
Esempio n. 2
0
    def test_n_pages(self):
        """Test that the paginator computes the total number of pages correctly
        """
        # Total items ->  number of pages
        expected_results_2x2 = [
            (0, 0),
            (1, 1),
            (2, 1),
            (3, 1),
            (4, 1),
            (5, 2),
            (6, 2),
            (7, 2),
            (8, 2),
            (9, 3),
        ]

        for total, expected_pages in expected_results_2x2:
            paginator = Paginator(2, 2, total)
            self.assertEqual(paginator.n_pages, expected_pages)

        # Total items ->  number of pages
        expected_results_1x1 = [
            (0, 0),
            (1, 1),
            (2, 2),
            (3, 3),
        ]

        for total, expected_pages in expected_results_1x1:
            paginator = Paginator(1, 1, total)
            self.assertEqual(paginator.n_pages, expected_pages)
Esempio n. 3
0
    async def initialize_display(self):
        await super().initialize_display()
        panel_coords = self.panel_coords

        # Set up rolling pad to keep recently loaded files loaded
        self.paginator = Paginator(self.rows, self.cols, len(self.views))
        self.pad_paginator = Paginator(
            self.rows, self.cols,
            min((self.paginator.items_per_page * int(
                np.ceil(len(self.views) / self.paginator.items_per_page))),
                (self.paginator.items_per_page * int(
                    np.floor(var.MAX_CURSES_WINDOWS /
                             self.paginator.items_per_page)))))

        self.setup_pad_page_mappings(self.current_selection)
        self.current_page = self.paginator.item_to_page(self.current_selection)
        self.current_pad_page = self._page_to_pad_page[self.current_page]

        self.create_pad(panel_coords["main"].nlines,
                        panel_coords["main"].ncols)
        self.last_size = self.stdscr.getmaxyx()
Esempio n. 4
0
    async def initialize_display(self):
        await super().initialize_display()
        panel_coords = self.panel_coords

        # Set up rolling pad to keep recently loaded files loaded
        self.paginator = Paginator(self.rows, self.cols, self.n_items)
        self.create_pad(panel_coords["main"].nlines,
                        panel_coords["main"].ncols)

        for idx, window in enumerate(self.windows):
            window.border(0, )
            window.addstr(0, 1, "Window {}".format(idx))
            page_string = "Panel {}".format(idx)
            page_string = pad_string(page_string, side="right", max_len=10)
            window.addstr(window.getmaxyx()[0] - 1,
                          window.getmaxyx()[1] - 1 - len(page_string),
                          page_string)
Esempio n. 5
0
    def test_items_on_page(self):
        # Test multiple
        p = Paginator(3, 3, 18)
        self.assertEqual(p.items_on_page(0), [0, 1, 2, 3, 4, 5, 6, 7, 8])
        self.assertEqual(p.items_on_page(1),
                         [9, 10, 11, 12, 13, 14, 15, 16, 17])
        with self.assertRaises(ValueError):
            p.items_on_page(2)

        # Test uneven
        p = Paginator(3, 3, 19)
        self.assertEqual(p.items_on_page(0), [0, 1, 2, 3, 4, 5, 6, 7, 8])
        self.assertEqual(p.items_on_page(1),
                         [9, 10, 11, 12, 13, 14, 15, 16, 17])
        self.assertEqual(p.items_on_page(2), [18])
        with self.assertRaises(ValueError):
            p.items_on_page(3)

        # Test uneven
        p = Paginator(3, 3, 4)
        self.assertEqual(p.items_on_page(0), [0, 1, 2, 3])
        with self.assertRaises(ValueError):
            p.items_on_page(1)
Esempio n. 6
0
 def test_n_visible(self):
     for i in range(1, 10):
         for j in range(1, 10):
             p = Paginator(i, j, 100)
             self.assertEqual(p.items_per_page, i * j)
Esempio n. 7
0
class InspecGridApp(InspecCursesApp):
    def __init__(
        self,
        rows,
        cols,
        files,
        padx=0,
        pady=0,
        cmap=None,
        file_reader=None,
        view_class=None,
        transform=None,
        map=None,
        threads=4,
        **kwargs,
    ):
        """App for viewing files in a grid pattern
        """
        super().__init__(**kwargs)
        self.rows = rows
        self.cols = cols

        self.state = {}

        self._slot_to_page = {}
        self._page_to_slot = {}

        self.current_selection = 0
        self.current_page = 0
        self.current_page_slot = 0

        self.cmap = load_cmap(cmap)
        self.map = map
        self.reader = file_reader

        if isinstance(transform, InspecTransform):
            self._transforms = [transform]
            self._selected_transform_idx = 0
        elif isinstance(transform, list) and all(
            [isinstance(t, InspecTransform) for t in transform]):
            self._transforms = transform
            self._selected_transform_idx = 0
        else:
            raise ValueError(
                "transform parameter must be a InspecTransform or a list of InspecTransforms"
            )

        self.views = []
        idx = 0
        for filename in files:
            try:
                self.views.append(
                    view_class(self, dict(filename=filename), idx))
            except:  # TODO better warning when files dont load right?
                pass
            else:
                idx += 1
        self.windows = []

        self._n_threads = threads
        self._window_idx_to_tasks = defaultdict(list)
        self.executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=self._n_threads, )

    @property
    def transform(self):
        return self._transforms[self._selected_transform_idx]

    @property
    def current_view(self):
        return self.views[self.current_selection]

    def create_pad(self,
                   screen_height,
                   screen_width,
                   window_pady=1,
                   window_padx=1):
        """Create a curses pad to represent panels we can page through horizontally
        """
        panel_occupies = (screen_height // self.rows,
                          screen_width // self.cols)

        full_cols = self.pad_paginator.n_pages * self.cols

        # Make the pad one extra long
        pad_width = full_cols * panel_occupies[1] + self.cols * panel_occupies[
            1]
        pad_height = self.rows * panel_occupies[0]

        self.pad = curses.newpad(pad_height, pad_width)
        self.page_width = panel_occupies[1] * self.cols

        self.windows = []
        for col in range(full_cols):
            for row in range(self.rows):
                coord = PanelCoord(nlines=panel_occupies[0] - 2 * window_pady,
                                   ncols=panel_occupies[1] - 2 * window_padx,
                                   y=panel_occupies[0] * row + window_pady,
                                   x=panel_occupies[1] * col + window_padx)
                self.windows.append(self.pad.subwin(*coord))

        return self.pad

    def _assign_page_to_pad_page(self, page, pad_page):
        self._pad_page_to_page[pad_page] = page
        self._page_to_pad_page[page] = pad_page

    def setup_pad_page_mappings(self, selection_idx):
        """Sets up pad pages and takes care of edge conditions
        """
        self._pad_page_to_page = {}
        self._page_to_pad_page = {}

        half_pad = self.pad_paginator.n_pages // 2
        target_page = self.paginator.item_to_page(selection_idx)

        iter_pad_pages = range(self.pad_paginator.n_pages)
        if target_page <= half_pad:
            iter_pages = range(self.paginator.n_pages)
        elif half_pad < target_page < (self.paginator.n_pages - half_pad):
            iter_pages = range(target_page - half_pad, target_page + half_pad)
        else:
            iter_pages = range(
                self.paginator.n_pages - self.pad_paginator.n_pages,
                self.paginator.n_pages,
            )

        for pad_page, page in zip(iter_pad_pages, iter_pages):
            self._assign_page_to_pad_page(page, pad_page)

    def compute_char_array(self, file_view, window_idx, *args):
        window = self.windows[window_idx]
        desired_size = self.map.max_img_shape(*window.getmaxyx())
        img, meta = self.transform.convert(*args,
                                           output_size=(desired_size[0],
                                                        desired_size[1]))
        char_array = self.map.to_char_array(img)
        char_array = CursesRenderer.apply_cmap_to_char_array(
            self.cmap, char_array)
        self.q.put_nowait(
            (char_array, file_view, window_idx, self.current_page))

    def cleanup(self):
        self.executor.shutdown(wait=True)

    def refresh_window(self, file_view, window_idx):
        loop = asyncio.get_event_loop()
        if file_view.needs_redraw:
            try:
                data, _ = self.reader.read_file(file_view.data["filename"])
            except RuntimeError:
                self.debug("File {} is not readable".format(
                    file_view.data["filename"]))
                task = None
            else:
                for prev_task in self._window_idx_to_tasks[window_idx]:
                    prev_task.cancel()
                self._window_idx_to_tasks[window_idx] = []
                task = loop.run_in_executor(self.executor,
                                            self.compute_char_array, file_view,
                                            window_idx, data)
                self._window_idx_to_tasks[window_idx].append(task)
                file_view.needs_redraw = False

    def annotate_view(self, file_view, window):
        # Annotate the view
        maxy, maxx = window.getmaxyx()
        if file_view.idx == self.current_selection:
            window.border(0, )
        else:
            window.border(1, 1, 1, 1)

        window.addstr(0, 1, os.path.basename(file_view.data["filename"]))

    async def check_size_reset(self):
        curr_size = self.stdscr.getmaxyx()
        if curr_size != self.last_size:
            curses.resizeterm(*curr_size)
            self.stdscr.clear()
            self.stdscr.refresh()
            self.pad.clear()
            self.windows = []
            await self.initialize_display()
            self.last_size = curr_size
            for view in self.views:
                view.needs_redraw = True
            # This is a hack to wait for the refresh loop to consume stuff
            # Otherwise the next
            await asyncio.sleep(self._refresh_interval * 2)

    async def handle_key(self, ch):
        """Handle key presses"""
        if ch == ord("q"):
            self.close()
        elif ch == curses.KEY_LEFT or ch == ord("h"):
            self.left()
        elif ch == curses.KEY_RIGHT or ch == ord("l"):
            self.right()
        elif ch == curses.KEY_UP or ch == ord("k"):
            self.up()
        elif ch == curses.KEY_DOWN or ch == ord("j"):
            self.down()
        elif ch == curses.KEY_RESIZE:
            self.check_size_reset()
        elif ch == ord("r"):
            rows = self.prompt("Set rows [0-9]: ", int)
            if rows and 0 < rows <= 9:
                self.stdscr.clear()
                self.stdscr.refresh()
                self.pad.clear()
                self.windows = []
                self.rows = rows
                await self.initialize_display()
                for view in self.views:
                    view.needs_redraw = True
                # This is a hack to wait for the refresh loop to consume stuff
                await asyncio.sleep(self._refresh_interval * 2)
        elif ch == ord("c"):
            cols = self.prompt("Set cols [0-9]: ", int)
            if cols and 0 < cols <= 9:
                self.stdscr.clear()
                self.stdscr.refresh()
                self.pad.clear()
                self.windows = []
                self.cols = cols
                await self.initialize_display()
                for view in self.views:
                    view.needs_redraw = True
                # This is a hack to wait for the refresh loop to consume stuff
                await asyncio.sleep(self._refresh_interval * 2)
        elif ch == ord("m"):
            resp = self.prompt(
                "Choose colormap ['greys', 'viridis', 'plasma', ...]: ", str)
            if resp in VALID_CMAPS:
                self.cmap = load_cmap(resp)
            for view in self.views:
                view.needs_redraw = True
        elif ch == ord("p"):
            page = self.prompt("Jump to page: ", int)
            if page and 0 < page <= self.paginator.n_pages:
                self.jump_to_page(page - 1)
        elif ch == ord("z"):
            self._selected_transform_idx = (self._selected_transform_idx +
                                            1) % len(self._transforms)
            for view in self.views:
                view.needs_redraw = True

    def left(self):
        """Return if the selection has changed, the current, and previous selections"""
        if self.current_selection <= 0:
            return False, self.current_selection, self.current_selection
        else:
            prev_selection = self.current_selection
            self.current_selection = max(0, self.current_selection - self.rows)
            if self.current_page != self.paginator.item_to_page(
                    self.current_selection):
                self.prev_page()
            return True, self.current_selection, prev_selection

    def right(self):
        """Return if the selection has changed, the current, and previous selections"""
        if self.current_selection >= len(self.views) - 1:
            return False, self.current_selection, self.current_selection
        else:
            prev_selection = self.current_selection
            self.current_selection = min(
                len(self.views) - 1, self.current_selection + self.rows)
            if self.current_page != self.paginator.item_to_page(
                    self.current_selection):
                self.next_page()
            return True, self.current_selection, prev_selection

    def up(self):
        """Return if the selection has changed, the current, and previous selections"""
        if self.current_selection <= 0:
            return False, self.current_selection, self.current_selection
        else:
            self.current_selection -= 1
            if self.current_page != self.paginator.item_to_page(
                    self.current_selection):
                self.prev_page()
            return True, self.current_selection, self.current_selection + 1

    def down(self):
        """Return if the selection has changed, the current, and previous selections"""
        if self.current_selection >= len(self.views) - 1:
            return False, self.current_selection, self.current_selection
        else:
            self.current_selection += 1
            if self.current_page != self.paginator.item_to_page(
                    self.current_selection):
                self.next_page()
            return True, self.current_selection, self.current_selection - 1

    def next_page(self):
        if self.current_page == self.paginator.n_pages - 1:
            return

        self.current_page += 1
        self.current_pad_page = (self.current_pad_page +
                                 1) % self.pad_paginator.n_pages
        self.resolve_page_move()

    def prev_page(self):
        if self.current_page == 0:
            return

        self.current_page -= 1
        self.current_pad_page = (self.current_pad_page -
                                 1) % self.pad_paginator.n_pages
        self.resolve_page_move()

    def jump_to_page(self, new_page):
        if new_page in self._page_to_pad_page:
            self.current_pad_page = self._page_to_pad_page[new_page]
            self.current_page = new_page
        elif np.abs(new_page - self.current_page) < self.pad_paginator.n_pages:
            move_n = new_page - self.current_page
            self.current_pad_page = (self.current_pad_page +
                                     move_n) % self.pad_paginator.n_pages
            self.current_page = new_page
        else:
            self.current_page = 0

        self.current_selection = self.paginator.items_on_page(
            self.current_page)[0]
        self.resolve_page_move()

    def resolve_page_move(self):
        """Resolve issues when we have moved to an unloaded page and update pages -> files
        """
        self.update_pad_position()
        if self._pad_page_to_page[self.current_pad_page] != self.current_page:
            self._assign_page_to_pad_page(self.current_page,
                                          self.current_pad_page)
            for view_idx in self.paginator.items_on_page(self.current_page):
                self.views[view_idx].needs_redraw = True

    async def initialize_display(self):
        await super().initialize_display()
        panel_coords = self.panel_coords

        # Set up rolling pad to keep recently loaded files loaded
        self.paginator = Paginator(self.rows, self.cols, len(self.views))
        self.pad_paginator = Paginator(
            self.rows, self.cols,
            min((self.paginator.items_per_page * int(
                np.ceil(len(self.views) / self.paginator.items_per_page))),
                (self.paginator.items_per_page * int(
                    np.floor(var.MAX_CURSES_WINDOWS /
                             self.paginator.items_per_page)))))

        self.setup_pad_page_mappings(self.current_selection)
        self.current_page = self.paginator.item_to_page(self.current_selection)
        self.current_pad_page = self._page_to_pad_page[self.current_page]

        self.create_pad(panel_coords["main"].nlines,
                        panel_coords["main"].ncols)
        self.last_size = self.stdscr.getmaxyx()

    async def refresh(self):
        """Called each 1/refresh_rate, for updating the display"""
        await self.check_size_reset()
        await super().refresh()

        window_indexes = self.pad_paginator.items_on_page(
            self.current_pad_page)
        view_indexes = self.paginator.items_on_page(self.current_page)

        for window_idx, view_idx in itertools.zip_longest(
                window_indexes, view_indexes):
            window = self.windows[window_idx]
            if view_idx is not None:
                file_view = self.views[view_idx]
                self.annotate_view(file_view, window)
                self.refresh_window(file_view, window_idx)
            else:
                window.clear()

        self.draw_page_number()
        self.update_pad_position()

    def start_tasks(self):
        super().start_tasks()
        asyncio.create_task(self.receive_data())

    def post_display(self):
        super().post_display()
        self.q = asyncio.Queue()

    async def receive_data(self):
        while True:
            char_array, file_view, window_idx, current_page = await self.q.get(
            )
            # Make sure we havent changed pages since the task was launched
            if current_page == self.current_page:
                window = self.windows[window_idx]
                try:
                    CursesRenderer.render(window, char_array)
                except CursesRenderError:
                    self.debug("Renderer failed, possibly due to resize")
                self.annotate_view(file_view, window)
            else:
                file_view.needs_redraw = True

    def update_pad_position(self):
        """Move the visible portion of the curses pad to the correct section
        """
        main_coord = self.panel_coords["main"]
        self.pad.refresh(
            0,
            self.page_width * self.current_pad_page,
            main_coord.y,
            main_coord.x,
            main_coord.nlines - 1 - main_coord.y,
            main_coord.ncols - 1 - main_coord.x,
        )

    def draw_page_number(self):
        page_str = "p{}/{}".format(self.current_page + 1,
                                   self.paginator.n_pages)
        try:
            self.status_window.addstr(
                0, self.panel_coords["status"].ncols - 1 - len(page_str),
                page_str)
            self.status_window.refresh()
        except curses.error:
            pass