diff --git a/.github/workflows/build_docs_test.yml b/.github/workflows/build_docs_test.yml index cb98821..9fcfd6c 100644 --- a/.github/workflows/build_docs_test.yml +++ b/.github/workflows/build_docs_test.yml @@ -19,10 +19,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.9 - name: Build Docs run: | cd docs/scripts && bash generateFromDocstrings.sh \ No newline at end of file diff --git a/.github/workflows/build_package_test_linux.yml b/.github/workflows/build_package_test_linux.yml index 3255ccc..08720cb 100644 --- a/.github/workflows/build_package_test_linux.yml +++ b/.github/workflows/build_package_test_linux.yml @@ -21,10 +21,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.9 - name: Update pip run: | python -m pip install --upgrade pip diff --git a/.github/workflows/build_package_test_win.yml b/.github/workflows/build_package_test_win.yml index bf6eb77..968fce2 100644 --- a/.github/workflows/build_package_test_win.yml +++ b/.github/workflows/build_package_test_win.yml @@ -21,10 +21,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.9 - name: Update pip run: | python -m pip install --upgrade pip diff --git a/.github/workflows/py_cui_test.yml b/.github/workflows/py_cui_test.yml index 0155944..b878048 100644 --- a/.github/workflows/py_cui_test.yml +++ b/.github/workflows/py_cui_test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/examples/controls/dropdown.py b/examples/controls/dropdown.py new file mode 100644 index 0000000..ddfc79a --- /dev/null +++ b/examples/controls/dropdown.py @@ -0,0 +1,21 @@ +"""The most basic possible use case for py_cui + +@author: Jakub Wlodek +@created: 12-Aug-2019 +""" + +# Import the lib +import py_cui + +# create the CUI object. Will have a 3 by 3 grid with indexes from 0,0 to 2,2 +root = py_cui.PyCUI(3, 3) + +# Add a label to the center of the CUI in the 1,1 grid position +dropdown = root.add_custom_widget(py_cui.widgets.DropdownMenu, 'Example Dropdown', 1, 1, 1, 1, 1, 0, 10) +textbox = root.add_text_box('Test Text Box', 0, 1, 1, 1, 1, 0, "Hi") + +for i in range(15): + dropdown.add_item(f'Test{i}') + +# Start/Render the CUI +root.start() diff --git a/examples/expanded_palette.py b/examples/expanded_palette.py new file mode 100644 index 0000000..ec4f1cf --- /dev/null +++ b/examples/expanded_palette.py @@ -0,0 +1,86 @@ +"""A simple example demonstrating how to add additional colors to your UI past the defaults. + +@author: Jakub Wlodek +@created: 17-Apr-2023 +""" + + +import py_cui + +root = py_cui.PyCUI(3, 3) + +# The default colors created for you by py_cui are essentially every foreground/background +# combination using the default basic 8 colors: White, Black, Blue, Red, Green, Yellow, Cyan, and Magenta. + +# For conveniance, these color pairs have variables that can be used as shortcuts, ex. py_cui.colors.GREEN_ON_BLACK will allow +# for green text on a black background. + +# These default colors make up 56 foreground/background combinations. In total, py_cui allows for up to 256 (in supported terminals). +# The remaining 200 foreground/background pairs can be customized with the below function calls. + + +# The add_color_pair function takes two arguments, a foreground color, and a background color, each being a number from 0-255. +# The default colors have codes: 0 - BLACK, 1- RED, 2 - GREEN, 3 - YELLOW, 4 - BLUE, 5 - MAGENTA, 6 - CYAN, 7 - WHITE. +# 208 represents a shade of orange in my terminal emulator. + +ORANGE_ON_BLACK = root.add_color_pair(208, 0) + + +# Additional color codes can depend on the terminal emulator, however, the below program can help identifying them. +# It will print (on a black background), the code for each of the 256 supported colors - in the respective color. +# The above number (203) was printed orange in my terminal emulator (gnome-terminal). So (208, 0) will be orange on a black background. +""" +import curses + +def show_color_codes(stdscr): + key = 0 + + # Clear and refresh the screen for a blank canvas + stdscr.clear() + stdscr.refresh() + + # Start colors in curses + curses.start_color() + + for i in range(256): + curses.init_pair(i+1, i, 0) + + # Loop where k is the last character pressed + while (k != ord('q')): + + # Initialization + stdscr.clear() + height, width = stdscr.getmaxyx() + + xpos = 0 + ypos = 0 + for i in range(256): + temp = int(i / 25) + if temp > ypos: + ypos += 1 + xpos = 0 + stdscr.addstr(ypos, xpos, " ", curses.color_pair(0)) + stdscr.addstr(ypos, xpos + 1, str(i), curses.color_pair(i)) + stdscr.addstr(ypos, xpos + 1 + len(str(i)), " ", curses.color_pair(0)) + xpos = xpos + len(str(i)) + 2 + + # Refresh the screen + stdscr.refresh() + + # Wait for next input + key = stdscr.getch() + +def main(): + curses.wrapper(show_color_codes) + +if __name__ == "__main__": + main() +""" + + +# Add a label to the center of the CUI in the 1,1 grid position +label = root.add_label('Hello py_cui in a non-standard color!!!', 1, 1) +label.set_color(ORANGE_ON_BLACK) + +# Start/Render the CUI +root.start() diff --git a/py_cui/__init__.py b/py_cui/__init__.py index 1e90b36..8b139f4 100644 --- a/py_cui/__init__.py +++ b/py_cui/__init__.py @@ -135,6 +135,17 @@ def __init__( height = self._simulated_terminal[0] width = self._simulated_terminal[1] + # Curses supports up to 256 color pairs. Outside of the default color pairs (56 total combos) + # Allow user to customize remaining 200 + self._num_color_pairs = 56 + self._available_color_pairs = 200 + self._color_map = py_cui.colors._COLOR_MAP + + # Data structure mapping color combos to pair code for faster lookup + self._reverse_color_map = {} + for i in range(56): + self._reverse_color_map[self._color_map[i+1]] = i+1 + # Add status and title bar self.title_bar = py_cui.statusbar.StatusBar( self._title, BLACK_ON_WHITE, root=self, is_title_bar=True @@ -168,7 +179,7 @@ def __init__( # Initialize grid, renderer, and widget dict self._grid = py_cui.grid.Grid( - num_rows, num_cols, self._height, self._width, self._logger + self, num_rows, num_cols, self._height, self._width, self._logger ) self._stdscr: Any = None self._refresh_timeout = -1 @@ -198,6 +209,20 @@ def __init__( # Callback to fire when CUI is stopped. self._on_stop: Optional[Callable[[], Any]] = None + def add_color_pair(self, foreground_color: int, bkgd_color: int): + if self._available_color_pairs == 0: + raise py_cui.errors.PyCUIError("Maximum number of color pairs permitted has been exceeded!") + else: + self._available_color_pairs -= 1 + self._num_color_pairs += 1 + self._color_map[self._num_color_pairs] = (foreground_color, bkgd_color) + self._reverse_color_map[(foreground_color, bkgd_color)] = self._num_color_pairs + return self._num_color_pairs + + def get_color_code(self, foreground_color: int, bkgd_color: int) -> int: + + return self._reverse_color_map[(foreground_color, bkgd_color)] + def set_refresh_timeout(self, timeout: int): """Sets the CUI auto-refresh timeout to a number of seconds. @@ -238,6 +263,14 @@ def set_widget_cycle_key( self._reverse_cycle_key = reverse_cycle_key def set_toggle_live_debug_key(self, toggle_debug_key: int) -> None: + """Sets the keybinding for opening/closing popup debug log. + + Parameters + ---------- + toggle_debug_key : py_cui.keys.KEY + Key code for debug log open/close toggle + """ + self._toggle_live_debug_key = toggle_debug_key def enable_logging( @@ -383,8 +416,8 @@ def _initialize_colors(self) -> None: # Start colors in curses. # For each color pair in color map, initialize color combination. curses.start_color() - for color_pair in py_cui.colors._COLOR_MAP.keys(): - fg_color, bg_color = py_cui.colors._COLOR_MAP[color_pair] + for color_pair in self._color_map.keys(): + fg_color, bg_color = self._color_map[color_pair] curses.init_pair(color_pair, fg_color, bg_color) def _initialize_widget_renderer(self) -> None: @@ -1795,7 +1828,10 @@ def _draw(self, stdscr) -> None: self._logger.info( f"handling mouse press for elem: {in_element.get_title()}" ) - in_element._handle_mouse_press(x, y, mouse_event) + + move_focus = in_element._handle_mouse_press(x, y, mouse_event) + if move_focus: + self.move_focus(in_element) # Otherwise, if not a popup, select the clicked on widget elif in_element is not None and not isinstance( diff --git a/py_cui/grid.py b/py_cui/grid.py index 640bff6..e4494da 100644 --- a/py_cui/grid.py +++ b/py_cui/grid.py @@ -31,7 +31,7 @@ class Grid: """ - def __init__(self, num_rows: int, num_columns: int, height: int, width: int, logger: 'py_cui.debug.PyCUILogger'): + def __init__(self, parent, num_rows: int, num_columns: int, height: int, width: int, logger: 'py_cui.debug.PyCUILogger'): """Constructor for the Grid class Parameters @@ -46,6 +46,7 @@ def __init__(self, num_rows: int, num_columns: int, height: int, width: int, log The width in characters of the terminal window """ + self._parent = parent self._num_rows = num_rows self._num_columns = num_columns self._height = height diff --git a/py_cui/renderer.py b/py_cui/renderer.py index ac3b78c..4853447 100644 --- a/py_cui/renderer.py +++ b/py_cui/renderer.py @@ -377,7 +377,7 @@ def draw_text(self, ui_element: 'py_cui.ui.UIElement', line: str, y: int, center self.set_color_mode(ui_element.get_border_color()) if bordered: - self._stdscr.addstr(y, start_x + padx, self._border_characters['VERTICAL']) + self._stdscr.addstr(y, start_x + padx, self._border_characters['VERTICAL'] + ' ') current_start_x = current_start_x + 2 self.unset_color_mode(ui_element.get_border_color()) @@ -409,7 +409,7 @@ def draw_text(self, ui_element: 'py_cui.ui.UIElement', line: str, y: int, center self.set_color_mode(ui_element.get_border_color()) if bordered: - self._stdscr.addstr(y, stop_x - padx - 1, self._border_characters['VERTICAL']) + self._stdscr.addstr(y, stop_x - padx - 2, ' ' + self._border_characters['VERTICAL']) self.unset_color_mode(ui_element.get_border_color()) diff --git a/py_cui/ui.py b/py_cui/ui.py index 06ae250..a5e13f7 100644 --- a/py_cui/ui.py +++ b/py_cui/ui.py @@ -961,6 +961,41 @@ def set_selected_item(self, selected_item: Any): self._view_items[self._selected_item] = selected_item + def get_item_index(self, index: int): + """Function that returns reference to item at given index + + Paramters + --------- + index : int + Index of object + + Returns + ------- + item : Any + Item at specified index in the list, or None if index is invalid. + """ + + try: + return self._view_items[index] + except IndexError: + return None + + + def set_item_index(self, item: Any, index: int): + """Function that sets the item at the specified index of the menu + + Parameters + ---------- + item: Any + Item to put in menu at given index + index: int + Index at which to put the specified item. + """ + + if item is not None and len(self._view_items) > index and index >= 0: + self._view_items[index] = item + + class CheckBoxMenuImplementation(MenuImplementation): """Class representing checkbox menu ui implementation @@ -1051,6 +1086,15 @@ def mark_item_as_not_checked(self, item) -> None: self._selected_item_dict[item] = False +class DropdownMenuImplementation(MenuImplementation): + + def __init__(self, logger, max_height): + + super().__init__(logger) + self.max_height = max_height + self.opened = False + + class TextBlockImplementation(UIImplementation): """Base class for TextBlockImplementation diff --git a/py_cui/widget_set.py b/py_cui/widget_set.py index 9f3deac..8bf14da 100644 --- a/py_cui/widget_set.py +++ b/py_cui/widget_set.py @@ -60,7 +60,7 @@ def __init__(self, num_rows: int, num_cols: int, logger: 'py_cui.debug.PyCUILogg status_bars_height = self._root.title_bar.get_height() + self._root.status_bar.get_height() self._height = self._height - status_bars_height - 2 - self._grid = py_cui.grid.Grid(num_rows, num_cols, self._height, self._width, logger) + self._grid = py_cui.grid.Grid(root, num_rows, num_cols, self._height, self._width, logger) self._selected_widget: Optional[int] = None self._logger = logger diff --git a/py_cui/widgets.py b/py_cui/widgets.py index bbaab38..3669c53 100644 --- a/py_cui/widgets.py +++ b/py_cui/widgets.py @@ -84,6 +84,19 @@ def __init__(self, id, title: str, grid: 'py_cui.grid.Grid', row: int, column: i self._border_color = self._default_color self.update_height_width() + self._move_focus_map = {} + for mouse_event in py_cui.keys.MOUSE_EVENTS: + self._move_focus_map[mouse_event] = False + + self._context_menu = None + + + def _get_parent_ui(self): + """Function used to get reference to parent UI instance for interfacing with popups and context menus + """ + + return self._grid._parent + def add_key_command(self, key: Union[int, List[int]], command: Callable[[],Any]) -> None: """Maps a keycode to a function that will be executed when in focus mode @@ -103,7 +116,7 @@ def add_key_command(self, key: Union[int, List[int]], command: Callable[[],Any]) self._key_commands[key] = command - def add_mouse_command(self, mouse_event: int, command: Callable[[],Any]) -> None: + def add_mouse_command(self, mouse_event: int, command: Callable[[],Any], move_focus = False) -> None: """Maps a keycode to a function that will be executed when in focus mode Parameters @@ -127,6 +140,9 @@ def add_mouse_command(self, mouse_event: int, command: Callable[[],Any]) -> None self._mouse_commands[mouse_event] = command + # Specify whether we want to shift focus to the clicked-on widget based on the event type + self._move_focus_map[mouse_event] = move_focus + def update_key_command(self, key: Union[int, List[int]], command: Callable[[],Any]) -> Any: """Maps a keycode to a function that will be executed when in focus mode, if key is already mapped @@ -336,6 +352,8 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int): else: command() + return self._move_focus_map[mouse_event] + def _handle_key_press(self, key_pressed: int) -> None: """Base class function that handles all assigned key presses. @@ -504,7 +522,7 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int): # For scroll menu, handle custom mouse press after initial event, since we will likely want to # have access to the newly selected item - Widget._handle_mouse_press(self, x, y, mouse_event) + return Widget._handle_mouse_press(self, x, y, mouse_event) @@ -611,12 +629,13 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int) -> None: Coordinates of mouse press """ - Widget._handle_mouse_press(self, x, y, mouse_event) + move_focus = Widget._handle_mouse_press(self, x, y, mouse_event) viewport_top = self._start_y + self._pady + 1 if viewport_top <= y and viewport_top + len(self._view_items) - self._top_view >= y: elem_clicked = y - viewport_top + self._top_view self.set_selected_item_index(elem_clicked) self.mark_item_as_checked(self._view_items[elem_clicked]) + return move_focus def _handle_key_press(self, key_pressed: int) -> None: @@ -678,6 +697,217 @@ def _draw(self) -> None: self._renderer.reset_cursor(self) +class RadioMenu(CheckBoxMenu): + + def __init__(self, *args): + super.__init__(args) + + def toggle_item_checked(self, item: Any): + + if not self._selected_item_dict[item]: + for curr in self._selected_item_dict.keys(): + if self._selected_item_dict[curr]: + self._selected_item_dict[curr] = False + + self._selected_item_dict[item] = not self._selected_item_dict[item] + + def mark_item_as_checked(self, item: Any) -> None: + self.toggle_item_checked(item) + + + def mark_item_as_not_checked(self, item) -> None: + self.toggle_item_checked(item) + + +class DropdownMenu(Widget, py_cui.ui.DropdownMenuImplementation): + + + def __init__(self, id, title: str, grid: 'py_cui.grid.Grid', row: int, column: int, row_span: int, column_span: int, padx: int, pady: int, logger, max_height: int): + """Initializer for CheckBoxMenu Widget + """ + + Widget.__init__(self, id, title, grid, row, column, row_span, column_span, padx, pady, logger) + py_cui.ui.DropdownMenuImplementation.__init__(self, logger, max_height) + self.set_help_text('Focus mode on Dropdown. Use up/down to scroll. Use Enter to open, Backspace to close, arrows to navigate.') + + self._selected_from_dropdown = None + + # Shift focus to dropdown in event of click or double click. + self._move_focus_map[py_cui.keys.LEFT_MOUSE_CLICK] = True + self._move_focus_map[py_cui.keys.LEFT_MOUSE_DBL_CLICK] = True + + + def update_height_width(self) -> None: + Widget.update_height_width(self) + padx, _ = self.get_padding() + _, start_y = self.get_start_position() + height, width = self.get_absolute_dimensions() + self._dropdown_center = start_y + int(height / 2) + 1 + self._horiz_viewport_width = width - 2 * padx - 3 + + + def set_selected_dropdown_option(self, item): + self._title = str(item) + self._selected_from_dropdown = item + + + def get_selected_dropdown_option(self): + return self._selected_from_dropdown + + + def _get_actual_max_height(self): + + window_height, _ = self._grid.get_dimensions_absolute() + dropdown_top = self._dropdown_center - 1 + dropdown_bottom = self._dropdown_center + 1 + + # Room up is the space between the top of the dropdown itself to the top of + room_up = dropdown_top - 1 - 1 + room_down = window_height - dropdown_bottom - 1 + + if room_down >= self.max_height: + return self.max_height, True + elif room_up >= self.max_height: + return self.max_height, False + elif room_up > room_down: + return room_up, False + else: + return room_down, True + + + def _get_render_text(self): + num_spaces = len(self._title) + 1 - self._horiz_viewport_width + open_closed = '^' + if not self.opened: + open_closed = 'v' + if num_spaces > 0: + return self._title + ' ' * num_spaces + open_closed + else: + return py_cui.fit_text(self._horiz_viewport_width - 1, self._title) + open_closed + + + def _handle_mouse_press(self, x: int, y: int, mouse_event: int) -> None: + """Override of base class function, handles mouse press in menu + + Parameters + ---------- + x, y : int + Coordinates of mouse press + """ + + move_focus = Widget._handle_mouse_press(self, x, y, mouse_event) + if abs(y - self._dropdown_center) <= 1 and mouse_event in [py_cui.keys.LEFT_MOUSE_DBL_CLICK, py_cui.keys.LEFT_MOUSE_CLICK]: + self.opened = not self.opened + if self.opened: + viewport_height, _ = self._get_actual_max_height() + viewport_height = viewport_height - 1 + if self._selected_from_dropdown is None: + self._top_view = 0 + else: + for i, view_item in enumerate(self._view_items): + if view_item == self._selected_from_dropdown: + if len(self._view_items) - i > viewport_height: + self._top_view = i + else: + self._top_view = len(self._view_items) - viewport_height - 1 + return move_focus + + + def _handle_key_press(self, key_pressed: int) -> None: + """Override of key presses. + + First, run the superclass function, scrolling should still work. + Adds Enter command to toggle selection + + Parameters + ---------- + key_pressed : int + key code of pressed key + """ + + Widget._handle_key_press(self, key_pressed) + + viewport_height, _ = self._get_actual_max_height() + viewport_height = viewport_height - 1 + + if self.opened: + if key_pressed == py_cui.keys.KEY_UP_ARROW: + self._scroll_up() + if key_pressed == py_cui.keys.KEY_DOWN_ARROW: + self._scroll_down(viewport_height) + if key_pressed == py_cui.keys.KEY_HOME: + self._jump_to_top() + if key_pressed == py_cui.keys.KEY_END: + self._jump_to_bottom(viewport_height) + if key_pressed == py_cui.keys.KEY_PAGE_UP: + self._jump_up() + if key_pressed == py_cui.keys.KEY_PAGE_DOWN: + self._jump_down(viewport_height) + if key_pressed in py_cui.keys.KEY_BACKSPACE: + self.opened = False + if key_pressed == py_cui.keys.KEY_ENTER: + self.set_selected_dropdown_option(self._view_items[self._selected_item]) + self.opened = False + else: + if key_pressed == py_cui.keys.KEY_ENTER: + if self._selected_from_dropdown is None: + self._top_view = 0 + else: + for i, view_item in enumerate(self._view_items): + if view_item == self._selected_from_dropdown: + if len(self._view_items) - i > viewport_height: + self._top_view = i + else: + self._top_view = len(self._view_items) - viewport_height - 1 + self.opened = True + + + def _draw(self) -> None: + """Overrides base class draw function + """ + + Widget._draw(self) + self._renderer.set_color_mode(self._color) + self._renderer.draw_border(self, fill=False, with_title=False) + + self._renderer.draw_text(self, self._get_render_text(), self._dropdown_center, selected=self._selected) + + if self.opened: + actual_height, dropdown_dir_down = self._get_actual_max_height() + + if self.is_selected(): + self._renderer._set_bold() + + if dropdown_dir_down: + self._renderer._draw_border_bottom(self, self._dropdown_center + 2 + actual_height) + start_y = self._dropdown_center + 2 + else: + self._renderer._draw_border_top(self, self._dropdown_center - 2 - actual_height, False) + start_y = self._dropdown_center - actual_height - 1 + + if self.is_selected(): + self._renderer._unset_bold() + + line_counter = 0 + counter = 0 + + for item in self._view_items: + line = str(item) + if line_counter < self._top_view: + line_counter = line_counter + 1 + else: + if counter >= actual_height: + break + if line_counter == self._selected_item: + self._renderer.draw_text(self, line, start_y + counter, selected=True) + else: + self._renderer.draw_text(self, line, start_y + counter) + counter = counter + 1 + line_counter = line_counter + 1 + + self._renderer.unset_color_mode(self._color) + self._renderer.reset_cursor(self, fill=False) + class Button(Widget): """Basic button widget. @@ -747,6 +977,10 @@ def __init__(self, id, title: str, grid: 'py_cui.grid.Grid', row: int, column: i self.update_height_width() self.set_help_text('Focus mode on TextBox. Press Esc to exit focus mode.') + # Shift focus to textbox in event of click or double click. + self._move_focus_map[py_cui.keys.LEFT_MOUSE_CLICK] = True + self._move_focus_map[py_cui.keys.LEFT_MOUSE_DBL_CLICK] = True + def update_height_width(self) -> None: """Need to update all cursor positions on resize @@ -774,7 +1008,7 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int) -> None: Coordinates of mouse press """ - Widget._handle_mouse_press(self, x, y, mouse_event) + move_focus = Widget._handle_mouse_press(self, x, y, mouse_event) if y == self._cursor_y and x >= self._cursor_max_left and x <= self._cursor_max_right: if x <= len(self._text) + self._cursor_max_left: old_text_pos = self._cursor_text_pos @@ -784,6 +1018,7 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int) -> None: else: self._cursor_x = self._cursor_max_left + len(self._text) self._cursor_text_pos = len(self._text) + return move_focus def _handle_key_press(self, key_pressed: int) -> None: @@ -854,6 +1089,10 @@ def __init__(self, id, title: str, grid: 'py_cui.grid.Grid', row: int, column: i self.update_height_width() self.set_help_text('Focus mode on TextBlock. Press Esc to exit focus mode.') + # Shift focus to text block in event of click or double click. + self._move_focus_map[py_cui.keys.LEFT_MOUSE_CLICK] = True + self._move_focus_map[py_cui.keys.LEFT_MOUSE_DBL_CLICK] = True + def update_height_width(self) -> None: """Function that updates the position of the text and cursor on resize @@ -883,7 +1122,7 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int) -> None: Coordinates of mouse press """ - Widget._handle_mouse_press(self, x, y, mouse_event) + move_focus = Widget._handle_mouse_press(self, x, y, mouse_event) if mouse_event == py_cui.keys.LEFT_MOUSE_CLICK: if y >= self._cursor_max_up and y <= self._cursor_max_down: @@ -907,6 +1146,8 @@ def _handle_mouse_press(self, x: int, y: int, mouse_event: int) -> None: self._cursor_x = self._cursor_max_left + len(line) self._cursor_text_pos_x = len(line) + return move_focus + def _handle_key_press(self, key_pressed: int) -> None: """Override of base class handle key press function diff --git a/tests/conftest.py b/tests/conftest.py index 1343096..d13d528 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,14 @@ def LOGGER(): return dbg.PyCUILogger('PYCUI TEST') +@pytest.fixture +def PYCUI(): + + def _PYCUI(rows, cols, height, width): + return py_cui.PyCUI(rows, cols, simulated_terminal=[height, width]) + + return _PYCUI + @pytest.fixture def RENDERER(request, LOGGER): @@ -17,11 +25,11 @@ def RENDERER(request, LOGGER): @pytest.fixture -def GRID(request, LOGGER): +def GRID(request, PYCUI, LOGGER): def _GRID(rows, cols, height, width): - return py_cui.grid.Grid(rows, cols, height, width, LOGGER) + return py_cui.grid.Grid(PYCUI, rows, cols, height, width, LOGGER) return _GRID @@ -43,15 +51,6 @@ def _CUSTOMWIDGET(id, name, row, col, rowspan, colspan): return _CUSTOMWIDGET -@pytest.fixture -def PYCUI(): - - def _PYCUI(rows, cols, height, width): - return py_cui.PyCUI(rows, cols, simulated_terminal=[height, width]) - - return _PYCUI - - @pytest.fixture def WIDGETSET(request, LOGGER):