From ae5abcb675cf06c4f018bad45bf34453fd66d618 2015-01-27 10:39:54
From: Bussonnier Matthias <bussonniermatthias@gmail.com>
Date: 2015-01-27 10:39:54
Subject: [PATCH] [editor] mark unsaved changes

Put indicator before time, change title.

And cleanup implementation.

---

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 = $('<div/>');
+        $('.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 90127fd..de62f4a 100644
--- a/IPython/html/static/style/style.min.css
+++ b/IPython/html/static/style/style.min.css
@@ -8870,6 +8870,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;