mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
1620 lines
42 KiB
JavaScript
Executable File
1620 lines
42 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
|
|
(function(require){ var module={} // make module AMD/node compatible...
|
|
/*********************************************************************/
|
|
|
|
var actions = require('lib/actions')
|
|
var features = require('lib/features')
|
|
var toggler = require('lib/toggler')
|
|
var keyboard = require('lib/keyboard')
|
|
|
|
var core = require('features/core')
|
|
var widgets = require('features/ui-widgets')
|
|
|
|
var widget = require('lib/widget/widget')
|
|
var browse = require('lib/widget/browse')
|
|
var overlay = require('lib/widget/overlay')
|
|
var drawer = require('lib/widget/drawer')
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
|
|
var GLOBAL_KEYBOARD =
|
|
module.GLOBAL_KEYBOARD = {
|
|
'Global': {
|
|
doc: 'Global bindings that take priority over other sections.',
|
|
pattern: '*',
|
|
|
|
},
|
|
|
|
'Slideshow': {
|
|
pattern: '.slideshow-running',
|
|
drop: [
|
|
'Esc',
|
|
'Up', 'Down', 'Enter',
|
|
'R', 'L', 'G', 'T',
|
|
],
|
|
|
|
Esc: 'toggleSlideshow: "off" -- Exit slideshow',
|
|
Space: 'Esc',
|
|
Enter: 'slideshowDialog',
|
|
|
|
Left: 'resetSlideshowTimer',
|
|
Right: 'resetSlideshowTimer',
|
|
Home: 'resetSlideshowTimer',
|
|
End: 'resetSlideshowTimer',
|
|
|
|
T: 'slideshowIntervalDialog',
|
|
R: 'toggleSlideshowDirection -- Reverse slideshow direction',
|
|
L: 'toggleSlideshowLooping -- Toggle slideshow looping',
|
|
},
|
|
|
|
// XXX do we need to prevent up/down navigation here, it may get confusing?
|
|
// XXX do we need to disable fast sorting here???
|
|
'Single Image': {
|
|
pattern: '.single-image-mode',
|
|
drop: [
|
|
'Esc',
|
|
|
|
// do not crop in single image mode...
|
|
'C', 'F2',
|
|
|
|
// zooming...
|
|
'#0', '#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8', '#9',
|
|
],
|
|
|
|
|
|
// handle in next section...
|
|
'(': 'NEXT',
|
|
')': 'NEXT',
|
|
'^': 'NEXT',
|
|
'$': 'NEXT',
|
|
|
|
// zooming...
|
|
'#1': 'fitScreen',
|
|
// XXX should these also be implemented in the same way as 4-9???
|
|
'#2': 'fitNormal',
|
|
'alt+#2': 'setNormalScale -- Set current image size as normal',
|
|
'ctrl+shift+#2': 'setNormalScale: null -- Reset normal image size to default',
|
|
'#3': 'fitSmall',
|
|
'alt+#3': 'setSmallScale -- Set current image size as small',
|
|
'ctrl+shift+#3': 'setSmallScale: null -- Reset small image size to default',
|
|
|
|
// NOTE: these are the same, the only difference is the number...
|
|
'#4': 'fitCustom: 4 -- Set cutom image size',
|
|
'alt+#4': 'setCustomSize: 4 -- Set current image size as custom',
|
|
'ctrl+shift+#4': 'setCustomSize: 4 null -- Clear custom image size',
|
|
|
|
'#5': 'fitCustom: 5 -- Set cutom image size',
|
|
'alt+#5': 'setCustomSize: 5 -- Set current image size as custom',
|
|
'ctrl+shift+#5': 'setCustomSize: 5 null -- Clear custom image size',
|
|
|
|
'#6': 'fitCustom: 6 -- Set cutom image size',
|
|
'alt+#6': 'setCustomSize: 6 -- Set current image size as custom',
|
|
'ctrl+shift+#6': 'setCustomSize: 6 null -- Clear custom image size',
|
|
|
|
'#7': 'fitCustom: 7 -- Set cutom image size',
|
|
'alt+#7': 'setCustomSize: 7 -- Set current image size as custom',
|
|
'ctrl+shift+#7': 'setCustomSize: 7 null -- Clear custom image size',
|
|
|
|
'#8': 'fitCustom: 8 -- Set cutom image size',
|
|
'alt+#8': 'setCustomSize: 8 -- Set current image size as custom',
|
|
'ctrl+shift+#8': 'setCustomSize: 8 null -- Clear custom image size',
|
|
|
|
'#9': 'fitCustom: 9 -- Set cutom image size',
|
|
'alt+#9': 'setCustomSize: 9 -- Set current image size as custom',
|
|
'ctrl+shift+#9': 'setCustomSize: 9 null -- Clear custom image size',
|
|
|
|
'#0': 'fitCustom: 0 -- Set cutom image size',
|
|
'alt+#0': 'setCustomSize: 0 -- Set current image size as custom',
|
|
'ctrl+shift+#0': 'setCustomSize: 0 null -- Clear custom image size',
|
|
|
|
Esc: 'toggleSingleImage: "off" -- Exit single image view',
|
|
|
|
// ignore sorting and reversing...
|
|
// XXX not sure about these yet, especially reversing...
|
|
shift_R: 'DROP',
|
|
shift_S: 'DROP',
|
|
},
|
|
|
|
// XXX add "save as collection..."
|
|
'Crop': {
|
|
pattern: '.crop-mode',
|
|
|
|
Esc: 'uncrop',
|
|
ctrl_Esc: 'uncropAll',
|
|
|
|
W: 'testAction2 -- XXX DEBUG: remove when done...',
|
|
},
|
|
|
|
'Range': {
|
|
doc: 'Range editing',
|
|
pattern: '.brace',
|
|
|
|
// XXX add:
|
|
// - range navigation
|
|
// - range manipulation
|
|
|
|
Esc: 'clearRange',
|
|
},
|
|
|
|
// XXX add "save as collection..." (???)
|
|
// XXX cleanup...
|
|
'Viewer': {
|
|
doc: 'NOTE: binding priority is the same as the order of sections '+
|
|
'on this page.',
|
|
pattern: '*',
|
|
|
|
F1: 'browseActions: "/Help/" -- Help menu...',
|
|
|
|
alt_X: 'close',
|
|
alt_F4: 'close',
|
|
meta_Q: 'close',
|
|
|
|
// XXX
|
|
F5: 'reload!: "full" -- Reload viewer (full)',
|
|
/*F5: keyboard.doc('Reload viewer (full)',
|
|
function(){
|
|
//a.stop()
|
|
//killAllWorkers()
|
|
// .done(function(){
|
|
// reload()
|
|
// })
|
|
location.reload()
|
|
return false
|
|
}),
|
|
//*/
|
|
|
|
F12: 'showDevTools',
|
|
// NOTE: these are for systems where F** keys are not available
|
|
// or do other stuff...
|
|
meta_alt_I: 'F12',
|
|
ctrl_shift_p: 'F12',
|
|
|
|
|
|
// dialogs...
|
|
// XXX should this be all here or in respective sections???
|
|
alt_A: 'browseActions',
|
|
alt_F: 'browseActions: "/File/" -- File menu...',
|
|
alt_E: 'browseActions: "/Edit/" -- Edit menu...',
|
|
alt_N: 'browseActions: "/Navigate/" -- Navigate menu...',
|
|
|
|
//alt_S: 'browseActions: "/Sort/" -- Sort menu...',
|
|
alt_shift_A: 'listActions',
|
|
|
|
|
|
// open/save...
|
|
O: 'browsePath',
|
|
ctrl_S: 'saveIndexHere',
|
|
ctrl_shift_S: 'exportDialog',
|
|
|
|
|
|
// external editors...
|
|
// XXX not sure if this is the right way to go...
|
|
E: 'openInExtenalEditor',
|
|
shift_E: 'openInExtenalEditor: 1 -- Open in alternative editor',
|
|
ctrl_E: 'listExtenalEditors',
|
|
|
|
|
|
// history...
|
|
ctrl_H: 'listURLHistory',
|
|
ctrl_shift_H: 'listSaveHistory',
|
|
|
|
U: 'undo',
|
|
ctrl_Z: 'undo',
|
|
shift_U: 'redo',
|
|
ctrl_shift_Z: 'redo',
|
|
alt_H: 'browseActions: "/History/" -- History menu...',
|
|
|
|
|
|
// tilt...
|
|
// XXX experimental, not sure if wee need this with a keyboard...
|
|
T: 'rotateRibbonCCW -- Tilt ribbons counter clock wise',
|
|
shift_T: 'rotateRibbonCW -- Tilt ribbons clock wise',
|
|
alt_T: 'resetRibbonRotation -- Reset ribbon tilt',
|
|
|
|
|
|
// NOTE: this is handled by the wrapper at this point, so we do
|
|
// not have to do anything here...
|
|
F11: 'toggleFullScreen',
|
|
ctrl_F: 'F11',
|
|
meta_F: 'F11',
|
|
|
|
ctrl_R: 'loadNewImages!',
|
|
ctrl_alt_R: 'reload!',
|
|
ctrl_shift_R: 'F5',
|
|
|
|
|
|
// modes...
|
|
Enter: 'toggleSingleImage',
|
|
S: 'slideshowDialog',
|
|
|
|
|
|
// statusbar...
|
|
shift_I: 'toggleStatusBar',
|
|
G: 'editStatusBarIndex!',
|
|
shift_G: 'toggleStatusBarIndexMode!',
|
|
|
|
|
|
// theme...
|
|
//ctrl_B: 'toggleTheme!',
|
|
//ctrl_shift_B: 'toggleTheme!: "prev"',
|
|
'ctrl+-': 'darkerTheme!',
|
|
'ctrl++': 'lighterTheme!',
|
|
|
|
|
|
// navigation...
|
|
Left: 'prevImage',
|
|
shift_Space: 'Left',
|
|
Backspace: 'Left',
|
|
Right: 'nextImage',
|
|
Space: 'Right',
|
|
|
|
'(': 'prevImageInOrder',
|
|
')': 'nextImageInOrder',
|
|
|
|
PgUp: 'prevScreen',
|
|
ctrl_Left: 'prevScreen',
|
|
// XXX need to prevent default on mac + browser...
|
|
meta_Left: 'prevScreen',
|
|
|
|
PgDown: 'nextScreen',
|
|
ctrl_Right: 'nextScreen',
|
|
// XXX need to prevent default on mac + browser...
|
|
meta_Right: 'nextScreen',
|
|
|
|
Home: 'firstImage',
|
|
ctrl_Home: 'firstGlobalImage',
|
|
shift_Home: 'firstRibbon',
|
|
End: 'lastImage',
|
|
ctrl_End: 'lastGlobalImage',
|
|
shift_End: 'lastRibbon',
|
|
// NOTE: these (vim-like) bindings have been added by request as
|
|
// it would seem that not all keyboards have a convenient
|
|
// Home/End buttons...
|
|
'^': 'Home',
|
|
'$': 'End',
|
|
|
|
Up: 'prevRibbon',
|
|
caps_shift_Up: 'prevRibbon',
|
|
Down: 'nextRibbon',
|
|
caps_shift_Down: 'nextRibbon',
|
|
|
|
|
|
// shifting...
|
|
shift_Up: 'shiftImageUp',
|
|
caps_Up: 'shiftImageUp',
|
|
alt_shift_Up: 'travelImageUp',
|
|
ctrl_shift_Up: 'shiftImageUpNewRibbon',
|
|
ctrl_Up: 'shiftMarkedUp',
|
|
|
|
shift_Down: 'shiftImageDown',
|
|
caps_Down: 'shiftImageDown',
|
|
alt_shift_Down: 'travelImageDown',
|
|
ctrl_shift_Down: 'shiftImageDownNewRibbon',
|
|
ctrl_Down: 'shiftMarkedDown',
|
|
|
|
alt_Left: 'shiftImageLeft!',
|
|
alt_Right: 'shiftImageRight!',
|
|
|
|
shift_B: 'setBaseRibbon',
|
|
|
|
alt_PgUp: 'shiftRibbonUp',
|
|
alt_PgDown: 'shiftRibbonDown',
|
|
|
|
|
|
// editing...
|
|
R: 'rotateCW',
|
|
L: 'rotateCCW',
|
|
H: 'flipHorizontal',
|
|
V: 'flipVertical',
|
|
|
|
|
|
// ribbon image stuff...
|
|
Menu: 'browseActions!: "/Image/" -- Image menu...',
|
|
alt_I: 'Menu',
|
|
alt_R: 'browseActions: "/Ribbon/" -- Ribbon menu...',
|
|
|
|
|
|
// ranges...
|
|
// XXX experimental
|
|
// XXX add border jumping to Home/End...
|
|
'{': 'openRange',
|
|
'}': 'closeRange',
|
|
'*': 'setRangeBorder',
|
|
|
|
|
|
// zooming...
|
|
'+': 'zoomIn',
|
|
'=': '+',
|
|
'-': 'zoomOut',
|
|
'_': '-',
|
|
|
|
'#0': 'fitMax',
|
|
'#1': 'fitImage',
|
|
'shift+#1': 'fitRibbon',
|
|
'ctrl+#1': 'fitOrig!',
|
|
'#2': 'fitImage: 2 -- Fit 2 Images',
|
|
'#3': 'fitImage: 3 -- Fit 3 images',
|
|
'shift+#3': 'fitRibbon: 3.5 -- Fit 3.5 ribbons',
|
|
'#4': 'fitImage: 4 -- Fit 4 images',
|
|
'#5': 'fitImage: 5 -- Fit 5 images',
|
|
'shift+#5': 'fitRibbon: 5.5 -- Fit 5.5 ribbons',
|
|
'#6': 'fitImage: 6 -- Fit 6 images',
|
|
'#7': 'fitImage: 7 -- Fit 7 images',
|
|
'#8':'fitImage: 8 -- Fit 8 images',
|
|
'#9': 'fitImage: 9 -- Fit 9 images',
|
|
|
|
|
|
// cropping...
|
|
F2: 'cropRibbon',
|
|
D: 'cropOutRibbon',
|
|
shift_F2: 'cropOutRibbonsBelow',
|
|
ctrl_F2: 'cropMarked',
|
|
alt_F2: 'cropBookmarked',
|
|
C: 'browseActions: "/Crop/" -- Crop menu...',
|
|
|
|
|
|
// metadata...
|
|
I: 'showMetadata',
|
|
ctrl_shift_I: 'showMetadata: "current" "full" -- Show full metadata',
|
|
|
|
|
|
// marking...
|
|
M: 'toggleMark',
|
|
ctrl_A: 'toggleMark!: "ribbon" "on" -- Mark all images in ribbon',
|
|
ctrl_shift_A: 'toggleMarkBlock!',
|
|
ctrl_D: 'toggleMark!: "ribbon" "off" -- Unmark all images in ribbon',
|
|
ctrl_I: 'toggleMark!: "ribbon" -- Invert marks in ribbon',
|
|
',': 'prevMarked',
|
|
'.': 'nextMarked',
|
|
alt_M: 'browseActions: "/Mark/" -- Mark menu...',
|
|
|
|
|
|
// bookmarking...
|
|
B: 'toggleBookmark',
|
|
'[': 'prevBookmarked',
|
|
']': 'nextBookmarked',
|
|
alt_B: 'browseActions: "/Bookmark/" -- Bookmark menu...',
|
|
|
|
|
|
|
|
// copy/paste...
|
|
// do the default copy thing...
|
|
// NOTE: this stops the default: handler from getting the ctrl:
|
|
// key case...
|
|
ctrl_C: '',
|
|
ctrl_V: '',
|
|
|
|
|
|
// sort...
|
|
//shift_S: 'sortImages: "Date" -- Sort images by date',
|
|
shift_S: 'sortImages -- Sort images',
|
|
// XXX need to make this save to base_path if it exists and
|
|
// ask the user if it does not... now it always asks.
|
|
shift_R: 'reverseImages',
|
|
alt_S: 'sortDialog',
|
|
|
|
|
|
// filters...
|
|
';': 'togglePreviewFilter: "Show shadows" -- Preview shadows',
|
|
"'": 'togglePreviewFilter: "Black and white" -- Preview black and white',
|
|
|
|
// doc...
|
|
// XXX for debug...
|
|
//ctrl_G: function(){ $('.viewer').toggleClass('visible-gid') },
|
|
//'?': 'showKeyboardBindings',
|
|
'?': 'browseKeyboardBindings',
|
|
|
|
|
|
//W: 'testAction -- XXX DEBUG: remove when done...',
|
|
W: 'nonAction -- XXX DEBUG: remove when done...',
|
|
},
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
// XXX add loading/storing of kb bindings...
|
|
|
|
// XXX need a clean deep copy to restore...
|
|
var KeyboardActions = actions.Actions({
|
|
config: {
|
|
// Sets the target element to which the keyboard event handler
|
|
// is bound...
|
|
//
|
|
// Supported values:
|
|
// 'window' - window element
|
|
// 'document' - document element
|
|
// 'viewer' - the viewer (default)
|
|
// null - default element
|
|
// <css selector> - any css selector
|
|
//
|
|
// NOTE: this value is not live, to update the target restart
|
|
// the handler by cycling the toggler off and on...
|
|
// NOTE: the target element must be focusable...
|
|
'keyboard-event-source': 'window',
|
|
|
|
// limit key repeat to one per N milliseconds.
|
|
//
|
|
// Set this to -1 or null to run keys without any limitations.
|
|
'keyboard-max-key-repeat-rate': 0,
|
|
|
|
// The amount of keyboard "quiet" time to wait for when
|
|
// .pauseKeyboardRepeat(..) is called...
|
|
'keyboard-repeat-pause-check': 100,
|
|
},
|
|
|
|
get keybindings(){
|
|
return this.__keyboard_config },
|
|
get keyboard(){
|
|
var that = this
|
|
// XXX should this be here on in start event???
|
|
var kb = this.__keyboard_object =
|
|
this.__keyboard_object
|
|
|| keyboard.KeyboardWithCSSModes(
|
|
function(data){
|
|
if(data){
|
|
that.__keyboard_config = data
|
|
} else {
|
|
return that.__keyboard_config
|
|
}
|
|
},
|
|
function(){ return that.dom })
|
|
return kb },
|
|
|
|
testKeyboardDoc: ['- Interface/',
|
|
core.doc`Self-test action keyboard configuration.`,
|
|
{self_test: true},
|
|
function(){
|
|
var that = this
|
|
var keys = this.keyboard.keys()
|
|
|
|
var index = {}
|
|
Object.keys(keys).forEach(function(mode){
|
|
Object.keys(keys[mode]).forEach(function(code){
|
|
if(code == ''){
|
|
return
|
|
}
|
|
|
|
var a = keyboard.parseActionCall(code)
|
|
var doc = a.doc || that.getDocTitle(a.action) || null
|
|
|
|
// check if we have no doc...
|
|
if(doc == null || doc == ''){
|
|
console.warn('Action has no doc: "'
|
|
+ a.action +'" at: "'+ code +'"')
|
|
}
|
|
|
|
// see if two actions have the same doc...
|
|
//
|
|
// This problem can be fixed by:
|
|
// - setting a different doc in .keybindings...
|
|
// - updating action doc...
|
|
if(index[doc] && index[doc] != a.action){
|
|
console.warn('Actions have same doc/title: "'
|
|
+ index[doc] +'" and "'+ a.action
|
|
+'" at: "'+ code +'"')
|
|
}
|
|
|
|
index[doc] = a.action
|
|
})
|
|
})
|
|
}],
|
|
|
|
|
|
// Key bindings ---------------------------------------------------
|
|
// XXX need a clean deep copy to restore...
|
|
resetKeyBindings: ['Interface/Restore default key bindings',
|
|
core.doc`Restore default keyboard bindings`,
|
|
function(){
|
|
thiis.__keyboard_config = GLOBAL_KEYBOARD }],
|
|
keyHandler: ['- Interface/Get or set key handler',
|
|
// XXX this is essentially a copy of the docs for keyboard.handler(..),
|
|
// find a way to reuse...
|
|
core.doc`Get/set/unset handler for key...
|
|
|
|
In general if handler is not passed this will get the handlers,
|
|
if a handler is given this will set the handler, if the passed
|
|
handler is either null or '' then it will be unbound.
|
|
|
|
Get handler for key in all modes...
|
|
.keyHandler(<key>)
|
|
.keyHandler('*', <key>)
|
|
-> <info>
|
|
|
|
Get handlers for key in applicable modes...
|
|
.keyHandler('?', <key>)
|
|
.keyHandler('test', <key>)
|
|
-> <info>
|
|
|
|
Get handler for key in a specific mode...
|
|
.keyHandler(<mode>, <key>)
|
|
-> <info>
|
|
|
|
Get handler for key in a specific list of modes...
|
|
.keyHandler([ <mode>, .. ], <key>)
|
|
-> <info>
|
|
|
|
|
|
Bind handler to key in specific mode...
|
|
.keyHandler(mode, key, handler)
|
|
-> this
|
|
|
|
Bind handler to key in all modes...
|
|
.keyHandler('*', key, handler)
|
|
-> this
|
|
|
|
Bind handler to key in applicable modes...
|
|
.keyHandler('?', key, handler)
|
|
.keyHandler('test', key, handler)
|
|
-> this
|
|
|
|
Bind handler to key in a specific list of modes...
|
|
.keyHandler([mode, ..], key, handler)
|
|
-> this
|
|
|
|
|
|
Unbind handler from key in specific mode...
|
|
.keyHandler(mode, key, null)
|
|
.keyHandler(mode, key, '')
|
|
-> this
|
|
|
|
Unbind handler from key in all modes...
|
|
.keyHandler('*', key, null)
|
|
.keyHandler('*', key, '')
|
|
-> this
|
|
|
|
Unbind handler from key in applicable modes...
|
|
.keyHandler('?', key, null)
|
|
.keyHandler('?', key, '')
|
|
.keyHandler('test', key, null)
|
|
.keyHandler('test', key, '')
|
|
-> this
|
|
|
|
Unbind handler from key in a specific list of modes...
|
|
.keyHandler([mode, ..], key, null)
|
|
.keyHandler([mode, ..], key, '')
|
|
-> this
|
|
|
|
|
|
<info> format:
|
|
{
|
|
<mode>: <handler>,
|
|
...
|
|
}
|
|
|
|
|
|
NOTE: this is essentially aproxy to .keyboard.handler(..) for
|
|
more info see its docs.
|
|
`,
|
|
function(mode, key, action){
|
|
var res = this.keyboard.handler(mode, key, action)
|
|
// return res only if we get a handler...
|
|
if(!action){
|
|
return res
|
|
}
|
|
}],
|
|
getKeysForAction: ['- Interface/',
|
|
core.doc`Get normalized, flat set of actions and keys that trigger them...
|
|
|
|
Format:
|
|
{
|
|
<action>: [
|
|
<key>,
|
|
...
|
|
],
|
|
...
|
|
}
|
|
|
|
NOTE: this does not check overloading between modes i.e. if two
|
|
actions in two different modes are bound to the same key
|
|
only one is shown...
|
|
XXX is this a bug???
|
|
...at this point can't find when this produces inconsistencies...
|
|
`,
|
|
function(actions, modes){
|
|
var that = this
|
|
var res = {}
|
|
|
|
// Normalize handler to one of the following forms:
|
|
// - "<action>"
|
|
// - "<action>: <arg> ..."
|
|
//
|
|
// We intentionally the following because they are not
|
|
// relevant to the actual action function:
|
|
// - .doc
|
|
// - .no_default
|
|
// - .stop_propagation
|
|
var normalizeHandler = function(action){
|
|
var a = keyboard.parseActionCall(action.doc || action)
|
|
return a.action in that ?
|
|
a.action
|
|
+(a.arguments.length > 0 ?
|
|
(': '+ a.arguments.map(JSON.stringify))
|
|
: '')
|
|
: null
|
|
}
|
|
|
|
actions = actions || '*'
|
|
actions = (actions != '*' && actions instanceof Array) ?
|
|
actions
|
|
: [actions]
|
|
actions = actions != '*' ?
|
|
// normalize the inputs...
|
|
actions.map(normalizeHandler)
|
|
: actions
|
|
|
|
modes = modes || this.keyboard.modes()
|
|
modes = modes == '*' ? Object.keys(this.keybindings)
|
|
: modes instanceof Array ? modes
|
|
: [modes]
|
|
|
|
var keys = this.keyboard.keys()
|
|
|
|
// build the result -- flatten the key list...
|
|
modes.forEach(function(mode){
|
|
mode in keys
|
|
&& Object.keys(keys[mode])
|
|
// parse the actions...
|
|
.forEach(function(action){
|
|
var t = normalizeHandler(action)
|
|
if(t && (actions == '*' || actions.indexOf(t) >= 0)){
|
|
res[t] = (res[t] || []).concat(keys[mode][action])
|
|
}
|
|
})
|
|
})
|
|
|
|
return res
|
|
}],
|
|
|
|
|
|
// keyboard handling ----------------------------------------------
|
|
keyPress: ['- Interface/Handle key or keyboard event',
|
|
core.doc`Handle key / keyboard event...
|
|
|
|
Handle key...
|
|
.keyPress(<key>)
|
|
|
|
Handle key and call func if key is not bound...
|
|
.keyPress(<key>, <func>)
|
|
|
|
Handle key event...
|
|
.keyPress(<event>)
|
|
|
|
Handle key and call func if key is not bound...
|
|
.keyPress(<event>, <func>)
|
|
|
|
|
|
NOTE: care must be taken when using or binding to this (especially
|
|
the .pre stage) as this may introduce a lag into user input.
|
|
`,
|
|
{ keepDialogTitle: true },
|
|
function(key, no_match){
|
|
var that = this
|
|
// get/set the handler...
|
|
var handler = this.__key_press_handler =
|
|
this.__key_press_handler
|
|
//|| keyboard.makeKeyboardHandler(this.keyboard, null, this)
|
|
|| keyboard.makePausableKeyboardHandler(
|
|
this.keyboard,
|
|
null,
|
|
this,
|
|
function(){ return that.config['keyboard-repeat-pause-check'] })
|
|
// do the call...
|
|
return handler(key, no_match)
|
|
}],
|
|
toggleKeyboardHandling: ['- Interface/Keyboard handling',
|
|
core.doc`Toggle keyboard handling on/off`,
|
|
toggler.Toggler(null, function(_, state){
|
|
var that = this
|
|
|
|
if(state == null){
|
|
return this.__keyboard_handler ? 'on' : 'off'
|
|
}
|
|
|
|
var kb = this.keyboard
|
|
|
|
// start/reset keyboard handling...
|
|
if(state == 'on'){
|
|
// NOTE: the target element must be focusable...
|
|
var target =
|
|
this.__keyboard_event_source =
|
|
this.config['keyboard-event-source'] == null ? this.dom
|
|
: this.config['keyboard-event-source'] == 'window' ? $(window)
|
|
: this.config['keyboard-event-source'] == 'viewer' ? this.dom
|
|
: this.config['keyboard-event-source'] == 'document' ? $(document)
|
|
: $(this.config['keyboard-event-source'])
|
|
|
|
// need to reset...
|
|
this.__keyboard_handler
|
|
&& target.off('keydown', this.__keyboard_handler)
|
|
|
|
// make the base handler...
|
|
var handler = this.keyPress.bind(this)
|
|
|
|
// setup base keyboard for devel, in case something breaks...
|
|
// This branch does not drop keys...
|
|
if(this.config['keyboard-max-key-repeat-rate'] < 0
|
|
|| this.config['keyboard-max-key-repeat-rate'] == null){
|
|
this.__keyboard_handler = handler
|
|
|
|
// drop keys if repeating too fast...
|
|
// NOTE: this is done for smoother animations...
|
|
} else {
|
|
handler =
|
|
this.__keyboard_handler =
|
|
keyboard.dropRepeatingkeys(
|
|
handler,
|
|
function(){
|
|
return that.config['keyboard-max-key-repeat-rate'] })
|
|
}
|
|
|
|
target.keydown(handler)
|
|
|
|
// stop keyboard handling...
|
|
} else {
|
|
this.__keyboard_event_source
|
|
&& this.__keyboard_event_source
|
|
.off('keydown', this.__keyboard_handler)
|
|
|
|
//delete this.__keyboard_object
|
|
delete this.__keyboard_handler
|
|
delete this.__keyboard_event_source
|
|
}
|
|
},
|
|
['on', 'off'])],
|
|
pauseKeyboardRepeat: ['- Interface/Drop keys until none are pressed for a timout',
|
|
core.doc`Drop keys until non are pressed for a timeout...
|
|
|
|
This is useful for stopping repeating (held down) keys after some
|
|
event.`,
|
|
function(){
|
|
this.config['keyboard-repeat-pause-check'] > 0
|
|
&& this.keyboard.pauseRepeat
|
|
&& this.keyboard.pauseRepeat() }],
|
|
})
|
|
|
|
var Keyboard =
|
|
module.Keyboard = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'keyboard',
|
|
depends: [
|
|
'ui',
|
|
],
|
|
suggested: [
|
|
'self-test',
|
|
'keyboard-ui',
|
|
],
|
|
|
|
actions: KeyboardActions,
|
|
|
|
handlers: [
|
|
['start',
|
|
function(){
|
|
var that = this
|
|
this.__keyboard_config = this.keybindings || GLOBAL_KEYBOARD
|
|
|
|
this.toggleKeyboardHandling('on')
|
|
}],
|
|
|
|
// pause keyboard repeat...
|
|
['shiftImageUp.pre shiftImageDown.pre',
|
|
function(){
|
|
var r = this.current_ribbon
|
|
|
|
return function(){
|
|
// pause repeat if shifting last image out of the ribbon...
|
|
if(this.data.ribbons[r] == null
|
|
|| this.data.ribbons[r].len == 0){
|
|
this.pauseKeyboardRepeat()
|
|
}
|
|
}
|
|
}],
|
|
|
|
/*
|
|
['keyHandler',
|
|
function(res, mode, key, action){
|
|
action && this.testKeyboardDoc() }],
|
|
//*/
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
// XXX make these usable for any keyboard handler, not just the builtin...
|
|
var KeyboardUIActions = actions.Actions({
|
|
config: {
|
|
// NOTE: this is defined in ui-dialogs feature...
|
|
//'ui-confirm-timeout': 2000,
|
|
},
|
|
|
|
// XXX sub-group by path (???)
|
|
// XXX make this usable for other other handlers...
|
|
browseKeyboardBindings: ['Help/Keyboard bindings...',
|
|
core.doc`Keyboard bindings viewer...
|
|
|
|
|
|
This adds several keyboard bindings to the dialog:
|
|
? - show current action doc, if available.
|
|
|
|
options format:
|
|
{
|
|
// Classes to add to the dialog...
|
|
cls: 'edit',
|
|
|
|
// If true, show non-actions in the list...
|
|
// This would include:
|
|
// - aliases
|
|
// - special handlers (like: DROP, NEXT, ...)
|
|
// - actions of features not loaded or not available
|
|
show_non_actions: false,
|
|
|
|
// If true will show the placeholder text in sections that
|
|
// contain no mappings...
|
|
//
|
|
// This can also be a string, which will be shown as empty
|
|
// section text.
|
|
empty_section_text: true,
|
|
|
|
// Function used to get/generate text to represent an action
|
|
// in the list.
|
|
//
|
|
// If not defined or false action doc will be used if
|
|
// available.
|
|
get_key_text: <function>,
|
|
|
|
|
|
// Button structures for different sections...
|
|
//
|
|
// All of these must confirm to the lib/widget/browse buttons
|
|
// format:
|
|
// {
|
|
// [<html>: <func>],
|
|
// ...
|
|
// }
|
|
//
|
|
// (see: browse.Browser.update(..) doc for more info)
|
|
//
|
|
// Mode section title...
|
|
mode_buttons: <buttons>,
|
|
|
|
// Mode actions...
|
|
//
|
|
// If false or undefined the element will not be shown.
|
|
//
|
|
// NOTE: these are shown at the end of a section.
|
|
mode_actions: <buttons>,
|
|
|
|
// Key binding buttons...
|
|
key_buttons: <buttons>,
|
|
|
|
// Dropped key list buttons...
|
|
drop_buttons: <buttons>,
|
|
}
|
|
|
|
NOTE: this is designed to be a viewer by default but provide enough
|
|
configurability to support editing without actually including
|
|
any editing mechanics.
|
|
This should separate the editing mechanics from the actual
|
|
view layout while at the same time keeping it consistent.
|
|
The main drawback could be that this will complicate the
|
|
layout development until (if) it stabilizes.
|
|
`,
|
|
widgets.makeUIDialog(function(path, options){
|
|
options = options || {}
|
|
|
|
var that = this
|
|
var kb = this.keyboard
|
|
|
|
// get doc...
|
|
var getKeyText = options.get_key_text === undefined ?
|
|
function(action){
|
|
var doc = action.doc ? action.doc
|
|
: action.action in this ? this.getDocTitle(action.action)
|
|
: action.action
|
|
return doc.length == 0 ? action.action : doc
|
|
}
|
|
: options.get_key_text
|
|
|
|
var dialog = browse.makeLister(null,
|
|
function(path, make){
|
|
var keys = kb.keys('*')
|
|
var keybindings = kb.keyboard
|
|
|
|
Object.keys(keybindings)
|
|
.forEach(function(mode){
|
|
var dropped = keybindings[mode].drop || []
|
|
var bound_ignored = []
|
|
|
|
|
|
// section heading (mode)...
|
|
make.Heading(mode, {
|
|
doc: keybindings[mode].doc,
|
|
not_filtered_out: true,
|
|
// XXX should sections be searchable???
|
|
//not_searchable: true,
|
|
buttons: options.mode_buttons,
|
|
})
|
|
.attr({ mode: mode })
|
|
.addClass('mode')
|
|
|
|
// bindings...
|
|
var c = 0
|
|
Object.keys(keys[mode] || {}).forEach(function(action){
|
|
|
|
var o = keyboard.parseActionCall(action)
|
|
|
|
if(getKeyText){
|
|
var doc = ''
|
|
var text = getKeyText.call(that, o)
|
|
|
|
} else {
|
|
var doc = o.doc
|
|
var text = o.action
|
|
+ (o.no_default ? '!' : '')
|
|
+ (o.arguments.length > 0 ?
|
|
(': '+ o.arguments.map(JSON.stringify).join(' '))
|
|
: '')
|
|
}
|
|
|
|
var hidden = !options.show_non_actions
|
|
// hide all non-actions...
|
|
&& !(o.action in that
|
|
// except: functions represented by their doc...
|
|
|| keybindings[mode][action] == null
|
|
&& kb.handler(mode, keys[mode][action][0])
|
|
instanceof Function)
|
|
|
|
// NOTE: wee need the button spec to be
|
|
// searchable, thus we are not using
|
|
// the keys attr as in .browseActions(..)
|
|
make([text, ' ', '$BUTTONS']
|
|
.concat($('<span>')
|
|
.addClass('text')
|
|
.html(keys[mode][action]
|
|
// mark key if it's dropped...
|
|
.map(function(s){
|
|
if(dropped == '*'){
|
|
s += '<sup>*</sup>'
|
|
} else {
|
|
s = s.split('+')
|
|
var k = s.pop()
|
|
var i = dropped.indexOf(k)
|
|
i >= 0
|
|
&& bound_ignored
|
|
.push(dropped[i])
|
|
s.push(k + (i >= 0 ? '<sup>*</sup>' : ''))
|
|
s = s.join('+')
|
|
}
|
|
return s
|
|
})
|
|
.join(' / '))),
|
|
{
|
|
// hide stuff that is not an action...
|
|
hidden: hidden,
|
|
disabled: hidden,
|
|
|
|
buttons: options.key_buttons,
|
|
})
|
|
.attr({
|
|
'mode': mode,
|
|
'code': action,
|
|
'action': o.action,
|
|
'doc': doc.trim() != '' ?
|
|
doc
|
|
: (kb.special_handlers[action]
|
|
|| null),
|
|
})
|
|
.addClass('key'
|
|
// special stuff...
|
|
+ (action in kb.special_handlers ?
|
|
' special-action'
|
|
: '')
|
|
// aliases...
|
|
+ (o.action in that ? '' : ' non-action'))
|
|
c++
|
|
})
|
|
|
|
// no keys in view mode...
|
|
// XXX is adding info stuff like this a correct
|
|
// thing to do in code?
|
|
c == 0 && options.empty_section_text !== false
|
|
&& make(options.empty_section_text || 'No bindings...',
|
|
{
|
|
disabled: true,
|
|
hide_on_search: true,
|
|
})
|
|
.attr('mode', mode)
|
|
.addClass('info')
|
|
|
|
// unpropagated and unbound keys...
|
|
make(['Unpropagated and unbound keys:',
|
|
// NOTE: this blank is so as to avoid
|
|
// sticking the action and keys
|
|
// together in path...
|
|
' ',
|
|
'$BUTTONS',
|
|
dropped == '*' ?
|
|
dropped
|
|
: dropped
|
|
.filter(function(k){
|
|
return bound_ignored.indexOf(k) == -1 })
|
|
.join(' / ')],
|
|
{
|
|
buttons: options.drop_buttons,
|
|
})
|
|
.addClass('drop-list')
|
|
.attr('mode', mode)
|
|
|
|
// controls...
|
|
if(options.mode_actions){
|
|
var elem = make('New:', {
|
|
buttons: options.mode_actions,
|
|
})
|
|
.attr('mode', mode)
|
|
.addClass('new')
|
|
}
|
|
})
|
|
|
|
// notes...
|
|
// XXX is adding info stuff like this a correct
|
|
// thing to do in code?
|
|
make('---')
|
|
make($('<span>')
|
|
.addClass('text')
|
|
.html('<sup>*</sup> keys not propogated to next section.'),
|
|
{
|
|
disabled: true ,
|
|
hide_on_search: true,
|
|
})
|
|
.addClass('info')
|
|
},
|
|
{
|
|
path: path,
|
|
cls: [
|
|
'key-bindings',
|
|
'no-item-numbers',
|
|
options.cls,
|
|
].join(' '),
|
|
})
|
|
|
|
// handle '?' button to browse path...
|
|
dialog.showDoc = function(){
|
|
var action = dialog.select('!').attr('action')
|
|
action
|
|
&& action in that
|
|
&& that.showDoc(action)
|
|
}
|
|
// clone the bindings so as not to mess up the global browser...
|
|
dialog.keybindings = JSON.parse(JSON.stringify(dialog.keybindings))
|
|
dialog.keyboard.handler('General', '?', 'showDoc')
|
|
|
|
return dialog
|
|
})],
|
|
editKeyboardBindings: ['Interface/Keyboard bindings editor...',
|
|
core.doc`Keyboard bindings editor...
|
|
|
|
This is similar to .browseKeyboardBindings(..) but adds editing
|
|
functionality...
|
|
|
|
This adds several keyboard bindings to the dialog:
|
|
N / K - add new key binding to current mode.
|
|
M - add new mode.
|
|
|
|
NOTE: current mode is the one where focus/selection is, if no
|
|
item is selected first mode is assumed.
|
|
NOTE: for more details see: .browseKeyboardBindings(..)`,
|
|
widgets.uiDialog(function(path){
|
|
var that = this
|
|
var to_select
|
|
|
|
var sortModes = function(list){
|
|
that.keyboard.sortModes(
|
|
list.find('[mode]')
|
|
.map(function(){ return $(this).attr('mode')})
|
|
.toArray()
|
|
.unique())
|
|
}
|
|
|
|
var dialog = this.browseKeyboardBindings(
|
|
path,
|
|
{
|
|
cls: 'edit',
|
|
show_non_actions: true,
|
|
empty_section_text: false,
|
|
get_key_text: false,
|
|
|
|
// mode...
|
|
mode_buttons: [
|
|
// up...
|
|
['⏶', function(_, cur){
|
|
var mode = cur.attr('mode')
|
|
var elems = cur.parent().find('[mode="'+mode+'"]')
|
|
var prev = elems.first().prev('[mode]').attr('mode')
|
|
|
|
// move only if we have somewhere to move...
|
|
if(prev){
|
|
cur.parent().find('[mode="'+prev+'"]')
|
|
.first()
|
|
.before(elems)
|
|
dialog.select(elems.first())
|
|
|
|
// do the actual section ordering...
|
|
sortModes(cur.parent())
|
|
}
|
|
}],
|
|
// down...
|
|
['⏷', function(_, cur){
|
|
var mode = cur.attr('mode')
|
|
var elems = cur.parent().find('[mode="'+mode+'"]')
|
|
var next = elems.last().next('[mode]').attr('mode')
|
|
|
|
// move only if we have somewhere to move...
|
|
if(next){
|
|
cur.parent().find('[mode="'+next+'"]')
|
|
.last()
|
|
.after(elems)
|
|
dialog.select(elems.first())
|
|
|
|
// do the actual section ordering...
|
|
sortModes(cur.parent())
|
|
}
|
|
}],
|
|
],
|
|
mode_actions: [
|
|
// XXX focus resulting key...
|
|
['key', function(_, cur){
|
|
that.editKeyBinding(
|
|
cur.attr('mode'),
|
|
null,
|
|
function(e){ to_select = e })
|
|
.close(function(){ dialog.update() }) }],
|
|
// XXX place element...
|
|
// XXX focus resulting mode...
|
|
['mode', function(_, cur){
|
|
// XXX need to pass order info...
|
|
that.editKeyboardMode(
|
|
null,
|
|
function(e){ to_select = e })
|
|
.close(function(){ dialog.update() }) }],
|
|
],
|
|
})
|
|
// XXX should this be only a button thing (done in .browseKeyboardBindings(..))
|
|
// or also the main action???
|
|
.open(function(){
|
|
var cur = dialog.select('!')
|
|
var sub_dialog
|
|
|
|
// key...
|
|
if(cur.hasClass('key')){
|
|
sub_dialog = that
|
|
.editKeyBinding(
|
|
cur.attr('mode'),
|
|
cur.attr('code'),
|
|
function(e){ to_select = e })
|
|
|
|
// mode...
|
|
} else if(cur.hasClass('mode')){
|
|
sub_dialog = that
|
|
.editKeyboardMode(
|
|
cur.attr('mode'),
|
|
null,
|
|
function(e){ to_select = e })
|
|
|
|
// dropped...
|
|
} else if(cur.hasClass('drop-list')){
|
|
sub_dialog = that
|
|
.editKeyboardModeDroppedKeys(cur.attr('mode'))
|
|
}
|
|
|
|
sub_dialog
|
|
&& sub_dialog
|
|
.close(function(evt, mode){
|
|
mode != 'cancel' && dialog.update() })
|
|
})
|
|
// select updated/new items...
|
|
.on('update', function(){
|
|
to_select
|
|
// XXX this does not work for modes...
|
|
&& dialog.select(to_select)
|
|
to_select = null
|
|
})
|
|
|
|
dialog.newKey = function(){
|
|
that.editKeyBinding(this.select('!').attr('mode')
|
|
|| Object.keys(kb.keyboard)[0]) }
|
|
dialog.newMode = function(){
|
|
that.editKeyboardMode() }
|
|
|
|
dialog.keyboard
|
|
.handler('General', 'N', 'newKey')
|
|
.handler('General', 'K', 'newKey')
|
|
.handler('General', 'M', 'newMode')
|
|
|
|
return dialog
|
|
})],
|
|
// XXX add action completion... (???)
|
|
editKeyBinding: ['- Interface/Key mapping...',
|
|
core.doc`Key mapping editor...
|
|
|
|
Changes made in the editor are applied only when the dialog is
|
|
closed, so it is possible to cancel any edit within the dialog
|
|
by either pressing Q or the "Cancel edits" button.
|
|
|
|
This adds several keyboard bindings to the dialog:
|
|
Q - quit without saving changes.
|
|
`,
|
|
widgets.makeUIDialog(function(mode, code, callback){
|
|
var that = this
|
|
var abort = false
|
|
var orig_code = code
|
|
|
|
// list the keys (cache)...
|
|
var keys = that.keyboard.keys(code)
|
|
keys = mode in keys ?
|
|
(keys[mode][code] || [])
|
|
: []
|
|
var orig_keys = keys.slice()
|
|
|
|
var dialog = browse.makeLister(null,
|
|
function(path, make){
|
|
var cfg = {
|
|
start_on: 'open',
|
|
edit_text: 'last',
|
|
clear_on_edit: false,
|
|
reset_on_commit: false,
|
|
}
|
|
|
|
// XXX make editable???
|
|
make(['Mode:', mode || ''])
|
|
|
|
// XXX add completion...
|
|
// ...datalist seems not to work with non input fields...
|
|
make.Editable(['Code:', code || ''], {
|
|
start_on: 'open',
|
|
edit_text: 'last',
|
|
clear_on_edit: false,
|
|
reset_on_commit: false,
|
|
buttons: [
|
|
['⋯', function(evt, elem){
|
|
code = code || ''
|
|
// highlight the current action...
|
|
var a = keyboard.parseActionCall(code)
|
|
var p = a.action in that ?
|
|
that.getDocPath(a.action)
|
|
: ''
|
|
// use the action menu to select actions...
|
|
var dialog = that.browseActions(p, {
|
|
no_disabled: true,
|
|
no_hidden: true,
|
|
callback: function(action){
|
|
code = action
|
|
elem.find('.text').last().text(action)
|
|
dialog.close()
|
|
},
|
|
})
|
|
|
|
dialog.dom.attr({
|
|
'dialog-title': 'Action picker...',
|
|
'keep-dialog-title': true,
|
|
})
|
|
}],
|
|
],
|
|
})
|
|
.on('edit-commit',
|
|
function(evt, text){ code = text })
|
|
|
|
make('---')
|
|
|
|
make.EditableList(keys, {
|
|
unique: true,
|
|
|
|
normalize: keyboard.normalizeKey,
|
|
check: keyboard.isKey,
|
|
})
|
|
|
|
make('---')
|
|
|
|
make.ConfirmAction('Delete', {
|
|
callback: function(){
|
|
keys = []
|
|
dialog.close()
|
|
},
|
|
timeout: that.config['ui-confirm-timeout'] || 2000,
|
|
buttons: [
|
|
['Cancel edit', function(){
|
|
abort = true
|
|
make.dialog.close('cancel')
|
|
}],
|
|
],
|
|
})
|
|
},
|
|
{
|
|
cls: 'table-view',
|
|
})
|
|
// save the keys...
|
|
// XXX for some reason when Esc this is called twice...
|
|
.on('close', function(){
|
|
if(abort){
|
|
return
|
|
}
|
|
|
|
// remove keys...
|
|
orig_keys
|
|
.filter(function(k){ return keys.indexOf(k) < 0 })
|
|
.forEach(function(k){
|
|
that.keyHandler(mode, k, '')
|
|
})
|
|
|
|
var new_keys = code == orig_code ?
|
|
keys.filter(function(k){ orig_keys.indexOf(k) < 0 })
|
|
: keys
|
|
|
|
// add keys...
|
|
new_keys
|
|
.forEach(function(k){
|
|
that.keyHandler(mode, k, code) })
|
|
|
|
callback
|
|
&& callback.call(that, code)
|
|
})
|
|
|
|
dialog.abort = function(){
|
|
abort = true
|
|
this.close()
|
|
}
|
|
dialog.keyboard
|
|
.handler('General', 'Q', 'abort')
|
|
|
|
return dialog
|
|
})],
|
|
editKeyboardMode: ['- Interface/Mode...',
|
|
core.doc`Mode editor...
|
|
|
|
Create new mode...
|
|
.editKeyboardMode()
|
|
-> dialog
|
|
|
|
Edit/create mode by <name>...
|
|
.editKeyboardMode(<name>)
|
|
-> dialog
|
|
|
|
|
|
Changes made in the editor are applied only when the dialog is
|
|
closed, so it is possible to cancel any edit within the dialog
|
|
by either pressing Q or the "Cancel edits" button.
|
|
|
|
This adds several keyboard bindings to the dialog:
|
|
Q - quit without saving changes.
|
|
|
|
|
|
NOTE: empty mode name will not get saved.
|
|
`,
|
|
widgets.makeUIDialog(function(mode, callback){
|
|
var that = this
|
|
var abort = false
|
|
var doc = (that.keybindings[mode] || {}).doc
|
|
var pattern = (that.keybindings[mode] || {}).pattern || mode
|
|
|
|
var orig_mode = mode in that.keybindings ? mode : null
|
|
|
|
var dialog = browse.makeLister(null,
|
|
function(path, make){
|
|
var cfg = {
|
|
start_on: 'open',
|
|
edit_text: 'last',
|
|
clear_on_edit: false,
|
|
reset_on_commit: false,
|
|
}
|
|
|
|
make.Editable(['Mode:', mode || ''], cfg)
|
|
.on('edit-commit',
|
|
function(evt, text){ mode = text.trim() })
|
|
make.Editable(['Doc:', doc || ''], cfg)
|
|
.on('edit-commit',
|
|
function(evt, text){ doc = text.trim() })
|
|
make.Editable(['Pattern:', pattern], cfg)
|
|
.on('edit-commit',
|
|
function(evt, text){ pattern = text })
|
|
|
|
make('---')
|
|
|
|
make.ConfirmAction('Delete', {
|
|
callback: function(){
|
|
if(mode in that.keybindings){
|
|
delete that.keybindings[mode]
|
|
}
|
|
dialog.close()
|
|
},
|
|
timeout: that.config['ui-confirm-timeout'] || 2000,
|
|
buttons: [
|
|
['Cancel edit', function(){
|
|
abort = true
|
|
make.dialog.close('cancel')
|
|
}],
|
|
],
|
|
})
|
|
},
|
|
{
|
|
cls: 'table-view',
|
|
})
|
|
.on('close', function(){
|
|
if(abort){
|
|
return
|
|
}
|
|
|
|
var data = that.keybindings[orig_mode] || {}
|
|
|
|
data.doc = doc
|
|
data.pattern = pattern
|
|
|
|
// update mode name if it changed...
|
|
if(mode != orig_mode && mode != ''){
|
|
var order = Object.keys(that.keybindings)
|
|
|
|
if(orig_mode){
|
|
order[order.indexOf(orig_mode)] = mode
|
|
delete that.keybindings[orig_mode]
|
|
}
|
|
|
|
that.keybindings[mode] = data
|
|
|
|
that.keyboard.sortModes(order)
|
|
}
|
|
|
|
callback
|
|
&& callback.call(that, mode)
|
|
})
|
|
|
|
dialog.abort = function(){
|
|
abort = true
|
|
this.close('cancel')
|
|
}
|
|
dialog.keyboard
|
|
.handler('General', 'Q', 'abort')
|
|
|
|
return dialog
|
|
})],
|
|
editKeyboardModeDroppedKeys: ['- Interface/Dropped keys...',
|
|
core.doc`Edit keys dropped after a mode...
|
|
|
|
Edit the first mode...
|
|
.editKeyboardModeDroppedKeys()
|
|
-> dialog
|
|
|
|
Edit a specific mode...
|
|
.editKeyboardModeDroppedKeys(<mode>)
|
|
-> dialog
|
|
-> false
|
|
NOTE: if a mode does not exist this will not create a
|
|
dialog and will return false.
|
|
|
|
Changes made in the editor are applied only when the dialog is
|
|
closed, so it is possible to cancel any edit within the dialog
|
|
by either pressing Q or the "Cancel edits" button.
|
|
|
|
This adds several keyboard bindings to the dialog:
|
|
Q - quit without saving changes.
|
|
`,
|
|
widgets.makeUIDialog(function(mode){
|
|
var that = this
|
|
var abort = false
|
|
|
|
mode = mode || Object.keys(that.keybindings)[0]
|
|
|
|
if(!(mode in that.keybindings)){
|
|
return false
|
|
}
|
|
|
|
var drop = (that.keybindings[mode].drop || []).slice()
|
|
|
|
var dialog = browse.makeLister(null,
|
|
function(path, make){
|
|
var drop_all
|
|
|
|
// the list editor...
|
|
make.EditableList(function(keys){
|
|
// get...
|
|
if(keys === undefined){
|
|
return drop && drop != '*' ? drop : []
|
|
|
|
// set...
|
|
} else {
|
|
drop = drop_all ? '*' : keys
|
|
}
|
|
},
|
|
{
|
|
unique: true,
|
|
|
|
normalize: keyboard.normalizeKey,
|
|
check: keyboard.isKey,
|
|
})
|
|
|
|
make.Separator()
|
|
|
|
// '*' toggler...
|
|
// XXX should this close the dialog???
|
|
make.ConfirmAction('Drop all keys', {
|
|
callback: function(){
|
|
drop_all = '*'
|
|
|
|
make.dialog.close()
|
|
},
|
|
timeout: that.config['ui-confirm-timeout'] || 2000,
|
|
buttons: [
|
|
['Cancel edit', function(){
|
|
abort = true
|
|
make.dialog.close('cancel')
|
|
}],
|
|
],
|
|
})
|
|
})
|
|
.on('close', function(){
|
|
if(!abort){
|
|
that.keybindings[mode].drop = drop
|
|
}
|
|
})
|
|
|
|
dialog.abort = function(){
|
|
abort = true
|
|
this.close()
|
|
}
|
|
dialog.keyboard
|
|
.handler('General', 'Q', 'abort')
|
|
|
|
return dialog
|
|
})],
|
|
|
|
|
|
/*/ XXX move to gen2
|
|
// XXX need to pre-process the docs...
|
|
// - remove the path component...
|
|
// - insert the action name where not doc present...
|
|
// XXX cleanup CSS
|
|
showKeyboardBindings: ['- Interface/Show keyboard bindings...',
|
|
widgets.makeUIDialog('Drawer',
|
|
function(){
|
|
return keyboard.buildKeybindingsHelpHTML(
|
|
this.__keyboard_config,
|
|
this,
|
|
function(action){
|
|
return Object.keys(this.getPath(action))[0] })
|
|
},
|
|
{
|
|
background: 'white',
|
|
focusable: true,
|
|
})],
|
|
//*/
|
|
})
|
|
|
|
|
|
var KeyboardUI =
|
|
module.KeyboardUI = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'keyboard-ui',
|
|
depends: [
|
|
'keyboard',
|
|
'ui-dialogs',
|
|
],
|
|
|
|
actions: KeyboardUIActions,
|
|
})
|
|
|
|
|
|
|
|
|
|
/**********************************************************************
|
|
* vim:set ts=4 sw=4 : */ return module })
|