Repository: napari/napari

Issue:
Range slider does not render in proper place in multi monitor environment on linux
## 🐛 Bug

![20200604_165422_HDR](https://user-images.githubusercontent.com/3826210/83773561-f6849780-a684-11ea-959e-9ddec2d620bd.jpg)


On my system (Ubuntu 19.10 with gnome shell and nvidia drivers) when napari is not on the most left screen then range slider is not rendered in proper place. 

<!-- A clear and concise description of what the bug is. -->

## To Reproduce

Steps to reproduce the behaviour:

1. Start napari on mulit screeen computer
2. Move window from left screen
3. Click right on range small slider to open big

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior
Slider should be placed properly (inside napari window)
<!-- A clear and concise description of what you expected to happen. -->

## Environment
napari: 0.3.5.dev3+ge9df14f6
Platform: Linux-5.3.0-55-generic-x86_64-with-debian-buster-sid
Python: 3.7.2 (default, Aug 1 2019, 23:29:28) [GCC 8.3.0]
Qt: 5.14.2
PySide2: 5.14.2.2
NumPy: 1.18.2
SciPy: 1.4.1
Dask: 2.12.0
VisPy: 0.6.4

GL version: 4.6.0 NVIDIA 390.132
MAX_TEXTURE_SIZE: 32768

Plugins:
- napari-plugin-engine: 0.1.5
- napari-plugins: 0.1.0
- svg: 0.1.3
 - Please copy and paste the information at napari info option in help menubar here:

 - Any other relevant information:

Ubuntu 19.10, Gnome Shell with nvidia drivers on 3 screen configuration (FullHD, WQHD, FullHD)  

## Additional context

<!-- Add any other context about the problem here. -->
Opening this view for range slider block print screen.


Discussion:
Same on two monitor Linux configuration (Ubuntu 18.04 with gnome shell).
thanks @Czaki, that does look bad.  Very hard for me to test as I don't have that setup... We would be very grateful for any debugging and/or PRs you can submit on this
@tlambert03 I will be happy to create PR, but If you can provide any suggestion where in the code this slider is created and where have position calculated. It go through napari event system so it is hard to trace. 
Sure, the handling of the right-click event on the clims slider starts here:

https://github.com/napari/napari/blob/e9df14f6c6f09641faa0e2b41f57a0d55632f7f0/napari/_qt/layers/qt_image_base_layer.py#L107-L116

the `self.clim_pop` object is an instance of [`QRangeSliderPopup`](https://github.com/napari/napari/blob/master/napari/_qt/qt_range_slider_popup.py#L89), which is subclassed from [`QtPopup`](https://github.com/napari/napari/blob/e9df14f6c6f09641faa0e2b41f57a0d55632f7f0/napari/_qt/qt_modal.py#L6)... and the `self.clim_pop.show_at()` call at the bottom of that code block is the key part.  The `show_at` method is [in `QtPopup`](https://github.com/napari/napari/blob/e9df14f6c6f09641faa0e2b41f57a0d55632f7f0/napari/_qt/qt_modal.py#L62).  What you will likely need to fix is the logic in these lines:

https://github.com/napari/napari/blob/e9df14f6c6f09641faa0e2b41f57a0d55632f7f0/napari/_qt/qt_modal.py#L125-L134

Thanks, I can also confirm this bug on macos 10.14
And looking on code this should happen on any multi monitor system. I will work to fix this. 
@tlambert03 
Why this logic is introduced? Range slider is aligned to napari window. (on first screen). So what is the motivation to move it? User can move window.  
the discussion was around how to handle an undocked layer control panel.  which may or may not be on the same monitor as the viewer... see https://github.com/napari/napari/pull/869 for discussion.
(sorry, I should have linked that the first time... couldn't find it at first!)
feel free to change any logic you need, the main _behavior_ we were after is:
1. If the layer control is docked (has a parent), then it should be shown relative to the parent window.
2. if the layer control is undocked, it should be shown relative to the undocked window.
3. it should always be some minimum width (650 here), even if the parent window is narrower.
4. if a (narrow) undocked control panel is close enough to the edge of the screen such that centering the popup on parent would make some of the widget go off the screen, then it should be pinned to the edge of the screen. 

Resolving patch:
diff --git a/napari/_qt/qt_modal.py b/napari/_qt/qt_modal.py
--- a/napari/_qt/qt_modal.py
+++ b/napari/_qt/qt_modal.py
@@ -1,4 +1,4 @@
-from qtpy.QtCore import QPoint, Qt
+from qtpy.QtCore import QPoint, Qt, QRect
 from qtpy.QtGui import QCursor, QGuiApplication
 from qtpy.QtWidgets import QDialog, QFrame, QVBoxLayout
 
@@ -119,18 +119,31 @@ def show_at(self, position='top', *, win_ratio=0.9, min_length=0):
         elif isinstance(position, (tuple, list)):
             assert len(position) == 4, '`position` argument must have length 4'
             left, top, width, height = position
+        else:
+            raise ValueError(f"Wrong type of position {position}")
 
         # necessary for transparent round corners
         self.resize(self.sizeHint())
         # make sure the popup is completely on the screen
         # In Qt ≥5.10 we can use screenAt to know which monitor the mouse is on
-        if hasattr(QGuiApplication, 'screenAt'):
-            screen_size = QGuiApplication.screenAt(QCursor.pos()).size()
+
+        if hasattr(QGuiApplication, "screenAt"):
+            screen_geometry: QRect = QGuiApplication.screenAt(
+                QCursor.pos()
+            ).geometry()
         else:
-            # otherwise we just use the size of the first monitor
-            screen_size = QGuiApplication.screens()[0].size()
-        left = max(min(screen_size.width() - width, left), 0)
-        top = max(min(screen_size.height() - height, top), 0)
+            # This widget is deprecated since Qt 5.11
+            from qtpy.QtWidgets import QDesktopWidget
+
+            screen_num = QDesktopWidget().screenNumber(QCursor.pos())
+            screen_geometry = QGuiApplication.screens()[screen_num].geometry()
+
+        left = max(
+            min(screen_geometry.right() - width, left), screen_geometry.left()
+        )
+        top = max(
+            min(screen_geometry.bottom() - height, top), screen_geometry.top()
+        )
         self.setGeometry(left, top, width, height)
         self.show()
 


Test patch: