Merge pull request #987 from Carreau/notebook-tooltip

Add function call tooltips to html notebook.  

On open parens, if the user either pauses a bit or hits <tab>, a tooltip appears, with buttons to expand it further (and automatic scroll bars), open the help in the full pager and close.
Fernando Perez 14 years ago
commit 4bd904c7ee

@ -115,6 +115,18 @@ span.section_row_buttons a {
float: right;
}
#timebeforetooltip_span {
float: right;
}
#tooltipontab_span {
float: right;
}
#smartcompleter_span {
float: right;
}
.checkbox_label {
font-size: 85%;
float: right;
@ -321,7 +333,7 @@ div.text_cell_render {
.ansigrey {color: grey;}
.ansibold {font-weight: bold;}
.completions {
.completions , .tooltip{
position: absolute;
z-index: 10;
overflow: auto;
@ -337,6 +349,63 @@ div.text_cell_render {
font-family: monospace;
}
@-moz-keyframes fadeIn {
from {opacity:0;}
to {opacity:1;}
}
@-webkit-keyframes fadeIn {
from {opacity:0;}
to {opacity:1;}
}
@keyframes fadeIn {
from {opacity:0;}
to {opacity:1;}
}
/*"close" "expand" and "Open in pager button" of
/* the tooltip*/
.tooltip a{
float:right;
}
/*properties of tooltip after "expand"*/
.bigtooltip{
height:60%;
}
/*properties of tooltip before "expand"*/
.smalltooltip{
text-overflow: ellipsis;
overflow: hidden;
height:15%;
}
.tooltip{
/*transition when "expand"ing tooltip */
-webkit-transition-property: height;
-webkit-transition-duration: 1s;
-moz-transition-property: height;
-moz-transition-duration: 1s;
transition-property: height;
transition-duration: 1s;
max-width:700px;
border-radius: 0px 10px 10px 10px;
box-shadow: 3px 3px 5px #999;
/*fade-in animation when inserted*/
-webkit-animation: fadeIn 200ms;
-moz-animation: fadeIn 200ms;
animation: fadeIn 200ms;
vertical-align: middle;
background: #FDFDD8;
outline: none;
padding: 3px;
margin: 0px;
font-family: monospace;
min-height:50px;
}
@media print {
body { overflow: visible !important; }
.ui-widget-content { border: 0px; }

@ -85,7 +85,6 @@ var IPython = (function (IPython) {
}
};
// Subclasses must implement create_element.
Cell.prototype.create_element = function () {};

@ -47,24 +47,60 @@ var IPython = (function (IPython) {
this.collapse()
};
//TODO, try to diminish the number of parameters.
CodeCell.prototype.request_tooltip_after_time = function (pre_cursor,time,that){
if (pre_cursor === "" || pre_cursor === "(" ) {
// don't do anything if line beggin with '(' or is empty
} else {
// Will set a timer to request tooltip in `time`
that.tooltip_timeout = setTimeout(function(){
IPython.notebook.request_tool_tip(that, pre_cursor)
},time);
}
};
CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
// This method gets called in CodeMirror's onKeyDown/onKeyPress
// handlers and is used to provide custom key handling. Its return
// value is used to determine if CodeMirror should ignore the event:
// true = ignore, false = don't ignore.
// note that we are comparing and setting the time to wait at each key press.
// a better wqy might be to generate a new function on each time change and
// assign it to CodeCell.prototype.request_tooltip_after_time
tooltip_wait_time = this.notebook.time_before_tooltip;
tooltip_on_tab = this.notebook.tooltip_on_tab;
var that = this;
// whatever key is pressed, first, cancel the tooltip request before
// they are sent, and remove tooltip if any
if(event.type === 'keydown' && this.tooltip_timeout != null){
CodeCell.prototype.remove_and_cancell_tooltip(that.tooltip_timeout);
that.tooltip_timeout=null;
}
if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey)) {
// Always ignore shift-enter in CodeMirror as we handle it.
return true;
}else if (event.which === 40 && event.type === 'keypress' && tooltip_wait_time >= 0) {
// triger aon keypress (!) otherwise inconsistent event.which depending on plateform
// browser and keyboard layout !
// Pressing '(' , request tooltip, don't forget to reappend it
var cursor = editor.getCursor();
var pre_cursor = editor.getRange({line:cursor.line,ch:0},cursor).trim()+'(';
CodeCell.prototype.request_tooltip_after_time(pre_cursor,tooltip_wait_time,that);
} else if (event.keyCode === 9 && event.type == 'keydown') {
// Tab completion.
var cur = editor.getCursor();
var pre_cursor = editor.getRange({line:cur.line,ch:0},cur).trim();
if (pre_cursor === "") {
//Do not trim here because of tooltip
var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
if (pre_cursor.trim() === "") {
// Don't autocomplete if the part of the line before the cursor
// is empty. In this case, let CodeMirror handle indentation.
return false;
} else if ((pre_cursor.substr(-1) === "("|| pre_cursor.substr(-1) === " ") && tooltip_on_tab ) {
CodeCell.prototype.request_tooltip_after_time(pre_cursor,0,that);
} else {
pre_cursor.trim();
// Autocomplete the current line.
event.stop();
var line = editor.getLine(cur.line);
@ -108,11 +144,130 @@ var IPython = (function (IPython) {
};
};
CodeCell.prototype.remove_and_cancell_tooltip = function(timeout)
{
// note that we don't handle closing directly inside the calltip
// as in the completer, because it is not focusable, so won't
// get the event.
clearTimeout(timeout);
$('#tooltip').remove();
}
CodeCell.prototype.finish_tooltip = function (reply) {
defstring=reply.definition;
docstring=reply.docstring;
if(docstring == null){docstring="<empty docstring>"};
name=reply.name;
var that = this;
var tooltip = $('<div/>').attr('id', 'tooltip').addClass('tooltip');
// remove to have the tooltip not Limited in X and Y
tooltip.addClass('smalltooltip');
var pre=$('<pre/>').html(utils.fixConsole(docstring));
var expandlink=$('<a/>').attr('href',"#");
expandlink.addClass("ui-corner-all"); //rounded corner
expandlink.attr('role',"button");
//expandlink.addClass('ui-button');
//expandlink.addClass('ui-state-default');
var expandspan=$('<span/>').text('Expand');
expandspan.addClass('ui-icon');
expandspan.addClass('ui-icon-plus');
expandlink.append(expandspan);
expandlink.attr('id','expanbutton');
expandlink.click(function(){
tooltip.removeClass('smalltooltip');
tooltip.addClass('bigtooltip');
$('#expanbutton').remove();
setTimeout(function(){that.code_mirror.focus();}, 50);
});
var morelink=$('<a/>').attr('href',"#");
morelink.attr('role',"button");
morelink.addClass('ui-button');
//morelink.addClass("ui-corner-all"); //rounded corner
//morelink.addClass('ui-state-default');
var morespan=$('<span/>').text('Open in Pager');
morespan.addClass('ui-icon');
morespan.addClass('ui-icon-arrowstop-l-n');
morelink.append(morespan);
morelink.click(function(){
var msg_id = IPython.notebook.kernel.execute(name+"?");
IPython.notebook.msg_cell_map[msg_id] = IPython.notebook.selected_cell().cell_id;
CodeCell.prototype.remove_and_cancell_tooltip(that.tooltip_timeout);
setTimeout(function(){that.code_mirror.focus();}, 50);
});
var closelink=$('<a/>').attr('href',"#");
closelink.attr('role',"button");
closelink.addClass('ui-button');
//closelink.addClass("ui-corner-all"); //rounded corner
//closelink.adClass('ui-state-default'); // grey background and blue cross
var closespan=$('<span/>').text('Close');
closespan.addClass('ui-icon');
closespan.addClass('ui-icon-close');
closelink.append(closespan);
closelink.click(function(){
CodeCell.prototype.remove_and_cancell_tooltip(that.tooltip_timeout);
setTimeout(function(){that.code_mirror.focus();}, 50);
});
//construct the tooltip
tooltip.append(closelink);
tooltip.append(expandlink);
tooltip.append(morelink);
if(defstring){
defstring_html= $('<pre/>').html(utils.fixConsole(defstring));
tooltip.append(defstring_html);
}
tooltip.append(pre);
var pos = this.code_mirror.cursorCoords();
tooltip.css('left',pos.x+'px');
tooltip.css('top',pos.yBot+'px');
$('body').append(tooltip);
// issues with cross-closing if multiple tooltip in less than 5sec
// keep it comented for now
// setTimeout(CodeCell.prototype.remove_and_cancell_tooltip, 5000);
};
CodeCell.prototype.finish_completing = function (matched_text, matches) {
// console.log("Got matches", matched_text, matches);
var newm = new Array();
if(this.notebook.smart_completer)
{
kwargs = new Array();
other = new Array();
for(var i=0;i<matches.length; ++i){
if(matches[i].substr(-1) === '='){
kwargs.push(matches[i]);
}else{other.push(matches[i]);}
}
newm = kwargs.concat(other);
matches=newm;
}
if (!this.is_completing || matches.length === 0) {return;}
//try to check if the user is typing tab at least twice after a word
// and completion is "done"
fallback_on_tooltip_after=2
if(matches.length==1 && matched_text === matches[0])
{
if(this.npressed >fallback_on_tooltip_after && this.prevmatch==matched_text)
{
console.log('Ok, you really want to complete after pressing tab '+this.npressed+' times !');
console.log('You should understand that there is no (more) completion for that !');
console.log("I'll show you the tooltip, will you stop bothering me ?");
this.request_tooltip_after_time(matched_text+'(',0,this);
return;
}
this.prevmatch=matched_text
this.npressed=this.npressed+1;
}
else
{
this.prevmatch="";
this.npressed=0;
}
var that = this;
var cur = this.completion_cursor;

@ -170,6 +170,19 @@ var IPython = (function (IPython) {
};
};
Kernel.prototype.object_info_request = function (objname) {
if(typeof(objname)!=null)
{
var content = {
oname : objname.toString(),
};
var msg = this.get_msg("object_info_request", content);
this.shell_channel.send(JSON.stringify(msg));
return msg.header.msg_id;
}
return;
}
Kernel.prototype.execute = function (code) {
var content = {
code : code,

@ -27,6 +27,9 @@ var IPython = (function (IPython) {
this.style();
this.create_elements();
this.bind_events();
this.set_tooltipontab(true);
this.set_smartcompleter(true);
this.set_timebeforetooltip(1200);
};
@ -621,6 +624,21 @@ var IPython = (function (IPython) {
};
Notebook.prototype.set_timebeforetooltip = function (time) {
console.log("change time before tooltip to : "+time);
this.time_before_tooltip = time;
};
Notebook.prototype.set_tooltipontab = function (state) {
console.log("change tooltip on tab to : "+state);
this.tooltip_on_tab = state;
};
Notebook.prototype.set_smartcompleter = function (state) {
console.log("Smart completion (kwargs first) changed to to : "+state);
this.smart_completer = state;
};
Notebook.prototype.set_autoindent = function (state) {
var cells = this.cells();
len = cells.length;
@ -700,9 +718,22 @@ var IPython = (function (IPython) {
this.dirty = true;
} else if (msg_type === "complete_reply") {
cell.finish_completing(content.matched_text, content.matches);
};
var payload = content.payload || [];
this.handle_payload(cell, payload);
} else if (msg_type === "object_info_reply"){
//console.log('back from object_info_request : ')
rep = reply.content;
if(rep.found)
{
cell.finish_tooltip(rep);
}
} else {
//console.log("unknown reply:"+msg_type);
}
// when having a rely from object_info_reply,
// no payload so no nned to handle it
if(typeof(content.payload)!='undefined') {
var payload = content.payload || [];
this.handle_payload(cell, payload);
}
};
@ -868,6 +899,30 @@ var IPython = (function (IPython) {
};
Notebook.prototype.request_tool_tip = function (cell,func) {
// Feel free to shorten this logic if you are better
// than me in regEx
// basicaly you shoul be able to get xxx.xxx.xxx from
// something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
// remove everything between matchin bracket (need to iterate)
matchBracket = /\([^\(\)]+\)/g;
oldfunc = func;
func = func.replace(matchBracket,"");
while( oldfunc != func )
{
oldfunc = func;
func = func.replace(matchBracket,"");
}
// remove everythin after last open bracket
endBracket = /\([^\(]*$/g;
func = func.replace(endBracket,"");
var re = /[a-zA-Z._]+$/g;
var msg_id = this.kernel.object_info_request(re.exec(func));
if(typeof(msg_id)!='undefined'){
this.msg_cell_map[msg_id] = cell.cell_id;
}
};
Notebook.prototype.complete_cell = function (cell, line, cursor_pos) {
var msg_id = this.kernel.complete(line, cursor_pos);
this.msg_cell_map[msg_id] = cell.cell_id;

@ -51,6 +51,7 @@ $(document).ready(function () {
IPython.quick_help.element.addClass('hidden'); // shortcuts are disabled in read_only
$('button#new_notebook').addClass('hidden');
$('div#cell_section').addClass('hidden');
$('div#config_section').addClass('hidden');
$('div#kernel_section').addClass('hidden');
$('span#login_widget').removeClass('hidden');
// left panel starts collapsed, but the collapse must happen after

@ -251,6 +251,32 @@
</div>
</div>
<div id="config_section">
<div class="section_header">
<h3>Config</h3>
</div>
<div class="section_content">
<div class="section_row">
<span id="tooltipontab_span">
<input type="checkbox" id="tooltipontab" checked="true"></input>
</span>
<span class="checkbox_label" id="tooltipontab_label">Tooltip on tab:</span>
</div>
<div class="section_row">
<span id="smartcompleter_span">
<input type="checkbox" id="smartcompleter" checked="true"></input>
</span>
<span class="checkbox_label" id="smartcompleter_label">Smart completer:</span>
</div>
<div class="section_row">
<span id="timebeforetooltip_span">
<input type="text" id="timebeforetooltip" value="1200"></input>
</span>
<span class="numeric_input_label" id="timebeforetooltip_label">Time before tooltip : </span>
</div>
</div>
</div>
</div>
<div id="left_panel_splitter"></div>
<div id="notebook_panel">

Loading…
Cancel
Save