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)
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 = None # type: Any super().__init__(self.main_window()) def left_column_view(self) -> Any: return LeftColumnView(View.LEFT_WIDTH, 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', tline=LIST_TITLE_BAR_LINE, bline='', 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=LIST_TITLE_BAR_LINE, tline=LIST_TITLE_BAR_LINE, trcorner=LIST_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(?): ', ('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() @asynch def set_typeahead_footer(self, suggestions: List[str], state: Optional[int], is_truncated: bool) -> None: if suggestions: # Wrap by space. footer_text = [' ' + s + ' ' for s in suggestions] # type: List[Any] if state is not None: footer_text[state] = ('code', 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 # 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('OPEN_DRAFT', key): saved_draft = self.model.session_draft_message() if saved_draft: if saved_draft['type'] == 'stream': self.write_box.stream_box_view( caption=saved_draft['display_recipient'], title=saved_draft['subject'], stream_id=saved_draft['stream_id'], ) elif saved_draft['type'] == 'private': email_txt = saved_draft['display_recipient'] recipient_user_ids = [ self.model.user_dict[email.strip()]['user_id'] for email in email_txt.split(",") ] self.write_box.private_box_view( email=email_txt, 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.middle_column.set_focus('footer') else: self.set_footer_text( 'No draft message was saved in' ' this session.', 3) 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)
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=u'Messages', bline=u'', tline=u'━', trcorner=u'│', tlcorner=u'│') 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 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 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('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('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.editor_mode = True self.controller.editor = 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.editor_mode = True self.controller.editor = 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().keypress(size, key)
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)
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)
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)