aboutsummaryrefslogtreecommitdiffstats
path: root/klippy/extras/display
diff options
context:
space:
mode:
authorJanar Sööt <janar.soot@gmail.com>2020-08-09 16:29:55 +0300
committerGitHub <noreply@github.com>2020-08-09 09:29:55 -0400
commitd991b4c83bf02ec86d51532fdefc234f507e9397 (patch)
treebc4160af9c1928bccfb24b08e932ffd9906b1d5b /klippy/extras/display
parentefebbb9a2f7e43eee1d4a68b9d3821a2f3ca0b76 (diff)
downloadkutter-d991b4c83bf02ec86d51532fdefc234f507e9397.tar.gz
kutter-d991b4c83bf02ec86d51532fdefc234f507e9397.tar.xz
kutter-d991b4c83bf02ec86d51532fdefc234f507e9397.zip
menu: Replace menu with new Jinja2 template system (#2344)
menu.cfg: - jinja2 template scripting - new Setup menu - new Calibration menu menu: - redesigned menu code - jinja2 support - option to reverse menu up and down directions - functionality set to support menu injection from other modules - a new way of defining menu hierarchy - other adjustments Signed-off-by: Janar Sööt <janar.soot@gmail.com>
Diffstat (limited to 'klippy/extras/display')
-rw-r--r--klippy/extras/display/display.py2
-rw-r--r--klippy/extras/display/menu.cfg778
-rw-r--r--klippy/extras/display/menu.py1788
3 files changed, 1143 insertions, 1425 deletions
diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py
index 3bdc47ea..11f60a57 100644
--- a/klippy/extras/display/display.py
+++ b/klippy/extras/display/display.py
@@ -87,7 +87,7 @@ class PrinterLCD:
name = config.get_name()
if name == 'display':
# only load menu for primary display
- self.menu = menu.MenuManager(config, self.lcd_chip)
+ self.menu = menu.MenuManager(config, self)
self.printer.load_object(config, "display_status")
# Configurable display
self.display_templates = {}
diff --git a/klippy/extras/display/menu.cfg b/klippy/extras/display/menu.cfg
index 56de02bd..95b3c29b 100644
--- a/klippy/extras/display/menu.cfg
+++ b/klippy/extras/display/menu.cfg
@@ -2,620 +2,726 @@
# See the "example-menu.cfg" file for description of common config parameters.
### DEFAULT MENU ###
+# Main
+# + Tune
+# + Speed: 000%
+# + Flow: 000%
+# + Offset Z:00.00
+# + OctoPrint
+# + Pause printing
+# + Resume printing
+# + Abort printing
+# + SD Card
+# + Start printing
+# + Resume printing
+# + Pause printing
+# + ... (files)
+# + Control
+# + Home All
+# + Home Z
+# + Home X/Y
+# + Steppers off
+# + Fan: OFF
+# + Fan speed: 000%
+# + Lights: OFF
+# + Lights: 000%
+# + Move 10mm
+# + Move X:000.0
+# + Move Y:000.0
+# + Move Z:000.0
+# + Move E:+000.0
+# + Move 1mm
+# + Move X:000.0
+# + Move Y:000.0
+# + Move Z:000.0
+# + Move E:+000.0
+# + Move 0.1mm
+# + Move X:000.0
+# + Move Y:000.0
+# + Move Z:000.0
+# + Move E:+000.0
+# + Temperature
+# + Ex0:000 (0000)
+# + Ex1:000 (0000)
+# + Bed:000 (0000)
+# + Preheat PLA
+# + Preheat all
+# + Preheat hotend
+# + Preheat hotbed
+# + Preheat ABS
+# + Preheat all
+# + Preheat hotend
+# + Preheat hotbed
+# + Cooldown
+# + Cooldown all
+# + Cooldown hotend
+# + Cooldown hotbed
+# + Filament
+# + Ex0:000 (0000)
+# + Load Fil. fast
+# + Load Fil. slow
+# + Unload Fil.fast
+# + Unload Fil.slow
+# + Feed: 000.0
+# + Setup
+# + Save config
+# + Restart
+# + Restart host
+# + Restart FW
+# + PID tuning
+# + Tune Hotend PID
+# + Tune Hotbed PID
+# + Calibration
+# + Delta cal. auto
+# + Delta cal. man
+# + Start probing
+# + Move Z: 000.00
+# + Test Z: ++
+# + Accept
+# + Abort
+# + Bed probe
+# + Dump parameters
### menu main ###
[menu __main]
type: list
-name: Main Menu
-items:
- __tune
- __octoprint
- __sdcard
- __control
- __temp
- __filament
- __prepare
+name: Main
### menu tune ###
-[menu __tune]
+[menu __main __tune]
type: list
-enable: toolhead.is_printing
+enable: {printer.idle_timeout.state == "Printing"}
name: Tune
-items:
- .__speed
- .__flow
- .__offsetz
-[menu __tune __speed]
+[menu __main __tune __speed]
type: input
-name: "Speed: {1:3d}%"
-parameter: gcode.speed_factor
-transform:
- map(0,2,0,200)
+name: Speed: {'%3d' % (menu.input*100)}%
+input: {printer.gcode.speed_factor}
input_min: 0
input_max: 2
input_step: 0.01
-realtime: true
-gcode: M220 S{1:d}
+realtime: True
+gcode:
+ M220 S{'%d' % (menu.input*100)}
-[menu __tune __flow]
+[menu __main __tune __flow]
type: input
-name: "Flow: {1:3d}%"
-parameter: gcode.extrude_factor
-transform:
- map(0,2,0,200)
+name: Flow: {'%3d' % (menu.input*100)}%
+input: {printer.gcode.extrude_factor}
input_min: 0
input_max: 2
input_step: 0.01
-realtime: true
-gcode: M221 S{1:d}
+realtime: True
+gcode:
+ M221 S{'%d' % (menu.input*100)}
-[menu __tune __offsetz]
+[menu __main __tune __offsetz]
type: input
-name: "Offset Z:{0:05.3f} "
-parameter: gcode.homing_zpos
+name: Offset Z:{'%05.3f' % menu.input}
+input: {printer.gcode.homing_zpos}
input_min: -5
input_max: 5
input_step: 0.005
-realtime: true
-gcode: SET_GCODE_OFFSET Z={0:.3f} MOVE=1
+realtime: True
+gcode:
+ SET_GCODE_OFFSET Z={'%.3f' % menu.input} MOVE=1
+
### menu octoprint ###
-[menu __octoprint]
+[menu __main __octoprint]
type: list
name: OctoPrint
-items:
- .__pause
- .__resume
- .__abort
-[menu __octoprint __pause]
+[menu __main __octoprint __pause]
type: command
-enable: toolhead.is_printing
+enable: {printer.idle_timeout.state == "Printing"}
name: Pause printing
-action: respond action:pause
gcode:
+ {printer.gcode.action_respond_info('action:pause')}
-[menu __octoprint __resume]
+[menu __main __octoprint __resume]
type: command
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Resume printing
-action: respond action:resume
gcode:
+ {printer.gcode.action_respond_info('action:resume')}
-[menu __octoprint __abort]
+[menu __main __octoprint __abort]
type: command
-enable: toolhead.is_printing
+enable: {printer.idle_timeout.state == "Printing"}
name: Abort printing
-action: respond action:cancel
gcode:
+ {printer.gcode.action_respond_info('action:cancel')}
### menu virtual sdcard ###
-[menu __sdcard]
-type: vsdcard
+[menu __main __sdcard]
+type: vsdlist
+enable: {('virtual_sdcard' in printer)}
name: SD Card
-items:
- .__start
- .__resume
- .__pause
-[menu __sdcard __start]
+[menu __main __sdcard __start]
type: command
-enable: !toolhead.is_printing
+enable: {('virtual_sdcard' in printer) and not printer.idle_timeout.state == "Printing"}
name: Start printing
gcode: M24
-[menu __sdcard __resume]
+[menu __main __sdcard __resume]
type: command
-enable: toolhead.is_printing
+enable: {('virtual_sdcard' in printer) and printer.idle_timeout.state == "Printing"}
name: Resume printing
gcode: M24
-[menu __sdcard __pause]
+[menu __main __sdcard __pause]
type: command
-enable: toolhead.is_printing
+enable: {('virtual_sdcard' in printer) and printer.idle_timeout.state == "Printing"}
name: Pause printing
gcode: M25
### menu control ###
-[menu __control]
+[menu __main __control]
type: list
name: Control
-items:
- .__home
- .__homez
- .__homexy
- .__move_10mm
- .__move_1mm
- .__move_01mm
- .__disable
- .__fanonoff
- .__fanspeed
- .__caselightonoff
- .__caselightpwm
-[menu __control __home]
+[menu __main __control __home]
type: command
+enable: {not printer.idle_timeout.state == "Printing"}
name: Home All
gcode: G28
-enable: !toolhead.is_printing
-[menu __control __homez]
+[menu __main __control __homez]
type: command
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Home Z
gcode: G28 Z
-[menu __control __homexy]
+[menu __main __control __homexy]
type: command
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Home X/Y
gcode: G28 X Y
-[menu __control __disable]
+[menu __main __control __disable]
type: command
-name: Disable steppers
+name: Steppers off
gcode:
M84
M18
-[menu __control __fanonoff]
+[menu __main __control __fanonoff]
type: input
-enable: fan.is_enabled
-name: Fan {1:3s}
-parameter: fan.speed
-transform:
- choose('OFF','ON')
- choose(0,255)
+enable: {'fan' in printer}
+name: Fan: {'ON ' if menu.input else 'OFF'}
+input: {printer.fan.speed}
input_min: 0
input_max: 1
input_step: 1
-gcode: M106 S{2:d}
+gcode:
+ M106 S{255 if menu.input else 0}
-[menu __control __fanspeed]
+[menu __main __control __fanspeed]
type: input
-enable: fan.is_enabled
-name: Fan speed: {1:3d}%
-parameter: fan.speed
-transform:
- map(0,1,0,100)
- map(0,1,0,255)
+enable: {'fan' in printer}
+name: Fan speed: {'%3d' % (menu.input*100)}%
+input: {printer.fan.speed}
input_min: 0
input_max: 1
input_step: 0.01
-gcode: M106 S{2:d}
+gcode:
+ M106 S{'%d' % (menu.input*255)}
-[menu __control __caselightonoff]
+[menu __main __control __caselightonoff]
type: input
-enable: output_pin.caselight.is_enabled
-name: Case light: {1:3s}
-parameter: output_pin.caselight.value
-transform:
- choose('OFF','ON')
- choose(0,1)
+enable: {'output_pin caselight' in printer}
+name: Lights: {'ON ' if menu.input else 'OFF'}
+input: {printer['output_pin caselight'].value}
input_min: 0
input_max: 1
input_step: 1
-gcode: SET_PIN PIN=caselight VALUE={2}
+gcode:
+ SET_PIN PIN=caselight VALUE={1 if menu.input else 0}
-[menu __control __caselightpwm]
+[menu __main __control __caselightpwm]
type: input
-enable: output_pin.caselight.is_enabled
-name: Case light: {0:4.0%}
-parameter: output_pin.caselight.value
+enable: {'output_pin caselight' in printer}
+name: Lights: {'%3d' % (menu.input*100)}%
+input: {printer['output_pin caselight'].value}
input_min: 0.0
input_max: 1.0
input_step: 0.01
-gcode: SET_PIN PIN=caselight VALUE={0:.2f}
+gcode:
+ SET_PIN PIN=caselight VALUE={menu.input}
### menu move 10mm ###
-[menu __control __move_10mm]
+[menu __main __control __move_10mm]
type: list
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Move 10mm
-items:
- .__axis_z
- .__axis_x, .__axis_y
- .__axis_e
-[menu __control __move_10mm __axis_x]
+[menu __main __control __move_10mm __axis_x]
type: input
-name: "X:{0:05.1f} "
-parameter: gcode.move_xpos
+name: Move X:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.x}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 10.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 X{0:.1f} F2400
+ G1 X{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_10mm __axis_y]
+[menu __main __control __move_10mm __axis_y]
type: input
-name: "Y:{0:05.1f} "
-parameter: gcode.move_ypos
+name: Move Y:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.y}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 10.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 Y{0:.1f} F2400
+ G1 Y{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_10mm __axis_z]
+[menu __main __control __move_10mm __axis_z]
type: input
-enable: !toolhead.is_printing
-name: "Move Z:{0:05.1f}"
-parameter: gcode.move_zpos
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Move Z:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.z}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 10.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 Z{0:.1f} F240
+ G1 Z{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_10mm __axis_e]
+[menu __main __control __move_10mm __axis_e]
type: input
-enable: !toolhead.is_printing
-name: "Move E:{0:+06.1f}"
-parameter: 0
-input_min: -50.0
-input_max: 50.0
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Move E:{'%+06.1f' % menu.input}
+input: 0
+input_min: -50
+input_max: 50
input_step: 10.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
M83
- G1 E{0:.1f} F240
+ G1 E{menu.input} F240
+ RESTORE_GCODE_STATE NAME=__move__axis
### menu move 1mm ###
-[menu __control __move_1mm]
+[menu __main __control __move_1mm]
type: list
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Move 1mm
-items:
- .__axis_z
- .__axis_x, .__axis_y
- .__axis_e
-[menu __control __move_1mm __axis_x]
+[menu __main __control __move_1mm __axis_x]
type: input
-name: "X:{0:05.1f} "
-parameter: gcode.move_xpos
+name: Move X:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.x}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 1.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 X{0:.1f} F2400
+ G1 X{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_1mm __axis_y]
+[menu __main __control __move_1mm __axis_y]
type: input
-name: "Y:{0:05.1f} "
-parameter: gcode.move_ypos
+name: Move Y:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.y}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 1.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 Y{0:.1f} F2400
+ G1 Y{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_1mm __axis_z]
+[menu __main __control __move_1mm __axis_z]
type: input
-enable: !toolhead.is_printing
-name: "Move Z:{0:05.1f}"
-parameter: gcode.move_zpos
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Move Z:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.z}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 1.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 Z{0:.1f} F240
+ G1 Z{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_1mm __axis_e]
+[menu __main __control __move_1mm __axis_e]
type: input
-enable: !toolhead.is_printing
-name: "Move E:{0:+06.1f}"
-parameter: 0
-input_min: -50.0
-input_max: 50.0
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Move E:{'%+06.1f' % menu.input}
+input: 0
+input_min: -50
+input_max: 50
input_step: 1.0
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
M83
- G1 E{0:.1f} F240
+ G1 E{menu.input} F240
+ RESTORE_GCODE_STATE NAME=__move__axis
### menu move 0.1mm ###
-[menu __control __move_01mm]
+[menu __main __control __move_01mm]
type: list
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Move 0.1mm
-items:
- .__axis_z
- .__axis_x, .__axis_y
- .__axis_e
-[menu __control __move_01mm __axis_x]
+[menu __main __control __move_01mm __axis_x]
type: input
-name: "X:{0:05.1f} "
-parameter: gcode.move_xpos
+name: Move X:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.x}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 0.1
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 X{0:.1f} F2400
+ G1 X{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_01mm __axis_y]
+[menu __main __control __move_01mm __axis_y]
type: input
-name: "Y:{0:05.1f} "
-parameter: gcode.move_ypos
+name: Move Y:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.y}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 0.1
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 Y{0:.1f} F2400
+ G1 Y{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_01mm __axis_z]
+[menu __main __control __move_01mm __axis_z]
type: input
-enable: !toolhead.is_printing
-name: "Move Z:{0:05.1f}"
-parameter: gcode.move_zpos
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Move Z:{'%05.1f' % menu.input}
+input: {printer.gcode.gcode_position.z}
input_min: 0
-input_max: 200.0
+input_max: 200
input_step: 0.1
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
G90
- G1 Z{0:.1f} F240
+ G1 Z{menu.input}
+ RESTORE_GCODE_STATE NAME=__move__axis
-[menu __control __move_01mm __axis_e]
+[menu __main __control __move_01mm __axis_e]
type: input
-enable: !toolhead.is_printing
-name: "Move E:{0:+06.1f}"
-parameter: 0
-input_min: -50.0
-input_max: 50.0
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Move E:{'%+06.1f' % menu.input}
+input: 0
+input_min: -50
+input_max: 50
input_step: 0.1
gcode:
+ SAVE_GCODE_STATE NAME=__move__axis
M83
- G1 E{0:.1f} F240
+ G1 E{menu.input} F240
+ RESTORE_GCODE_STATE NAME=__move__axis
### menu temperature ###
-[menu __temp]
+[menu __main __temp]
type: list
name: Temperature
-items:
- .__hotend0_current, .__hotend0_target
- .__hotend1_current, .__hotend1_target
- .__hotbed_current, .__hotbed_target
- .__preheat_pla
- .__preheat_abs
- .__cooldown
-[menu __temp __hotend0_current]
-type: item
-enable: extruder.is_enabled
-name: "Ex0:{0:4.0f} T"
-parameter: extruder.temperature
-
-[menu __temp __hotend0_target]
+[menu __main __temp __hotend0_target]
type: input
-enable: extruder.is_enabled
-name: "{0:4.0f}"
-parameter: extruder.target
+enable: {'extruder' in printer}
+name: {"Ex0:%3.0f (%4.0f)" % (menu.input, printer.extruder.temperature)}
+input: {printer.extruder.target}
input_min: 0
-input_max: 250
+input_max: {printer.configfile.config.extruder.max_temp}
input_step: 1
-input_step2: 10
-gcode: M104 T0 S{0:.0f}
-
-[menu __temp __hotend1_current]
-type: item
-enable: extruder1.is_enabled
-name: "Ex1:{0:4.0f} T"
-parameter: extruder1.temperature
+gcode: M104 T0 S{'%.0f' % menu.input}
-[menu __temp __hotend1_target]
+[menu __main __temp __hotend1_target]
type: input
-enable: extruder1.is_enabled
-name: "{0:4.0f}"
-parameter: extruder1.target
+enable: {'extruder1' in printer}
+name: {"Ex1:%3.0f (%4.0f)" % (menu.input, printer.extruder1.temperature)}
+input: {printer.extruder1.target}
input_min: 0
-input_max: 250
+input_max: {printer.configfile.config.extruder1.max_temp}
input_step: 1
-input_step2: 10
-gcode: M104 T1 S{0:.0f}
+gcode: M104 T1 S{'%.0f' % menu.input}
-[menu __temp __hotbed_current]
-type: item
-enable: heater_bed.is_enabled
-name: "Bed:{0:4.0f} T"
-parameter: heater_bed.temperature
-
-[menu __temp __hotbed_target]
+[menu __main __temp __hotbed_target]
type: input
-enable: heater_bed.is_enabled
-name: "{0:4.0f}"
-parameter: heater_bed.target
+enable: {'heater_bed' in printer}
+name: {"Bed:%3.0f (%4.0f)" % (menu.input, printer.heater_bed.temperature)}
+input: {printer.heater_bed.target}
input_min: 0
-input_max: 130
+input_max: {printer.configfile.config.heater_bed.max_temp}
input_step: 1
-input_step2: 10
-gcode: M140 S{0:.0f}
+gcode: M140 S{'%.0f' % menu.input}
-[menu __temp __preheat_pla]
+[menu __main __temp __preheat_pla]
type: list
name: Preheat PLA
-items:
- .__all
- .__hotend
- .__hotbed
-[menu __temp __preheat_pla __all]
+[menu __main __temp __preheat_pla __all]
type: command
-enable: extruder.is_enabled,heater_bed.is_enabled
+enable: {('extruder' in printer) and ('heater_bed' in printer)}
name: Preheat all
gcode:
M140 S60
M104 S200
-[menu __temp __preheat_pla __hotend]
+[menu __main __temp __preheat_pla __hotend]
type: command
-enable: extruder.is_enabled
+enable: {'extruder' in printer}
name: Preheat hotend
gcode: M104 S200
-[menu __temp __preheat_pla __hotbed]
+[menu __main __temp __preheat_pla __hotbed]
type: command
-enable: heater_bed.is_enabled
+enable: {'heater_bed' in printer}
name: Preheat hotbed
gcode: M140 S60
-[menu __temp __preheat_abs]
+[menu __main __temp __preheat_abs]
type: list
name: Preheat ABS
-items:
- .__all
- .__hotend
- .__hotbed
-[menu __temp __preheat_abs __all]
+[menu __main __temp __preheat_abs __all]
type: command
-enable: extruder.is_enabled,heater_bed.is_enabled
+enable: {('extruder' in printer) and ('heater_bed' in printer)}
name: Preheat all
gcode:
M140 S110
M104 S245
-[menu __temp __preheat_abs __hotend]
+[menu __main __temp __preheat_abs __hotend]
type: command
-enable: extruder.is_enabled
+enable: {'extruder' in printer}
name: Preheat hotend
gcode: M104 S245
-[menu __temp __preheat_abs __hotbed]
+[menu __main __temp __preheat_abs __hotbed]
type: command
-enable: heater_bed.is_enabled
+enable: {'heater_bed' in printer}
name: Preheat hotbed
gcode: M140 S110
-[menu __temp __cooldown]
+[menu __main __temp __cooldown]
type: list
name: Cooldown
-items:
- .__all
- .__hotend
- .__hotbed
-[menu __temp __cooldown __all]
+[menu __main __temp __cooldown __all]
type: command
-enable: extruder.is_enabled,heater_bed.is_enabled
+enable: {('extruder' in printer) and ('heater_bed' in printer)}
name: Cooldown all
gcode:
M104 S0
M140 S0
-[menu __temp __cooldown __hotend]
+[menu __main __temp __cooldown __hotend]
type: command
-enable: extruder.is_enabled
+enable: {'extruder' in printer}
name: Cooldown hotend
gcode: M104 S0
-[menu __temp __cooldown __hotbed]
+[menu __main __temp __cooldown __hotbed]
type: command
-enable: heater_bed.is_enabled
+enable: {'heater_bed' in printer}
name: Cooldown hotbed
gcode: M140 S0
### menu filament ###
-[menu __filament]
+[menu __main __filament]
type: list
name: Filament
-items:
- __temp __hotend0_current, __temp __hotend0_target
- .__unload
- .__load
- .__feed
-[menu __filament __load]
+[menu __main __filament __hotend0_target]
+type: input
+enable: {'extruder' in printer}
+name: {"Ex0:%3.0f (%4.0f)" % (menu.input, printer.extruder.temperature)}
+input: {printer.extruder.target}
+input_min: 0
+input_max: {printer.configfile.config.extruder.max_temp}
+input_step: 1
+gcode: M104 T0 S{'%.0f' % menu.input}
+
+[menu __main __filament __loadf]
type: command
-name: Load Filament
+name: Load Fil. fast
gcode:
+ SAVE_GCODE_STATE NAME=__filament__load
M83
- G1 E50 F1000
- G1 E50 F1000
- G1 E50 F1000
- G1 E50 F1000
- G1 E50 F300
- G1 E50 F300
- M82
+ G1 E50 F960
+ RESTORE_GCODE_STATE NAME=__filament__load
-[menu __filament __unload]
+[menu __main __filament __loads]
type: command
-name: Unload Filament
+name: Load Fil. slow
gcode:
+ SAVE_GCODE_STATE NAME=__filament__load
M83
- G1 E-50 F1000
- G1 E-50 F1000
- G1 E-50 F1000
- G1 E-50 F1000
- G1 E-50 F1800
- G1 E-50 F1800
- G1 E-50 F1800
- G1 E-50 F1800
- M82
+ G1 E50 F240
+ RESTORE_GCODE_STATE NAME=__filament__load
-[menu __filament __feed]
+[menu __main __filament __unloadf]
+type: command
+name: Unload Fil.fast
+gcode:
+ SAVE_GCODE_STATE NAME=__filament__load
+ M83
+ G1 E-50 F960
+ RESTORE_GCODE_STATE NAME=__filament__load
+
+[menu __main __filament __unloads]
+type: command
+name: Unload Fil.slow
+gcode:
+ SAVE_GCODE_STATE NAME=__filament__load
+ M83
+ G1 E-50 F240
+ RESTORE_GCODE_STATE NAME=__filament__load
+
+[menu __main __filament __feed]
type: input
-name: Feed: {0:.1f}
-parameter: 0
+name: Feed: {'%.1f' % menu.input}
+input: 5
input_step: 0.1
gcode:
+ SAVE_GCODE_STATE NAME=__filament__load
M83
- G1 E{0:.1f} F30
+ G1 E{'%.1f' % menu.input} F60
+ RESTORE_GCODE_STATE NAME=__filament__load
-### menu prepare ###
-[menu __prepare]
+### menu setup ###
+[menu __main __setup]
type: list
-enable: !toolhead.is_printing
-name: Prepare
-items:
- .__delta_calib
- .__bedprobe
- .__hotend_pid_tuning
- .__hotbed_pid_tuning
- .__host_restart
- .__firmware_restart
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Setup
-[menu __prepare __host_restart]
+[menu __main __setup __save_config]
type: command
-enable: !toolhead.is_printing
+name: Save config
+gcode: SAVE_CONFIG
+
+[menu __main __setup __restart]
+type: list
+name: Restart
+
+[menu __main __setup __restart __host_restart]
+type: command
+enable: {not printer.idle_timeout.state == "Printing"}
name: Restart host
gcode: RESTART
-[menu __prepare __firmware_restart]
+[menu __main __setup __restart __firmware_restart]
type: command
-enable: !toolhead.is_printing
+enable: {not printer.idle_timeout.state == "Printing"}
name: Restart FW
gcode: FIRMWARE_RESTART
-[menu __prepare __delta_calib]
+[menu __main __setup __tuning]
+type: list
+name: PID tuning
+
+[menu __main __setup __tuning __hotend_pid_tuning]
type: command
-enable: !toolhead.is_printing
-name: Delta calibrate
-gcode: DELTA_CALIBRATE
+enable: {(not printer.idle_timeout.state == "Printing") and ('extruder' in printer)}
+name: Tune Hotend PID
+gcode: PID_CALIBRATE HEATER=extruder TARGET=210 WRITE_FILE=1
-[menu __prepare __bedprobe]
+[menu __main __setup __tuning __hotbed_pid_tuning]
type: command
-enable: !toolhead.is_printing
+enable: {(not printer.idle_timeout.state == "Printing") and ('heater_bed' in printer)}
+name: Tune Hotbed PID
+gcode: PID_CALIBRATE HEATER=heater_bed TARGET=60 WRITE_FILE=1
+
+[menu __main __setup __calib]
+type: list
+name: Calibration
+
+[menu __main __setup __calib __delta_calib_auto]
+type: command
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Delta cal. auto
+gcode:
+ G28
+ DELTA_CALIBRATE
+
+[menu __main __setup __calib __delta_calib_man]
+type: list
+enable: {not printer.idle_timeout.state == "Printing"}
+name: Delta cal. man
+
+[menu __main __setup __calib __bedprobe]
+type: command
+enable: {not printer.idle_timeout.state == "Printing"}
name: Bed probe
gcode: PROBE
-[menu __prepare __hotend_pid_tuning]
+[menu __main __setup __calib __delta_calib_man __start]
type: command
-enable: !toolhead.is_printing, extruder.is_enabled
-name: Tune Hotend PID
-gcode: PID_CALIBRATE HEATER=extruder TARGET=210 WRITE_FILE=1
+name: Start probing
+gcode:
+ G28
+ DELTA_CALIBRATE METHOD=manual
+
+[menu __main __setup __calib __delta_calib_man __move_z]
+type: input
+name: Move Z: {'%03.2f' % menu.input}
+input: {printer.gcode.gcode_position.z}
+input_step: 1
+realtime: True
+gcode:
+ {%- if menu.event == 'change' -%}
+ G1 Z{'%.2f' % menu.input}
+ {%- elif menu.event == 'long_click' -%}
+ G1 Z{'%.2f' % menu.input}
+ SAVE_GCODE_STATE NAME=__move__axis
+ G91
+ G1 Z2
+ G1 Z-2
+ RESTORE_GCODE_STATE NAME=__move__axis
+ {%- endif -%}
+
+[menu __main __setup __calib __delta_calib_man __test_z]
+type: input
+name: Test Z: {['++','+','+.01','+.05','+.1','+.5','-.5','-.1','-.05','-.01','-','--'][menu.input|int]}
+input: 6
+input_min: 0
+input_max: 11
+input_step: 1
+gcode:
+ {%- if menu.event == 'long_click' -%}
+ TESTZ Z={['++','+','+.01','+.05','+.1','+.5','-.5','-.1','-.05','-.01','-','--'][menu.input|int]}
+ {%- endif -%}
-[menu __prepare __hotbed_pid_tuning]
+[menu __main __setup __calib __delta_calib_man __accept]
type: command
-enable: !toolhead.is_printing, heater_bed.is_enabled
-name: Tune Hotbed PID
-gcode: PID_CALIBRATE HEATER=heater_bed TARGET=60 WRITE_FILE=1
+name: Accept
+gcode: ACCEPT
+
+[menu __main __setup __calib __delta_calib_man __abort]
+type: command
+name: Abort
+gcode: ABORT
+
+[menu __main __setup __dump]
+type: command
+name: Dump parameters
+gcode:
+ {% for name1 in printer %}
+ {% for name2 in printer[name1] %}
+ { printer.gcode.action_respond_info("printer['%s'].%s = %s" % (name1, name2, printer[name1][name2])) }
+ {% else %}
+ { printer.gcode.action_respond_info("printer['%s'] = %s" % (name1, printer[name1])) }
+ {% endfor %}
+ {% endfor %}
diff --git a/klippy/extras/display/menu.py b/klippy/extras/display/menu.py
index e99b367c..7dc05864 100644
--- a/klippy/extras/display/menu.py
+++ b/klippy/extras/display/menu.py
@@ -1,45 +1,86 @@
# -*- coding: utf-8 -*-
# Basic LCD menu support
#
-# Based on the RaspberryPiLcdMenu from Alan Aufderheide, February 2013
-# Copyright (C) 2018 Janar Sööt <janar.soot@gmail.com>
+# Copyright (C) 2020 Janar Sööt <janar.soot@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
-import os, logging, sys, ast, re, string
+import os, logging
+from string import Template
from . import menu_keys
+from .. import gcode_macro
+
+
+class sentinel:
+ pass
+
class error(Exception):
pass
-# static class for cursor
-class MenuCursor:
- NONE = ' '
- SELECT = '>'
- EDIT = '*'
+class MenuConfig(dict):
+ """Wrapper for dict to emulate configfile get_name for namespace.
+ __ns - item namespace key, used in item relative paths
+ $__id - generated id text variable
+ """
+ def get_name(self):
+ __id = '__menu_' + hex(id(self)).lstrip("0x").rstrip("L")
+ return Template('menu ' + self.get(
+ '__ns', __id)).safe_substitute(__id=__id)
+ def get_prefix_options(self, prefix):
+ return [o for o in self.keys() if o.startswith(prefix)]
-# Menu element baseclass
+
+# Scriptable menu element abstract baseclass
class MenuElement(object):
- def __init__(self, manager, config, namespace=''):
- self.cursor = config.get('cursor', MenuCursor.SELECT)
- self._namespace = namespace
+ def __init__(self, manager, config):
+ if type(self) is MenuElement:
+ raise error(
+ 'Abstract MenuElement cannot be instantiated directly')
self._manager = manager
- self._width = self._asint(config.get('width', '0'))
- self._scroll = self._asbool(config.get('scroll', 'false'))
- self._enable = self._aslist(config.get('enable', 'true'),
- flatten=False)
- self._name = self._asliteral(config.get('name'))
+ self.cursor = '>'
+ # scroll is always on
+ self._scroll = True
+ self._index = manager.asint(config.get('index', ''), None)
+ self._enable_tpl = manager.gcode_macro.load_template(
+ config, 'enable', 'True')
+ self._name_tpl = manager.gcode_macro.load_template(
+ config, 'name')
+ # item namespace - used in relative paths
+ self._ns = str(" ".join(config.get_name().split(' ')[1:])).strip()
+ self._last_heartbeat = None
self.__scroll_offs = 0
self.__scroll_diff = 0
self.__scroll_dir = None
self.__last_state = True
- if len(self.cursor) < 1:
- raise error("Cursor with unexpected length, expecting 1.")
+ # display width is used and adjusted by cursor size
+ self._width = self.manager.cols - len(self._cursor)
+ # menu scripts
+ self._script_tpls = {}
+ # init
+ self.init()
# override
- def _render(self):
- return self._name
+ def init(self):
+ pass
+
+ def _name(self):
+ context = self.get_context()
+ return self.manager.asflat(self._name_tpl.render(context))
+
+ def _load_scripts(self, config, *args, **kwargs):
+ """Load script(s) from config"""
+
+ prefix = kwargs.get('prefix', '')
+ for arg in args:
+ name = arg[len(prefix):]
+ if name in self._script_tpls:
+ logging.info(
+ "Declaration of '%s' hides "
+ "previous script declaration" % (name,))
+ self._script_tpls[name] = self.manager.gcode_macro.load_template(
+ config, arg, '')
# override
def _second_tick(self, eventtime):
@@ -50,10 +91,6 @@ class MenuElement(object):
return False
# override
- def is_readonly(self):
- return True
-
- # override
def is_scrollable(self):
return True
@@ -62,16 +99,33 @@ class MenuElement(object):
return self.eval_enable()
# override
- def reset_editing(self):
+ def start_editing(self):
pass
+ # override
+ def stop_editing(self):
+ pass
+
+ # override
+ def get_context(self, cxt=None):
+ # get default menu context
+ context = self.manager.get_context(cxt)
+ context['menu'].update({
+ 'width': self._width,
+ 'ns': self.get_ns()
+ })
+ return context
+
def eval_enable(self):
- return self._parse_bool(self._enable)
+ context = self.get_context()
+ return self.manager.asbool(self._enable_tpl.render(context))
- def init(self):
+ # Called when a item is selected
+ def select(self):
self.__clear_scroll()
def heartbeat(self, eventtime):
+ self._last_heartbeat = eventtime
state = bool(int(eventtime) & 1)
if self.__last_state ^ state:
self.__last_state = state
@@ -97,7 +151,7 @@ class MenuElement(object):
else:
self.__clear_scroll()
- def __render_scroll(self, s):
+ def __name_scroll(self, s):
if self.__scroll_dir is None:
self.__scroll_dir = 0
self.__scroll_offs = 0
@@ -105,137 +159,99 @@ class MenuElement(object):
self.__scroll_offs:self._width + self.__scroll_offs
].ljust(self._width)
- def render(self, scroll=False):
- s = str(self._render())
+ def render_name(self, selected=False):
+ s = str(self._name())
+ # scroller
if self._width > 0:
self.__scroll_diff = len(s) - self._width
- if (scroll and self._scroll is True and self.is_scrollable()
+ if (selected and self._scroll is True and self.is_scrollable()
and self.__scroll_diff > 0):
- s = self.__render_scroll(s)
+ s = self.__name_scroll(s)
else:
self.__clear_scroll()
s = s[:self._width].ljust(self._width)
else:
self.__clear_scroll()
- return s
-
- def _parse_bool(self, lst):
- try:
- return any([
- all([
- self._lookup_bool(l2) for l2 in self._words_aslist(l1)
- ]) for l1 in lst
- ])
- except Exception:
- logging.exception("Boolean parsing error")
- return False
-
- def _lookup_bool(self, b):
- if not self._asbool(b):
- if b[0] == '!': # logical negation:
- return not (not not self._lookup_parameter(b[1:]))
- else:
- return not not self._lookup_parameter(b)
- return True
-
- def _lookup_parameter(self, literal):
- if self._isfloat(literal):
- return float(literal)
+ # add cursors
+ if selected and not self.is_editing():
+ s = self.cursor + s
+ elif selected and self.is_editing():
+ s = '*' + s
else:
- # only 2 level dot notation
- keys = literal.rsplit('.', 1)
- name = keys[0] if keys[0:1] else None
- attr = keys[1] if keys[1:2] else None
- if isinstance(self._manager.parameters, dict):
- return (self._manager.parameters.get(name) or {}).get(attr)
- else:
- logging.error("Parameter storage is not dictionary")
- return None
-
- def _asliteral(self, s):
- s = str(s).strip()
- if s.startswith(('"', "'")):
- s = s[1:]
- if s.endswith(('"', "'")):
- s = s[:-1]
+ s = ' ' + s
return s
- def _asbool(self, s, default=False):
- if s is None:
- return default
- if isinstance(s, bool):
- return s
- s = str(s).strip()
- return s.lower() in ('y', 'yes', 't', 'true', 'on', '1')
+ def get_ns(self, name='.'):
+ name = str(name).strip()
+ if name.startswith('..'):
+ name = ' '.join(
+ [(' '.join(str(self._ns).split(' ')[:-1])), name[2:]])
+ elif name.startswith('.'):
+ name = ' '.join([str(self._ns), name[1:]])
+ return name.strip()
- def _asint(self, s, default=0):
- if s is None:
- return default
- if isinstance(s, (int, float)):
- return int(s)
- s = str(s).strip()
- return int(float(s)) if self._isfloat(s) else int(default)
-
- def _asfloat(self, s, default=0.0):
- if s is None:
- return default
- if isinstance(s, (int, float)):
- return float(s)
- s = str(s).strip()
- return float(s) if self._isfloat(s) else float(default)
+ def send_event(self, event, *args):
+ return self.manager.send_event(
+ "%s:%s" % (self.get_ns(), str(event)), *args)
- def _lines_aslist(self, value, default=[]):
- if isinstance(value, str):
- value = filter(None, [x.strip() for x in value.splitlines()])
- try:
- return list(value)
- except Exception:
- logging.exception("Lines as list parsing error")
- return list(default)
-
- def _words_aslist(self, value, sep=',', default=[]):
- if isinstance(value, str):
- value = filter(None, [x.strip() for x in value.split(sep)])
- try:
- return list(value)
- except Exception:
- logging.exception("Words as list parsing error")
- return list(default)
+ def get_script(self, name):
+ if name in self._script_tpls:
+ return self._script_tpls[name]
+ return None
- def _aslist(self, value, flatten=True, default=[]):
- values = self._lines_aslist(value)
- if not flatten:
- return values
- result = []
- for value in values:
- subvalues = self._words_aslist(value, sep=',')
- result.extend(subvalues)
+ def run_script(self, name, **kwargs):
+ event = kwargs.get('event', None)
+ context = kwargs.get('context', None)
+ render_only = kwargs.get('render_only', False)
+ result = ""
+ # init context
+ context = self.get_context(context)
+ if name in self._script_tpls:
+ context['menu'].update({
+ 'event': event or name
+ })
+ result = self._script_tpls[name].render(context)
+ if not render_only:
+ # run result as gcode
+ self.manager.queue_gcode(result)
+ # default behaviour
+ _handle = getattr(self, "handle_script_" + name, None)
+ if callable(_handle):
+ _handle()
return result
- def _isfloat(self, value):
- try:
- float(value)
- return True
- except ValueError:
- return False
+ @property
+ def cursor(self):
+ return str(self._cursor)[:1]
+
+ @cursor.setter
+ def cursor(self, value):
+ self._cursor = str(value)[:1]
@property
- def namespace(self):
- return self._namespace
+ def manager(self):
+ return self._manager
- @namespace.setter
- def namespace(self, ns):
- self._namespace = ns
+ @property
+ def index(self):
+ return self._index
-# menu container baseclass
class MenuContainer(MenuElement):
- def __init__(self, manager, config, namespace=''):
- super(MenuContainer, self).__init__(manager, config, namespace)
- self._show_back = self._asbool(config.get('show_back', 'true'))
- self._show_title = self._asbool(config.get('show_title', 'true'))
+ """Menu container abstract class"""
+ def __init__(self, manager, config):
+ if type(self) is MenuContainer:
+ raise error(
+ 'Abstract MenuContainer cannot be instantiated directly')
+ super(MenuContainer, self).__init__(manager, config)
+ self.cursor = '>'
+ self.__selected = None
self._allitems = []
+ self._names = []
self._items = []
+
+ def init(self):
+ super(MenuContainer, self).init()
# recursive guard
self._parents = []
@@ -247,33 +263,45 @@ class MenuContainer(MenuElement):
def is_accepted(self, item):
return isinstance(item, MenuElement)
- def is_readonly(self):
- return False
-
def is_editing(self):
return any([item.is_editing() for item in self._items])
- def reset_editing(self):
+ def stop_editing(self):
for item in self._items:
if item.is_editing():
- item.reset_editing()
+ item.stop_editing()
- def _lookup_item(self, item):
+ def lookup_item(self, item):
if isinstance(item, str):
- s = item.strip()
- if s.startswith('.'):
- s = ' '.join([self.namespace, s[1:]])
- item = self._manager.lookup_menuitem(s)
- return item
+ name = item.strip()
+ ns = self.get_ns(name)
+ return (self.manager.lookup_menuitem(ns), name)
+ elif isinstance(item, MenuElement):
+ return (item, item.get_ns())
+ return (None, item)
- def find_item(self, item):
- index = None
- if item in self._items:
- index = self._items.index(item)
- else:
- for con in self._items:
- if isinstance(con, MenuContainer) and item in con:
- index = self._items.index(con)
+ # overload
+ def _lookup_item(self, item):
+ return self.lookup_item(item)
+
+ def _index_of(self, item):
+ try:
+ index = None
+ if isinstance(item, str):
+ s = item.strip()
+ index = self._names.index(s)
+ elif isinstance(item, MenuElement):
+ index = self._items.index(item)
+ return index
+ except ValueError:
+ return None
+
+ def index_of(self, item, look_inside=False):
+ index = self._index_of(item)
+ if index is None and look_inside is True:
+ for con in self:
+ if isinstance(con, MenuContainer) and con._index_of(item):
+ index = self._index_of(con)
return index
def add_parents(self, parents):
@@ -284,811 +312,382 @@ class MenuContainer(MenuElement):
def assert_recursive_relation(self, parents=None):
assert self not in (parents or self._parents), \
- "Recursive relation of '%s' container" % (self.namespace,)
+ "Recursive relation of '%s' container" % (self.get_ns(),)
+
+ def insert_item(self, s, index=None):
+ self._insert_item(s, index)
- def append_item(self, s):
- item = self._lookup_item(s)
+ def _insert_item(self, s, index=None):
+ item, name = self._lookup_item(s)
if item is not None:
if not self.is_accepted(item):
raise error("Menu item '%s'is not accepted!" % str(type(item)))
+ if isinstance(item, (MenuElement)):
+ item.init()
if isinstance(item, (MenuContainer)):
item.add_parents(self._parents)
item.add_parents(self)
item.assert_recursive_relation()
- item.populate_items()
- self._allitems.append(item)
+ item.populate()
+ if index is None:
+ self._allitems.append((item, name))
+ else:
+ self._allitems.insert(index, (item, name))
- def populate_items(self):
+ # overload
+ def _populate(self):
+ pass
+
+ def populate(self):
self._allitems = [] # empty list
- if self._show_back is True:
- name = '[..]'
- if self._show_title:
- name += ' %s' % str(self._name)
- self.append_item(MenuCommand(self._manager, {
- 'name': name, 'gcode': '', 'action': 'back'}, self.namespace))
for name in self._names_aslist():
- self.append_item(name)
+ self._insert_item(name)
+ # populate successor items
+ self._populate()
+ # send populate event
+ self.send_event('populate', self)
self.update_items()
def update_items(self):
- self._items = [item for item in self._allitems if item.is_enabled()]
-
- def __iter__(self):
- return iter(self._items)
-
- def __len__(self):
- return len(self._items)
-
- def __getitem__(self, key):
- return self._items[key]
+ _a = [(item, name) for item, name in self._allitems
+ if item.is_enabled()]
+ self._items, self._names = zip(*_a) or ([], [])
+ # select methods
+ def init_selection(self):
+ self.select_at(0)
-class MenuItem(MenuElement):
- def __init__(self, manager, config, namespace=''):
- super(MenuItem, self).__init__(manager, config, namespace)
- self.parameter = config.get('parameter', '')
- self.transform = config.get('transform', '')
-
- def _parse_transform(self, t):
- flist = {
- 'int': int,
- 'float': float,
- 'bool': bool,
- 'str': str,
- 'abs': abs,
- 'bin': bin,
- 'hex': hex,
- 'oct': oct
- }
-
- def mapper(left_min, left_max, right_min, right_max, cast_fn, index=0):
- # interpolate
- left_span = left_max - left_min
- right_span = right_max - right_min
- scale_factor = float(right_span) / float(left_span)
-
- def map_fn(values):
- return cast_fn(
- right_min + (values[index] - left_min) * scale_factor
- )
- return map_fn
-
- def scaler(scale_factor, cast_fn, index=0):
- def scale_fn(values):
- return cast_fn(values[index] * scale_factor)
- return scale_fn
-
- def chooser(choices, cast_fn, index=0):
- def choose_fn(values):
- return choices[cast_fn(values[index])]
- return choose_fn
-
- def timerizer(key, index=0):
- time = {}
-
- def time_fn(values):
- try:
- seconds = int(values[index])
- except Exception:
- logging.exception("Seconds parsing error")
- seconds = 0
-
- time['days'], time['seconds'] = divmod(seconds, 86400)
- time['hours'], time['seconds'] = divmod(time['seconds'], 3600)
- time['minutes'], time['seconds'] = divmod(time['seconds'], 60)
-
- if key in time:
- return time[key]
- else:
- return 0
- return time_fn
-
- def functionizer(key, index=0):
- def func_fn(values):
- if key in flist and callable(flist[key]):
- return flist[key](values[index])
- else:
- logging.error("Unknown function: '%s'" % str(key))
- return values[index]
- return func_fn
+ def select_at(self, index):
+ self.__selected = index
+ # select element
+ item = self.selected_item()
+ if isinstance(item, MenuElement):
+ item.select()
+ return item
- fn = None
- t = str(t).strip()
- # transform: idx.func(a,b,...)
- m = re.search(r"^(\d*)(?:\.?)([\S]+)(\(.*\))$", t)
- if m is not None:
- index = int(m.group(1) or 0)
- fname = str(m.group(2)).lower()
- try:
- o = ast.literal_eval(m.group(3))
- if (fname == 'map' and isinstance(o, tuple) and len(o) == 4
- and isinstance(o[3], (float, int))):
- # mapper (interpolate), cast type by last parameter type
- fn = mapper(o[0], o[1], o[2], o[3], type(o[3]), index)
- elif (fname == 'choose' and isinstance(o, tuple)
- and len(o) == 2):
- # boolean chooser for 2 size tuple
- fn = chooser(o, bool, index)
- elif fname == 'choose' and isinstance(o, tuple) and len(o) > 2:
- # int chooser for list
- fn = chooser(o, int, index)
- elif (fname == 'choose' and isinstance(o, dict) and o.keys()
- and isinstance(o.keys()[0], (int, float, str))):
- # chooser, cast type by first key type
- fn = chooser(o, type(o.keys()[0]), index)
- elif fname == 'scale' and isinstance(o, (float, int)):
- # scaler, cast type depends from scale factor type
- fn = scaler(o, type(o), index)
- elif fname in ('days', 'hours', 'minutes', 'seconds'):
- fn = timerizer(fname, index)
- elif fname in flist:
- fn = functionizer(fname, index)
- else:
- logging.error(
- "Unknown transform function: '%s'" % str(m.group(0)))
- except Exception:
- logging.exception("Transform parsing error")
+ def select_item(self, needle):
+ if isinstance(needle, MenuElement):
+ if self.selected_item() is not needle:
+ index = self.index_of(needle)
+ if index is not None:
+ self.select_at(index)
else:
- logging.error(
- "Invalid transform parameter: '%s'" % str(t))
- return fn
+ logging.error("Cannot select non menuitem")
+ return self.selected
- def _transform_aslist(self):
- return list(filter(None, (
- self._parse_transform(t) for t in self._aslist(
- self.transform, flatten=False)
- )))
+ def selected_item(self):
+ if isinstance(self.selected, int) and 0 <= self.selected < len(self):
+ return self[self.selected]
+ else:
+ return None
- def _parameter_aslist(self):
- lst = []
- for p in self._words_aslist(self.parameter):
- lst.append(self._lookup_parameter(p))
- if lst[-1] is None:
- logging.error("Parameter '%s' not found" % str(p))
- return list(lst)
+ def select_next(self):
+ if not isinstance(self.selected, int):
+ index = 0 if len(self) else None
+ elif 0 <= self.selected < len(self) - 1:
+ index = self.selected + 1
+ else:
+ index = self.selected
+ return self.select_at(index)
- def _prepare_values(self, value=None):
- values = []
- for i, v in enumerate(self._parameter_aslist()):
- values += [value if i == 0 and value is not None else v]
- if values:
- try:
- values += [t(list(values)) for t in self._transform_aslist()]
- except Exception:
- logging.exception("Transformation execution failed")
- return tuple(values)
+ def select_prev(self):
+ if not isinstance(self.selected, int):
+ index = 0 if len(self) else None
+ elif 0 < self.selected < len(self):
+ index = self.selected - 1
+ else:
+ index = self.selected
+ return self.select_at(index)
- def _get_formatted(self, literal, val=None):
- values = self._prepare_values(val)
- if isinstance(literal, str) and len(values) > 0:
- try:
- literal = literal.format(*values)
- except Exception:
- logging.exception("Literal formatting failed")
- return literal
+ # override
+ def render_container(self, eventtime):
+ return ("", None)
- def _render(self):
- return self._get_formatted(self._name)
+ def __iter__(self):
+ return iter(self._items)
+ def __len__(self):
+ return len(self._items)
-class MenuCommand(MenuItem):
- def __init__(self, manager, config, namespace=''):
- super(MenuCommand, self).__init__(manager, config, namespace)
- self._gcode = config.get('gcode', '')
- self._action = config.get('action', None)
- if self._action is None and not self._gcode:
- raise error("Missing or empty 'gcode' option")
+ def __getitem__(self, key):
+ return self._items[key]
- def is_readonly(self):
- return False
+ @property
+ def selected(self):
+ return self.__selected
- def get_gcode(self):
- return self._get_formatted(self._gcode)
- def __call__(self):
- if self._action is not None:
- try:
- fmt = self._get_formatted(self._action)
- args = fmt.split()
- self._manager.run_action(args[0], *args[1:])
- except Exception:
- logging.exception("Action formatting failed")
+class MenuCommand(MenuElement):
+ def __init__(self, manager, config):
+ super(MenuCommand, self).__init__(manager, config)
+ self._load_scripts(config, 'gcode')
class MenuInput(MenuCommand):
- def __init__(self, manager, config, namespace=''):
- super(MenuInput, self).__init__(manager, config, namespace)
- self._reverse = self._asbool(config.get('reverse', 'false'))
- self._realtime = self._asbool(config.get('realtime', 'false'))
- self._readonly = self._aslist(
- config.get('readonly', 'false'), flatten=False)
- self._input_value = None
- self.__last_value = None
- self._input_min = config.getfloat('input_min', sys.float_info.min)
- self._input_max = config.getfloat('input_max', sys.float_info.max)
+ def __init__(self, manager, config,):
+ super(MenuInput, self).__init__(manager, config)
+ self._realtime = manager.asbool(config.get('realtime', 'false'))
+ self._input_tpl = manager.gcode_macro.load_template(config, 'input')
+ self._input_min_tpl = manager.gcode_macro.load_template(
+ config, 'input_min', '-999999.0')
+ self._input_max_tpl = manager.gcode_macro.load_template(
+ config, 'input_max', '999999.0')
self._input_step = config.getfloat('input_step', above=0.)
- self._input_step2 = config.getfloat('input_step2', 0, minval=0.)
- self._longpress_gcode = config.get('longpress_gcode', '')
+
+ def init(self):
+ super(MenuInput, self).init()
+ self._is_dirty = False
+ self.__last_change = None
+ self._input_value = None
def is_scrollable(self):
return False
- def is_readonly(self):
- return self._parse_bool(self._readonly)
+ def is_editing(self):
+ return self._input_value is not None
- def _render(self):
- return self._get_formatted(self._name, self._input_value)
+ def stop_editing(self):
+ if not self.is_editing():
+ return
+ self._reset_value()
- def get_gcode(self):
- return self._get_formatted(self._gcode, self._input_value)
+ def start_editing(self):
+ if self.is_editing():
+ return
+ self._init_value()
- def get_longpress_gcode(self):
- return self._get_formatted(self._longpress_gcode, self._input_value)
+ def heartbeat(self, eventtime):
+ super(MenuInput, self).heartbeat(eventtime)
+ if (self._is_dirty is True
+ and self.__last_change is not None
+ and self._input_value is not None
+ and (eventtime - self.__last_change) > 0.250):
+ if self._realtime is True:
+ self.run_script('gcode', event='change')
+ self.run_script('change')
+ self._is_dirty = False
- def is_editing(self):
- return self._input_value is not None
+ def get_context(self, cxt=None):
+ context = super(MenuInput, self).get_context(cxt)
+ context['menu'].update({
+ 'input': self.manager.asfloat(
+ self._eval_value() if self._input_value is None
+ else self._input_value)
+ })
+ return context
- def reset_editing(self):
- self.reset_value()
+ def eval_enable(self):
+ context = super(MenuInput, self).get_context()
+ return self.manager.asbool(self._enable_tpl.render(context))
+
+ def _eval_min(self):
+ context = super(MenuInput, self).get_context()
+ return self._input_min_tpl.render(context)
+
+ def _eval_max(self):
+ context = super(MenuInput, self).get_context()
+ return self._input_max_tpl.render(context)
+
+ def _eval_value(self):
+ context = super(MenuInput, self).get_context()
+ return self._input_tpl.render(context)
- def _onchange(self):
- self._manager.queue_gcode(self.get_gcode())
+ def _value_changed(self):
+ self.__last_change = self._last_heartbeat
+ self._is_dirty = True
- def init_value(self):
+ def _init_value(self):
self._input_value = None
- self.__last_value = None
- if not self.is_readonly():
- args = self._prepare_values()
- if len(args) > 0 and self._isfloat(args[0]):
- self._input_value = float(args[0])
- if self._realtime:
- self._onchange()
- else:
- logging.error("Cannot init input value")
+ self._input_min = self.manager.asfloat(self._eval_min())
+ self._input_max = self.manager.asfloat(self._eval_max())
+ value = self._eval_value()
+ if self.manager.isfloat(value):
+ self._input_value = min(self._input_max, max(
+ self._input_min, self.manager.asfloat(value)))
+ self._value_changed()
+ else:
+ logging.error("Cannot init input value")
- def reset_value(self):
+ def _reset_value(self):
self._input_value = None
+ def _get_input_step(self, fast_rate=False):
+ return ((10.0 * self._input_step) if fast_rate and (
+ (self._input_max - self._input_min) / self._input_step > 100.0)
+ else self._input_step)
+
def inc_value(self, fast_rate=False):
last_value = self._input_value
- input_step = (self._input_step2 if fast_rate and self._input_step2 > 0
- else self._input_step)
if self._input_value is None:
return
- if(self._reverse is True):
- self._input_value -= abs(input_step)
- else:
- self._input_value += abs(input_step)
+ input_step = self._get_input_step(fast_rate)
+ self._input_value += abs(input_step)
self._input_value = min(self._input_max, max(
self._input_min, self._input_value))
- if self._realtime and last_value != self._input_value:
- self._onchange()
+ if last_value != self._input_value:
+ self._value_changed()
def dec_value(self, fast_rate=False):
last_value = self._input_value
- input_step = (self._input_step2 if fast_rate and self._input_step2 > 0
- else self._input_step)
if self._input_value is None:
return
- if(self._reverse is True):
- self._input_value += abs(input_step)
- else:
- self._input_value -= abs(input_step)
+ input_step = self._get_input_step(fast_rate)
+ self._input_value -= abs(input_step)
self._input_value = min(self._input_max, max(
self._input_min, self._input_value))
- if self._realtime and last_value != self._input_value:
- self._onchange()
-
-
-class MenuGroup(MenuContainer):
- def __init__(self, manager, config, namespace='', sep=','):
- super(MenuGroup, self).__init__(manager, config, namespace)
- self._sep = sep
- self._show_back = False
- self.selected = None
- self.use_cursor = self._asbool(config.get('use_cursor', 'false'))
- self.items = config.get('items', '')
-
- def is_accepted(self, item):
- return (super(MenuGroup, self).is_accepted(item)
- and type(item) is not MenuCard)
-
- def is_scrollable(self):
- return False
-
- def is_enabled(self):
- return not not len(self)
-
- def is_readonly(self):
- return all([item.is_readonly() for item in self._items])
-
- def _names_aslist(self):
- return self._words_aslist(self.items, sep=self._sep)
-
- def init(self):
- super(MenuGroup, self).init()
- for item in self._items:
- item.init()
-
- def _render_item(self, item, selected=False, scroll=False):
- name = "%s" % str(item.render(scroll))
- if selected and not self.is_editing():
- if self.use_cursor:
- name = (item.cursor if isinstance(item, MenuElement)
- else MenuCursor.SELECT) + name
- else:
- name = (name if self._manager.blink_slow_state
- else ' '*len(name))
- elif selected and self.is_editing():
- if self.use_cursor:
- name = MenuCursor.EDIT + name
- else:
- name = (name if self._manager.blink_fast_state
- else ' '*len(name))
- elif self.use_cursor:
- name = MenuCursor.NONE + name
- return name
-
- def _render(self):
- s = ""
- if self.selected is not None:
- self.selected = (
- (self.selected % len(self)) if len(self) > 0 else None)
-
- for i, item in enumerate(self):
- s += self._render_item(item, (i == self.selected), True)
- return s
+ if last_value != self._input_value:
+ self._value_changed()
- def _call_selected(self, method=None, *args):
- res = None
- if self.selected is not None:
- try:
- if method is None:
- res = self[self.selected]
- else:
- res = getattr(self[self.selected], method)(*args)
- except Exception:
- logging.exception("Call selected error")
- return res
-
- def reset_editing(self):
- return self._call_selected('reset_editing')
-
- def is_editing(self):
- return self._call_selected('is_editing')
-
- def inc_value(self, fast_rate=False):
- self._call_selected('inc_value', fast_rate)
-
- def dec_value(self, fast_rate=False):
- self._call_selected('dec_value', fast_rate)
-
- def selected_item(self):
- return self._call_selected()
-
- def find_next_item(self):
- if self.selected is None:
- self.selected = 0
- elif self.selected < len(self) - 1:
- self.selected += 1
- else:
- self.selected = None
- # skip readonly
- while (self.selected is not None
- and self.selected < len(self)
- and self._call_selected('is_readonly')):
- if self.selected < len(self) - 1:
- self.selected = (self.selected + 1)
- else:
- self.selected = None
- return self.selected
-
- def find_prev_item(self):
- if self.selected is None:
- self.selected = len(self) - 1
- elif self.selected > 0:
- self.selected -= 1
- else:
- self.selected = None
- # skip readonly
- while (self.selected is not None
- and self.selected >= 0
- and self._call_selected('is_readonly')):
- self.selected = (self.selected - 1) if self.selected > 0 else None
- return self.selected
-
-
-class MenuItemGroup(MenuGroup):
- def __init__(self, manager, config, namespace='', sep='|'):
- super(MenuItemGroup, self).__init__(manager, config, namespace, sep)
-
- def is_readonly(self):
- return True
-
- def is_accepted(self, item):
- return type(item) is MenuItem
-
-
-class MenuCycler(MenuGroup):
- def __init__(self, manager, config, namespace='', sep=','):
- super(MenuCycler, self).__init__(manager, config, namespace, sep)
- self._interval = 0
- self.__interval_cnt = 0
- self.__alllen = 0
- self._curr_idx = 0
-
- def is_readonly(self):
- return True
-
- def is_accepted(self, item):
- return type(item) in (MenuItem, MenuItemGroup)
-
- def _lookup_item(self, item):
- if isinstance(item, str) and '|' in item:
- item = MenuItemGroup(self._manager, {
- 'name': ' '.join([self._name, 'ItemGroup']),
- 'items': item
- }, self.namespace, '|')
- elif isinstance(item, str) and item.isdigit():
- try:
- self._interval = max(0, int(item))
- except Exception:
- logging.exception("Interval parsing error")
- item = None
- return super(MenuCycler, self)._lookup_item(item)
-
- def _second_tick(self, eventtime):
- super(MenuCycler, self)._second_tick(eventtime)
- if self._interval > 0:
- self.__interval_cnt = (self.__interval_cnt + 1) % self._interval
- if self.__interval_cnt == 0 and self.__alllen > 0:
- self._curr_idx = (self._curr_idx + 1) % self.__alllen
- else:
- self._curr_idx = 0
-
- def heartbeat(self, eventtime):
- super(MenuCycler, self).heartbeat(eventtime)
- for item in self._items:
- item.heartbeat(eventtime)
-
- def update_items(self):
- items = [item for item in self._allitems if item.is_enabled()]
- self.__alllen = len(items)
- if self.__alllen > 0:
- self._curr_idx = self._curr_idx % self.__alllen
- self._items = [items[self._curr_idx]]
- else:
- self._curr_idx = 0
- self._items = []
+ # default behaviour on click
+ def handle_script_click(self):
+ if not self.is_editing():
+ self.start_editing()
+ elif self.is_editing():
+ self.stop_editing()
class MenuList(MenuContainer):
- def __init__(self, manager, config, namespace=''):
- super(MenuList, self).__init__(manager, config, namespace)
- self._enter_gcode = config.get('enter_gcode', None)
- self._leave_gcode = config.get('leave_gcode', None)
- self.items = config.get('items', '')
-
- def is_accepted(self, item):
- return (super(MenuList, self).is_accepted(item)
- and type(item) is not MenuCard)
+ def __init__(self, manager, config):
+ super(MenuList, self).__init__(manager, config)
+ self._show_title = True
def _names_aslist(self):
- return self._lines_aslist(self.items)
+ return self.manager.lookup_children(self.get_ns())
- def _lookup_item(self, item):
- if isinstance(item, str) and ',' in item:
- item = MenuGroup(self._manager, {
- 'name': ' '.join([self._name, 'Group']),
- 'items': item
- }, self.namespace, ',')
- return super(MenuList, self)._lookup_item(item)
-
- def update_items(self):
- super(MenuList, self).update_items()
- for item in self._items:
- if isinstance(item, MenuGroup) and not item.is_editing():
- item.update_items()
+ def _populate(self):
+ super(MenuList, self)._populate()
+ # add back as first item
+ name = '..'
+ if self._show_title:
+ name += ' %s' % str(self._name())
+ item = self.manager.menuitem_from({
+ 'type': 'command',
+ 'name': self.manager.asliteral(name),
+ 'gcode': '{menu.back()}'
+ })
+ self.insert_item(item, 0)
- def get_enter_gcode(self):
- return self._enter_gcode
-
- def get_leave_gcode(self):
- return self._leave_gcode
+ def render_container(self, eventtime):
+ rows = []
+ selected_row = None
+ try:
+ for row, item in enumerate(self):
+ s = ""
+ selected = (row == self.selected)
+ if selected:
+ item.heartbeat(eventtime)
+ selected_row = len(rows)
+ name = str(item.render_name(selected))
+ if isinstance(item, MenuList):
+ s += name[:self.manager.cols-1].ljust(self.manager.cols-1)
+ s += '>'
+ else:
+ s += name[:self.manager.cols].ljust(self.manager.cols)
+ rows.append(s)
+ except Exception:
+ logging.exception('List rendering error')
+ return ("\n".join(rows), selected_row)
-class MenuVSDCard(MenuList):
- def __init__(self, manager, config, namespace=''):
- super(MenuVSDCard, self).__init__(manager, config, namespace)
+class MenuVSDList(MenuList):
+ def __init__(self, manager, config):
+ super(MenuVSDList, self).__init__(manager, config)
- def _populate_files(self):
- sdcard = self._manager.objs.get('virtual_sdcard')
+ def _populate(self):
+ super(MenuVSDList, self)._populate()
+ sdcard = self.manager.printer.lookup_object('virtual_sdcard', None)
if sdcard is not None:
files = sdcard.get_file_list()
for fname, fsize in files:
gcode = [
'M23 /%s' % str(fname)
]
- self.append_item(MenuCommand(self._manager, {
- 'name': '%s' % str(fname),
- 'cursor': '+',
- 'gcode': "\n".join(gcode),
- 'scroll': True,
- # mind the cursor size in width
- 'width': (self._manager.cols-1)
+ self.insert_item(self.manager.menuitem_from({
+ 'type': 'command',
+ 'name': self.manager.asliteral(fname),
+ 'gcode': "\n".join(gcode)
}))
- def populate_items(self):
- super(MenuVSDCard, self).populate_items()
- self._populate_files()
-
-
-class MenuCard(MenuGroup):
- def __init__(self, manager, config, namespace=''):
- super(MenuCard, self).__init__(manager, config, namespace)
- self.content = config.get('content')
- self._allow_without_selection = self._asbool(
- config.get('allow_without_selection', 'true'))
- if not self.items:
- self.content = self._parse_content_items(self.content)
-
- def _parse_content_items(self, content):
- formatter = string.Formatter()
- out = ""
- items = []
-
- try:
- parsed_content = list(formatter.parse(content))
- except Exception:
- logging.exception("Card content parsing error")
-
- for part in parsed_content:
- # (literal_text, field_name, format_spec, conversion)
- out += part[0]
- if part[1]:
- out += "{%s%s%s}" % (
- len(items),
- ("!" + part[3]) if part[3] else '',
- (":" + part[2]) if part[2] else '',
- )
- items.append(str(part[1]))
-
- self.items = "\n".join(items)
- return out
-
- def _names_aslist(self):
- return self._lines_aslist(self.items)
-
- def _content_aslist(self):
- return filter(None, [
- self._asliteral(s) for s in self._lines_aslist(self.content)
- ])
-
- def update_items(self):
- self._items = self._allitems[:]
- for item in self._items:
- if isinstance(item, MenuGroup) and not item.is_editing():
- item.update_items()
-
- def _lookup_item(self, item):
- if isinstance(item, str) and ',' in item:
- item = MenuCycler(self._manager, {
- 'name': ' '.join([self._name, 'Cycler']),
- 'items': item
- }, self.namespace, ',')
- return super(MenuCard, self)._lookup_item(item)
-
- def render_content(self, eventtime):
- if self.selected is not None:
- self.selected = (
- (self.selected % len(self)) if len(self) > 0 else None)
- if self._allow_without_selection is False and self.selected is None:
- self.selected = 0 if len(self) > 0 else None
-
- items = []
- for i, item in enumerate(self):
- name = ''
- if item.is_enabled():
- item.heartbeat(eventtime)
- name = self._render_item(item, (i == self.selected), True)
- items.append(name)
- lines = []
- for line in self._content_aslist():
- try:
- lines.append(str(line).format(*items))
- except Exception:
- logging.exception('Card rendering error')
- return lines
-
- def _render(self):
- return self._name
-
-
-class MenuDeck(MenuList):
- def __init__(self, manager, config, namespace=''):
- super(MenuDeck, self).__init__(manager, config, namespace)
- self._menu = config.get('longpress_menu', None)
- self.menu = None
- self._show_back = False
- self._show_title = False
- if not self.items:
- card = MenuCard(self._manager, {
- 'name': ' '.join([self._name, 'Card']),
- 'use_cursor': config.get('use_cursor', 'false'),
- 'allow_without_selection': config.get(
- 'allow_without_selection', 'true'),
- 'content': config.get('content')
- }, self.namespace)
- name = " ".join(
- config.get_name().split()[1:]) + "__singlecarddeck__"
- self._manager.add_menuitem(name, card)
- self.items = name
-
- def _populate_menu(self):
- self.menu = None
- if self._menu is not None:
- menu = self._manager.lookup_menuitem(self._menu)
- if isinstance(menu, MenuContainer):
- menu.assert_recursive_relation(self._parents)
- menu.populate_items()
- self.menu = menu
-
- def populate_items(self):
- super(MenuDeck, self).populate_items()
- self._populate_menu()
-
- def _names_aslist(self):
- return self._aslist(self.items)
-
- def is_accepted(self, item):
- return type(item) is MenuCard
-
- def _render(self):
- return self._name
-
menu_items = {
- 'item': MenuItem,
'command': MenuCommand,
'input': MenuInput,
'list': MenuList,
- 'vsdcard': MenuVSDCard,
- #'deck': MenuDeck,
- #'card': MenuCard
+ 'vsdlist': MenuVSDList
}
+
MENU_UPDATE_DELAY = .100
-TIMER_DELAY = .200
-BLINK_FAST_SEQUENCE = (True, True, False, False)
-BLINK_SLOW_SEQUENCE = (True, True, True, True, False, False, False)
+TIMER_DELAY = .100
class MenuManager:
- def __init__(self, config, lcd_chip):
+ def __init__(self, config, display):
self.running = False
self.menuitems = {}
self.menustack = []
- self._autorun = False
+ self.children = {}
self.top_row = 0
- self.selected = 0
- self.blink_fast_state = True
- self.blink_slow_state = True
- self.blink_fast_idx = 0
- self.blink_slow_idx = 0
self.timeout_idx = 0
- self.lcd_chip = lcd_chip
+ self.display = display
self.printer = config.get_printer()
self.pconfig = self.printer.lookup_object('configfile')
self.gcode = self.printer.lookup_object('gcode')
self.gcode_queue = []
- self.parameters = {}
- self.objs = {}
+ self.context = {}
self.root = None
self._root = config.get('menu_root', '__main')
- self.cols, self.rows = lcd_chip.get_dimensions()
+ self.cols, self.rows = self.display.lcd_chip.get_dimensions()
self.timeout = config.getint('menu_timeout', 0)
self.timer = 0
+ self.eventtime = 0
+ # reverse container navigation
+ self._reverse_navigation = config.getboolean(
+ 'menu_reverse_navigation', False)
+ # load printer objects
+ self.gcode_macro = self.printer.load_object(config, 'gcode_macro')
# register itself for printer callbacks
self.printer.add_object('menu', self)
self.printer.register_event_handler("klippy:ready", self.handle_ready)
+ # register for key events
menu_keys.MenuKeys(config, self.key_event)
- # Add MENU commands
- self.gcode.register_mux_command("MENU", "DO", 'dump', self.cmd_DO_DUMP,
- desc=self.cmd_DO_help)
-
# Load local config file in same directory as current module
self.load_config(os.path.dirname(__file__), 'menu.cfg')
# Load items from main config
self.load_menuitems(config)
# Load menu root
- self.load_root()
+ self.root = self.lookup_menuitem(self._root)
+ # send init event
+ self.send_event('init', self)
def handle_ready(self):
- # Load all available printer objects
- for cfg_name, obj in self.printer.lookup_objects():
- name = ".".join(str(cfg_name).split())
- self.objs[name] = obj
- logging.debug("Load module '%s' -> %s" % (
- str(name), str(obj.__class__)))
# start timer
reactor = self.printer.get_reactor()
reactor.register_timer(self.timer_event, reactor.NOW)
def timer_event(self, eventtime):
- # take next from sequence
- self.blink_fast_idx = (
- (self.blink_fast_idx + 1) % len(BLINK_FAST_SEQUENCE)
- )
- self.blink_slow_idx = (
- (self.blink_slow_idx + 1) % len(BLINK_SLOW_SEQUENCE)
- )
- self.timeout_idx = (self.timeout_idx + 1) % 5 # 0.2*5 = 1s
- self.blink_fast_state = (
- not not BLINK_FAST_SEQUENCE[self.blink_fast_idx]
- )
- self.blink_slow_state = (
- not not BLINK_SLOW_SEQUENCE[self.blink_slow_idx]
- )
+ self.eventtime = eventtime
+ self.timeout_idx = (self.timeout_idx + 1) % 10 # 0.1*10 = 1s
if self.timeout_idx == 0:
self.timeout_check(eventtime)
return eventtime + TIMER_DELAY
def timeout_check(self, eventtime):
- # check timeout
if (self.is_running() and self.timeout > 0
- and not self._timeout_autorun_root()):
+ and isinstance(self.root, MenuContainer)):
if self.timer >= self.timeout:
self.exit()
- self.timer += 1
+ else:
+ self.timer += 1
else:
self.timer = 0
- def _timeout_autorun_root(self):
- return (self._autorun is True and self.root is not None
- and self.stack_peek() is self.root and self.selected == 0)
-
- def restart_root(self, root=None, force_exit=True):
- if self.is_running():
- self.exit(force_exit)
- self.load_root(root)
-
- def load_root(self, root=None, autorun=False):
- root = self._root if root is None else root
- if root is not None:
- self.root = self.lookup_menuitem(root)
- if isinstance(self.root, MenuDeck):
- self._autorun = True
- else:
- self._autorun = autorun
-
- def register_object(self, obj, name=None, override=False):
- """Register an object with a "get_status" callback"""
- if obj is not None:
- if name is None:
- name = obj.__class__.__name__
- if override or name not in self.objs:
- self.objs[name] = obj
-
- def unregister_object(self, name):
- """Unregister an object from "get_status" callback list"""
- if name is not None:
- if not isinstance(name, str):
- name = name.__class__.__name__
- if name in self.objs:
- self.objs.pop(name)
-
- def after(self, timeout, callback, *args):
- """Helper method for reactor.register_callback.
- The callback will be executed once after given timeout (sec)."""
- def callit(eventtime):
- callback(eventtime, *args)
- reactor = self.printer.get_reactor()
- starttime = reactor.monotonic() + max(0., float(timeout))
- reactor.register_callback(callit, starttime)
+ def send_event(self, event, *args):
+ return self.printer.send_event("menu:" + str(event), *args)
def is_running(self):
return self.running
@@ -1096,85 +695,71 @@ class MenuManager:
def begin(self, eventtime):
self.menustack = []
self.top_row = 0
- self.selected = 0
self.timer = 0
if isinstance(self.root, MenuContainer):
- self.update_parameters(eventtime)
- self.root.populate_items()
+ # send begin event
+ self.send_event('begin', self)
+ self.update_context(eventtime)
+ if isinstance(self.root, MenuContainer):
+ self.root.init_selection()
+ self.root.populate()
self.stack_push(self.root)
self.running = True
return
elif self.root is not None:
- logging.error("Invalid root '%s', menu stopped!" % str(self._root))
+ logging.error("Invalid root, menu stopped!")
+ self.running = False
self.running = False
def get_status(self, eventtime):
return {
- 'eventtime': eventtime,
'timeout': self.timeout,
- 'autorun': self._autorun,
- 'isRunning': self.running,
- 'is2004': (self.rows == 4 and self.cols == 20),
- 'is2002': (self.rows == 2 and self.cols == 20),
- 'is1604': (self.rows == 4 and self.cols == 16),
- 'is1602': (self.rows == 2 and self.cols == 16),
- 'is20xx': (self.cols == 20),
- 'is16xx': (self.cols == 16)
+ 'running': self.running,
+ 'rows': self.rows,
+ 'cols': self.cols
}
- def update_parameters(self, eventtime):
- self.parameters = {}
- objs = dict(self.objs)
- # getting info this way is more like hack
- # all modules should have special reporting method (maybe get_status)
- # for available parameters
- # Only 2 level dot notation
- for name in objs.keys():
- try:
- if objs[name] is not None:
- class_name = str(objs[name].__class__.__name__)
- get_status = getattr(objs[name], "get_status", None)
- if callable(get_status):
- self.parameters[name] = get_status(eventtime)
- else:
- self.parameters[name] = {}
+ def _action_back(self, force=False, update=True):
+ self.back(force, update)
+ return ""
- self.parameters[name].update({'is_enabled': True})
- # get additional info
- if class_name == 'ToolHead':
- pos = objs[name].get_position()
- self.parameters[name].update({
- 'xpos': pos[0],
- 'ypos': pos[1],
- 'zpos': pos[2],
- 'epos': pos[3]
- })
- self.parameters[name].update({
- 'is_printing': (
- self.parameters[name]['status'] == "Printing"),
- 'is_ready': (
- self.parameters[name]['status'] == "Ready"),
- 'is_idle': (
- self.parameters[name]['status'] == "Idle")
- })
- else:
- self.parameters[name] = {'is_enabled': False}
- except Exception:
- logging.exception("Parameter '%s' update error" % str(name))
+ def _action_exit(self, force=False):
+ self.exit(force)
+ return ""
+
+ def get_context(self, cxt=None):
+ context = dict(self.context)
+ if isinstance(cxt, dict):
+ context.update(cxt)
+ return context
+
+ def update_context(self, eventtime):
+ # menu default jinja2 context
+ self.context = {
+ 'printer': gcode_macro.GetStatusWrapper(self.printer, eventtime),
+ 'menu': {
+ 'eventtime': eventtime,
+ 'back': self._action_back,
+ 'exit': self._action_exit
+ }
+ }
def stack_push(self, container):
if not isinstance(container, MenuContainer):
raise error("Wrong type, expected MenuContainer")
top = self.stack_peek()
if top is not None:
- self.queue_gcode(top.get_leave_gcode())
- self.queue_gcode(container.get_enter_gcode())
+ if isinstance(top, MenuList):
+ top.run_script('leave')
+ if isinstance(container, MenuList):
+ container.run_script('enter')
if not container.is_editing():
container.update_items()
+ container.init_selection()
self.menustack.append(container)
- def stack_pop(self):
+ def stack_pop(self, update=True):
container = None
if self.stack_size() > 0:
container = self.menustack.pop()
@@ -1184,12 +769,16 @@ class MenuManager:
if top is not None:
if not isinstance(container, MenuContainer):
raise error("Wrong type, expected MenuContainer")
- if not top.is_editing():
+ if not top.is_editing() and update is True:
top.update_items()
- self.queue_gcode(container.get_leave_gcode())
- self.queue_gcode(top.get_enter_gcode())
+ top.init_selection()
+ if isinstance(container, MenuList):
+ container.run_script('leave')
+ if isinstance(top, MenuList):
+ top.run_script('enter')
else:
- self.queue_gcode(container.get_leave_gcode())
+ if isinstance(container, MenuList):
+ container.run_script('leave')
return container
def stack_size(self):
@@ -1201,72 +790,36 @@ class MenuManager:
container = self.menustack[self.stack_size() - lvl - 1]
return container
- def _unescape_cchars(self, text):
- def fixup(m):
- text = str(m.group(0))
- if text[:2] == "\\x":
- try:
- return "%c" % (int(text[2:], 16),)
- except ValueError:
- logging.exception('Custom character unescape error')
- else:
- return text
- return re.sub(r'\\x[0-9a-f]{2}', fixup, str(text), flags=re.IGNORECASE)
-
def render(self, eventtime):
lines = []
- self.update_parameters(eventtime)
+ self.update_context(eventtime)
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
container.heartbeat(eventtime)
- if(isinstance(container, MenuDeck) and not container.is_editing()):
- container.update_items()
- # clamps
- self.top_row = max(0, min(
- self.top_row, len(container) - self.rows))
- self.selected = max(0, min(
- self.selected, len(container) - 1))
- if isinstance(container, MenuDeck):
- container[self.selected].heartbeat(eventtime)
- lines = container[self.selected].render_content(eventtime)
+ content, viewport_row = container.render_container(eventtime)
+ if viewport_row is not None:
+ while viewport_row >= (self.top_row + self.rows):
+ self.top_row += 1
+ while viewport_row < self.top_row and self.top_row > 0:
+ self.top_row -= 1
else:
- for row in range(self.top_row, self.top_row + self.rows):
- s = ""
- if row < len(container):
- selected = (row == self.selected)
- current = container[row]
- if selected:
- current.heartbeat(eventtime)
- if (isinstance(current, (MenuInput, MenuGroup))
- and current.is_editing()):
- s += MenuCursor.EDIT
- elif isinstance(current, MenuElement):
- s += current.cursor
- else:
- s += MenuCursor.SELECT
- else:
- s += MenuCursor.NONE
-
- name = "%s" % str(current.render(selected))
- i = len(s)
- if isinstance(current, MenuList):
- s += name[:self.cols-i-1].ljust(self.cols-i-1)
- s += '>'
- else:
- s += name[:self.cols-i].ljust(self.cols-i)
- lines.append(s.ljust(self.cols))
+ self.top_row = 0
+ rows = self.aslatin(content).splitlines()
+ for row in range(0, self.rows):
+ try:
+ text = self.stripliterals(rows[self.top_row + row])
+ except IndexError:
+ text = ""
+ lines.append(text.ljust(self.cols))
return lines
def screen_update_event(self, eventtime):
+ # screen update
if self.is_running():
- self.lcd_chip.clear()
+ self.display.lcd_chip.clear()
for y, line in enumerate(self.render(eventtime)):
- self.lcd_chip.write_text(0, y, self._unescape_cchars(line))
- self.lcd_chip.flush()
- return eventtime + MENU_UPDATE_DELAY
- elif not self.is_running() and self._autorun is True:
- # lets start and populate the menu items
- self.begin(eventtime)
+ self.display.draw_text(y, 0, line, eventtime)
+ self.display.lcd_chip.flush()
return eventtime + MENU_UPDATE_DELAY
else:
return 0
@@ -1275,141 +828,90 @@ class MenuManager:
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
self.timer = 0
- current = container[self.selected]
- if (isinstance(current, (MenuInput, MenuGroup))
- and current.is_editing()):
+ current = container.selected_item()
+ if isinstance(current, MenuInput) and current.is_editing():
current.dec_value(fast_rate)
- elif (isinstance(current, MenuGroup)
- and current.find_prev_item() is not None):
- pass
else:
- if self.selected == 0:
- return
- if self.selected > self.top_row:
- self.selected -= 1
+ if self._reverse_navigation is True:
+ container.select_next() # reverse
else:
- self.top_row -= 1
- self.selected -= 1
- # init element
- if isinstance(container[self.selected], MenuElement):
- container[self.selected].init()
- # wind up group last item or init item
- if isinstance(container[self.selected], MenuGroup):
- container[self.selected].find_prev_item()
+ container.select_prev() # normal
def down(self, fast_rate=False):
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
self.timer = 0
- current = container[self.selected]
- if (isinstance(current, (MenuInput, MenuGroup))
- and current.is_editing()):
+ current = container.selected_item()
+ if isinstance(current, MenuInput) and current.is_editing():
current.inc_value(fast_rate)
- elif (isinstance(current, MenuGroup)
- and current.find_next_item() is not None):
- pass
else:
- if self.selected >= len(container) - 1:
- return
- if self.selected < self.top_row + self.rows - 1:
- self.selected += 1
+ if self._reverse_navigation is True:
+ container.select_prev() # reverse
else:
- self.top_row += 1
- self.selected += 1
- # init element
- if isinstance(container[self.selected], MenuElement):
- container[self.selected].init()
- # wind up group first item
- if isinstance(container[self.selected], MenuGroup):
- container[self.selected].find_next_item()
+ container.select_next() # normal
- def back(self):
+ def back(self, force=False, update=True):
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
self.timer = 0
- current = container[self.selected]
- if (isinstance(current, (MenuInput, MenuGroup))
- and current.is_editing()):
- return
+ current = container.selected_item()
+ if isinstance(current, MenuInput) and current.is_editing():
+ if force is True:
+ current.stop_editing()
+ else:
+ return
parent = self.stack_peek(1)
if isinstance(parent, MenuContainer):
- self.stack_pop()
- index = parent.find_item(container)
- if index is not None and index < len(parent):
- self.top_row = index
- self.selected = index
- else:
- self.top_row = 0
- self.selected = 0
- # init element
- if isinstance(parent[self.selected], MenuElement):
- parent[self.selected].init()
- # wind up group first item or init item
- if isinstance(parent[self.selected], MenuGroup):
- parent[self.selected].find_next_item()
+ self.stack_pop(update)
+ index = parent.index_of(container, True)
+ if index is not None:
+ parent.select_at(index)
+ elif parent.selected_item() is None:
+ parent.init_selection()
+
else:
self.stack_pop()
self.running = False
- def select(self, long_press=False):
- container = self.stack_peek()
- if self.running and isinstance(container, MenuContainer):
- self.timer = 0
- current = container[self.selected]
- if isinstance(current, MenuGroup):
- current = current.selected_item()
- if isinstance(current, MenuList):
- self.stack_push(current)
- self.top_row = 0
- self.selected = 0
- elif isinstance(current, MenuInput):
- if current.is_editing():
- if long_press is True:
- self.queue_gcode(current.get_gcode())
- self.queue_gcode(current.get_longpress_gcode())
- else:
- self.queue_gcode(current.get_gcode())
- current.reset_value()
- else:
- current.init_value()
- elif isinstance(current, MenuCommand):
- current()
- self.queue_gcode(current.get_gcode())
-
def exit(self, force=False):
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
- current = container[self.selected]
- if (not force and isinstance(current, (MenuInput, MenuGroup))
+ self.timer = 0
+ current = container.selected_item()
+ if (not force and isinstance(current, MenuInput)
and current.is_editing()):
return
- self.queue_gcode(container.get_leave_gcode())
+ if isinstance(container, MenuList):
+ container.run_script('leave')
+ self.send_event('exit', self)
self.running = False
- def run_action(self, action, *args):
- try:
- action = str(action).strip().lower()
- if action == 'back':
- self.back()
- elif action == 'exit':
- self.exit()
- elif action == 'respond':
- self.gcode.respond_info("{}".format(' '.join(map(str, args))),
- log=False)
- elif action == 'event' and len(args) > 0:
- if len(str(args[0])) > 0:
- self.printer.send_event(
- "menu:action:" + str(args[0]), *args[1:])
- else:
- logging.error("Malformed event call: {} {}".format(
- action, ' '.join(map(str, args))))
+ def push_container(self, menu):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ if (isinstance(menu, MenuContainer)
+ and not container.is_editing()
+ and menu is not container):
+ self.stack_push(menu)
+ return True
+ return False
+
+ def press(self, event='click'):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ self.timer = 0
+ current = container.selected_item()
+ if isinstance(current, MenuContainer):
+ self.stack_push(current)
+ elif isinstance(current, MenuCommand):
+ current.run_script('gcode', event=event)
+ current.run_script(event)
else:
- logging.error("Unknown action %s" % (action))
- except Exception:
- logging.exception("Malformed action call")
+ # current is None, no selection. passthru to container
+ container.run_script(event)
def queue_gcode(self, script):
- if script is None:
+ if not script:
return
if not self.gcode_queue:
reactor = self.printer.get_reactor()
@@ -1425,20 +927,42 @@ class MenuManager:
logging.exception("Script running error")
self.gcode_queue.pop(0)
- def add_menuitem(self, name, menu):
+ def menuitem_from(self, config):
+ if isinstance(config, dict):
+ config = MenuConfig(dict(config))
+ return self.aschoice(
+ config, 'type', menu_items)(self, config)
+
+ def add_menuitem(self, name, item):
if name in self.menuitems:
logging.info(
"Declaration of '%s' hides "
"previous menuitem declaration" % (name,))
- self.menuitems[name] = menu
+ self.menuitems[name] = item
+ if isinstance(item, MenuElement):
+ parent = item.get_ns('..')
+ if parent:
+ if item.index is not None:
+ self.children.setdefault(parent, []).insert(
+ item.index, item.get_ns())
+ else:
+ self.children.setdefault(parent, []).append(
+ item.get_ns())
- def lookup_menuitem(self, name):
+ def lookup_menuitem(self, name, default=sentinel):
if name is None:
return None
- if name not in self.menuitems:
+ if name in self.menuitems:
+ return self.menuitems[name]
+ if default is sentinel:
raise self.printer.config_error(
"Unknown menuitem '%s'" % (name,))
- return self.menuitems[name]
+ return default
+
+ def lookup_children(self, ns):
+ if ns in self.children:
+ return list(self.children[ns])
+ return list()
def load_config(self, *args):
cfg = None
@@ -1454,55 +978,21 @@ class MenuManager:
def load_menuitems(self, config):
for cfg in config.get_prefix_sections('menu '):
- name = " ".join(cfg.get_name().split()[1:])
- item = cfg.getchoice('type', menu_items)(self, cfg, name)
- self.add_menuitem(name, item)
-
- cmd_DO_help = "Menu do things"
+ item = self.menuitem_from(cfg)
+ self.add_menuitem(item.get_ns(), item)
- def cmd_DO_DUMP(self, gcmd):
- for key1 in self.parameters:
- if type(self.parameters[key1]) == dict:
- for key2 in self.parameters[key1]:
- msg = "{0}.{1} = {2}".format(
- key1, key2,
- self.parameters[key1].get(key2)
- )
- gcmd.respond_info(msg)
- else:
- msg = "{0} = {1}".format(key1, self.parameters.get(key1))
- gcmd.respond_info(msg)
-
- def _short_click_callback(self, eventtime):
+ def _click_callback(self, eventtime, event):
if self.is_running():
- self.select()
+ self.press(event)
else:
# lets start and populate the menu items
self.begin(eventtime)
- def _long_click_callback(self, eventtime):
- if not self.is_running():
- # lets start and populate the menu items
- self.begin(eventtime)
- else:
- container = self.stack_peek()
- if isinstance(container, MenuDeck):
- menu = container.menu
- if (isinstance(menu, MenuList)
- and not container.is_editing()
- and menu is not container):
- self.stack_push(menu)
- self.top_row = 0
- self.selected = 0
- return
- if container.is_editing():
- self.select(True)
-
def key_event(self, key, eventtime):
if key == 'click':
- self._short_click_callback(eventtime)
+ self._click_callback(eventtime, key)
elif key == 'long_click':
- self._long_click_callback(eventtime)
+ self._click_callback(eventtime, key)
elif key == 'up':
self.up(False)
elif key == 'fast_up':
@@ -1513,3 +1003,125 @@ class MenuManager:
self.down(True)
elif key == 'back':
self.back()
+
+ # Collection of manager class helper methods
+
+ @classmethod
+ def stripliterals(cls, s):
+ """Literals are beginning or ending by the double or single quotes"""
+ s = str(s)
+ if (s.startswith('"') and s.endswith('"')) or \
+ (s.startswith("'") and s.endswith("'")):
+ s = s[1:-1]
+ return s
+
+ @classmethod
+ def asliteral(cls, s):
+ """Enclose text by the single quotes"""
+ return "'" + str(s) + "'"
+
+ @classmethod
+ def aslatin(cls, s):
+ if isinstance(s, str):
+ return s
+ elif isinstance(s, unicode):
+ return unicode(s).encode('latin-1', 'ignore')
+ else:
+ return str(s)
+
+ @classmethod
+ def asflatline(cls, s):
+ return ''.join(cls.aslatin(s).splitlines())
+
+ @classmethod
+ def asflat(cls, s):
+ return cls.stripliterals(cls.asflatline(s))
+
+ @classmethod
+ def asbool(cls, s):
+ if isinstance(s, (bool, int, float)):
+ return bool(s)
+ elif cls.isfloat(s):
+ return bool(cls.asfloat(s))
+ s = str(s).strip()
+ return s.lower() in ('y', 'yes', 't', 'true', 'on', '1')
+
+ @classmethod
+ def asint(cls, s, default=sentinel):
+ if isinstance(s, (int, float)):
+ return int(s)
+ s = str(s).strip()
+ prefix = s[0:2]
+ try:
+ if prefix == '0x':
+ return int(s, 16)
+ elif prefix == '0b':
+ return int(s, 2)
+ else:
+ return int(float(s))
+ except ValueError as e:
+ if default is not sentinel:
+ return default
+ raise e
+
+ @classmethod
+ def asfloat(cls, s, default=sentinel):
+ if isinstance(s, (int, float)):
+ return float(s)
+ s = str(s).strip()
+ try:
+ return float(s)
+ except ValueError as e:
+ if default is not sentinel:
+ return default
+ raise e
+
+ @classmethod
+ def isfloat(cls, value):
+ try:
+ float(value)
+ return True
+ except ValueError:
+ return False
+
+ @classmethod
+ def lines_aslist(cls, value, default=[]):
+ if isinstance(value, str):
+ value = filter(None, [x.strip() for x in value.splitlines()])
+ try:
+ return list(value)
+ except Exception:
+ logging.exception("Lines as list parsing error")
+ return list(default)
+
+ @classmethod
+ def words_aslist(cls, value, sep=',', default=[]):
+ if isinstance(value, str):
+ value = filter(None, [x.strip() for x in value.split(sep)])
+ try:
+ return list(value)
+ except Exception:
+ logging.exception("Words as list parsing error")
+ return list(default)
+
+ @classmethod
+ def aslist(cls, value, flatten=True, default=[]):
+ values = cls.lines_aslist(value)
+ if not flatten:
+ return values
+ result = []
+ for value in values:
+ subvalues = cls.words_aslist(value, sep=',')
+ result.extend(subvalues)
+ return result
+
+ @classmethod
+ def aschoice(cls, config, option, choices, default=sentinel):
+ if default is not sentinel:
+ c = config.get(option, default)
+ else:
+ c = config.get(option)
+ if c not in choices:
+ raise error("Choice '%s' for option '%s'"
+ " is not a valid choice" % (c, option))
+ return choices[c]