Git commit 40f1d87fd9c86525b3c85da5eb4e75c763e66ab7 by Jasem Mutlaq, on behalf 
of Akarsh Simha.
Committed on 19/03/2024 at 01:15.
Pushed by mutlaqja into branch 'master'.

Introduce Views: A way to quickly reorient the sky-map to match the view 
through an instrument

This merge request improves the ability to orient the sky map to match the view 
through any instrument by introducing "Views"

A view is a collection of settings: the orientation of the sky map, how the 
orientation changes as the sky map is panned, whether it is mirrored or not, 
and optionally the field-of-view to set the map to.

If no views are defined, KStars introduces a set of standard / "demo" views by 
default. Existing views can be edited and new views can be added using the 
"Edit Views..." interface. They can also be re-ordered in the interface. The 
ordering of the views in the "Edit Views..." dialog defines the order in which 
views will be cycled through using the keyboard shortcuts Shift + Page Up and 
Shift + Page Down. Thus, you can set up the views for easily switching between 
naked eye / finder scope / telescope views for easy star-hopping.

M  +51   -7    doc/config.docbook
A  +-    --    doc/newview.png
A  +-    --    doc/viewmanager.png
M  +4    -0    kstars/CMakeLists.txt
M  +9    -1    kstars/auxiliary/dms.cpp
M  +3    -0    kstars/auxiliary/dms.h
M  +110  -10   kstars/auxiliary/ksuserdb.cpp
M  +17   -3    kstars/auxiliary/ksuserdb.h
A  +205  -0    kstars/auxiliary/skymapview.cpp     [License: GPL(v2.0+)]
A  +81   -0    kstars/auxiliary/skymapview.h     [License: GPL(v2.0+)]
M  +1    -0    kstars/data/kstars.qrc
M  +2    -0    kstars/data/kstarsui.rc
A  +-    --    kstars/data/observer.png
A  +399  -0    kstars/dialogs/newview.ui
A  +420  -0    kstars/dialogs/viewsdialog.cpp     [License: GPL(v2.0+)]
A  +104  -0    kstars/dialogs/viewsdialog.h     [License: GPL(v2.0+)]
A  +139  -0    kstars/dialogs/viewsdialog.ui
M  +32   -2    kstars/kstars.cpp
M  +16   -0    kstars/kstars.h
M  +14   -3    kstars/kstars.kcfg
M  +73   -16   kstars/kstarsactions.cpp
M  +66   -7    kstars/kstarsinit.cpp
M  +1    -0    kstars/skycomponents/hipscomponent.cpp
M  +43   -4    kstars/skymap.cpp
M  +3    -0    kstars/skymap.h
M  +18   -4    kstars/skymapevents.cpp
M  +24   -0    kstars/widgets/unitspinboxwidget.cpp
M  +34   -13   kstars/widgets/unitspinboxwidget.h
M  +23   -20   kstars/widgets/unitspinboxwidget.ui

https://invent.kde.org/education/kstars/-/commit/40f1d87fd9c86525b3c85da5eb4e75c763e66ab7

diff --git a/doc/config.docbook b/doc/config.docbook
index c107223f6a..f3e24af35d 100644
--- a/doc/config.docbook
+++ b/doc/config.docbook
@@ -1624,31 +1624,75 @@ from the 
<menuchoice><guimenu>Settings</guimenu><guisubmenu>FOV Symbols</guisubm
     You can tweak various settings to make the orientation of the sky map 
match the view through your optical instrument.
   </para>
   <para>
-    First, pick the coordinate system that matches your mount. For an 
equatorially mounted instrument, switch to the Equatorial Coordinate mode in 
the <guimenu>View</guimenu> menu. The option to toggle the coordinate system 
should read <guilabel>Switch to Horizontal View (Horizontal 
Coordinates)</guilabel> when the current mode is Equatorial Coordinates. For an 
altazimuth-mounted instrument or naked-eye viewing, switch to Horizontal 
Coordinates, so that the option in the <guimenu>View</guimenu> menu reads 
<guilabel>Switch to Star Globe View (Equatorial Coordinates)</guilabel>. This 
sets the base coordinate system used to render the sky map, and also sets the 
reference for the orientation of the skymap: zenith or north.
+    First, pick the coordinate system that matches your mount. For an 
equatorially mounted instrument, switch to the Equatorial Coordinate mode in 
the <guimenu>View</guimenu> menu or by pressing the <keycap>Space</keycap> key. 
The option to toggle the coordinate system should read <guilabel>Switch to 
Horizontal View (Horizontal Coordinates)</guilabel> when the current mode is 
Equatorial Coordinates. For an altazimuth-mounted instrument or naked-eye 
viewing, switch to Horizontal Coordinates, so that the option in the 
<guimenu>View</guimenu> menu reads <guilabel>Switch to Star Globe View 
(Equatorial Coordinates)</guilabel>. This sets the base coordinate system used 
to render the sky map, and also sets the reference for the orientation of the 
skymap: zenith or north.
   </para>
   <para>
-    If your instrument is using an erecting prism, typically used on 
Schmidt-Cassegrain and refracting type telescopes, the view through the 
eyepiece will be mirrored horizontally. You can have the sky map match this by 
checking the <guilabel>Mirrored View</guilabel> option under the 
<guimenu>View</guimenu> menu.
+    If your instrument is using an erecting prism, typically used on 
Schmidt-Cassegrain and refracting type telescopes, the view through the 
eyepiece will be mirrored horizontally. You can have the sky map match this by 
checking the <guilabel>Mirrored View</guilabel> option under the 
<guimenu>View</guimenu> menu, or using the key combination 
<keycombo>&Ctrl;&Shift;<keycap>M</keycap></keycombo>.
   </para>
   <para>
-    Next, to rotate the sky map freely, you can hold down the &Shift; key and 
drag the mouse on the sky map. A temporary overlay will appear showing the 
direction of north and zenith at the point, and displaying the angle they make 
with the vertical in a counterclockwise sense. The orientations of zenith and 
north will update as you rotate the sky map. Letting go of &Shift; or the mouse 
button will stop the rotation operation. As you pan the sky map or focus it on 
different objects, the rotation you set is retained as an offset from the 
reference direction. The reference direction is north when using Equatorial 
Coordinates and zenith when using Horizontal Coordinates. As a reminder, the 
reference direction is solid and brighter in the temporary overlay. For the two 
common orientations of erect and inverted, the rotation can be set / reset 
using the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu. Select "North Down" or "Zenith 
Down" as is applicable to set an orientation of 180 degrees.
+    Next, to rotate the sky map freely, you can hold down the &Shift; key and 
drag the mouse on the sky map. A temporary overlay will appear showing the 
direction of north and zenith at the point, and displaying the angle they make 
with the vertical in a counterclockwise sense. The orientations of zenith and 
north will update as you rotate the sky map. Letting go of &Shift; or the mouse 
button will stop the rotation operation. As you pan the sky map or focus it on 
different objects, the rotation you set is retained as an offset from the 
reference direction. The reference direction is north when using Equatorial 
Coordinates and zenith when using Horizontal Coordinates. As a reminder, the 
reference direction is solid and brighter in the temporary overlay. The 
temporary overlay also marks the East direction, which will be clockwise from 
north when mirrored and counter-clockwise when not mirrored. For the two common 
orientations of erect and inverted, the rotation can be set / reset using the 
<menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu. Select <guilabel>North 
Down</guilabel> or <guilabel>Zenith Down</guilabel> as is applicable to set an 
orientation of 180 degrees.
   </para>
   <para>
-    If you are visually observing through an eyepiece of an instrument, you 
may need to do some more correction. For the common case of a large Dobsonian 
telescope (or more generally a Newtonian design mounted on an altazimuth 
mount), a systematic additional correction is of help. This correction applies 
because we stand erect while using the telescope irrespective of the angle the 
telescope tube is making with the ground. So as we move the telescope in 
altitude, an additional correction depending on the altitude of the object 
needs to be applied to make the sky map match the view through the eyepiece. 
This correction is enabled by checking the <guilabel>Erect observer 
correction</guilabel> checkbox in the 
<menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu. This correction only makes sense 
in Horizontal Coordinate mode and is disabled when using equatorial coordinates.
+    If you are visually observing through an eyepiece of an instrument, you 
may need to do some more correction. For the common case of a large Dobsonian 
telescope (or more generally a Newtonian design mounted on an altazimuth 
mount), an additional systematic correction is of help. This correction applies 
because we stand erect while using the telescope irrespective of the angle the 
telescope tube is making with the ground. So as we move the telescope in 
altitude, an additional correction depending on the altitude of the object 
needs to be applied to make the sky map match the view through the eyepiece 
where the observer is standing erect. This correction is enabled by choosing 
the appropriate "Erect observer correction" option in the 
<menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu. The correction depends on which 
side the telescope's focuser is placed by the manufacturer. If when observing 
just above the horizon through the eyepiece, the sky is on the observer's right 
side (and the mirror to the left), pick the <guilabel>Erect observer 
correction, right-handed</guilabel> option. Similarly, if the sky is to the 
left of the observer, choose the <guilabel>Erect observer correct, 
left-handed</guilabel> option. This correction only makes sense in Horizontal 
Coordinate mode and is disabled when using equatorial coordinates.
   </para>
   <para>
-    Finally we provide some examples of how to use these settings for various 
instruments:
+    We now provide some examples of how to use these settings for various 
instruments:
     <itemizedlist>
       <listitem><para>Naked-eye observing: Choose Horizontal Coordinates and a 
<guilabel>Zenith Up</guilabel> orientation under 
<menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice>.</para></listitem>
       <listitem><para>Camera on an equatorially mounted telescope: Choose 
Equatorial Coordinates and adjust the orientation of the sky map so that it 
matches your camera. As your mount points to different regions of the sky, the 
orientation should be rendered correctly.</para></listitem>
       <listitem><para>Using binoculars: Same settings as Naked-eye 
observing</para></listitem>
       <listitem><para>Eyepiece of an altazimuth Schmidt-Cassegrain telescope 
with an erecting prism: Under the <guimenu>View</guimenu> menu, choose 
<guilabel>Mirrored View</guilabel>, and under the <guisubmenu>Skymap 
Orientation</guisubmenu> sub-menu, choose <guilabel>Zenith Up</guilabel>. 
Finally, tweak the rotation manually to match the eyepiece view according to 
the angle you are using for your erecting prism.</para></listitem>
-      <listitem><para>Using a RACI finder scope on an altazimuth mounted 
telescope: Same settings as Naked-eye observing, except you may need to tweak 
the orientation manually once if you have it mounted at an 
angle</para></listitem>
+      <listitem><para>Using a RACI finder scope on an altazimuth mounted 
telescope, looking straight down into it: Same settings as Naked-eye observing, 
except you may need to tweak the orientation manually once if you have it 
mounted at an angle</para></listitem>
+      <listitem><para>Using a RACI finder scope on an altazimuth mounted 
telescope, looking through it from the side: In addition to the aforementioned, 
enable Erect observer correction for the appropriate side.</para></listitem>
       <listitem><para>Using a straight-through (inverted view) finder scope on 
an altazimuth mounted telescope: Choose Horizontal Coordinates and a sky-map 
orientation of <guilabel>Zenith Down</guilabel> in 
<menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu</para></listitem>
-      <listitem><para>Eyepiece of a Dobsonian telescope: Choose Horizontal 
Coordinates, and in the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu, select <guilabel>Zenith 
Down</guilabel> and check the <guilabel>Erect observer correction</guilabel> 
option. Then adjust the orientation manually once to match your telescope 
eyepiece view, and it should henceforth track it correctly.</para></listitem>
+      <listitem><para>Eyepiece of a Dobsonian telescope: Choose Horizontal 
Coordinates, and in the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap 
Orientation</guisubmenu></menuchoice> submenu, select <guilabel>Zenith 
Down</guilabel> and enable the erect observer correction, picking the 
left/right handed option as is appropriate for your telescope. Then adjust the 
orientation manually once to match your telescope eyepiece view, and it should 
henceforth track it correctly.</para></listitem>
     </itemizedlist>
   </para>
+  <para>
+    It is typical in visual astronomy to use at least three different 
instruments: the unaided eye, a finder scope, and the main telescope. The 
orientations of these three will have different settings and will need frequent 
modification of all the aforementioned options. To make it easy to adjust these 
settings together, KStars provides the <guilabel>Views</guilabel> feature. This 
feature is accessible through the 
<menuchoice><guimenu>View</guimenu><guisubmenu>Views</guisubmenu></menuchoice> 
menu and the options contained therein. The <guilabel>Arbitrary</guilabel> view 
is not a real view, but the option that gets selected when the sky-map 
orientation is modified manually through the previously described options. The 
rest of the views are bona fide views. New views may be added, or the existing 
views may be edited, removed, or re-ordered using the 
<menuchoice><guimenu>View</guimenu><guisubmenu>Views</guisubmenu><guimenuitem>Edit
 Views...</guimenuitem></menuchoice> option. Choosing this brings up a window 
to manage the views:
+  </para>
+  <screenshot>
+    <screeninfo>Dialog to manage sky map views</screeninfo>
+    <mediaobject>
+      <imageobject>
+       <imagedata fileref="viewmanager.png" format="PNG"/>
+      </imageobject>
+      <textobject>
+       <phrase>Manage Sky Map Views</phrase>
+      </textobject>
+    </mediaobject>
+  </screenshot>
+  <para>
+    To remove a view, simply select the view from the list and delete it using 
the <guibutton>Remove</guibutton> button. To re-order the views, use the mouse 
to drag the view you wish to move and drop it at its destination in-between two 
other entries. To edit a view, select the view from the list and click 
<guibutton>Edit...</guibutton>. To create a new view, click the 
<guibutton>New...</guibutton> button. The <guibutton>Edit...</guibutton> and 
<guibutton>New...</guibutton> options bring up a view editor interface:
+  </para>
+  <screenshot>
+    <screeninfo>Dialog to create a new view or edit an existing 
one</screeninfo>
+    <mediaobject>
+      <imageobject>
+       <imagedata fileref="newview.png" format="PNG"/>
+      </imageobject>
+      <textobject>
+       <phrase>Edit / Create View</phrase>
+      </textobject>
+    </mediaobject>
+  </screenshot>
+
+  <para>
+  The <guilabel>Name</guilabel> field carries a unique name for the View. The 
<guilabel>Mount Type</guilabel> determines whether the reference direction used 
for orientation will be north or zenith. Typically, one would set this to the 
type of mount used for the telescope. However, when using refractors and 
Schmidt-Cassegrain Telescopes (SCTs) with a rotatable diagonal, the observer 
will have a tendency to re-orient the eyepiece for comfort so that the eyepiece 
remains at a fixed angle with respect to the zenith. For this reason, it makes 
sense to choose <guilabel>Altazimuth</guilabel> mounting even when the 
telescope is actually on an equatorial mount. Choose 
<guilabel>Equatorial</guilabel> mounting when the focuser will not be 
re-oriented, such as when using a camera on an equatorially mounted telescope.
+
+  For Newtonian telescopes that invert (i.e. rotate by 180 degrees but do not 
change the handedness) of the view, pick the <guilabel>Inverted</guilabel> 
option. This is also the correct option for straight-through refractors and 
finder scopes. When using a erecting prism diagonal, the prism erects the 
inverted image by flipping it up-down. This results overall in a left-right 
mirrored image. Thus for telescopes that use an erecting prism, pick 
<guilabel>Mirrored</guilabel>. A special kind of prism called an Amici roof 
prism not only erects the image vertically, but it also prevents left-right 
mirroring of the image. Finder scopes incorporating such a diagonal are 
normally called "Right-Angle Correct Image" or RACI finder scopes. Such 
diagonals may also be used on refractors and SCTs. When using such a prism that 
produces a correct image, choose the <guilabel>Correct</guilabel> option. The 
<guilabel>Mirrored on the vertical axis</guilabel> option is not encountered in 
typical astronomical instruments, but is provided for completeness.
+
+  Two more factors need to be considered: one is the angle of the eyepiece 
with respect to the reference direction (north / zenith), and the other is the 
orientation of the observer's head (and notion of the vertical) which we 
explained when describing the erect observer correction feature. These two 
aspects are configured using the single slider titled <guilabel>Eyepiece 
Angle</guilabel>. Two illustrations below the slider show the interpretation of 
this setting; on the left, as seen from the front as is more convenient for 
Newtonian telescopes, and on the right as is seen from the back, more 
convenient for refractors and Cassegrains. The observer naturally stands on the 
side that makes it more convenient to look through the eyepiece, so the erect 
observer correction is automatically adjusted accordingly. For eyepiece angles 
that are less than -1 degree on the slider, the <guilabel>Erect observer 
correction, right-handed</guilabel> option is applied. Similarly, for eyepiece 
angles that are greater than +1 degree, the <guilabel>Erect observer 
correction, left-handed</guilabel> is applied. At 0 degrees, no erect observer 
correction is applied. This correction is indicated by a silhouette of a person 
standing on the appropriate side of the telescope. In our convention, most 
mass-manufactured Dobsonians seem to have a correction around +45 degrees. 
Incidentally, this correction is also useful for finder scopes with diagonals.
+
+  One may want to explicitly disable the erect observer correction even when 
the eyepiece angle is not zero. This is useful in case the view comes from a 
CCD camera that does not change angle with respect to the telescope body 
(unlike an observer's head), or if the display showing KStars' sky map is 
mounted on the telescope body itself. In this case the <guilabel>Display 
mounted on the telescope</guilabel> option can be checked. For the opposite 
effect, i.e. where the eyepiece angle is zero, but the observer is leaning to 
look through the eyepiece from one of the two sides, set the eyepiece angle to 
plus or minus 2 degrees to enable the erect observer correction; the minor 
difference will not be noticeable.
+
+  Finally, one may want triggering of the view to also set the field-of-view 
of the sky map to some value, for example to set the FOV of a finder scope. In 
this case, the <guilabel>Also set the field of view</guilabel> check-box may be 
checked, and an approximate field-of-view to adjust may be specified. If this 
is not enabled, the zoom level of the sky map is not altered when this view is 
applied.
+  </para>
+
 </sect1>
 
+
+
 &hips;
 
 </chapter>
diff --git a/doc/newview.png b/doc/newview.png
new file mode 100644
index 0000000000..296ce073ed
Binary files /dev/null and b/doc/newview.png differ
diff --git a/doc/viewmanager.png b/doc/viewmanager.png
new file mode 100644
index 0000000000..a96d59bfb3
Binary files /dev/null and b/doc/viewmanager.png differ
diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt
index 74791fa1d5..7cec6bace9 100644
--- a/kstars/CMakeLists.txt
+++ b/kstars/CMakeLists.txt
@@ -678,6 +678,7 @@ set(kstars_dialogs_SRCS
     dialogs/finddialog.cpp
     dialogs/focusdialog.cpp
     dialogs/fovdialog.cpp
+    dialogs/viewsdialog.cpp
     dialogs/locationdialog.cpp
     dialogs/timedialog.cpp
     dialogs/exportimagedialog.cpp
@@ -700,12 +701,14 @@ set(kstars_dialogsui_SRCS
     dialogs/finddialog.ui
     dialogs/focusdialog.ui
     dialogs/fovdialog.ui
+    dialogs/viewsdialog.ui
     dialogs/locationdialog.ui
     dialogs/wizwelcome.ui
     dialogs/wizlocation.ui
     dialogs/wizdownload.ui
     dialogs/wizdata.ui
     dialogs/newfov.ui
+    dialogs/newview.ui
     dialogs/exportimagedialog.ui
 )
 
@@ -924,6 +927,7 @@ SET(kstars_extra_kstars_SRCS
     auxiliary/imageviewer.cpp
     auxiliary/xplanetimageviewer.cpp
     auxiliary/fov.cpp
+    auxiliary/skymapview.cpp
     auxiliary/thumbnailpicker.cpp
     auxiliary/thumbnaileditor.cpp
     auxiliary/imageexporter.cpp
diff --git a/kstars/auxiliary/dms.cpp b/kstars/auxiliary/dms.cpp
index 348acb0d4c..4ad0abfc2b 100644
--- a/kstars/auxiliary/dms.cpp
+++ b/kstars/auxiliary/dms.cpp
@@ -251,11 +251,19 @@ int dms::msecond(void) const
 const dms dms::reduce(void) const
 {
     if (std::isnan(D))
-        return dms(0);
+        return dms(0); // FIXME: Why 0 and not NaN? -- asimha
 
     return dms(D - 360.0 * floor(D / 360.0));
 }
 
+double dms::reduce(const double D)
+{
+    if (std::isnan(D))
+        return D;
+
+    return (D - 360.0 * floor(D / 360.0));
+}
+
 const dms dms::deltaAngle(dms angle) const
 {
     double angleDiff = D - angle.Degrees();
diff --git a/kstars/auxiliary/dms.h b/kstars/auxiliary/dms.h
index 3cf7a43114..f5a349ca4f 100644
--- a/kstars/auxiliary/dms.h
+++ b/kstars/auxiliary/dms.h
@@ -405,6 +405,9 @@ class dms
          */
     static dms fromString(const QString &s, bool deg);
 
+    /** Reduce an angle in degrees expressed as a double */
+    static double reduce(const double D);
+
     inline dms operator-() { return dms(-D); }
 #ifdef COUNT_DMS_SINCOS_CALLS
     static long unsigned dms_constructor_calls; // counts number of DMS 
constructor calls
diff --git a/kstars/auxiliary/ksuserdb.cpp b/kstars/auxiliary/ksuserdb.cpp
index b9f69e6607..7887e250bb 100644
--- a/kstars/auxiliary/ksuserdb.cpp
+++ b/kstars/auxiliary/ksuserdb.cpp
@@ -281,7 +281,6 @@ bool KSUserDB::Initialize()
                         "settings TEXT DEFAULT NULL)"))
             qCWarning(KSTARS) << query.lastError();
 
-
         // Add DSLR lenses table
         if (!query.exec("CREATE TABLE dslrlens ( "
                         "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
@@ -551,7 +550,6 @@ bool KSUserDB::RebuildDB()
                   " sure to compensate for this effect the flux conserving 
clip-resampling option.', '9', 'equatorial', '512', 'jpeg fits',"
                   "'http://alaskybis.u-strasbg.fr/Fermi/Color', '1')");
 
-
     tables.append("CREATE TABLE dslr (id INTEGER DEFAULT NULL PRIMARY KEY 
AUTOINCREMENT, "
                   "Model TEXT DEFAULT NULL, "
                   "Width INTEGER DEFAULT NULL, "
@@ -559,7 +557,6 @@ bool KSUserDB::RebuildDB()
                   "PixelW REAL DEFAULT 5.0,"
                   "PixelH REAL DEFAULT 5.0)");
 
-
     tables.append("CREATE TABLE effectivefov ( "
                   "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
                   "Train TEXT DEFAULT NULL, "
@@ -855,7 +852,6 @@ bool KSUserDB::GetAllDarkFrames(QList<QVariantMap> 
&darkFrames)
     return true;
 }
 
-
 /* Effective FOV Section */
 
 
////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1240,7 +1236,6 @@ bool KSUserDB::GetAllHIPSSources(QList<QMap<QString, 
QString>> &HIPSSources)
     return true;
 }
 
-
 /* DSLR Section */
 
 
////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -2505,6 +2500,116 @@ bool KSUserDB::GetAllImageOverlays(QList<ImageOverlay> 
*imageOverlayList)
     return true;
 }
 
+void KSUserDB::CreateSkyMapViewTableIfNecessary()
+{
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    QString command = "CREATE TABLE IF NOT EXISTS SkyMapViews ( "
+                      "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
+                      "name TEXT NOT NULL UNIQUE, "
+                      "data JSON)";
+    QSqlQuery query(db);
+    if (!query.exec(command))
+    {
+        qCDebug(KSTARS) << query.lastError();
+        qCDebug(KSTARS) << query.executedQuery();
+    }
+}
+
+bool KSUserDB::DeleteAllSkyMapViews()
+{
+    CreateSkyMapViewTableIfNecessary();
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel views(nullptr, db);
+    views.setTable("SkyMapViews");
+    views.setFilter("id >= 1");
+    views.select();
+    views.removeRows(0, views.rowCount());
+    views.submitAll();
+
+    QSqlQuery query(db);
+    QString dropQuery = QString("DROP TABLE SkyMapViews");
+    if (!query.exec(dropQuery))
+        qCWarning(KSTARS) << query.lastError().text();
+
+    return true;
+}
+
+bool KSUserDB::AddSkyMapView(const SkyMapView &view)
+{
+    CreateSkyMapViewTableIfNecessary();
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel views(nullptr, db);
+    views.setTable("SkyMapViews");
+    views.setFilter("name LIKE \'" + view.name + "\'");
+    views.select();
+
+    if (views.rowCount() > 0)
+    {
+        QSqlRecord record = views.record(0);
+        record.setValue("name", view.name);
+        record.setValue("data", 
QJsonDocument(view.toJson()).toJson(QJsonDocument::Compact));
+        views.setRecord(0, record);
+        views.submitAll();
+    }
+    else
+    {
+        int row = 0;
+        views.insertRows(row, 1);
+
+        views.setData(views.index(row, 1), view.name);  // row,0 is 
autoincerement ID
+        views.setData(views.index(row, 2), 
QJsonDocument(view.toJson()).toJson(QJsonDocument::Compact));
+        views.submitAll();
+    }
+    return true;
+}
+
+bool KSUserDB::GetAllSkyMapViews(QList<SkyMapView> &skyMapViewList)
+{
+    CreateSkyMapViewTableIfNecessary();
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    skyMapViewList.clear();
+    QSqlTableModel views(nullptr, db);
+    views.setTable("SkyMapViews");
+    views.select();
+
+    for (int i = 0; i < views.rowCount(); ++i)
+    {
+        QSqlRecord record         = views.record(i);
+
+        const QString name        = record.value("name").toString();
+        const QJsonDocument data  = 
QJsonDocument::fromJson(record.value("data").toString().toUtf8());
+        Q_ASSERT((!data.isNull()) && data.isObject());
+        if (data.isNull() || !data.isObject())
+        {
+            qCCritical(KSTARS) << "Data associated with sky map view " << name 
<< " is invalid!";
+            continue;
+        }
+        SkyMapView o(name, data.object());
+        skyMapViewList.append(o);
+    }
+
+    views.clear();
+    return true;
+}
+
 int KSUserDB::AddProfile(const QString &name)
 {
     auto db = QSqlDatabase::database(m_ConnectionName);
@@ -2544,8 +2649,6 @@ bool KSUserDB::DeleteProfile(const 
QSharedPointer<ProfileInfo> &pi)
     if (rc == false)
         qCWarning(KSTARS) << query.lastQuery() << query.lastError().text();
 
-
-
     return rc;
 }
 
@@ -2679,7 +2782,6 @@ bool KSUserDB::SaveProfile(const 
QSharedPointer<ProfileInfo> &pi)
     /*if (pi->customDrivers.isEmpty() == false && !query.exec(QString("INSERT 
INTO custom_driver (drivers, profile) 
VALUES('%1',%2)").arg(pi->customDrivers).arg(pi->id)))
         qDebug()  << query.lastQuery() << query.lastError().text();*/
 
-
     return true;
 }
 
@@ -3014,7 +3116,6 @@ void KSUserDB::AddProfileSettings(uint32_t profile, const 
QByteArray &settings)
     if (!profileSettings.submitAll())
         qCWarning(KSTARS) << profileSettings.lastError();
 
-
 }
 
 void KSUserDB::UpdateProfileSettings(uint32_t profile, const QByteArray 
&settings)
@@ -3036,7 +3137,6 @@ void KSUserDB::UpdateProfileSettings(uint32_t profile, 
const QByteArray &setting
         qCWarning(KSTARS) << profileSettings.lastError();
 }
 
-
 void KSUserDB::DeleteProfileSettings(uint32_t profile)
 {
     auto db = QSqlDatabase::database(m_ConnectionName);
diff --git a/kstars/auxiliary/ksuserdb.h b/kstars/auxiliary/ksuserdb.h
index c6f0d0fad7..b1c5b228bd 100644
--- a/kstars/auxiliary/ksuserdb.h
+++ b/kstars/auxiliary/ksuserdb.h
@@ -7,6 +7,7 @@
 #pragma once
 
 #include "auxiliary/profileinfo.h"
+#include "skymapview.h"
 #ifndef KSTARS_LITE
 #include "oal/oal.h"
 #endif
@@ -79,7 +80,6 @@ class KSUserDB
         bool DeleteDarkFrame(const QString &filename);
         bool GetAllDarkFrames(QList<QVariantMap> &darkFrames);
 
-
         
/************************************************************************
          ******************************* Effective FOVs 
*************************
          
************************************************************************/
@@ -88,7 +88,6 @@ class KSUserDB
         bool DeleteEffectiveFOV(const QString &id);
         bool GetAllEffectiveFOVs(QList<QVariantMap> &effectiveFOVs);
 
-
         
/************************************************************************
          ******************************* Driver Alias *************************
          
************************************************************************/
@@ -170,6 +169,19 @@ class KSUserDB
         /** @brief Gets all the image overlay rows from the database **/
         bool GetAllImageOverlays(QList<ImageOverlay> *imageOverlayList);
 
+        
/************************************************************************
+         ****************************** Sky Map Views 
***************************
+         
************************************************************************/
+
+        /** @brief Deletes all the sky map views stored in the database */
+        bool DeleteAllSkyMapViews();
+
+        /** @brief Adds a new sky map view row in the database */
+        bool AddSkyMapView(const SkyMapView &view);
+
+        /** @brief Gets all the sky map view rows from the database */
+        bool GetAllSkyMapViews(QList<SkyMapView> &skyMapViewList);
+
         
/************************************************************************
          ********************************* Flags 
********************************
          
************************************************************************/
@@ -394,7 +406,6 @@ class KSUserDB
          **/
         bool GetOpticalTrains(uint32_t profileID, QList<QVariantMap> 
&opticalTrains);
 
-
         
/************************************************************************
          ******************************** Profile Settings 
**********************
          
************************************************************************/
@@ -479,6 +490,9 @@ class KSUserDB
         /** @brief creates the image overlay table if it doesn't already exist 
**/
         void CreateImageOverlayTableIfNecessary();
 
+        /** @brief creates the image overlay table if it doesn't already exist 
**/
+        void CreateSkyMapViewTableIfNecessary();
+
 #if 0
         /**
          * @brief Imports flags data from previous format
diff --git a/kstars/auxiliary/skymapview.cpp b/kstars/auxiliary/skymapview.cpp
new file mode 100644
index 0000000000..1b578dcdd7
--- /dev/null
+++ b/kstars/auxiliary/skymapview.cpp
@@ -0,0 +1,205 @@
+/*
+   SPDX-FileCopyrightText: 2024 Akarsh Simha <aka...@kde.org>
+
+   SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "skymapview.h"
+#include "nan.h"
+#include "kstarsdata.h"
+#include "ksuserdb.h"
+#include <kstars_debug.h>
+
+SkyMapView::SkyMapView(const QString &name_, const QJsonObject &jsonData)
+    : name(name_)
+{
+    // Check version
+    if (jsonData["version"].toString() != "1.0.0")
+    {
+        qCCritical(KSTARS) << "Unhandled SkyMapView JSON schema version " << 
jsonData["version"].toString();
+        return;
+    }
+    useAltAz = jsonData["useAltAz"].toBool();
+    viewAngle = jsonData["viewAngle"].toDouble();
+    mirror = jsonData["mirror"].toBool();
+    inverted = jsonData["inverted"].toBool();
+    fov = jsonData["fov"].isNull() ? NaN::d : jsonData["fov"].toDouble();
+    erectObserver = jsonData["erectObserver"].toBool();
+}
+
+QJsonObject SkyMapView::toJson() const
+{
+    return QJsonObject
+    {
+        {"version", "1.0.0"},
+        {"useAltAz", useAltAz},
+        {"viewAngle", viewAngle},
+        {"mirror", mirror},
+        {"inverted", inverted},
+        {"fov", fov},
+        {"erectObserver", erectObserver}
+    };
+}
+
+// // // SkyMapViewManager // // //
+
+QList<SkyMapView> SkyMapViewManager::m_views;
+
+QList<SkyMapView> SkyMapViewManager::defaults()
+{
+    QList<SkyMapView> views;
+
+    SkyMapView view;
+
+    view.name = i18nc("Set the sky-map view to zenith up", "Zenith Up");
+    view.useAltAz = true;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = false;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to zenith down", "Zenith Down");
+    view.useAltAz = true;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = true;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to north up", "North Up");
+    view.useAltAz = false;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = false;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to north down", "North Down");
+    view.useAltAz = false;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = true;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to match a Schmidt-Cassegrain 
telescope with erecting prism pointed upwards",
+                      "SCT with upward diagonal");
+    view.useAltAz = true;
+    view.viewAngle = 0;
+    view.mirror = true;
+    view.inverted = false;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to match the view through a 
typical Dobsonian telescope", "Typical Dobsonian");
+    view.useAltAz = true;
+    view.viewAngle = 45;
+    view.mirror = false;
+    view.inverted = true;
+    view.fov = NaN::d;
+    view.erectObserver = true;
+    views << view;
+
+    return views;
+}
+
+bool SkyMapViewManager::save()
+{
+    // FIXME, this is very inefficient
+    bool success = true;
+    Q_ASSERT(!!KStarsData::Instance());
+    if (!KStarsData::Instance())
+    {
+        qCCritical(KSTARS) << "Cannot save sky map views, no KStarsData 
instance.";
+        return false;
+    }
+    KSUserDB *db = KStarsData::Instance()->userdb();
+    Q_ASSERT(!!db);
+    if (!db)
+    {
+        qCCritical(KSTARS) << "Cannot save sky map views, no KSUserDB 
instance.";
+        return false;
+    }
+    success = success && db->DeleteAllSkyMapViews();
+    if (!success)
+    {
+        qCCritical(KSTARS) << "Failed to flush sky map views from the 
database";
+        return success;
+    }
+    for (const auto &view : m_views)
+    {
+        bool result = db->AddSkyMapView(view);
+        success = success && result;
+        Q_ASSERT(result);
+        if (!result)
+        {
+            qCCritical(KSTARS) << "Failed to commit Sky Map View " << 
view.name << " to the database!";
+        }
+    }
+    return success;
+}
+
+const QList<SkyMapView> &SkyMapViewManager::readViews()
+{
+    Q_ASSERT(!!KStarsData::Instance());
+    if (!KStarsData::Instance())
+    {
+        qCCritical(KSTARS) << "Cannot read sky map views, no KStarsData 
instance.";
+        return m_views;
+    }
+    KSUserDB *db = KStarsData::Instance()->userdb();
+    Q_ASSERT(!!db);
+    if (!db)
+    {
+        qCCritical(KSTARS) << "Cannot save sky map views, no KSUserDB 
instance.";
+        return m_views;
+    }
+    m_views.clear();
+    bool result = db->GetAllSkyMapViews(m_views);
+    Q_ASSERT(result);
+    if (!result)
+    {
+        qCCritical(KSTARS) << "Failed to read sky map views from the 
database!";
+    }
+    if (m_views.isEmpty())
+    {
+        m_views = defaults();
+    }
+    return m_views;
+}
+
+void SkyMapViewManager::drop()
+{
+    m_views.clear();
+}
+
+std::optional<SkyMapView> SkyMapViewManager::viewNamed(const QString &name)
+{
+    for (auto it = m_views.begin(); it != m_views.end(); ++it)
+    {
+        if (it->name == name)
+        {
+            return *it;
+        }
+    }
+    return std::nullopt;
+}
+
+bool SkyMapViewManager::removeView(const QString &name)
+{
+    for (auto it = m_views.begin(); it != m_views.end(); ++it)
+    {
+        if (it->name == name)
+        {
+            m_views.erase(it);
+            return true;
+        }
+    }
+    return false;
+}
diff --git a/kstars/auxiliary/skymapview.h b/kstars/auxiliary/skymapview.h
new file mode 100644
index 0000000000..b728bbef1d
--- /dev/null
+++ b/kstars/auxiliary/skymapview.h
@@ -0,0 +1,81 @@
+/*
+    SPDX-FileCopyrightText: 2024 Akarsh Simha <aka...@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include <QString>
+#include <QJsonObject>
+#include <QList>
+#include <optional>
+
+/**
+ * @struct SkyMapView
+ * Carries parameters of a sky map view
+ */
+struct SkyMapView
+{
+    explicit SkyMapView(const QString &name_, const QJsonObject &jsonData);
+    SkyMapView() = default;
+    QJsonObject toJson() const;
+
+    QString name; // Name of this view (can be empty)
+    bool useAltAz; // Mount type is alt-az when true
+    double viewAngle; // Focuser rotation in degrees, within [-90°, 90°]
+    bool mirror; // Mirrored left-right
+    bool inverted; // 180° rotation if true
+    double fov; // fov in degrees, NaN when disabled
+    bool erectObserver; // Erect observer correction
+};
+
+/**
+ * @class SkyMapViewManager
+ * Manages a list of sky map views
+ * @author Akarsh Simha
+ * @version 1.0
+ */
+class SkyMapViewManager
+{
+    public:
+        /** @short Read the list of views from the database */
+        static const QList<SkyMapView> &readViews();
+
+        /** @short Drop the list */
+        static void drop();
+
+        /** @short Add a view */
+        inline static void addView(const SkyMapView &newView)
+        {
+            m_views.append(newView);
+        }
+
+        /** @short Remove a view
+         * Note: This is currently an O(N) operation
+         */
+        static bool removeView(const QString &name);
+
+        /**
+         * @short Get the view with the given name
+         * @note This is currently an O(N) operation
+         */
+        static std::optional<SkyMapView> viewNamed(const QString &name);
+
+        /** @short Get the list of available views */
+        static const QList<SkyMapView> &getViews()
+        {
+            return m_views;
+        }
+
+        /** @short Commit the list of views to the database */
+        static bool save();
+
+    private:
+        SkyMapViewManager() = default;
+        ~SkyMapViewManager() = default;
+        static QList<SkyMapView> m_views;
+
+        /** @short Fill list with standard views */
+        static QList<SkyMapView> defaults();
+};
diff --git a/kstars/data/kstars.qrc b/kstars/data/kstars.qrc
index a1f67ea315..88cda34b91 100755
--- a/kstars/data/kstars.qrc
+++ b/kstars/data/kstars.qrc
@@ -81,6 +81,7 @@
         <file>noimage.png</file>
         <file>reticle12.png</file>
         <file>reticle24.png</file>
+        <file>observer.png</file>
     </qresource>
     <qresource prefix="/videos">
         <file>sm_animation.gif</file>
diff --git a/kstars/data/kstarsui.rc b/kstars/data/kstarsui.rc
index 8a59a30064..1ba9272f40 100644
--- a/kstars/data/kstarsui.rc
+++ b/kstars/data/kstarsui.rc
@@ -50,6 +50,8 @@
                 <Action name="coordsys" />
                 <Action name="mirror_skymap" />
                 <Action name="skymap_orientation" />
+                <Action name="views" />
+               <Separator />
                 <Action name="toggle_terrain" />
                 <Action name="toggle_image_overlays" />
                 <Menu name="projection"><text>&amp;Projection</text>
diff --git a/kstars/data/observer.png b/kstars/data/observer.png
new file mode 100644
index 0000000000..cb941ab594
Binary files /dev/null and b/kstars/data/observer.png differ
diff --git a/kstars/dialogs/newview.ui b/kstars/dialogs/newview.ui
new file mode 100644
index 0000000000..fbe7b9f383
--- /dev/null
+++ b/kstars/dialogs/newview.ui
@@ -0,0 +1,399 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewView</class>
+ <widget class="QDialog" name="NewView">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>640</width>
+    <height>662</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Add / Edit View</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Name:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="viewNameLineEdit">
+       <property name="placeholderText">
+        <string>9x50 RACI finder on Dob</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="Line" name="line_4">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Mount Type:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="mountTypeComboBox">
+       <property name="currentIndex">
+        <number>1</number>
+       </property>
+       <item>
+        <property name="text">
+         <string>Equatorial</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Altazimuth</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_5">
+     <property name="text">
+      <string>**Note:** Choose mount type &quot;Altazimuth&quot; when visually 
observing through SCTs / refractors, irrespective of the actual 
mounting.</string>
+     </property>
+     <property name="textFormat">
+      <enum>Qt::MarkdownText</enum>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line_3">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QGridLayout" name="gridLayout">
+     <item row="3" column="0">
+      <widget class="QRadioButton" name="correctViewType">
+       <property name="text">
+        <string>Correct (e.g. RACI finder or refractor with Amici roof 
prism)</string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QRadioButton" name="mirroredViewType">
+       <property name="text">
+        <string>Mirrored (e.g. Cassegrain or refractor with erecting 
prism)</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QRadioButton" name="invertedViewType">
+       <property name="text">
+        <string>Inverted (e.g. straight through finder, Newtonian)</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>View Type:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="4" column="0">
+      <widget class="QRadioButton" name="invertedMirroredViewType">
+       <property name="text">
+        <string>Mirrored on the vertical axis (i.e. inverted and 
mirrored)</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QLabel" name="label_4">
+       <property name="text">
+        <string>Eyepiece Angle:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QSlider" name="viewingAngleSlider">
+       <property name="minimum">
+        <number>-179</number>
+       </property>
+       <property name="maximum">
+        <number>179</number>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="tickPosition">
+        <enum>QSlider::TicksBothSides</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="viewingAngleLabel">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>40</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>###°</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QGridLayout" name="gridLayout_2">
+     <item row="1" column="1">
+      <widget class="QLabel" name="label_8">
+       <property name="text">
+        <string>Telescopes with the eyepiece at the bottom</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_7">
+       <property name="text">
+        <string>Telescopes with the eyepiece at the top</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="3" column="0" colspan="2">
+      <widget class="QLabel" name="label_6">
+       <property name="text">
+        <string>The human silhouette indicates on which side of the telescope 
the observer is assumed to stand.</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLabel" name="viewingAnglePreviewBottom">
+       <property name="minimumSize">
+        <size>
+         <width>200</width>
+         <height>150</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <widget class="QLabel" name="viewingAnglePreviewTop">
+       <property name="minimumSize">
+        <size>
+         <width>200</width>
+         <height>150</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_9">
+       <property name="text">
+        <string>(Preview shows view down a Newtonian's tube)</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="QLabel" name="label_10">
+       <property name="text">
+        <string>(Preview shows view of the back of an SCT)</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="disableErectObserverCheckBox">
+     <property name="text">
+      <string>Display mounted on the telescope (also check this if using a 
camera instead of visual observation)</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line_2">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_4">
+     <item>
+      <widget class="QCheckBox" name="fieldOfViewCheckBox">
+       <property name="minimumSize">
+        <size>
+         <width>0</width>
+         <height>30</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Also set the field of view</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="UnitSpinBoxWidget" name="fieldOfViewSpinBox" 
native="true">
+       <property name="minimumSize">
+        <size>
+         <width>195</width>
+         <height>0</height>
+        </size>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>UnitSpinBoxWidget</class>
+   <extends>QWidget</extends>
+   <header>widgets/unitspinboxwidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>NewView</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>250</x>
+     <y>647</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>NewView</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>318</x>
+     <y>647</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/kstars/dialogs/viewsdialog.cpp b/kstars/dialogs/viewsdialog.cpp
new file mode 100644
index 0000000000..79bdfbb8ed
--- /dev/null
+++ b/kstars/dialogs/viewsdialog.cpp
@@ -0,0 +1,420 @@
+/*
+    SPDX-FileCopyrightText: 2003 Jason Harris <kst...@30doradus.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "viewsdialog.h"
+#include <QPointer>
+#include <QPainter>
+#include <QMessageBox>
+#include <QPainterPath>
+#include <QPainterPathStroker>
+#include <kstars_debug.h>
+
+#include "Options.h"
+
+ViewsDialogUI::ViewsDialogUI(QWidget *parent) : QFrame(parent)
+{
+    setupUi(this);
+}
+
+//----ViewsDialogStringListModel-----//
+Qt::ItemFlags ViewsDialogStringListModel::flags(const QModelIndex &index) const
+{
+    Qt::ItemFlags defaultFlags = QStringListModel::flags(index);
+    if (index.isValid())
+    {
+        return defaultFlags & (~Qt::ItemIsDropEnabled);
+    }
+    return defaultFlags;
+}
+
+//---------ViewsDialog---------------//
+ViewsDialog::ViewsDialog(QWidget *p) : QDialog(p)
+{
+#ifdef Q_OS_OSX
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+#endif
+    ui = new ViewsDialogUI(this);
+
+    setWindowTitle(i18nc("@title:window", "Manage Sky Map Views"));
+
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    mainLayout->addWidget(ui);
+    setLayout(mainLayout);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | 
QDialogButtonBox::Close);
+    mainLayout->addWidget(buttonBox);
+    connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+    connect(buttonBox, SIGNAL(rejected()), this, SLOT(close()));
+
+    // Read list of Views and for each view, create a listbox entry
+    m_model = new ViewsDialogStringListModel(this);
+    syncModel();
+    ui->ViewListBox->setModel(m_model);
+    ui->ViewListBox->setDragDropMode(QAbstractItemView::InternalMove);
+    ui->ViewListBox->setDefaultDropAction(Qt::MoveAction);
+    ui->ViewListBox->setDragDropOverwriteMode(false);
+
+    connect(ui->ViewListBox->selectionModel(), 
&QItemSelectionModel::currentChanged, this, &ViewsDialog::slotSelectionChanged);
+    connect(m_model, &ViewsDialogStringListModel::rowsMoved, this, 
&ViewsDialog::syncFromModel);
+    connect(ui->NewButton, SIGNAL(clicked()), SLOT(slotNewView()));
+    connect(ui->EditButton, SIGNAL(clicked()), SLOT(slotEditView()));
+    connect(ui->RemoveButton, SIGNAL(clicked()), SLOT(slotRemoveView()));
+
+}
+
+void ViewsDialog::syncModel()
+{
+    QStringList viewNames;
+    for(const auto &view : SkyMapViewManager::getViews())
+    {
+        viewNames.append(view.name);
+    }
+    m_model->setStringList(viewNames);
+}
+
+void ViewsDialog::syncFromModel()
+{
+    // FIXME: Inefficient code, but it's okay because number of items is small
+    QHash<QString, SkyMapView> nameToViewMap;
+    for(const auto &view : SkyMapViewManager::getViews())
+    {
+        nameToViewMap.insert(view.name, view);
+    }
+    QStringList updatedList = m_model->stringList();
+    SkyMapViewManager::drop();
+    for (const auto &view : updatedList)
+    {
+        SkyMapViewManager::addView(nameToViewMap[view]);
+    }
+    SkyMapViewManager::save();
+}
+
+void ViewsDialog::slotSelectionChanged(const QModelIndex &current, const 
QModelIndex &prev)
+{
+    Q_UNUSED(prev);
+    bool enable = current.isValid();
+    ui->RemoveButton->setEnabled(enable);
+    ui->EditButton->setEnabled(enable);
+}
+
+void ViewsDialog::slotNewView()
+{
+    QPointer<NewView> newViewDialog = new NewView(this);
+    if (newViewDialog->exec() == QDialog::Accepted)
+    {
+        const auto view = newViewDialog->getView();
+        SkyMapViewManager::addView(view);
+        m_model->insertRow(m_model->rowCount());
+        QModelIndex index = m_model->index(m_model->rowCount() - 1, 0);
+        m_model->setData(index, view.name);
+        ui->ViewListBox->setCurrentIndex(index);
+    }
+    delete newViewDialog;
+}
+
+void ViewsDialog::slotEditView()
+{
+    //Preload current values
+    QModelIndex currentIndex = ui->ViewListBox->currentIndex();
+    if (!currentIndex.isValid())
+        return;
+    const QString viewName = m_model->data(currentIndex).toString();
+    std::optional<SkyMapView> view = SkyMapViewManager::viewNamed(viewName);
+    Q_ASSERT(!!view);
+    if (!view)
+    {
+        qCCritical(KSTARS) << "Programming Error";
+        return; // Eh?
+    }
+
+    // Create dialog
+    QPointer<NewView> newViewDialog = new NewView(this, view);
+    if (newViewDialog->exec() == QDialog::Accepted)
+    {
+        // Overwrite Views
+        SkyMapViewManager::removeView(viewName);
+        const auto view = newViewDialog->getView();
+        SkyMapViewManager::addView(view);
+        syncModel();
+    }
+    delete newViewDialog;
+}
+
+void ViewsDialog::slotRemoveView()
+{
+    QModelIndex currentIndex = ui->ViewListBox->currentIndex();
+    if (!currentIndex.isValid())
+        return;
+    const QString viewName = m_model->data(currentIndex).toString();
+    if (SkyMapViewManager::removeView(viewName))
+    {
+        m_model->removeRow(currentIndex.row());
+    }
+}
+
+//-------------NewViews------------------//
+
+class SliderResetEventFilter : public QObject
+{
+    public:
+        SliderResetEventFilter(QSlider *slider, QObject *parent = nullptr)
+            : QObject(parent)
+            , m_slider(slider)
+        {
+            if (m_slider)
+            {
+                m_slider->installEventFilter(this);
+            }
+        }
+
+        bool eventFilter(QObject *obj, QEvent *event)
+        {
+            if (obj == m_slider && event->type() == 
QEvent::MouseButtonDblClick)
+            {
+                QMouseEvent *mouseEvent = dynamic_cast<QMouseEvent*>(event);
+                Q_ASSERT(!!mouseEvent);
+                if (mouseEvent->button() == Qt::LeftButton)
+                {
+                    m_slider->setValue(0);
+                    return true;
+                }
+            }
+            return QObject::eventFilter(obj, event);
+        }
+
+    private:
+        QSlider *m_slider;
+};
+
+NewView::NewView(QWidget *parent, std::optional<SkyMapView> _view) : 
QDialog(parent)
+{
+    setupUi(this);
+
+    if (_view)
+    {
+        setWindowTitle(i18nc("@title:window", "Edit View"));
+    }
+    else
+    {
+        setWindowTitle(i18nc("@title:window", "New View"));
+    }
+
+    fieldOfViewSpinBox->addUnit("degrees", 1.0);
+    fieldOfViewSpinBox->addUnit("arcmin", 1 / 60.);
+    fieldOfViewSpinBox->addUnit("arcsec", 1 / 3600.);
+    fieldOfViewSpinBox->doubleSpinBox->setMaximum(600.0);
+    fieldOfViewSpinBox->doubleSpinBox->setMinimum(0.01);
+    fieldOfViewSpinBox->setEnabled(false);
+    fieldOfViewSpinBox->doubleSpinBox->setValue(1.0);
+
+    // Enable the "OK" button only when the "Name" field is not empty
+    connect(viewNameLineEdit, &QLineEdit::textChanged, [&](const QString & 
text)
+    {
+        buttonBox->button(QDialogButtonBox::Ok)->setDisabled(text.isEmpty());
+    });
+
+    // Enable the FOV spin box and unit combo only when the Set FOV checkbox 
is checked
+    connect(fieldOfViewCheckBox, &QCheckBox::toggled, fieldOfViewSpinBox, 
&UnitSpinBoxWidget::setEnabled);
+
+    // Update the angle value and graphic when the viewing angle slider is 
changed
+    connect(viewingAngleSlider, &QSlider::valueChanged, [&](const double value)
+    {
+        viewingAngleLabel->setText(QString("%1°").arg(QString::number(value)));
+        this->updateViewingAnglePreviews();
+    });
+    viewingAngleSlider->setValue(0); // Force the updates
+
+    // Update the viewing angle graphic when the erect observer correction is 
enabled / disabled
+    connect(disableErectObserverCheckBox, &QCheckBox::toggled, this, 
&NewView::updateViewingAnglePreviews);
+
+    // Disable erect observer when using equatorial mount
+    connect(mountTypeComboBox, 
QOverload<int>::of(&QComboBox::currentIndexChanged), [&](const int index) {
+        if (index == 0)
+        {
+            // Equatorial
+            disableErectObserverCheckBox->setChecked(true);
+            disableErectObserverCheckBox->setEnabled(false);
+        }
+        else
+        {
+            // Altazimuth
+            disableErectObserverCheckBox->setEnabled(true);
+        }
+    });
+
+
+    // Set up everything else
+    m_topPreview = new QPixmap(400, 300);
+    m_bottomPreview = new QPixmap(400, 300);
+    m_observerPixmap = new QPixmap(":/images/observer.png");
+    new SliderResetEventFilter(viewingAngleSlider);
+
+    // Finally, initialize fields as required
+    if (_view)
+    {
+        const auto view = *_view;
+        m_originalName = view.name;
+        viewNameLineEdit->setText(view.name);
+        mountTypeComboBox->setCurrentIndex(view.useAltAz ? 1 : 0);
+        if (view.inverted && view.mirror)
+        {
+            invertedMirroredViewType->setChecked(true);
+        }
+        else if (view.inverted)
+        {
+            invertedViewType->setChecked(true);
+        }
+        else if (view.mirror)
+        {
+            mirroredViewType->setChecked(true);
+        }
+        else
+        {
+            correctViewType->setChecked(true);
+        }
+
+        viewingAngleSlider->setValue(view.viewAngle);
+        disableErectObserverCheckBox->setChecked(!view.erectObserver);
+        if (!std::isnan(view.fov))
+        {
+            fieldOfViewCheckBox->setChecked(true);
+            fieldOfViewSpinBox->doubleSpinBox->setValue(view.fov);
+        }
+    }
+
+}
+
+NewView::~NewView()
+{
+    delete m_topPreview;
+    delete m_bottomPreview;
+    delete m_observerPixmap;
+}
+
+const SkyMapView NewView::getView() const
+{
+    struct SkyMapView view;
+
+    view.name      = viewNameLineEdit->text();
+    view.useAltAz  = (mountTypeComboBox->currentIndex() > 0);
+    view.viewAngle = viewingAngleSlider->value();
+    view.mirror    = invertedMirroredViewType->isChecked() || 
mirroredViewType->isChecked();
+    view.inverted  = invertedMirroredViewType->isChecked() || 
invertedViewType->isChecked();
+    view.fov       = fieldOfViewCheckBox->isChecked() ? 
fieldOfViewSpinBox->value() : NaN::d;
+    view.erectObserver = !(disableErectObserverCheckBox->isChecked());
+
+    return view;
+}
+
+void NewView::done(int r)
+{
+    if (r == QDialog::Accepted)
+    {
+        const QString name = viewNameLineEdit->text();
+        if (name != m_originalName)
+        {
+            if (!!SkyMapViewManager::viewNamed(name))
+            {
+                QMessageBox::critical(this, i18n("Conflicting View Name"),
+                                      i18n("There already exists a view with 
the name you attempted to use. Please choose a different name for this view."));
+                return;
+            }
+        }
+    }
+    QDialog::done(r);
+    return;
+}
+
+void NewView::updateViewingAnglePreviews()
+{
+    Q_ASSERT(!!m_topPreview);
+    Q_ASSERT(!!m_bottomPreview);
+    Q_ASSERT(!!m_observerPixmap);
+
+    QPen pen(this->palette().color(QPalette::WindowText));
+    {
+        m_topPreview->fill(Qt::transparent);
+        float cx = m_topPreview->width() / 2., cy = m_topPreview->height() / 
2.;
+        float size = std::min(m_topPreview->width(), m_topPreview->height());
+        float r = 0.75 * (size / 2.);
+        QPainter p(m_topPreview);
+
+        // Circle representing tube / secondary cage
+        pen.setWidth(5);
+        p.setPen(pen);
+        p.drawEllipse(QPointF(cx, cy), r, r);
+
+        // Cross hairs representing secondary vanes
+        pen.setWidth(3);
+        p.setPen(pen);
+        p.drawLine(cx - r, cy, cx + r, cy);
+        p.drawLine(cx, cy - r, cx, cy + r);
+
+        // Focuser
+        QPainterPathStroker stroker;
+        stroker.setWidth(20.f);
+        QPainterPath focuserPath;
+        double theta = dms::DegToRad * (viewingAngleSlider->value() - 90);
+        focuserPath.moveTo(cx + (r + 5.) * std::cos(theta), cy + (r + 5.) * 
std::sin(theta));
+        focuserPath.lineTo(cx + (r + 25.) * std::cos(theta), cy + (r + 25.) * 
std::sin(theta));
+        p.drawPath(stroker.createStroke(focuserPath));
+
+        // Observer
+        if (!disableErectObserverCheckBox->isChecked() && 
std::abs(viewingAngleSlider->value()) > 1)
+        {
+            p.drawPixmap(QPointF(
+                             viewingAngleSlider->value() > 0 ? 
m_topPreview->width() - m_observerPixmap->width() : 0,
+                             m_topPreview->height() - 
m_observerPixmap->height()),
+                         viewingAngleSlider->value() < 0 ?
+                         m_observerPixmap->transformed(QMatrix(-1, 0, 0, 1, 0, 
0)) :
+                         *m_observerPixmap);
+        }
+        p.end();
+
+        // Display the pixmap to the QLabel
+        
viewingAnglePreviewTop->setPixmap(m_topPreview->scaled(viewingAnglePreviewTop->width(),
 viewingAnglePreviewTop->height(),
+                                          Qt::KeepAspectRatio, 
Qt::SmoothTransformation));
+    }
+
+    {
+        m_bottomPreview->fill(Qt::transparent);
+        float cx = m_bottomPreview->width() / 2., cy = 
m_bottomPreview->height() / 2.;
+        float size = std::min(m_bottomPreview->width(), 
m_bottomPreview->height());
+        float r = 0.75 * (size / 2.);
+        QPainter p(m_bottomPreview);
+
+        // Circle representing the back of an SCT
+        pen.setWidth(5);
+        p.setPen(pen);
+        p.drawEllipse(QPointF(cx, cy), r, r);
+
+        // Focuser
+        QPainterPathStroker stroker;
+        stroker.setWidth(20.f);
+        QPainterPath focuserPath;
+        double theta = dms::DegToRad * (-viewingAngleSlider->value() - 90);
+        focuserPath.moveTo(cx, cy);
+        focuserPath.lineTo(cx + 25. * std::cos(theta), cy + 25. * 
std::sin(theta));
+        p.drawPath(stroker.createStroke(focuserPath));
+
+        // Observer
+        if (!disableErectObserverCheckBox->isChecked() && 
std::abs(viewingAngleSlider->value()) > 1)
+        {
+            p.drawPixmap(QPointF(
+                             viewingAngleSlider->value() < 0 ? 
m_bottomPreview->width() - m_observerPixmap->width() : 0,
+                             m_bottomPreview->height() - 
m_observerPixmap->height()),
+                         viewingAngleSlider->value() > 0 ?
+                         m_observerPixmap->transformed(QMatrix(-1, 0, 0, 1, 0, 
0)) :
+                         *m_observerPixmap);
+        }
+
+        // Display the pixmap on the QLabel
+        p.end();
+        viewingAnglePreviewBottom->setPixmap(m_bottomPreview->scaled(
+                viewingAnglePreviewBottom->width(), 
viewingAnglePreviewBottom->height(),
+                Qt::KeepAspectRatio, Qt::SmoothTransformation));
+    }
+}
diff --git a/kstars/dialogs/viewsdialog.h b/kstars/dialogs/viewsdialog.h
new file mode 100644
index 0000000000..6db6003773
--- /dev/null
+++ b/kstars/dialogs/viewsdialog.h
@@ -0,0 +1,104 @@
+/*
+    SPDX-FileCopyrightText: 2003 Jason Harris <kst...@30doradus.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef VIEWSDIALOG_H_
+#define VIEWSDIALOG_H_
+
+#include <QPaintEvent>
+#include <QDialog>
+#include <QDoubleSpinBox>
+#include <QStringList>
+#include <QStringListModel>
+#include <optional>
+#include "skymapview.h"
+
+#include "ui_viewsdialog.h"
+#include "ui_newview.h"
+
+class ViewsDialogUI : public QFrame, public Ui::ViewsDialog
+{
+        Q_OBJECT
+    public:
+        explicit ViewsDialogUI(QWidget *parent = nullptr);
+};
+
+class ViewsDialogStringListModel : public QStringListModel
+{
+    public:
+        explicit ViewsDialogStringListModel(QObject* parent = nullptr)
+            : QStringListModel(parent) {}
+
+        Qt::ItemFlags flags(const QModelIndex &index) const override;
+};
+
+/**
+ * @class ViewsDialog
+ * @brief ViewsDialog is dialog to select a Sky Map View (or create a new one)
+ *
+ * A sky map view is a collection of settings that defines the
+ * orientation and scale of the sky map and how it changes as the user
+ * pans around.
+ *
+ * @author Akarsh Simha
+ * @version 1.0
+ */
+class ViewsDialog : public QDialog
+{
+        Q_OBJECT
+    public:
+        explicit ViewsDialog(QWidget *parent = nullptr);
+
+    private slots:
+        void slotNewView();
+        void slotEditView();
+        void slotRemoveView();
+        void slotSelectionChanged(const QModelIndex &, const QModelIndex &);
+
+    private:
+
+        /** Sync the model from the view manager */
+        void syncModel();
+
+        /** Sync the model to the view manager */
+        void syncFromModel();
+
+        QStringListModel* m_model;
+        unsigned int currentItem() const;
+        ViewsDialogUI *ui;
+        static int viewID;
+};
+
+/**
+ * @class NewView
+ * Dialog for defining a new View
+ * @author Akarsh Simha
+ * @version 1.0
+ */
+class NewView : public QDialog, private Ui::NewView
+{
+        Q_OBJECT
+    public:
+        /** Create new dialog
+             * @param parent parent widget
+             * @param view to copy data from. If it's empty will create empty 
one.
+             */
+        explicit NewView(QWidget *parent = nullptr, std::optional<SkyMapView> 
view = std::nullopt);
+        ~NewView() override;
+
+        /** Return the view struct. */
+        const SkyMapView getView() const;
+
+    public slots:
+        void updateViewingAnglePreviews();
+        virtual void done(int r) override;
+
+    private:
+        QString m_originalName;
+        QPixmap *m_observerPixmap; // Icon for an observer
+        QPixmap *m_topPreview, *m_bottomPreview;
+};
+
+#endif
diff --git a/kstars/dialogs/viewsdialog.ui b/kstars/dialogs/viewsdialog.ui
new file mode 100644
index 0000000000..527b31aea0
--- /dev/null
+++ b/kstars/dialogs/viewsdialog.ui
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ViewsDialog</class>
+ <widget class="QWidget" name="ViewsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>275</width>
+    <height>325</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Edit Sky Map Views</string>
+  </property>
+  <layout class="QHBoxLayout">
+   <property name="spacing">
+    <number>6</number>
+   </property>
+   <property name="leftMargin">
+    <number>10</number>
+   </property>
+   <property name="topMargin">
+    <number>10</number>
+   </property>
+   <property name="rightMargin">
+    <number>10</number>
+   </property>
+   <property name="bottomMargin">
+    <number>10</number>
+   </property>
+   <item>
+    <widget class="QListView" name="ViewListBox">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="defaultDropAction">
+      <enum>Qt::IgnoreAction</enum>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QVBoxLayout">
+     <property name="spacing">
+      <number>6</number>
+     </property>
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <widget class="QPushButton" name="NewButton">
+       <property name="toolTip">
+        <string>Add a new FOV symbol</string>
+       </property>
+       <property name="whatsThis">
+        <string>Add a new field-of-view (FOV) symbol to the list.  You can 
define the size, shape, and color of the new symbol.</string>
+       </property>
+       <property name="text">
+        <string>New...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer>
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Fixed</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>85</width>
+         <height>16</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="EditButton">
+       <property name="toolTip">
+        <string>Modify the highlighted FOV symbol</string>
+       </property>
+       <property name="whatsThis">
+        <string>Press this button to modify the highlighted FOV symbol.  You 
can change its size, shape and color.</string>
+       </property>
+       <property name="text">
+        <string>Edit...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="RemoveButton">
+       <property name="toolTip">
+        <string>Remove highlighted FOV symbol</string>
+       </property>
+       <property name="whatsThis">
+        <string>Press this button to remove the highlighted FOV symbol from 
the list.</string>
+       </property>
+       <property name="text">
+        <string>Remove</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer>
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Expanding</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>85</width>
+         <height>126</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/kstars/kstars.cpp b/kstars/kstars.cpp
index 6cd2f27545..ccaf2086f7 100644
--- a/kstars/kstars.cpp
+++ b/kstars/kstars.cpp
@@ -166,7 +166,9 @@ KStars::KStars(bool doSplash, bool clockrun, const QString 
&startdate)
     telescopeGroup->setExclusive(false);
     domeGroup       = new QActionGroup(this);
     domeGroup->setExclusive(false);
+    viewsGroup      = new QActionGroup(this);
     skymapOrientationGroup = new QActionGroup(this);
+    erectObserverCorrectionGroup = new QActionGroup(this);
 
     m_KStarsData = KStarsData::Create();
     Q_ASSERT(m_KStarsData);
@@ -342,8 +344,10 @@ void KStars::applyConfig(bool doApplyFocus)
     actionCollection()->action("show_flags")->setChecked(Options::showFlags());
     
actionCollection()->action("show_supernovae")->setChecked(Options::showSupernovae());
     
actionCollection()->action("show_satellites")->setChecked(Options::showSatellites());
-    
actionCollection()->action("erect_observer_correction")->setChecked(Options::erectObserverCorrection());
-    
actionCollection()->action("erect_observer_correction")->setEnabled(Options::useAltAz());
+    erectObserverCorrectionGroup->setEnabled(Options::useAltAz());
+    
actionCollection()->action("erect_observer_correction_off")->setChecked(Options::erectObserverCorrection()
 == 0);
+    
actionCollection()->action("erect_observer_correction_left")->setChecked(Options::erectObserverCorrection()
 == 1);
+    
actionCollection()->action("erect_observer_correction_right")->setChecked(Options::erectObserverCorrection()
 == 2);
     
actionCollection()->action("mirror_skymap")->setChecked(Options::mirrorSkyMap());
     statusBar()->setVisible(Options::showStatusBar());
 
@@ -545,6 +549,32 @@ void KStars::selectPreviousFov()
     map()->update();
 }
 
+void KStars::selectNextView()
+{
+    QList<QAction*> actions = viewsGroup->actions();
+    int currentIndex = actions.indexOf(viewsGroup->checkedAction());
+    int newIndex = currentIndex + 1;
+    if (newIndex == actions.count() - 1)
+    {
+        newIndex++; // Skip "Arbitrary"
+    }
+    actions[newIndex % actions.count()]->activate(QAction::Trigger);
+    map()->slotDisplayFadingText(actions[newIndex % 
actions.count()]->data().toString());
+}
+
+void KStars::selectPreviousView()
+{
+    QList<QAction*> actions = viewsGroup->actions();
+    int currentIndex = actions.indexOf(viewsGroup->checkedAction());
+    int newIndex = currentIndex - 1;
+    if (currentIndex <= 0)
+    {
+        newIndex = actions.count() - 2; // Skip "Arbitrary"
+    }
+    actions[newIndex]->activate(QAction::Trigger);
+    map()->slotDisplayFadingText(actions[newIndex]->data().toString());
+}
+
 //FIXME Port to QML2
 //#if 0
 void KStars::showWISettingsUI()
diff --git a/kstars/kstars.h b/kstars/kstars.h
index 4604aff0e2..3f17cc298e 100644
--- a/kstars/kstars.h
+++ b/kstars/kstars.h
@@ -192,6 +192,10 @@ class KStars : public KXmlGuiWindow
 
         void selectPreviousFov();
 
+        void selectNextView();
+
+        void selectPreviousView();
+
         void showWISettingsUI();
 
         void showWI(ObsConditions *obs);
@@ -199,6 +203,9 @@ class KStars : public KXmlGuiWindow
         /** Load HIPS information and repopulate menu. */
         void repopulateHIPS();
 
+        /** Load Views and repopulate menu. */
+        void repopulateViews();
+
         void repopulateOrientation();
 
         WIEquipSettings *getWIEquipSettings()
@@ -732,9 +739,15 @@ class KStars : public KXmlGuiWindow
         /** Select the Target symbol (a.k.a. field-of-view indicator) */
         void slotTargetSymbol(bool flag);
 
+        /** Apply the provided sky map view */
+        void slotApplySkyMapView(const QString &viewName);
+
         /** Select the HIPS Source catalog. */
         void slotHIPSSource();
 
+        /** Invoke the Views editor window */
+        void slotEditViews();
+
         /** Invoke the Field-of-View symbol editor window */
         void slotFOVEdit();
 
@@ -855,6 +868,7 @@ class KStars : public KXmlGuiWindow
 
         KActionMenu *colorActionMenu { nullptr };
         KActionMenu *fovActionMenu { nullptr };
+        KActionMenu *viewsActionMenu { nullptr };
         KActionMenu *hipsActionMenu { nullptr };
         KActionMenu *orientationActionMenu { nullptr };
 
@@ -907,6 +921,8 @@ class KStars : public KXmlGuiWindow
         QActionGroup *hipsGroup { nullptr };
         QActionGroup *telescopeGroup { nullptr };
         QActionGroup *domeGroup { nullptr };
+        QActionGroup *erectObserverCorrectionGroup { nullptr };
+        QActionGroup *viewsGroup { nullptr };
 
         bool DialogIsObsolete { false };
         bool StartClockRunning { false };
diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg
index 4729deb9cc..2edf07e4a8 100644
--- a/kstars/kstars.kcfg
+++ b/kstars/kstars.kcfg
@@ -811,10 +811,21 @@
          <whatsthis>Enable this if you want the sky map to be mirrored 
left-right, e.g. to match the view through an erecting prism.</whatsthis>
          <default>false</default>
       </entry>
-      <entry name="ErectObserverCorrection" type="Bool">
+      <entry name="ErectObserverCorrection" type="Enum">
          <label>Orients the sky-map to account for an erect observer at the 
eyepiece</label>
-         <whatsthis>Enable this if you are using your eye at the eyepiece in 
an altazimuth mounted Newtonian telescope. This accounts for the fact that the 
observer stands erect as the telescope moves up and down, so that the 
orientation of the sky map will track what is seen in your eyepiece once it is 
set up correctly.</whatsthis>
-         <default>false</default>
+         <whatsthis>Enable this if you are using your eye at the eyepiece in 
an altazimuth mounted Newtonian telescope. This accounts for the fact that the 
observer stands erect as the telescope moves up and down, so that the 
orientation of the sky map will track what is seen in your eyepiece once it is 
set up correctly. Choose the handedness of the correction according which side 
of the telescope the eyepiece appears when looking from the back of the 
telescope</whatsthis>
+        <choices>
+          <choice name="Off">
+            <label>Off</label>
+          </choice>
+          <choice name="Left">
+            <label>Left</label>
+          </choice>
+          <choice name="Right">
+            <label>Right</label>
+          </choice>
+        </choices>
+         <default>0</default> <!-- Off -->
       </entry>
       <entry name="ZoomScrollFactor" type="Double">
          <label>Zoom scroll sensitivity.</label>
diff --git a/kstars/kstarsactions.cpp b/kstars/kstarsactions.cpp
index c0f7c291fd..f3932f787a 100644
--- a/kstars/kstarsactions.cpp
+++ b/kstars/kstarsactions.cpp
@@ -20,6 +20,7 @@
 #include "dialogs/finddialog.h"
 #include "dialogs/focusdialog.h"
 #include "dialogs/fovdialog.h"
+#include "dialogs/viewsdialog.h"
 #include "dialogs/locationdialog.h"
 #include "dialogs/timedialog.h"
 #include "dialogs/catalogsdbui.h"
@@ -1705,9 +1706,7 @@ void KStars::slotCoordSys()
         actionCollection()
         ->action("down_orientation")
         ->setText(i18nc("Orientation of the sky map", "North &Down"));
-        actionCollection()
-        ->action("erect_observer_correction")
-        ->setEnabled(false);
+        erectObserverCorrectionGroup->setEnabled(false);
     }
     else
     {
@@ -1726,10 +1725,9 @@ void KStars::slotCoordSys()
         actionCollection()
         ->action("down_orientation")
         ->setText(i18nc("Orientation of the sky map", "Zenith &Down"));
-        actionCollection()
-        ->action("erect_observer_correction")
-        ->setEnabled(true);
+        erectObserverCorrectionGroup->setEnabled(true);
     }
+    actionCollection()->action("view:arbitrary")->setChecked(true);
     map()->forceUpdate();
 }
 
@@ -1743,18 +1741,12 @@ void KStars::slotSkyMapOrientation()
     {
         Options::setSkyRotation(180.0);
     }
-    else if (sender() == actionCollection()->action("mirror_skymap"))
-    {
-        ;
-    }
-    else
-    {
-        Q_ASSERT(false && "Unhandled orientation action");
-        qCWarning(KSTARS) << "Unhandled orientation action";
-    }
 
     
Options::setMirrorSkyMap(actionCollection()->action("mirror_skymap")->isChecked());
-    
Options::setErectObserverCorrection(actionCollection()->action("erect_observer_correction")->isChecked());
+    Options::setErectObserverCorrection(
+        
actionCollection()->action("erect_observer_correction_off")->isChecked() ? 0 : (
+            
actionCollection()->action("erect_observer_correction_left")->isChecked() ? 1 : 
2));
+    actionCollection()->action("view:arbitrary")->setChecked(true);
     map()->forceUpdate();
 }
 
@@ -1810,6 +1802,60 @@ void KStars::slotTargetSymbol(bool flag)
     map()->forceUpdate();
 }
 
+void KStars::slotApplySkyMapView(const QString &viewName)
+{
+
+    auto view = SkyMapViewManager::viewNamed(viewName);
+    if (!view)
+    {
+        qCWarning(KSTARS) << "View named " << viewName << " not found!";
+        return;
+    }
+
+    // FIXME: Ugly hack to update the menus correctly...
+    // we set the opposite coordinate system setting and call slotCoordSys to 
toggle
+    Options::setUseAltAz(!view->useAltAz);
+    slotCoordSys();
+
+    Options::setMirrorSkyMap(view->mirror);
+    
actionCollection()->action("mirror_skymap")->setChecked(Options::mirrorSkyMap());
+
+    int erectObserverCorrection = 0;
+    double viewAngle = view->viewAngle;
+    if (view->erectObserver && view->useAltAz)
+    {
+        if (viewAngle > 0.)
+        {
+            erectObserverCorrection = 1;
+            viewAngle -= 90.; // FIXME: Check
+        }
+        if (viewAngle < 0.)
+        {
+            erectObserverCorrection = 2;
+            viewAngle += 90.; // FIXME: Check
+        }
+    }
+    if (view->inverted)
+    {
+        viewAngle += 180.; // FIXME: Check
+    }
+
+    Options::setErectObserverCorrection(erectObserverCorrection);
+    Options::setSkyRotation(dms::reduce(viewAngle));
+    if (!std::isnan(view->fov))
+    {
+        Options::setZoomFactor(map()->width() / (3 * view->fov * 
dms::DegToRad));
+    }
+    repopulateOrientation(); // Update the menus
+    qCDebug(KSTARS) << "Alt/Az: " << Options::useAltAz()
+                    << "Mirror: " << Options::mirrorSkyMap()
+                    << "Rotation: " << Options::skyRotation()
+                    << "Erect Obs: " << Options::erectObserverCorrection()
+                    << "FOV: " << view->fov;
+    
actionCollection()->action(QString("view:%1").arg(viewName))->setChecked(true);
+    map()->forceUpdate();
+}
+
 void KStars::slotHIPSSource()
 {
     QAction *selectedAction = qobject_cast<QAction *>(sender());
@@ -1827,6 +1873,17 @@ void KStars::slotHIPSSource()
     map()->forceUpdate();
 }
 
+void KStars::slotEditViews()
+{
+    QPointer<ViewsDialog> viewsDialog = new ViewsDialog(this);
+    if (viewsDialog->exec() == QDialog::Accepted)
+    {
+        SkyMapViewManager::save();
+        repopulateViews();
+    }
+    delete viewsDialog;
+}
+
 void KStars::slotFOVEdit()
 {
     QPointer<FOVDialog> fovdlg = new FOVDialog(this);
diff --git a/kstars/kstarsinit.cpp b/kstars/kstarsinit.cpp
index 8774628ac8..0fbc6b3e90 100644
--- a/kstars/kstarsinit.cpp
+++ b/kstars/kstarsinit.cpp
@@ -377,6 +377,14 @@ void KStars::initActions()
     FOVManager::readFOVs();
     repopulateFOV();
 
+    //Add Views menu actions
+    viewsActionMenu = actionCollection()->add<KActionMenu>("views");
+    viewsActionMenu->setText(i18n("&Views"));
+    viewsActionMenu->setDelayed(false);
+    viewsActionMenu->setIcon(QIcon::fromTheme("text_rotation"));
+    SkyMapViewManager::readViews();
+    repopulateViews();
+
     //Add HIPS Sources actions
     hipsActionMenu = actionCollection()->add<KActionMenu>("hipssources");
     hipsActionMenu->setText(i18n("HiPS All Sky Overlay"));
@@ -746,13 +754,64 @@ void KStars::repopulateOrientation()
                          "This mode is selected automatically if you manually 
rotated the sky map using Shift + Drag mouse action, to inform you that the 
orientation is arbitrary")));
 
     orientationActionMenu->addSeparator();
-    QAction *erectObserverAction = newToggleAction(
-                                       actionCollection(), 
"erect_observer_correction",
-                                       i18nc("Orient sky map for an erect 
observer", "Erect observer correction"),
-                                       this, SLOT(slotSkyMapOrientation()));
-    erectObserverAction << ToolTip(i18nc("Orient sky map for an erect 
observer",
-                                         "Enable this mode if you are visually 
using a Newtonian telescope on an altazimuth mount. It will correct the 
orientation of the sky-map to account for the observer remaining erect as the 
telescope moves up and down, unlike a camera which would rotate with the 
telescope. This only makes sense in Horizontal Coordinate mode and is disabled 
when using Equatorial Coordinates. Typically makes sense to combine this with 
Zenith Down orientation."));
-    orientationActionMenu->addAction(erectObserverAction);
+
+    orientationActionMenu->addAction(
+        actionCollection()->addAction(
+            "erect_observer_correction_off", this, 
SLOT(slotSkyMapOrientation()))
+        << i18nc("Do not adjust the orientation of the sky map for an erect 
observer", "No correction")
+        << AddToGroup(erectObserverCorrectionGroup)
+        << Checked(Options::erectObserverCorrection() == 0)
+        << ToolTip(i18nc("Orientation of the sky map",
+                         "Select this if you are using a camera on the 
telescope, or have the sky map display mounted on your telescope")));
+
+    orientationActionMenu->addAction(
+        actionCollection()->addAction(
+            "erect_observer_correction_left", this, 
SLOT(slotSkyMapOrientation()))
+        << i18nc("Adjust the orientation of the sky map for an erect observer, 
left-handed telescope",
+                 "Erect observer correction, left-handed")
+        << AddToGroup(erectObserverCorrectionGroup)
+        << Checked(Options::erectObserverCorrection() == 1)
+        << ToolTip(i18nc("Orientation of the sky map",
+                         "Select this if you are visually observing using a 
Dobsonian telescope which has the focuser appearing on the left side when 
looking up the telescope tube. This feature will correct the orientation of the 
sky-map to account for the observer remaining erect as the telescope moves up 
and down, unlike a camera which would rotate with the telescope. Typically 
makes sense to combine this with Zenith Down orientation.")));
+
+    orientationActionMenu->addAction(
+        actionCollection()->addAction(
+            "erect_observer_correction_right", this, 
SLOT(slotSkyMapOrientation()))
+        << i18nc("Adjust the orientation of the sky map for an erect observer, 
left-handed telescope",
+                 "Erect observer correction, right-handed")
+        << AddToGroup(erectObserverCorrectionGroup)
+        << Checked(Options::erectObserverCorrection() == 2)
+        << ToolTip(i18nc("Orientation of the sky map",
+                         "Select this if you are visually observing using a 
Dobsonian telescope which has the focuser appearing on the right side when 
looking up the telescope tube. This feature will correct the orientation of the 
sky-map to account for the observer remaining erect as the telescope moves up 
and down, unlike a camera which would rotate with the telescope. Typically 
makes sense to combine this with Zenith Down orientation.")));
+
+}
+
+void KStars::repopulateViews()
+{
+    viewsActionMenu->menu()->clear();
+
+    QList<QAction*> actions = viewsGroup->actions();
+    for (auto &action : actions)
+        viewsGroup->removeAction(action);
+
+    for (const auto &view : SkyMapViewManager::getViews())
+    {
+        QAction* action = 
actionCollection()->addAction(QString("view:%1").arg(view.name), this, [ = ]()
+        {
+            slotApplySkyMapView(view.name);
+        })
+                << view.name << AddToGroup(viewsGroup) << Checked(false);
+        viewsActionMenu->addAction(action);
+        action->setData(view.name);
+    }
+    viewsActionMenu->addAction(
+        actionCollection()->addAction("view:arbitrary")
+        << i18nc("Arbitrary Sky Map View", "Arbitrary") << 
AddToGroup(viewsGroup) << Checked(true)); // FIXME
+
+    // Add menu bottom
+    QAction *ka = actionCollection()->addAction("edit_views", this, 
SLOT(slotEditViews())) << i18n("Edit Views...");
+    viewsActionMenu->addSeparator();
+    viewsActionMenu->addAction(ka);
 }
 
 void KStars::repopulateFOV()
diff --git a/kstars/skycomponents/hipscomponent.cpp 
b/kstars/skycomponents/hipscomponent.cpp
index a42d444dc8..04f99cc172 100644
--- a/kstars/skycomponents/hipscomponent.cpp
+++ b/kstars/skycomponents/hipscomponent.cpp
@@ -44,6 +44,7 @@ void HIPSComponent::draw(SkyPainter *skyp)
                         view.height == m_previousViewParams.height &&
                         view.zoomFactor == m_previousViewParams.zoomFactor &&
                         view.rotationAngle == 
m_previousViewParams.rotationAngle &&
+                        view.mirror == m_previousViewParams.mirror &&
                         view.useAltAz == m_previousViewParams.useAltAz
                     );
     if (sameView && Options::isTracking() && SkyMap::IsFocused())
diff --git a/kstars/skymap.cpp b/kstars/skymap.cpp
index b0a48ecb99..955211a56a 100644
--- a/kstars/skymap.cpp
+++ b/kstars/skymap.cpp
@@ -56,6 +56,9 @@
 #include <QClipboard>
 #include <QInputDialog>
 #include <QDesktopServices>
+#include <QPropertyAnimation>
+#include <QGraphicsOpacityEffect>
+#include <QGraphicsSimpleTextItem>
 
 #include <QProcess>
 #include <QFileDialog>
@@ -722,8 +725,8 @@ void SkyMap::slotEndRulerMode()
                                     ((f->sizeX() >= f->sizeY() && f->sizeY() 
!= 0) ? f->sizeY() : f->sizeX()));
             }
             fov = nameToFovMap[QInputDialog::getItem(this, i18n("Star Hopper: 
Choose a field-of-view"),
-                                                           i18n("FOV to use 
for star hopping:"), nameToFovMap.keys(), 0,
-                                                           false, &ok)];
+                               i18n("FOV to use for star hopping:"), 
nameToFovMap.keys(), 0,
+                               false, &ok)];
         }
         else
         {
@@ -1206,8 +1209,14 @@ dms SkyMap::determineSkyRotation()
     // orientation of the field. This would not apply to a CCD camera
     // plugged into the same telescope, since the CCD would rotate as
     // seen from the ground when the telescope moves in altitude.
-    return dms(Options::skyRotation() - (
-                   (Options::erectObserverCorrection() && Options::useAltAz()) 
? focus()->alt().Degrees() : 0.0));
+
+    double erectObserverCorrection = 0.;
+    if (Options::useAltAz() && Options::erectObserverCorrection() > 0)
+    {
+        erectObserverCorrection = (Options::erectObserverCorrection() == 1) ? 
focus()->alt().Degrees() : -focus()->alt().Degrees();
+    }
+
+    return dms(Options::skyRotation() + erectObserverCorrection);
 }
 
 void SkyMap::slotSetSkyRotation(double angle)
@@ -1229,6 +1238,7 @@ void SkyMap::slotSetSkyRotation(double angle)
         {
             
kstars->actionCollection()->action("arbitrary_orientation")->setChecked(true);
         }
+        kstars->actionCollection()->action("view:arbitrary")->setChecked(true);
     }
     forceUpdate();
 }
@@ -1363,3 +1373,32 @@ void SkyMap::slotStartXplanetViewer()
     else
         new XPlanetImageViewer(i18n("Saturn"), this);
 }
+
+void SkyMap::slotDisplayFadingText(const QString &text)
+{
+    QLabel *fadingLabel = new QLabel(this);
+    fadingLabel->setText(text);
+    QFont font = fadingLabel->font();
+    QPalette palette = fadingLabel->palette();
+    font.setPointSize(32);
+    palette.setColor(fadingLabel->foregroundRole(), 
KStarsData::Instance()->colorScheme()->colorNamed("BoxTextColor"));
+    QColor backgroundColor = 
KStarsData::Instance()->colorScheme()->colorNamed("BoxBGColor");
+    backgroundColor.setAlpha(192);
+    palette.setColor(fadingLabel->backgroundRole(), backgroundColor);
+    fadingLabel->setFont(font);
+    fadingLabel->setAutoFillBackground(true);
+    fadingLabel->setPalette(palette);
+    fadingLabel->setAlignment(Qt::AlignCenter);
+    fadingLabel->adjustSize();
+    fadingLabel->move(QPoint((width() - fadingLabel->width()) / 2, (0.75 * 
height() - fadingLabel->height() / 2)));
+    QGraphicsOpacityEffect* fadingEffect = new 
QGraphicsOpacityEffect(fadingLabel);
+    fadingLabel->setGraphicsEffect(fadingEffect);
+    fadingLabel->show();
+
+    QPropertyAnimation* animation = new QPropertyAnimation(fadingEffect, 
"opacity", fadingLabel);
+    animation->setDuration(1500);
+    animation->setStartValue(1.0);
+    animation->setEndValue(0.0);
+    connect(animation, &QPropertyAnimation::finished, fadingLabel, 
&QLabel::deleteLater);
+    animation->start();
+}
diff --git a/kstars/skymap.h b/kstars/skymap.h
index 65542107ea..19c1079e57 100644
--- a/kstars/skymap.h
+++ b/kstars/skymap.h
@@ -480,6 +480,9 @@ class SkyMap : public QGraphicsView
              */
         void slotRemovePlanetTrail();
 
+        /** @short Render a fading text label on the screen to flash 
information */
+        void slotDisplayFadingText(const QString &text);
+
         /** Checks whether the timestep exceeds a threshold value.  If so, sets
              * ClockSlewing=true and sets the SimClock to ManualMode.
              */
diff --git a/kstars/skymapevents.cpp b/kstars/skymapevents.cpp
index ff53f7e348..ae2eb76bd1 100644
--- a/kstars/skymapevents.cpp
+++ b/kstars/skymapevents.cpp
@@ -363,13 +363,27 @@ void SkyMap::keyPressEvent(QKeyEvent *e)
 
         case Qt::Key_PageUp:
         {
-            KStars::Instance()->selectPreviousFov();
+            if (shiftPressed)
+            {
+                KStars::Instance()->selectPreviousView();
+            }
+            else
+            {
+                KStars::Instance()->selectPreviousFov();
+            }
             break;
         }
 
         case Qt::Key_PageDown:
         {
-            KStars::Instance()->selectNextFov();
+            if (shiftPressed)
+            {
+                KStars::Instance()->selectNextView();
+            }
+            else
+            {
+                KStars::Instance()->selectNextFov();
+            }
             break;
         }
 
@@ -409,7 +423,7 @@ bool SkyMap::event(QEvent *event)
     {
         QGestureEvent* gestureEvent = static_cast<QGestureEvent*>(event);
 
-        if (QPinchGesture *pinch = 
static_cast<QPinchGesture*>(gestureEvent->gesture(Qt::PinchGesture)))
+        if (QPinchGesture *pinch = static_cast<QPinchGesture * 
>(gestureEvent->gesture(Qt::PinchGesture)))
         {
             QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags();
 
@@ -435,7 +449,7 @@ bool SkyMap::event(QEvent *event)
                 }
             }
         }
-        if (QTapAndHoldGesture *tapAndHold = 
static_cast<QTapAndHoldGesture*>(gestureEvent->gesture(Qt::TapAndHoldGesture)))
+        if (QTapAndHoldGesture *tapAndHold = static_cast<QTapAndHoldGesture * 
>(gestureEvent->gesture(Qt::TapAndHoldGesture)))
         {
             m_tapAndHoldMode = true;
             if (tapAndHold->state() == Qt::GestureFinished)
diff --git a/kstars/widgets/unitspinboxwidget.cpp 
b/kstars/widgets/unitspinboxwidget.cpp
index 6ca2aa4ccc..c8164927b1 100644
--- a/kstars/widgets/unitspinboxwidget.cpp
+++ b/kstars/widgets/unitspinboxwidget.cpp
@@ -4,6 +4,7 @@
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 #include "unitspinboxwidget.h"
+#include<cmath>
 
 UnitSpinBoxWidget::UnitSpinBoxWidget(QWidget *parent) : QWidget(parent), 
ui(new Ui::UnitSpinBoxWidget)
 {
@@ -34,3 +35,26 @@ double UnitSpinBoxWidget::value() const
     double value            = doubleSpinBox->value();
     return value * conversionFactor;
 }
+
+void UnitSpinBoxWidget::setValue(const double value)
+{
+    if (value < 1e-20 && value > -1e-20)
+    {
+        // Practically zero
+        doubleSpinBox->setValue(value);
+        return;
+    }
+    std::vector<double> diffs;
+    for (int index = 0; index < comboBox->count(); ++index)
+    {
+        QVariant qv = comboBox->itemData(index);
+        double conversionFactor = qv.value<double>();
+        diffs.push_back(std::abs(std::abs(value) / conversionFactor - 1.));
+    }
+    auto it = std::min_element(diffs.cbegin(), diffs.cend());
+    int index = std::distance(diffs.cbegin(), it);
+    comboBox->setCurrentIndex(index);
+    QVariant qv = comboBox->itemData(index);
+    double conversionFactor = qv.value<double>();
+    doubleSpinBox->setValue(value / conversionFactor);
+}
diff --git a/kstars/widgets/unitspinboxwidget.h 
b/kstars/widgets/unitspinboxwidget.h
index 13ed1a9f15..97664b4eac 100644
--- a/kstars/widgets/unitspinboxwidget.h
+++ b/kstars/widgets/unitspinboxwidget.h
@@ -18,28 +18,49 @@
  */
 class UnitSpinBoxWidget : public QWidget
 {
-    Q_OBJECT
+        Q_OBJECT
 
-  public:
-    explicit UnitSpinBoxWidget(QWidget *parent = nullptr);
-    ~UnitSpinBoxWidget() override;
+    public:
+        QComboBox *comboBox;
+        QDoubleSpinBox *doubleSpinBox;
 
-    /**
+        explicit UnitSpinBoxWidget(QWidget *parent = nullptr);
+        ~UnitSpinBoxWidget() override;
+
+        /**
          * @brief addUnit Adds a item to the combo box
          * @param unitName The name of the unit to be displayed
          * @param conversionFactor The factor the value of a unit must be 
multiplied by
          */
-    void addUnit(const QString &unitName, double conversionFactor);
+        void addUnit(const QString &unitName, double conversionFactor);
+
+        /** @return whether the widget is enabled */
+        inline bool enabled()
+        {
+            Q_ASSERT(comboBox->isEnabled() == doubleSpinBox->isEnabled());
+            return doubleSpinBox->isEnabled();
+        }
 
-    /**
-         * @brief value Returns value upon conversion
+        /** @brief value Returns value upon conversion */
+        double value() const;
+
+    public slots:
+        /**
+         * @brief Sets the given value
+         * @param value The value to set
+         * @note Automatically optimizes the display to use the best unit for 
the given value
          */
-    double value() const;
+        void setValue(const double value);
+
+        /** @brief Enables the widget */
+        void setEnabled(bool enabled)
+        {
+            comboBox->setEnabled(enabled);
+            doubleSpinBox->setEnabled(enabled);
+        }
 
-  private:
-    Ui::UnitSpinBoxWidget *ui;
-    QComboBox *comboBox;
-    QDoubleSpinBox *doubleSpinBox;
+    private:
+        Ui::UnitSpinBoxWidget *ui;
 };
 
 #endif // UNITSPINBOXWIDGET_H
diff --git a/kstars/widgets/unitspinboxwidget.ui 
b/kstars/widgets/unitspinboxwidget.ui
index dea56360a9..b688e28ac5 100644
--- a/kstars/widgets/unitspinboxwidget.ui
+++ b/kstars/widgets/unitspinboxwidget.ui
@@ -7,31 +7,34 @@
     <x>0</x>
     <y>0</y>
     <width>179</width>
-    <height>44</height>
+    <height>32</height>
    </rect>
   </property>
-  <widget class="QComboBox" name="comboBox">
-   <property name="geometry">
-    <rect>
-     <x>90</x>
-     <y>10</y>
-     <width>80</width>
-     <height>25</height>
-    </rect>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <property name="spacing">
+    <number>0</number>
    </property>
-  </widget>
-  <widget class="QDoubleSpinBox" name="doubleSpinBox">
-   <property name="geometry">
-    <rect>
-     <x>10</x>
-     <y>10</y>
-     <width>66</width>
-     <height>26</height>
-    </rect>
+   <property name="leftMargin">
+    <number>0</number>
    </property>
-  </widget>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QDoubleSpinBox" name="doubleSpinBox"/>
+   </item>
+   <item>
+    <widget class="QComboBox" name="comboBox"/>
+   </item>
+  </layout>
  </widget>
- <layoutdefault spacing="6" margin="11"/>
+ <layoutdefault spacing="0" margin="0"/>
  <resources/>
  <connections/>
 </ui>

Reply via email to