diff --git a/IPython/html/static/edit/js/editor.js b/IPython/html/static/edit/js/editor.js index 2f6ef18..dd12ea4 100644 --- a/IPython/html/static/edit/js/editor.js +++ b/IPython/html/static/edit/js/editor.js @@ -25,12 +25,16 @@ function($, var Editor = function(selector, options) { var that = this; this.selector = selector; + this.clean = false; this.contents = options.contents; this.events = options.events; this.base_url = options.base_url; this.file_path = options.file_path; this.config = options.config; this.codemirror = new CodeMirror($(this.selector)[0]); + this.codemirror.on('changes', function(cm, changes){ + that._clean_state(); + }); this.generation = -1; // It appears we have to set commands on the CodeMirror class, not the @@ -49,7 +53,11 @@ function($, ); that._set_codemirror_options(cmopts); that.events.trigger('config_changed.Editor', {config: that.config}); + that._clean_state(); }); + this.clean_sel = $('
'); + $('.last_modified').before(this.clean_sel); + this.clean_sel.addClass('dirty-indicator-dirty'); }; // default CodeMirror options @@ -78,6 +86,7 @@ function($, that.save_enabled = true; that.generation = cm.changeGeneration(); that.events.trigger("file_loaded.Editor", model); + that._clean_state(); }).catch( function(error) { that.events.trigger("file_load_failed.Editor", error); @@ -147,6 +156,7 @@ function($, that.file_path = model.path; that.events.trigger('file_renamed.Editor', model); that._set_mode_for_model(model); + that._clean_state(); } ); }; @@ -169,9 +179,26 @@ function($, that.events.trigger("file_saving.Editor"); return this.contents.save(this.file_path, model).then(function(data) { that.events.trigger("file_saved.Editor", data); + that._clean_state(); }); }; - + + Editor.prototype._clean_state = function(){ + var clean = this.codemirror.isClean(this.generation); + if (clean === this.clean){ + return + } else { + this.clean = clean; + } + if(clean){ + this.events.trigger("save_status_clean.Editor"); + this.clean_sel.attr('class','dirty-indicator-clean').attr('title','No changes to save'); + } else { + this.events.trigger("save_status_dirty.Editor"); + this.clean_sel.attr('class','dirty-indicator-dirty').attr('title','Unsaved changes'); + } + }; + Editor.prototype._set_codemirror_options = function (options) { // update codemirror options from a dict var codemirror = this.codemirror; @@ -181,6 +208,7 @@ function($, } codemirror.setOption(opt, value); }); + var that = this; }; Editor.prototype.update_codemirror_options = function (options) { diff --git a/IPython/html/static/edit/js/savewidget.js b/IPython/html/static/edit/js/savewidget.js index c71cafd..456e42b 100644 --- a/IPython/html/static/edit/js/savewidget.js +++ b/IPython/html/static/edit/js/savewidget.js @@ -17,6 +17,7 @@ define([ this.events = options.events; this.editor = options.editor; this._last_modified = undefined; + this._filename = undefined; this.keyboard_manager = options.keyboard_manager; if (this.selector !== undefined) { this.element = $(selector); @@ -30,6 +31,12 @@ define([ this.element.find('span.filename').click(function () { that.rename(); }); + this.events.on('save_status_clean.Editor', function (evt) { + that.update_document_title(); + }); + this.events.on('save_status_dirty.Editor', function (evt) { + that.update_document_title(undefined, true); + }); this.events.on('file_loaded.Editor', function (evt, model) { that.update_filename(model.name); that.update_document_title(model.name); @@ -104,8 +111,11 @@ define([ this.element.find('span.filename').text(filename); }; - SaveWidget.prototype.update_document_title = function (filename) { - document.title = filename; + SaveWidget.prototype.update_document_title = function (filename, dirty) { + if(filename){ + this._filename = filename; + } + document.title = (dirty?'*':'')+this._filename; }; SaveWidget.prototype.update_address_bar = function (path) { diff --git a/IPython/html/static/edit/less/edit.less b/IPython/html/static/edit/less/edit.less index c136ed4..972da2b 100644 --- a/IPython/html/static/edit/less/edit.less +++ b/IPython/html/static/edit/less/edit.less @@ -1,3 +1,17 @@ +.dirty-indicator{ + .fa(); + width:20px; +} +.dirty-indicator-dirty{ + .dirty-indicator(); +} + +.dirty-indicator-clean{ + .dirty-indicator(); + &:before{ + .icon(@fa-var-check); + } +} #filename { font-size: 16pt; diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 2164b15..47b14e1 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -8902,6 +8902,66 @@ ul#new-menu { header */ margin-bottom: -1px; } +.dirty-indicator { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 20px; +} +.dirty-indicator.pull-left { + margin-right: .3em; +} +.dirty-indicator.pull-right { + margin-left: .3em; +} +.dirty-indicator-dirty { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 20px; +} +.dirty-indicator-dirty.pull-left { + margin-right: .3em; +} +.dirty-indicator-dirty.pull-right { + margin-left: .3em; +} +.dirty-indicator-clean { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 20px; +} +.dirty-indicator-clean.pull-left { + margin-right: .3em; +} +.dirty-indicator-clean.pull-right { + margin-left: .3em; +} +.dirty-indicator-clean:before { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\f00c"; +} +.dirty-indicator-clean:before.pull-left { + margin-right: .3em; +} +.dirty-indicator-clean:before.pull-right { + margin-left: .3em; +} #filename { font-size: 16pt; display: table;