mirror of
https://github.com/markqvist/NomadNet.git
synced 2026-10-26 19:01:44 -04:00
Add a built-in Micron page/file editor under [ My Node ], extend Micron with nestable collapsible section folding and multi-row form inputs, misc changes
This commit is contained in:
parent
997bae397c
commit
61812aa6ae
10 changed files with 1459 additions and 61 deletions
|
|
@ -25,6 +25,8 @@ An masked field : `B444`<!|password`Value of Field>`b
|
|||
|
||||
An small field : `B444`<8|small`test>`b, and some more text.
|
||||
|
||||
A multi-row field : `B444`<40x4|notes`>`b
|
||||
|
||||
Two fields : `B444`<8|one`One>`b `B444`<8|two`Two>`b
|
||||
|
||||
The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!.
|
||||
|
|
|
|||
|
|
@ -169,6 +169,10 @@ GLYPHS = {
|
|||
("audio", "[~]", "\u266b", "\uf1c7"),
|
||||
("pin", "*", "\u2605", "\uf08d"),
|
||||
("copy", "[C]", "\u29c9", "\uf0c5"),
|
||||
("folder", "[+]", "\u25b8", "\uf07b"),
|
||||
("folder_open", "[-]", "\u25be", "\uf07c"),
|
||||
("fold_open", "-", "▾", "▾"),
|
||||
("fold_closed", "+", "▸", "▸"),
|
||||
}
|
||||
|
||||
class TextUI:
|
||||
|
|
|
|||
|
|
@ -347,6 +347,8 @@ class Browser:
|
|||
if target_idx is None:
|
||||
return
|
||||
|
||||
self.reveal_index(int(target_idx))
|
||||
|
||||
row_offset = self._rows_above(int(target_idx), cols)
|
||||
|
||||
try:
|
||||
|
|
@ -363,7 +365,7 @@ class Browser:
|
|||
browser_cols = screen_cols-lw-2
|
||||
cols = browser_cols
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not calculate browser width, anchors will be inaccurate: {e}", RNS.LOG_WARNING)
|
||||
RNS.trace_exception(e)
|
||||
cols = 100
|
||||
|
|
@ -373,8 +375,10 @@ class Browser:
|
|||
def _rows_above(self, index, cols):
|
||||
if index <= 0 or not self.attr_maps: return 0
|
||||
|
||||
hidden = self.hidden_indices()
|
||||
total = 0
|
||||
for i in range(min(index, len(self.attr_maps))):
|
||||
if i in hidden: continue
|
||||
try: total += self.attr_maps[i].rows((cols,))
|
||||
except Exception: total += 1
|
||||
|
||||
|
|
@ -627,6 +631,106 @@ class Browser:
|
|||
self.page_partials = {}
|
||||
self.browser_body = urwid.AttrMap(ScrollBar(Scrollable(pile, force_forward_keypress=True), thumb_char="\u2503", trough_char=" "), "scrollbar")
|
||||
self.detect_partials()
|
||||
self.init_folds()
|
||||
|
||||
# render an arbitrary markup buffer (used by the page editor preview) and
|
||||
# return the content widget, with partials/folds/fields wired as usual
|
||||
def render_markup_buffer(self, markup):
|
||||
self.status = Browser.DONE
|
||||
self.markup = markup
|
||||
self.page_background_color = None
|
||||
self.page_foreground_color = None
|
||||
for tag, attr in (("#!bg=", "page_background_color"), ("#!fg=", "page_foreground_color")):
|
||||
pos = markup.find(tag)
|
||||
if pos >= 0:
|
||||
endpos = markup.find("\n", pos)
|
||||
if endpos < 0: endpos = len(markup)
|
||||
if endpos-(pos+len(tag)) in (3, 6):
|
||||
setattr(self, attr, markup[pos+len(tag):endpos])
|
||||
try:
|
||||
self.attr_maps = markup_to_attrmaps(strip_modifiers(markup), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
||||
except Exception as e:
|
||||
self.attr_maps = [urwid.AttrMap(urwid.Text(f"Could not render preview: {e}"), "browser_inactive")]
|
||||
self.update_page_display()
|
||||
return self.browser_body
|
||||
|
||||
def init_folds(self):
|
||||
self.fold_sections = {}
|
||||
self.collapsed_headings = set()
|
||||
attrmaps = self.attr_maps
|
||||
header_levels = getattr(attrmaps, "header_levels", None) or {}
|
||||
row_levels = getattr(attrmaps, "row_levels", None) or {}
|
||||
collapsibles = getattr(attrmaps, "collapsibles", None) or {}
|
||||
if not collapsibles:
|
||||
return
|
||||
|
||||
n = len(attrmaps)
|
||||
for idx in sorted(collapsibles.keys()):
|
||||
widget = self.collapsible_widget_at(idx)
|
||||
if widget is None: continue
|
||||
depth = header_levels.get(idx, 1)
|
||||
end = n
|
||||
for i in range(idx+1, n):
|
||||
if row_levels.get(i, depth) < depth or \
|
||||
(i in header_levels and header_levels.get(i, 1) <= depth):
|
||||
end = i; break
|
||||
self.fold_sections[widget] = (idx+1, end)
|
||||
if collapsibles[idx]:
|
||||
self.collapsed_headings.add(widget)
|
||||
try: widget.set_collapsed(True)
|
||||
except Exception: pass
|
||||
|
||||
if self.collapsed_headings:
|
||||
self.rebuild_visible()
|
||||
|
||||
def collapsible_widget_at(self, idx):
|
||||
try:
|
||||
o = self.attr_maps[idx]._original_widget
|
||||
if getattr(o, "is_collapsible_heading", False): return o
|
||||
except Exception: pass
|
||||
return None
|
||||
|
||||
def hidden_indices(self):
|
||||
hidden = set()
|
||||
for widget in getattr(self, "collapsed_headings", ()):
|
||||
rng = getattr(self, "fold_sections", {}).get(widget)
|
||||
if rng: hidden.update(range(rng[0], rng[1]))
|
||||
return hidden
|
||||
|
||||
def rebuild_visible(self):
|
||||
pile = getattr(self, "page_pile", None)
|
||||
if pile is None: return
|
||||
hidden = self.hidden_indices()
|
||||
opts = pile.options()
|
||||
pile.contents = [(w, opts) for i, w in enumerate(self.attr_maps) if i not in hidden]
|
||||
try:
|
||||
if pile.focus_position >= len(pile.contents):
|
||||
pile.focus_position = max(0, len(pile.contents)-1)
|
||||
except Exception: pass
|
||||
|
||||
def fold_changed(self, heading_widget):
|
||||
if getattr(heading_widget, "collapsed", False):
|
||||
self.collapsed_headings.add(heading_widget)
|
||||
else:
|
||||
self.collapsed_headings.discard(heading_widget)
|
||||
self.rebuild_visible()
|
||||
try:
|
||||
for pos, (w, _) in enumerate(self.page_pile.contents):
|
||||
if getattr(w, "_original_widget", None) is heading_widget:
|
||||
self.page_pile.focus_position = pos
|
||||
break
|
||||
except Exception: pass
|
||||
|
||||
def reveal_index(self, idx):
|
||||
changed = False
|
||||
for widget in list(getattr(self, "collapsed_headings", ())):
|
||||
rng = getattr(self, "fold_sections", {}).get(widget)
|
||||
if rng and rng[0] <= idx < rng[1]:
|
||||
self.collapsed_headings.discard(widget)
|
||||
try: widget.set_collapsed(False)
|
||||
except Exception: pass
|
||||
changed = True
|
||||
if changed: self.rebuild_visible()
|
||||
|
||||
def parse_url(self, url):
|
||||
path = None
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from nomadnet.util import sanitize_name
|
|||
from RNS.Utilities.rngit.util import MarkdownToMicron
|
||||
from RNS.Utilities.rngit.highlight import SyntaxHighlighter
|
||||
from .MicronParser import markup_to_attrmaps
|
||||
from .Helpers import ClickableIcon, osc52_copy
|
||||
from .Helpers import ClickableIcon, osc52_copy, qr_ascii
|
||||
from .ReadlineEdit import ReadlineMixin, ReadlineEdit
|
||||
from nomadnet.util import strip_modifiers, strip_micron, strip_escaped_micron, unescape_micron, strip_non_formatting_tags
|
||||
from nomadnet.ui import THEME_DARK, THEME_LIGHT
|
||||
|
|
@ -639,22 +639,7 @@ class ConversationsDisplay():
|
|||
self.show_qr_dialog(addr, title=display)
|
||||
|
||||
def show_qr_dialog(self, data, title=None):
|
||||
qr_text = None
|
||||
try:
|
||||
import qrcode
|
||||
try:
|
||||
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=1, border=1)
|
||||
qr.add_data(data)
|
||||
qr.make()
|
||||
import io
|
||||
buf = io.StringIO()
|
||||
qr.print_ascii(out=buf, invert=False)
|
||||
qr_text = buf.getvalue().rstrip("\n")
|
||||
except Exception as e:
|
||||
RNS.log("QR generation failed: "+str(e), RNS.LOG_ERROR)
|
||||
qr_text = None
|
||||
except Exception:
|
||||
qr_text = None
|
||||
qr_text = qr_ascii(data)
|
||||
|
||||
def dismiss(_b):
|
||||
self._restore_listbox_pane()
|
||||
|
|
|
|||
1053
nomadnet/ui/textui/Editor.py
Normal file
1053
nomadnet/ui/textui/Editor.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1420,6 +1420,7 @@ With micron you can easily create structured documents and pages with formatting
|
|||
`F79d`_`[A Few Demo Outputs`#a-few-demo-outputs]`_`f
|
||||
`F79d`_`[Micron Tags`#micron-tags]`_`f
|
||||
`F79d`_`[High Level Stuff`#high-level-stuff]`_`f
|
||||
`F79d`_`[Collapsible Sections`#collapsible-sections]`_`f
|
||||
`F79d`_`[Colors`#colors]`_`f
|
||||
`F79d`_`[Page Foreground and Background Colors`#page-foreground-and-background-colors]`_`f
|
||||
`F79d`_`[Links`#links]`_`f
|
||||
|
|
@ -1587,6 +1588,32 @@ A sub sub section. We could continue, but you get the point.
|
|||
Wait! It's worth noting that we can also create sections without headings. They look like this.
|
||||
``
|
||||
|
||||
>Collapsible Sections
|
||||
|
||||
Any section heading can be made collapsible, turning it into an accordion the reader can fold away and reveal on demand. Prefix a heading with \\`+ to make it foldable and open by default, or with \\`- to make it foldable but start collapsed:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
`+>Open by default
|
||||
This text can be folded away by selecting the heading.
|
||||
|
||||
`->Hidden until expanded
|
||||
This starts collapsed, and appears when the reader expands it.
|
||||
`=
|
||||
``
|
||||
|
||||
A collapsible heading folds everything beneath it, up to the next heading of the same or a higher level, exactly like a normal section. It can therefore contain anything: text, tables, input fields, sub-sections, and even live partials.
|
||||
|
||||
To fold or unfold a section, move onto its heading and press Enter or Space, or click it with the mouse. Open sections are marked with ▾ and collapsed ones with ▸.
|
||||
|
||||
You can choose your own indicators for a page by placing a #!fold directive near the top, giving the open glyph followed by the collapsed glyph:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
#!fold [-] [+]
|
||||
`=
|
||||
``
|
||||
|
||||
|
||||
>Colors
|
||||
|
||||
|
|
@ -1781,6 +1808,14 @@ A sized input field: `B444`<16|with_size`>`b
|
|||
`=
|
||||
``
|
||||
|
||||
You can also reserve more than one row for a field by appending `!xROWS`! to the width, which is useful for longer, multi-line input. This creates a field that is 40 columns wide and 5 rows tall:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
A multi-row input field: `B444`<40x5|notes`>`b
|
||||
`=
|
||||
``
|
||||
|
||||
It is possible to mask fields, for example for use with passwords and similar:
|
||||
|
||||
`Faaa
|
||||
|
|
@ -1809,6 +1844,8 @@ A sized input field: `B444`<16|with_size`>`B333
|
|||
|
||||
A masked input field: `B444`<!|masked_demo`hidden text>`B333
|
||||
|
||||
A multi-row input field: `B444`<40x5|notes`>`B333
|
||||
|
||||
Full control: `B444`<!32|all_options`hidden text>`B333
|
||||
`b
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,21 @@ def osc52_copy(text):
|
|||
return False
|
||||
|
||||
|
||||
def qr_ascii(data):
|
||||
try:
|
||||
import qrcode
|
||||
import io
|
||||
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=1, border=1)
|
||||
qr.add_data(data)
|
||||
qr.make()
|
||||
buf = io.StringIO()
|
||||
qr.print_ascii(out=buf, invert=False)
|
||||
return buf.getvalue().rstrip("\n")
|
||||
except Exception as e:
|
||||
RNS.log("QR generation failed: "+str(e), RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
|
||||
class ClickableIcon(urwid.Text):
|
||||
_selectable = False
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .Interfaces import *
|
|||
from .Map import *
|
||||
from .Log import *
|
||||
from .Guide import *
|
||||
from .Editor import *
|
||||
import urwid
|
||||
|
||||
class SubDisplays():
|
||||
|
|
@ -23,6 +24,7 @@ class SubDisplays():
|
|||
self.map_display = MapDisplay(self.app)
|
||||
self.log_display = LogDisplay(self.app)
|
||||
self.guide_display = GuideDisplay(self.app)
|
||||
self.page_editor_display = PageEditorDisplay(self.app) if app.enable_node else None
|
||||
|
||||
if app.firstrun:
|
||||
self.active_display = self.guide_display
|
||||
|
|
@ -136,6 +138,14 @@ class MainDisplay():
|
|||
self.sub_displays.active_display = self.sub_displays.guide_display
|
||||
self.update_active_sub_display()
|
||||
|
||||
def show_page_editor(self, user_data):
|
||||
if not self.app.enable_node:
|
||||
return
|
||||
self.sub_displays.active_display = self.sub_displays.page_editor_display
|
||||
self.update_active_sub_display()
|
||||
self.sub_displays.page_editor_display.start()
|
||||
self.frame.focus_position = "body"
|
||||
|
||||
def update_active_sub_display(self):
|
||||
self.frame.contents["body"] = (self.sub_displays.active().widget, None)
|
||||
self.update_active_shortcuts()
|
||||
|
|
@ -195,13 +205,17 @@ class MenuDisplay():
|
|||
button_config = (10, MenuButton("Config", on_press=handler.show_config))
|
||||
button_interfaces = (14, MenuButton("Interfaces", on_press=handler.show_interfaces))
|
||||
button_guide = (9, MenuButton("Guide", on_press=handler.show_guide))
|
||||
button_mynode = (11, MenuButton("My Node", on_press=handler.show_page_editor))
|
||||
button_quit = (8, MenuButton("Quit", on_press=handler.quit))
|
||||
|
||||
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
|
||||
if self.app.config["textui"]["hide_guide"]:
|
||||
buttons = [menu_text, button_conversations, button_network, button_channels, button_log, button_interfaces, button_config, button_quit]
|
||||
else:
|
||||
buttons = [menu_text, button_conversations, button_network, button_channels, button_log, button_interfaces, button_config, button_guide, button_quit]
|
||||
buttons = [menu_text, button_conversations, button_network]
|
||||
if self.app.enable_node:
|
||||
buttons.append(button_mynode)
|
||||
buttons += [button_channels, button_log, button_interfaces, button_config]
|
||||
if not self.app.config["textui"]["hide_guide"]:
|
||||
buttons.append(button_guide)
|
||||
buttons.append(button_quit)
|
||||
|
||||
columns = MenuColumns(buttons, dividechars=1)
|
||||
columns.handler = handler
|
||||
|
|
|
|||
|
|
@ -36,6 +36,15 @@ SECTION_INDENT = 2
|
|||
INDENT_RIGHT = 1
|
||||
MAX_TABLE_WIDTH = 100
|
||||
|
||||
DEFAULT_FOLD_GLYPHS = ("▾", "▸")
|
||||
|
||||
def default_fold_glyphs():
|
||||
try:
|
||||
g = nomadnet.NomadNetworkApp.get_shared_instance().ui.glyphs
|
||||
return (g.get("fold_open", DEFAULT_FOLD_GLYPHS[0]), g.get("fold_closed", DEFAULT_FOLD_GLYPHS[1]))
|
||||
except Exception:
|
||||
return DEFAULT_FOLD_GLYPHS
|
||||
|
||||
def default_state(fg=None, bg=None):
|
||||
global SELECTED_STYLES
|
||||
ensure_selected_styles()
|
||||
|
|
@ -132,15 +141,26 @@ def markup_to_attrmaps(markup, url_delegate = None, fg_color=None, bg_color=None
|
|||
|
||||
if state.get("_header_pending"):
|
||||
state["header_rows"].append(row_index)
|
||||
state.setdefault("header_levels", {})[row_index] = state["depth"]
|
||||
cp = state.get("_collapsible_pending")
|
||||
if cp is not None:
|
||||
state.setdefault("collapsibles", {})[row_index] = bool(cp["collapsed"])
|
||||
state["_collapsible_pending"] = None
|
||||
state["_header_pending"] = False
|
||||
|
||||
row_levels = state.setdefault("row_levels", {})
|
||||
for display_widget in display_widgets:
|
||||
row_levels[len(attrmaps)] = state["depth"]
|
||||
attrmap = urwid.AttrMap(display_widget, make_style(state))
|
||||
attrmaps.append(attrmap)
|
||||
|
||||
try:
|
||||
setattr(attrmaps, "anchors", anchors)
|
||||
setattr(attrmaps, "header_rows", list(state.get("header_rows", [])))
|
||||
setattr(attrmaps, "header_levels", dict(state.get("header_levels", {})))
|
||||
setattr(attrmaps, "row_levels", dict(state.get("row_levels", {})))
|
||||
setattr(attrmaps, "collapsibles", dict(state.get("collapsibles", {})))
|
||||
setattr(attrmaps, "fold_glyphs", state.get("fold_glyphs") or default_fold_glyphs())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -222,6 +242,7 @@ class FormColumns(urwid.Columns):
|
|||
MIN_FIELD_WIDTH = 4
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _natural_width(widget, maxcol, focused):
|
||||
# mirror the width urwid.Columns itself computes for a PACK column
|
||||
if not isinstance(widget, urwid.Widget):
|
||||
|
|
@ -297,6 +318,95 @@ class FormColumns(urwid.Columns):
|
|||
return widths
|
||||
|
||||
|
||||
class MicronField(ReadlineEdit):
|
||||
def __init__(self, *args, min_rows=1, **kwargs):
|
||||
try: self._min_rows = max(1, int(min_rows))
|
||||
except (TypeError, ValueError): self._min_rows = 1
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def rows(self, size, focus=False):
|
||||
return max(self._min_rows, super().rows(size, focus))
|
||||
|
||||
def render(self, size, focus=False):
|
||||
canv = super().render(size, focus)
|
||||
if canv.rows() < self._min_rows:
|
||||
canv = urwid.CompositeCanvas(canv)
|
||||
canv.pad_trim_top_bottom(0, self._min_rows - canv.rows())
|
||||
return canv
|
||||
|
||||
|
||||
class CollapsibleHeading(urwid.WidgetWrap):
|
||||
_selectable = True
|
||||
ignore_focus = False
|
||||
|
||||
def __init__(self, segments, align, heading_style, depth,
|
||||
collapsed=False, glyphs=None, delegate=None):
|
||||
self._segments = list(segments)
|
||||
self.align = align
|
||||
self.heading_style = heading_style
|
||||
self.depth = depth
|
||||
self.collapsed = bool(collapsed)
|
||||
self.glyphs = glyphs if glyphs is not None else default_fold_glyphs()
|
||||
self.delegate = delegate
|
||||
self.is_collapsible_heading = True
|
||||
super().__init__(self._build())
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def _glyph(self):
|
||||
return self.glyphs[1] if self.collapsed else self.glyphs[0]
|
||||
|
||||
def _glyph_col(self):
|
||||
if self._segments and isinstance(self._segments[0], str):
|
||||
return len(self._segments[0])
|
||||
return 0
|
||||
|
||||
def _build(self):
|
||||
segs = list(self._segments)
|
||||
marker = self._glyph() + " "
|
||||
if segs and isinstance(segs[0], str):
|
||||
segs.insert(1, marker)
|
||||
else:
|
||||
segs.insert(0, marker)
|
||||
return urwid.AttrMap(urwid.Text(segs, align=self.align), self.heading_style)
|
||||
|
||||
def set_collapsed(self, collapsed):
|
||||
collapsed = bool(collapsed)
|
||||
if collapsed != self.collapsed:
|
||||
self.collapsed = collapsed
|
||||
self._w = self._build()
|
||||
self._invalidate()
|
||||
|
||||
def toggle(self):
|
||||
self.collapsed = not self.collapsed
|
||||
self._w = self._build()
|
||||
self._invalidate()
|
||||
if self.delegate is not None and hasattr(self.delegate, "fold_changed"):
|
||||
self.delegate.fold_changed(self)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self._command_map[key] == urwid.ACTIVATE or key == " ":
|
||||
self.toggle()
|
||||
return None
|
||||
return key
|
||||
|
||||
def mouse_event(self, size, event, button, x, y, focus):
|
||||
if button == 1 and is_mouse_press(event):
|
||||
self.toggle()
|
||||
return True
|
||||
return False
|
||||
|
||||
def render(self, size, focus=False):
|
||||
canv = super().render(size, focus)
|
||||
if focus:
|
||||
canv = urwid.CompositeCanvas(canv)
|
||||
gx = self._glyph_col()
|
||||
if self.align == "left" and gx < size[0]:
|
||||
canv.cursor = (gx, 0)
|
||||
return canv
|
||||
|
||||
|
||||
def parse_line(line, state, url_delegate):
|
||||
pre_escape = False
|
||||
if len(line) > 0:
|
||||
|
|
@ -309,6 +419,14 @@ def parse_line(line, state, url_delegate):
|
|||
|
||||
# Only parse content if not in literal state
|
||||
if not state["literal"]:
|
||||
collapsible_heading = False
|
||||
collapsed_initial = False
|
||||
if (line.startswith("`+") or line.startswith("`-")) and line[2:3] == ">":
|
||||
collapsible_heading = True
|
||||
collapsed_initial = line[1] == "-"
|
||||
line = line[2:]
|
||||
first_char = line[0]
|
||||
|
||||
# Apply markup sanitization
|
||||
if first_char == ">" and "`<" in line:
|
||||
# Remove heading status from lines containing fields
|
||||
|
|
@ -322,6 +440,10 @@ def parse_line(line, state, url_delegate):
|
|||
|
||||
# Check for comments
|
||||
elif first_char == "#":
|
||||
parts = line.split()
|
||||
if parts and parts[0] == "#!fold":
|
||||
if len(parts) >= 3: state["fold_glyphs"] = (parts[1], parts[2])
|
||||
elif len(parts) == 2: state["fold_glyphs"] = (parts[1], parts[1])
|
||||
return None
|
||||
|
||||
# Check for tables
|
||||
|
|
@ -395,6 +517,11 @@ def parse_line(line, state, url_delegate):
|
|||
|
||||
heading_style = first_style
|
||||
output.insert(0, " "*left_indent(state))
|
||||
if collapsible_heading:
|
||||
state["_collapsible_pending"] = {"collapsed": collapsed_initial}
|
||||
glyphs = state.get("fold_glyphs") or default_fold_glyphs()
|
||||
return [CollapsibleHeading(output, state["align"], heading_style,
|
||||
state["depth"], collapsed_initial, glyphs, url_delegate)]
|
||||
return [urwid.AttrMap(urwid.Text(output, align=state["align"]), heading_style)]
|
||||
else:
|
||||
return None
|
||||
|
|
@ -440,8 +567,9 @@ def parse_line(line, state, url_delegate):
|
|||
fd = o["data"]
|
||||
fn = o["name"]
|
||||
fs = o["style"]
|
||||
frows = o.get("rows", 1)
|
||||
fmask = "*" if o["masked"] else None
|
||||
f = ReadlineEdit(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask)
|
||||
f = MicronField(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask, min_rows=frows)
|
||||
f.field_name = fn
|
||||
fa = urwid.AttrMap(f, fs)
|
||||
widgets.append((fw, fa))
|
||||
|
|
@ -759,11 +887,12 @@ def make_output(state, line, url_delegate, pre_escape=False):
|
|||
field_content = line[field_start:backtick_pos]
|
||||
field_masked = False
|
||||
field_width = 24
|
||||
field_rows = 1
|
||||
field_type = "field"
|
||||
field_name = field_content
|
||||
field_value = ""
|
||||
field_data = ""
|
||||
field_prechecked = False
|
||||
field_prechecked = False
|
||||
|
||||
# check if field_content contains '|'
|
||||
if '|' in field_content:
|
||||
|
|
@ -784,10 +913,16 @@ def make_output(state, line, url_delegate, pre_escape=False):
|
|||
|
||||
# Handle field width
|
||||
if len(field_flags) > 0:
|
||||
width_flag, _, rows_flag = field_flags.partition("x")
|
||||
try:
|
||||
field_width = min(int(field_flags), 256)
|
||||
field_width = min(int(width_flag), 256)
|
||||
except ValueError:
|
||||
pass # Ignore invalid width
|
||||
if rows_flag:
|
||||
try:
|
||||
field_rows = max(1, min(int(rows_flag), 256))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Check for value and pre-checked flag
|
||||
if len(f_components) > 2:
|
||||
|
|
@ -831,6 +966,7 @@ def make_output(state, line, url_delegate, pre_escape=False):
|
|||
"type": "field",
|
||||
"name": field_name,
|
||||
"width": field_width,
|
||||
"rows": field_rows,
|
||||
"masked": field_masked,
|
||||
"data": field_data,
|
||||
"style": make_style(state)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from nomadnet.util import sanitize_name
|
|||
|
||||
from .Browser import Browser
|
||||
from .ReadlineEdit import ReadlineEdit
|
||||
from .Helpers import qr_ascii
|
||||
|
||||
class NetworkDisplayShortcuts():
|
||||
def __init__(self, app):
|
||||
|
|
@ -26,7 +27,17 @@ class NetworkDisplayShortcuts():
|
|||
class DialogLineBox(urwid.LineBox):
|
||||
def keypress(self, size, key):
|
||||
if key == "esc":
|
||||
self.delegate.update_conversation_list()
|
||||
delegate = getattr(self, "delegate", None)
|
||||
if delegate is not None and hasattr(delegate, "update_conversation_list"):
|
||||
delegate.update_conversation_list()
|
||||
elif delegate is not None and getattr(delegate, "parent", None) is not None:
|
||||
try:
|
||||
delegate.dialog_open = False
|
||||
options = delegate.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
|
||||
delegate.parent.left_pile.contents[1] = (type(delegate)(delegate.app, delegate.parent), options)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
else:
|
||||
return super(DialogLineBox, self).keypress(size, key)
|
||||
|
||||
|
|
@ -82,7 +93,7 @@ class AnnounceInfo(urwid.WidgetWrap):
|
|||
is_node = False
|
||||
is_pn = False
|
||||
if info_type == "node" or info_type == True:
|
||||
type_string = "Nomad Network Node " + g["node"]
|
||||
type_string = "NomadNet Node " + g["node"]
|
||||
is_node = True
|
||||
elif info_type == "pn":
|
||||
type_string = "LXMF Propagation Node " + g["sent"]
|
||||
|
|
@ -634,7 +645,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
|
|||
lxmf_addr_str = "No associated Propagation Node known"
|
||||
|
||||
|
||||
type_string = "Nomad Network Node " + g["node"]
|
||||
type_string = "NomadNet Node " + g["node"]
|
||||
|
||||
if trust_level == DirectoryEntry.UNTRUSTED:
|
||||
trust_str = "Untrusted"
|
||||
|
|
@ -1331,22 +1342,27 @@ class LocalPeer(urwid.WidgetWrap):
|
|||
|
||||
announce_button = urwid.Button("Announce Now", on_press=announce_query)
|
||||
|
||||
self.display_widget = urwid.Pile(
|
||||
[
|
||||
t_id,
|
||||
i_id,
|
||||
e_name,
|
||||
urwid.Divider(g["divider1"]),
|
||||
self.t_last_announce,
|
||||
announce_button,
|
||||
urwid.Divider(g["divider1"]),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Save", on_press=save_query)),
|
||||
(urwid.WEIGHT, 0.1, urwid.Text("")),
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Node Info", on_press=node_info_query)),
|
||||
]),
|
||||
]
|
||||
)
|
||||
peer_widgets = [
|
||||
t_id,
|
||||
i_id,
|
||||
e_name,
|
||||
urwid.Divider(g["divider1"]),
|
||||
self.t_last_announce,
|
||||
announce_button,
|
||||
urwid.Divider(g["divider1"]),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Save", on_press=save_query)),
|
||||
(urwid.WEIGHT, 0.1, urwid.Text("")),
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Node Info", on_press=node_info_query)),
|
||||
]),
|
||||
]
|
||||
|
||||
if self.app.enable_node:
|
||||
peer_widgets.append(
|
||||
urwid.AttrMap(urwid.Text(" "+g["node"]+" Node Online", align=urwid.LEFT), "shortcutbar")
|
||||
)
|
||||
|
||||
self.display_widget = urwid.Pile(peer_widgets)
|
||||
|
||||
super().__init__(urwid.LineBox(self.display_widget, title="Local Peer Info"))
|
||||
|
||||
|
|
@ -1420,6 +1436,37 @@ class NodeInfo(urwid.WidgetWrap):
|
|||
def connect_query(sender):
|
||||
self.parent.browser.retrieve_url(RNS.hexrep(self.app.node.destination.hash, delimit=False))
|
||||
|
||||
def edit_query(sender):
|
||||
self.app.ui.main_display.show_page_editor(None)
|
||||
|
||||
def show_qr_query(sender):
|
||||
def dismiss_dialog(sender):
|
||||
self.dialog_open = False
|
||||
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
|
||||
self.parent.left_pile.contents[1] = (NodeInfo(self.app, self.parent), options)
|
||||
|
||||
dest_addr = RNS.hexrep(self.app.node.destination.hash, delimit=False)
|
||||
qr_text = qr_ascii(dest_addr)
|
||||
|
||||
rows = [urwid.Text("")]
|
||||
if qr_text is not None:
|
||||
rows.append(urwid.Text(qr_text, align=urwid.CENTER))
|
||||
else:
|
||||
rows.append(urwid.Text("Node destination address:", align=urwid.CENTER))
|
||||
rows += [
|
||||
urwid.Text(""),
|
||||
urwid.Text("< "+dest_addr+" >", align=urwid.CENTER),
|
||||
urwid.Text(""),
|
||||
urwid.Button("Close", on_press=dismiss_dialog),
|
||||
]
|
||||
|
||||
dialog = DialogLineBox(urwid.Pile(rows), title="Node Destination")
|
||||
dialog.delegate = self
|
||||
|
||||
self.dialog_open = True
|
||||
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
|
||||
self.parent.left_pile.contents[1] = (dialog, options)
|
||||
|
||||
if NodeInfo.announce_timer == None:
|
||||
self.t_last_announce = NodeAnnounceTime(self.app)
|
||||
NodeInfo.announce_timer = self.t_last_announce
|
||||
|
|
@ -1467,7 +1514,9 @@ class NodeInfo(urwid.WidgetWrap):
|
|||
|
||||
announce_button = urwid.Button("Announce", on_press=announce_query)
|
||||
connect_button = urwid.Button("Browse", on_press=connect_query)
|
||||
edit_button = urwid.Button("Edit", on_press=edit_query)
|
||||
reset_button = urwid.Button("Rst Stats", on_press=stats_query)
|
||||
qr_button = urwid.Button("Show Destination", on_press=show_qr_query)
|
||||
|
||||
if not self.app.disable_propagation:
|
||||
pile = urwid.Pile([
|
||||
|
|
@ -1483,14 +1532,13 @@ class NodeInfo(urwid.WidgetWrap):
|
|||
self.t_total_pages,
|
||||
self.t_total_files,
|
||||
urwid.Divider(g["divider1"]),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 5, urwid.Button("Back", on_press=show_peer_info)),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 6, connect_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 8, reset_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 7, announce_button),
|
||||
urwid.Pile([
|
||||
urwid.Button("Peer Info", on_press=show_peer_info),
|
||||
connect_button,
|
||||
edit_button,
|
||||
reset_button,
|
||||
announce_button,
|
||||
qr_button,
|
||||
])
|
||||
])
|
||||
else:
|
||||
|
|
@ -1505,14 +1553,13 @@ class NodeInfo(urwid.WidgetWrap):
|
|||
self.t_total_pages,
|
||||
self.t_total_files,
|
||||
urwid.Divider(g["divider1"]),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 5, urwid.Button("Back", on_press=show_peer_info)),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 6, connect_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 8, reset_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 7, announce_button),
|
||||
urwid.Pile([
|
||||
urwid.Button("Peer Info", on_press=show_peer_info),
|
||||
connect_button,
|
||||
edit_button,
|
||||
reset_button,
|
||||
announce_button,
|
||||
qr_button,
|
||||
])
|
||||
])
|
||||
else:
|
||||
|
|
@ -1640,10 +1687,11 @@ class NetworkDisplay():
|
|||
self.known_nodes_display.delegate = self
|
||||
|
||||
self.list_display = 1
|
||||
default_info_display = self.node_info_display if self.app.enable_node else self.local_peer_display
|
||||
self.left_pile = NetworkLeftPile([
|
||||
(urwid.WEIGHT, 1, self.known_nodes_display),
|
||||
# (urwid.PACK, self.network_stats_display),
|
||||
(urwid.PACK, self.local_peer_display),
|
||||
(urwid.PACK, default_info_display),
|
||||
])
|
||||
|
||||
self.left_pile.parent = self
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue