diff --git a/tests/core/test_core.py b/tests/core/test_core.py index be05d490cc..c9532d1ea2 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -477,6 +477,9 @@ def test_stream_muting_confirmation_popup( stream_name: str = "PTEST", ) -> None: pop_up = mocker.patch(MODULE + ".PopUpConfirmationView") + pop_up.return_value.width = 30 + pop_up.return_value.height = 6 + text = mocker.patch(MODULE + ".urwid.Text") partial = mocker.patch(MODULE + ".partial") controller.model.muted_streams = muted_streams @@ -484,7 +487,7 @@ def test_stream_muting_confirmation_popup( controller.stream_muting_confirmation_popup(stream_id, stream_name) - text.assert_called_with( + text.assert_any_call( ("bold", f"Confirm {action} of stream '{stream_name}' ?"), "center", ) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index d58b6c3353..c6190faffe 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -45,11 +45,20 @@ class TestPopUpConfirmationView: @pytest.fixture def popup_view(self, mocker: MockerFixture) -> PopUpConfirmationView: self.controller = mocker.Mock() + self.controller.maximum_popup_dimensions.return_value = (70, 25) + + self.text = Text("Some question?") + self.callback = mocker.Mock() + self.list_walker = mocker.patch(LISTWALKER, return_value=[]) + self.divider = mocker.patch(MODULE + ".urwid.Divider") - self.text = mocker.patch(MODULE + ".urwid.Text") + self.divider.return_value.rows.return_value = 1 + self.wrapper_w = mocker.patch(MODULE + ".urwid.WidgetWrap") + self.wrapper_w.return_value.rows.return_value = 1 + return PopUpConfirmationView( self.controller, self.text, @@ -68,6 +77,7 @@ def test_exit_popup_yes( self, mocker: MockerFixture, popup_view: PopUpConfirmationView ) -> None: popup_view.exit_popup_yes(mocker.Mock()) + self.callback.assert_called_once_with() assert self.controller.exit_popup.called @@ -75,11 +85,12 @@ def test_exit_popup_no( self, mocker: MockerFixture, popup_view: PopUpConfirmationView ) -> None: popup_view.exit_popup_no(mocker.Mock()) + self.callback.assert_not_called() assert self.controller.exit_popup.called @pytest.mark.parametrize("key", keys_for_command("EXIT_POPUP")) - def test_exit_popup_EXIT_POPUP( + def test_keypress__EXIT_POPUP( self, popup_view: PopUpConfirmationView, key: str, @@ -87,6 +98,7 @@ def test_exit_popup_EXIT_POPUP( ) -> None: size = widget_size(popup_view) popup_view.keypress(size, key) + self.callback.assert_not_called() assert self.controller.exit_popup.called @@ -119,8 +131,8 @@ def pop_up_view_autouse(self, mocker: MockerFixture) -> None: self.command, self.width, self.title, - self.header, - self.footer, + header=self.header, + footer=self.footer, ) def test_init(self, mocker: MockerFixture) -> None: diff --git a/zulipterminal/core.py b/zulipterminal/core.py index eec62826f5..50aa5a3387 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -19,7 +19,6 @@ from typing_extensions import Literal from zulipterminal.api_types import Composition, Message -from zulipterminal.config.symbols import POPUP_CONTENT_BORDER, POPUP_TOP_LINE from zulipterminal.config.themes import ThemeSpec from zulipterminal.config.ui_sizes import ( MAX_LINEAR_SCALING_WIDTH, @@ -42,6 +41,7 @@ MsgInfoView, NoticeView, PopUpConfirmationView, + PopUpFrame, StreamInfoView, StreamMembersView, UserInfoView, @@ -222,23 +222,21 @@ def clamp(n: int, minn: int, maxn: int) -> int: return max_popup_cols, max_popup_rows def show_pop_up(self, to_show: Any, style: str) -> None: - text = urwid.Text(to_show.title, align="center") - title_map = urwid.AttrMap(urwid.Filler(text), style) - title_box_adapter = urwid.BoxAdapter(title_map, height=1) - title_top = urwid.AttrMap(urwid.Divider(POPUP_TOP_LINE), "popup_border") - title = urwid.Pile([title_top, title_box_adapter]) - - content = urwid.LineBox(to_show, **POPUP_CONTENT_BORDER) + if to_show.title is not None: + # +2 to height, due to title enhancement + # TODO: Ideally this would be in PopUpFrame + extra_height = 2 + else: + extra_height = 0 self.loop.widget = urwid.Overlay( - urwid.AttrMap(urwid.Frame(header=title, body=content), "popup_border"), + PopUpFrame(to_show, to_show.title, style), self.view, align="center", valign="middle", # +2 to both of the following, due to LineBox - # +2 to height, due to title enhancement width=to_show.width + 2, - height=to_show.height + 4, + height=to_show.height + 2 + extra_height, ) def is_any_popup_open(self) -> bool: @@ -494,9 +492,8 @@ def show_media_confirmation_popup( "?", ] ) - self.loop.widget = PopUpConfirmationView( - self, question, callback, location="center" - ) + popup = PopUpConfirmationView(self, question, callback) + self.show_pop_up(popup, "area:msg") def search_messages(self, text: str) -> None: # Search for a text in messages @@ -523,9 +520,9 @@ def save_draft_confirmation_popup(self, draft: Composition) -> None: "center", ) save_draft = partial(self.model.save_draft, draft) - self.loop.widget = PopUpConfirmationView( - self, question, save_draft, location="center" - ) + + popup = PopUpConfirmationView(self, question, save_draft) + self.show_pop_up(popup, "area:msg") def stream_muting_confirmation_popup( self, stream_id: int, stream_name: str @@ -537,7 +534,8 @@ def stream_muting_confirmation_popup( "center", ) mute_this_stream = partial(self.model.toggle_stream_muted_status, stream_id) - self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream) + popup = PopUpConfirmationView(self, question, mute_this_stream) + self.show_pop_up(popup, "area:msg") def exit_compose_confirmation_popup(self) -> None: question = urwid.Text( @@ -549,10 +547,9 @@ def exit_compose_confirmation_popup(self) -> None: "center", ) write_box = self.view.write_box - popup_view = PopUpConfirmationView( - self, question, write_box.exit_compose_box, location="center" - ) - self.loop.widget = popup_view + + popup_view = PopUpConfirmationView(self, question, write_box.exit_compose_box) + self.show_pop_up(popup_view, "area:msg") def copy_to_clipboard(self, text: str, text_category: str) -> None: try: @@ -667,11 +664,10 @@ def prompting_exit_handler(self, signum: int, frame: Any) -> None: ("bold", " Please confirm that you wish to exit Zulip-Terminal "), "center", ) - popup_view = PopUpConfirmationView( - self, question, self.deregister_client, location="center" - ) - self.loop.widget = popup_view - self.loop.run() + + popup_view = PopUpConfirmationView(self, question, self.deregister_client) + self.show_pop_up(popup_view, "area:msg") + self.loop.run() # Appears necessary to return control from signal handler def _raise_exception(self, *args: Any, **kwargs: Any) -> Literal[True]: if self._exception_info is not None: diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index c2034b3ef7..e1cc937422 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -24,6 +24,8 @@ CHECK_MARK, COLUMN_TITLE_BAR_LINE, PINNED_STREAMS_DIVIDER, + POPUP_CONTENT_BORDER, + POPUP_TOP_LINE, SECTION_DIVIDER_LINE, ) from zulipterminal.config.ui_mappings import ( @@ -34,7 +36,6 @@ STREAM_ACCESS_TYPE, STREAM_POST_POLICY, ) -from zulipterminal.config.ui_sizes import LEFT_WIDTH from zulipterminal.helper import ( TidiedUserInfo, asynch, @@ -949,6 +950,26 @@ def __init__(self, text: str) -> None: PopUpViewTableContent = Sequence[Tuple[str, Sequence[Union[str, Tuple[str, Any]]]]] +class PopUpFrame(urwid.WidgetDecoration, urwid.WidgetWrap): + def __init__(self, body: Any, title: Optional[str], style: str) -> None: + content = urwid.LineBox(body, **POPUP_CONTENT_BORDER) + + if title is not None: + text = urwid.Text(title, align="center") + title_map = urwid.AttrMap(urwid.Filler(text), style) + title_box_adapter = urwid.BoxAdapter(title_map, height=1) + title_top = urwid.AttrMap(urwid.Divider(POPUP_TOP_LINE), "popup_border") + frame_title = urwid.Pile([title_top, title_box_adapter]) + titled_content = urwid.Frame(body=content, header=frame_title) + else: + titled_content = urwid.Frame(body=content) + + styled = urwid.AttrMap(titled_content, "popup_border") + + urwid.WidgetDecoration.__init__(self, body) + urwid.WidgetWrap.__init__(self, styled) + + class PopUpView(urwid.Frame): def __init__( self, @@ -957,6 +978,7 @@ def __init__( command: str, requested_width: int, title: str, + *, header: Optional[Any] = None, footer: Optional[Any] = None, ) -> None: @@ -1294,19 +1316,17 @@ def __init__(self, controller: Any, title: str) -> None: [("", rendered_menu_content)], column_widths ) - super().__init__(controller, body, "MARKDOWN_HELP", popup_width, title, header) - - -PopUpConfirmationViewLocation = Literal["top-left", "center"] + super().__init__( + controller, body, "MARKDOWN_HELP", popup_width, title, header=header + ) -class PopUpConfirmationView(urwid.Overlay): +class PopUpConfirmationView(urwid.Frame): def __init__( self, controller: Any, question: Any, success_callback: Callable[[], None], - location: PopUpConfirmationViewLocation = "top-left", ) -> None: self.controller = controller self.success_callback = success_callback @@ -1317,32 +1337,18 @@ def __init__( display_widget = urwid.GridFlow([yes, no], 3, 5, 1, "center") wrapped_widget = urwid.WidgetWrap(display_widget) widgets = [question, urwid.Divider(), wrapped_widget] - prompt = urwid.LineBox(urwid.ListBox(urwid.SimpleFocusListWalker(widgets))) + prompt = urwid.ListBox(urwid.SimpleFocusListWalker(widgets)) - if location == "top-left": - align = "left" - valign = "top" - width = LEFT_WIDTH + 1 - height = 8 - else: - align = "center" - valign = "middle" - - max_cols, max_rows = controller.maximum_popup_dimensions() - # +2 to compensate for the LineBox characters. - width = min(max_cols, max(question.pack()[0], len("Yes"), len("No"))) + 2 - height = min(max_rows, sum(widget.rows((width,)) for widget in widgets)) + 2 - - urwid.Overlay.__init__( - self, - prompt, - self.controller.view, - align=align, - valign=valign, - width=width, - height=height, + self.title = None + + max_cols, max_rows = controller.maximum_popup_dimensions() + self.width = min(max_cols, max(question.pack()[0], len("Yes"), len("No"))) + self.height = min( + max_rows, sum(widget.rows((self.width,)) for widget in widgets) ) + super().__init__(prompt) + def exit_popup_yes(self, args: Any) -> None: self.success_callback() self.controller.exit_popup() @@ -1932,8 +1938,8 @@ def __init__( "MSG_INFO", max_cols, title, - urwid.Pile(msg_box.header), - urwid.Pile(msg_box.footer), + header=urwid.Pile(msg_box.header), + footer=urwid.Pile(msg_box.footer), ) def keypress(self, size: urwid_Size, key: str) -> str: @@ -1984,8 +1990,8 @@ def __init__( "MSG_INFO", max_cols, title, - urwid.Pile(msg_box.header), - urwid.Pile(msg_box.footer), + header=urwid.Pile(msg_box.header), + footer=urwid.Pile(msg_box.footer), ) def keypress(self, size: urwid_Size, key: str) -> str: