Example #1
0
class View(urwid.WidgetWrap):
    """
    A class responsible for providing the application's interface.
    """

    LEFT_WIDTH = 27
    RIGHT_WIDTH = 23

    def __init__(self, controller: Any) -> None:
        self.controller = controller
        self.palette = controller.theme
        self.model = controller.model
        self.users = self.model.users
        self.pinned_streams = self.model.pinned_streams
        self.unpinned_streams = self.model.unpinned_streams
        self.write_box = WriteBox(self)
        self.search_box = SearchBox(self.controller)
        super().__init__(self.main_window())

    def left_column_view(self) -> Any:
        return LeftColumnView(View.LEFT_WIDTH, self)

    def message_view(self) -> Any:
        self.middle_column = MiddleColumnView(self, self.model, self.write_box,
                                              self.search_box)
        return urwid.LineBox(self.middle_column,
                             title='Messages',
                             bline='',
                             tline='━',
                             trcorner='│',
                             tlcorner='│')

    def right_column_view(self) -> Any:
        self.users_view = RightColumnView(View.RIGHT_WIDTH, self)
        return urwid.LineBox(self.users_view,
                             title="Users",
                             tlcorner='━',
                             tline='━',
                             lline='',
                             trcorner='━',
                             blcorner='─',
                             rline='',
                             bline='',
                             brcorner='')

    def get_random_help(self) -> List[Any]:
        # Get random allowed hotkey (ie. eligible for being displayed as a tip)
        allowed_commands = commands_for_random_tips()
        if not allowed_commands:
            return [
                'Help(?): ',
            ]
        random_command = random.choice(allowed_commands)
        return [
            'Help(?): ',
            ('code', ' ' + ', '.join(random_command['keys']) + ' '),
            ' ' + random_command['help_text'],
        ]

    @asynch
    def set_footer_text(self,
                        text_list: Optional[List[Any]] = None,
                        duration: Optional[float] = None) -> None:
        if text_list is None:
            text = self.get_random_help()
        else:
            text = text_list
        self._w.footer.set_text(text)
        self.controller.update_screen()
        if duration is not None:
            assert duration > 0
            time.sleep(duration)
            self.set_footer_text()

    def footer_view(self) -> Any:
        text_header = self.get_random_help()
        return urwid.AttrWrap(urwid.Text(text_header), 'footer')

    def main_window(self) -> Any:
        self.left_panel = self.left_column_view()
        self.center_panel = self.message_view()
        self.right_panel = self.right_column_view()
        if self.controller.autohide:
            body = [
                (View.LEFT_WIDTH, self.left_panel),
                ('weight', 10, self.center_panel),
                (0, self.right_panel),
            ]
        else:
            body = [
                (View.LEFT_WIDTH, self.left_panel),
                ('weight', 10, self.center_panel),
                (View.RIGHT_WIDTH, self.right_panel),
            ]
        self.body = urwid.Columns(body, focus_column=0)
        # NOTE: set_focus_changed_callback is actually called before the
        # focus is set, so the message is not read yet, it will be read when
        # the focus is changed again either vertically or horizontally.
        self.body._contents.set_focus_changed_callback(
            self.model.msg_list.read_message)
        div_char = '═'

        title_text = " {full_name} ({email}) - {server_name} ({url}) ".format(
            full_name=self.model.user_full_name,
            email=self.model.user_email,
            server_name=self.model.server_name,
            url=self.model.server_url)

        title_bar = urwid.Columns([
            urwid.Divider(div_char=div_char),
            (len(title_text), urwid.Text([title_text])),
            urwid.Divider(div_char=div_char),
        ])

        w = urwid.Frame(self.body,
                        title_bar,
                        focus_part='body',
                        footer=self.footer_view())
        return w

    def show_left_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return
        width = View.LEFT_WIDTH if visible else 0
        self.body.contents[0] = (
            self.left_panel,
            self.body.options(width_type='given', width_amount=width),
        )
        if visible:
            self.body.focus_position = 0

    def show_right_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return
        width = View.RIGHT_WIDTH if visible else 0
        self.body.contents[2] = (
            self.right_panel,
            self.body.options(width_type='given', width_amount=width),
        )
        if visible:
            self.body.focus_position = 2

    # FIXME: The type of size should be urwid_Size; this needs checking
    def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]:
        self.model.new_user_input = True
        if self.controller.is_in_editor_mode():
            return self.controller.current_editor().keypress((size[1], ), key)
        # Redirect commands to message_view.
        elif (is_command_key('SEARCH_MESSAGES', key)
              or is_command_key('NEXT_UNREAD_TOPIC', key)
              or is_command_key('NEXT_UNREAD_PM', key)
              or is_command_key('PRIVATE_MESSAGE', key)):
            self.body.focus_col = 1
            self.middle_column.keypress(size, key)
            return key
        elif is_command_key('ALL_PM', key):
            self.model.controller.show_all_pm(self)
            self.body.focus_col = 1
        elif is_command_key('ALL_STARRED', key):
            self.model.controller.show_all_starred(self)
            self.body.focus_col = 1
        elif is_command_key('ALL_MENTIONS', key):
            self.model.controller.show_all_mentions(self)
            self.body.focus_col = 1
        elif is_command_key('SEARCH_PEOPLE', key):
            # Start User Search if not in editor_mode
            self.body.focus_position = 2
            self.users_view.keypress(size, 'w')
            self.show_left_panel(visible=False)
            self.show_right_panel(visible=True)
            self.user_search.set_edit_text("")
            self.controller.enter_editor_mode_with(self.user_search)
            return key
        elif (is_command_key('SEARCH_STREAMS', key)
              or is_command_key('SEARCH_TOPICS', key)):
            # jump stream search
            self.body.focus_position = 0
            self.left_panel.keypress(size, 'q')
            self.show_right_panel(visible=False)
            self.show_left_panel(visible=True)
            if self.left_panel.is_in_topic_view:
                search_box = self.topic_w.topic_search_box
            else:
                search_box = self.stream_w.stream_search_box
            search_box.set_edit_text("")
            self.controller.enter_editor_mode_with(search_box)
            return key
        elif is_command_key('ABOUT', key):
            self.controller.show_about()
            return key
        elif is_command_key('HELP', key):
            # Show help menu
            self.controller.show_help()
            return key
        # replace alternate keys with arrow/functional keys
        # This is needed for navigating in widgets
        # other than message_view.
        elif is_command_key('GO_UP', key):
            key = 'up'
        elif is_command_key('GO_DOWN', key):
            key = 'down'
        elif is_command_key('GO_LEFT', key):
            key = 'left'
        elif is_command_key('GO_RIGHT', key):
            key = 'right'
        elif is_command_key('SCROLL_UP', key):
            key = 'page up'
        elif is_command_key('SCROLL_DOWN', key):
            key = 'page down'
        elif is_command_key('GO_TO_BOTTOM', key):
            key = 'end'
        return super().keypress(size, key)
Example #2
0
class View(urwid.WidgetWrap):
    """
    A class responsible for providing the application's interface.
    """

    LEFT_WIDTH = 25
    RIGHT_WIDTH = 25

    def __init__(self, controller: Any) -> None:
        self.controller = controller
        self.palette = controller.theme
        self.model = controller.model
        self.client = controller.client
        self.users = self.model.users
        self.pinned_streams = self.model.pinned_streams
        self.unpinned_streams = self.model.unpinned_streams
        self.write_box = WriteBox(self)
        self.search_box = SearchBox(self.controller)
        super(View, self).__init__(self.main_window())

    def left_column_view(self) -> Any:
        return LeftColumnView(View.LEFT_WIDTH, self)

    def message_view(self) -> Any:
        self.middle_column = MiddleColumnView(self, self.model, self.write_box,
                                              self.search_box)
        return urwid.LineBox(self.middle_column, bline="")

    def right_column_view(self) -> Any:
        self.users_view = RightColumnView(View.RIGHT_WIDTH, self)
        return urwid.LineBox(self.users_view,
                             title=u"Users",
                             tlcorner=u'─',
                             tline=u'─',
                             lline=u'',
                             trcorner=u'─',
                             blcorner=u'─',
                             rline=u'',
                             bline=u'',
                             brcorner=u'')

    def get_random_help(self) -> List[Any]:
        # Get a hotkey randomly from KEY_BINDINGS
        random_int = random.randint(0, len(KEY_BINDINGS) - 1)
        hotkey = list(KEY_BINDINGS.items())[random_int]
        return [
            'Help(?): ',
            ('code', ' ' + ', '.join(hotkey[1]['keys']) + ' '),
            ' ' + hotkey[1]['help_text'],  # type: ignore
        ]

    def set_footer_text(self, text_list: Optional[List[Any]] = None) -> None:
        if text_list is None:
            text = self.get_random_help()
        else:
            text = text_list
        self._w.footer.set_text(text)
        self.controller.update_screen()

    def footer_view(self) -> Any:
        text_header = self.get_random_help()
        return urwid.AttrWrap(urwid.Text(text_header), 'footer')

    def main_window(self) -> Any:
        self.left_panel = self.left_column_view()
        self.center_panel = self.message_view()
        self.right_panel = self.right_column_view()
        if self.controller.autohide:
            body = [
                (View.LEFT_WIDTH, self.left_panel),
                ('weight', 10, self.center_panel),
                (0, self.right_panel),
            ]
        else:
            body = [
                (View.LEFT_WIDTH, self.left_panel),
                ('weight', 10, self.center_panel),
                (View.RIGHT_WIDTH, self.right_panel),
            ]
        self.body = urwid.Columns(body, focus_column=0)

        div_char = '═'
        profile = self.controller.client.get_profile()

        base_url = '{uri.scheme}://{uri.netloc}/'.format(
            uri=urlparse(self.controller.client.base_url))

        title_text = " {full_name} ({email}) - {server} ".format(
            server=base_url, **profile)
        title_bar = urwid.Columns([
            urwid.Divider(div_char=div_char),
            (len(title_text), urwid.Text([title_text])),
            urwid.Divider(div_char=div_char),
        ])

        w = urwid.Frame(self.body,
                        title_bar,
                        focus_part='body',
                        footer=self.footer_view())
        return w

    def show_left_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return
        width = 25 if visible else 0
        self.body.contents[0] = (
            self.left_panel,
            self.body.options(width_type='given', width_amount=width),
        )
        if visible:
            self.body.focus_col = 0

    def show_right_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return
        width = 25 if visible else 0
        self.body.contents[2] = (
            self.right_panel,
            self.body.options(width_type='given', width_amount=width),
        )
        if visible:
            self.body.focus_col = 2

    def keypress(self, size: Tuple[int, int], key: str) -> str:
        self.model.new_user_input = True
        if self.controller.editor_mode:
            return self.controller.editor.keypress((size[1], ), key)
        # Redirect commands to message_view.
        elif is_command_key('SEARCH_MESSAGES', key) or\
                is_command_key('NEXT_UNREAD_TOPIC', key) or\
                is_command_key('NEXT_UNREAD_PM', key) or\
                is_command_key('PRIVATE_MESSAGE', key):
            self.body.focus_col = 1
            self.middle_column.keypress(size, key)
            return key
        elif is_command_key('SEARCH_PEOPLE', key):
            # Start User Search if not in editor_mode
            self.users_view.keypress(size, 'w')
            self.show_left_panel(visible=False)
            self.show_right_panel(visible=True)
            self.user_search.set_edit_text("")
            self.controller.editor_mode = True
            self.controller.editor = self.user_search
            return key
        elif is_command_key('SEARCH_STREAMS', key):
            # jump stream search
            self.left_panel.keypress(size, 'q')
            self.show_right_panel(visible=False)
            self.show_left_panel(visible=True)
            self.stream_w.search_box.set_edit_text("")
            self.controller.editor_mode = True
            self.controller.editor = self.stream_w.search_box
            return key
        elif is_command_key('HELP', key):
            # Show help menu
            self.controller.show_help()
            return key
        # replace alternate keys with arrow/functional keys
        # This is needed for navigating in widgets
        # other than message_view.
        elif is_command_key('PREVIOUS_MESSAGE', key):
            key = 'up'
        elif is_command_key('NEXT_MESSAGE', key):
            key = 'down'
        elif is_command_key('GO_LEFT', key):
            key = 'left'
        elif is_command_key('GO_RIGHT', key):
            key = 'right'
        elif is_command_key('SCROLL_TO_TOP', key):
            key = 'page up'
        elif is_command_key('SCROLL_TO_BOTTOM', key):
            key = 'page down'
        elif is_command_key('END_MESSAGE', key):
            key = 'end'
        return super(View, self).keypress(size, key)
Example #3
0
class View(urwid.WidgetWrap):
    """
    A class responsible for providing the application's interface.
    """
    def __init__(self, controller: Any) -> None:
        self.controller = controller
        self.palette = controller.theme
        self.model = controller.model
        self.users = self.model.users
        self.pinned_streams = self.model.pinned_streams
        self.unpinned_streams = self.model.unpinned_streams
        self.write_box = WriteBox(self)
        self.search_box = SearchBox(self.controller)

        self.message_view: Any = None
        self.displaying_selection_hint = False

        super().__init__(self.main_window())

    def left_column_view(self) -> Any:
        tab = TabView(
            f"{AUTOHIDE_TAB_LEFT_ARROW} STREAMS & TOPICS {AUTOHIDE_TAB_LEFT_ARROW}"
        )
        panel = LeftColumnView(self)
        return panel, tab

    def middle_column_view(self) -> Any:
        self.middle_column = MiddleColumnView(self, self.model, self.write_box,
                                              self.search_box)
        return urwid.LineBox(
            self.middle_column,
            title="Messages",
            title_attr="column_title",
            tline=COLUMN_TITLE_BAR_LINE,
            bline="",
            trcorner="│",
            tlcorner="│",
        )

    def right_column_view(self) -> Any:
        tab = TabView(
            f"{AUTOHIDE_TAB_RIGHT_ARROW} USERS {AUTOHIDE_TAB_RIGHT_ARROW}")
        self.users_view = RightColumnView(self)
        panel = urwid.LineBox(
            self.users_view,
            title="Users",
            title_attr="column_title",
            tlcorner=COLUMN_TITLE_BAR_LINE,
            tline=COLUMN_TITLE_BAR_LINE,
            trcorner=COLUMN_TITLE_BAR_LINE,
            lline="",
            blcorner="─",
            rline="",
            bline="",
            brcorner="",
        )
        return panel, tab

    def get_random_help(self) -> List[Any]:
        # Get random allowed hotkey (ie. eligible for being displayed as a tip)
        allowed_commands = commands_for_random_tips()
        if not allowed_commands:
            return ["Help(?): "]
        random_command = random.choice(allowed_commands)
        return [
            "Help(?): ",
            ("footer_contrast", " " + ", ".join(random_command["keys"]) + " "),
            " " + random_command["help_text"],
        ]

    @asynch
    def set_footer_text(
        self,
        text_list: Optional[List[Any]] = None,
        style: str = "footer",
        duration: Optional[float] = None,
    ) -> None:
        # Avoid updating repeatedly (then pausing and showing default text)
        # This is simple, though doesn't avoid starting one thread for each call
        if text_list == self._w.footer.text:
            return

        if text_list is None:
            text = self.get_random_help()
        else:
            text = text_list
        self.frame.footer.set_text(text)
        self.frame.footer.set_attr_map({None: style})
        self.controller.update_screen()
        if duration is not None:
            assert duration > 0
            time.sleep(duration)
            self.set_footer_text()

    @asynch
    def set_typeahead_footer(self, suggestions: List[str],
                             state: Optional[int], is_truncated: bool) -> None:
        if suggestions:
            # Wrap by space.
            footer_text: List[Any] = [" " + s + " " for s in suggestions]
            if state is not None:
                footer_text[state] = ("footer_contrast", footer_text[state])
            if is_truncated:
                footer_text += [" [more] "]
            footer_text.insert(0, [" "])  # Add leading space.
        else:
            footer_text = [" [No matches found]"]

        self.set_footer_text(footer_text)

    def footer_view(self) -> Any:
        text_header = self.get_random_help()
        return urwid.AttrWrap(urwid.Text(text_header), "footer")

    def main_window(self) -> Any:
        self.left_panel, self.left_tab = self.left_column_view()
        self.center_panel = self.middle_column_view()
        self.right_panel, self.right_tab = self.right_column_view()
        if self.controller.autohide:
            body = [
                (TAB_WIDTH, self.left_tab),
                ("weight", 10, self.center_panel),
                (TAB_WIDTH, self.right_tab),
            ]
        else:
            body = [
                (LEFT_WIDTH, self.left_panel),
                ("weight", 10, self.center_panel),
                (RIGHT_WIDTH, self.right_panel),
            ]
        self.body = urwid.Columns(body, focus_column=0)

        # NOTE: message_view is None, but middle_column_view is called above
        # and sets it.
        assert self.message_view is not None
        # NOTE: set_focus_changed_callback is actually called before the
        # focus is set, so the message is not read yet, it will be read when
        # the focus is changed again either vertically or horizontally.
        self.body._contents.set_focus_changed_callback(
            self.message_view.read_message)

        title_text = " {full_name} ({email}) - {server_name} ({url}) ".format(
            full_name=self.model.user_full_name,
            email=self.model.user_email,
            server_name=self.model.server_name,
            url=self.model.server_url,
        )

        title_bar = urwid.Columns([
            urwid.Divider(div_char=APPLICATION_TITLE_BAR_LINE),
            (len(title_text), urwid.Text([title_text])),
            urwid.Divider(div_char=APPLICATION_TITLE_BAR_LINE),
        ])

        self.frame = urwid.Frame(self.body,
                                 title_bar,
                                 focus_part="body",
                                 footer=self.footer_view())

        # Show left panel on startup in autohide mode
        self.show_left_panel(visible=True)

        return self.frame

    def show_left_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return

        if visible:
            self.frame.body = urwid.Overlay(
                urwid.Columns([(LEFT_WIDTH, self.left_panel),
                               (1, urwid.SolidFill("▏"))]),
                self.body,
                align="left",
                width=LEFT_WIDTH + 1,
                valign="top",
                height=("relative", 100),
            )
        else:
            self.frame.body = self.body
            # FIXME: This can be avoided after fixing the "sacrificing 1st
            # unread msg" issue and setting focus_column=1 when initializing.
            self.body.focus_position = 1

    def show_right_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return

        if visible:
            self.frame.body = urwid.Overlay(
                urwid.Columns([(1, urwid.SolidFill("▕")),
                               (RIGHT_WIDTH, self.right_panel)]),
                self.body,
                align="right",
                width=RIGHT_WIDTH + 1,
                valign="top",
                height=("relative", 100),
            )
        else:
            self.frame.body = self.body
            # FIXME: This can be avoided after fixing the "sacrificing 1st
            # unread msg" issue and setting focus_column=1 when initializing.
            self.body.focus_position = 1

    def keypress(self, size: urwid_Box, key: str) -> Optional[str]:
        self.model.new_user_input = True
        if self.controller.is_in_editor_mode():
            return self.controller.current_editor().keypress((size[1], ), key)
        # Redirect commands to message_view.
        elif (is_command_key("SEARCH_MESSAGES", key)
              or is_command_key("NEXT_UNREAD_TOPIC", key)
              or is_command_key("NEXT_UNREAD_PM", key)
              or is_command_key("STREAM_MESSAGE", key)
              or is_command_key("PRIVATE_MESSAGE", key)):
            self.show_left_panel(visible=False)
            self.show_right_panel(visible=False)
            self.body.focus_col = 1
            self.middle_column.keypress(size, key)
            return key
        elif is_command_key("ALL_PM", key):
            self.pm_button.activate(key)
        elif is_command_key("ALL_STARRED", key):
            self.starred_button.activate(key)
        elif is_command_key("ALL_MENTIONS", key):
            self.mentioned_button.activate(key)
        elif is_command_key("SEARCH_PEOPLE", key):
            # Start User Search if not in editor_mode
            self.show_left_panel(visible=False)
            self.show_right_panel(visible=True)
            self.body.focus_position = 2
            self.users_view.keypress(size, key)
            return key
        elif is_command_key("SEARCH_STREAMS", key) or is_command_key(
                "SEARCH_TOPICS", key):
            # jump stream search
            self.show_right_panel(visible=False)
            self.show_left_panel(visible=True)
            self.body.focus_position = 0
            self.left_panel.keypress(size, key)
            return key
        elif is_command_key("OPEN_DRAFT", key):
            saved_draft = self.model.session_draft_message()
            if saved_draft:
                self.show_left_panel(visible=False)
                self.show_right_panel(visible=False)
                if saved_draft["type"] == "stream":
                    stream_id = self.model.stream_id_from_name(
                        saved_draft["to"])
                    self.write_box.stream_box_view(
                        caption=saved_draft["to"],
                        title=saved_draft["subject"],
                        stream_id=stream_id,
                    )
                elif saved_draft["type"] == "private":
                    recipient_user_ids = saved_draft["to"]
                    self.write_box.private_box_view(
                        recipient_user_ids=recipient_user_ids, )
                content = saved_draft["content"]
                self.write_box.msg_write_box.edit_text = content
                self.write_box.msg_write_box.edit_pos = len(content)
                self.body.focus_col = 1
                self.middle_column.set_focus("footer")
            else:
                self.controller.report_error(
                    "No draft message was saved in this session.")
            return key
        elif is_command_key("ABOUT", key):
            self.controller.show_about()
            return key
        elif is_command_key("HELP", key):
            # Show help menu
            self.controller.show_help()
            return key
        elif is_command_key("MARKDOWN_HELP", key):
            self.controller.show_markdown_help()
            return key
        return super().keypress(size, key)

    def mouse_event(self, size: urwid_Box, event: str, button: int, col: int,
                    row: int, focus: bool) -> bool:
        if event == "mouse drag":
            self.model.controller.view.set_footer_text(
                [
                    "Try pressing ",
                    ("footer_contrast", f" {MOUSE_SELECTION_KEY} "),
                    " and dragging to select text.",
                ],
                "task:warning",
            )
            self.displaying_selection_hint = True
        elif event == "mouse release" and self.displaying_selection_hint:
            self.model.controller.view.set_footer_text()
            self.displaying_selection_hint = False

        return super().mouse_event(size, event, button, col, row, focus)
Example #4
0
class View(urwid.WidgetWrap):
    """
    A class responsible for providing the application's interface.
    """

    LEFT_WIDTH = 27
    RIGHT_WIDTH = 23

    def __init__(self, controller: Any) -> None:
        self.controller = controller
        self.palette = controller.theme
        self.model = controller.model
        self.users = self.model.users
        self.pinned_streams = self.model.pinned_streams
        self.unpinned_streams = self.model.unpinned_streams
        self.write_box = WriteBox(self)
        self.search_box = SearchBox(self.controller)

        self.message_view: Any = None

        super().__init__(self.main_window())

    def left_column_view(self) -> Any:
        return LeftColumnView(self)

    def middle_column_view(self) -> Any:
        self.middle_column = MiddleColumnView(self, self.model, self.write_box,
                                              self.search_box)
        return urwid.LineBox(
            self.middle_column,
            title="Messages",
            title_attr="column_title",
            tline=COLUMN_TITLE_BAR_LINE,
            bline="",
            trcorner="│",
            tlcorner="│",
        )

    def right_column_view(self) -> Any:
        self.users_view = RightColumnView(self)
        return urwid.LineBox(
            self.users_view,
            title="Users",
            title_attr="column_title",
            tlcorner=COLUMN_TITLE_BAR_LINE,
            tline=COLUMN_TITLE_BAR_LINE,
            trcorner=COLUMN_TITLE_BAR_LINE,
            lline="",
            blcorner="─",
            rline="",
            bline="",
            brcorner="",
        )

    def get_random_help(self) -> List[Any]:
        # Get random allowed hotkey (ie. eligible for being displayed as a tip)
        allowed_commands = commands_for_random_tips()
        if not allowed_commands:
            return ["Help(?): "]
        random_command = random.choice(allowed_commands)
        return [
            "Help(?): ",
            ("footer_contrast", " " + ", ".join(random_command["keys"]) + " "),
            " " + random_command["help_text"],
        ]

    @asynch
    def set_footer_text(
        self,
        text_list: Optional[List[Any]] = None,
        style: str = "footer",
        duration: Optional[float] = None,
    ) -> None:
        if text_list is None:
            text = self.get_random_help()
        else:
            text = text_list
        self._w.footer.set_text(text)
        self._w.footer.set_attr_map({None: style})
        self.controller.update_screen()
        if duration is not None:
            assert duration > 0
            time.sleep(duration)
            self.set_footer_text()

    @asynch
    def set_typeahead_footer(self, suggestions: List[str],
                             state: Optional[int], is_truncated: bool) -> None:
        if suggestions:
            # Wrap by space.
            footer_text: List[Any] = [" " + s + " " for s in suggestions]
            if state is not None:
                footer_text[state] = ("footer_contrast", footer_text[state])
            if is_truncated:
                footer_text += [" [more] "]
            footer_text.insert(0, [" "])  # Add leading space.
        else:
            footer_text = [" [No matches found]"]

        self.set_footer_text(footer_text)

    def footer_view(self) -> Any:
        text_header = self.get_random_help()
        return urwid.AttrWrap(urwid.Text(text_header), "footer")

    def main_window(self) -> Any:
        self.left_panel = self.left_column_view()
        self.center_panel = self.middle_column_view()
        self.right_panel = self.right_column_view()
        if self.controller.autohide:
            body = [
                (View.LEFT_WIDTH, self.left_panel),
                ("weight", 10, self.center_panel),
                (0, self.right_panel),
            ]
        else:
            body = [
                (View.LEFT_WIDTH, self.left_panel),
                ("weight", 10, self.center_panel),
                (View.RIGHT_WIDTH, self.right_panel),
            ]
        self.body = urwid.Columns(body, focus_column=0)

        # NOTE: message_view is None, but middle_column_view is called above
        # and sets it.
        assert self.message_view is not None
        # NOTE: set_focus_changed_callback is actually called before the
        # focus is set, so the message is not read yet, it will be read when
        # the focus is changed again either vertically or horizontally.
        self.body._contents.set_focus_changed_callback(
            self.message_view.read_message)

        title_text = " {full_name} ({email}) - {server_name} ({url}) ".format(
            full_name=self.model.user_full_name,
            email=self.model.user_email,
            server_name=self.model.server_name,
            url=self.model.server_url,
        )

        title_bar = urwid.Columns([
            urwid.Divider(div_char=APPLICATION_TITLE_BAR_LINE),
            (len(title_text), urwid.Text([title_text])),
            urwid.Divider(div_char=APPLICATION_TITLE_BAR_LINE),
        ])

        w = urwid.Frame(self.body,
                        title_bar,
                        focus_part="body",
                        footer=self.footer_view())
        return w

    def show_left_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return
        width = View.LEFT_WIDTH if visible else 0
        self.body.contents[0] = (
            self.left_panel,
            self.body.options(width_type="given", width_amount=width),
        )
        if visible:
            self.body.focus_position = 0

    def show_right_panel(self, *, visible: bool) -> None:
        if not self.controller.autohide:
            return
        width = View.RIGHT_WIDTH if visible else 0
        self.body.contents[2] = (
            self.right_panel,
            self.body.options(width_type="given", width_amount=width),
        )
        if visible:
            self.body.focus_position = 2

    def keypress(self, size: urwid_Box, key: str) -> Optional[str]:
        self.model.new_user_input = True
        if self.controller.is_in_editor_mode():
            return self.controller.current_editor().keypress((size[1], ), key)
        # Redirect commands to message_view.
        elif (is_command_key("SEARCH_MESSAGES", key)
              or is_command_key("NEXT_UNREAD_TOPIC", key)
              or is_command_key("NEXT_UNREAD_PM", key)
              or is_command_key("STREAM_MESSAGE", key)
              or is_command_key("PRIVATE_MESSAGE", key)):
            self.body.focus_col = 1
            self.middle_column.keypress(size, key)
            return key
        elif is_command_key("ALL_PM", key):
            self.model.controller.narrow_to_all_pm()
            self.body.focus_col = 1
        elif is_command_key("ALL_STARRED", key):
            self.model.controller.narrow_to_all_starred()
            self.body.focus_col = 1
        elif is_command_key("ALL_MENTIONS", key):
            self.model.controller.narrow_to_all_mentions()
            self.body.focus_col = 1
        elif is_command_key("SEARCH_PEOPLE", key):
            # Start User Search if not in editor_mode
            self.body.focus_position = 2
            self.users_view.keypress(size, key)
            self.show_left_panel(visible=False)
            self.show_right_panel(visible=True)
            self.controller.enter_editor_mode_with(self.user_search)
            return key
        elif is_command_key("SEARCH_STREAMS", key) or is_command_key(
                "SEARCH_TOPICS", key):
            # jump stream search
            self.body.focus_position = 0
            self.left_panel.keypress(size, key)
            self.show_right_panel(visible=False)
            self.show_left_panel(visible=True)
            if self.left_panel.is_in_topic_view:
                search_box = self.topic_w.topic_search_box
            else:
                search_box = self.stream_w.stream_search_box
            self.controller.enter_editor_mode_with(search_box)
            return key
        elif is_command_key("OPEN_DRAFT", key):
            saved_draft = self.model.session_draft_message()
            if saved_draft:
                if saved_draft["type"] == "stream":
                    stream_id = self.model.stream_id_from_name(
                        saved_draft["to"])
                    self.write_box.stream_box_view(
                        caption=saved_draft["to"],
                        title=saved_draft["subject"],
                        stream_id=stream_id,
                    )
                elif saved_draft["type"] == "private":
                    email_list = saved_draft["to"]
                    recipient_user_ids = [
                        self.model.user_dict[email.strip()]["user_id"]
                        for email in email_list
                    ]
                    self.write_box.private_box_view(
                        emails=email_list,
                        recipient_user_ids=recipient_user_ids,
                    )
                content = saved_draft["content"]
                self.write_box.msg_write_box.edit_text = content
                self.write_box.msg_write_box.edit_pos = len(content)
                self.body.focus_col = 1
                self.middle_column.set_focus("footer")
            else:
                self.controller.report_error(
                    "No draft message was saved in this session.")
            return key
        elif is_command_key("ABOUT", key):
            self.controller.show_about()
            return key
        elif is_command_key("HELP", key):
            # Show help menu
            self.controller.show_help()
            return key
        # replace alternate keys with arrow/functional keys
        # This is needed for navigating in widgets
        # other than message_view.
        elif is_command_key("GO_UP", key):
            key = "up"
        elif is_command_key("GO_DOWN", key):
            key = "down"
        elif is_command_key("GO_LEFT", key):
            key = "left"
        elif is_command_key("GO_RIGHT", key):
            key = "right"
        elif is_command_key("SCROLL_UP", key):
            key = "page up"
        elif is_command_key("SCROLL_DOWN", key):
            key = "page down"
        elif is_command_key("GO_TO_BOTTOM", key):
            key = "end"
        return super().keypress(size, key)
Example #5
0
class View(urwid.WidgetWrap):
    """
    A class responsible for providing the application's interface.
    """
    palette = {
        'default': [
                (None,           'light gray',    'black'),
                ('selected',     'light magenta', 'dark blue'),
                ('msg_selected', 'light red',     'black'),
                ('header',       'dark cyan',     'dark blue',  'bold'),
                ('custom',       'white',         'dark blue',  'underline'),
                ('content',      'white',         'black',      'standout'),
                ('name',         'yellow',        'black'),
                ('unread',       'black',         'light gray'),
                ('active',       'white',         'black'),
                ('idle',         'yellow',        'black')
                ],
        'light': [
                (None,           'black',        'white'),
                ('selected',     'white',        'dark blue'),
                ('msg_selected', 'dark blue',    'light gray'),
                ('header',       'white',        'dark blue',  'bold'),
                ('custom',       'white',        'dark blue',  'underline'),
                ('content',      'black',        'light gray', 'standout'),
                ('name',         'dark magenta', 'light gray', 'bold'),
                ],
        'blue': [
                (None,           'black',        'light blue'),
                ('selected',     'white',        'dark blue'),
                ('msg_selected', 'black',        'light gray'),
                ('header',       'black',        'dark blue',  'bold'),
                ('custom',       'white',        'dark blue',  'underline'),
                ('content',      'black',        'light gray', 'standout'),
                ('name',         'dark red',     'light gray', 'bold'),
                ]
                }

    def __init__(self, controller: Any) -> None:
        self.controller = controller
        self.model = controller.model
        self.client = controller.client
        self.users = self.model.users
        self.streams = self.model.streams
        self.write_box = WriteBox(self)
        self.search_box = SearchBox(self.controller)
        super(View, self).__init__(self.main_window())

    def menu_view(self) -> Any:
        count = self.model.unread_counts.get('all_msg', 0)
        self.home_button = HomeButton(self.controller, count=count)
        count = self.model.unread_counts.get('all_pms', 0)
        self.pm_button = PMButton(self.controller, count=count)
        menu_btn_list = [
            self.home_button,
            self.pm_button,
            ]
        w = urwid.ListBox(urwid.SimpleFocusListWalker(menu_btn_list))
        return w

    def streams_view(self) -> Any:
        streams_btn_list = list()
        for stream in self.streams:
            unread_count = self.model.unread_counts.get(stream[1], 0)
            streams_btn_list.append(
                    StreamButton(
                        stream,
                        controller=self.controller,
                        view=self,
                        count=unread_count,
                    )
            )
        self.stream_w = StreamsView(streams_btn_list)
        w = urwid.LineBox(self.stream_w, title="Streams")
        return w

    def left_column_view(self) -> Any:
        left_column_structure = [
            (4, self.menu_view()),
            self.streams_view(),
        ]
        w = urwid.Pile(left_column_structure)
        return w

    def message_view(self) -> Any:
        self.middle_column = MiddleColumnView(self.model, self.write_box,
                                              self.search_box)
        w = urwid.LineBox(self.middle_column)
        return w

    def right_column_view(self) -> Any:
        self.users_view = RightColumnView(self)
        w = urwid.LineBox(self.users_view, title=u"Users")
        return w

    def main_window(self) -> Any:
        left_column = self.left_column_view()
        center_column = self.message_view()
        right_column = self.right_column_view()
        body = [
            ('weight', 3, left_column),
            ('weight', 10, center_column),
            ('weight', 3, right_column),
        ]
        self.body = urwid.Columns(body, focus_column=1)
        w = urwid.LineBox(self.body, title=u"Zulip")
        return w

    def keypress(self, size: Tuple[int, int], key: str) -> str:
        if self.controller.editor_mode:
            return self.controller.editor.keypress((20,), key)

        elif key == "w":
            # Start User Search if not in editor_mode
            self.users_view.keypress(size, 'w')
            self.body.focus_col = 2
            self.user_search.set_edit_text("")
            self.controller.editor_mode = True
            self.controller.editor = self.user_search
            return key

        else:
            return super(View, self).keypress(size, get_key(key))
Example #6
0
class View(urwid.WidgetWrap):
    """
    A class responsible for providing the application's interface.
    """
    palette = {
        'default':
        [(None, 'white', 'black'), ('selected', 'light magenta', 'dark blue'),
         ('msg_selected', 'light green', 'black'),
         ('header', 'dark cyan', 'dark blue', 'bold'),
         ('custom', 'white', 'dark blue', 'underline'),
         ('content', 'white', 'black', 'standout'),
         ('name', 'yellow, bold', 'black'), ('unread', 'light blue', 'black'),
         ('active', 'white', 'black'), ('idle', 'yellow', 'black'),
         ('title', 'white, bold', 'black'), ('time', 'light blue', 'black'),
         ('bar', 'white', 'dark gray'), ('emoji', 'light magenta', 'black'),
         ('span', 'light red, bold', 'black'), ('link', 'light blue', 'black'),
         ('blockquote', 'brown', 'black'), ('code', 'black', 'white'),
         ('bold', 'white, bold', 'black'),
         ('footer', 'white', 'dark red', 'bold')],
        'light': [
            (None, 'black', 'white'),
            ('selected', 'white', 'dark blue'),
            ('msg_selected', 'dark blue', 'light gray'),
            ('header', 'white', 'dark blue', 'bold'),
            ('custom', 'white', 'dark blue', 'underline'),
            ('content', 'black', 'light gray', 'standout'),
            ('name', 'dark magenta', 'light gray', 'bold'),
        ],
        'blue': [
            (None, 'black', 'light blue'),
            ('selected', 'white', 'dark blue'),
            ('msg_selected', 'black', 'light gray'),
            ('header', 'black', 'dark blue', 'bold'),
            ('custom', 'white', 'dark blue', 'underline'),
            ('content', 'black', 'light gray', 'standout'),
            ('name', 'dark red', 'light gray', 'bold'),
        ]
    }

    def __init__(self, controller: Any) -> None:
        self.controller = controller
        self.model = controller.model
        self.client = controller.client
        self.users = self.model.users
        self.streams = self.model.streams
        self.write_box = WriteBox(self)
        self.search_box = SearchBox(self.controller)
        super(View, self).__init__(self.main_window())

    def left_column_view(self) -> Any:
        self.left_col_w = LeftColumnView(self)
        return self.left_col_w

    def message_view(self) -> Any:
        self.middle_column = MiddleColumnView(self.model, self.write_box,
                                              self.search_box)
        w = urwid.LineBox(self.middle_column, bline="")
        return w

    def right_column_view(self) -> Any:
        self.users_view = RightColumnView(self)
        w = urwid.LineBox(self.users_view,
                          title=u"Users",
                          tlcorner=u'─',
                          tline=u'─',
                          lline=u'',
                          trcorner=u'─',
                          blcorner=u'─',
                          rline=u'',
                          bline=u'',
                          brcorner=u'')
        return w

    def get_random_help(self) -> List[Any]:
        # Get a hotkey randomly from KEY_BINDINGS
        random_int = random.randint(0, len(KEY_BINDINGS) - 1)
        hotkey = list(KEY_BINDINGS.items())[random_int]
        return [
            'Help(?): ',
            ('code', ' ' + ', '.join(hotkey[1]['keys']) + ' '),
            ' ' + hotkey[1]['help_text'],  # type: ignore
        ]

    def footer_view(self) -> Any:
        text_header = self.get_random_help()
        return urwid.AttrWrap(urwid.Text(text_header), 'footer')

    def handle_typing_event(self, event: Dict['str', Any]) -> None:
        # If the user is in pm narrow with the person typing
        if len(self.model.narrow) == 1 and\
                self.model.narrow[0][0] == 'pm_with' and\
                event['sender']['email'] in self.model.narrow[0][1].split(','):
            if event['op'] == 'start':
                user = self.model.user_dict[event['sender']['email']]
                self._w.footer.set_text(
                    [' ', ('code', user['full_name']), ' is typing...'])
                self.controller.update_screen()
            elif event['op'] == 'stop':
                self._w.footer.set_text(self.get_random_help())
                self.controller.update_screen()

    def main_window(self) -> Any:
        self.left_column = self.left_column_view()
        self.center_column = self.message_view()
        self.right_column = self.right_column_view()
        body = [
            (0, self.left_column),
            ('weight', 10, self.center_column),
            (0, self.right_column),
        ]
        self.body = urwid.Columns(body, focus_column=1)

        div_char = '═'
        profile = self.controller.client.get_profile()

        base_url = '{uri.scheme}://{uri.netloc}/'.format(
            uri=urlparse(self.controller.client.base_url))

        title_text = " {full_name} ({email}) - {server} ".format(
            server=base_url, **profile)
        title_bar = urwid.Columns([
            urwid.Divider(div_char=div_char),
            (len(title_text), urwid.Text([title_text])),
            urwid.Divider(div_char=div_char),
        ])

        w = urwid.Frame(self.body,
                        title_bar,
                        focus_part='body',
                        footer=self.footer_view())
        return w

    def toggle_left_panel(self) -> None:
        self.body.contents[0] = (
            self.left_column,
            self.body.options(width_type='given', width_amount=0),
        )
        self.body.focus_col = 1

    def keypress(self, size: Tuple[int, int], key: str) -> str:
        self.model.new_user_input = True
        if self.controller.editor_mode:
            return self.controller.editor.keypress((size[1], ), key)
        # Redirect commands to message_view.
        elif is_command_key('GO_BACK', key):
            self.toggle_left_panel()
        elif is_command_key('SEARCH_MESSAGES', key) or\
                is_command_key('NEXT_UNREAD_TOPIC', key) or\
                is_command_key('NEXT_UNREAD_PM', key) or\
                is_command_key('PRIVATE_MESSAGE', key):
            self.body.focus_col = 1
            self.middle_column.keypress(size, key)
            return key
        elif is_command_key('SEARCH_PEOPLE', key):
            self.body.contents[0] = (
                self.right_column,
                self.body.options(width_type='given', width_amount=25),
            )
            # Start User Search if not in editor_mode
            self.users_view.keypress(size, 'w')
            self.body.focus_col = 0
            self.user_search.set_edit_text("")
            self.controller.editor_mode = True
            self.controller.editor = self.user_search
            return key
        elif is_command_key('SEARCH_STREAMS', key):
            self.body.contents[0] = (
                self.left_column,
                self.body.options(width_type='given', width_amount=25),
            )
            # jump stream search
            self.left_col_w.keypress(size, 'q')
            self.body.focus_col = 0
            self.stream_w.search_box.set_edit_text("")
            self.controller.editor_mode = True
            self.controller.editor = self.stream_w.search_box
            return key
        elif is_command_key('HELP', key):
            # Show help menu
            self.controller.show_help()
            return key
        # replace alternate keys with arrow/functional keys
        # This is needed for navigating in widgets
        # other than message_view.
        elif is_command_key('PREVIOUS_MESSAGE', key):
            key = 'up'
        elif is_command_key('NEXT_MESSAGE', key):
            key = 'down'
        elif is_command_key('GO_LEFT', key):
            key = 'left'
        elif is_command_key('GO_RIGHT', key):
            key = 'right'
        elif is_command_key('SCROLL_TO_TOP', key):
            key = 'page up'
        elif is_command_key('SCROLL_TO_BOTTOM', key):
            key = 'page down'
        elif is_command_key('END_MESSAGE', key):
            key = 'end'
        return super(View, self).keypress(size, key)