/* AIRMenuBuilder.js - Revision: 1.5 */ /* ADOBE SYSTEMS INCORPORATED Copyright 2007-2008 Adobe Systems Incorporated. All Rights Reserved. NOTICE: Adobe permits you to modify and distribute this file only in accordance with the terms of Adobe AIR SDK license agreement. You may have received this file from a source other than Adobe. Nonetheless, you may modify or distribute this file only in accordance with such agreement. */ (function AIRMenuBuilder () { function constructor ( ) { window ['air'] = window ['air'] || {}; window.air['ui'] = window.air['ui'] || {}; window.air.ui['Menu'] = new Menu(); registry = new FieldsRegistry(); currentOS = runtime.flash.system.Capabilities.os; } var currentOS = null; var registry = null; var File = runtime.flash.filesystem.File; var FileStream = runtime.flash.filesystem.FileStream; var FileMode = runtime.flash.filesystem.FileMode; var NativeMenu = runtime.flash.display.NativeMenu; var NativeMenuItem = runtime.flash.display.NativeMenuItem; var SELECT = runtime.flash.events.Event.SELECT; var KEYBOARD = runtime.flash.ui.Keyboard; var COMPLETE = runtime.flash.events.Event.COMPLETE; var IO_ERROR = runtime.flash.events.IOErrorEvent.IO_ERROR; var NativeApplication = runtime.flash.desktop.NativeApplication; var NativeWindow = runtime.flash.display.NativeWindow; var Loader = runtime.flash.display.Loader; var URLRequest = runtime.flash.net.URLRequest; var BitmapData = runtime.flash.display.BitmapData; /** * CLASS FieldsRegistry * @class * @private */ function FieldsRegistry () { this.proof = function (name, value) { if (!validateName(name)) { return null }; switch (name) { case FieldsRegistry.ENABLED: case FieldsRegistry.ALT_KEY: case FieldsRegistry.SHIFT_KEY: case FieldsRegistry.CMD_KEY: case FieldsRegistry.CTRL_KEY: case FieldsRegistry.TOGGLED: case FieldsRegistry.DEFAULT_KEY: return (typeof value == 'boolean')? value: (typeof value == 'string')? (value.toLowerCase() == 'false')? false : true : getDefault (name); case FieldsRegistry.KEY_EQUIVALENT: var d; return (typeof value == 'string')? (value.length == 1)? value : getDefault (name) : getDefault (name); case FieldsRegistry.LABEL: return (typeof value == 'string')? (value.length != 0)? value: getDefault (name) : getDefault (name); case FieldsRegistry.MNEMONIC_INDEX: var n; return (typeof value == 'number')? value: (typeof value == 'string')? (!isNaN ( n = parseInt(value) ))? n : getDefault (name) : getDefault (name); case FieldsRegistry.TYPE: return (typeof value == 'string') ? (validateType(value))? value : getDefault (name) : getDefault (name); case FieldsRegistry.ON_SELECT: var f; return (typeof value == 'function')? value : (typeof value == 'string')? (typeof (f = window[value]) == 'function')? f : getDefault (name) : getDefault (name); } } this.iterateFields = function (callback, scope) { var f, n, fr = FieldsRegistry; for (f in fr) { n = fr [f] !== fr.prototype? fr [f] : null; if (n && !validateType(n)) { callback.call ( scope || window, n ) }; } } var validateType = function (type) { return type == FieldsRegistry.REGULAR || type == FieldsRegistry.SEPARATOR || type == FieldsRegistry.CHECK; } var validateName = function (fieldName) { for (var f in FieldsRegistry) { if (FieldsRegistry[f] == fieldName) { return true }; } return false; } var getDefault = function (fieldName) { switch (fieldName) { case FieldsRegistry.ALT_KEY: case FieldsRegistry.SHIFT_KEY: case FieldsRegistry.TOGGLED: return false; case FieldsRegistry.ENABLED: case FieldsRegistry.DEFAULT_KEY: return true; case FieldsRegistry.KEY_EQUIVALENT: case FieldsRegistry.ON_SELECT: return null; case FieldsRegistry.LABEL: return ' '; case FieldsRegistry.MNEMONIC_INDEX: return -1; case FieldsRegistry.TYPE: return FieldsRegistry.REGULAR; case FieldsRegistry.CMD_KEY: case FieldsRegistry.CTRL_KEY: default: return null; } } } FieldsRegistry.ALT_KEY = 'altKey'; FieldsRegistry.CMD_KEY = 'cmdKey'; FieldsRegistry.CTRL_KEY = 'ctrlKey'; FieldsRegistry.ENABLED = 'enabled'; FieldsRegistry.KEY_EQUIVALENT = 'keyEquivalent'; FieldsRegistry.LABEL = 'label'; FieldsRegistry.MNEMONIC_INDEX = 'mnemonicIndex'; FieldsRegistry.SHIFT_KEY = 'shiftKey'; FieldsRegistry.TOGGLED = 'toggled'; FieldsRegistry.TYPE = 'type'; FieldsRegistry.ON_SELECT = 'onSelect'; FieldsRegistry.DEFAULT_KEY = 'defaultKeyEquivalentModifiers'; FieldsRegistry.SEPARATOR = 'separator'; FieldsRegistry.CHECK = 'check'; FieldsRegistry.REGULAR = 'regular'; /** * CLASS Menu * Description * Loads a user menu defined as XML or JSON, and sets it as one of the * supported menu types. * @class * @author ciacob */ function Menu() { var buildMenu = function (source, type) { var b = new Builder(); b.loadData (source, type); return b.build(); } var attachMenu = function (menu, type, target, icons) { var s = new Shell(); s.link(menu, type, target, icons); } /** * Load a menu defined in XML format. * @param source * An object containing XML menu(s) to be loaded for various OS-es. * @return * A NativeMenu object built from the given XML source. */ this.createFromXML = function ( source ) { return buildMenu ( source, Builder.XML ); } /** * Same as air.ui.Menu.fromXML, except it handles JSON data. */ this.createFromJSON = function ( source ) { return buildMenu ( source, Builder.JSON ); } /** * - on Windows: sets the given nativeMenu object as the NativeWindow's * menu; * - on Mac: inserts the items of the given nativeMenu object between * the 'Edit' and 'Window' default menus; * @param nativeMenu * A NativeMenu returned by one of the air.ui.Menu.from... * functions. * @param overwrite * A boolean that will change the behavior on Mac. If true, the * default menus will be replaced entirely by the given nativeMenu */ this.setAsMenu = function ( nativeMenu, overwrite ) { if (!arguments.length) { throw (new Error( "No argument given for the 'setAsMenu()' method." )); } var style = overwrite? Shell.MENU | Shell.OVERWRITE : Shell.MENU; attachMenu (nativeMenu, style); } /** * Displays the given menu as a contextual menu when the user right * clicks a certain DOM element. * @param nativeMenu * A NativeMenu returned by one of the air.ui.Menu.from... * functions. * @param domElement * The DOM Element to link with the given nativeMenu. The * contextual menu will only show when the user right clicks over * domElement. This attribute is optional. If missing, the context * menu will display on every right-click over the application. */ this.setAsContextMenu = function ( nativeMenu, domElement ) { if (!arguments.length) { throw (new Error( "No argument given for the 'setAsContextMenu()' method." )); } if (arguments.length < 2) { domElement = Shell.UNSPECIFIED }; attachMenu (nativeMenu, Shell.CONTEXT, domElement); } /** * Sets the given nativeMenu as the * ''NativeApplication.nativeApplication.icon.menu'' property. * @param nativeMenu * A NativeMenu returned by one of the air.ui.Menu.from... * functions. * @param icons * An array holding icon file paths or bitmap data objects. * If specified, these will be used as the application's * tray/dock icons. * @throws * If no bitmap data was set for the ''icon'' object and no default * icons are specified in the application descriptor. */ this.setAsIconMenu = function ( nativeMenu, icons ) { if (!arguments.length) { throw (new Error( "No argument given for the 'setAsIconMenu()' method." )); } attachMenu (nativeMenu, Shell.ICON, null, icons); } } /** * CLASS DataSource * @public * @abstract */ function DataSource() { var _this = this; var legalExtensions = ['xml', 'js']; var rSeed = null; var DATA_OBJECT = 1; var INLINE_STRING = 2; var FILE_PATH = 3; var FILE_OBJECT = 4; var ILLEGAL_TYPE = 5; function getFileContent (file) { var ret = ''; var fileStream = new FileStream(); fileStream.open(file, FileMode.READ); try { ret = fileStream.readUTFBytes(file.size); } catch(e) { throw( new Error(["Error\n", "ID: ", e.errorID, "\n", "Message: ", e.message, "\n"].join('')) ); } fileStream.close(); return ret; } function checkExtension (url, whiteList) { var match = url.match(/\.([^\.]*)$/); var extension = match? match[1] : null; for(var i=0; i 0); if (isError) { var errText = doc.getElementsByTagName(err)[0].innerText; var msg = errText.split(':'); msg.length -= 1; msg = msg.join(':\n'); throw (new Error ([ 'Could not parse data: malformed XML file.', msg ].join('\n'))); } that.document = doc; } } that.getRoot = function() { return that.document.documentElement; } that.getChildren = function (node) { var ret = []; if(node) { if(node.hasChildNodes && node.hasChildNodes()){ var children = node.childNodes; for(var i=0; i 0) { var prev = iterable[i-1]; prev['nextSibling'] = child; } } } } return ret; } that.getNextSibling = function (node) { if (node) { if(node !== that.getRoot()) { if (node['nextSibling']) { return node['nextSibling'] }; } } return null; } that.getParent = function (node) { if (node) { if(node !== that.getRoot()) { if (node['parent']) { return node['parent'] }; } } return null; } that.hasChildren = function (node) { if (node) { var iterable = (node === that.getRoot())? node: (node ['items'])? node ['items']: null; if (iterable) { return iterable.length && iterable.length > 0; } return false; } return false; } that.addChildAt = function (node, newChild, index) { if (node && newChild) { var children = that.getChildren (node) || (function() { node['items'] = []; return node['items']; })(); index = Math.min(Math.max(0, index), children.length); children.splice (index, 0, newChild); if (index > 0) { children [index-1]['nextSibling'] = children [index] }; if (index < children.length-1) { children[index]['nextSibling'] = children [index]+1 }; node['items'] = children; } } that.removeChildAt = function (node, index) { if (node) { var children = that.getChildren (node) || (function() { node['items'] = []; return node['items']; })(); index = Math.min(Math.max(0, index), children.length); var removed = children [index]; children.splice (index, 1); if(index > 0 && index < children.length) { children [index-1]['nextSibling'] = children [index]; } node['items'] = children; return removed; } return null; } that.createNode = function (properties) { var node = {}; for (var p in properties) { that.setProperty (node, p, properties[p]) }; if (that.getProperty (node, 'id') == null) { that.setProperty (node, 'id', that.generateUID()); } return node; } that.getProperty = function (node, propName) { if (node) { return registry.proof(propName, node[propName]) }; return null; } that.setProperty = function (node, propName, propValue) { if (node) { node[propName] = registry.proof(propName, propValue); } } this.$JSONDataSource.apply (this, arguments); } /** * CLASS Builder * @private * @class */ function Builder() { var ds, root = null; function createDataSource (source, type) { var ret = null; if (type == Builder.XML) { ret = new XMLDataSource ( source ) }; if (type == Builder.JSON) { ret = new JSONDataSource( source )}; return ret; } function buildMenu() { var w = new Walker(ds, buildItem); w.walk (); } function buildItem (item) { // Get & parse info about the item to be built: var summary = ds.getSummary (item); var isFirstLevel = (!ds.getParent(item)); var isItemDisabled = (!summary[FieldsRegistry.ENABLED]); var hasChildren = ds.hasChildren(item); var isItemSeparator = (summary [FieldsRegistry.TYPE] == FieldsRegistry.SEPARATOR); var isItemAToggle = (summary [FieldsRegistry.TYPE] == FieldsRegistry.CHECK); // Build the NativeMenuItem to represent this item: var ret = parseLabelForMnemonic (summary [FieldsRegistry.LABEL]); var nmi = new NativeMenuItem ( ret[0], isItemSeparator ); // Attach features for this item: var parsedMnemonicIndex = ret[1]; if (parsedMnemonicIndex >= 0) { summary [FieldsRegistry.MNEMONIC_INDEX] = parsedMnemonicIndex; }; var mnemonicIndex = summary [FieldsRegistry.MNEMONIC_INDEX]; if (mnemonicIndex != -1) { nmi.mnemonicIndex = mnemonicIndex }; if (isItemAToggle) { var toggler = function (event) { var val = !ds.getProperty (item, FieldsRegistry.TOGGLED); ds.setProperty (item, FieldsRegistry.TOGGLED, val); nmi.checked = val; } nmi.addEventListener (SELECT, toggler); nmi.checked = summary [FieldsRegistry.TOGGLED]; } if (summary [FieldsRegistry.ON_SELECT]) { var f = function (event) { var target = event.target; summary [FieldsRegistry.ON_SELECT].call ( window, event, summary ); } nmi.addEventListener (SELECT, f); } attachKeyEquivalentHandler (nmi, summary); if ( isItemDisabled ) { nmi.enabled = false }; // Attach our item within the menu structure: item['_widget_'] = nmi; if (hasChildren) { nmi.submenu = new NativeMenu() }; var data = nmi.data || (nmi.data = {}); data['item'] = item; var parMnu = null; var parItem = ds.getParent(item); if (parItem) { var parWidget = parItem['_widget_']; parMnu = parWidget.submenu; if (!parMnu) { return }; } else { parMnu = root || ( root = new NativeMenu() ); } parMnu.addItem(nmi); } function qReplace (tStr, searchStr , replaceStr) { var index; while ((index = tStr.indexOf (searchStr)) >= 0) { var arr = tStr.split(''); arr.splice (index, searchStr.length, replaceStr); tStr = arr.join(''); } return tStr; } function parseLabelForMnemonic (label) { var l = label; if (l) { l = qReplace(l, '__', '[UNDERSCORE]'); l = qReplace(l, '_', '[MNEMONIC]'); l = qReplace(l, '[UNDERSCORE]', '_'); var mi = l.indexOf ('[MNEMONIC]'); l = qReplace(l, '[MNEMONIC]', ''); if (mi >= 0) { return [l, mi] }; } return [l, -1]; } function attachKeyEquivalentHandler (nativeItem, summary) { if (summary[FieldsRegistry.DEFAULT_KEY]) { // Linux implementation needs this check: var def = nativeItem.keyEquivalentModifiers && nativeItem.keyEquivalentModifiers[0]? nativeItem.keyEquivalentModifiers[0] : null; if (def && typeof def != "undefined") { if (summary[FieldsRegistry.CTRL_KEY] === false) { if (def == KEYBOARD.CONTROL) { def = null }; } if (summary[FieldsRegistry.CMD_KEY] === false) { if (def == KEYBOARD.COMMAND) { def = null }; } } } var key; if (key = summary[FieldsRegistry.KEY_EQUIVALENT]) { var mods = []; if (def) { mods.push(def) }; if (summary[FieldsRegistry.CTRL_KEY]) { mods.push (KEYBOARD.CONTROL); } if (summary[FieldsRegistry.CMD_KEY]) { mods.push (KEYBOARD.COMMAND); } if (summary[FieldsRegistry.ALT_KEY]) { mods.push (KEYBOARD.ALTERNATE); } key = (summary[FieldsRegistry.SHIFT_KEY])? key.toUpperCase() : key.toLowerCase(); nativeItem.keyEquivalent = key; nativeItem.keyEquivalentModifiers = mods; } } this.loadData = function (source, type) { if (source) { ds = createDataSource (source, type) } else { throw new Error([ "Cannot create menu. ", "Provided data source is null" ].join('')) } } this.build = function() { if(ds) {buildMenu()}; return root; } } Builder.XML = 0x10; Builder.JSON = 0x20; /** * CLASS NIConnector * @private * @class */ function NIConnector () { var that = this; var LAST = 0x1; var BEFORE_LAST = 0x2; var ni; var nativeMenu; var overwrite; var allSet; var isMac; function $NIConnector (oNi, oNewNativeMenu, bOverwriteExisting) { if (oNi && oNewNativeMenu) { allSet = true; ni = oNi; nativeMenu = oNewNativeMenu; overwrite = bOverwriteExisting; isMac = currentOS.indexOf('Mac') >= 0; if (typeof NIConnector.defaultMenu == "undefined") { var app = NativeApplication.nativeApplication; NIConnector.defaultMenu = app.menu; } } } function isDefaultApplicationMenu () { var app = NativeApplication.nativeApplication; return (app.menu == NIConnector.defaultMenu); } function purge () { while (ni.menu.numItems) { ni.menu.removeItemAt (0) } } function add ( style ) { if (!ni.menu) { replace(); return; } var addFunction = (style == LAST)? ni.menu.addItem : function (item) { ni.menu.addItemAt (item, ni.menu.numItems-1); } var item; while (nativeMenu.numItems && (item = nativeMenu.removeItemAt(0))) { if(isMac && !item.submenu) { continue }; addFunction.call (that, item); } } function replace () { ni.menu = nativeMenu; } this.doConnect = function () { if (allSet) { if (overwrite) { if (isMac) { purge (); add (LAST); } else { replace() }; } else { if (isMac) { if (isDefaultApplicationMenu()) { add (BEFORE_LAST) } else { add (LAST) }; } else { add (LAST) }; } } } $NIConnector.apply (this, arguments); } NIConnector.defaultMenu; /** * CLASS Shell * @private * @class */ function Shell() { function $Shell(){} var that = this; var CONTEXT_MENU = 'contextmenu'; var app = NativeApplication.nativeApplication; var uidSeed = 0; var DEFAULT_ID = "DEFAULT_ID"; var isMac = currentOS.indexOf('Mac') >= 0; var isBitmapData = function(obj) { return obj && obj.constructor && obj.constructor === (new BitmapData (1, 1)); } var resolveDomEl = function (obj) { var ret = null; if (obj) { if (typeof obj == 'object' && obj.nodeType == 1) { ret = obj }; if (typeof obj == 'string') { var el; if (el = document.getElementById(obj)) { ret = el }; } } return ret; } var checkUserIcon = function (obj) { var icon = app.icon; return icon.bitmaps.length > 0; } var getIcons = function (userIcons) { var ret = []; var entries = []; if (userIcons && userIcons.length) { entries = userIcons; } else { var p = new DOMParser(); var descr = String(app.applicationDescriptor); var descrDoc = p.parseFromString(descr, "text/xml"); var appEl = descrDoc.getElementsByTagName('application')[0]; var iconEl = appEl.getElementsByTagName('icon')[0]; if (iconEl) { var iconEntries = iconEl.getElementsByTagName('*'); for (var i=0; i 0; if (!haveDefaultIcons) { if (!isMac) { throw (new Error([ "Cannot set the icon menu.", "On operating systems that do not provide a default", "tray icon, you must specify one before calling", "setAsIconMenu().", "Alternativelly, you can specify default icons in the", "application's XML descriptor." ].join('\n'))); } } var doAttach = function(bitmaps){ setBitmaps (bitmaps); app.icon.menu = menu; } if (defaultIcons) { loadDefaultBitmaps(defaultIcons, doAttach); } } } this.link = function (oMenu, style, target, icons) { if (Shell.MENU & style) { var bOverwrite = style & Shell.OVERWRITE; return linkMenu(oMenu, bOverwrite); } if (Shell.CONTEXT & style) {return linkContextMenu(oMenu, target)}; if (Shell.ICON & style) { return linkIconMenu(oMenu, icons) }; } $Shell.apply (this, arguments); } Shell.UNSPECIFIED = -1; Shell.MENU = 1; Shell.CONTEXT = 2; Shell.ICON = 4; Shell.OVERWRITE = 8; Shell.resolve = function (pathOrFile) { var file = null; try { file = File(pathOrFile); } catch(e) { file = File.applicationDirectory.resolvePath (pathOrFile); if (!file.exists) { try { file = new File (pathOrFile); } catch(e) { // must be a path, both 'relative' AND 'non-existing'. } } } return file; } /** * CLASS Walker * @class * @private */ function Walker() { var t, c, currentItem, allSet, item; function $Walker (target, callback) { if (target && target instanceof DataSource) { t = target; } if (callback && typeof callback == "function") { c = callback; } if (t && c) { allSet = true }; } function getNearestAncestorSibling(node) { while (node) { node = t.getParent(node); if(node) { var s = t.getNextSibling(node); if (s) { return s }; } } return null; } function getFirstChildOfRoot() { return t.getChildren(t.getRoot())[0] || null; } function doTraverse() { if (allSet) { while (item = getNext()) { c.call (window, item) }; } else { throw (new Error([ 'Cannot traverse data tree.', 'Please check the arguments you provided to the Walker class.', ].join('\n'))); } } function getNext() { if (currentItem === null) { return null }; if (typeof currentItem == 'undefined') { currentItem = getFirstChildOfRoot(); } if (t.hasChildren(currentItem)) { var parentNode = currentItem; currentItem = t.getChildren(currentItem)[0]; return parentNode; } if(t.getNextSibling(currentItem)) { var current = currentItem; currentItem = t.getNextSibling(currentItem); return current; } var ci = currentItem; currentItem = getNearestAncestorSibling(currentItem); return ci; } this.walk = function (callback) { doTraverse(); if (typeof callback == "function") { callback.call (this) }; } $Walker.apply (this, arguments); } constructor.apply (this, arguments); })();