Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify confirmation popups into popup framework #1534

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion tests/core/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,14 +477,17 @@ 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
controller.loop = mocker.Mock()

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",
)
Expand Down
20 changes: 16 additions & 4 deletions tests/ui_tools/test_popups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -68,25 +77,28 @@ 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

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,
widget_size: Callable[[Widget], urwid_Size],
) -> None:
size = widget_size(popup_view)
popup_view.keypress(size, key)

self.callback.assert_not_called()
assert self.controller.exit_popup.called

Expand Down Expand Up @@ -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:
Expand Down
50 changes: 23 additions & 27 deletions zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -42,6 +41,7 @@
MsgInfoView,
NoticeView,
PopUpConfirmationView,
PopUpFrame,
StreamInfoView,
StreamMembersView,
UserInfoView,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
74 changes: 40 additions & 34 deletions zulipterminal/ui_tools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -34,7 +36,6 @@
STREAM_ACCESS_TYPE,
STREAM_POST_POLICY,
)
from zulipterminal.config.ui_sizes import LEFT_WIDTH
from zulipterminal.helper import (
TidiedUserInfo,
asynch,
Expand Down Expand Up @@ -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,
Expand All @@ -957,6 +978,7 @@ def __init__(
command: str,
requested_width: int,
title: str,
*,
header: Optional[Any] = None,
footer: Optional[Any] = None,
) -> None:
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Loading