729 lines
27 KiB
Text
729 lines
27 KiB
Text
|
#!/usr/bin/env python3
|
||
|
#
|
||
|
# Copyright (c) 2016 James Murphy
|
||
|
# Licensed under the GPL version 2 only
|
||
|
#
|
||
|
# monitor_manager is an i3blocks blocklet script to quickly manage your
|
||
|
# connected output devices
|
||
|
|
||
|
|
||
|
from tkinter import *
|
||
|
from tkinter import messagebox
|
||
|
from shutil import which
|
||
|
import tkinter.font as font
|
||
|
from subprocess import call, check_output, CalledProcessError
|
||
|
import re
|
||
|
import os
|
||
|
|
||
|
DESKTOP_SYMBOL = "\uf108"
|
||
|
|
||
|
UP_ARROW = "\uf062"
|
||
|
DOWN_ARROW = "\uf063"
|
||
|
UNBLANKED_SYMBOL = "\uf06e"
|
||
|
BLANKED_SYMBOL = "\uf070"
|
||
|
NOT_CLONED_SYMBOL = "\uf096"
|
||
|
PRIMARY_SYMBOL = "\uf005"
|
||
|
SECONDARY_SYMBOL = "\uf006"
|
||
|
CLONED_SYMBOL = "\uf24d"
|
||
|
ROTATION_NORMAL = "\uf151"
|
||
|
ROTATION_LEFT = "\uf191"
|
||
|
ROTATION_RIGHT = "\uf152"
|
||
|
ROTATION_INVERTED = "\uf150"
|
||
|
REFLECTION_NORMAL = "\uf176"
|
||
|
REFLECTION_X = "\uf07e"
|
||
|
REFLECTION_Y = "\uf07d"
|
||
|
REFLECTION_XY = "\uf047"
|
||
|
TOGGLE_ON = "\uf205"
|
||
|
TOGGLE_OFF = "\uf204"
|
||
|
APPLY_SYMBOL = "\uf00c"
|
||
|
CANCEL_SYMBOL = "\uf00d"
|
||
|
ARANDR_SYMBOL = "\uf085"
|
||
|
REFRESH_SYMBOL = "\uf021"
|
||
|
|
||
|
strbool = lambda s: s.lower() in ['t', 'true', '1']
|
||
|
def _default(name, default='', arg_type=strbool):
|
||
|
val = default
|
||
|
if name in os.environ:
|
||
|
val = os.environ[name]
|
||
|
return arg_type(val)
|
||
|
|
||
|
|
||
|
SHOW_ON_OFF = _default("SHOW_ON_OFF","1")
|
||
|
SHOW_NAMES = _default("SHOW_NAMES", "1")
|
||
|
SHOW_PRIMARY = _default("SHOW_PRIMARY", "1")
|
||
|
SHOW_MODE = _default("SHOW_MODE", "1")
|
||
|
SHOW_BLANKED = _default("SHOW_BLANKED", "1")
|
||
|
SHOW_DUPLICATE = _default("SHOW_DUPLICATE", "1")
|
||
|
SHOW_ROTATION = _default("SHOW_ROTATION", "1")
|
||
|
SHOW_REFLECTION = _default("SHOW_REFLECTION", "1")
|
||
|
SHOW_BRIGHTNESS = _default("SHOW_BRIGHTNESS", "1")
|
||
|
SHOW_BRIGHTNESS_VALUE = _default("SHOW_BRIGHTNESS_VALUE", "0")
|
||
|
SHOW_UP_DOWN = _default("SHOW_UP_DOWN", "1")
|
||
|
|
||
|
FONTAWESOME_FONT_FAMILY = "FontAwesome"
|
||
|
FONTAWESOME_FONT_SIZE = 11
|
||
|
FONTAWESOME_FONT = (FONTAWESOME_FONT_FAMILY, FONTAWESOME_FONT_SIZE)
|
||
|
DEFAULT_FONT_FAMILY = _default("FONT_FAMILY","DejaVu Sans Mono", str)
|
||
|
DEFAULT_FONT_SIZE = _default("FONT_SIZE", 11, int)
|
||
|
DEFAULT_FONT = (DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE)
|
||
|
|
||
|
BRIGHTNESS_SLIDER_HANDLE_LENGTH = 20
|
||
|
BRIGHTNESS_SLIDER_WIDTH = 15
|
||
|
BRIGHTNESS_SLIDER_LENGTH = 50
|
||
|
|
||
|
WINDOW_CLOSE_TO_BOUNDARY_BUFFER = _default("CLOSE_TO_BOUNDARY_BUFFER", 20, int)
|
||
|
|
||
|
class Output:
|
||
|
def __init__(self, name=None, w=None, h=None, x=None, y=None, rate=None,
|
||
|
active=False, primary=False, sameAs=None, blanked=False, rotation="normal",
|
||
|
reflection="normal", brightness=1.0):
|
||
|
self.name = name
|
||
|
self.w = w
|
||
|
self.h = h
|
||
|
self.x = x
|
||
|
self.y = y
|
||
|
self.rate = rate
|
||
|
self.active = active
|
||
|
self.primary = primary
|
||
|
self.sameAs = sameAs
|
||
|
self.blanked = blanked
|
||
|
self.modes = []
|
||
|
self.currentModeIndex = None
|
||
|
self.preferredModeIndex = None
|
||
|
self.row = None
|
||
|
self.rotation = rotation
|
||
|
self.reflection = reflection
|
||
|
self.brightness = brightness
|
||
|
|
||
|
def setPreferredMode(self):
|
||
|
if self.preferredModeIndex != None:
|
||
|
self.setMode(self.preferredModeIndex)
|
||
|
elif self.modes != None:
|
||
|
self.setMode(0)
|
||
|
|
||
|
def setMode(self, index):
|
||
|
self.w, self.h, self.rate = self.modes[index]
|
||
|
self.currentModeIndex = index
|
||
|
|
||
|
def realOutputs():
|
||
|
outputs = []
|
||
|
xrandrText = check_output(["xrandr","--verbose"],universal_newlines=True)
|
||
|
outputBlocks = re.split(r'\n(?=\S)', xrandrText, re.MULTILINE)
|
||
|
infoPattern = re.compile(
|
||
|
r'^(\S+)' # output name
|
||
|
' connected ' # must be connected
|
||
|
'(primary )?' # check if primary output
|
||
|
'((\d+)x(\d+)\+(\d+)\+(\d+) )?' # width x height + xoffset + yoffset
|
||
|
'(\(\S+\) )?' # mode code (0x4a)
|
||
|
'(normal|left|inverted|right)? ?' # rotation
|
||
|
'(X axis|Y axis|X and Y axis)?') # reflection
|
||
|
brightnessPattern = re.compile(r'^\tBrightness: ([\d.]+)', re.MULTILINE)
|
||
|
modePattern = re.compile(r'^ (\d+)x(\d+)[^\n]*?\n +h:[^\n]*?\n +v:[^\n]*?([\d.]+)Hz$', re.MULTILINE)
|
||
|
for outputBlock in outputBlocks:
|
||
|
output = Output()
|
||
|
infoMatch = infoPattern.match(outputBlock)
|
||
|
if infoMatch:
|
||
|
output.name = infoMatch.group(1)
|
||
|
if infoMatch.group(2):
|
||
|
output.primary = True
|
||
|
if infoMatch.group(3):
|
||
|
output.active = True
|
||
|
output.w, output.h, output.x, output.y = map(int,infoMatch.group(4, 5, 6, 7))
|
||
|
if infoMatch.group(9):
|
||
|
output.rotation = infoMatch.group(9)
|
||
|
if output.rotation in ["left", "right"]:
|
||
|
output.w, output.h = output.h, output.w
|
||
|
if infoMatch.group(10):
|
||
|
if infoMatch.group(10) == "X axis":
|
||
|
output.reflection = "x"
|
||
|
elif infoMatch.group(10) == "Y axis":
|
||
|
output.reflection = "y"
|
||
|
elif infoMatch.group(10) == "X and Y axis":
|
||
|
output.reflection = "xy"
|
||
|
else:
|
||
|
output.reflection = "normal"
|
||
|
else:
|
||
|
output.reflection = "normal"
|
||
|
brightnessMatch = brightnessPattern.search(outputBlock)
|
||
|
if brightnessMatch:
|
||
|
try:
|
||
|
brightness = float(brightnessMatch.group(1))
|
||
|
output.brightness = brightness
|
||
|
if abs(brightness) < 1e-09:
|
||
|
output.blanked = True
|
||
|
except ValueError:
|
||
|
pass
|
||
|
modeMatches = modePattern.finditer(outputBlock)
|
||
|
for i, modeMatch in enumerate(modeMatches):
|
||
|
if "*current" in modeMatch.group(0):
|
||
|
output.currentModeIndex = i
|
||
|
output.rate = modeMatch.group(3)
|
||
|
if "+preferred" in modeMatch.group(0):
|
||
|
output.preferredModeIndex = i
|
||
|
output.modes.append(modeMatch.group(1,2,3))
|
||
|
outputs.append(output)
|
||
|
outputs.sort(key=lambda m: m.x if m.x != None else -1)
|
||
|
prev = None
|
||
|
for output in outputs:
|
||
|
if prev != None and output.active and prev.active and output.x == prev.x:
|
||
|
output.sameAs = prev.name
|
||
|
else:
|
||
|
prev = output
|
||
|
|
||
|
return outputs
|
||
|
|
||
|
def modestr(mode):
|
||
|
return "{}x{}@{}".format(*mode)
|
||
|
|
||
|
def status(self):
|
||
|
if self.active:
|
||
|
if self.sameAs == None or self.sameAs == self.name:
|
||
|
if self.w and self.h and self.rate:
|
||
|
return "{}x{}@{}".format(self.w, self.h, self.rate)
|
||
|
else:
|
||
|
return "auto"
|
||
|
else:
|
||
|
return "duplicate {}".format(self.sameAs)
|
||
|
else:
|
||
|
return "Inactive"
|
||
|
|
||
|
|
||
|
def __str__(self):
|
||
|
return "{} {}x{}+{}+{} active:{}, primary:{}\nmodes:{}\ncurrentIndex:{} preferredIndex:{}".format(
|
||
|
self.name, self.w, self.h, self.x, self.y, self.active, self.primary, self.modes, self.currentModeIndex, self.preferredModeIndex)
|
||
|
|
||
|
class MonitorManager():
|
||
|
def __init__(self, root):
|
||
|
self.root = root
|
||
|
self.root.withdraw()
|
||
|
self.root.resizable(0,0)
|
||
|
self.root.wm_title("Monitor Manager")
|
||
|
self.frame = None
|
||
|
self.outputs = []
|
||
|
self.hardRefreshList()
|
||
|
style = {'relief':FLAT, 'padx':1, 'pady':1, 'anchor':'w', 'font':FONTAWESOME_FONT}
|
||
|
|
||
|
self.infoLabel = Label(self.root, text="", **style)
|
||
|
self.infoLabel.config(font=DEFAULT_FONT)
|
||
|
|
||
|
self.bottomRow = []
|
||
|
|
||
|
self.applyButton = Button(self.root, text=APPLY_SYMBOL, **style)
|
||
|
self.bottomRow.append(self.applyButton)
|
||
|
|
||
|
self.refreshButton = Button(self.root, text=REFRESH_SYMBOL, **style)
|
||
|
self.bottomRow.append(self.refreshButton)
|
||
|
|
||
|
if which("arandr"):
|
||
|
self.arandrButton = Button(self.root, text=ARANDR_SYMBOL, **style)
|
||
|
self.bottomRow.append(self.arandrButton)
|
||
|
else:
|
||
|
self.arandrButton = None
|
||
|
|
||
|
self.cancelButton = Button(self.root, text=CANCEL_SYMBOL, **style)
|
||
|
self.bottomRow.append(self.cancelButton)
|
||
|
|
||
|
self.infoLabel.grid(row=1,column=0, columnspan=len(self.bottomRow))
|
||
|
self.gridRow(2, self.bottomRow)
|
||
|
|
||
|
self.moveToMouse()
|
||
|
self.root.deiconify()
|
||
|
|
||
|
def registerBindings(self):
|
||
|
self.root.bind("<Return>", self.handleApply)
|
||
|
self.root.bind("<Escape>", self.handleCancel)
|
||
|
|
||
|
self.applyButton.bind("<Button-1>", self.handleApply)
|
||
|
self.setInfo(self.applyButton, "Apply changes")
|
||
|
|
||
|
self.refreshButton.bind("<Button-1>", self.hardRefreshList)
|
||
|
self.setInfo(self.refreshButton, "Refresh list")
|
||
|
|
||
|
if self.arandrButton:
|
||
|
self.arandrButton.bind("<Button-1>", self.handleArandr)
|
||
|
self.setInfo(self.arandrButton, "Launch aRandR")
|
||
|
|
||
|
self.cancelButton.bind("<Button-1>", self.handleCancel)
|
||
|
self.setInfo(self.cancelButton, "Cancel")
|
||
|
|
||
|
for toggleButton in self.toggleButtons:
|
||
|
toggleButton.bind("<Button-1>", self.toggleActive)
|
||
|
toggleButton.bind("<Button-4>", self.handleUp)
|
||
|
toggleButton.bind("<Button-5>", self.handleDown)
|
||
|
self.setInfo(toggleButton, "Turn output on/off")
|
||
|
|
||
|
for primaryButton in self.primaryButtons:
|
||
|
primaryButton.bind("<Button-1>", self.setPrimary)
|
||
|
self.setInfo(primaryButton, "Set primary output")
|
||
|
|
||
|
for blankedButton in self.blankedButtons:
|
||
|
blankedButton.bind("<Button-1>", self.toggleBlanked)
|
||
|
self.setInfo(blankedButton, "Show/hide output")
|
||
|
|
||
|
for duplicateButton in self.duplicateButtons:
|
||
|
duplicateButton.bind("<Button-1>", self.toggleDuplicate)
|
||
|
self.setInfo(duplicateButton, "Duplicate another output")
|
||
|
|
||
|
for rotateButton in self.rotateButtons:
|
||
|
rotateButton.bind("<Button-1>", self.cycleRotation)
|
||
|
self.setInfo(rotateButton, "Rotate output")
|
||
|
|
||
|
for reflectButton in self.reflectButtons:
|
||
|
reflectButton.bind("<Button-1>", self.cycleReflection)
|
||
|
self.setInfo(reflectButton, "Reflect output")
|
||
|
|
||
|
for brightnessSlider in self.brightnessSliders:
|
||
|
brightnessSlider.bind("<ButtonRelease-1>", self.updateBrightness)
|
||
|
self.setInfo(brightnessSlider, "Adjust brightness")
|
||
|
|
||
|
for upButton in self.upButtons:
|
||
|
upButton.bind("<Button-1>", self.handleUp)
|
||
|
upButton.bind("<Button-4>", self.handleUp)
|
||
|
upButton.bind("<Button-5>", self.handleDown)
|
||
|
self.setInfo(upButton, "Move up")
|
||
|
|
||
|
for downButton in self.downButtons:
|
||
|
downButton.bind("<Button-1>", self.handleDown)
|
||
|
downButton.bind("<Button-4>", self.handleUp)
|
||
|
downButton.bind("<Button-5>", self.handleDown)
|
||
|
self.setInfo(downButton, "Move down")
|
||
|
|
||
|
def gridRow(self, row, widgets):
|
||
|
column = 0
|
||
|
for w in widgets:
|
||
|
w.grid(row=row, column=column)
|
||
|
column += 1
|
||
|
|
||
|
def moveToMouse(self):
|
||
|
root = self.root
|
||
|
root.update_idletasks()
|
||
|
width = root.winfo_reqwidth()
|
||
|
height = root.winfo_reqheight()
|
||
|
x = root.winfo_pointerx() - width//2
|
||
|
y = root.winfo_pointery() - height//2
|
||
|
screen_width = root.winfo_screenwidth()
|
||
|
screen_height = root.winfo_screenheight()
|
||
|
if x+width > screen_width - WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
|
||
|
x = screen_width - WINDOW_CLOSE_TO_BOUNDARY_BUFFER - width
|
||
|
elif x < WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
|
||
|
x = WINDOW_CLOSE_TO_BOUNDARY_BUFFER
|
||
|
if y+height > screen_height - WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
|
||
|
y = screen_height - WINDOW_CLOSE_TO_BOUNDARY_BUFFER - height
|
||
|
elif y < WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
|
||
|
y = WINDOW_CLOSE_TO_BOUNDARY_BUFFER
|
||
|
|
||
|
root.geometry('+{}+{}'.format(x, y))
|
||
|
|
||
|
def setInfo(self, widget, info):
|
||
|
widget.bind("<Enter>", lambda e: self.infoLabel.config(text=info, fg="black"))
|
||
|
widget.bind("<Leave>", lambda e: self.infoLabel.config(text=""))
|
||
|
|
||
|
def handleApply(self, e=None):
|
||
|
self.root.after_idle(self.doHandleApply)
|
||
|
|
||
|
def doHandleApply(self):
|
||
|
if not self.getUserConfirmationIfDangerousConfiguration():
|
||
|
return
|
||
|
command = ["xrandr"]
|
||
|
if not self.existsPrimary():
|
||
|
command += ["--noprimary"]
|
||
|
partition = self.sameAsPartition()
|
||
|
prevFirstActive = None
|
||
|
for p in partition:
|
||
|
firstActive = None
|
||
|
for output in p:
|
||
|
command += ["--output", output.name]
|
||
|
if output.active:
|
||
|
if firstActive == None:
|
||
|
firstActive = output
|
||
|
else:
|
||
|
command += ["--same-as", firstActive.name]
|
||
|
if output.primary:
|
||
|
command += ["--primary"]
|
||
|
if prevFirstActive != None:
|
||
|
command += ["--right-of", prevFirstActive.name]
|
||
|
if output.w != None and output.h != None and output.rate != None:
|
||
|
command += ["--mode", "{}x{}".format(output.w,output.h)]
|
||
|
command += ["--rate", output.rate ]
|
||
|
else:
|
||
|
command += ["--auto"]
|
||
|
command += ["--brightness", str(output.brightness)]
|
||
|
command += ["--rotate", output.rotation]
|
||
|
command += ["--reflect", output.reflection]
|
||
|
else:
|
||
|
command += ["--off"]
|
||
|
if firstActive:
|
||
|
prevFirstActive = firstActive
|
||
|
self.root.after_idle(lambda: self.executeXrandrCommand(command))
|
||
|
|
||
|
def executeXrandrCommand(self, command):
|
||
|
try:
|
||
|
check_output(command, universal_newlines=True)
|
||
|
except CalledProcessError as err:
|
||
|
self.infoLabel.config(text="xrandr returned nonzero exit status {}".format(err.returncode), fg="red")
|
||
|
|
||
|
def getUserConfirmationIfDangerousConfiguration(self):
|
||
|
result = "yes"
|
||
|
if all(map(lambda o: o.blanked or not o.active, self.outputs)):
|
||
|
result = messagebox.askquestion("All blanked or off",
|
||
|
"All ouputs are set to be turned off or blanked, continue?",
|
||
|
icon="warning")
|
||
|
return result == "yes"
|
||
|
|
||
|
def sameAsPartition(self):
|
||
|
partition = []
|
||
|
for output in self.outputs:
|
||
|
place = None
|
||
|
for p in partition:
|
||
|
if place != None:
|
||
|
break;
|
||
|
for o in p:
|
||
|
if place == None and (output.sameAs == o.name or o.sameAs == output.name):
|
||
|
place = p
|
||
|
break;
|
||
|
if place:
|
||
|
place.append(output)
|
||
|
else:
|
||
|
partition.append([output])
|
||
|
return partition
|
||
|
|
||
|
|
||
|
def handleCancel(self, e=None):
|
||
|
self.root.destroy()
|
||
|
|
||
|
def handleArandr(self, e=None):
|
||
|
call(["i3-msg", "-q", "exec", "arandr"])
|
||
|
self.root.destroy()
|
||
|
|
||
|
def handleUp(self, e):
|
||
|
row = e.widget.output.row
|
||
|
if row > 0:
|
||
|
self.swapOutputRows(row-1, row)
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def handleDown(self, e):
|
||
|
row = e.widget.output.row
|
||
|
n = len(self.outputs)
|
||
|
if row + 1 < n:
|
||
|
self.swapOutputRows(row, row+1)
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def swapOutputRows(self, row1, row2):
|
||
|
outputs = self.outputs
|
||
|
outputs[row1],outputs[row2] = outputs[row2],outputs[row1]
|
||
|
outputs[row1].row = row1
|
||
|
outputs[row2].row = row2
|
||
|
for widget in self.frame.grid_slaves(row=row2):
|
||
|
widget.output = outputs[row2]
|
||
|
for widget in self.frame.grid_slaves(row=row1):
|
||
|
widget.output = outputs[row1]
|
||
|
|
||
|
def setPrimary(self, e):
|
||
|
output = e.widget.output
|
||
|
output.primary = not output.primary
|
||
|
for otherOutput in self.outputs:
|
||
|
if otherOutput != output:
|
||
|
otherOutput.primary = False
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def existsPrimary(self):
|
||
|
for output in self.outputs:
|
||
|
if output.primary:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def toggleActive(self, e):
|
||
|
output = e.widget.output
|
||
|
output.active = not output.active
|
||
|
if output.active:
|
||
|
output.setPreferredMode()
|
||
|
else:
|
||
|
for otherOutput in self.outputs:
|
||
|
if otherOutput.sameAs == output.name:
|
||
|
otherOutput.sameAs = None
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def toggleBlanked(self, e):
|
||
|
output = e.widget.output
|
||
|
if output.blanked:
|
||
|
output.blanked = False
|
||
|
output.brightness = 1.0
|
||
|
else:
|
||
|
output.blanked = True
|
||
|
output.brightness = 0.0
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def updateBrightness(self, e):
|
||
|
output = e.widget.output
|
||
|
output.brightness = .01 * e.widget.get()
|
||
|
output.blanked = False
|
||
|
if abs(output.brightness) < 1e-09:
|
||
|
output.blanked = True
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def cycleRotation(self, e):
|
||
|
output = e.widget.output
|
||
|
if output.rotation == "normal":
|
||
|
output.rotation = "right"
|
||
|
elif output.rotation == "right":
|
||
|
output.rotation = "inverted"
|
||
|
elif output.rotation == "inverted":
|
||
|
output.rotation = "left"
|
||
|
else:
|
||
|
output.rotation = "normal"
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def rotationSymbol(self, rotation):
|
||
|
return {
|
||
|
"normal": ROTATION_NORMAL,
|
||
|
"left": ROTATION_LEFT,
|
||
|
"right": ROTATION_RIGHT,
|
||
|
"inverted": ROTATION_INVERTED,
|
||
|
}[rotation]
|
||
|
|
||
|
def cycleReflection(self, e):
|
||
|
output = e.widget.output
|
||
|
if output.reflection == "normal":
|
||
|
output.reflection = "x"
|
||
|
elif output.reflection == "x":
|
||
|
output.reflection = "y"
|
||
|
elif output.reflection == "y":
|
||
|
output.reflection = "xy"
|
||
|
else:
|
||
|
output.reflection = "normal"
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def reflectionSymbol(self, reflection):
|
||
|
return {
|
||
|
"normal": REFLECTION_NORMAL,
|
||
|
"x": REFLECTION_X,
|
||
|
"y": REFLECTION_Y,
|
||
|
"xy": REFLECTION_XY,
|
||
|
}[reflection]
|
||
|
|
||
|
def toggleDuplicate(self, e):
|
||
|
duplicateButton = e.widget
|
||
|
optionMenu = duplicateButton.statusOptionMenu
|
||
|
output = optionMenu.output
|
||
|
if output.sameAs != None:
|
||
|
output.sameAs = None
|
||
|
self.setMenuToOutput(optionMenu, output)
|
||
|
else:
|
||
|
self.setMenuToDuplicate(optionMenu)
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def getDuplicableOutputsFor(self, output):
|
||
|
return [o for o in self.outputs if o != output and o.sameAs == None]
|
||
|
|
||
|
def softRefreshList(self, e=None):
|
||
|
for widget in set().union(self.nameLabels, self.primaryButtons,
|
||
|
self.statusOptionMenus, self.blankedButtons,
|
||
|
self.duplicateButtons, self.rotateButtons, self.reflectButtons,
|
||
|
self.brightnessSliders, self.upButtons, self.downButtons):
|
||
|
widget.config(fg="gray" if not widget.output.active else "black")
|
||
|
|
||
|
for widget in self.toggleButtons:
|
||
|
widget.config(text=TOGGLE_ON if widget.output.active else TOGGLE_OFF)
|
||
|
|
||
|
for widget in self.nameLabels:
|
||
|
widget.config(text=widget.output.name)
|
||
|
|
||
|
for widget in self.primaryButtons:
|
||
|
widget.config(text=PRIMARY_SYMBOL if widget.output.primary else SECONDARY_SYMBOL)
|
||
|
if not widget.output.primary:
|
||
|
widget.config(fg="gray")
|
||
|
|
||
|
for widget in self.statusOptionMenus:
|
||
|
widget.config(text=widget.output.status())
|
||
|
if widget.output.sameAs != None:
|
||
|
self.setMenuToDuplicate(widget)
|
||
|
self.setInfo(widget, "Select output to duplicate")
|
||
|
else:
|
||
|
self.setMenuToOutput(widget, widget.output)
|
||
|
self.setInfo(widget, "Select output mode")
|
||
|
|
||
|
for widget in self.blankedButtons:
|
||
|
widget.config(text=BLANKED_SYMBOL if widget.output.blanked else UNBLANKED_SYMBOL)
|
||
|
|
||
|
for widget in self.duplicateButtons:
|
||
|
widget.config(text=CLONED_SYMBOL if widget.output.sameAs else NOT_CLONED_SYMBOL)
|
||
|
|
||
|
for widget in self.rotateButtons:
|
||
|
widget.config(text=self.rotationSymbol(widget.output.rotation))
|
||
|
|
||
|
for widget in self.reflectButtons:
|
||
|
widget.config(text=self.reflectionSymbol(widget.output.reflection))
|
||
|
|
||
|
for widget in self.brightnessSliders:
|
||
|
widget.set(int(100*widget.output.brightness))
|
||
|
|
||
|
def hardRefreshList(self, e=None):
|
||
|
self.outputs = Output.realOutputs()
|
||
|
self.root.after_idle(self.populateGrid)
|
||
|
|
||
|
def populateGrid(self):
|
||
|
oldFrame = self.frame
|
||
|
self.frame = Frame(self.root)
|
||
|
self.frame.grid(row=0, column=0, columnspan=len(self.bottomRow))
|
||
|
self.toggleButtons = []
|
||
|
self.nameLabels = []
|
||
|
self.primaryButtons = []
|
||
|
self.statusOptionMenus = []
|
||
|
self.blankedButtons = []
|
||
|
self.duplicateButtons = []
|
||
|
self.rotateButtons = []
|
||
|
self.reflectButtons = []
|
||
|
self.brightnessSliders = []
|
||
|
self.upButtons = []
|
||
|
self.downButtons = []
|
||
|
for row, output in enumerate(self.outputs):
|
||
|
self.makeLabelRow(output, row)
|
||
|
self.registerBindings()
|
||
|
if oldFrame:
|
||
|
oldFrame.destroy()
|
||
|
|
||
|
def makeLabelRow(self, output, row):
|
||
|
output.row = row
|
||
|
style = {'relief':FLAT, 'padx':1, 'pady':1, 'anchor':'w'}
|
||
|
widgets = []
|
||
|
|
||
|
toggleButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
|
||
|
toggleButton.output = output
|
||
|
self.toggleButtons.append(toggleButton)
|
||
|
if SHOW_ON_OFF:
|
||
|
widgets.append(toggleButton)
|
||
|
|
||
|
nameLabel = Label(self.frame, font=DEFAULT_FONT)
|
||
|
nameLabel.output = output
|
||
|
self.nameLabels.append(nameLabel)
|
||
|
if SHOW_NAMES:
|
||
|
widgets.append(nameLabel)
|
||
|
|
||
|
primaryButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
|
||
|
primaryButton.output = output
|
||
|
self.primaryButtons.append(primaryButton)
|
||
|
if not output.primary:
|
||
|
primaryButton.config(fg="gray")
|
||
|
if SHOW_PRIMARY:
|
||
|
widgets.append(primaryButton)
|
||
|
|
||
|
var = StringVar(self.frame)
|
||
|
statusOptionMenu = OptionMenu(self.frame, var, None)
|
||
|
statusOptionMenu.output = output
|
||
|
statusOptionMenu.var = var
|
||
|
statusOptionMenu.config(relief=FLAT)
|
||
|
self.statusOptionMenus.append(statusOptionMenu)
|
||
|
if SHOW_MODE or SHOW_DUPLICATE:
|
||
|
widgets.append(statusOptionMenu)
|
||
|
|
||
|
blankedButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
|
||
|
blankedButton.output = output
|
||
|
self.blankedButtons.append(blankedButton)
|
||
|
if SHOW_BLANKED:
|
||
|
widgets.append(blankedButton)
|
||
|
|
||
|
duplicateButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
|
||
|
duplicateButton.statusOptionMenu = statusOptionMenu
|
||
|
duplicateButton.output = output
|
||
|
self.duplicateButtons.append(duplicateButton)
|
||
|
if SHOW_DUPLICATE:
|
||
|
widgets.append(duplicateButton)
|
||
|
|
||
|
rotateButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
|
||
|
rotateButton.output = output
|
||
|
self.rotateButtons.append(rotateButton)
|
||
|
if SHOW_ROTATION:
|
||
|
widgets.append(rotateButton)
|
||
|
|
||
|
reflectButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
|
||
|
reflectButton.output = output
|
||
|
self.reflectButtons.append(reflectButton)
|
||
|
if SHOW_REFLECTION:
|
||
|
widgets.append(reflectButton)
|
||
|
|
||
|
brightnessSlider = Scale(self.frame, orient="horizontal", from_=0, to=100,
|
||
|
length=BRIGHTNESS_SLIDER_LENGTH, showvalue=SHOW_BRIGHTNESS_VALUE,
|
||
|
sliderlength=BRIGHTNESS_SLIDER_HANDLE_LENGTH,
|
||
|
width=BRIGHTNESS_SLIDER_WIDTH, font=FONTAWESOME_FONT)
|
||
|
brightnessSlider.output = output
|
||
|
self.brightnessSliders.append(brightnessSlider)
|
||
|
if SHOW_BRIGHTNESS:
|
||
|
widgets.append(brightnessSlider)
|
||
|
|
||
|
upButton = Button(self.frame, text=UP_ARROW, font=FONTAWESOME_FONT, **style)
|
||
|
upButton.output = output
|
||
|
self.upButtons.append(upButton)
|
||
|
if SHOW_UP_DOWN:
|
||
|
widgets.append(upButton)
|
||
|
|
||
|
downButton = Button(self.frame, text=DOWN_ARROW, font=FONTAWESOME_FONT, **style)
|
||
|
downButton.output = output
|
||
|
self.downButtons.append(downButton)
|
||
|
if SHOW_UP_DOWN:
|
||
|
widgets.append(downButton)
|
||
|
|
||
|
for widget in widgets:
|
||
|
widget.output = output
|
||
|
self.gridRow(row, widgets)
|
||
|
self.softRefreshList()
|
||
|
|
||
|
def setMenuToOutput(self, optionMenu, output):
|
||
|
menu = optionMenu["menu"]
|
||
|
var = optionMenu.var
|
||
|
modes = output.modes
|
||
|
menu.delete(0, END)
|
||
|
for i, mode in enumerate(modes):
|
||
|
label = Output.modestr(mode)
|
||
|
menu.add_command(label=label, command=setLabelAndOutputModeFunc(var,label,output,i))
|
||
|
if output.currentModeIndex != None:
|
||
|
var.set(Output.modestr(modes[output.currentModeIndex]))
|
||
|
elif output.preferredModeIndex != None:
|
||
|
var.set(Output.modestr(modes[output.preferredModeIndex]))
|
||
|
elif len(modes) > 0:
|
||
|
var.set(Output.modestr(modes[0]))
|
||
|
|
||
|
def setMenuToDuplicate(self, optionMenu):
|
||
|
menu = optionMenu["menu"]
|
||
|
var = optionMenu.var
|
||
|
output = optionMenu.output
|
||
|
menu.delete(0, END)
|
||
|
duplicables = self.getDuplicableOutputsFor(output)
|
||
|
defaultIndex = 0
|
||
|
for i,otherOutput in enumerate(duplicables):
|
||
|
label = otherOutput.name
|
||
|
menu.add_command(label=label, command=setLabelAndSameAsFunc(var,label,output))
|
||
|
if label == output.sameAs:
|
||
|
defaultIndex = i
|
||
|
if len(duplicables) > 0:
|
||
|
var.set(menu.entrycget(defaultIndex, "label"))
|
||
|
output.sameAs = menu.entrycget(defaultIndex, "label")
|
||
|
else:
|
||
|
var.set("None")
|
||
|
|
||
|
def handleFocusOut(self, event):
|
||
|
self.root.destroy()
|
||
|
|
||
|
def setLabelAndOutputModeFunc(var, label, output, i):
|
||
|
def func():
|
||
|
var.set(label)
|
||
|
output.setMode(i)
|
||
|
return func
|
||
|
|
||
|
def setLabelAndSameAsFunc(var, sameAs, output):
|
||
|
def func():
|
||
|
var.set(sameAs)
|
||
|
output.sameAs = sameAs
|
||
|
return func
|
||
|
|
||
|
if os.environ.get('BLOCK_BUTTON') == "1":
|
||
|
if os.fork() != 0:
|
||
|
root = Tk()
|
||
|
if DEFAULT_FONT_FAMILY and DEFAULT_FONT_SIZE:
|
||
|
font.nametofont("TkDefaultFont").config(family=DEFAULT_FONT_FAMILY, size=DEFAULT_FONT_SIZE)
|
||
|
manager = MonitorManager(root)
|
||
|
root.mainloop()
|
||
|
else:
|
||
|
print(DESKTOP_SYMBOL)
|
||
|
else:
|
||
|
print(DESKTOP_SYMBOL)
|