]> source.dussan.org Git - tigervnc.git/commitdiff
Add emulated middle mouse button 942/head
authorAlex Tanskanen <aleta@cendio.com>
Mon, 13 Jan 2020 14:23:20 +0000 (15:23 +0100)
committerAlex Tanskanen <aleta@cendio.com>
Thu, 16 Jan 2020 08:04:49 +0000 (09:04 +0100)
Not every mouse has three buttons e.g. laptops. Some OS might not
have support for middle mouse button emulation.

This commit adds emulation for middle mouse button when pressing both
left and right mouse button simultaneously.

vncviewer/CMakeLists.txt
vncviewer/EmulateMB.cxx [new file with mode: 0644]
vncviewer/EmulateMB.h [new file with mode: 0644]
vncviewer/OptionsDialog.cxx
vncviewer/OptionsDialog.h
vncviewer/Viewport.cxx
vncviewer/Viewport.h
vncviewer/parameters.cxx
vncviewer/parameters.h
vncviewer/vncviewer.man

index 3c18646327ed8dcde366ff2cd37e09c7ed4cca61..caf6d7a82cd48d1f28dc5cae674409541a86d5a8 100644 (file)
@@ -6,6 +6,7 @@ set(VNCVIEWER_SOURCES
   menukey.cxx
   CConn.cxx
   DesktopWindow.cxx
+  EmulateMB.cxx
   UserDialog.cxx
   ServerDialog.cxx
   Surface.cxx
diff --git a/vncviewer/EmulateMB.cxx b/vncviewer/EmulateMB.cxx
new file mode 100644 (file)
index 0000000..2233274
--- /dev/null
@@ -0,0 +1,289 @@
+/* Copyright 2020 Alex Tanskanen for Cendio AB
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
+ * USA.
+ */
+
+/*
+ * Based on xf86-input-evdev
+ *
+ * Copyright 1990,91 by Thomas Roell, Dinkelscherben, Germany.
+ * Copyright 1993 by David Dawes <dawes@xfree86.org>
+ * Copyright 2002 by SuSE Linux AG, Author: Egbert Eich
+ * Copyright 1994-2002 by The XFree86 Project, Inc.
+ * Copyright 2002 by Paul Elliott
+ * (Ported from xf86-input-mouse, above copyrights taken from there)
+ * Copyright © 2008 University of South Australia
+ *
+ * Permission to use, copy, modify, distribute, and sell this software
+ * and its documentation for any purpose is hereby granted without
+ * fee, provided that the above copyright notice appear in all copies
+ * and that both that copyright notice and this permission notice
+ * appear in supporting documentation, and that the name of the authors
+ * not be used in advertising or publicity pertaining to distribution of the
+ * software without specific, written prior permission.  The authors make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+ * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
+ * NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+ * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+ * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <assert.h>
+
+#include <rfb/Exception.h>
+
+#include "parameters.h"
+#include "i18n.h"
+#include "EmulateMB.h"
+
+/*
+ * Lets create a simple finite-state machine for 3 button emulation:
+ *
+ * We track buttons 1 and 3 (left and right).  There are 11 states:
+ *   0 ground           - initial state
+ *   1 delayed left     - left pressed, waiting for right
+ *   2 delayed right    - right pressed, waiting for left
+ *   3 pressed middle   - right and left pressed, emulated middle sent
+ *   4 pressed left     - left pressed and sent
+ *   5 pressed right    - right pressed and sent
+ *   6 released left    - left released after emulated middle
+ *   7 released right   - right released after emulated middle
+ *   8 repressed left   - left pressed after released left
+ *   9 repressed right  - right pressed after released right
+ *  10 pressed both     - both pressed, not emulating middle
+ *
+ * At each state, we need handlers for the following events
+ *   0: no buttons down
+ *   1: left button down
+ *   2: right button down
+ *   3: both buttons down
+ *   4: emulate3Timeout passed without a button change
+ * Note that button events are not deltas, they are the set of buttons being
+ * pressed now.  It's possible (ie, mouse hardware does it) to go from (eg)
+ * left down to right down without anything in between, so all cases must be
+ * handled.
+ *
+ * a handler consists of three values:
+ *   0: action1
+ *   1: action2
+ *   2: new emulation state
+ *
+ * action > 0: ButtonPress
+ * action = 0: nothing
+ * action < 0: ButtonRelease
+ *
+ * The comment preceeding each section is the current emulation state.
+ * The comments to the right are of the form
+ *      <button state> (<events>) -> <new emulation state>
+ * which should be read as
+ *      If the buttons are in <button state>, generate <events> then go to
+ *      <new emulation state>.
+ */
+static const signed char stateTab[11][5][3] = {
+/* 0 ground */
+  {
+    {  0,  0,  0 },   /* nothing -> ground (no change) */
+    {  0,  0,  1 },   /* left -> delayed left */
+    {  0,  0,  2 },   /* right -> delayed right */
+    {  2,  0,  3 },   /* left & right (middle press) -> pressed middle */
+    {  0,  0, -1 }    /* timeout N/A */
+  },
+/* 1 delayed left */
+  {
+    {  1, -1,  0 },   /* nothing (left event) -> ground */
+    {  0,  0,  1 },   /* left -> delayed left (no change) */
+    {  1, -1,  2 },   /* right (left event) -> delayed right */
+    {  2,  0,  3 },   /* left & right (middle press) -> pressed middle */
+    {  1,  0,  4 },   /* timeout (left press) -> pressed left */
+  },
+/* 2 delayed right */
+  {
+    {  3, -3,  0 },   /* nothing (right event) -> ground */
+    {  3, -3,  1 },   /* left (right event) -> delayed left (no change) */
+    {  0,  0,  2 },   /* right -> delayed right (no change) */
+    {  2,  0,  3 },   /* left & right (middle press) -> pressed middle */
+    {  3,  0,  5 },   /* timeout (right press) -> pressed right */
+  },
+/* 3 pressed middle */
+  {
+    { -2,  0,  0 },   /* nothing (middle release) -> ground */
+    {  0,  0,  7 },   /* left -> released right */
+    {  0,  0,  6 },   /* right -> released left */
+    {  0,  0,  3 },   /* left & right -> pressed middle (no change) */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 4 pressed left */
+  {
+    { -1,  0,  0 },   /* nothing (left release) -> ground */
+    {  0,  0,  4 },   /* left -> pressed left (no change) */
+    { -1,  0,  2 },   /* right (left release) -> delayed right */
+    {  3,  0, 10 },   /* left & right (right press) -> pressed both */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 5 pressed right */
+  {
+    { -3,  0,  0 },   /* nothing (right release) -> ground */
+    { -3,  0,  1 },   /* left (right release) -> delayed left */
+    {  0,  0,  5 },   /* right -> pressed right (no change) */
+    {  1,  0, 10 },   /* left & right (left press) -> pressed both */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 6 released left */
+  {
+    { -2,  0,  0 },   /* nothing (middle release) -> ground */
+    { -2,  0,  1 },   /* left (middle release) -> delayed left */
+    {  0,  0,  6 },   /* right -> released left (no change) */
+    {  1,  0,  8 },   /* left & right (left press) -> repressed left */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 7 released right */
+  {
+    { -2,  0,  0 },   /* nothing (middle release) -> ground */
+    {  0,  0,  7 },   /* left -> released right (no change) */
+    { -2,  0,  2 },   /* right (middle release) -> delayed right */
+    {  3,  0,  9 },   /* left & right (right press) -> repressed right */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 8 repressed left */
+  {
+    { -2, -1,  0 },   /* nothing (middle release, left release) -> ground */
+    { -2,  0,  4 },   /* left (middle release) -> pressed left */
+    { -1,  0,  6 },   /* right (left release) -> released left */
+    {  0,  0,  8 },   /* left & right -> repressed left (no change) */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 9 repressed right */
+  {
+    { -2, -3,  0 },   /* nothing (middle release, right release) -> ground */
+    { -3,  0,  7 },   /* left (right release) -> released right */
+    { -2,  0,  5 },   /* right (middle release) -> pressed right */
+    {  0,  0,  9 },   /* left & right -> repressed right (no change) */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+/* 10 pressed both */
+  {
+    { -1, -3,  0 },   /* nothing (left release, right release) -> ground */
+    { -3,  0,  4 },   /* left (right release) -> pressed left */
+    { -1,  0,  5 },   /* right (left release) -> pressed right */
+    {  0,  0, 10 },   /* left & right -> pressed both (no change) */
+    {  0,  0, -1 },   /* timeout N/A */
+  },
+};
+
+EmulateMB::EmulateMB()
+  : state(0), emulatedButtonMask(0), timer(this)
+{
+}
+
+void EmulateMB::filterPointerEvent(const rfb::Point& pos, int buttonMask)
+{
+  int btstate;
+  int action1, action2;
+  int lastState;
+
+  if (!emulateMiddleButton) {
+     sendPointerEvent(pos, buttonMask);
+     return;
+  }
+
+  lastButtonMask = buttonMask;
+  lastPos = pos;
+
+  btstate = 0;
+
+  if (buttonMask & 0x1)
+    btstate |= 0x1;
+
+  if (buttonMask & 0x4)
+    btstate |= 0x2;
+
+  if ((state > 10) || (state < 0))
+    throw rfb::Exception(_("Invalid state for 3 button emulation"));
+
+  action1 = stateTab[state][btstate][0];
+  if (action1 != 0)
+    sendAction(pos, buttonMask, action1);
+
+  action2 = stateTab[state][btstate][1];
+  if (action2 != 0)
+    sendAction(pos, buttonMask, action2);
+
+  if ((action1 == 0) && (action2 == 0)) {
+    buttonMask &= ~0x5;
+    buttonMask |= emulatedButtonMask;
+    sendPointerEvent(pos, buttonMask);
+  }
+
+  lastState = state;
+  state = stateTab[state][btstate][2];
+
+  if (lastState != state) {
+    timer.stop();
+
+    if (stateTab[state][4][2] >= 0)
+      timer.start(50);
+  }
+}
+
+bool EmulateMB::handleTimeout(rfb::Timer *t)
+{
+  int action1, action2;
+
+  if (&timer != t)
+    return false;
+
+  if ((state > 10) || (state < 0))
+    throw rfb::Exception(_("Invalid state for 3 button emulation"));
+
+  assert(stateTab[state][4][2] >= 0);
+
+  action1 = stateTab[state][4][0];
+  if (action1 != 0)
+    sendAction(lastPos, lastButtonMask, action1);
+
+  action2 = stateTab[state][4][1];
+  if (action2 != 0)
+    sendAction(lastPos, lastButtonMask, action2);
+
+  state = stateTab[state][4][2];
+
+  return false;
+}
+
+void EmulateMB::sendAction(const rfb::Point& pos, int buttonMask, int action)
+{
+  assert(action != 0);
+
+  if (action < 0)
+    emulatedButtonMask &= ~(1 << ((-action) - 1));
+  else
+    emulatedButtonMask |= (1 << (action - 1));
+
+  buttonMask &= ~0x5;
+  buttonMask |= emulatedButtonMask;
+  sendPointerEvent(pos, buttonMask);
+}
diff --git a/vncviewer/EmulateMB.h b/vncviewer/EmulateMB.h
new file mode 100644 (file)
index 0000000..e2a70d8
--- /dev/null
@@ -0,0 +1,47 @@
+/* Copyright 2020 Alex Tanskanen for Cendio AB
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
+ * USA.
+ */
+
+#ifndef __EMULATEMB__
+#define __EMULATEMB__
+
+#include <rfb/Timer.h>
+#include <rfb/Rect.h>
+
+class EmulateMB : public rfb::Timer::Callback {
+public:
+  EmulateMB();
+
+  void filterPointerEvent(const rfb::Point& pos, int buttonMask);
+
+protected:
+  virtual void sendPointerEvent(const rfb::Point& pos, int buttonMask)=0;
+
+  virtual bool handleTimeout(rfb::Timer *t);
+
+private:
+  void sendAction(const rfb::Point& pos, int buttonMask, int action);
+
+private:
+  int state;
+  int emulatedButtonMask;
+  int lastButtonMask;
+  rfb::Point lastPos;
+  rfb::Timer timer;
+};
+
+#endif
index ada5b288bea35ac4d092acc589e397eab68423ab..872a49cfeee26c7ea82839785c5dd39ed8d8404f 100644 (file)
@@ -262,6 +262,7 @@ void OptionsDialog::loadOptions(void)
   const char *menuKeyBuf;
 
   viewOnlyCheckbox->value(viewOnly);
+  emulateMBCheckbox->value(emulateMiddleButton);
   acceptClipboardCheckbox->value(acceptClipboard);
 #if !defined(WIN32) && !defined(__APPLE__)
   setPrimaryCheckbox->value(setPrimary);
@@ -375,6 +376,7 @@ void OptionsDialog::storeOptions(void)
 
   /* Input */
   viewOnly.setParam(viewOnlyCheckbox->value());
+  emulateMiddleButton.setParam(emulateMBCheckbox->value());
   acceptClipboard.setParam(acceptClipboardCheckbox->value());
 #if !defined(WIN32) && !defined(__APPLE__)
   setPrimary.setParam(setPrimaryCheckbox->value());
@@ -696,6 +698,12 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th)
                                                   _("View only (ignore mouse and keyboard)")));
   ty += CHECK_HEIGHT + TIGHT_MARGIN;
 
+  emulateMBCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty,
+                                                   CHECK_MIN_WIDTH,
+                                                   CHECK_HEIGHT,
+                                                   _("Emulate middle mouse button")));
+  ty += CHECK_HEIGHT + TIGHT_MARGIN;
+
   acceptClipboardCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty,
                                                          CHECK_MIN_WIDTH,
                                                          CHECK_HEIGHT,
index c1775607fb39283bb07329a0eb0da734f8983b42..fd3b9500f2e47701c9af88ec31a852bc4288e760 100644 (file)
@@ -107,6 +107,7 @@ protected:
 
   /* Input */
   Fl_Check_Button *viewOnlyCheckbox;
+  Fl_Check_Button *emulateMBCheckbox;
   Fl_Check_Button *acceptClipboardCheckbox;
 #if !defined(WIN32) && !defined(__APPLE__)
   Fl_Check_Button *setPrimaryCheckbox;
index 2a40d15f4211331accc046349a3455d1945f0bd2..b0b2a15abf67c006bbc9098be854d96e312b39c5 100644 (file)
@@ -660,6 +660,26 @@ int Viewport::handle(int event)
   return Fl_Widget::handle(event);
 }
 
+void Viewport::sendPointerEvent(const rfb::Point& pos, int buttonMask)
+{
+  if (viewOnly)
+      return;
+
+  if ((pointerEventInterval == 0) || (buttonMask != lastButtonMask)) {
+    try {
+      cc->writer()->writePointerEvent(pos, buttonMask);
+    } catch (rdr::Exception& e) {
+      vlog.error("%s", e.str());
+      exit_vncviewer(e.str());
+    }
+  } else {
+    if (!Fl::has_timeout(handlePointerTimeout, this))
+      Fl::add_timeout((double)pointerEventInterval/1000.0,
+                      handlePointerTimeout, this);
+  }
+  lastPointerPos = pos;
+  lastButtonMask = buttonMask;
+}
 
 bool Viewport::hasFocus()
 {
@@ -780,22 +800,7 @@ void Viewport::flushPendingClipboard()
 
 void Viewport::handlePointerEvent(const rfb::Point& pos, int buttonMask)
 {
-  if (!viewOnly) {
-    if (pointerEventInterval == 0 || buttonMask != lastButtonMask) {
-      try {
-        cc->writer()->writePointerEvent(pos, buttonMask);
-      } catch (rdr::Exception& e) {
-        vlog.error("%s", e.str());
-        exit_vncviewer(e.str());
-      }
-    } else {
-      if (!Fl::has_timeout(handlePointerTimeout, this))
-        Fl::add_timeout((double)pointerEventInterval/1000.0,
-                        handlePointerTimeout, this);
-    }
-    lastPointerPos = pos;
-    lastButtonMask = buttonMask;
-  }
+  filterPointerEvent(pos, buttonMask);
 }
 
 
index 1fb93c668b3cbbdd8855d7d97325e84ac22a8c2a..306beee9726ffc4f87a90b2365b5dc5e5356510d 100644 (file)
@@ -26,6 +26,8 @@
 
 #include <FL/Fl_Widget.H>
 
+#include "EmulateMB.h"
+
 class Fl_Menu_Button;
 class Fl_RGB_Image;
 
@@ -33,7 +35,7 @@ class CConn;
 class PlatformPixelBuffer;
 class Surface;
 
-class Viewport : public Fl_Widget {
+class Viewport : public Fl_Widget, public EmulateMB {
 public:
 
   Viewport(int w, int h, const rfb::PixelFormat& serverPF, CConn* cc_);
@@ -67,6 +69,9 @@ public:
 
   int handle(int event);
 
+protected:
+  virtual void sendPointerEvent(const rfb::Point& pos, int buttonMask);
+
 private:
   bool hasFocus();
 
index 0cf5436e1f31285ac3d8b7fd396f1269bc0ae279..bb8e47fa9d8e78f1bada15c60c7b734a318562e2 100644 (file)
@@ -55,6 +55,10 @@ static LogWriter vlog("Parameters");
 IntParameter pointerEventInterval("PointerEventInterval",
                                   "Time in milliseconds to rate-limit"
                                   " successive pointer events", 17);
+BoolParameter emulateMiddleButton("EmulateMiddleButton",
+                                  "Emulate middle mouse button by pressing "
+                                  "left and right mouse buttons simultaneously",
+                                  false);
 BoolParameter dotWhenNoCursor("DotWhenNoCursor",
                               "Show the dot cursor when the server sends an "
                               "invisible cursor", false);
@@ -158,6 +162,7 @@ static VoidParameter* parameterArray[] = {
   &CSecurityTLS::X509CRL,
 #endif // HAVE_GNUTLS
   &SecurityClient::secTypes,
+  &emulateMiddleButton,
   &dotWhenNoCursor,
   &autoSelect,
   &fullColour,
index 2ccd9287cc40450e14c72f93cd8556dd8031d807..94aec9fa9f2c34d7266dac20494e335b9ce8208c 100644 (file)
@@ -23,6 +23,7 @@
 #include <rfb/Configuration.h>
 
 extern rfb::IntParameter pointerEventInterval;
+extern rfb::BoolParameter emulateMiddleButton;
 extern rfb::BoolParameter dotWhenNoCursor;
 
 extern rfb::StringParameter passwordFile;
index 5b49f9bd719a2ccacc826525c5fffc886501ee43..9b652a625e8f1c8cd4afe8f6fb03b185e1abec7f 100644 (file)
@@ -275,6 +275,11 @@ Time in milliseconds to rate-limit successive pointer events. Default is
 17 ms (60 Hz).
 .
 .TP
+.B \-EmulateMiddleButton
+Emulate middle mouse button by pressing left and right mouse buttons
+simultaneously. Default is off.
+.
+.TP
 .B \-Log \fIlogname\fP:\fIdest\fP:\fIlevel\fP
 Configures the debug log settings.  \fIdest\fP can currently be \fBstderr\fP or
 \fBstdout\fP, and \fIlevel\fP is between 0 and 100, 100 meaning most verbose