Skip to content

Introduce help context #1484

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

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
de8d7a6
ui: Make the Help hotkey suggestion in the footer more conventional.
Niloth-p Jul 10, 2024
9fccce3
refactor: ui/core/boxes: Separate out function to reset footer text.
Niloth-p Jul 15, 2024
13410a2
refactor: ui/core: Separate footer events from set_footer_text().
Niloth-p Jul 15, 2024
b9b5ee8
ui: Add state variable to track live footer events.
Niloth-p Jul 15, 2024
af00b47
lint-hotkeys: Handle the doc file does not exist exception.
Niloth-p Jul 15, 2024
d2e0c17
refactor: lint-hotkeys: Remove the write_hotkeys_file() function.
Niloth-p Jul 15, 2024
4e8783c
refactor: lint-hotkeys: Make int error flag boolean & use SystemExit.
Niloth-p Jul 15, 2024
e954870
lint-hotkeys: Rewrite function docstrings and comments.
Niloth-p Jul 15, 2024
4ed23c7
refactor: lint-hotkeys: Rename get_hotkeys_file_string().
Niloth-p Jul 15, 2024
9717776
refactor: lint-hotkeys: Rename ambiguous keyword "action" to "batch".
Niloth-p Jul 15, 2024
11a7ead
refactor: lint-hotkeys: Improve variable naming of generated dict.
Niloth-p Jul 17, 2024
3237251
lint-hotkeys: Refactor the flow by creating new ENTRY_BY_CATEGORIES.
Niloth-p Jul 15, 2024
188bd7f
lint-hotkeys: Delay generation of file string when linting.
Niloth-p Jul 17, 2024
0dd8aa4
refactor: lint-hotkeys: Split out the help text linting function.
Niloth-p Jul 17, 2024
5286488
refactor: lint-hotkeys: Split out the function that lints categories.
Niloth-p Jul 17, 2024
4e7a4f2
lint-hotkeys: Restructure the error messages of lint_help_text().
Niloth-p Jul 17, 2024
efc8f82
lint-hotkeys: Lint for typos in key_category values.
Niloth-p Jul 17, 2024
ac8e715
refactor: lint-hotkeys: Use help_group instead of HELP_CATEGORIES.
Niloth-p Jul 17, 2024
58ec337
refactor: lint-hotkeys: Create generic variable entries_by_group.
Niloth-p Jul 17, 2024
3ce52db
refactor: lint-hotkeys: Create generic variables for key_category.
Niloth-p Jul 17, 2024
4911685
refactor: lint-hotkeys: Replace 'category(ies)' with 'group(s)'.
Niloth-p Jul 17, 2024
b35e0ce
refactor: lint-hotkeys: Delete constant OUTPUT_FILE_NAME, compute it.
Niloth-p Jul 17, 2024
001814b
refactor: lint-hotkeys: Replace usage of OUTPUT_FILE.
Niloth-p Jul 17, 2024
e2c7e57
refactor: lint-hotkeys: Generalize the traversal of key group values.
Niloth-p Jul 17, 2024
f908dc6
refactor: lint-hotkeys: Use bundled group values.
Niloth-p Jul 18, 2024
f5d0bc4
keys: Add a contexts field to Key Binding.
Niloth-p Jul 8, 2024
a8dd4de
lint-hotkeys: Add argument to generate a keys file grouped by contexts.
Niloth-p Jul 18, 2024
a76da7f
keys: Use the help contexts in generating random help tips.
Niloth-p Jul 8, 2024
9c48eb7
keys: Include previously excluded random help hints.
Niloth-p Apr 16, 2024
2e1b0f0
ui: Enable footer texts to display contextual help tips.
Niloth-p Jul 16, 2024
1b6ad6b
ui/keys: Display the context of the footer hint.
Niloth-p Jul 10, 2024
f75a309
contexts/core/ui: Track the currently focused widget in the UI.
Niloth-p Jul 11, 2024
58b1a96
core/ui/views/keys: Add a Contextual Help Menu.
Niloth-p Jul 10, 2024
3513d73
keys/views: Add mapping of contexts to broader contexts they belong to.
Niloth-p Jul 16, 2024
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
1 change: 1 addition & 0 deletions docs/developer-file-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Zulip Terminal uses [Zulip's API](https://zulip.com/api/) to store and retrieve
| Folder | File | Description |
| ---------------------- | ------------------- | ----------------------------------------------------------------------------------------|
| zulipterminal | api_types.py | Types from the Zulip API, translated into python, to improve type checking |
| | contexts.py | Tracks the currently focused widget in the UI |
| | core.py | Defines the `Controller`, which sets up the `Model`, `View`, and how they interact |
| | helper.py | Helper functions used in multiple places |
| | model.py | Defines the `Model`, fetching and storing data retrieved from the Zulip server |
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def controller(self, mocker: MockerFixture) -> Controller:
self.poll_for_events = mocker.patch(MODEL + ".poll_for_events")
mocker.patch(MODULE + ".Controller.show_loading")
self.main_loop = mocker.patch(
MODULE + ".urwid.MainLoop", return_value=mocker.Mock()
MODULE + ".FocusTrackingMainLoop", return_value=mocker.Mock()
)

self.config_file = "path/to/zuliprc"
Expand Down
125 changes: 125 additions & 0 deletions zulipterminal/contexts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
Tracks the currently focused widget in the UI
"""
import re
from typing import Dict, List, Optional, Tuple, Union

import urwid

from zulipterminal.config.themes import ThemeSpec


# These include contexts that do not have any help hints as well,
# for the sake of completeness
AUTOHIDE_PREFIXES: Dict[Tuple[Union[int, str], ...], str] = {
(1, 0, 0): "menu_button",
(1, 0, 1, "body"): "stream_topic_button",
(1, 0, 1, "header"): "left_panel_search_box",
(1, "body"): "message_box",
(1, "header"): "message_search_box",
(1, "footer"): "compose_box",
(1, 1, "header"): "user_search_box",
(1, 1, "body"): "user_button",
}

NON_AUTOHIDE_PREFIXES: Dict[Tuple[Union[int, str], ...], str] = {
(0, 0): "menu_button",
(0, 1, "body"): "stream_topic_button",
(0, 1, "header"): "left_panel_search_box",
(1, "body"): "message_box",
(1, "header"): "message_search_box",
(1, "footer"): "compose_box",
(2, "header"): "user_search_box",
(2, "body"): "user_button",
}


class FocusTrackingMainLoop(urwid.MainLoop):
def __init__(
self,
widget: urwid.Widget,
palette: ThemeSpec,
screen: Optional[urwid.BaseScreen],
) -> None:
super().__init__(widget, palette, screen)
self.previous_focus_path = None
self.view = widget

def process_input(self, input: List[str]) -> None:
super().process_input(input)
self.track_focus_change()

def track_focus_change(self) -> None:
Comment on lines +37 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem straightforward to track focus-change events via a derived class here, to ensure it is called after each input event 👍

However, while it may be due to the other complexity, I'm not clear if we need the rest of the code to explicitly be in the loop class?

focus_path = self.widget.get_focus_path()
if focus_path != self.previous_focus_path:
# Update view's context irrespective of the focused widget
self.view.context = self.get_context_name(focus_path)

self.previous_focus_path = focus_path

def get_context_name(self, focus_path: Tuple[Union[int, str]]) -> str:
widget_in_focus = self.get_widget_in_focus(focus_path)

if self.widget != self.view:
overlay_widget_to_context_map = {
"msg_info_popup": "msg_info",
"stream_info_popup": "stream_info",
"emoji_list_popup": "emoji_list",
"about_popup": "about",
}
return overlay_widget_to_context_map.get(widget_in_focus, "popup")

widget_suffix_to_context_map = {
"user_button": "user",
"message_box": "message",
"stream_topic_button": (
"topic" if self.widget.left_panel.is_in_topic_view else "stream"
),
"topic": "topic",
"compose_box": "compose_box",
"search_box": "editor",
Comment on lines +63 to +80
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wary of this approach, since this feels fragile with respect to the internal naming of the implementation. If we renamed objects (or classes?) then I get the feeling that this would need fixing-up?

Could we use get_focus_widgets with new class constants to achieve this?

"button": "button",
}
for suffix, context in widget_suffix_to_context_map.items():
if widget_in_focus.endswith(suffix):
return context

return "general"

def get_widget_in_focus(self, focus_path: Tuple[Union[int, str], ...]) -> str:
if isinstance(self.widget, urwid.Overlay):
# NoticeView and PopUpConfirmationView do not shift focus path on opening,
# until the user presses a recognized key that doesn't close the popup.
Comment on lines +91 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this affected by uniform use of show_pop_up, at least for the latter? My refactor would adjust that, though I'm not sure about the former.

if len(focus_path) > 2:
# View -> AttrMap -> Frame -> LineBox -> the named Popup
popup_widget = (
self.widget.contents[1][0].original_widget
).body.original_widget
popup_widget_class = popup_widget.__class__.__name__

if popup_widget_class == "EmojiPickerView":
popup_widget_class = (
"EmojiSearchView"
if focus_path[2] == "header"
else "EmojiListView"
)

# PascalCase to snake_case
return re.sub(
r"view",
r"popup",
re.sub(r"(?<!^)(?=[A-Z])", "_", popup_widget_class).lower(),
)

return "unrecognized_widget"

focus_path = tuple(focus_path[1:])
prefix_map = (
AUTOHIDE_PREFIXES
if self.widget.controller.autohide
else NON_AUTOHIDE_PREFIXES
)
return next(
(prefix_map[key] for key in prefix_map if focus_path[: len(key)] == key),
"unrecognized_widget",
)
3 changes: 2 additions & 1 deletion zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
MAX_LINEAR_SCALING_WIDTH,
MIN_SUPPORTED_POPUP_WIDTH,
)
from zulipterminal.contexts import FocusTrackingMainLoop
from zulipterminal.helper import asynch, suppress_output
from zulipterminal.model import Model
from zulipterminal.platform_code import PLATFORM
Expand Down Expand Up @@ -104,7 +105,7 @@ def __init__(

screen = Screen()
screen.set_terminal_properties(colors=self.color_depth)
self.loop = urwid.MainLoop(self.view, self.theme, screen=screen)
self.loop = FocusTrackingMainLoop(self.view, self.theme, screen=screen)

# urwid pipe for concurrent screen update handling
self._update_pipe = self.loop.watch_pipe(self._draw_screen)
Expand Down
5 changes: 4 additions & 1 deletion zulipterminal/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import random
import re
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple, Union

import urwid

Expand Down Expand Up @@ -182,6 +182,9 @@ def footer_view(self) -> Any:
text_header = self.get_random_help()
return urwid.AttrWrap(urwid.Text(text_header), "footer")

def get_focus_path(self) -> Tuple[Union[int, str], ...]:
return self.frame.get_focus_path()

def main_window(self) -> Any:
self.left_panel, self.left_tab = self.left_column_view()
self.center_panel = self.middle_column_view()
Expand Down