feat(interface): 组件自动聚焦与键盘操作改进
This commit is contained in:
@@ -51,4 +51,4 @@ class ConfigContext:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
config_var.reset(self._token) # type: ignore
|
||||
config_var.reset(self._token) # type: ignore
|
||||
@@ -6,6 +6,11 @@
|
||||
height: 3;
|
||||
}
|
||||
|
||||
#analysis {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.repo-list-item {
|
||||
layout: grid;
|
||||
grid-size: 1;
|
||||
|
||||
@@ -19,8 +19,9 @@ import sys
|
||||
class AboutScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("z", "go_back", "关于"),
|
||||
]
|
||||
|
||||
SUB_TITLE = "关于"
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer, Horizontal, Vertical
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
|
||||
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static, Markdown
|
||||
from textual import events, on
|
||||
from textual.reactive import reactive
|
||||
|
||||
@@ -74,10 +74,11 @@ class DashboardScreen(Screen):
|
||||
),
|
||||
id="header",
|
||||
)
|
||||
|
||||
yield ListView(id="repo_list", classes="repo-list") # 单元集选择
|
||||
|
||||
from heurams.services.attic import Attic
|
||||
a = Attic('ana', {'totaltime': 0, 'openpuzzles': 0, 'puzzles_err': 0})
|
||||
yield Label(f"版本 {version.ver} {version.stage.capitalize()}") # 版本信息
|
||||
yield Label(f"在 {round(a.data['totaltime'], 2)} 秒内处理了 {a.data['openpuzzles']} 个谜题, 正确率{'无法求解' if not a.data['openpuzzles'] else ' ' + str(round(100 * (1 - a.data['puzzles_err']/a.data['openpuzzles']), 2)) + '%'}, 平均速度{'无法求解' if not a.data['totaltime'] else ' ' + str(round(a.data['openpuzzles']/a.data['totaltime'], 2)) + '个/s'}", id='analysis') # 版本信息
|
||||
yield Footer()
|
||||
|
||||
@on(events.ScreenResume)
|
||||
@@ -201,6 +202,5 @@ class DashboardScreen(Screen):
|
||||
logger.debug(f"event.button.id: {event.button.id}")
|
||||
if event.button.id.startswith("slaunch_repo_"): # type: ignore
|
||||
from .preparation import launch
|
||||
|
||||
launch(repo=self.repolink[event.button.id.lstrip("slaunch_repo_")], app=self.app, scheduled_num=-1) # type: ignore
|
||||
launch(repo=self.repolink[event.button.id.removeprefix("slaunch_repo_")], app=self.app, scheduled_num=-1) # type: ignore
|
||||
# TODO: 这样启动的记忆实例的状态机无法绑定到 PreparationScreen 中
|
||||
|
||||
@@ -16,12 +16,10 @@ from heurams.kernel.reactor import *
|
||||
from heurams.services.favorite_service import favorite_manager
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
|
||||
from .. import shim
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
@@ -29,6 +27,7 @@ class MemScreen(Screen):
|
||||
("d", "toggle_dark", ""),
|
||||
("v", "play_voice", "朗读"),
|
||||
("*", "toggle_favorite", "收藏"),
|
||||
("r", "resume_mark"),
|
||||
("n", "block_prompt"),
|
||||
("s", "block_prompt"),
|
||||
("z", "block_prompt"),
|
||||
@@ -60,10 +59,13 @@ class MemScreen(Screen):
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
import time
|
||||
from heurams.services.attic import Attic
|
||||
a = Attic('ana', {'openqueue': 0})
|
||||
a.data['openqueue'] += 1
|
||||
if config_var.get()['interface']['global']['show_header']:
|
||||
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
|
||||
with ScrollableContainer():
|
||||
@@ -78,6 +80,10 @@ class MemScreen(Screen):
|
||||
|
||||
def on_mount(self):
|
||||
self.expander = self.procession.get_expander()
|
||||
from heurams.services.attic import Attic
|
||||
import time
|
||||
a = Attic('ana', {'last': time.time()})
|
||||
a.data['last'] = time.time()
|
||||
self.mount_puzzle()
|
||||
self.update_display()
|
||||
|
||||
@@ -107,6 +113,7 @@ class MemScreen(Screen):
|
||||
|
||||
def mount_puzzle(self):
|
||||
"""挂载当前谜题组件"""
|
||||
from heurams.services.attic import Attic
|
||||
if self.procession.route == RouterState.FINISHED:
|
||||
self.mount_finished_widget()
|
||||
return
|
||||
@@ -117,6 +124,8 @@ class MemScreen(Screen):
|
||||
|
||||
def mount_finished_widget(self):
|
||||
"""挂载已完成组件"""
|
||||
a = Attic('ana', {'finished': 0})
|
||||
a.data['finished'] += 1
|
||||
container = self.query_one("#puzzle_container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
@@ -168,6 +177,13 @@ class MemScreen(Screen):
|
||||
if self.expander.state == "retronly":
|
||||
self.forward_atom(self.expander.get_quality())
|
||||
self.update_state()
|
||||
from heurams.services.attic import Attic
|
||||
a = Attic('ana', {'openpuzzles': 0})
|
||||
a = Attic('ana', {'totaltime': 0})
|
||||
a.data['openpuzzles'] += 1
|
||||
import time
|
||||
a.data['totaltime'] += time.time() - a.data['last']
|
||||
a.data['last'] = time.time()
|
||||
self.mount_puzzle()
|
||||
self.update_display()
|
||||
|
||||
@@ -187,6 +203,8 @@ class MemScreen(Screen):
|
||||
logger.debug(f"Quality: {quality}")
|
||||
self.atom_reporter(quality)
|
||||
if quality <= 3:
|
||||
a = Attic('ana', {'puzzles_err': 0})
|
||||
a.data['puzzles_err'] += 1
|
||||
self.procession.append()
|
||||
self.update_state() # 刷新状态
|
||||
self.procession.forward(1)
|
||||
@@ -241,3 +259,11 @@ class MemScreen(Screen):
|
||||
|
||||
def action_block_prompt(self):
|
||||
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")
|
||||
|
||||
def action_resume_mark(self):
|
||||
from heurams.services.attic import Attic
|
||||
import time
|
||||
a = Attic('ana')
|
||||
l = a.data['last']
|
||||
a.data['last'] = time.time()
|
||||
self.app.notify(f"时间恢复已修正: {l} -> {a.data['last']}")
|
||||
@@ -51,7 +51,9 @@ class PreparationScreen(Screen):
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
from heurams.services.attic import Attic
|
||||
a = Attic('ana', {'openpre': 0})
|
||||
a.data['openpre'] += 1
|
||||
if config_var.get()['interface']['global']['show_header']:
|
||||
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
|
||||
with ScrollableContainer(id="main_container"):
|
||||
|
||||
@@ -32,6 +32,7 @@ class SettingScreen(Screen):
|
||||
SUB_TITLE = "设置"
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("s", "go_back", "设置"),
|
||||
]
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import TypedDict
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Label, Markdown
|
||||
from textual.events import Key
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
@@ -50,6 +51,7 @@ class ClozePuzzle(BasePuzzleWidget):
|
||||
self.hashtable = {}
|
||||
self.alia = alia
|
||||
self._load()
|
||||
self.btn_shortcuts = {}
|
||||
self.hashmap = dict()
|
||||
|
||||
def _load(self):
|
||||
@@ -67,15 +69,24 @@ class ClozePuzzle(BasePuzzleWidget):
|
||||
yield Label(self.puzzle.wording, id="sentence")
|
||||
yield Markdown(f"> {self.listprint(self.inputlist)}", id="inputpreview")
|
||||
# 渲染当前问题的选项
|
||||
with ScrollableContainer(id="btn-container"):
|
||||
with ScrollableContainer(id="btn-container") as s:
|
||||
c = 0
|
||||
for i in self.ans:
|
||||
h = str(hash(i))
|
||||
if hash(i) in self.hashmap.keys():
|
||||
continue
|
||||
c += 1
|
||||
self.hashmap[h] = i
|
||||
btnid = f"sel000-{h}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
self.btn_shortcuts[f'{c}'] = btnid
|
||||
yield Button(f'[{c}] {i}', id=f"{btnid}")
|
||||
s.focus()
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
self.btn_shortcuts[f'0'] = 'delete'
|
||||
self.btn_shortcuts[f'backspace'] = 'delete'
|
||||
self.btn_shortcuts[f'delete'] = 'delete'
|
||||
|
||||
def listprint(self, lst):
|
||||
s = ""
|
||||
@@ -117,3 +128,10 @@ class ClozePuzzle(BasePuzzleWidget):
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
|
||||
def on_key(self, event: Key) -> None:
|
||||
self.notify(event.key)
|
||||
if event.key in self.btn_shortcuts:
|
||||
btn_id = self.btn_shortcuts.get(event.key)
|
||||
btn_id = '#' + btn_id
|
||||
self.query_one(btn_id, Button).press()
|
||||
|
||||
@@ -9,7 +9,7 @@ import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from heurams.services.hasher import hash
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from textual.events import Key
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -51,6 +51,7 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
self.hashmap = dict()
|
||||
self.cursor = 0
|
||||
self.atom = atom
|
||||
self.btn_shortcuts = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
@@ -75,14 +76,24 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
|
||||
|
||||
# 渲染当前问题的选项
|
||||
with ScrollableContainer(id="btn-container"):
|
||||
c = 0
|
||||
with ScrollableContainer(id="btn-container") as s:
|
||||
for i in current_options:
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}"
|
||||
if i in [' ', '']:
|
||||
continue
|
||||
c += 1
|
||||
h = str(hash(i))
|
||||
self.hashmap[h] = i
|
||||
btnid = f"sel{str(self.cursor).zfill(3)}-{h}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
self.btn_shortcuts[f'{c}'] = f'{btnid}'
|
||||
yield Button(f'[{c}] ' + i, id=f"{btnid}")
|
||||
s.focus()
|
||||
yield Button("退格", id="delete")
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
self.btn_shortcuts['0'] = f'delete'
|
||||
self.btn_shortcuts['delete'] = f'delete'
|
||||
self.btn_shortcuts['backspace'] = f'delete'
|
||||
|
||||
def update_display(self, error=0):
|
||||
# 更新预览标签
|
||||
@@ -140,20 +151,25 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
for child in container.children
|
||||
if hasattr(child, "id") and child.id and child.id.startswith("sel")
|
||||
]
|
||||
|
||||
container.focus()
|
||||
for button in buttons_to_remove:
|
||||
logger.info(button)
|
||||
container.remove_children("#" + button.id) # type: ignore
|
||||
|
||||
# 添加当前题目的选项按钮
|
||||
c = 0
|
||||
current_question_index = len(self.inputlist)
|
||||
if current_question_index < len(self.puzzle.options):
|
||||
current_options = self.puzzle.options[current_question_index]
|
||||
for option in current_options:
|
||||
if option in ['', ' ']:
|
||||
continue
|
||||
c += 1
|
||||
button_id = f"sel{str(self.cursor).zfill(3)}-{hash(option)}"
|
||||
if button_id not in self.hashmap:
|
||||
self.hashmap[button_id[7:]] = option
|
||||
new_button = Button(option, id=button_id)
|
||||
new_button = Button(f"[{c}] " + option, id=button_id)
|
||||
self.btn_shortcuts[f"{c}"] = button_id
|
||||
container.mount(new_button)
|
||||
|
||||
def handler(self, rating):
|
||||
@@ -161,3 +177,10 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
|
||||
def on_key(self, event: Key) -> None:
|
||||
self.notify(event.key)
|
||||
if event.key in self.btn_shortcuts:
|
||||
btn_id = self.btn_shortcuts.get(event.key)
|
||||
btn_id = '#' + btn_id
|
||||
self.query_one(btn_id, Button).press()
|
||||
@@ -49,7 +49,7 @@ class Recognition(BasePuzzleWidget):
|
||||
def compose(self):
|
||||
from heurams.context import config_var
|
||||
|
||||
autovoice = config_var.get()["interface"]["widgets"]["autovoice"]
|
||||
autovoice = config_var.get()["interface"]["widgets"]['recognition']["autovoice"]
|
||||
if autovoice:
|
||||
self.screen.action_play_voice() # type: ignore
|
||||
cfg: RecognitionConfig = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||
@@ -96,8 +96,9 @@ class Recognition(BasePuzzleWidget):
|
||||
if isinstance(item, str):
|
||||
yield Markdown(item)
|
||||
|
||||
with Center():
|
||||
yield Button("我已知晓", id="ok")
|
||||
with Center() as c:
|
||||
with Button("我已知晓", id="ok") as b:
|
||||
b.focus()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "ok":
|
||||
|
||||
@@ -72,7 +72,7 @@ class MCQPuzzle(BasePuzzle):
|
||||
|
||||
# 确保至少有4个干扰项
|
||||
while len(self.jammer) < 4:
|
||||
self.jammer.append(" " * (4 - len(self.jammer)))
|
||||
self.jammer.append("")
|
||||
|
||||
unique_jammers = set(jammer + list(self.mapping.values()))
|
||||
|
||||
|
||||
54
src/heurams/services/attic.py
Normal file
54
src/heurams/services/attic.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# Attic 服务
|
||||
import pickle as pkl
|
||||
from heurams.services.logger import get_logger
|
||||
from heurams.context import config_var
|
||||
from pathlib import Path
|
||||
from heurams.services.hasher import get_md5
|
||||
import atexit
|
||||
from heurams.services import timer
|
||||
from heurams.services.exceptions import WTFException
|
||||
logger = get_logger(__name__)
|
||||
|
||||
def singleton(cls):
|
||||
instances = {}
|
||||
def get_instance(ident, default):
|
||||
key = ident
|
||||
if key not in instances:
|
||||
instances[key] = cls(ident)
|
||||
instances[key].patch_dict(default)
|
||||
return instances[key]
|
||||
return get_instance
|
||||
|
||||
atticdir = Path(config_var.get()['global']['paths']['misc']) / 'attics'
|
||||
atticdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@singleton
|
||||
class Attic:
|
||||
def __init__(self, ident, default:dict={}):
|
||||
self.ident = ident
|
||||
self.ident = self.ident.replace('<DAYSTAMP>', str(timer.get_daystamp()))
|
||||
self.ident = self.ident.replace('<TIMESTAMP>', str(timer.get_timestamp()))
|
||||
if '<' in ident or '>' in ident:
|
||||
raise WTFException
|
||||
#self.ident = get_md5(self.ident)
|
||||
self.pklpath = atticdir / f'{self.ident}.pkl'
|
||||
atexit.register(self.save)
|
||||
self.data = default
|
||||
if self.pklpath.exists():
|
||||
try:
|
||||
self.load()
|
||||
return
|
||||
except:
|
||||
self.pklpath.unlink(missing_ok=True)
|
||||
self.pklpath.touch(exist_ok=True)
|
||||
|
||||
def patch_dict(self, dct):
|
||||
self.data.update({k: v for k, v in dct.items() if k not in self.data})
|
||||
|
||||
def save(self):
|
||||
with open(atticdir / f'{self.ident}.pkl', 'wb') as f:
|
||||
pkl.dump(self.data, f)
|
||||
|
||||
def load(self):
|
||||
with open(atticdir / f'{self.ident}.pkl', 'rb') as f:
|
||||
self.data.update(dict(pkl.load(f)))
|
||||
@@ -62,8 +62,7 @@ class FavoriteManager:
|
||||
|
||||
def _get_file_path(self) -> Path:
|
||||
"""获取收藏文件路径"""
|
||||
config_path = Path(config_var.get()["global"]["paths"]["data"])
|
||||
fav_path = config_path / "global" / "favorites.json"
|
||||
fav_path = Path(config_var.get()["global"]["paths"]["misc"]) / "favorites.json5"
|
||||
fav_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return fav_path
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ def get_md5(text):
|
||||
|
||||
|
||||
def hash(text):
|
||||
logger.debug(f"计算MD5-时间复合哈希, 输入`{text}`")
|
||||
result = hashlib.md5(f"{text}{random.randint(0,1000)}".encode("utf-8")).hexdigest()
|
||||
logger.debug("哈希结果: %s...", result[:8])
|
||||
return result
|
||||
#logger.debug(f"计算MD5-时间复合哈希, 输入`{text}`")
|
||||
#result = hashlib.md5(f"{text}{random.randint(0,1000)}".encode("utf-8")).hexdigest()
|
||||
#logger.debug("哈希结果: %s...", result[:8])
|
||||
#return result
|
||||
return get_md5(text)
|
||||
Reference in New Issue
Block a user