Twizy: Drivemode Button Editor

../../../../_images/drivemode-config.png

This plugin has been added to the Twizy code. It’s used here as a more complex example of what can be done by plugins.

It’s an editor for the drivemode buttons the Twizy adds to the dashboard for quick tuning profile changes. The editor allows to change the layout (number and order of buttons) and the profiles to use.

It includes a profile selector built with the dialog widget, that retrieves the available profiles from the OVMS config store.

Install: not necessary if vehicle Twizy is configured (see “Twizy” menu). If you’d like to test this for another vehicle, add as a page plugin e.g. /test/dmconfig.

drivemode-config.htm (hint: right click, save as)

  1<!--
  2  Twizy page plugin: Drivemode Button Configuration
  3  Note: included in firmware v3.2
  4-->
  5
  6<style>
  7.fullscreened .panel-single .panel-body {
  8  padding: 10px;
  9}
 10
 11.btn-group-lg>.btn,
 12.btn-lg {
 13  padding: 10px 3px;
 14  overflow: hidden;
 15}
 16#loadmenu .btn {
 17  font-weight: bold;
 18}
 19.btn-group.btn-group-lg.exchange {
 20  width: 2%;
 21}
 22#editor .btn:hover {
 23  background-color: #e6e6e6;
 24}
 25#editor .btn-group-lg>.btn,
 26#editor .btn-lg {
 27  padding: 10px 0px;
 28  font-size: 15px;
 29}
 30#editor .exchange .btn {
 31  font-weight: bold;
 32}
 33.night #editor .btn {
 34  color: #000;
 35  background: #888;
 36}
 37.night #editor .btn.focus,
 38.night #editor .btn:focus,
 39.night #editor .btn:hover {
 40  background-color: #e0e0e0;
 41}
 42
 43.radio-list {
 44  height: 313px;
 45  overflow-y: auto;
 46  overflow-x: hidden;
 47  padding-right: 15px;
 48}
 49.radio-list .radio {
 50  overflow: hidden;
 51}
 52.radio-list .key {
 53  min-width: 20px;
 54  display: inline-block;
 55  text-align: center;
 56  margin: 0 10px;
 57}
 58.radio-list kbd {
 59  min-width: 60px;
 60  display: inline-block;
 61  text-align: center;
 62  margin: 0 20px 0 10px;
 63}
 64.radio-list .radio label {
 65  width: 100%;
 66  text-align: left;
 67  padding: 8px 30px;
 68}
 69.radio-list .radio label.active {
 70  background-color: #337ab7;
 71  color: #fff;
 72  outline: none;
 73}
 74.radio-list .radio label.active input {
 75  outline: none;
 76}
 77.night .radio-list .radio label:hover {
 78  color: #fff;
 79}
 80</style>
 81
 82<div class="panel panel-primary">
 83  <div class="panel-heading">Drivemode Button Configuration</div>
 84  <div class="panel-body">
 85    <form action="#">
 86
 87    <p id="info">Loading button configuration…</p>
 88
 89    <div id="loadmenu" class="btn-group btn-group-justified"></div>
 90    <div id="editor" class="btn-group btn-group-justified">
 91      <div class="btn-group btn-group-lg add back">
 92        <button type="button" class="btn" title="Add button"></button>
 93      </div>
 94    </div>
 95
 96    <br>
 97    <br>
 98    <div class="text-center">
 99      <button type="button" class="btn btn-default" onclick="reloadpage()">Reset</button>
100      <button type="button" class="btn btn-primary action-save">Save</button>
101    </div>
102
103    </form>
104  </div>
105  <div class="panel-footer">
106    <a class="btn btn-sm btn-default" target="#main" href="/dashboard">Dashboard</a>
107    <a class="btn btn-sm btn-default" target="#main" href="/xrt/drivemode">Drivemode</a>
108  </div>
109</div>
110
111<div id="key-dialog" />
112
113<script>
114(function(){
115
116  var $menu = $('#loadmenu'), $edit = $('#editor'), $back = $edit.find('.back');
117
118  var pbuttons = [0,1,2,3];
119  var plist = [
120    { label: "STD", title: "Standard" },
121    { label: "PWR", title: "Power" },
122    { label: "ECO", title: "Economy" },
123    { label: "ICE", title: "Winter" },
124  ];
125  
126  // load profile list & button config:
127  $('.panel').addClass("loading");
128  var plistloader = loadcmd('config list xrt').then(function(data) {
129    var lines = data.split('\n'), line, i, key;
130    for (i = 0; i < lines.length; i++) {
131      line = lines[i].match(/profile([0-9]{2})\.?([^:]*): (.*)/);
132      if (line && line.length == 4) {
133        key = Number(line[1]);
134        if (key < 1 || key > 99) continue;
135        if (!plist[key]) plist[key] = {};
136        plist[key][line[2]||"profile"] = line[3];
137        continue;
138      }
139      line = lines[i].match(/profile_buttons: (.*)/);
140      if (line && line.length == 2) {
141        try {
142          pbuttons = JSON.parse("[" + line[1] + "]");
143        } catch (e) {
144          console.error(e);
145        }
146      }
147    }
148  });
149
150  // prep key dialog:
151  $('#key-dialog').dialog({
152    show: false,
153    title: "Select Profile",
154    buttons: [{ label: 'Cancel', btnClass: 'default' },{ label: 'Select', btnClass: 'primary' }],
155    onShow: function(input) {
156      var $this = $(this), dlg = $this.data("dialog");
157      $this.addClass("loading");
158      plistloader.then(function(data) {
159        var curkey = dlg.options.key || 0, i, label, title;
160        $plist = $('<div class="radio-list" data-toggle="buttons" />');
161        for (i = 0; i <= Math.min(99, plist.length-1); i++) {
162          if (plist[i] && (i==0 || plist[i].profile)) {
163            label = plist[i].label || "–";
164            title = plist[i].title || (plist[i].profile.substr(0, 10) + "…");
165          } else {
166            label = "–";
167            title = "–new–";
168          }
169          $plist.append('<div class="radio"><label class="btn">' +
170            '<input type="radio" name="key" value="' + i + '"><span class="key">' +
171            ((i==0) ? "STD" : ("#" + ((i<10)?'0':'') + i)) + '</span> <kbd>' +
172            encode_html(label) + '</kbd> ' + encode_html(title) + '</label></div>');
173        }
174        $this.find('.modal-body').html($plist).find('input[value="'+curkey+'"]')
175          .prop("checked", true).parent().addClass("active");
176        $plist
177          .on('dblclick', 'label.btn', function(ev) { $this.dialog('triggerButton', 1); })
178          .on('keypress', function(ev) { if (ev.which==13) $this.dialog('triggerButton', 1); });
179        $this.removeClass("loading");
180      });
181    },
182    onShown: function(input) {
183      $(this).find('.btn.active').focus();
184    },
185    onHide: function(input) {
186      var $this = $(this), dlg = $this.data("dialog");
187      var key = $this.find('input[name="key"]:checked').val();
188      if (key !== undefined && input.button && input.button.index)
189        dlg.options.onAction.call(this, key);
190    },
191  });
192
193  // profile selection:
194  $('#loadmenu').on('click', 'button', function(ev) {
195    var $this = $(this);
196    $('#key-dialog').dialog({
197      show: true,
198      key: $this.val(),
199      onAction: function(key) {
200        var prof = plist[key] || {};
201        $this.val(key);
202        $this.attr("title", prof.title || "");
203        $this.text(prof.label || ("#"+((key<10)?"0":"")+key));
204      },
205    });
206  });
207
208  // create buttons:
209  
210  function addButton(key, front) {
211    var prof = plist[key] || {};
212    if ($menu[0].childElementCount == 0) {
213      $back.before('\
214        <div class="btn-group btn-group-lg add front">\
215          <button type="button" class="btn" title="Add button">✚</button>\
216        </div>\
217        <div class="btn-group btn-group-lg remove">\
218          <button type="button" class="btn" title="Remove button">✖</button>\
219        </div>');
220    } else {
221      $back.before('\
222        <div class="btn-group btn-group-lg exchange">\
223          <button type="button" class="btn" title="Exchange buttons">⇄</button>\
224        </div>\
225        <div class="btn-group btn-group-lg remove">\
226          <button type="button" class="btn" title="Remove button">✖</button>\
227        </div>');
228    }
229    var $btn = $('\
230      <div class="btn-group btn-group-lg">\
231        <button type="button" value="{key}" class="btn btn-default" title="{title}">{label}</button>\
232      </div>'
233      .replace("{key}", key)
234      .replace("{title}", encode_html(prof.title || ""))
235      .replace("{label}", encode_html(prof.label || "#"+((key<10)?"0":"")+key)));
236    if (front)
237      $menu.prepend($btn);
238    else
239      $menu.append($btn);
240  }
241  
242  plistloader.then(function() {
243    var key, prof;
244    for (var i = 0; i < pbuttons.length; i++) {
245      addButton(pbuttons[i]);
246    }
247    $('.panel').removeClass("loading");
248    $('#info').text("Click button to select profile:");
249  });
250
251  // editor buttons:
252  $('#editor').on('click', '.add .btn', function(ev) {
253    addButton(0, $(this).parent().hasClass("front"));
254  }).on('click', '.remove .btn', function(ev) {
255    var $this = $(this), pos = $edit.find('.btn').index(this) >> 1;
256    $($menu.children().get(pos)).detach();
257    if (pos > 0 || $menu[0].childElementCount == 0)
258      $this.parent().prev().detach();
259    else if ($menu[0].childElementCount != 0)
260      $this.parent().next().detach();
261    $this.parent().detach();
262  }).on('click', '.exchange .btn', function(ev) {
263    var pos = $edit.find('.btn').index(this) >> 1;
264    if (pos > 0) $($menu.children().get(pos)).insertBefore($($menu.children().get(pos-1)));
265  });
266
267  // save:
268  $('.action-save').on('click', function(ev) {
269    pbuttons = [];
270    $menu.find('.btn').each(function() { pbuttons.push(this.value) });
271    loadcmd('config set xrt profile_buttons ' + pbuttons.toString())
272      .fail(function(request, textStatus, errorThrown) {
273        confirmdialog("Save Configuration", xhrErrorInfo(request, textStatus, errorThrown), ["Close"]);
274      })
275      .done(function(res) {
276        confirmdialog("Save Configuration", res, ["Close"]);
277      });
278  });
279
280})();
281</script>