Преглед изворни кода

键盘加 ⇧ 层:不常用的功能收进 shift,含排列数与组合数

- 右下角新增 ⇧ 键。开着的时候键盘整体换成 shift 层(有 shift 层的键显示备用
  功能),按一次只生效一次,跟计算器上的 shift 一样
- shift 层:∫→∏(连乘)、solve→nsolve、sin/cos/tan→asin/acos/atan、ln→lg、
  √→|x|、`(`→P(排列数 FactorialPower[n,k])、`!`→C(组合数 Binomial[n,k])
- 排列数/组合数直接给出两个参数槽,填完第一个按逗号进第二个
  (FactorialPower[5,2]=20、Binomial[5,2]=10,都在 JVM 上验过)

Co-Authored-By: Claude Code <noreply@anthropic.com>
nonvioce пре 3 недеља
родитељ
комит
1cab0d65d1
2 измењених фајлова са 81 додато и 11 уклоњено
  1. 53 11
      lib/calculator_page.dart
  2. 28 0
      test/widget_test.dart

+ 53 - 11
lib/calculator_page.dart

@@ -210,8 +210,23 @@ class _CalculatorPageState extends State<CalculatorPage> {
 
   // ------------------------------------------------------------------ 按键
 
-  void _press(_Key k) {
+  /// ⇧ 层是否开着。按一次只生效一次(跟计算器上的 shift 一样)。
+  bool _shift = false;
+
+  void _press(_Key raw) {
+    // shift 键本身:切一下层,不往表达式里写东西
+    if (raw.act == _Act.shift) {
+      setState(() => _shift = !_shift);
+      return;
+    }
+    // 开着 shift 且这个键有 shift 层:按 shift 层那个动作,然后复位
+    final k = (_shift && raw.shifted != null) ? raw.shifted! : raw;
+    if (_shift) setState(() => _shift = false);
+
     switch (k.act) {
+      case _Act.shift:
+        return; // 上面已经处理过,这里只是让 switch 穷尽
+
       case _Act.char:
         if (k.mergeDigit) {
           // 数字键并入左边那个数字字面量,`42` 才是一个数而不是两个节点
@@ -445,20 +460,30 @@ class _CalculatorPageState extends State<CalculatorPage> {
         final rows = <List<_Key>>[
           // 1 大运算符
           [
-            _Key('∫', _Act.bigOp, payload: 'Integrate', prefill: ['x']),
+            _Key('∫', _Act.bigOp, payload: 'Integrate', prefill: ['x'],
+                shifted: _Key('∏', _Act.bigOp,
+                    payload: 'Product', prefill: ['n', '1', 'Infinity'])),
             _Key('Σ', _Act.bigOp,
                 payload: 'Sum', prefill: ['n', '1', 'Infinity']),
             _Key('d/dx', _Act.fnPrefixed, payload: 'D', prefill: ['', 'x']),
             _Key('lim', _Act.bigOp, payload: 'Limit', prefill: ['x', '0']),
-            _Key('solve', _Act.fnPrefixed, payload: 'Solve', prefill: ['', 'x']),
+            _Key('solve', _Act.fnPrefixed, payload: 'Solve',
+                prefill: ['', 'x'],
+                shifted: _Key('nsolve', _Act.fnPrefixed, payload: 'NSolve',
+                    prefill: ['', 'x'])),
           ],
           // 2 常用函数
           [
-            _Key('sin', _Act.fn, payload: 'Sin'),
-            _Key('cos', _Act.fn, payload: 'Cos'),
-            _Key('tan', _Act.fn, payload: 'Tan'),
-            _Key('ln', _Act.fn, payload: 'Log'),
-            _Key('√', _Act.sqrt),
+            _Key('sin', _Act.fn, payload: 'Sin',
+                shifted: _Key('asin', _Act.fn, payload: 'ArcSin')),
+            _Key('cos', _Act.fn, payload: 'Cos',
+                shifted: _Key('acos', _Act.fn, payload: 'ArcCos')),
+            _Key('tan', _Act.fn, payload: 'Tan',
+                shifted: _Key('atan', _Act.fn, payload: 'ArcTan')),
+            _Key('ln', _Act.fn, payload: 'Log',
+                shifted: _Key('lg', _Act.fn, payload: 'Log10')),
+            _Key('√', _Act.sqrt,
+                shifted: _Key('|x|', _Act.fn, payload: 'Abs')),
           ],
           // 3 变量
           [
@@ -484,8 +509,12 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('7', _Act.char, payload: '7', mergeDigit: true),
             _Key('8', _Act.char, payload: '8', mergeDigit: true),
             _Key('9', _Act.char, payload: '9', mergeDigit: true),
-            _Key('(', _Act.group),
-            _Key('!', _Act.postfix),
+            _Key('(', _Act.group,
+                shifted: _Key('P', _Act.fnPrefixed,
+                    payload: 'FactorialPower', prefill: ['', ''])),
+            _Key('!', _Act.postfix,
+                shifted: _Key('C', _Act.fnPrefixed,
+                    payload: 'Binomial', prefill: ['', ''])),
           ],
           [
             _Key('4', _Act.char, payload: '4', mergeDigit: true),
@@ -511,6 +540,8 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('e', _Act.char, payload: 'E', tint: true, display: 'e'),
           ],
           [
+            // ⇧ 开着的时候这个键是深色的 —— 一眼能看出"现在是 shift 层"
+            _Key('⇧', _Act.shift, tint: true, primary: _shift),
             _Key('⌫', _Act.back, tint: true),
             _Key('CLR', _Act.clear, tint: true),
             // 虚数单位。送进 Symja 的是 I,编辑框里显示小写 i
@@ -549,7 +580,10 @@ class _CalculatorPageState extends State<CalculatorPage> {
     );
   }
 
-  Widget _keyButton(_Key k) {
+  Widget _keyButton(_Key raw) {
+    // shift 开着就整体换成 shift 层:有 shift 层的键显示备用功能,
+    // 没有的保持原样。这样"按一下看看有什么"就能发现 shift 层。
+    final k = (_shift && raw.shifted != null) ? raw.shifted! : raw;
     final scheme = Theme.of(context).colorScheme;
     // E-Ink 下按键的底色是白的(配色压成了白底黑字),跟背景一个颜色就看不见了,
     // 所以改成**描边**区分:功能键/提交键一圈黑,数字键一圈浅灰。
@@ -604,6 +638,7 @@ class _CalculatorPageState extends State<CalculatorPage> {
 
 enum _Act {
   char,
+  shift,
   fn,
   fnPrefixed,
   bigOp,
@@ -642,6 +677,12 @@ class _Key {
   final bool tint;
   final bool primary;
 
+  /// 按了 ⇧ 之后这个键变成什么。null = 这个键没有 shift 层。
+  ///
+  /// 用"整个备用键"而不是几个零散字段:shift 前后的动作类型本来就可能不同
+  /// (`√` 是 sqrt、shift 之后是普通函数 `Abs`),嵌套一个 _Key 最省事。
+  final _Key? shifted;
+
   const _Key(
     this.label,
     this.act, {
@@ -652,6 +693,7 @@ class _Key {
     this.mathVar = false,
     this.tint = false,
     this.primary = false,
+    this.shifted,
   });
 }
 

+ 28 - 0
test/widget_test.dart

@@ -351,6 +351,34 @@ void main() {
       expect(await lastExpr(tester), 'Det[{{1,2}}]');
     });
 
+    testWidgets('shift 层:C 是组合数、P 是排列数', (tester) async {
+      await pumpApp(tester);
+
+      await press(tester, '⇧');
+      await press(tester, 'C'); // Shif 层的 Binomial[□, □]
+      await press(tester, '5');
+      await press(tester, ',');
+      await press(tester, '2');
+      expect(await lastExpr(tester), 'Binomial[5, 2]');
+
+      await press(tester, 'CLR');
+      await press(tester, '⇧');
+      await press(tester, 'P'); // FactorialPower[□, □]
+      await press(tester, '5');
+      await press(tester, ',');
+      await press(tester, '2');
+      expect(await lastExpr(tester), 'FactorialPower[5, 2]');
+    });
+
+    testWidgets('shift 只吃一次:按完一个键就复位', (tester) async {
+      await pumpApp(tester);
+
+      await press(tester, '⇧');
+      await press(tester, 'asin'); // shift 层里的 sin
+      await press(tester, 'sin'); // 已经复位了,这一下是普通的 Sin
+      expect(await lastExpr(tester), 'ArcSin[Sin[Null]]');
+    });
+
     testWidgets('函数面板能按中文说明搜(不用记得住 Variance)', (tester) async {
       await pumpApp(tester);
       await tester.tap(find.byIcon(Icons.functions));