Repository: TigerVNC/tigervnc (language: cpp)

Issue:
Vncviewer - show mouse symbol rather than dot when no cursor .  [$100]
In vncviewer - there is an option for "show dot when no cursor " , though this is very helpful . 
its  unusable , while using VNC client , remote mouse lags and the dot which creates a perception of speed is pretty invisible . 

I think in vncviewer project : 

adding an option to show imaginary mouse pointer or an option to replace dot with icons similar to mouse cursor will be a good add . 

I know this may not be a valid request , but i plan to propose a bounty and bountysource expects a issue url :) 

https://app.bountysource.com/issues/109362423-vncviewer-show-mouse-symbol-rather-than-dot-when-no-cursor


Bounty of 100 USD - i am ok , even if i can get a working local copy . 

<bountysource-plugin>

---
There is a **[$100 open bounty](https://app.bountysource.com/issues/109362423-vncviewer-show-mouse-symbol-rather-than-dot-when-no-cursor?utm_campaign=plugin&utm_content=tracker%2F3557444&utm_medium=issues&utm_source=github)** on this issue. Add to the bounty at [Bountysource](https://app.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F3557444&utm_medium=issues&utm_source=github).
</bountysource-plugin>


Discussion / hints:
Could you describe your setup a bit more? So we can better understand the circumstances where things aren't working well for you.
I've taken a look at this and I believe to have added the feature requested - a PR will follow soon.

I've also uncovered a bug in the Java version of vncviewer related to cursor rendering, which I will address in a separate PR (and create a separate issue for).
To explain this better i really need to upload a  video . 

Here is my situation , i connect to vnc server using tigervnc . 

I have a latency of around 315 milli seconds between my computer and vnc server . 
What happens is - every time i move mouse , mouse pointer drags slowly . as a developer this gives me a feeling of slow response . 

So currently instead of following the mouse of vnc server (remote host ) , what i do is i follow the dot (dot is local and no network) . Eventually remote cursor (with usual latency follows my dot ) . 

What i was hoping to achieve was : 

- instead of dot , if get either a larger dot or a mouse icon , it would be ease for me to work . 





@krystof1119 @CendioOssman  - Sorry for the delayed response . 


That's the precise scenario that local cursor is supposed to solve. So, it sounds like your server simply doesn't support that. Which server are you using?
No response. Closing.
Hi @CendioOssman 

As @ahmedyo mentioned on a long latency (here >>500ms) link it is is very hard to work with as the mouse is all over and it takes for ages to properly position a mouse (like being connected to a KVM windows session).
I guess the problem is a small dot might not be good to see and a bigger one could help.

But in my case the problem is that there is no local mouse pointer at all, only the remote one.
Here it is TigherVNC 1.13.1 on MacOS 13.4.1.
What is the VNC server in this setup?
This is absolutely something I'd also like to be able to do. 
I just can't make my brain go "pointer isn't mouse, dot is mouse", so I'm having issues with over-shooting my intended destination (by about a dot-width), and having to wait for the server cursor to arrive anyway, or just having to move the mouse slower than I want to because it's not quite following where I think it should be based on my physical movements.
(Gif of TigerVNC with dot, the dot for some reason is freakishly big in the capture, but is normal-sized in the actual screen):
![](https://i.imgur.com/7BeLovJ.gif)

Other viewers like TurboVNC do have a proper pointer-looking image as the 'dot', which I've found feels quite a bit easier to subconsciously use as a kind of promise of "This is where the remote pointer will eventually be, feel free to send a click", but they lack other features that makes Tiger the superior choice.
(Gif example of the TurboVNC local pointer)
![](https://i.imgur.com/5YjLHb2.gif)

I'm not sure whether it's just that the dot is too small (which doesn't come through on the gifs of course) or whether it's a bit of lingering uncertainty over which part of the dot is the 'point', or whether it's because the dot isn't a system-level cursor so even it's lagging slightly behind where the 'actual' cursor is, or it's a mental thing about what I'm used to cursors looking like, or some combination of the above, but the viewer where the client cursor is an actual pointer just feels better.

I wouldn't be against the pointer being a different colour to differentiate it from the remote cursor either, as it is admittedly more difficult to tell at a glance which is the client cursor and which is the server. That's something I can easily fix in the server by changing the windows cursor colour setting though, since I never access it outside of VNC.

Also, to be clear, hiding the cursor on the clientside entirely is something I've tried, but it feels even ickier, even with the relatively slight amount of latency.

Fix patch:
diff --git a/java/com/tigervnc/vncviewer/OptionsDialog.java b/java/com/tigervnc/vncviewer/OptionsDialog.java
index 2fbd9d9c20..881d684811 100644
--- a/java/com/tigervnc/vncviewer/OptionsDialog.java
+++ b/java/com/tigervnc/vncviewer/OptionsDialog.java
@@ -152,9 +152,11 @@ protected void processFocusEvent(final FocusEvent e) {
 
   /* Misc. */
   JCheckBox sharedCheckbox;
-  JCheckBox dotWhenNoCursorCheckbox;
+  JCheckBox alwaysCursorCheckbox;
   JCheckBox acceptBellCheckbox;
 
+  JComboBox cursorTypeChoice;
+
   /* SSH */
   JCheckBox tunnelCheckbox;
   JCheckBox viaCheckbox;
@@ -311,6 +313,7 @@ private void loadOptions()
     handleAutoselect();
     handleCompression();
     handleJpeg();
+    handleAlwaysCursor();
 
     /* Security */
     Security security = new Security(SecurityClient.secTypes);
@@ -458,7 +461,9 @@ private void loadOptions()
 
     /* Misc. */
     sharedCheckbox.setSelected(shared.getValue());
-    dotWhenNoCursorCheckbox.setSelected(dotWhenNoCursor.getValue());
+    alwaysCursorCheckbox.setSelected(alwaysCursor.getValue());
+    String cursorTypeStr = cursorType.getValueStr();
+    cursorTypeChoice.setSelectedItem(cursorTypeStr);
     acceptBellCheckbox.setSelected(acceptBell.getValue());
 
     /* SSH */
@@ -613,8 +618,9 @@ else if (mediumcolorButton.isSelected())
 
     /* Misc. */
     shared.setParam(sharedCheckbox.isSelected());
-    dotWhenNoCursor.setParam(dotWhenNoCursorCheckbox.isSelected());
+    alwaysCursor.setParam(alwaysCursorCheckbox.isSelected());
     acceptBell.setParam(acceptBellCheckbox.isSelected());
+    cursorType.setParam((String)cursorTypeChoice.getSelectedItem());
 
     /* SSH */
     tunnel.setParam(tunnelCheckbox.isSelected());
@@ -1173,31 +1179,54 @@ private JPanel createMiscPanel() {
     MiscPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
     sharedCheckbox =
       new JCheckBox("Shared (don't disconnect other viewers)");
-    dotWhenNoCursorCheckbox = new JCheckBox("Show dot when no cursor");
+    alwaysCursorCheckbox = new JCheckBox("Show local cursor when not provided by server");
+    alwaysCursorCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleAlwaysCursor();
+      }
+    });
+    JLabel cursorTypeLabel = new JLabel("Cursor type:");
+    String[] cursorTypes = {"Dot", "System"};
+    cursorTypeChoice = new MyJComboBox(cursorTypes);
+    cursorTypeChoice.setPrototypeDisplayValue("System.");
     acceptBellCheckbox = new JCheckBox("Beep when requested by the server");
     MiscPanel.add(sharedCheckbox,
                   new GridBagConstraints(0, 0,
-                                         1, 1,
+                                         REMAINDER, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    MiscPanel.add(dotWhenNoCursorCheckbox,
+    MiscPanel.add(alwaysCursorCheckbox,
                   new GridBagConstraints(0, 1,
-                                         1, 1,
+                                         REMAINDER, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    MiscPanel.add(acceptBellCheckbox,
+    MiscPanel.add(cursorTypeLabel,
                   new GridBagConstraints(0, 2,
                                          1, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    MiscPanel.add(Box.createRigidArea(new Dimension(5, 0)),
+    MiscPanel.add(cursorTypeChoice,
+                  new GridBagConstraints(1, 2,
+                                         1, 1,
+                                         LIGHT, LIGHT,
+                                         LINE_START, NONE,
+                                         new Insets(0, 5, 4, 0),
+                                         NONE, NONE));
+    MiscPanel.add(acceptBellCheckbox,
                   new GridBagConstraints(0, 3,
+                                         REMAINDER, 1,
+                                         LIGHT, LIGHT,
+                                         LINE_START, NONE,
+                                         new Insets(0, 0, 4, 0),
+                                         NONE, NONE));
+    MiscPanel.add(Box.createRigidArea(new Dimension(5, 0)),
+                  new GridBagConstraints(0, 4,
                                          REMAINDER, REMAINDER,
                                          HEAVY, HEAVY,
                                          LINE_START, BOTH,
@@ -1633,5 +1662,10 @@ private void handleRfbState()
     }
   }
 
+  private void handleAlwaysCursor()
+  {
+    cursorTypeChoice.setEnabled(alwaysCursorCheckbox.isSelected());
+  }
+
   static LogWriter vlog = new LogWriter("OptionsDialog");
 }
diff --git a/java/com/tigervnc/vncviewer/Parameters.java b/java/com/tigervnc/vncviewer/Parameters.java
index 8c8465cba1..e763837010 100644
--- a/java/com/tigervnc/vncviewer/Parameters.java
+++ b/java/com/tigervnc/vncviewer/Parameters.java
@@ -40,9 +40,19 @@ public class Parameters {
 
   public static BoolParameter dotWhenNoCursor
   = new BoolParameter("DotWhenNoCursor",
-    "Show the dot cursor when the server sends an invisible cursor",
+    "[DEPRECATED] Show the dot cursor when the server sends an invisible cursor",
     false);
 
+  public static BoolParameter alwaysCursor
+  = new BoolParameter("AlwaysCursor",
+    "Show the local cursor when the server sends an invisible cursor",
+    false);
+
+  public static StringParameter cursorType
+  = new StringParameter("CursorType",
+    "Specify which cursor type the local cursor should be. Should be either Dot or System",
+    "Dot");
+
   public static BoolParameter sendLocalUsername
   = new BoolParameter("SendLocalUsername",
     "Send the local username for SecurityTypes "+
@@ -282,7 +292,8 @@ public class Parameters {
     CSecurityTLS.X509CA,
     CSecurityTLS.X509CRL,
     SecurityClient.secTypes,
-    dotWhenNoCursor,
+    alwaysCursor,
+    cursorType,
     autoSelect,
     fullColor,
     lowColorLevel,
@@ -315,6 +326,10 @@ public class Parameters {
     sshKeyFile,
   };
 
+  static VoidParameter[] readOnlyParameterArray = {
+    dotWhenNoCursor
+  };
+
 
   static LogWriter vlog = new LogWriter("Parameters");
 
@@ -447,29 +462,35 @@ public static String loadViewerParameters(String filename) throws Exception {
         invalidParameterName = false;
       } else {
         for (int i = 0; i < parameterArray.length; i++) {
-          if (parameterArray[i] instanceof StringParameter) {
-            if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) {
+          VoidParameter parameter;
+          if (i < parameterArray.length) {
+            parameter = parameterArray[i];
+          } else {
+            parameter = readOnlyParameterArray[i - parameterArray.length];
+          }
+          if (parameter instanceof StringParameter) {
+            if (line.substring(0,idx).trim().equalsIgnoreCase(parameter.getName())) {
               if (value.length() > 256) {
                 vlog.error(String.format("Failed to read line %d in file %s: %s",
                            lineNr, filepath, "Invalid format or too large value"));
                 continue;
               }
-              ((StringParameter)parameterArray[i]).setParam(value);
+              ((StringParameter)parameter).setParam(value);
               invalidParameterName = false;
             }
-          } else if (parameterArray[i] instanceof IntParameter) {
-            if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) {
-              ((IntParameter)parameterArray[i]).setParam(value);
+          } else if (parameter instanceof IntParameter) {
+            if (line.substring(0,idx).trim().equalsIgnoreCase(parameter.getName())) {
+              ((IntParameter)parameter).setParam(value);
               invalidParameterName = false;
             }
-          } else if (parameterArray[i] instanceof BoolParameter) {
-            if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) {
-              ((BoolParameter)parameterArray[i]).setParam(value);
+          } else if (parameter instanceof BoolParameter) {
+            if (line.substring(0,idx).trim().equalsIgnoreCase(parameter.getName())) {
+              ((BoolParameter)parameter).setParam(value);
               invalidParameterName = false;
             }
           } else {
             vlog.error(String.format("Unknown parameter type for parameter %s",
-                       parameterArray[i].getName()));
+                       parameter.getName()));
 
           }
         }
@@ -517,6 +538,10 @@ public static void saveToReg(String servername) {
       }
     }
 
+    for (int i = 0; i < readOnlyParameterArray.length; i++) {
+      UserPreferences.delete(hKey, readOnlyParameterArray[i].getName());
+    }
+
     UserPreferences.save(hKey);
   }
 
@@ -528,28 +553,34 @@ public static String loadFromReg() {
     if (servername == null)
       servername = "";
 
-    for (int i = 0; i < parameterArray.length; i++) {
-      if (parameterArray[i] instanceof StringParameter) {
-        if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) {
+    for (int i = 0; i < parameterArray.length + readOnlyParameterArray.length; i++) {
+      VoidParameter parameter;
+      if (i < parameterArray.length) {
+        parameter = parameterArray[i];
+      } else {
+        parameter = readOnlyParameterArray[i - parameterArray.length];
+      }
+      if (parameter instanceof StringParameter) {
+        if (UserPreferences.get(hKey, parameter.getName()) != null) {
           String stringValue =
-            UserPreferences.get(hKey, parameterArray[i].getName());
-          ((StringParameter)parameterArray[i]).setParam(stringValue);
+            UserPreferences.get(hKey, parameter.getName());
+          ((StringParameter)parameter).setParam(stringValue);
         }
-      } else if (parameterArray[i] instanceof IntParameter) {
-        if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) {
+      } else if (parameter instanceof IntParameter) {
+        if (UserPreferences.get(hKey, parameter.getName()) != null) {
           int intValue =
-            UserPreferences.getInt(hKey, parameterArray[i].getName());
-          ((IntParameter)parameterArray[i]).setParam(intValue);
+            UserPreferences.getInt(hKey, parameter.getName());
+          ((IntParameter)parameter).setParam(intValue);
         }
-      } else if (parameterArray[i] instanceof BoolParameter) {
-        if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) {
+      } else if (parameter instanceof BoolParameter) {
+        if (UserPreferences.get(hKey, parameter.getName()) != null) {
           boolean booleanValue =
-            UserPreferences.getBool(hKey, parameterArray[i].getName());
-          ((BoolParameter)parameterArray[i]).setParam(booleanValue);
+            UserPreferences.getBool(hKey, parameter.getName());
+          ((BoolParameter)parameter).setParam(booleanValue);
         }
       } else {
         vlog.error(String.format("Unknown parameter type for parameter %s",
-                   parameterArray[i].getName()));
+                   parameter.getName()));
       }
     }
 
diff --git a/java/com/tigervnc/vncviewer/UserPreferences.java b/java/com/tigervnc/vncviewer/UserPreferences.java
index 544774dbd5..44c62343bc 100644
--- a/java/com/tigervnc/vncviewer/UserPreferences.java
+++ b/java/com/tigervnc/vncviewer/UserPreferences.java
@@ -144,6 +144,11 @@ public static void load(String nName) {
     }
   }
 
+  public static void delete(String nName, String key) {
+      Preferences node = root.node(nName);
+      node.remove(key);
+  }
+
   static LogWriter vlog = new LogWriter("UserPreferences");
 }
 
diff --git a/java/com/tigervnc/vncviewer/Viewport.java b/java/com/tigervnc/vncviewer/Viewport.java
index e12ef2a685..d376886910 100644
--- a/java/com/tigervnc/vncviewer/Viewport.java
+++ b/java/com/tigervnc/vncviewer/Viewport.java
@@ -167,8 +167,15 @@ public void setCursor(int width, int height, Point hotspot,
     for (i = 0; i < width*height; i++)
       if (data[i*4 + 3] != 0) break;
 
-    if ((i == width*height) && dotWhenNoCursor.getValue()) {
-      vlog.debug("cursor is empty - using dot");
+    useSystemCursor = false;
+    if ((i == width*height) && alwaysCursor.getValue()) {
+      String cursorTypeStr = cursorType.getValueStr();
+      if (cursorTypeStr.matches("^System$")) {
+        useSystemCursor = true;
+      } else {
+        vlog.debug("cursor is empty - using dot");
+      }
+      // Do this anyway to prevent cursor being null
       cursor = new BufferedImage(5, 5, BufferedImage.TYPE_INT_ARGB_PRE);
       cursor.setRGB(0, 0, 5, 5, dotcursor_xpm, 0, 5);
       cursorHotspot.x = cursorHotspot.y = 3;
@@ -216,7 +223,10 @@ private void setCursor(Image img, int x, int y)
 
     hotspot = new java.awt.Point(x, y);
     softCursor = tk.createCustomCursor(img, hotspot, name);
-    setCursor(softCursor);
+    if (useSystemCursor)
+      setCursor(java.awt.Cursor.getDefaultCursor());
+    else
+      setCursor(softCursor);
   }
 
   public void resize(int x, int y, int w, int h) {
@@ -830,6 +840,7 @@ public DownMap(int capacity) {
   float scaleRatioX, scaleRatioY;
 
   static BufferedImage cursor;
+  static boolean useSystemCursor = false;
   Point cursorHotspot = new Point();
 
 }
diff --git a/java/com/tigervnc/vncviewer/VncViewer.java b/java/com/tigervnc/vncviewer/VncViewer.java
index 067f4eff6f..e4f12fd9d9 100644
--- a/java/com/tigervnc/vncviewer/VncViewer.java
+++ b/java/com/tigervnc/vncviewer/VncViewer.java
@@ -212,6 +212,8 @@ public VncViewer(String[] argv) {
 
     // Check if the server name in reality is a configuration file
     potentiallyLoadConfigurationFile(vncServerName);
+
+    migrateDeprecatedOptions();
   }
 
   public static void usage() {
@@ -449,6 +451,15 @@ public void run() {
     }
   }
 
+  static void migrateDeprecatedOptions() {
+    if (dotWhenNoCursor.getValue()) {
+      vlog.info("DotWhenNoCursor is deprecated, set AlwaysCursor to 1 and CursorType to 'Dot' instead");
+
+      alwaysCursor.setParam(true);
+      cursorType.setParam("Dot");
+    }
+  }
+
   public static CConn cc;
   public static StringParameter config
   = new StringParameter("Config",
diff --git a/vncviewer/OptionsDialog.cxx b/vncviewer/OptionsDialog.cxx
index e04065eced..d6f3970dbf 100644
--- a/vncviewer/OptionsDialog.cxx
+++ b/vncviewer/OptionsDialog.cxx
@@ -352,7 +352,14 @@ void OptionsDialog::loadOptions(void)
   /* Misc. */
   sharedCheckbox->value(shared);
   reconnectCheckbox->value(reconnectOnError);
-  dotCursorCheckbox->value(dotWhenNoCursor);
+  alwaysCursorCheckbox->value(alwaysCursor);
+  if (!strcasecmp(cursorType, "system")) {
+    cursorTypeChoice->value(1);
+  } else {
+    // Default
+    cursorTypeChoice->value(0);
+  }
+  handleAlwaysCursor(alwaysCursorCheckbox, this);
 }
 
 
@@ -486,7 +493,14 @@ void OptionsDialog::storeOptions(void)
   /* Misc. */
   shared.setParam(sharedCheckbox->value());
   reconnectOnError.setParam(reconnectCheckbox->value());
-  dotWhenNoCursor.setParam(dotCursorCheckbox->value());
+  alwaysCursor.setParam(alwaysCursorCheckbox->value());
+
+  if (cursorTypeChoice->value() == 1) {
+    cursorType.setParam("System");
+  } else {
+    // Default
+    cursorType.setParam("Dot");
+  }
 
   std::map<OptionsCallback*, void*>::const_iterator iter;
 
@@ -835,11 +849,21 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th)
                                                      _("Emulate middle mouse button")));
     ty += CHECK_HEIGHT + TIGHT_MARGIN;
 
-    dotCursorCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty,
-                                                    CHECK_MIN_WIDTH,
-                                                    CHECK_HEIGHT,
-                                                    _("Show dot when no cursor")));
+    alwaysCursorCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty,
+                                                       CHECK_MIN_WIDTH,
+                                                       CHECK_HEIGHT,
+                                                       _("Show local cursor when not provided by server")));
+    alwaysCursorCheckbox->callback(handleAlwaysCursor, this);
     ty += CHECK_HEIGHT + TIGHT_MARGIN;
+
+    /* Cursor type */
+    cursorTypeChoice = new Fl_Choice(LBLLEFT(tx, ty, 150, CHOICE_HEIGHT, _("Cursor type")));
+
+    fltk_menu_add(cursorTypeChoice, _("Dot"), 0, nullptr, nullptr, 0);
+    fltk_menu_add(cursorTypeChoice, _("System"), 0, nullptr, nullptr, 0);
+
+    ty += CHOICE_HEIGHT + TIGHT_MARGIN;
+
   }
   ty -= TIGHT_MARGIN;
 
@@ -1181,3 +1205,14 @@ void OptionsDialog::handleScreenConfigTimeout(void *data)
 
     self->monitorArrangement->value(fullScreenSelectedMonitors.getParam());
 }
+
+void OptionsDialog::handleAlwaysCursor(Fl_Widget* /*widget*/, void *data)
+{
+  OptionsDialog *dialog = (OptionsDialog*)data;
+
+  if (dialog->alwaysCursorCheckbox->value()) {
+    dialog->cursorTypeChoice->activate();
+  } else {
+    dialog->cursorTypeChoice->deactivate();
+  }
+}
diff --git a/vncviewer/OptionsDialog.h b/vncviewer/OptionsDialog.h
index f6ca89b1ba..86a1423aa6 100644
--- a/vncviewer/OptionsDialog.h
+++ b/vncviewer/OptionsDialog.h
@@ -60,6 +60,7 @@ class OptionsDialog : public Fl_Window {
   static void handleAutoselect(Fl_Widget *widget, void *data);
   static void handleCompression(Fl_Widget *widget, void *data);
   static void handleJpeg(Fl_Widget *widget, void *data);
+  static void handleAlwaysCursor(Fl_Widget *widget, void *data);
 
   static void handleX509(Fl_Widget *widget, void *data);
   static void handleRSAAES(Fl_Widget *widget, void *data);
@@ -115,7 +116,8 @@ class OptionsDialog : public Fl_Window {
   Fl_Check_Button *viewOnlyCheckbox;
   Fl_Group *mouseGroup;
   Fl_Check_Button *emulateMBCheckbox;
-  Fl_Check_Button *dotCursorCheckbox;
+  Fl_Check_Button *alwaysCursorCheckbox;
+  Fl_Choice *cursorTypeChoice;
   Fl_Group *keyboardGroup;
   Fl_Check_Button *systemKeysCheckbox;
   Fl_Choice *menuKeyChoice;
diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx
index 032fd32217..db1f1a95fe 100644
--- a/vncviewer/Viewport.cxx
+++ b/vncviewer/Viewport.cxx
@@ -122,7 +122,8 @@ Viewport::Viewport(int w, int h, const rfb::PixelFormat& /*serverPF*/, CConn* cc
     altGrArmed(false),
 #endif
     firstLEDState(true), pendingClientClipboard(false),
-    menuCtrlKey(false), menuAltKey(false), cursor(nullptr)
+    menuCtrlKey(false), menuAltKey(false), cursor(nullptr),
+    cursorIsBlank(false)
 {
 #if !defined(WIN32) && !defined(__APPLE__)
   XkbDescPtr xkb;
@@ -259,7 +260,12 @@ void Viewport::setCursor(int width, int height, const Point& hotspot,
   for (i = 0; i < width*height; i++)
     if (data[i*4 + 3] != 0) break;
 
-  if ((i == width*height) && dotWhenNoCursor) {
+  cursorIsBlank = i == width*height;
+
+  if (cursorIsBlank && alwaysCursor) {
+    // This is the default in case the local cursor should be displayed yet cursorType is invalid.
+    // Since the cursor variable isn't used if the cursorType is system, we can do this without checking the current
+    // type which helps handle changing the type while the viewer is running.
     vlog.debug("cursor is empty - using dot");
 
     Fl_Pixmap pxm(dotcursor_xpm);
@@ -280,7 +286,16 @@ void Viewport::setCursor(int width, int height, const Point& hotspot,
   }
 
   if (Fl::belowmouse() == this)
+    showCursor();
+}
+
+void Viewport::showCursor()
+{
+  if (cursorIsBlank && alwaysCursor && !strcasecmp("system", cursorType)) {
+    window()->cursor(FL_CURSOR_DEFAULT);
+  } else {
     window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y);
+  }
 }
 
 void Viewport::handleClipboardRequest()
@@ -583,7 +598,7 @@ int Viewport::handle(int event)
     return 1;
 
   case FL_ENTER:
-    window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y);
+    showCursor();
     // Yes, we would like some pointer events please!
     return 1;
 
@@ -1299,7 +1314,7 @@ void Viewport::popupContextMenu()
 
   // Back to our proper mouse pointer.
   if (Fl::belowmouse())
-    window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y);
+    showCursor();
 
   if (m == nullptr)
     return;
diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h
index d936c70898..934fa73273 100644
--- a/vncviewer/Viewport.h
+++ b/vncviewer/Viewport.h
@@ -77,6 +77,9 @@ class Viewport : public Fl_Widget, public EmulateMB {
 
   unsigned int getModifierMask(unsigned int keysym);
 
+  // Show the currently set (or system) cursor
+  void showCursor();
+
   static void handleClipboardChange(int source, void *data);
 
   void flushPendingClipboard();
@@ -136,6 +139,7 @@ class Viewport : public Fl_Widget, public EmulateMB {
 
   Fl_RGB_Image *cursor;
   rfb::Point cursorHotspot;
+  bool cursorIsBlank;
 };
 
 #endif
diff --git a/vncviewer/parameters.cxx b/vncviewer/parameters.cxx
index a03623db74..42f337f171 100644
--- a/vncviewer/parameters.cxx
+++ b/vncviewer/parameters.cxx
@@ -61,8 +61,15 @@ BoolParameter emulateMiddleButton("EmulateMiddleButton",
                                   "left and right mouse buttons simultaneously",
                                   false);
 BoolParameter dotWhenNoCursor("DotWhenNoCursor",
-                              "Show the dot cursor when the server sends an "
+                              "[DEPRECATED] Show the dot cursor when the server sends an "
                               "invisible cursor", false);
+BoolParameter alwaysCursor("AlwaysCursor",
+                           "Show the local cursor when the server sends an "
+                           "invisible cursor", false);
+StringParameter cursorType("CursorType",
+                           "Specify which cursor type the local cursor should be. "
+                           "Should be either Dot or System",
+                           "Dot");
 
 BoolParameter alertOnFatalError("AlertOnFatalError",
                                 "Give a dialog on connection problems rather "
@@ -198,7 +205,8 @@ static VoidParameter* parameterArray[] = {
   /* Input */
   &viewOnly,
   &emulateMiddleButton,
-  &dotWhenNoCursor,
+  &alwaysCursor,
+  &cursorType,
   &acceptClipboard,
   &sendClipboard,
 #if !defined(WIN32) && !defined(__APPLE__)
@@ -210,7 +218,8 @@ static VoidParameter* parameterArray[] = {
 };
 
 static VoidParameter* readOnlyParameterArray[] = {
-  &fullScreenAllMonitors
+  &fullScreenAllMonitors,
+  &dotWhenNoCursor
 };
 
 // Encoding Table
diff --git a/vncviewer/parameters.h b/vncviewer/parameters.h
index df7bc42059..b161dd9d2c 100644
--- a/vncviewer/parameters.h
+++ b/vncviewer/parameters.h
@@ -33,7 +33,9 @@
 
 extern rfb::IntParameter pointerEventInterval;
 extern rfb::BoolParameter emulateMiddleButton;
-extern rfb::BoolParameter dotWhenNoCursor;
+extern rfb::BoolParameter dotWhenNoCursor; // deprecated
+extern rfb::BoolParameter alwaysCursor;
+extern rfb::StringParameter cursorType;
 
 extern rfb::StringParameter passwordFile;
 
diff --git a/vncviewer/vncviewer.cxx b/vncviewer/vncviewer.cxx
index 27946f3fc5..cc3a247bde 100644
--- a/vncviewer/vncviewer.cxx
+++ b/vncviewer/vncviewer.cxx
@@ -535,6 +535,12 @@ migrateDeprecatedOptions()
 
     fullScreenMode.setParam("all");
   }
+  if (dotWhenNoCursor) {
+    vlog.info(_("DotWhenNoCursor is deprecated, set AlwaysCursor to 1 and CursorType to 'Dot' instead"));
+
+    alwaysCursor.setParam(true);
+    cursorType.setParam("Dot");
+  }
 }
 
 #ifndef WIN32
diff --git a/vncviewer/vncviewer.man b/vncviewer/vncviewer.man
index 1a2821266e..d724ebf9a7 100644
--- a/vncviewer/vncviewer.man
+++ b/vncviewer/vncviewer.man
@@ -285,8 +285,19 @@ Use specified lossless compression level. 0 = Low, 9 = High. Default is 2.
 Use custom compression level. Default if \fBCompressLevel\fP is specified.
 .
 .TP
-.B \-DotWhenNoCursor
-Show the dot cursor when the server sends an invisible cursor. Default is off.
+.B \-DotWhenNoCursor (DEPRECATED)
+Show the dot cursor when the server sends an invisible cursor. Replaced by
+\fB-AlwaysCursor\fP and \fB-CursorType=Dot\fP
+.
+.TP
+.B \-AlwaysCursor
+Show a local cursor when the server sends an invisible cursor. Default is off.
+.
+.TP
+.B \-CursorType \fItype\fP
+Specify which cursor type to use when a local cursor is shown. It should be
+either "Dot", or "System". Ignored if AlwaysCursor is off.
+The default is "Dot".
 .
 .TP
 .B \-PointerEventInterval \fItime\fP


Test patch: