diff --git a/css/forms.css b/css/forms.css index e22bee6..74f604c 100644 --- a/css/forms.css +++ b/css/forms.css @@ -44,6 +44,22 @@ input[type="url"]:focus, input[type="week"]:focus { padding: 0.75em 2em; } +/* icon button */ +.bt.icon { + -webkit-appearance: none; + + width: 32px; + height: 32px; + + border: 0; + padding: 0; + cursor: pointer; + color: white; + + background: center center no-repeat none; + text-indent: -10000em; +} + /* primary button */ .bt.primary { border: 1px solid #e0e0e0; diff --git a/css/piano.css b/css/piano.css index 815b22d..a66a5a2 100644 --- a/css/piano.css +++ b/css/piano.css @@ -258,25 +258,6 @@ a { text-align: center; } -.main .control button { - -webkit-appearance: none; - - width: 32px; - height: 32px; - - border: 0; - cursor: pointer; - color: white; - - background: none; -} - -.main .control .switch-play-state, .main .control .open-channels-window, -.main .control .close { - background: center center no-repeat; - text-indent: -10000em; -} - .main .control > * { display: block; margin: 0 auto 1em auto; @@ -424,6 +405,28 @@ a { background-image: url('../images/icons/instruments/organ.png'); } +/** actions **/ +.vex .channels li button.sound, .vex .channels li button.notes { + width: 16px; + height: 16px; +} + +.vex .channels li button.sound { + background-image: url('../images/icons/instruments/sound-on.png'); +} + +.vex .channels li button.sound.off { + background-image: url('../images/icons/instruments/sound-off.png'); +} + +.vex .channels li button.notes { + background-image: url('../images/icons/instruments/notes-on.png'); +} + +.vex .channels li button.notes.off { + background-image: url('../images/icons/instruments/notes-off.png'); +} + /** * UI Components */ diff --git a/images/icons/instruments/notes-off.png b/images/icons/instruments/notes-off.png new file mode 100644 index 0000000..f232d0c Binary files /dev/null and b/images/icons/instruments/notes-off.png differ diff --git a/images/icons/instruments/notes-on.png b/images/icons/instruments/notes-on.png new file mode 100644 index 0000000..24a4589 Binary files /dev/null and b/images/icons/instruments/notes-on.png differ diff --git a/images/icons/instruments/sound-off.png b/images/icons/instruments/sound-off.png new file mode 100644 index 0000000..6ad4236 Binary files /dev/null and b/images/icons/instruments/sound-off.png differ diff --git a/images/icons/instruments/sound-on.png b/images/icons/instruments/sound-on.png new file mode 100644 index 0000000..3a06010 Binary files /dev/null and b/images/icons/instruments/sound-on.png differ diff --git a/js/components/Channel/Board.js b/js/components/Channel/Board.js index 50ce076..4cd78f3 100644 --- a/js/components/Channel/Board.js +++ b/js/components/Channel/Board.js @@ -7,9 +7,10 @@ /** * Modal for managing channels * - * @prop {channels: Array} List of channels - * @prop {open: func} Called to open the window - * @prop {close: func} Called to close the window + * @prop {channels: Array} List of channels + * @prop {switch: func} Called to switch a param on given channel + * @prop {open: func} Called to open the window + * @prop {close: func} Called to close the window */ App.components.modal('Channel', { displayName: 'Board', @@ -18,6 +19,7 @@ propTypes: { channels: React.PropTypes.array, + switch: React.PropTypes.func, open: React.PropTypes.func, close: React.PropTypes.func }, @@ -26,21 +28,37 @@ return { channels: [], + switch: function () {}, open: function () {}, close: function () {} }; }, + /** + * Creates a function to switch a param on given channel + * + * @param {channel: object} Channel object + */ + switch: function (channel) { + return function (param) { + this.props.switch(param, channel.id); + }.bind(this); + }, + /** * Render modal */ render: function () { var title = 'Instruments', channels; + console.log('rendering!'); + channels = this.props.channels.map(function (channel) { channel.key = 'channel-' + channel.id; + channel.switch = this.switch(channel); + return App.components.Channel.Channel(channel); - }); + }, this); return React.DOM.form({ className: 'vex-dialog-form channels' diff --git a/js/components/Channel/Channel.js b/js/components/Channel/Channel.js index ef11546..6009b21 100644 --- a/js/components/Channel/Channel.js +++ b/js/components/Channel/Channel.js @@ -1,10 +1,17 @@ +/*jshint browser:true */ /*globals React, App */ (function () { 'use strict'; /** - * Display channel + * Display a channel + * + * @prop {id: number} Channel ID + * @prop {program: number} Program number + * @prop {sound: bool} Whether sound should be played or not + * @prop {notes: bool} Whether notes should be displayed or not + * @prop {switch: func} Called to switch a param */ App.components.create('Channel', { displayName: 'Channel', @@ -191,25 +198,38 @@ propTypes: { id: React.PropTypes.number.isRequired, program: React.PropTypes.number.isRequired, - muted: React.PropTypes.bool, - solo: React.PropTypes.bool + sound: React.PropTypes.bool, + notes: React.PropTypes.bool, + + switch: React.PropTypes.func }, getDefaultProps: function () { return { - id: 0, - program: 0, - muted: false, - solo: false + sound: true, + notes: true, + + switch: function () {} }; }, + + /** + * Create a function to switch given parameter + * + * @param {param: string} Parameter name + */ + switcher: function (param) { + return function (e) { + this.props.switch(param); + e.preventDefault(); + }.bind(this); + }, /** * Render channel */ render: function () { - var title = 'Gestion des canaux', type, name, - channel = App.components.Channel.Channel; + var type, name, channel = App.components.Channel.Channel; if (this.props.id === 9) { type = channel.types[2]; @@ -222,7 +242,21 @@ return React.DOM.li({ className: 'channel ' + type, 'data-channel': this.props.id - }, React.DOM.span(null, name)); + }, React.DOM.span(null, [ + name, + React.DOM.button({ + key: 'sound', + className: 'bt icon sound ' + + ((this.props.notes) ? 'on' : 'off'), + onClick: this.switcher('notes') + }, 'Afficher/masquer'), + React.DOM.button({ + key: 'notes', + className: 'bt icon notes ' + + ((this.props.sound) ? 'on' : 'off'), + onClick: this.switcher('sound') + }, 'Jouer/Ne pas jouer') + ])); } }); }()); \ No newline at end of file diff --git a/js/components/Control.js b/js/components/Control.js index 1dacf81..ba66fd4 100644 --- a/js/components/Control.js +++ b/js/components/Control.js @@ -86,20 +86,20 @@ if (this.props.opened) { controls.push(React.DOM.button({ key: 'switchPlayState', - className: (this.props.playing) ? - 'switch-play-state pause' : 'switch-play-state', + className: 'bt icon ' + ((this.props.playing) ? + 'switch-play-state pause' : 'switch-play-state'), onClick: this.switchPlayState }, 'Play/pause')); controls.push(React.DOM.button({ key: 'openChannelsWindow', - className: 'open-channels-window', + className: 'bt icon open-channels-window', onClick: this.props.showChannelsModal }, 'Ouvrir le gestionnaire de canaux')); controls.push(App.components.UI.Selector({ key: 'setPlaySpeed', - className: 'set-play-speed', + className: 'bt icon set-play-speed', values: [1, 0.5, 0.3, 1, 2, 3], value: this.props.speed, @@ -109,14 +109,14 @@ controls.push(React.DOM.button({ key: 'close', - className: 'close', + className: 'bt icon close', onClick: this.props.close })); } else { controls.push(React.DOM.button({ key: 'close', - className: 'close', + className: 'bt icon close', onClick: window.close })); diff --git a/js/components/Main.js b/js/components/Main.js index c5f1e35..40c768d 100644 --- a/js/components/Main.js +++ b/js/components/Main.js @@ -1,4 +1,4 @@ -/*jshint es5: true */ +/*jshint browser:true */ /*globals App, React */ (function () { @@ -294,11 +294,13 @@ if (event.time >= start && event.time < end) { switch (event.type) { case 'noteOn': - this.refs.keyboard.on( - event.note, - event.channel, - event.velocity - ); + if (this.state.meta.channels[event.channel].sound) { + this.refs.keyboard.on( + event.note, + event.channel, + event.velocity + ); + } break; case 'noteOff': this.refs.keyboard.off( @@ -348,6 +350,39 @@ e.preventDefault(); }, + /** + * Display a modal for managing channels + */ + showChannelsModal: function () { + if (this.state.url === '') { + return; + } + + this.channelsModal.open(); + }, + + /** + * Switch given parameter on channel + * + * @param {param: string} Parameter to switch + * @param {channel: number} Channel ID + */ + switchChannelParam: function (param, channel) { + var channels = this.state.meta.channels.slice(0), + length = channels.length, i; + + for (i = 0; i < length; i += 1) { + if (channels[i].id === channel) { + channels[i][param] = !channels[i][param]; + } + } + + this.state.meta.channels = channels; + this.setState({ + meta: this.state.meta + }); + }, + /** * Delegates */ @@ -377,18 +412,6 @@ }); }, - showChannelsModal: function () { - if (this.state.url === '') { - return; - } - - var modal = App.components.Channel.Modal({ - channels: this.state.meta.channels - }); - - modal.open(); - }, - /** * Render panel */ @@ -405,6 +428,7 @@ ref: 'noteboard', notes: this.state.meta.notes, + channels: this.state.meta.channels, playing: this.state.playing, speed: this.state.speed, @@ -444,6 +468,11 @@ onDragEnd: this.resume }); + this.channelsModal = App.components.Channel.Board({ + channels: this.state.meta.channels, + switch: this.switchChannelParam + }); + return React.DOM.div({ className: 'main', onDragOver: this.dragOver, diff --git a/js/components/Note/Board.js b/js/components/Note/Board.js index 472ab0d..4e90929 100644 --- a/js/components/Note/Board.js +++ b/js/components/Note/Board.js @@ -1,3 +1,4 @@ +/*jshint browser:true */ /*globals React, App */ (function () { @@ -7,6 +8,7 @@ * Displays a noteboard from given notes * * @prop {notes: array} Array of notes to show + * @prop {channels: array} Array of configured channels * @prop {time: int} Playback time * @prop {length: int} Playback length * @prop {speed: int} Playback speed @@ -34,7 +36,8 @@ * State and props config */ propTypes: { - notes: React.PropTypes.array.isRequired, + notes: React.PropTypes.array, + channels: React.PropTypes.array, time: React.PropTypes.number, length: React.PropTypes.number, @@ -50,7 +53,7 @@ getDefaultProps: function () { return { notes: [], - + channels: [], playing: false, time: 0, length: 0, @@ -151,6 +154,7 @@ className: 'score', notes: this.props.notes, + channels: this.props.channels, time: this.props.time }); } else { diff --git a/js/components/Note/Score.js b/js/components/Note/Score.js index c5a95df..3237a23 100644 --- a/js/components/Note/Score.js +++ b/js/components/Note/Score.js @@ -1,4 +1,5 @@ -/*globals React, Kinetic, App */ +/*jshint browser:true */ +/*globals React, App */ (function () { 'use strict'; @@ -6,8 +7,9 @@ /** * Display a set of notes * - * @prop {notes: array} List of notes - * @prop {time: int} Current playing time + * @prop {notes: array} List of notes + * @prop {channels: array} List of configured channels + * @prop {time: int} Current playing time */ App.components.create('Note', { displayName: 'Score', @@ -35,12 +37,12 @@ propTypes: { notes: React.PropTypes.array.isRequired, + channels: React.PropTypes.array.isRequired, time: React.PropTypes.number }, getDefaultProps: function () { return { - notes: [], time: 0 }; }, @@ -170,7 +172,8 @@ xUnit = width / 52, yUnit = xUnit * 5, maxTime = Math.ceil(height / yUnit), note, length, i, ctx, offset, x, y, noteWidth, noteHeight, - channels = App.components.Note.Score.channels, count = 0; + colors = App.components.Note.Score.channels, count = 0, + channels = this.props.channels; ctx = this.getDOMNode().getContext('2d'); ctx.clearRect(0, 0, width, height); @@ -183,7 +186,8 @@ note = notes[i]; if (note.start + note.length > time && - note.start < time + maxTime) { + note.start < time + maxTime && + channels[note.channel].notes) { offset = this.getOffset(note.note); count += 1; @@ -201,7 +205,7 @@ noteWidth = Math.floor(xUnit / 2); } - ctx.fillStyle = channels[note.channel]; + ctx.fillStyle = colors[note.channel]; ctx.strokeRect(x, y, noteWidth, noteHeight); ctx.fillRect(x, y, noteWidth, noteHeight); } diff --git a/js/components/UI/Selector.js b/js/components/UI/Selector.js index 69865a6..179d32e 100644 --- a/js/components/UI/Selector.js +++ b/js/components/UI/Selector.js @@ -1,6 +1,6 @@ -/*globals React, App */ +/*globals jQuery, React, App */ -(function () { +(function ($) { 'use strict'; /** @@ -82,10 +82,15 @@ * Render handle */ render: function () { - return React.DOM.button({ - className: 'selector', - onClick: this.click - }, this.state.value); + var props = $.extend({}, this.props); + + delete props.value; + delete props.index; + props.onClick = this.click; + props.className = 'selector ' + + ((props.className) ? props.className : ''); + + return React.DOM.button(props, this.state.value); } }); -}()); \ No newline at end of file +}(jQuery)); \ No newline at end of file diff --git a/js/midi/file.js b/js/midi/file.js index 1818562..cb72bdd 100644 --- a/js/midi/file.js +++ b/js/midi/file.js @@ -28,15 +28,13 @@ * * Represent a channel object */ - function Channel(options) { - options = options || {}; - - this.id = options.id || 0; - this.meta = options.meta || {}; - this.program = options.program || 0; - this.muted = options.muted || false; - this.hidden = options.solo || false; - this.pending = options.pending || {}; + function Channel(id) { + this.id = id; + this.meta = {}; + this.program = 0; + this.sound = true; + this.notes = true; + this.pending = {}; } /** @@ -69,14 +67,13 @@ */ function pushNote(meta, event, time) { var channel = event.channel, - note = event.noteNumber, - data = { - start: time, - channel: channel, - note: note - }; + note = event.noteNumber; - meta.channels[channel].pending[note] = data; + meta.channels[channel].pending[note] = new Note({ + start: time, + channel: channel, + note: note + }); } /** @@ -152,7 +149,7 @@ function parseData(data) { return new window.Promise(function (resolve, reject) { var tracks, events, tracksLength, eventsLength, length, - event, helpLink, i, j, + event, i, j, // parsing data timeline = [], meta = {}, channelPrefix = null, metaObject, @@ -166,14 +163,11 @@ try { data = new MidiFile(data); } catch (err) { - helpLink = 'http://fr.wikipedia.org/wiki/Fichier_midi'; - if (err === 'Bad .mid file - header not found') { reject(new Error( 'Le fichier n\'est pas un fichier MIDI ' + 'valide. Ce logiciel ne prend en charge ' + - 'que les fichiers de type ' + - 'MIDI.' + 'que les fichiers de type MIDI.' )); } else { reject(new Error( @@ -206,9 +200,7 @@ length = 16; for (i = 0; i < length; i += 1) { - meta.channels[i] = new Channel({ - id: i - }); + meta.channels[i] = new Channel(i); } // parse all events