mirror of
https://github.com/markqvist/NomadNet.git
synced 2024-10-01 01:26:07 -04:00
Implemented micron markup parser
This commit is contained in:
parent
4c226b7e00
commit
2f2fdd6a0c
@ -20,6 +20,7 @@ THEMES = {
|
||||
# Style name # 16-color style # Monochrome style # 88, 256 and true-color style
|
||||
('heading', 'light gray,underline', 'default', 'underline', 'g93,underline', 'default'),
|
||||
('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'),
|
||||
('scrollbar', 'black', 'light gray', 'standout', '#444', 'default'),
|
||||
('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'),
|
||||
('body_text', 'white', 'default', 'default', '#ddd', 'default'),
|
||||
('error_text', 'dark red', 'default', 'default', 'dark red', 'default'),
|
||||
@ -91,9 +92,6 @@ class TextUI:
|
||||
|
||||
self.palette = THEMES[theme]["urwid_theme"]
|
||||
|
||||
for entry in nomadnet.ui.textui.MarkupParser.URWID_THEME:
|
||||
self.palette.append(entry)
|
||||
|
||||
if self.app.config["textui"]["glyphs"] == "plain":
|
||||
glyphset = "plain"
|
||||
elif self.app.config["textui"]["glyphs"] == "unicoode":
|
||||
|
@ -2,7 +2,8 @@ import RNS
|
||||
import urwid
|
||||
import nomadnet
|
||||
from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY
|
||||
from .MarkupParser import markup_to_attrmaps
|
||||
from .MicronParser import markup_to_attrmaps
|
||||
from .Scrollable import *
|
||||
|
||||
class GuideDisplayShortcuts():
|
||||
def __init__(self, app):
|
||||
@ -88,6 +89,7 @@ class TopicList(urwid.WidgetWrap):
|
||||
GuideEntry(self.app, guide_display, "Introduction"),
|
||||
GuideEntry(self.app, guide_display, "Conversations"),
|
||||
GuideEntry(self.app, guide_display, "Markup"),
|
||||
GuideEntry(self.app, guide_display, "First Run"),
|
||||
GuideEntry(self.app, guide_display, "Licenses & Credits"),
|
||||
]
|
||||
|
||||
@ -132,7 +134,8 @@ class GuideDisplay():
|
||||
def set_content_widgets(self, new_content):
|
||||
options = self.columns.options(width_type="weight", width_amount=1-GuideDisplay.list_width)
|
||||
pile = urwid.Pile(new_content)
|
||||
content = urwid.LineBox(urwid.Filler(pile, "top"))
|
||||
#content = urwid.LineBox(urwid.Filler(pile, "top"))
|
||||
content = urwid.LineBox(urwid.AttrMap(ScrollBar(Scrollable(pile), thumb_char="\u2503", trough_char=" "), "scrollbar"))
|
||||
|
||||
self.columns.contents[1] = (content, options)
|
||||
|
||||
@ -182,46 +185,240 @@ TOPIC_CONVERSATIONS = '''Conversations
|
||||
Conversations in Nomad Network
|
||||
'''
|
||||
|
||||
TOPIC_MARKUP = '''>Markup
|
||||
Nomad Network supports a simple and functional markup language called micron. It has a lean markup structure that adds very little overhead, and is still readable as plain text, but offers basic formatting and text structuring, ideal for displaying in a terminal.
|
||||
TOPIC_MARKUP = '''>Outputting Formatted Text
|
||||
|
||||
`cLorem ipsum dolor sit amet.
|
||||
|
||||
>>>>>>>>>>>>>>>
|
||||
-\u223f
|
||||
<
|
||||
|
||||
`c`!Hello!`! This is output from `*micron`*
|
||||
Micron generates formatted text for your terminal
|
||||
`a
|
||||
|
||||
>>Encoding
|
||||
`F222`BdddAll uM source files are encoded as UTF-8, and clients supporting uM display should support UTF-8.
|
||||
>>>>>>>>>>>>>>>
|
||||
-\u223f
|
||||
<
|
||||
|
||||
|
||||
Nomad Network supports a simple and functional markup language called `*micron`*. If you are familiar with `*markdown`* or `*HTML`*, you will feel right at home writing pages with micron.
|
||||
|
||||
With micron you can easily create structured documents and pages with formatting, colors, glyphs and icons, ideal for display in terminals.
|
||||
|
||||
>>Recommendations and Requirements
|
||||
|
||||
While micron can output formatted text to even the most basic terminal, there's a few capabilities your terminal `*must`* support to display micron output correctly, and some that, while not strictly necessary, make the experience a lot better.
|
||||
|
||||
>>>Encoding
|
||||
|
||||
All micron sources are intepreted as UTF-8, and micron assumes it can output UTF-8 characters to the terminal. If your terminal does not support UTF-8, output will be faulty.
|
||||
|
||||
>>>Colors
|
||||
|
||||
Shading and coloring text and backgrounds is integral to micron output, and while micron will attempt to gracefully degrade output even to 1-bit terminals, you will get the best output with terminals supporting at least 256 colors. True-color support is recommended.
|
||||
|
||||
>>>Terminal Font
|
||||
|
||||
While any font any unicode capable font can be used with micron, it's highly recommended to use a `*"Nerd Font"`* (see https://www.nerdfonts.com/), which will add a lot of extra glyphs and icons to your output.
|
||||
|
||||
> A Few Demo Outputs
|
||||
|
||||
`F222`Bddd
|
||||
|
||||
`cWith micron, you can control layout and presentation
|
||||
`a
|
||||
|
||||
``
|
||||
|
||||
`B33f
|
||||
|
||||
You can change background ...
|
||||
|
||||
``
|
||||
|
||||
`B393
|
||||
|
||||
`r... and foreground colors
|
||||
`r`F320... and foreground colors`f
|
||||
`a
|
||||
|
||||
`b
|
||||
|
||||
>>>Sections and `F900Headings`f
|
||||
You can define an arbitrary number of sections and sub-sections, each with their own heading
|
||||
If you want to make a break, horizontal dividers can be inserted. They can be plain, like the one below this text, or you can style them with unicode characters and glyphs, like the wavy divider in the beginning of this document.
|
||||
|
||||
-
|
||||
|
||||
Dividers inside section will adhere to section indents
|
||||
`cText can be `_underlined`_, `!bold`! or `*italic`*.
|
||||
|
||||
You can also `_`*`!`B5d5`F222combine`f`b`_ `_`Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg`` for some fabulous effects.
|
||||
`a
|
||||
|
||||
|
||||
>>>Sections and Headings
|
||||
|
||||
You can define an arbitrary number of sections and sub sections, each with their own named headings. Text inside sections will be automatically indented.
|
||||
|
||||
-
|
||||
|
||||
If you place a divider inside a section, it will adhere to the section indents.
|
||||
|
||||
>>>>>
|
||||
If no heading text is defined, the section will appear as a sub-section without a header. This can be useful for creating indented blocks of text, like this one.
|
||||
|
||||
>Micron tags
|
||||
|
||||
Tags are used to format text with micron. Some tags can appear anywhere in text, and some must appear at the beginning of a line. If you need to write text that contains a sequence that would be interpreted as a tag, you can escape it with the character \\.
|
||||
|
||||
In the following sections, the different tags will be introduced. Any styling set within micron can be reset to the default style by using the special \\`\\` tag anywhere in the markup, which will immediately remove any formatting previously specified.
|
||||
|
||||
>>Alignment
|
||||
|
||||
To control text alignment use the tag \\`c to center text, \\`l to left-align, \\`r to right-align, and \\`a to return to the default alignment of the document. Alignment tags must appear at the beginning of a line. Here is an example:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
`cThis line will be centered.
|
||||
So will this.
|
||||
`aThe alignment has now been returned to default.
|
||||
`rThis will be aligned to the right
|
||||
``
|
||||
`=
|
||||
``
|
||||
|
||||
The above markup produces the following output:
|
||||
|
||||
`Faaa`B333
|
||||
|
||||
`cThis line will be centered.
|
||||
So will this.
|
||||
|
||||
`aThe alignment has now been returned to default.
|
||||
|
||||
`rThis will be aligned to the right
|
||||
|
||||
``
|
||||
|
||||
|
||||
>>Formatting
|
||||
|
||||
Text can be formatted as `!bold`! by using the \\`! tag, `_underline`_ by using the \\`_ tag and `*italic`* by using the \\`* tag.
|
||||
|
||||
Here's an example of formatting text:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
We shall soon see `!bold`! paragraphs of text decorated with `_underlines`_ and `*italics`*. Some even dare `!`*`_combine`` them!
|
||||
`=
|
||||
``
|
||||
|
||||
The above markup produces the following output:
|
||||
|
||||
`Faaa`B333
|
||||
|
||||
We shall soon see `!bold`! paragraphs of text decorated with `_underlines`_ and `*italics`*. Some even dare `!`*`_combine`!`*`_ them!
|
||||
|
||||
``
|
||||
|
||||
|
||||
>>Sections
|
||||
|
||||
To create sections and subsections, use the > tag. This tag must be placed at the beginning of a line. To specify a sub-section of any level, use any number of > tags. If text is placed after a > tag, it will be used as a heading.
|
||||
|
||||
Here is an example of sections:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
>High Level Stuff
|
||||
This is a section. It contains this text.
|
||||
|
||||
>>Another Level
|
||||
This is a sub section.
|
||||
|
||||
>>>Going deeper
|
||||
A sub sub section. We could continue, but you get the point.
|
||||
|
||||
>>>>
|
||||
If no heading text is defined, the section will appear as a sub-section without a header.
|
||||
Wait! It's worth noting that we can also create sections without headings. They look like this.
|
||||
`=
|
||||
``
|
||||
|
||||
<-
|
||||
Horizontal dividers can be inserted
|
||||
The above markup produces the following output:
|
||||
|
||||
Text can be `_underlined`_, `!bold`! or `*italic`*. You can also `_`*`!`B5d5`F222combine`f`b`_ `_`Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg``!
|
||||
`Faaa`B333
|
||||
>High Level Stuff
|
||||
This is a section. It contains this text.
|
||||
|
||||
>>Another Level
|
||||
This is a sub section.
|
||||
|
||||
>>>Going deeper
|
||||
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.
|
||||
``
|
||||
|
||||
|
||||
>Colors
|
||||
|
||||
Foreground colors can be specified with the \\`F tag, followed by three hexadecimal characters. To return to the default foreground color, use the \\`f tag. Background color is specified in the same way, but by using the \\`B and \\`b tags.
|
||||
|
||||
Here's a few examples:
|
||||
|
||||
`Faaa
|
||||
`=
|
||||
You can use `B5d5`F222 color `f`b `Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg`f for some fabulous effects.
|
||||
`=
|
||||
``
|
||||
|
||||
The above markup produces the following output:
|
||||
|
||||
`Faaa`B333
|
||||
|
||||
You can use `B5d5`F222 color `f`B333 `Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg`f for some fabulous effects.
|
||||
|
||||
``
|
||||
|
||||
|
||||
>Literals
|
||||
|
||||
To display literal content, for example source-code, or blocks of text that should not be interpreted by micron, you can use literal blocks, specified by the \\`= tag. Below is the source code of this entire document, presented as a literal block.
|
||||
|
||||
-
|
||||
|
||||
`=
|
||||
'''
|
||||
TOPIC_MARKUP += TOPIC_MARKUP.replace("`=", "\\`=") + "[ micron source for document goes here, we don't want infinite recursion now, do we? ]\n\\`="
|
||||
TOPIC_MARKUP += "\n`=\n\n>Closing Remarks\n\nIf you made it all the way here, you should be well equipped to write documents and pages using micron. Thank you for staying with me.\n\n`c\U0001F332\n"
|
||||
|
||||
TOPIC_FIRST_RUN = '''>First Time Information
|
||||
|
||||
Hi there. This first run message will only appear once. It contains a few pointers on getting started with Nomad Network, and getting the most out of the program. You're currently located in the guide section of the program. I'm sorry I had to drag you here by force, but it will only happen this one time, I promise. If you ever get lost, return here and peruse the list of topics you see on the left. I will do my best to fill it with answers to mostly anything about Nomad Network.
|
||||
|
||||
To get the most out of Nomad Network, you will need a terminal that supports UTF-8 and at least 256 colors, ideally true-color. By default, Nomad Network starts in low-color mode. It does this for the sake of compatibility, but it does look rather ugly. If your terminal supports true-color or just 256 colors, you should go to the `![ Config ]`! menu item, launch the editor and change the configuration to use a high-color mode.
|
||||
|
||||
If you don't already have a Nerd Font installed (see https://www.nerdfonts.com/), I also highly recommend to do so, since it will greatly expand the amount of glyphs, icons and graphics that Nomad Network can use.
|
||||
|
||||
Nomad Network expects that you are already connected to some form of Reticulum network. That could be as simple as the default UDP-based demo interface on your local ethernet network, or as advanced as some elaborate hybrid RF and free-space optical laser network. This short guide won't go into any details on that, but you will find other entries in the guide that deal with network setup and configuration.
|
||||
|
||||
At least, if Nomad Network launches, it means that it is connected to a running Reticulum instance, that should in turn be connected to `*something`*, which should get you started.
|
||||
|
||||
Now go out there and explore. This is still early days. See what you can find and create.
|
||||
|
||||
>>>>>>>>>>>>>>>
|
||||
-\u223f
|
||||
<
|
||||
|
||||
'''
|
||||
|
||||
TOPIC_LICENSES = '''>Thanks, Acknowledgements and Licenses
|
||||
|
||||
Lorem Ipsum
|
||||
'''
|
||||
|
||||
TOPICS = {
|
||||
"Introduction": TOPIC_INTRODUCTION,
|
||||
"Conversations": TOPIC_CONVERSATIONS,
|
||||
"Markup": TOPIC_MARKUP,
|
||||
"First Run": TOPIC_FIRST_RUN,
|
||||
"Licenses & Credits": TOPIC_LICENSES,
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
import nomadnet
|
||||
import urwid
|
||||
import re
|
||||
|
||||
URWID_THEME = [
|
||||
# Style name 16-color style Monochrome style # 88, 256 and true-color style
|
||||
('plain', 'light gray', 'default', 'default', '#ddd', 'default'),
|
||||
('heading1', 'black', 'light gray', 'standout', '#222', '#bbb'),
|
||||
('heading2', 'black', 'light gray', 'standout', '#111', '#999'),
|
||||
('heading3', 'black', 'light gray', 'standout', '#000', '#777'),
|
||||
('f_underline', 'default,underline', 'default', 'default,underline', 'default,underline', 'default'),
|
||||
('f_bold', 'default,bold', 'default', 'default,bold', 'default,bold', 'default'),
|
||||
('f_italic', 'default,italics', 'default', 'default,italics', 'default,italics', 'default'),
|
||||
]
|
||||
|
||||
SYNTH_STYLES = []
|
||||
|
||||
SECTION_INDENT = 2
|
||||
INDENT_RIGHT = 1
|
||||
|
||||
def markup_to_attrmaps(markup):
|
||||
attrmaps = []
|
||||
|
||||
state = {
|
||||
"depth": 0,
|
||||
"fg_color": "default",
|
||||
"bg_color": "default",
|
||||
"formatting": {
|
||||
"bold": False,
|
||||
"underline": False,
|
||||
"italic": False,
|
||||
"strikethrough": False,
|
||||
"blink": False,
|
||||
},
|
||||
"default_align": "left",
|
||||
"align": "left",
|
||||
}
|
||||
|
||||
# Split entire document into lines for
|
||||
# processing.
|
||||
lines = markup.split("\n");
|
||||
|
||||
for line in lines:
|
||||
if len(line) > 0:
|
||||
display_widget = parse_line(line, state)
|
||||
else:
|
||||
display_widget = urwid.Text("")
|
||||
|
||||
if display_widget != None:
|
||||
attrmap = urwid.AttrMap(display_widget, make_style(state))
|
||||
attrmaps.append(attrmap)
|
||||
|
||||
return attrmaps
|
||||
|
||||
|
||||
def parse_line(line, state):
|
||||
first_char = line[0]
|
||||
|
||||
# Check if the command is an escape
|
||||
if first_char == "\\":
|
||||
line = line[1:]
|
||||
|
||||
# Check for section heading reset
|
||||
elif first_char == "<":
|
||||
state["depth"] = 0
|
||||
return parse_line(line[1:], state)
|
||||
|
||||
# Check for section headings
|
||||
elif first_char == ">":
|
||||
i = 0
|
||||
while i < len(line) and line[i] == ">":
|
||||
i += 1
|
||||
state["depth"] = i
|
||||
|
||||
for j in range(1, i+1):
|
||||
wanted_style = "heading"+str(i)
|
||||
if any(s[0]==wanted_style for s in URWID_THEME):
|
||||
style = wanted_style
|
||||
|
||||
line = line[state["depth"]:]
|
||||
if len(line) > 0:
|
||||
line = " "*left_indent(state)+line
|
||||
return urwid.AttrMap(urwid.Text(line), style)
|
||||
else:
|
||||
return None
|
||||
|
||||
# Check for horizontal dividers
|
||||
elif first_char == "-":
|
||||
if len(line) == 2:
|
||||
divider_char = line[1]
|
||||
else:
|
||||
divider_char = "\u2500"
|
||||
if state["depth"] == 0:
|
||||
return urwid.Divider(divider_char)
|
||||
else:
|
||||
return urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state))
|
||||
|
||||
output = make_output(state, line)
|
||||
|
||||
if output != None:
|
||||
if state["depth"] == 0:
|
||||
return urwid.Text(output, align=state["align"])
|
||||
else:
|
||||
return urwid.Padding(urwid.Text(output, align=state["align"]), left=left_indent(state), right=right_indent(state))
|
||||
else:
|
||||
return None
|
||||
|
||||
def left_indent(state):
|
||||
return (state["depth"]-1)*SECTION_INDENT
|
||||
|
||||
def right_indent(state):
|
||||
return (state["depth"]-1)*SECTION_INDENT
|
||||
|
||||
def make_part(state, part):
|
||||
return (make_style(state), part)
|
||||
|
||||
def make_style(state):
|
||||
def mono_color(fg, bg):
|
||||
return "default"
|
||||
def low_color(color):
|
||||
# TODO: Implement
|
||||
return "default"
|
||||
def high_color(color):
|
||||
if color == "default":
|
||||
return color
|
||||
else:
|
||||
return "#"+color
|
||||
|
||||
bold = state["formatting"]["bold"]
|
||||
underline = state["formatting"]["underline"]
|
||||
italic = state["formatting"]["italic"]
|
||||
fg = state["fg_color"]
|
||||
bg = state["bg_color"]
|
||||
|
||||
format_string = ""
|
||||
if bold:
|
||||
format_string += ",bold"
|
||||
if underline:
|
||||
format_string += ",underline"
|
||||
if italic:
|
||||
format_string += ",italics"
|
||||
|
||||
name = "micron_"+fg+"_"+bg+"_"+format_string
|
||||
if not name in SYNTH_STYLES:
|
||||
screen = nomadnet.NomadNetworkApp.get_shared_instance().ui.screen
|
||||
screen.register_palette_entry(name, low_color(fg)+format_string,low_color(bg),mono_color(fg, bg)+format_string,high_color(fg)+format_string,high_color(bg))
|
||||
SYNTH_STYLES.append(name)
|
||||
|
||||
return name
|
||||
|
||||
def make_output(state, line):
|
||||
output = []
|
||||
part = ""
|
||||
mode = "text"
|
||||
skip = 0
|
||||
for i in range(0, len(line)):
|
||||
c = line[i]
|
||||
if skip > 0:
|
||||
skip -= 1
|
||||
else:
|
||||
if mode == "formatting":
|
||||
if c == "_":
|
||||
state["formatting"]["underline"] ^= True
|
||||
elif c == "!":
|
||||
state["formatting"]["bold"] ^= True
|
||||
elif c == "*":
|
||||
state["formatting"]["italic"] ^= True
|
||||
elif c == "F":
|
||||
if len(line) > i+4:
|
||||
color = line[i+1:i+4]
|
||||
state["fg_color"] = color
|
||||
skip = 3
|
||||
elif c == "f":
|
||||
state["fg_color"] = "default"
|
||||
elif c == "B":
|
||||
if len(line) >= i+4:
|
||||
color = line[i+1:i+4]
|
||||
state["bg_color"] = color
|
||||
skip = 3
|
||||
elif c == "b":
|
||||
state["bg_color"] = "default"
|
||||
elif c == "`":
|
||||
state["formatting"]["bold"] = False
|
||||
state["formatting"]["underline"] = False
|
||||
state["formatting"]["italic"] = False
|
||||
state["fg_color"] = "default"
|
||||
state["bg_color"] = "default"
|
||||
elif c == "c":
|
||||
if state["align"] != "center":
|
||||
state["align"] = "center"
|
||||
else:
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "l":
|
||||
if state["align"] != "left":
|
||||
state["align"] = "left"
|
||||
else:
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "r":
|
||||
if state["align"] != "right":
|
||||
state["align"] = "right"
|
||||
else:
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "a":
|
||||
state["align"] = state["default_align"]
|
||||
|
||||
mode = "text"
|
||||
if len(part) > 0:
|
||||
output.append(make_part(state, part))
|
||||
|
||||
elif mode == "text":
|
||||
if c == "`":
|
||||
mode = "formatting"
|
||||
if len(part) > 0:
|
||||
output.append(make_part(state, part))
|
||||
part = ""
|
||||
else:
|
||||
part += c
|
||||
|
||||
if i == len(line)-1:
|
||||
if len(part) > 0:
|
||||
output.append(make_part(state, part))
|
||||
|
||||
if len(output) > 0:
|
||||
return output
|
||||
else:
|
||||
return None
|
275
nomadnet/ui/textui/MicronParser.py
Normal file
275
nomadnet/ui/textui/MicronParser.py
Normal file
@ -0,0 +1,275 @@
|
||||
import nomadnet
|
||||
import urwid
|
||||
import re
|
||||
|
||||
STYLES = {
|
||||
"plain": { "fg": "bbb", "bg": "default", "bold": False, "underline": False, "italic": False },
|
||||
"heading1": { "fg": "222", "bg": "bbb", "bold": False, "underline": False, "italic": False },
|
||||
"heading2": { "fg": "111", "bg": "999", "bold": False, "underline": False, "italic": False },
|
||||
"heading3": { "fg": "000", "bg": "777", "bold": False, "underline": False, "italic": False },
|
||||
}
|
||||
|
||||
SYNTH_STYLES = []
|
||||
|
||||
SECTION_INDENT = 2
|
||||
INDENT_RIGHT = 1
|
||||
|
||||
def markup_to_attrmaps(markup):
|
||||
attrmaps = []
|
||||
|
||||
state = {
|
||||
"literal": False,
|
||||
"depth": 0,
|
||||
"fg_color": "default",
|
||||
"bg_color": "default",
|
||||
"formatting": {
|
||||
"bold": False,
|
||||
"underline": False,
|
||||
"italic": False,
|
||||
"strikethrough": False,
|
||||
"blink": False,
|
||||
},
|
||||
"default_align": "left",
|
||||
"align": "left",
|
||||
}
|
||||
|
||||
# Split entire document into lines for
|
||||
# processing.
|
||||
lines = markup.split("\n");
|
||||
|
||||
for line in lines:
|
||||
if len(line) > 0:
|
||||
display_widget = parse_line(line, state)
|
||||
else:
|
||||
display_widget = urwid.Text("")
|
||||
|
||||
if display_widget != None:
|
||||
attrmap = urwid.AttrMap(display_widget, make_style(state))
|
||||
attrmaps.append(attrmap)
|
||||
|
||||
return attrmaps
|
||||
|
||||
|
||||
def parse_line(line, state):
|
||||
if len(line) > 0:
|
||||
first_char = line[0]
|
||||
|
||||
# Check for literals
|
||||
if len(line) == 2 and line == "`=":
|
||||
state["literal"] ^= True
|
||||
return None
|
||||
|
||||
# Only parse content if not in literal state
|
||||
if not state["literal"]:
|
||||
# Check if the command is an escape
|
||||
if first_char == "\\":
|
||||
line = line[1:]
|
||||
|
||||
# Check for section heading reset
|
||||
elif first_char == "<":
|
||||
state["depth"] = 0
|
||||
return parse_line(line[1:], state)
|
||||
|
||||
# Check for section headings
|
||||
elif first_char == ">":
|
||||
i = 0
|
||||
while i < len(line) and line[i] == ">":
|
||||
i += 1
|
||||
state["depth"] = i
|
||||
|
||||
for j in range(1, i+1):
|
||||
wanted_style = "heading"+str(i)
|
||||
if wanted_style in STYLES:
|
||||
style = STYLES[wanted_style]
|
||||
|
||||
line = line[state["depth"]:]
|
||||
if len(line) > 0:
|
||||
latched_style = state_to_style(state)
|
||||
style_to_state(style, state)
|
||||
|
||||
heading_style = make_style(state)
|
||||
output = make_output(state, line)
|
||||
|
||||
style_to_state(latched_style, state)
|
||||
|
||||
if len(output) > 0:
|
||||
first_style = output[0][0]
|
||||
|
||||
heading_style = first_style
|
||||
output.insert(0, " "*left_indent(state))
|
||||
return urwid.AttrMap(urwid.Text(output, align=state["align"]), heading_style)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
# Check for horizontal dividers
|
||||
elif first_char == "-":
|
||||
if len(line) == 2:
|
||||
divider_char = line[1]
|
||||
else:
|
||||
divider_char = "\u2500"
|
||||
if state["depth"] == 0:
|
||||
return urwid.Divider(divider_char)
|
||||
else:
|
||||
return urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state))
|
||||
|
||||
output = make_output(state, line)
|
||||
|
||||
if output != None:
|
||||
if state["depth"] == 0:
|
||||
return urwid.Text(output, align=state["align"])
|
||||
else:
|
||||
return urwid.Padding(urwid.Text(output, align=state["align"]), left=left_indent(state), right=right_indent(state))
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def left_indent(state):
|
||||
return (state["depth"]-1)*SECTION_INDENT
|
||||
|
||||
def right_indent(state):
|
||||
return (state["depth"]-1)*SECTION_INDENT
|
||||
|
||||
def make_part(state, part):
|
||||
return (make_style(state), part)
|
||||
|
||||
def state_to_style(state):
|
||||
return { "fg": state["fg_color"], "bg": state["bg_color"], "bold": state["formatting"]["bold"], "underline": state["formatting"]["underline"], "italic": state["formatting"]["italic"] }
|
||||
|
||||
def style_to_state(style, state):
|
||||
if style["fg"] != None:
|
||||
state["fg_color"] = style["fg"]
|
||||
if style["bg"] != None:
|
||||
state["bg_color"] = style["bg"]
|
||||
if style["bold"] != None:
|
||||
state["formatting"]["bold"] = style["bold"]
|
||||
if style["underline"] != None:
|
||||
state["formatting"]["underline"] = style["underline"]
|
||||
if style["italic"] != None:
|
||||
state["formatting"]["italic"] = style["italic"]
|
||||
|
||||
def make_style(state):
|
||||
def mono_color(fg, bg):
|
||||
return "default"
|
||||
def low_color(color):
|
||||
# TODO: Implement
|
||||
return "default"
|
||||
def high_color(color):
|
||||
if color == "default":
|
||||
return color
|
||||
else:
|
||||
return "#"+color
|
||||
|
||||
bold = state["formatting"]["bold"]
|
||||
underline = state["formatting"]["underline"]
|
||||
italic = state["formatting"]["italic"]
|
||||
fg = state["fg_color"]
|
||||
bg = state["bg_color"]
|
||||
|
||||
format_string = ""
|
||||
if bold:
|
||||
format_string += ",bold"
|
||||
if underline:
|
||||
format_string += ",underline"
|
||||
if italic:
|
||||
format_string += ",italics"
|
||||
|
||||
name = "micron_"+fg+"_"+bg+"_"+format_string
|
||||
if not name in SYNTH_STYLES:
|
||||
screen = nomadnet.NomadNetworkApp.get_shared_instance().ui.screen
|
||||
screen.register_palette_entry(name, low_color(fg)+format_string,low_color(bg),mono_color(fg, bg)+format_string,high_color(fg)+format_string,high_color(bg))
|
||||
SYNTH_STYLES.append(name)
|
||||
|
||||
return name
|
||||
|
||||
def make_output(state, line):
|
||||
output = []
|
||||
if state["literal"]:
|
||||
if line == "\\`=":
|
||||
line = "`="
|
||||
output.append(make_part(state, line))
|
||||
else:
|
||||
part = ""
|
||||
mode = "text"
|
||||
escape = False
|
||||
skip = 0
|
||||
for i in range(0, len(line)):
|
||||
c = line[i]
|
||||
if skip > 0:
|
||||
skip -= 1
|
||||
else:
|
||||
if mode == "formatting":
|
||||
if c == "_":
|
||||
state["formatting"]["underline"] ^= True
|
||||
elif c == "!":
|
||||
state["formatting"]["bold"] ^= True
|
||||
elif c == "*":
|
||||
state["formatting"]["italic"] ^= True
|
||||
elif c == "F":
|
||||
if len(line) >= i+4:
|
||||
color = line[i+1:i+4]
|
||||
state["fg_color"] = color
|
||||
skip = 3
|
||||
elif c == "f":
|
||||
state["fg_color"] = "default"
|
||||
elif c == "B":
|
||||
if len(line) >= i+4:
|
||||
color = line[i+1:i+4]
|
||||
state["bg_color"] = color
|
||||
skip = 3
|
||||
elif c == "b":
|
||||
state["bg_color"] = "default"
|
||||
elif c == "`":
|
||||
state["formatting"]["bold"] = False
|
||||
state["formatting"]["underline"] = False
|
||||
state["formatting"]["italic"] = False
|
||||
state["fg_color"] = "default"
|
||||
state["bg_color"] = "default"
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "c":
|
||||
if state["align"] != "center":
|
||||
state["align"] = "center"
|
||||
else:
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "l":
|
||||
if state["align"] != "left":
|
||||
state["align"] = "left"
|
||||
else:
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "r":
|
||||
if state["align"] != "right":
|
||||
state["align"] = "right"
|
||||
else:
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "a":
|
||||
state["align"] = state["default_align"]
|
||||
|
||||
mode = "text"
|
||||
if len(part) > 0:
|
||||
output.append(make_part(state, part))
|
||||
|
||||
elif mode == "text":
|
||||
if c == "\\":
|
||||
escape = True
|
||||
elif c == "`":
|
||||
if escape:
|
||||
part += c
|
||||
escape = False
|
||||
else:
|
||||
mode = "formatting"
|
||||
if len(part) > 0:
|
||||
output.append(make_part(state, part))
|
||||
part = ""
|
||||
else:
|
||||
part += c
|
||||
|
||||
if i == len(line)-1:
|
||||
if len(part) > 0:
|
||||
output.append(make_part(state, part))
|
||||
|
||||
if len(output) > 0:
|
||||
return output
|
||||
else:
|
||||
return None
|
421
nomadnet/ui/textui/Scrollable.py
Normal file
421
nomadnet/ui/textui/Scrollable.py
Normal file
@ -0,0 +1,421 @@
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details
|
||||
# http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
import urwid
|
||||
from urwid.widget import BOX, FIXED, FLOW
|
||||
|
||||
# Scroll actions
|
||||
SCROLL_LINE_UP = 'line up'
|
||||
SCROLL_LINE_DOWN = 'line down'
|
||||
SCROLL_PAGE_UP = 'page up'
|
||||
SCROLL_PAGE_DOWN = 'page down'
|
||||
SCROLL_TO_TOP = 'to top'
|
||||
SCROLL_TO_END = 'to end'
|
||||
|
||||
# Scrollbar positions
|
||||
SCROLLBAR_LEFT = 'left'
|
||||
SCROLLBAR_RIGHT = 'right'
|
||||
|
||||
class Scrollable(urwid.WidgetDecoration):
|
||||
def sizing(self):
|
||||
return frozenset([BOX,])
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def __init__(self, widget):
|
||||
"""Box widget that makes a fixed or flow widget vertically scrollable
|
||||
|
||||
TODO: Focusable widgets are handled, including switching focus, but
|
||||
possibly not intuitively, depending on the arrangement of widgets. When
|
||||
switching focus to a widget that is ouside of the visible part of the
|
||||
original widget, the canvas scrolls up/down to the focused widget. It
|
||||
would be better to scroll until the next focusable widget is in sight
|
||||
first. But for that to work we must somehow obtain a list of focusable
|
||||
rows in the original canvas.
|
||||
"""
|
||||
if not any(s in widget.sizing() for s in (FIXED, FLOW)):
|
||||
raise ValueError('Not a fixed or flow widget: %r' % widget)
|
||||
self._trim_top = 0
|
||||
self._scroll_action = None
|
||||
self._forward_keypress = None
|
||||
self._old_cursor_coords = None
|
||||
self._rows_max_cached = 0
|
||||
self.__super.__init__(widget)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
maxcol, maxrow = size
|
||||
|
||||
# Render complete original widget
|
||||
ow = self._original_widget
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
canv_full = ow.render(ow_size, focus)
|
||||
|
||||
# Make full canvas editable
|
||||
canv = urwid.CompositeCanvas(canv_full)
|
||||
canv_cols, canv_rows = canv.cols(), canv.rows()
|
||||
|
||||
if canv_cols <= maxcol:
|
||||
pad_width = maxcol - canv_cols
|
||||
if pad_width > 0:
|
||||
# Canvas is narrower than available horizontal space
|
||||
canv.pad_trim_left_right(0, pad_width)
|
||||
|
||||
if canv_rows <= maxrow:
|
||||
fill_height = maxrow - canv_rows
|
||||
if fill_height > 0:
|
||||
# Canvas is lower than available vertical space
|
||||
canv.pad_trim_top_bottom(0, fill_height)
|
||||
|
||||
if canv_cols <= maxcol and canv_rows <= maxrow:
|
||||
# Canvas is small enough to fit without trimming
|
||||
return canv
|
||||
|
||||
self._adjust_trim_top(canv, size)
|
||||
|
||||
# Trim canvas if necessary
|
||||
trim_top = self._trim_top
|
||||
trim_end = canv_rows - maxrow - trim_top
|
||||
trim_right = canv_cols - maxcol
|
||||
if trim_top > 0:
|
||||
canv.trim(trim_top)
|
||||
if trim_end > 0:
|
||||
canv.trim_end(trim_end)
|
||||
if trim_right > 0:
|
||||
canv.pad_trim_left_right(0, -trim_right)
|
||||
|
||||
# Disable cursor display if cursor is outside of visible canvas parts
|
||||
if canv.cursor is not None:
|
||||
curscol, cursrow = canv.cursor
|
||||
if cursrow >= maxrow or cursrow < 0:
|
||||
canv.cursor = None
|
||||
|
||||
# Figure out whether we should forward keypresses to original widget
|
||||
if canv.cursor is not None:
|
||||
# Trimmed canvas contains the cursor, e.g. in an Edit widget
|
||||
self._forward_keypress = True
|
||||
else:
|
||||
if canv_full.cursor is not None:
|
||||
# Full canvas contains the cursor, but scrolled out of view
|
||||
self._forward_keypress = False
|
||||
else:
|
||||
# Original widget does not have a cursor, but may be selectable
|
||||
|
||||
# FIXME: Using ow.selectable() is bad because the original
|
||||
# widget may be selectable because it's a container widget with
|
||||
# a key-grabbing widget that is scrolled out of view.
|
||||
# ow.selectable() returns True anyway because it doesn't know
|
||||
# how we trimmed our canvas.
|
||||
#
|
||||
# To fix this, we need to resolve ow.focus and somehow
|
||||
# ask canv whether it contains bits of the focused widget. I
|
||||
# can't see a way to do that.
|
||||
if ow.selectable():
|
||||
self._forward_keypress = True
|
||||
else:
|
||||
self._forward_keypress = False
|
||||
|
||||
return canv
|
||||
|
||||
def keypress(self, size, key):
|
||||
# Maybe offer key to original widget
|
||||
if self._forward_keypress:
|
||||
ow = self._original_widget
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
|
||||
# Remember previous cursor position if possible
|
||||
if hasattr(ow, 'get_cursor_coords'):
|
||||
self._old_cursor_coords = ow.get_cursor_coords(ow_size)
|
||||
|
||||
key = ow.keypress(ow_size, key)
|
||||
if key is None:
|
||||
return None
|
||||
|
||||
# Handle up/down, page up/down, etc
|
||||
command_map = self._command_map
|
||||
if command_map[key] == urwid.CURSOR_UP:
|
||||
self._scroll_action = SCROLL_LINE_UP
|
||||
elif command_map[key] == urwid.CURSOR_DOWN:
|
||||
self._scroll_action = SCROLL_LINE_DOWN
|
||||
|
||||
elif command_map[key] == urwid.CURSOR_PAGE_UP:
|
||||
self._scroll_action = SCROLL_PAGE_UP
|
||||
elif command_map[key] == urwid.CURSOR_PAGE_DOWN:
|
||||
self._scroll_action = SCROLL_PAGE_DOWN
|
||||
|
||||
elif command_map[key] == urwid.CURSOR_MAX_LEFT: # 'home'
|
||||
self._scroll_action = SCROLL_TO_TOP
|
||||
elif command_map[key] == urwid.CURSOR_MAX_RIGHT: # 'end'
|
||||
self._scroll_action = SCROLL_TO_END
|
||||
|
||||
else:
|
||||
return key
|
||||
|
||||
self._invalidate()
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
ow = self._original_widget
|
||||
if hasattr(ow, 'mouse_event'):
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
row += self._trim_top
|
||||
return ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _adjust_trim_top(self, canv, size):
|
||||
"""Adjust self._trim_top according to self._scroll_action"""
|
||||
action = self._scroll_action
|
||||
self._scroll_action = None
|
||||
|
||||
maxcol, maxrow = size
|
||||
trim_top = self._trim_top
|
||||
canv_rows = canv.rows()
|
||||
|
||||
if trim_top < 0:
|
||||
# Negative trim_top values use bottom of canvas as reference
|
||||
trim_top = canv_rows - maxrow + trim_top + 1
|
||||
|
||||
if canv_rows <= maxrow:
|
||||
self._trim_top = 0 # Reset scroll position
|
||||
return
|
||||
|
||||
def ensure_bounds(new_trim_top):
|
||||
return max(0, min(canv_rows - maxrow, new_trim_top))
|
||||
|
||||
if action == SCROLL_LINE_UP:
|
||||
self._trim_top = ensure_bounds(trim_top - 1)
|
||||
elif action == SCROLL_LINE_DOWN:
|
||||
self._trim_top = ensure_bounds(trim_top + 1)
|
||||
|
||||
elif action == SCROLL_PAGE_UP:
|
||||
self._trim_top = ensure_bounds(trim_top - maxrow + 1)
|
||||
elif action == SCROLL_PAGE_DOWN:
|
||||
self._trim_top = ensure_bounds(trim_top + maxrow - 1)
|
||||
|
||||
elif action == SCROLL_TO_TOP:
|
||||
self._trim_top = 0
|
||||
elif action == SCROLL_TO_END:
|
||||
self._trim_top = canv_rows - maxrow
|
||||
|
||||
else:
|
||||
self._trim_top = ensure_bounds(trim_top)
|
||||
|
||||
# If the cursor was moved by the most recent keypress, adjust trim_top
|
||||
# so that the new cursor position is within the displayed canvas part.
|
||||
# But don't do this if the cursor is at the top/bottom edge so we can still scroll out
|
||||
if self._old_cursor_coords is not None and self._old_cursor_coords != canv.cursor:
|
||||
self._old_cursor_coords = None
|
||||
curscol, cursrow = canv.cursor
|
||||
if cursrow < self._trim_top:
|
||||
self._trim_top = cursrow
|
||||
elif cursrow >= self._trim_top + maxrow:
|
||||
self._trim_top = max(0, cursrow - maxrow + 1)
|
||||
|
||||
def _get_original_widget_size(self, size):
|
||||
ow = self._original_widget
|
||||
sizing = ow.sizing()
|
||||
if FIXED in sizing:
|
||||
return ()
|
||||
elif FLOW in sizing:
|
||||
return (size[0],)
|
||||
|
||||
def get_scrollpos(self, size=None, focus=False):
|
||||
"""Current scrolling position
|
||||
|
||||
Lower limit is 0, upper limit is the maximum number of rows with the
|
||||
given maxcol minus maxrow.
|
||||
|
||||
NOTE: The returned value may be too low or too high if the position has
|
||||
changed but the widget wasn't rendered yet.
|
||||
"""
|
||||
return self._trim_top
|
||||
|
||||
def set_scrollpos(self, position):
|
||||
"""Set scrolling position
|
||||
|
||||
If `position` is positive it is interpreted as lines from the top.
|
||||
If `position` is negative it is interpreted as lines from the bottom.
|
||||
|
||||
Values that are too high or too low values are automatically adjusted
|
||||
during rendering.
|
||||
"""
|
||||
self._trim_top = int(position)
|
||||
self._invalidate()
|
||||
|
||||
def rows_max(self, size=None, focus=False):
|
||||
"""Return the number of rows for `size`
|
||||
|
||||
If `size` is not given, the currently rendered number of rows is returned.
|
||||
"""
|
||||
if size is not None:
|
||||
ow = self._original_widget
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
sizing = ow.sizing()
|
||||
if FIXED in sizing:
|
||||
self._rows_max_cached = ow.pack(ow_size, focus)[1]
|
||||
elif FLOW in sizing:
|
||||
self._rows_max_cached = ow.rows(ow_size, focus)
|
||||
else:
|
||||
raise RuntimeError('Not a flow/box widget: %r' % self._original_widget)
|
||||
return self._rows_max_cached
|
||||
|
||||
|
||||
class ScrollBar(urwid.WidgetDecoration):
|
||||
def sizing(self):
|
||||
return frozenset((BOX,))
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def __init__(self, widget, thumb_char=u'\u2588', trough_char=' ',
|
||||
side=SCROLLBAR_RIGHT, width=1):
|
||||
"""Box widget that adds a scrollbar to `widget`
|
||||
|
||||
`widget` must be a box widget with the following methods:
|
||||
- `get_scrollpos` takes the arguments `size` and `focus` and returns
|
||||
the index of the first visible row.
|
||||
- `set_scrollpos` (optional; needed for mouse click support) takes the
|
||||
index of the first visible row.
|
||||
- `rows_max` takes `size` and `focus` and returns the total number of
|
||||
rows `widget` can render.
|
||||
|
||||
`thumb_char` is the character used for the scrollbar handle.
|
||||
`trough_char` is used for the space above and below the handle.
|
||||
`side` must be 'left' or 'right'.
|
||||
`width` specifies the number of columns the scrollbar uses.
|
||||
"""
|
||||
if BOX not in widget.sizing():
|
||||
raise ValueError('Not a box widget: %r' % widget)
|
||||
self.__super.__init__(widget)
|
||||
self._thumb_char = thumb_char
|
||||
self._trough_char = trough_char
|
||||
self.scrollbar_side = side
|
||||
self.scrollbar_width = max(1, width)
|
||||
self._original_widget_size = (0, 0)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
maxcol, maxrow = size
|
||||
|
||||
sb_width = self._scrollbar_width
|
||||
ow_size = (max(0, maxcol - sb_width), maxrow)
|
||||
sb_width = maxcol - ow_size[0]
|
||||
|
||||
ow = self._original_widget
|
||||
ow_base = self.scrolling_base_widget
|
||||
ow_rows_max = ow_base.rows_max(size, focus)
|
||||
if ow_rows_max <= maxrow:
|
||||
# Canvas fits without scrolling - no scrollbar needed
|
||||
self._original_widget_size = size
|
||||
return ow.render(size, focus)
|
||||
ow_rows_max = ow_base.rows_max(ow_size, focus)
|
||||
|
||||
ow_canv = ow.render(ow_size, focus)
|
||||
self._original_widget_size = ow_size
|
||||
|
||||
pos = ow_base.get_scrollpos(ow_size, focus)
|
||||
posmax = ow_rows_max - maxrow
|
||||
|
||||
# Thumb shrinks/grows according to the ratio of
|
||||
# <number of visible lines> / <number of total lines>
|
||||
thumb_weight = min(1, maxrow / max(1, ow_rows_max))
|
||||
thumb_height = max(1, round(thumb_weight * maxrow))
|
||||
|
||||
# Thumb may only touch top/bottom if the first/last row is visible
|
||||
top_weight = float(pos) / max(1, posmax)
|
||||
top_height = int((maxrow - thumb_height) * top_weight)
|
||||
if top_height == 0 and top_weight > 0:
|
||||
top_height = 1
|
||||
|
||||
# Bottom part is remaining space
|
||||
bottom_height = maxrow - thumb_height - top_height
|
||||
assert thumb_height + top_height + bottom_height == maxrow
|
||||
|
||||
# Create scrollbar canvas
|
||||
# Creating SolidCanvases of correct height may result in "cviews do not
|
||||
# fill gaps in shard_tail!" or "cviews overflow gaps in shard_tail!"
|
||||
# exceptions. Stacking the same SolidCanvas is a workaround.
|
||||
# https://github.com/urwid/urwid/issues/226#issuecomment-437176837
|
||||
top = urwid.SolidCanvas(self._trough_char, sb_width, 1)
|
||||
thumb = urwid.SolidCanvas(self._thumb_char, sb_width, 1)
|
||||
bottom = urwid.SolidCanvas(self._trough_char, sb_width, 1)
|
||||
sb_canv = urwid.CanvasCombine(
|
||||
[(top, None, False)] * top_height +
|
||||
[(thumb, None, False)] * thumb_height +
|
||||
[(bottom, None, False)] * bottom_height,
|
||||
)
|
||||
|
||||
combinelist = [(ow_canv, None, True, ow_size[0]),
|
||||
(sb_canv, None, False, sb_width)]
|
||||
if self._scrollbar_side != SCROLLBAR_LEFT:
|
||||
return urwid.CanvasJoin(combinelist)
|
||||
else:
|
||||
return urwid.CanvasJoin(reversed(combinelist))
|
||||
|
||||
@property
|
||||
def scrollbar_width(self):
|
||||
"""Columns the scrollbar uses"""
|
||||
return max(1, self._scrollbar_width)
|
||||
|
||||
@scrollbar_width.setter
|
||||
def scrollbar_width(self, width):
|
||||
self._scrollbar_width = max(1, int(width))
|
||||
self._invalidate()
|
||||
|
||||
@property
|
||||
def scrollbar_side(self):
|
||||
"""Where to display the scrollbar; must be 'left' or 'right'"""
|
||||
return self._scrollbar_side
|
||||
|
||||
@scrollbar_side.setter
|
||||
def scrollbar_side(self, side):
|
||||
if side not in (SCROLLBAR_LEFT, SCROLLBAR_RIGHT):
|
||||
raise ValueError('scrollbar_side must be "left" or "right", not %r' % side)
|
||||
self._scrollbar_side = side
|
||||
self._invalidate()
|
||||
|
||||
@property
|
||||
def scrolling_base_widget(self):
|
||||
"""Nearest `original_widget` that is compatible with the scrolling API"""
|
||||
def orig_iter(w):
|
||||
while hasattr(w, 'original_widget'):
|
||||
w = w.original_widget
|
||||
yield w
|
||||
yield w
|
||||
|
||||
def is_scrolling_widget(w):
|
||||
return hasattr(w, 'get_scrollpos') and hasattr(w, 'rows_max')
|
||||
|
||||
for w in orig_iter(self):
|
||||
if is_scrolling_widget(w):
|
||||
return w
|
||||
raise ValueError('Not compatible to be wrapped by ScrollBar: %r' % w)
|
||||
|
||||
def keypress(self, size, key):
|
||||
return self._original_widget.keypress(self._original_widget_size, key)
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
ow = self._original_widget
|
||||
ow_size = self._original_widget_size
|
||||
handled = False
|
||||
if hasattr(ow, 'mouse_event'):
|
||||
handled = ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||
|
||||
if not handled and hasattr(ow, 'set_scrollpos'):
|
||||
if button == 4: # scroll wheel up
|
||||
pos = ow.get_scrollpos(ow_size)
|
||||
ow.set_scrollpos(pos - 1)
|
||||
return True
|
||||
elif button == 5: # scroll wheel down
|
||||
pos = ow.get_scrollpos(ow_size)
|
||||
ow.set_scrollpos(pos + 1)
|
||||
return True
|
||||
|
||||
return False
|
Loading…
Reference in New Issue
Block a user