packaging via poetry

This commit is contained in:
Roberto Alsina
2020-02-01 16:49:26 -03:00
parent 8cfd0072cd
commit 90fe2990bc
6 changed files with 35 additions and 6 deletions

1
xrandroll/__init__.py Normal file
View File

@ -0,0 +1 @@
from .__main__ import main

390
xrandroll/__main__.py Normal file
View File

@ -0,0 +1,390 @@
import os
import shlex
import subprocess
import sys
from copy import deepcopy
from PySide2.QtCore import QFile, QObject
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QGraphicsScene, QLabel
from .monitor_item import MonitorItem
def gen_xrandr_from_data(data):
"""Takes monitor data and generates a xrandr command line."""
cli = ["xrandr"]
for name, mon in data.items():
cli.append(f"--output {name}")
cli.append(f'--pos {int(mon["pos_x"])}x{int(mon["pos_y"])}')
cli.append(f'--mode {mon["current_mode"]}')
mod_x, mod_y = [int(n) for n in mon["current_mode"].split("x")]
if mon["orientation"] in (1, 3):
mod_x, mod_y = mod_y, mod_x
cli.append(f'--scale {mon["res_x"]/mod_x}x{mon["res_y"]/mod_y}')
cli.append(
f"--rotate {['normal', 'left', 'inverted', 'right'][mon['orientation']]}"
)
if mon["primary"]:
cli.append("--primary")
if not mon["enabled"]:
cli.append("--off")
return " ".join(cli)
def parse_monitor(line):
parts = line.split()
name = parts[0]
primary = "primary" in parts
if "+" in line: # Is enabled
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
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
return (
name,
primary,
int(res_x),
int(res_y),
int(w_in_mm),
int(h_in_mm),
int(pos_x),
int(pos_y),
enabled,
orientation,
)
def is_replica_of(a, b):
"""Return True if monitor a is a replica of b.
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"]
)
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.xrandr_info = {}
self.get_xrandr_info()
self.orig_xrandr_info = deepcopy(self.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.pos_label = QLabel(self.ui.sceneView)
self.pos_label.setText("FOOOOO")
self.pos_label.move(5, 5)
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 = [k for k in self.xrandr_info if self.xrandr_info[k]["primary"]]
if not primary:
print("Oops, no primary!")
return
primary = self.xrandr_info[primary[0]]
monitor = self.xrandr_info[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 = self.ui.screenCombo.currentText()
replicate = self.ui.replicaOf.currentText()
print(f"Making {mon} a replica of {replicate}")
if replicate in ("None", "", None):
print("TODO: make things non-replicas")
return
mon = self.xrandr_info[mon]
replicate = self.xrandr_info[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
if replicate["current_mode"] in mon["modes"]:
mon["current_mode"] = replicate["current_mode"]
else:
# Keep the current mode, and change scaling so it
# has the same effective size as the desired mode
mod_x, mod_y = [int(x) for x in mon["current_mode"].split("x")]
target_x, target_y = [replicate[x] for x in ["res_x", "res_y"]]
scale_x = 1000 * target_x / mod_x
scale_y = 1000 * target_y / mod_y
breakpoint()
print(target_x, target_y, mod_x, mod_y)
print(scale_x, scale_y)
self.ui.horizontalScale.setValue(scale_x)
self.ui.verticalScale.setValue(scale_y)
mon["item"].update_visuals(mon)
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)
print(cli)
subprocess.check_call(shlex.split(cli))
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)
mon_item = MonitorItem(data=monitor, window=self, name=name,)
# mon_item.setPos(monitor["pos_x"], monitor["pos_y"])
self.scene.addItem(mon_item)
monitor["item"] = mon_item
self.adjust_view()
self.scale_changed() # Trigger scale labels update
def orientation_changed(self):
mon = self.ui.screenCombo.currentText()
orientation = self.ui.orientationCombo.currentIndex()
self.xrandr_info[mon]["orientation"] = orientation
self.mode_changed()
def mode_changed(self):
mon = self.ui.screenCombo.currentText()
mode = self.ui.modes.currentText()
if not mode:
return
print(f"Changing {mon} to {mode}")
self.xrandr_info[mon]["current_mode"] = mode
mode_x, mode_y = mode.split("x")
# use resolution via scaling
if self.xrandr_info[mon]["orientation"] in (0, 2):
self.xrandr_info[mon]["res_x"] = int(
int(mode_x) * self.ui.horizontalScale.value() / 1000
)
self.xrandr_info[mon]["res_y"] = int(
int(mode_y) * self.ui.verticalScale.value() / 1000
)
else:
self.xrandr_info[mon]["res_x"] = int(
int(mode_y) * self.ui.horizontalScale.value() / 1000
)
self.xrandr_info[mon]["res_y"] = int(
int(mode_x) * self.ui.verticalScale.value() / 1000
)
self.xrandr_info[mon]["item"].update_visuals(self.xrandr_info[mon])
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 xrandr_info with new monitor positions"
for _, mon in self.xrandr_info.items():
item = mon["item"]
mon["pos_x"] = item.x()
mon["pos_y"] = item.y()
self.update_replica_of_data()
for _, mon in self.xrandr_info.items():
mon["item"].update_visuals(mon)
self.adjust_view()
def adjust_view(self):
self.ui.sceneView.resetTransform()
self.ui.sceneView.ensureVisible(self.scene.sceneRect(), 100, 100)
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)
def get_xrandr_info(self):
data = subprocess.check_output(["xrandr"]).decode("utf-8").splitlines()
name = None
for line in data:
if line and line[0] not in "S \t": # Output line
(
name,
primary,
res_x,
res_y,
w_in_mm,
h_in_mm,
pos_x,
pos_y,
enabled,
orientation,
) = parse_monitor(line)
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,
replica_of=[],
orientation=orientation,
)
elif line[0] == " ": # A mode
mode_name = line.strip().split()[0]
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
self.update_replica_of_data()
def update_replica_of_data(self):
for a in self.xrandr_info:
self.xrandr_info[a]["replica_of"] = []
for b in self.xrandr_info:
if a != b and is_replica_of(self.xrandr_info[a], self.xrandr_info[b]):
self.xrandr_info[a]["replica_of"].append(b)
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)
# Show modes
self.ui.modes.clear()
for mode in self.xrandr_info[name]["modes"]:
self.ui.modes.addItem(mode)
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")
]
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
self.ui.horizontalScale.setValue(h_scale * 1000)
self.ui.verticalScale.setValue(v_scale * 1000)
self.ui.primary.setChecked(self.xrandr_info[name]["primary"])
self.ui.enabled.setChecked(self.xrandr_info[name]["enabled"])
self.ui.orientationCombo.setCurrentIndex(self.xrandr_info[name]["orientation"])
self.ui.replicaOf.clear()
self.ui.replicaOf.addItem("None")
for mon in self.xrandr_info:
if mon != name:
self.ui.replicaOf.addItem(mon)
if mon in self.xrandr_info[name]["replica_of"]:
self.ui.replicaOf.setCurrentText(mon)
self.ui.modes.blockSignals(False)
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 = Window(loader.load(ui_file))
sys.exit(app.exec_())
if __name__ == "__main__":
main()

331
xrandroll/main.ui Normal file
View File

@ -0,0 +1,331 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Main</class>
<widget class="QDialog" name="Main">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>925</width>
<height>820</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGraphicsView" name="sceneView"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Monitor:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="screenCombo">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="enabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="primary">
<property name="text">
<string>Primary</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Mode:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="modes">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Orientation:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="orientationCombo">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>Normal</string>
</property>
</item>
<item>
<property name="text">
<string>Left (90° ccw)</string>
</property>
</item>
<item>
<property name="text">
<string>Inverted (180°)</string>
</property>
</item>
<item>
<property name="text">
<string>Right (90° cw)</string>
</property>
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Replica of:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="replicaOf">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Scaling mode:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="scaleModeCombo">
<item>
<property name="text">
<string>Manual</string>
</property>
</item>
<item>
<property name="text">
<string>Disabled (1x1)</string>
</property>
</item>
<item>
<property name="text">
<string>Automatic: physical dimensions</string>
</property>
</item>
<item>
<property name="text">
<string>Manual, same in both dimensions</string>
</property>
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Horizontal Scale:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSlider" name="horizontalScale">
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>4000</number>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="pageStep">
<number>25</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="horizontalScaleLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Vertical Scale:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="verticalScale">
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>4000</number>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="pageStep">
<number>25</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="verticalScaleLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="resetButton">
<property name="toolTip">
<string>Reset to initial configuration</string>
</property>
<property name="text">
<string>Reset</string>
</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>
<item>
<widget class="QPushButton" name="okButton">
<property name="toolTip">
<string>Apply configuration and close</string>
</property>
<property name="text">
<string>Ok</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyButton">
<property name="toolTip">
<string>Apply configuration</string>
</property>
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="toolTip">
<string>Close without applying configuration</string>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

70
xrandroll/monitor_item.py Normal file
View File

@ -0,0 +1,70 @@
from PySide2.QtCore import Qt, QObject
from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsTextItem
from PySide2.QtGui import QBrush
class MonitorItem(QGraphicsRectItem, QObject):
z = 0
def __init__(self, *a, **kw):
data = kw.pop("data")
self.name = kw.pop("name")
self.window = kw.pop("window")
super().__init__(0, 0, 0, 0)
self.setAcceptedMouseButtons(Qt.LeftButton)
self.label = QGraphicsTextItem("", self)
self.bottom_edge = QGraphicsRectItem(0, 0, 0, 0, self)
self.bottom_edge.setBrush(QBrush("red", Qt.SolidPattern))
self.update_visuals(data)
def update_visuals(self, data):
if data["replica_of"]:
label_text = f"{self.name} [{','.join(data['replica_of'])}]"
else:
label_text = self.name
if data["orientation"] in (0, 2):
self.setRect(0, 0, data["res_x"], data["res_y"])
if data["orientation"] == 0:
self.bottom_edge.setRect(0, data["res_y"] - 50, data["res_x"], 50)
if data["orientation"] == 2:
self.bottom_edge.setRect(0, 0, data["res_x"], 50)
else:
self.setRect(0, 0, data["res_y"], data["res_x"])
if data["orientation"] == 1:
self.bottom_edge.setRect(data["res_y"] - 50, 0, 50, data["res_x"])
if data["orientation"] == 3:
self.bottom_edge.setRect(0, 0, 50, data["res_x"])
self.setPos(data["pos_x"], data["pos_y"])
self.label.setPlainText(label_text)
label_scale = min(
self.rect().width() / self.label.boundingRect().width(),
self.rect().height() / self.label.boundingRect().height(),
)
self.label.setScale(label_scale)
if data["primary"]:
self.setBrush(QBrush("#eee8d5", Qt.SolidPattern))
self.setZValue(1)
else:
self.setBrush(QBrush("white", Qt.SolidPattern))
self.setZValue(self.z)
self.z -= 1
def mousePressEvent(self, event):
self.window.pos_label.show()
self.setCursor(Qt.ClosedHandCursor)
self.orig_pos = self.pos()
self.window.ui.screenCombo.setCurrentText(self.name)
def mouseReleaseEvent(self, event):
self.setCursor(Qt.OpenHandCursor)
self.window.monitor_moved()
self.window.pos_label.hide()
def mouseMoveEvent(self, event):
view = event.widget().parent()
click_pos = event.buttonDownScreenPos(Qt.LeftButton)
current_pos = event.screenPos()
self.setPos(
view.mapToScene(view.mapFromScene(self.orig_pos) + current_pos - click_pos)
)
self.window.show_pos(int(self.pos().x()), int(self.pos().y()))