Helios-KeyBindings.st 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. Smalltalk current createPackage: 'Helios-KeyBindings'!
  2. Object subclass: #HLBinding
  3. instanceVariableNames: 'key label'
  4. package: 'Helios-KeyBindings'!
  5. !HLBinding methodsFor: 'accessing'!
  6. atKey: aKey
  7. ^ nil
  8. !
  9. displayLabel
  10. ^ self label
  11. !
  12. key
  13. ^ key
  14. !
  15. key: anInteger
  16. key := anInteger
  17. !
  18. label
  19. ^ label
  20. !
  21. label: aString
  22. label := aString
  23. !
  24. shortcut
  25. ^ String fromCharCode: self key
  26. ! !
  27. !HLBinding methodsFor: 'actions'!
  28. applyOn: aKeyBinder
  29. !
  30. release
  31. ! !
  32. !HLBinding methodsFor: 'rendering'!
  33. renderActionFor: aBinder html: html
  34. html span class: 'command'; with: [
  35. html span
  36. class: 'label';
  37. with: self shortcut asLowercase.
  38. html a
  39. class: 'action';
  40. with: self displayLabel;
  41. onClick: [ aBinder applyBinding: self ] ]
  42. !
  43. renderOn: aBindingHelper html: html
  44. ! !
  45. !HLBinding methodsFor: 'testing'!
  46. isActive
  47. ^ self subclassResponsibility
  48. ! !
  49. !HLBinding class methodsFor: 'instance creation'!
  50. on: anInteger labelled: aString
  51. ^ self new
  52. key: anInteger;
  53. label: aString;
  54. yourself
  55. ! !
  56. HLBinding subclass: #HLBindingAction
  57. instanceVariableNames: 'command'
  58. package: 'Helios-KeyBindings'!
  59. !HLBindingAction methodsFor: 'accessing'!
  60. command
  61. ^ command
  62. !
  63. command: aCommand
  64. command := aCommand
  65. !
  66. input: aString
  67. self command input: aString
  68. !
  69. inputBinding
  70. ^ HLBindingInput new
  71. label: self command inputLabel;
  72. ghostText: self command displayLabel;
  73. defaultValue: self command defaultInput;
  74. inputCompletion: self command inputCompletion;
  75. callback: [ :val |
  76. self command
  77. input: val;
  78. execute ];
  79. yourself
  80. !
  81. inputWidget
  82. ^ HLBindingActionInputWidget new
  83. ghostText: self command displayLabel;
  84. defaultValue: self command defaultInput;
  85. inputCompletion: self command inputCompletion;
  86. callback: [ :value |
  87. self
  88. input: value;
  89. executeCommand ];
  90. yourself
  91. ! !
  92. !HLBindingAction methodsFor: 'actions'!
  93. applyOn: aKeyBinder
  94. self command isInputRequired
  95. ifTrue: [ aKeyBinder helper showWidget: self inputWidget ]
  96. ifFalse: [ self executeCommand ]
  97. !
  98. executeCommand
  99. self command execute.
  100. HLKeyBinder current deactivate
  101. ! !
  102. !HLBindingAction methodsFor: 'testing'!
  103. isActive
  104. ^ self command isActive
  105. ! !
  106. HLBinding subclass: #HLBindingGroup
  107. instanceVariableNames: 'bindings'
  108. package: 'Helios-KeyBindings'!
  109. !HLBindingGroup methodsFor: 'accessing'!
  110. activeBindings
  111. ^ self bindings select: [ :each | each isActive ]
  112. !
  113. add: aBinding
  114. ^ self bindings add: aBinding
  115. !
  116. addActionKey: anInteger labelled: aString callback: aBlock
  117. self add: ((HLBindingAction on: anInteger labelled: aString)
  118. callback: aBlock;
  119. yourself)
  120. !
  121. addGroupKey: anInteger labelled: aString
  122. self add: (HLBindingGroup on: anInteger labelled: aString)
  123. !
  124. at: aString
  125. ^ self bindings
  126. detect: [ :each | each label = aString ]
  127. ifNone: [ nil ]
  128. !
  129. at: aString add: aBinding
  130. | binding |
  131. binding := self at: aString.
  132. binding ifNil: [ ^ self ].
  133. binding add: aBinding
  134. !
  135. atKey: anInteger
  136. ^ self bindings
  137. detect: [ :each | each key = anInteger ]
  138. ifNone: [ nil ]
  139. !
  140. bindings
  141. ^ bindings ifNil: [ bindings := OrderedCollection new ]
  142. !
  143. displayLabel
  144. ^ super displayLabel, '...'
  145. ! !
  146. !HLBindingGroup methodsFor: 'actions'!
  147. release
  148. self bindings do: [ :each | each release ]
  149. ! !
  150. !HLBindingGroup methodsFor: 'rendering'!
  151. renderOn: aBindingHelper html: html
  152. self isActive ifTrue: [
  153. aBindingHelper renderBindingGroup: self on: html ]
  154. ! !
  155. !HLBindingGroup methodsFor: 'testing'!
  156. isActive
  157. ^ self activeBindings notEmpty
  158. ! !
  159. HLWidget subclass: #HLBindingActionInputWidget
  160. instanceVariableNames: 'input callback status wrapper ghostText message inputCompletion defaultValue messageTag'
  161. package: 'Helios-KeyBindings'!
  162. !HLBindingActionInputWidget commentStamp!
  163. My instances are built when a `HLBindingAction` that requires user input is applied.!
  164. !HLBindingActionInputWidget methodsFor: 'accessing'!
  165. callback
  166. ^ callback ifNil: [ callback := [ :value | ] ]
  167. !
  168. callback: aBlock
  169. callback := aBlock
  170. !
  171. defaultValue
  172. ^ defaultValue ifNil: [ '' ]
  173. !
  174. defaultValue: aString
  175. defaultValue := aString
  176. !
  177. ghostText
  178. ^ ghostText
  179. !
  180. ghostText: aText
  181. ghostText := aText
  182. !
  183. input
  184. ^ input
  185. !
  186. inputCompletion
  187. ^ inputCompletion ifNil: [ #() ]
  188. !
  189. inputCompletion: aCollection
  190. inputCompletion := aCollection
  191. !
  192. message
  193. ^ message ifNil: [ message := '' ]
  194. !
  195. message: aString
  196. message := aString
  197. !
  198. status
  199. ^ status ifNil: [ status := 'info' ]
  200. !
  201. status: aStatus
  202. status := aStatus
  203. ! !
  204. !HLBindingActionInputWidget methodsFor: 'actions'!
  205. clearStatus
  206. self status: 'info'.
  207. self message: ''.
  208. self refresh
  209. !
  210. errorStatus
  211. self status: 'error'.
  212. self refresh
  213. !
  214. evaluate: aString
  215. [ self callback value: aString ]
  216. on: Error
  217. do: [:ex |
  218. self input asJQuery
  219. one: 'keydown'
  220. do: [ self clearStatus ].
  221. self message: ex messageText.
  222. self errorStatus ]
  223. !
  224. refresh
  225. wrapper ifNil: [ ^ self ].
  226. wrapper class: self status.
  227. messageTag contents: self message
  228. ! !
  229. !HLBindingActionInputWidget methodsFor: 'rendering'!
  230. renderOn: html
  231. wrapper ifNil: [ wrapper := html span ].
  232. wrapper
  233. class: self status;
  234. with: [
  235. input := html input
  236. placeholder: self ghostText;
  237. value: self defaultValue;
  238. onKeyDown: [ :event |
  239. event which = 13 ifTrue: [
  240. self evaluate: input asJQuery val ] ]
  241. yourself.
  242. input asJQuery
  243. typeahead: #{ 'source' -> self inputCompletion }.
  244. messageTag := (html span
  245. class: 'help-inline';
  246. with: self message;
  247. yourself) ].
  248. "Evaluate with a timeout to ensure focus.
  249. Commands can be executed from a menu, clicking on the menu to
  250. evaluate the command would give it the focus otherwise"
  251. [ input asJQuery focus ] valueWithTimeout: 10
  252. ! !
  253. Object subclass: #HLKeyBinder
  254. instanceVariableNames: 'modifierKey helper bindings selectedBinding'
  255. package: 'Helios-KeyBindings'!
  256. !HLKeyBinder commentStamp!
  257. My `current` instance holds keybindings for Helios actions and evaluate them.
  258. Bindings can be nested by groups. The `bindings` instance variable holds the root of the key bindings tree.
  259. Bindings are instances of a concrete subclass of `HLBinding`.
  260. I am always either in 'active' or 'inactive' state. In active state I capture key down events and my `helper` widget is displayed at the bottom of the window. My `selectedBinding`, if any, is displayed by the helper.
  261. Bindings are evaluated through `applyBinding:`. If a binding is final (not a group of other bindings), evaluating it will result in deactivating the binder, and hiding the `helper` widget.!
  262. !HLKeyBinder methodsFor: 'accessing'!
  263. activationKey
  264. "SPACE"
  265. ^ 32
  266. !
  267. activationKeyLabel
  268. ^ 'ctrl + space'
  269. !
  270. bindings
  271. ^ bindings ifNil: [ bindings := self defaultBindings ]
  272. !
  273. escapeKey
  274. "ESC"
  275. ^ 27
  276. !
  277. helper
  278. ^ helper
  279. !
  280. selectedBinding
  281. ^ selectedBinding ifNil: [ self bindings ]
  282. ! !
  283. !HLKeyBinder methodsFor: 'actions'!
  284. activate
  285. self helper show
  286. !
  287. applyBinding: aBinding
  288. aBinding isActive ifFalse: [ ^ self ].
  289. self selectBinding: aBinding.
  290. aBinding applyOn: self
  291. !
  292. deactivate
  293. selectedBinding ifNotNil: [ selectedBinding release ].
  294. selectedBinding := nil.
  295. self helper hide
  296. !
  297. flushBindings
  298. bindings := nil
  299. !
  300. selectBinding: aBinding
  301. aBinding = selectedBinding ifTrue: [ ^ self ].
  302. selectedBinding := aBinding.
  303. self helper refresh
  304. ! !
  305. !HLKeyBinder methodsFor: 'defaults'!
  306. defaultBindings
  307. | group |
  308. group := HLBindingGroup new
  309. add: HLCloseTabCommand new asBinding;
  310. add: HLSwitchTabCommand new asBinding;
  311. yourself.
  312. HLOpenCommand registerConcreteClassesOn: group.
  313. ^ group
  314. ! !
  315. !HLKeyBinder methodsFor: 'events'!
  316. handleActiveKeyDown: event
  317. "ESC or ctrl+g deactivate the keyBinder"
  318. (event which = self escapeKey or: [
  319. event which = 71 and: [ event ctrlKey ] ])
  320. ifTrue: [
  321. self deactivate.
  322. event preventDefault.
  323. ^ false ].
  324. "Handle the keybinding"
  325. ^ self handleBindingFor: event
  326. !
  327. handleBindingFor: anEvent
  328. | binding |
  329. binding := self selectedBinding atKey: anEvent which.
  330. binding ifNotNil: [
  331. self applyBinding: binding.
  332. anEvent preventDefault.
  333. ^ false ]
  334. !
  335. handleInactiveKeyDown: event
  336. event which = self activationKey ifTrue: [
  337. event ctrlKey ifTrue: [
  338. self activate.
  339. event preventDefault.
  340. ^ false ] ]
  341. !
  342. handleKeyDown: event
  343. ^ self isActive
  344. ifTrue: [ self handleActiveKeyDown: event ]
  345. ifFalse: [ self handleInactiveKeyDown: event ]
  346. !
  347. setupEvents
  348. 'body' asJQuery keydown: [ :event | self handleKeyDown: event ]
  349. ! !
  350. !HLKeyBinder methodsFor: 'initialization'!
  351. initialize
  352. super initialize.
  353. helper := HLKeyBinderHelperWidget on: self.
  354. helper
  355. renderStart;
  356. renderCog
  357. ! !
  358. !HLKeyBinder methodsFor: 'testing'!
  359. isActive
  360. ^ ('.', self helper cssClass) asJQuery is: ':visible'
  361. !
  362. systemIsMac
  363. ^ navigator platform match: 'Mac'
  364. ! !
  365. HLKeyBinder class instanceVariableNames: 'current'!
  366. !HLKeyBinder class methodsFor: 'instance creation'!
  367. current
  368. ^ current ifNil: [ current := super new ]
  369. !
  370. new
  371. self shouldNotImplement
  372. ! !
  373. HLWidget subclass: #HLKeyBinderHelperWidget
  374. instanceVariableNames: 'keyBinder'
  375. package: 'Helios-KeyBindings'!
  376. !HLKeyBinderHelperWidget commentStamp!
  377. I am the widget responsible for displaying active keybindings in a bar at the bottom of the window. Each keybinding is an instance of `HLBinding`.
  378. Rendering is done through a double dispatch, see `#renderSelectedBindingOn:`.!
  379. !HLKeyBinderHelperWidget methodsFor: 'accessing'!
  380. cssClass
  381. ^ 'key_helper'
  382. !
  383. keyBinder
  384. ^ keyBinder
  385. !
  386. keyBinder: aKeyBinder
  387. keyBinder := aKeyBinder
  388. !
  389. mainId
  390. ^ 'binding-helper-main'
  391. !
  392. selectedBinding
  393. ^ self keyBinder selectedBinding
  394. ! !
  395. !HLKeyBinderHelperWidget methodsFor: 'actions'!
  396. hide
  397. ('.', self cssClass) asJQuery remove.
  398. self showCog
  399. !
  400. hideCog
  401. '#cog-helper' asJQuery hide
  402. !
  403. show
  404. self hideCog.
  405. self appendToJQuery: 'body' asJQuery
  406. !
  407. showCog
  408. '#cog-helper' asJQuery show
  409. !
  410. showWidget: aWidget
  411. "Some actions need to display more info to the user or request input.
  412. This method is the right place for that"
  413. ('#', self mainId) asJQuery empty.
  414. aWidget appendToJQuery: ('#', self mainId) asJQuery
  415. ! !
  416. !HLKeyBinderHelperWidget methodsFor: 'rendering'!
  417. renderBindingActionFor: aBinding on: html
  418. html span class: 'command'; with: [
  419. html span
  420. class: 'label';
  421. with: aBinding shortcut asLowercase.
  422. html a
  423. class: 'action';
  424. with: aBinding displayLabel;
  425. onClick: [ self keyBinder applyBinding: aBinding ] ]
  426. !
  427. renderBindingGroup: aBindingGroup on: html
  428. (aBindingGroup activeBindings
  429. sorted: [ :a :b | a key < b key ])
  430. do: [ :each | self renderBindingActionFor: each on: html ]
  431. !
  432. renderCloseOn: html
  433. html a
  434. class: 'close';
  435. with: [ (html tag: 'i') class: 'icon-remove' ];
  436. onClick: [ self keyBinder deactivate ]
  437. !
  438. renderCog
  439. [ :html |
  440. html
  441. div id: 'cog-helper';
  442. with: [
  443. html a
  444. with: [ (html tag: 'i') class: 'icon-cog' ];
  445. onClick: [ self keyBinder activate ] ] ]
  446. appendToJQuery: 'body' asJQuery
  447. !
  448. renderContentOn: html
  449. html div class: self cssClass; with: [
  450. self renderLabelOn:html.
  451. html div
  452. id: self mainId;
  453. with: [ self renderSelectedBindingOn: html ].
  454. self renderCloseOn: html ]
  455. !
  456. renderLabelOn: html
  457. html span
  458. class: 'selected';
  459. with: (self selectedBinding label ifNil: [ 'Action' ])
  460. !
  461. renderSelectedBindingOn: html
  462. self selectedBinding renderOn: self html: html
  463. !
  464. renderStart
  465. '#helper' asJQuery remove.
  466. [ :html |
  467. html div
  468. id: 'helper';
  469. with: 'Press ', self keyBinder activationKeyLabel, ' to start' ] appendToJQuery: 'body' asJQuery.
  470. [ '#helper' asJQuery fadeOut: 1000 ]
  471. valueWithTimeout: 2000
  472. ! !
  473. !HLKeyBinderHelperWidget class methodsFor: 'instance creation'!
  474. on: aKeyBinder
  475. ^ self new
  476. keyBinder: aKeyBinder;
  477. yourself
  478. ! !
  479. Object subclass: #HLRepeatedKeyDownHandler
  480. instanceVariableNames: 'repeatInterval delay interval keyBindings widget keyDown'
  481. package: 'Helios-KeyBindings'!
  482. !HLRepeatedKeyDownHandler commentStamp!
  483. I am responsible for handling repeated key down actions for widgets.
  484. ##Usage
  485. (self on: aWidget)
  486. whileKeyDown: 38 do: aBlock;
  487. whileKeyDown: 40 do: anotherBlock;
  488. bindKeys
  489. I perform an action block on a key press, wait for 300 ms and then preform the same action block every `repeatInterval` milliseconds until the key is released.!
  490. !HLRepeatedKeyDownHandler methodsFor: 'accessing'!
  491. keyBindings
  492. ^ keyBindings ifNil: [ keyBindings := Dictionary new ]
  493. !
  494. repeatInterval
  495. ^ repeatInterval ifNil: [ self defaultRepeatInterval ]
  496. !
  497. repeatInterval: anInteger
  498. repeatInterval := anInteger
  499. !
  500. widget
  501. ^ widget
  502. !
  503. widget: aWidget
  504. widget := aWidget
  505. ! !
  506. !HLRepeatedKeyDownHandler methodsFor: 'actions'!
  507. startRepeatingAction: aBlock
  508. ^ [ (self widget hasFocus)
  509. ifTrue: [ aBlock value ]
  510. ifFalse: [ self handleKeyUp ] ] valueWithInterval: self repeatInterval
  511. !
  512. whileKeyDown: aKey do: aBlock
  513. self keyBindings at: aKey put: aBlock
  514. ! !
  515. !HLRepeatedKeyDownHandler methodsFor: 'binding'!
  516. bindKeys
  517. self widget
  518. bindKeyDown: [ :e | self handleKeyDown: e ]
  519. keyUp: [ :e | self handleKeyUp ]
  520. !
  521. rebindKeys
  522. self
  523. unbindKeys;
  524. bindKeys
  525. !
  526. unbindKeys
  527. self widget unbindKeyDownKeyUp
  528. ! !
  529. !HLRepeatedKeyDownHandler methodsFor: 'defaults'!
  530. defaultRepeatInterval
  531. ^ 70
  532. ! !
  533. !HLRepeatedKeyDownHandler methodsFor: 'events handling'!
  534. handleEvent: anEvent forKey: anInteger action: aBlock
  535. (anEvent which = anInteger and: [ self isKeyDown not ])
  536. ifTrue: [ self whileKeyDownDo: aBlock ]
  537. !
  538. handleKeyDown: anEvent
  539. self keyBindings keysAndValuesDo: [ :key :action |
  540. self handleEvent: anEvent forKey: key action: action ]
  541. !
  542. handleKeyUp
  543. self isKeyDown ifTrue: [
  544. keyDown := false.
  545. interval ifNotNil: [ interval clearInterval ].
  546. delay ifNotNil: [ delay clearTimeout ] ]
  547. !
  548. whileKeyDownDo: aBlock
  549. keyDown := true.
  550. aBlock value.
  551. delay := [ interval := self startRepeatingAction: aBlock ]
  552. valueWithTimeout: 300
  553. ! !
  554. !HLRepeatedKeyDownHandler methodsFor: 'testing'!
  555. isKeyDown
  556. ^ keyDown ifNil: [ false ]
  557. ! !
  558. !HLRepeatedKeyDownHandler class methodsFor: 'instance creation'!
  559. on: aWidget
  560. ^ self new
  561. widget: aWidget;
  562. yourself
  563. ! !