Commit d3acc16e authored by olau@iola.dk's avatar olau@iola.dk

Removed work-around for slow jQuery mousemove events (closing issue

134), add new hooks and new API for interactive plugins, move
crosshair support to plugin


git-svn-id: https://flot.googlecode.com/svn/trunk@159 1e0a6537-2640-0410-bfb7-f154510ff394
parent 1f1866e6
...@@ -204,7 +204,7 @@ specified, the plot will furthermore extend the axis end-point to the ...@@ -204,7 +204,7 @@ specified, the plot will furthermore extend the axis end-point to the
nearest whole tick. The default value is "null" for the x axis and nearest whole tick. The default value is "null" for the x axis and
0.02 for the y axis which seems appropriate for most cases. 0.02 for the y axis which seems appropriate for most cases.
"labelWidth" and "labelHeight" specifies the maximum size of the tick "labelWidth" and "labelHeight" specifies a fixed size of the tick
labels in pixels. They're useful in case you need to align several labels in pixels. They're useful in case you need to align several
plots. plots.
...@@ -662,20 +662,6 @@ A "plotunselected" event with no arguments is emitted when the user ...@@ -662,20 +662,6 @@ A "plotunselected" event with no arguments is emitted when the user
clicks the mouse to remove the selection. clicks the mouse to remove the selection.
Customizing the crosshair
=========================
crosshair: {
mode: null or "x" or "y" or "xy"
color: color
}
You can enable crosshairs, thin lines, that follow the mouse by
setting the mode to one of "x", "y" or "xy". The "x" mode enables a
vertical crosshair that lets you trace the values on the x axis, "y"
enables a horizontal crosshair and "xy" enables them both.
Specifying gradients Specifying gradients
==================== ====================
...@@ -730,19 +716,11 @@ can call: ...@@ -730,19 +716,11 @@ can call:
Clear the selection rectangle. Pass in true to avoid getting a Clear the selection rectangle. Pass in true to avoid getting a
"plotunselected" event. "plotunselected" event.
- getSelection()
- setCrosshair(pos) Returns the current selection in the same format as the
"plotselected" event. If there's currently no selection, it
Set the position of the crosshair. Note that this is cleared if returns null.
the user moves the mouse. "pos" should be on the form { x: xpos,
y: ypos } (or x2 and y2 if you're using the secondary axes), which
is coincidentally the same format as what you get from a "plothover"
event. If "pos" is null, the crosshair is cleared.
- clearCrosshair()
Clear the crosshair.
- highlight(series, datapoint) - highlight(series, datapoint)
...@@ -787,7 +765,28 @@ can call: ...@@ -787,7 +765,28 @@ can call:
- draw() - draw()
Redraws the canvas. Redraws the plot canvas.
- triggerRedrawOverlay()
Schedules an update of an overlay canvas used for drawing
interactive things like the selection and point highlights. This
is mostly useful for writing plugins. The redraw doesn't happen
immediately, instead a timer is set to catch multiple successive
redraws (e.g. from a mousemove).
- width()/height()
Gets the width and height of the plotting area inside the grid.
This is smaller than the canvas or placeholder dimensions as some
extra space is needed (e.g. for labels).
- offset()
Returns the offset of the plotting area inside the grid relative
to the document, useful for instance for calculating mouse
positions (event.pageX/Y minus this offset is the pixel position
inside the plot).
There are also some members that let you peek inside the internal There are also some members that let you peek inside the internal
...@@ -851,7 +850,7 @@ Here's an overview of the phases Flot goes through: ...@@ -851,7 +850,7 @@ Here's an overview of the phases Flot goes through:
1. Plugin initialization, parsing options 1. Plugin initialization, parsing options
2. Constructing the canvas used for drawing 2. Constructing the canvases used for drawing
3. Set data: parsing data specification, calculating colors, 3. Set data: parsing data specification, calculating colors,
copying raw data points into internal format, copying raw data points into internal format,
...@@ -871,7 +870,7 @@ sub-object on the Plot object with the names mentioned below, e.g. ...@@ -871,7 +870,7 @@ sub-object on the Plot object with the names mentioned below, e.g.
var plot = $.plot(...); var plot = $.plot(...);
function f() { alert("hello!")}; function f(plot, series, datapoints) { alert("hello!")};
plot.hooks.processDatapoints.push(f); plot.hooks.processDatapoints.push(f);
...@@ -886,6 +885,7 @@ Currently available hooks (when in doubt, check the Flot source): ...@@ -886,6 +885,7 @@ Currently available hooks (when in doubt, check the Flot source):
Called after Flot has parsed and merged options. Rarely useful, but Called after Flot has parsed and merged options. Rarely useful, but
does allow customizations beyond simple merging of default values. does allow customizations beyond simple merging of default values.
- processRawData [phase 3] - processRawData [phase 3]
function(plot, series, data, datapoints) function(plot, series, data, datapoints)
...@@ -895,6 +895,7 @@ Currently available hooks (when in doubt, check the Flot source): ...@@ -895,6 +895,7 @@ Currently available hooks (when in doubt, check the Flot source):
points and sets datapoints.pointsize to the size of the points, points and sets datapoints.pointsize to the size of the points,
Flot will skip the copying/normalization step for this series. Flot will skip the copying/normalization step for this series.
- processDatapoints [phase 3] - processDatapoints [phase 3]
function(plot, series, datapoints) function(plot, series, datapoints)
...@@ -916,6 +917,51 @@ Currently available hooks (when in doubt, check the Flot source): ...@@ -916,6 +917,51 @@ Currently available hooks (when in doubt, check the Flot source):
doesn't check it or do any normalization on it afterwards. doesn't check it or do any normalization on it afterwards.
- bindEvents [phase 6]
function(plot, eventHolder)
Called after Flot has setup its event handlers. Should set any
necessary event handlers on eventHolder, a jQuery object with the
canvas, e.g.
function (plot, eventHolder) {
eventHolder.mousedown(function (e) {
alert("You pressed the mouse at " + e.pageX + " " + e.pageY);
});
}
Interesting events include click, mousemove, mouseup/down. You can
use all jQuery events. Usually, the event handlers will update the
state by drawing something (add a drawOverlay hook and call
triggerRedrawOverlay) or firing an externally visible event for
user code. See the crosshair plugin for an example.
Currently, eventHolder actually contains both the static canvas
used for the plot itself and the overlay canvas used for
interactive features because some versions of IE get the stacking
order wrong. The hook only gets one event, though (either for the
overlay or for the static canvas).
- drawOverlay [phase 7]
function (plot, canvascontext)
The drawOverlay hook is used for interactive things that need a
canvas to draw on. The model currently used by Flot works the way
that an extra overlay canvas is positioned on top of the static
canvas. This overlay is cleared and then completely redrawn
whenever something interesting happens. This hook is called when
the overlay canvas is to be redrawn.
"canvascontext" is the 2D context of the overlay canvas. You can
use this to draw things. You'll most likely need some of the
metrics computed by Flot, e.g. plot.width()/plot.height(). See the
crosshair plugin for an example.
Plugins Plugins
------- -------
......
...@@ -34,10 +34,6 @@ Changes: ...@@ -34,10 +34,6 @@ Changes:
- A new "plotselecting" event is now emitted while the user is making - A new "plotselecting" event is now emitted while the user is making
a selection. a selection.
- Added a new crosshairs feature for tracing the mouse position on the
axes, enable with crosshair { mode: "x"} (see the new tracking
example for a use).
- The "plothover" event is now emitted immediately instead of at most - The "plothover" event is now emitted immediately instead of at most
10 times per second, you'll have to put in a setTimeout yourself if 10 times per second, you'll have to put in a setTimeout yourself if
you're doing something really expensive on this event. you're doing something really expensive on this event.
...@@ -95,6 +91,10 @@ Changes: ...@@ -95,6 +91,10 @@ Changes:
them summed. This is useful for drawing additive/cumulative graphs them summed. This is useful for drawing additive/cumulative graphs
with bars and (currently unfilled) lines. with bars and (currently unfilled) lines.
- Crosshairs plugin: trace the mouse position on the axes, enable with
crosshair: { mode: "x"} (see the new tracking example for a use).
Bug fixes: Bug fixes:
- Fixed two corner-case bugs when drawing filled curves (report and - Fixed two corner-case bugs when drawing filled curves (report and
......
...@@ -28,7 +28,9 @@ support for VML which excanvas is relying on. It appears that some ...@@ -28,7 +28,9 @@ support for VML which excanvas is relying on. It appears that some
stripped down versions used for test environments on virtual machines stripped down versions used for test environments on virtual machines
lack the VML support. lack the VML support.
Also note that you need at least jQuery 1.2.6. Also note that you need at least jQuery 1.2.6 (but at least 1.3.2 is
recommended for interactive charts because of performance improvements
in event handling).
Basic usage Basic usage
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<!--[if IE]><script language="javascript" type="text/javascript" src="../excanvas.min.js"></script><![endif]--> <!--[if IE]><script language="javascript" type="text/javascript" src="../excanvas.min.js"></script><![endif]-->
<script language="javascript" type="text/javascript" src="../jquery.js"></script> <script language="javascript" type="text/javascript" src="../jquery.js"></script>
<script language="javascript" type="text/javascript" src="../jquery.flot.js"></script> <script language="javascript" type="text/javascript" src="../jquery.flot.js"></script>
<script language="javascript" type="text/javascript" src="../jquery.flot.crosshair.js"></script>
</head> </head>
<body> <body>
<h1>Flot Examples</h1> <h1>Flot Examples</h1>
...@@ -23,6 +24,7 @@ ...@@ -23,6 +24,7 @@
<p id="hoverdata"></p> <p id="hoverdata"></p>
<script id="source" language="javascript" type="text/javascript"> <script id="source" language="javascript" type="text/javascript">
var plot;
$(function () { $(function () {
var sin = [], cos = []; var sin = [], cos = [];
for (var i = 0; i < 14; i += 0.1) { for (var i = 0; i < 14; i += 0.1) {
...@@ -30,7 +32,7 @@ $(function () { ...@@ -30,7 +32,7 @@ $(function () {
cos.push([i, Math.cos(i)]); cos.push([i, Math.cos(i)]);
} }
var plot = $.plot($("#placeholder"), plot = $.plot($("#placeholder"),
[ { data: sin, label: "sin(x) = -0.00"}, [ { data: sin, label: "sin(x) = -0.00"},
{ data: cos, label: "cos(x) = -0.00" } ], { { data: cos, label: "cos(x) = -0.00" } ], {
series: { series: {
......
/*
Flot plugin for showing a crosshair, thin lines, when the mouse hovers
over the plot.
crosshair: {
mode: null or "x" or "y" or "xy"
color: color
}
Set the mode to one of "x", "y" or "xy". The "x" mode enables a
vertical crosshair that lets you trace the values on the x axis, "y"
enables a horizontal crosshair and "xy" enables them both. "color" is
the color of the crosshair (default is "rgba(170, 0, 0, 0.80)")
The plugin also adds two public methods:
- setCrosshair(pos)
Set the position of the crosshair. Note that this is cleared if
the user moves the mouse. "pos" should be on the form { x: xpos,
y: ypos } (or x2 and y2 if you're using the secondary axes), which
is coincidentally the same format as what you get from a "plothover"
event. If "pos" is null, the crosshair is cleared.
- clearCrosshair()
Clear the crosshair.
*/
(function ($) {
var options = {
crosshair: {
mode: null, // one of null, "x", "y" or "xy",
color: "rgba(170, 0, 0, 0.80)"
}
};
function init(plot) {
// position of crosshair in pixels
var crosshair = { x: -1, y: -1 };
plot.setCrosshair = function setCrosshair(pos) {
if (!pos)
crosshair.x = -1;
else {
var axes = plot.getAxes();
crosshair.x = Math.max(0, Math.min(pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plot.width()));
crosshair.y = Math.max(0, Math.min(pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plot.height()));
}
plot.triggerRedrawOverlay();
};
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
plot.hooks.bindEvents.push(function (plot, eventHolder) {
if (!plot.getOptions().crosshair.mode)
return;
eventHolder.mouseout(function () {
if (crosshair.x != -1) {
crosshair.x = -1;
plot.triggerRedrawOverlay();
}
});
eventHolder.mousemove(function (e) {
if (!plot.getSelection()) {
var offset = plot.offset();
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
plot.triggerRedrawOverlay();
}
else
crosshair.x = -1; // hide the crosshair while selecting
});
});
plot.hooks.drawOverlay.push(function (plot, ctx) {
var c = plot.getOptions().crosshair;
if (!c.mode)
return;
var plotOffset = plot.getPlotOffset();
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
if (crosshair.x != -1) {
ctx.strokeStyle = c.color;
ctx.lineWidth = 1;
ctx.lineJoin = "round";
ctx.beginPath();
if (c.mode.indexOf("x") != -1) {
ctx.moveTo(crosshair.x, 0);
ctx.lineTo(crosshair.x, plot.height());
}
if (c.mode.indexOf("y") != -1) {
ctx.moveTo(0, crosshair.y);
ctx.lineTo(plot.width(), crosshair.y);
}
ctx.stroke();
}
ctx.restore();
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'crosshair',
version: '1.0'
});
})(jQuery);
...@@ -98,10 +98,6 @@ ...@@ -98,10 +98,6 @@
selection: { selection: {
mode: null, // one of null, "x", "y" or "xy" mode: null, // one of null, "x", "y" or "xy"
color: "#e8cfac" color: "#e8cfac"
},
crosshair: {
mode: null, // one of null, "x", "y" or "xy",
color: "#aa0000"
} }
}, },
canvas = null, // the canvas for the plot itself canvas = null, // the canvas for the plot itself
...@@ -115,7 +111,9 @@ ...@@ -115,7 +111,9 @@
hooks = { hooks = {
processOptions: [], processOptions: [],
processRawData: [], processRawData: [],
processDatapoints: [] processDatapoints: [],
bindEvents: [],
drawOverlay: []
}, },
plot = this, plot = this,
// dedicated to storing data for buggy standard compliance cases // dedicated to storing data for buggy standard compliance cases
...@@ -127,15 +125,23 @@ ...@@ -127,15 +125,23 @@
plot.draw = draw; plot.draw = draw;
plot.clearSelection = clearSelection; plot.clearSelection = clearSelection;
plot.setSelection = setSelection; plot.setSelection = setSelection;
plot.getSelection = getSelection;
plot.getCanvas = function() { return canvas; }; plot.getCanvas = function() { return canvas; };
plot.getPlotOffset = function() { return plotOffset; }; plot.getPlotOffset = function() { return plotOffset; };
plot.width = function () { return plotWidth; }
plot.height = function () { return plotHeight; }
plot.offset = function () {
var o = eventHolder.offset();
o.left += plotOffset.left;
o.top += plotOffset.top;
return o;
};
plot.getData = function() { return series; }; plot.getData = function() { return series; };
plot.getAxes = function() { return axes; }; plot.getAxes = function() { return axes; };
plot.getOptions = function() { return options; }; plot.getOptions = function() { return options; };
plot.setCrosshair = setCrosshair;
plot.clearCrosshair = function () { setCrosshair(null); };
plot.highlight = highlight; plot.highlight = highlight;
plot.unhighlight = unhighlight; plot.unhighlight = unhighlight;
plot.triggerRedrawOverlay = triggerRedrawOverlay;
// public attributes // public attributes
plot.hooks = hooks; plot.hooks = hooks;
...@@ -494,6 +500,7 @@ ...@@ -494,6 +500,7 @@
// overlay canvas for interactive features // overlay canvas for interactive features
overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(target).get(0); overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(target).get(0);
octx = overlay.getContext("2d"); octx = overlay.getContext("2d");
octx.stroke();
} }
function bindEvents() { function bindEvents() {
...@@ -502,22 +509,17 @@ ...@@ -502,22 +509,17 @@
eventHolder = $([overlay, canvas]); eventHolder = $([overlay, canvas]);
// bind events // bind events
if (options.selection.mode != null || options.crosshair.mode != null if (options.selection.mode != null
|| options.grid.hoverable) { || options.grid.hoverable)
// FIXME: temp. work-around until jQuery bug 4398 is fixed eventHolder.mousemove(onMouseMove);
eventHolder.each(function () {
this.onmousemove = onMouseMove;
});
if (options.selection.mode != null) if (options.selection.mode != null)
eventHolder.mousedown(onMouseDown); eventHolder.mousedown(onMouseDown);
}
if (options.crosshair.mode != null)
eventHolder.mouseout(onMouseOut);
if (options.grid.clickable) if (options.grid.clickable)
eventHolder.click(onClick); eventHolder.click(onClick);
executeHooks(hooks.bindEvents, [eventHolder]);
} }
function setupGrid() { function setupGrid() {
...@@ -1695,8 +1697,9 @@ ...@@ -1695,8 +1697,9 @@
var lastMousePos = { pageX: null, pageY: null }, var lastMousePos = { pageX: null, pageY: null },
selection = { selection = {
first: { x: -1, y: -1}, second: { x: -1, y: -1}, first: { x: -1, y: -1}, second: { x: -1, y: -1},
show: false, active: false }, show: false,
crosshair = { pos: { x: -1, y: -1 } }, active: false
},
highlights = [], highlights = [],
clickIsMouseUp = false, clickIsMouseUp = false,
redrawTimeout = null, redrawTimeout = null,
...@@ -1779,34 +1782,16 @@ ...@@ -1779,34 +1782,16 @@
return null; return null;
} }
function onMouseMove(ev) { function onMouseMove(e) {
// FIXME: temp. work-around until jQuery bug 4398 is fixed
var e = ev || window.event;
if (e.pageX == null && e.clientX != null) {
var de = document.documentElement, b = document.body;
lastMousePos.pageX = e.clientX + (de && de.scrollLeft || b.scrollLeft || 0) - (de.clientLeft || 0);
lastMousePos.pageY = e.clientY + (de && de.scrollTop || b.scrollTop || 0) - (de.clientTop || 0);
}
else {
lastMousePos.pageX = e.pageX; lastMousePos.pageX = e.pageX;
lastMousePos.pageY = e.pageY; lastMousePos.pageY = e.pageY;
}
if (options.grid.hoverable) if (options.grid.hoverable)
triggerClickHoverEvent("plothover", lastMousePos, triggerClickHoverEvent("plothover", lastMousePos,
function (s) { return s["hoverable"] != false; }); function (s) { return s["hoverable"] != false; });
if (options.crosshair.mode != null) {
if (!selection.active) {
setPositionFromEvent(crosshair.pos, lastMousePos);
triggerRedrawOverlay();
}
else
crosshair.pos.x = -1; // hide the crosshair while selecting
}
if (selection.active) { if (selection.active) {
target.trigger("plotselecting", [ selectionIsSane() ? getSelectionForEvent() : null ]); target.trigger("plotselecting", [ getSelection() ]);
updateSelection(lastMousePos); updateSelection(lastMousePos);
} }
...@@ -1836,13 +1821,6 @@ ...@@ -1836,13 +1821,6 @@
$(document).one("mouseup", onSelectionMouseUp); $(document).one("mouseup", onSelectionMouseUp);
} }
function onMouseOut(ev) {
if (options.crosshair.mode != null && crosshair.pos.x != -1) {
crosshair.pos.x = -1;
triggerRedrawOverlay();
}
}
function onClick(e) { function onClick(e) {
if (clickIsMouseUp) { if (clickIsMouseUp) {
clickIsMouseUp = false; clickIsMouseUp = false;
...@@ -1908,13 +1886,13 @@ ...@@ -1908,13 +1886,13 @@
function triggerRedrawOverlay() { function triggerRedrawOverlay() {
if (!redrawTimeout) if (!redrawTimeout)
redrawTimeout = setTimeout(redrawOverlay, 30); redrawTimeout = setTimeout(drawOverlay, 30);
} }
function redrawOverlay() { function drawOverlay() {
redrawTimeout = null; redrawTimeout = null;
// redraw highlights // draw highlights
octx.save(); octx.save();
octx.clearRect(0, 0, canvasWidth, canvasHeight); octx.clearRect(0, 0, canvasWidth, canvasHeight);
octx.translate(plotOffset.left, plotOffset.top); octx.translate(plotOffset.left, plotOffset.top);
...@@ -1929,7 +1907,7 @@ ...@@ -1929,7 +1907,7 @@
drawPointHighlight(hi.series, hi.point); drawPointHighlight(hi.series, hi.point);
} }
// redraw selection // draw selection
if (selection.show && selectionIsSane()) { if (selection.show && selectionIsSane()) {
octx.strokeStyle = parseColor(options.selection.color).scale(null, null, null, 0.8).toString(); octx.strokeStyle = parseColor(options.selection.color).scale(null, null, null, 0.8).toString();
octx.lineWidth = 1; octx.lineWidth = 1;
...@@ -1944,27 +1922,9 @@ ...@@ -1944,27 +1922,9 @@
octx.fillRect(x, y, w, h); octx.fillRect(x, y, w, h);
octx.strokeRect(x, y, w, h); octx.strokeRect(x, y, w, h);
} }
// redraw crosshair
var pos = crosshair.pos, mode = options.crosshair.mode;
if (mode != null && pos.x != -1) {
octx.strokeStyle = parseColor(options.crosshair.color).scale(null, null, null, 0.8).toString();
octx.lineWidth = 1;
ctx.lineJoin = "round";
octx.beginPath();
if (mode.indexOf("x") != -1) {
octx.moveTo(pos.x, 0);
octx.lineTo(pos.x, plotHeight);
}
if (mode.indexOf("y") != -1) {
octx.moveTo(0, pos.y);
octx.lineTo(plotWidth, pos.y);
}
octx.stroke();
}
octx.restore(); octx.restore();
executeHooks(hooks.drawOverlay, [octx]);
} }
function highlight(s, point, auto) { function highlight(s, point, auto) {
...@@ -2039,23 +1999,10 @@ ...@@ -2039,23 +1999,10 @@
0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal); 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal);
} }
function setPositionFromEvent(pos, e) { function getSelection() {
var offset = eventHolder.offset(); if (!selectionIsSane())
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plotWidth); return null;
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plotHeight);
}
function setCrosshair(pos) {
if (pos == null)
crosshair.pos.x = -1;
else {
crosshair.pos.x = clamp(0, pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plotWidth);
crosshair.pos.y = clamp(0, pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plotHeight);
}
triggerRedrawOverlay();
}
function getSelectionForEvent() {
var x1 = Math.min(selection.first.x, selection.second.x), var x1 = Math.min(selection.first.x, selection.second.x),
x2 = Math.max(selection.first.x, selection.second.x), x2 = Math.max(selection.first.x, selection.second.x),
y1 = Math.max(selection.first.y, selection.second.y), y1 = Math.max(selection.first.y, selection.second.y),
...@@ -2074,7 +2021,7 @@ ...@@ -2074,7 +2021,7 @@
} }
function triggerSelectedEvent() { function triggerSelectedEvent() {
var r = getSelectionForEvent(); var r = getSelection();
target.trigger("plotselected", [ r ]); target.trigger("plotselected", [ r ]);
...@@ -2108,7 +2055,9 @@ ...@@ -2108,7 +2055,9 @@
} }
function setSelectionPos(pos, e) { function setSelectionPos(pos, e) {
setPositionFromEvent(pos, e); var offset = eventHolder.offset();
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plotWidth);
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plotHeight);
if (options.selection.mode == "y") { if (options.selection.mode == "y") {
if (pos == selection.first) if (pos == selection.first)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment