Twizy: Drivemode Button Editor
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>