xrandroll/xrandroll/main.py

342 lines
13 KiB
Python

import os
import shlex
import subprocess
import sys
import parse
from PySide2.QtCore import QFile, QObject, QTimer
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QGraphicsScene, QLabel
from . import xrandr
from .monitor_item import MonitorItem
class Window(QObject):
def __init__(self, ui):
super().__init__()
self.ui = ui
ui.show()
self.ui.setWindowTitle("Display Configuration")
self.ui.screenCombo.currentTextChanged.connect(self.monitor_selected)
self.ui.replicaOf.currentTextChanged.connect(self.replica_changed)
self.ui.orientationCombo.currentIndexChanged.connect(self.orientation_changed)
self.get_xrandr_info()
self.fill_ui()
self.ui.horizontalScale.valueChanged.connect(self.scale_changed)
self.ui.verticalScale.valueChanged.connect(self.scale_changed)
self.ui.modes.currentTextChanged.connect(self.mode_changed)
self.ui.applyButton.clicked.connect(self.do_apply)
self.ui.okButton.clicked.connect(self.do_ok)
self.ui.resetButton.clicked.connect(self.do_reset)
self.ui.cancelButton.clicked.connect(self.ui.reject)
self.ui.scaleModeCombo.currentTextChanged.connect(self.scale_mode_changed)
self.ui.primary.stateChanged.connect(self.primary_changed)
self.ui.enabled.stateChanged.connect(self.enabled_changed)
self.pos_label = QLabel(self.ui.sceneView)
self.pos_label.move(5, 5)
def enabled_changed(self):
mon = self.ui.screenCombo.currentText()
enabled = self.ui.enabled.isChecked()
print(f"Setting {mon} enabled status to {enabled}")
monitor = self.screen.monitors[mon]
monitor.enabled = enabled
if enabled and not monitor.get_current_mode():
# Choose a mode
self.ui.modes.setCurrentText(str(monitor.get_preferred_mode()))
self.mode_changed()
self.screen.update_replica_of()
for mon in self.screen.monitors.values():
mon.item.update_visuals(mon)
self.adjust_view()
def primary_changed(self):
mon_name = self.ui.screenCombo.currentText()
primary = self.ui.primary.isChecked()
if primary:
self.screen.set_primary(mon_name)
else:
self.screen.set_primary("foobar") # no primary
for monitor in self.screen.monitors.values():
monitor.item.update_visuals(monitor)
def scale_mode_changed(self):
mon = self.ui.screenCombo.currentText()
scale_mode = self.ui.scaleModeCombo.currentText()
print(f"Set {mon} scale mode to {scale_mode}")
if scale_mode == "Manual":
self.ui.horizontalScale.setEnabled(True)
self.ui.verticalScale.setEnabled(True)
try:
self.ui.horizontalScale.valueChanged.disconnect(
self.ui.verticalScale.setValue
)
except RuntimeError: # Not connected
pass
elif scale_mode == "Disabled (1x1)":
self.ui.verticalScale.setEnabled(False)
self.ui.horizontalScale.setEnabled(False)
self.ui.horizontalScale.setValue(1000)
self.ui.verticalScale.setValue(1000)
try:
self.ui.horizontalScale.valueChanged.disconnect(
self.ui.verticalScale.setValue
)
except RuntimeError: # Not connected
pass
elif scale_mode == "Automatic: physical dimensions":
# Calculate scale factors so that the logical pixels will be the same
# size as in the primary window
if self.ui.primary.isChecked():
print("Has no effect on primary display.")
return
# Find the primary monitor
primary = self.screen.get_primary()
if not primary:
print("Oops, no primary!")
return
monitor = self.screen.monitors[mon]
prim_density_x = primary.res_x / primary.w_in_mm
prim_density_y = primary.res_y / primary.h_in_mm
dens_x = monitor.res_x / monitor.w_in_mm
dens_y = monitor.res_y / monitor.h_in_mm
try:
self.ui.horizontalScale.valueChanged.disconnect(
self.ui.verticalScale.setValue
)
except RuntimeError: # Not connected
pass
self.ui.horizontalScale.setEnabled(False)
self.ui.verticalScale.setEnabled(False)
self.ui.horizontalScale.setValue(prim_density_x / dens_x * 1000)
self.ui.verticalScale.setValue(prim_density_y / dens_y * 1000)
elif scale_mode == "Manual, same in both dimensions":
self.ui.horizontalScale.setEnabled(True)
self.ui.verticalScale.setEnabled(False)
self.ui.horizontalScale.valueChanged.connect(self.ui.verticalScale.setValue)
self.ui.verticalScale.setValue(self.ui.horizontalScale.value())
def replica_changed(self):
mon_name = self.ui.screenCombo.currentText()
replicate = self.ui.replicaOf.currentText()
mon = self.screen.monitors[mon_name]
if replicate in ("None", "", None):
print(f"Making {mon_name} NOT a replica")
mon.pos_x += 300
else:
replicate = self.screen.monitors[replicate]
print(f"Making {mon_name} a replica of {replicate}")
# Making a replica implies:
# Set the same position
mon.pos_x = replicate.pos_x
mon.pos_y = replicate.pos_y
# Set the same mode if possible
matching_mode = mon.get_matching_mode(replicate.get_current_mode())
if matching_mode:
mon.set_current_mode(matching_mode.name)
else:
# Keep the current mode, and change scaling so it
# has the same effective size as the desired mode
c_mode = mon.get_current_mode()
mod_x, mod_y = c_mode.res_x, c_mode.res_y
r_mode = replicate.get_current_mode
target_x, target_y = r_mode.res_x, r_mode.res_y
scale_x = 1000 * target_x / mod_x
scale_y = 1000 * target_y / mod_y
self.ui.horizontalScale.setValue(scale_x)
self.ui.verticalScale.setValue(scale_y)
self.screen.update_replica_of()
for mon in self.screen.monitors.values():
mon.item.update_visuals(mon)
def run(self, commands):
for i, cmd in enumerate(commands, 1):
print(f"Running {cmd} [{i}/{len(commands)}]")
subprocess.check_call(shlex.split(cmd))
def do_reset(self):
self.run(self.reset_screen.generate())
self.fill_ui()
def do_ok(self):
self.do_apply()
self.ui.accept()
def do_apply(self):
cli = self.screen.generate()
self.run(cli)
def fill_ui(self):
"""Configure UI out of our screen data."""
self.scene = QGraphicsScene(self)
self.ui.sceneView.setScene(self.scene)
self.ui.screenCombo.clear()
for name, monitor in self.screen.monitors.items():
self.ui.screenCombo.addItem(name)
mon_item = MonitorItem(
data=monitor,
window=self,
name=name,
)
self.scene.addItem(mon_item)
monitor.item = mon_item
self.ui.screenCombo.setCurrentText(self.screen.choose_a_monitor())
self.adjust_view()
# self.scale_changed() # Trigger scale labels update
def orientation_changed(self):
mon_name = self.ui.screenCombo.currentText()
orientation = self.ui.orientationCombo.currentText().split()[0].lower()
self.screen.monitors[mon_name].orientation = orientation
self.mode_changed()
def mode_changed(self):
mon = self.ui.screenCombo.currentText()
mode = parse.search("({mode_name})", self.ui.modes.currentText())["mode_name"]
if not mode:
return
print(f"Changing {mon} to {mode}")
monitor = self.screen.monitors[mon]
monitor.set_current_mode(mode)
mode_x, mode_y = (
monitor.get_current_mode().res_x,
monitor.get_current_mode().res_y,
)
# use resolution via scaling
if monitor.orientation in ("normal", "inverted"):
monitor.res_x = int(mode_x * self.ui.horizontalScale.value() / 1000)
monitor.res_y = int(mode_y * self.ui.verticalScale.value() / 1000)
else:
monitor.res_x = int(mode_y * self.ui.horizontalScale.value() / 1000)
monitor.res_y = int(mode_x * self.ui.verticalScale.value() / 1000)
monitor.item.update_visuals(monitor)
def show_pos(self, x, y):
self.pos_label.setText(f"{x},{y}")
self.pos_label.resize(self.pos_label.sizeHint())
def monitor_moved(self):
"Update screen with new monitor positions"
for mon in self.screen.monitors.values():
item = mon.item
mon.pos_x = item.x()
mon.pos_y = item.y()
self.screen.update_replica_of()
for mon in self.screen.monitors.values():
mon.item.update_visuals(mon)
# Adjust view a little later
QTimer.singleShot(0, self.adjust_view)
def possible_snaps(self, name):
"""Return two lists of values to which the x and y position
of monitor "name" could snap to."""
snaps_x = []
snaps_y = []
for output, monitor in self.screen.monitors.items():
if output == name:
continue
else:
mode = monitor.get_current_mode()
mod_x, mod_y = mode.res_x, mode.res_y
snaps_x.append(monitor.pos_x)
snaps_x.append(monitor.pos_x + mod_x)
snaps_y.append(monitor.pos_y)
snaps_y.append(monitor.pos_y + mod_y)
return snaps_x, snaps_y
def adjust_view(self):
print("Adjusting view")
self.ui.sceneView.resetTransform()
self.ui.sceneView.ensureVisible(self.scene.sceneRect(), 100, 100)
try:
scale_factor = 0.8 * min(
self.ui.sceneView.width() / self.scene.sceneRect().width(),
self.ui.sceneView.height() / self.scene.sceneRect().height(),
)
self.ui.sceneView.scale(scale_factor, scale_factor)
except ZeroDivisionError:
# Don't worry
pass
def get_xrandr_info(self):
_xrandr_data = xrandr.read_data()
self.screen = xrandr.parse_data(_xrandr_data)
self.screen.update_replica_of()
self.reset_screen = xrandr.parse_data(_xrandr_data)
def monitor_selected(self, name):
if not name:
return
# needed so we don't flip through all modes as they are added
self.ui.modes.blockSignals(True)
self.ui.primary.blockSignals(True)
# Show modes
self.ui.modes.clear()
monitor = self.screen.monitors[name]
for _, mode in monitor.modes.items():
self.ui.modes.addItem(str(mode))
mode = monitor.get_current_mode()
self.ui.modes.setCurrentText(str(mode))
if monitor.orientation in ("normal", "inverted"):
h_scale = monitor.res_x / mode.res_x
v_scale = monitor.res_y / mode.res_y
else:
h_scale = monitor.res_y / mode.res_x
v_scale = monitor.res_x / mode.res_y
self.ui.horizontalScale.setValue(h_scale * 1000)
self.ui.verticalScale.setValue(v_scale * 1000)
self.ui.primary.setChecked(monitor.primary)
self.ui.enabled.setChecked(monitor.enabled)
self.ui.orientationCombo.setCurrentText(monitor.orientation)
self.ui.replicaOf.clear()
self.ui.replicaOf.addItem("None")
for mon in self.screen.monitors:
if mon != name:
self.ui.replicaOf.addItem(mon)
if mon in self.screen.monitors[name].replica_of:
self.ui.replicaOf.setCurrentText(mon)
self.ui.modes.blockSignals(False)
self.ui.primary.blockSignals(False)
guessed_scale_mode = monitor.guess_scale_mode()
self.ui.scaleModeCombo.setCurrentText(guessed_scale_mode)
self.scale_mode_changed()
def scale_changed(self):
self.ui.horizontalScaleLabel.setText(
f"{int(self.ui.horizontalScale.value()/10)}%"
)
self.ui.verticalScaleLabel.setText(f"{int(self.ui.verticalScale.value()/10)}%")
self.mode_changed() # Not really, but it's the same thing
def main():
app = QApplication(sys.argv)
ui_file = QFile(os.path.join(os.path.dirname(__file__), "main.ui"))
ui_file.open(QFile.ReadOnly)
loader = QUiLoader()
Window(loader.load(ui_file))
sys.exit(app.exec_())
if __name__ == "__main__":
main()