xrandroll/main.py

278 lines
9.7 KiB
Python
Raw Normal View History

2020-01-31 18:54:01 +00:00
from copy import deepcopy
2020-01-31 22:17:50 +00:00
import shlex
2020-01-31 16:46:06 +00:00
import subprocess
import sys
2020-01-31 22:17:50 +00:00
from PySide2.QtCore import QFile, QObject
2020-01-31 16:46:06 +00:00
from PySide2.QtUiTools import QUiLoader
2020-01-31 17:27:59 +00:00
from PySide2.QtWidgets import QApplication, QGraphicsScene
from monitor_item import MonitorItem
2020-01-31 16:46:06 +00:00
2020-01-31 21:18:24 +00:00
def gen_xrandr_from_data(data):
"""Takes monitor data and generates a xrandr command line."""
2020-01-31 22:17:50 +00:00
cli = ["xrandr"]
2020-01-31 21:18:24 +00:00
for name, mon in data.items():
2020-01-31 22:17:50 +00:00
cli.append(f"--output {name}")
2020-01-31 21:18:24 +00:00
cli.append(f'--pos {int(mon["pos_x"])}x{int(mon["pos_y"])}')
cli.append(f'--mode {mon["current_mode"]}')
2020-01-31 22:17:50 +00:00
mod_x, mod_y = [int(n) for n in mon["current_mode"].split("x")]
2020-01-31 23:55:28 +00:00
if mon["orientation"] in (1,3):
mod_x, mod_y = mod_y, mod_x
2020-01-31 21:18:24 +00:00
cli.append(f'--scale {mon["res_x"]/mod_x}x{mon["res_y"]/mod_y}')
2020-01-31 23:42:38 +00:00
cli.append(f"--rotate {['normal', 'left', 'inverted', 'right'][mon['orientation']]}")
2020-01-31 22:17:50 +00:00
if mon["primary"]:
cli.append("--primary")
if not mon["enabled"]:
cli.append("--off")
return " ".join(cli)
2020-01-31 21:18:24 +00:00
2020-01-31 16:46:06 +00:00
def parse_monitor(line):
parts = line.split()
name = parts[0]
primary = "primary" in parts
2020-01-31 18:57:02 +00:00
if "+" in line: # Is enabled
2020-01-31 18:54:01 +00:00
enabled = True
res_x, res_y = [p for p in parts if "x" in p][0].split("+")[0].split("x")
pos_x, pos_y = [p for p in parts if "x" in p][0].split("+")[1:]
w_in_mm, h_in_mm = [p.split("mm")[0] for p in parts if p.endswith("mm")]
else:
enabled = False
res_x = res_y = pos_x = pos_y = w_in_mm = h_in_mm = 0
2020-01-31 23:21:43 +00:00
left_side = line.split(' (normal left inverted ')[0]
orientation = 0
if 'left' in left_side:
orientation = 1
elif 'inverted' in left_side:
orientation = 2
elif 'right' in left_side:
orientation = 3
2020-01-31 16:46:06 +00:00
return (
name,
primary,
int(res_x),
int(res_y),
int(w_in_mm),
int(h_in_mm),
int(pos_x),
int(pos_y),
2020-01-31 18:57:02 +00:00
enabled,
2020-01-31 23:21:43 +00:00
orientation,
2020-01-31 16:46:06 +00:00
)
2020-01-31 19:16:02 +00:00
def is_replica_of(a, b):
"""Return True if monitor a is a replica of b.
2020-01-31 19:56:38 +00:00
2020-01-31 19:16:02 +00:00
Replica means same resolution and position.
"""
return (
a["pos_x"] == b["pos_x"]
and a["pos_y"] == b["pos_y"]
and a["res_x"] == b["res_x"]
and a["res_y"] == b["res_y"]
)
2020-01-31 17:27:59 +00:00
class Window(QObject):
def __init__(self, ui):
super().__init__()
self.ui = ui
ui.show()
2020-01-31 22:17:50 +00:00
self.ui.setWindowTitle('Display Configuration')
2020-01-31 17:27:59 +00:00
self.ui.screenCombo.currentTextChanged.connect(self.monitor_selected)
2020-01-31 23:55:28 +00:00
self.ui.orientationCombo.currentIndexChanged.connect(self.orientation_changed)
2020-01-31 17:27:59 +00:00
self.xrandr_info = {}
self.get_xrandr_info()
2020-01-31 18:54:01 +00:00
self.orig_xrandr_info = deepcopy(self.xrandr_info)
2020-01-31 17:27:59 +00:00
self.fill_ui()
2020-01-31 20:51:40 +00:00
self.ui.horizontalScale.valueChanged.connect(self.scale_changed)
self.ui.verticalScale.valueChanged.connect(self.scale_changed)
self.ui.modes.currentTextChanged.connect(self.mode_changed)
2020-01-31 21:18:24 +00:00
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)
def do_reset(self):
for n in self.xrandr_info:
self.xrandr_info[n].update(self.orig_xrandr_info[n])
self.fill_ui()
def do_ok(self):
self.do_apply()
self.ui.accept()
def do_apply(self):
cli = gen_xrandr_from_data(self.xrandr_info)
2020-01-31 23:55:28 +00:00
print(cli)
# subprocess.check_call(shlex.split(cli))
2020-01-31 20:34:02 +00:00
2020-01-31 17:27:59 +00:00
def fill_ui(self):
"""Load data from xrandr and setup the whole thing."""
self.scene = QGraphicsScene(self)
self.ui.sceneView.setScene(self.scene)
self.ui.screenCombo.clear()
for name, monitor in self.xrandr_info.items():
self.ui.screenCombo.addItem(name)
2020-01-31 20:21:41 +00:00
mon_item = MonitorItem(data=monitor, window=self, name=name,)
2020-01-31 20:21:04 +00:00
# mon_item.setPos(monitor["pos_x"], monitor["pos_y"])
2020-01-31 17:27:59 +00:00
self.scene.addItem(mon_item)
monitor["item"] = mon_item
self.adjust_view()
2020-01-31 21:18:24 +00:00
self.scale_changed() # Trigger scale labels update
2020-01-31 17:27:59 +00:00
2020-01-31 23:57:37 +00:00
def orientation_changed(self):
mon = self.ui.screenCombo.currentText()
orientation = self.ui.orientationCombo.currentIndex()
self.xrandr_info[mon]["orientation"] = orientation
self.mode_changed()
2020-01-31 20:51:40 +00:00
def mode_changed(self):
2020-01-31 20:34:02 +00:00
mon = self.ui.screenCombo.currentText()
mode = self.ui.modes.currentText()
2020-01-31 20:51:40 +00:00
if not mode:
return
2020-01-31 22:17:50 +00:00
print(f"Changing {mon} to {mode}")
self.xrandr_info[mon]["current_mode"] = mode
mode_x, mode_y = mode.split("x")
2020-01-31 20:34:02 +00:00
# use resolution via scaling
2020-01-31 23:42:38 +00:00
if self.xrandr_info[mon]["orientation"] in (0,2):
self.xrandr_info[mon]["res_x"] = int(
int(mode_x) * self.ui.horizontalScale.value() / 100
)
self.xrandr_info[mon]["res_y"] = int(
int(mode_y) * self.ui.verticalScale.value() / 100
)
else:
self.xrandr_info[mon]["res_x"] = int(
int(mode_y) * self.ui.horizontalScale.value() / 100
)
self.xrandr_info[mon]["res_y"] = int(
int(mode_x) * self.ui.verticalScale.value() / 100
)
2020-01-31 22:17:50 +00:00
self.xrandr_info[mon]["item"].update_visuals(self.xrandr_info[mon])
2020-01-31 20:34:02 +00:00
2020-01-31 19:53:36 +00:00
def monitor_moved(self):
"Update xrandr_info with new monitor positions"
for _, mon in self.xrandr_info.items():
2020-01-31 20:20:47 +00:00
item = mon["item"]
mon["pos_x"] = item.x()
mon["pos_y"] = item.y()
2020-01-31 19:56:38 +00:00
self.update_replica_of_data()
2020-01-31 19:53:36 +00:00
for _, mon in self.xrandr_info.items():
2020-01-31 20:20:47 +00:00
mon["item"].update_visuals(mon)
self.adjust_view()
2020-01-31 19:53:36 +00:00
2020-01-31 17:27:59 +00:00
def adjust_view(self):
self.ui.sceneView.resetTransform()
2020-01-31 17:27:59 +00:00
self.ui.sceneView.ensureVisible(self.scene.sceneRect(), 100, 100)
2020-01-31 20:20:47 +00:00
scale_factor = 0.8 * min(
2020-01-31 17:27:59 +00:00
self.ui.sceneView.width() / self.scene.sceneRect().width(),
self.ui.sceneView.height() / self.scene.sceneRect().height(),
2020-01-31 16:46:06 +00:00
)
2020-01-31 17:27:59 +00:00
self.ui.sceneView.scale(scale_factor, scale_factor)
def get_xrandr_info(self):
data = subprocess.check_output(["xrandr"]).decode("utf-8").splitlines()
2020-01-31 18:54:01 +00:00
name = None
for line in data:
if line and line[0] not in "S \t": # Output line
2020-01-31 18:57:02 +00:00
(
name,
primary,
res_x,
res_y,
w_in_mm,
h_in_mm,
pos_x,
pos_y,
enabled,
2020-01-31 23:21:43 +00:00
orientation,
2020-01-31 18:57:02 +00:00
) = parse_monitor(line)
2020-01-31 18:54:01 +00:00
self.xrandr_info[name] = dict(
primary=primary,
res_x=res_x,
res_y=res_y,
w_in_mm=w_in_mm,
h_in_mm=h_in_mm,
pos_x=pos_x,
pos_y=pos_y,
modes=[],
current_mode=None,
enabled=enabled,
2020-01-31 19:29:04 +00:00
replica_of=[],
2020-01-31 23:21:43 +00:00
orientation=orientation,
2020-01-31 18:54:01 +00:00
)
2020-01-31 18:57:02 +00:00
elif line[0] == " ": # A mode
2020-01-31 18:54:01 +00:00
mode_name = line.strip().split()[0]
2020-01-31 18:57:02 +00:00
self.xrandr_info[name]["modes"].append(mode_name)
if "*" in line:
print(f"Current mode for {name}: {mode_name}")
self.xrandr_info[name]["current_mode"] = mode_name
2020-01-31 19:56:38 +00:00
self.update_replica_of_data()
2020-01-31 18:57:02 +00:00
2020-01-31 19:56:38 +00:00
def update_replica_of_data(self):
2020-01-31 19:29:04 +00:00
for a in self.xrandr_info:
2020-01-31 20:20:47 +00:00
self.xrandr_info[a]["replica_of"] = []
2020-01-31 19:29:04 +00:00
for b in self.xrandr_info:
2020-01-31 19:56:38 +00:00
if a != b and is_replica_of(self.xrandr_info[a], self.xrandr_info[b]):
2020-01-31 20:20:47 +00:00
self.xrandr_info[a]["replica_of"].append(b)
2020-01-31 19:29:04 +00:00
2020-01-31 17:27:59 +00:00
def monitor_selected(self, name):
2020-01-31 21:18:24 +00:00
if not name:
return
2020-01-31 20:51:40 +00:00
# needed so we don't flip through all modes as they are added
self.ui.modes.blockSignals(True)
2020-01-31 18:54:01 +00:00
# Show modes
self.ui.modes.clear()
2020-01-31 18:57:02 +00:00
for mode in self.xrandr_info[name]["modes"]:
2020-01-31 18:54:01 +00:00
self.ui.modes.addItem(mode)
2020-01-31 18:57:02 +00:00
self.ui.modes.setCurrentText(self.xrandr_info[name]["current_mode"])
mod_x, mod_y = [
int(x) for x in self.xrandr_info[name]["current_mode"].split("x")
]
2020-01-31 23:42:38 +00:00
if self.xrandr_info[name]["orientation"] in (0,2):
h_scale = self.xrandr_info[name]["res_x"] / mod_x
v_scale = self.xrandr_info[name]["res_y"] / mod_y
else:
h_scale = self.xrandr_info[name]["res_y"] / mod_x
v_scale = self.xrandr_info[name]["res_x"] / mod_y
2020-01-31 18:54:01 +00:00
self.ui.horizontalScale.setValue(h_scale * 100)
self.ui.verticalScale.setValue(v_scale * 100)
2020-01-31 18:57:02 +00:00
self.ui.primary.setChecked(self.xrandr_info[name]["primary"])
self.ui.enabled.setChecked(self.xrandr_info[name]["enabled"])
2020-01-31 23:21:43 +00:00
self.ui.orientationCombo.setCurrentIndex(self.xrandr_info[name]["orientation"])
2020-01-31 18:57:02 +00:00
self.ui.replicaOf.clear()
self.ui.replicaOf.addItem("None")
for mon in self.xrandr_info:
if mon != name:
self.ui.replicaOf.addItem(mon)
2020-01-31 20:20:47 +00:00
if mon in self.xrandr_info[name]["replica_of"]:
2020-01-31 19:16:02 +00:00
self.ui.replicaOf.setCurrentText(mon)
2020-01-31 20:51:40 +00:00
self.ui.modes.blockSignals(False)
2020-01-31 18:54:01 +00:00
2020-01-31 20:51:40 +00:00
def scale_changed(self):
2020-01-31 18:57:02 +00:00
self.ui.horizontalScaleLabel.setText(f"{self.ui.horizontalScale.value()}%")
self.ui.verticalScaleLabel.setText(f"{self.ui.verticalScale.value()}%")
2020-01-31 20:51:40 +00:00
self.mode_changed() # Not really, but it's the same thing
2020-01-31 18:57:02 +00:00
2020-01-31 16:46:06 +00:00
if __name__ == "__main__":
app = QApplication(sys.argv)
ui_file = QFile("main.ui")
ui_file.open(QFile.ReadOnly)
loader = QUiLoader()
2020-01-31 17:27:59 +00:00
window = Window(loader.load(ui_file))
2020-01-31 16:46:06 +00:00
sys.exit(app.exec_())