mirror of
https://github.com/nwg-piotr/nwg-displays.git
synced 2026-04-30 07:49:28 -05:00
1314 lines
44 KiB
Python
1314 lines
44 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Output management utility for sway Wayland compositor, inspired by wdisplays and wlay
|
|
Project: https://github.com/nwg-piotr/nwg-displays
|
|
Author's email: nwg.piotr@gmail.com
|
|
Copyright (c) 2022-2024 Piotr Miller & Contributors
|
|
License: MIT
|
|
Depends on: 'python-i3ipc' 'gtk-layer-shell'
|
|
|
|
All the code below was built around this glorious snippet:
|
|
https://gist.github.com/KurtJacobson/57679e5036dc78e6a7a3ba5e0155dad1
|
|
Thank you, Kurt Jacobson!
|
|
"""
|
|
|
|
import argparse
|
|
import os.path
|
|
import sys
|
|
|
|
import gi
|
|
|
|
gi.require_version('Gtk', '3.0')
|
|
try:
|
|
gi.require_version('GtkLayerShell', '0.1')
|
|
except ValueError:
|
|
raise RuntimeError('\n\n' +
|
|
'If you haven\'t installed GTK Layer Shell, you need to point Python to the\n' +
|
|
'library by setting GI_TYPELIB_PATH and LD_LIBRARY_PATH to <build-dir>/src/.\n' +
|
|
'For example you might need to run:\n\n' +
|
|
'GI_TYPELIB_PATH=build/src LD_LIBRARY_PATH=build/src python3 ' + ' '.join(sys.argv))
|
|
|
|
from gi.repository import Gtk, GLib, GtkLayerShell
|
|
|
|
from nwg_displays.tools import *
|
|
|
|
from nwg_displays.__about__ import __version__
|
|
|
|
dir_name = os.path.dirname(__file__)
|
|
sway = os.getenv("SWAYSOCK") is not None
|
|
hypr = os.getenv("HYPRLAND_INSTANCE_SIGNATURE") is not None
|
|
|
|
config_dir = os.path.join(get_config_home(), "nwg-displays")
|
|
# This was done by mistake, and the config file need to be migrated to the proper path
|
|
old_config_dir = os.path.join(get_config_home(), "nwg-outputs")
|
|
|
|
sway_config_dir = os.path.join(get_config_home(), "sway")
|
|
if sway and not os.path.isdir(sway_config_dir):
|
|
print("WARNING: Couldn't find sway config directory '{}'".format(sway_config_dir), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
hypr_config_dir = os.path.join(get_config_home(), "hypr")
|
|
if hypr and not os.path.isdir(hypr_config_dir):
|
|
print("WARNING: Couldn't find Hyprland config directory '{}'".format(hypr_config_dir), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Create empty files if not found
|
|
if sway:
|
|
for name in ["outputs", "workspaces"]:
|
|
create_empty_file(os.path.join(sway_config_dir, name))
|
|
elif hypr:
|
|
for name in ["monitors.conf", "workspaces.conf"]:
|
|
create_empty_file(os.path.join(hypr_config_dir, name))
|
|
else:
|
|
eprint("Neither sway nor Hyprland detected, terminating")
|
|
sys.exit(1)
|
|
|
|
config = {}
|
|
outputs_path = ""
|
|
num_ws = 0
|
|
|
|
"""
|
|
i3.get_outputs() does not return some output attributes, especially when connected via hdmi.
|
|
i3.get_tree() on the other hand does not return inactive outputs. So we'll list attributes with .get_tree(),
|
|
and the add inactive outputs, if any, from what we detect with .get_outputs()
|
|
"""
|
|
outputs = {} # Active outputs, listed from the sway tree; stores name and all attributes.
|
|
outputs_activity = {} # Just a dictionary "name": is_active - from get_outputs()
|
|
workspaces = {} # "workspace_num": "display_name"
|
|
|
|
display_buttons = []
|
|
selected_output_button = None
|
|
|
|
# Glade form fields
|
|
form_name = None
|
|
form_description = None
|
|
form_dpms = None
|
|
form_adaptive_sync = None
|
|
form_custom_mode = None
|
|
form_view_scale = None
|
|
form_use_desc = None
|
|
form_x = None
|
|
form_y = None
|
|
form_width = None
|
|
form_height = None
|
|
form_scale = None
|
|
form_scale_filter = None
|
|
form_refresh = None
|
|
form_modes = None
|
|
form_transform = None
|
|
form_wrapper_box = None
|
|
form_workspaces = None
|
|
form_close = None
|
|
form_apply = None
|
|
form_version = None
|
|
form_mirror = None
|
|
form_ten_bit = None
|
|
|
|
dialog_win = None
|
|
confirm_win = None
|
|
src_tag = 0
|
|
counter = 0
|
|
|
|
"""
|
|
We need to rebuild the modes GtkComboBoxText on each DisplayButton click. Unfortunately appending an item fires the
|
|
"change" event every time (and we have no "value-changed" event here). Setting `on_mode_changed_silent` True will
|
|
prevent the `on_mode_changed` function from working.
|
|
"""
|
|
on_mode_changed_silent = False
|
|
|
|
# Value from config adjusted to current view scale
|
|
snap_threshold_scaled = None
|
|
|
|
fixed = Gtk.Fixed()
|
|
|
|
SENSITIVITY = 1
|
|
|
|
EvMask = Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON1_MOTION_MASK
|
|
|
|
offset_x = 0
|
|
offset_y = 0
|
|
px = 0
|
|
py = 0
|
|
max_x = 0
|
|
max_y = 0
|
|
|
|
voc = {}
|
|
|
|
|
|
def load_vocabulary():
|
|
global voc
|
|
# basic vocabulary (for en_US)
|
|
voc = load_json(os.path.join(dir_name, "langs", "en_US.json"))
|
|
if not voc:
|
|
eprint("Failed loading vocabulary, terminating")
|
|
sys.exit(1)
|
|
|
|
shell_data = load_shell_data()
|
|
|
|
lang = os.getenv("LANG")
|
|
if lang is None:
|
|
lang = "en_US"
|
|
else:
|
|
lang = lang.split(".")[0] if not shell_data["interface-locale"] else shell_data["interface-locale"]
|
|
|
|
# translate if translation available
|
|
if lang != "en_US":
|
|
loc_file = os.path.join(dir_name, "langs", "{}.json".format(lang))
|
|
if os.path.isfile(loc_file):
|
|
# localized vocabulary
|
|
loc = load_json(loc_file)
|
|
if not loc:
|
|
eprint("Failed loading translation into '{}'".format(lang))
|
|
else:
|
|
for key in loc:
|
|
voc[key] = loc[key]
|
|
|
|
|
|
def on_button_press_event(widget, event):
|
|
if widget != selected_output_button:
|
|
widget.indicator.show_up()
|
|
|
|
if event.button == 1:
|
|
for db in display_buttons:
|
|
if db.name == widget.name:
|
|
db.select()
|
|
else:
|
|
db.unselect()
|
|
|
|
p = widget.get_parent()
|
|
# offset == distance of parent widget from edge of screen ...
|
|
global offset_x, offset_y
|
|
offset_x, offset_y = p.get_window().get_position()
|
|
# plus distance from pointer to edge of widget
|
|
offset_x += event.x
|
|
offset_y += event.y
|
|
# max_x, max_y both relative to the parent
|
|
# note that we're rounding down now so that these max values don't get
|
|
# rounded upward later and push the widget off the edge of its parent.
|
|
global max_x, max_y
|
|
max_x = round_down_to_multiple(p.get_allocation().width - widget.get_allocation().width, SENSITIVITY)
|
|
max_y = round_down_to_multiple(p.get_allocation().height - widget.get_allocation().height, SENSITIVITY)
|
|
|
|
update_form_from_widget(widget)
|
|
|
|
|
|
def on_motion_notify_event(widget, event):
|
|
# x_root,x_root relative to screen
|
|
# x,y relative to parent (fixed widget)
|
|
# px,py stores previous values of x,y
|
|
|
|
global px, py
|
|
global offset_x, offset_y
|
|
|
|
# get starting values for x,y
|
|
x = event.x_root - offset_x
|
|
y = event.y_root - offset_y
|
|
# make sure the potential coordinates x,y:
|
|
# 1) will not push any part of the widget outside of its parent container
|
|
# 2) is a multiple of SENSITIVITY
|
|
x = round_to_nearest_multiple(max_val(min_val(x, max_x), 0), SENSITIVITY)
|
|
y = round_to_nearest_multiple(max_val(min_val(y, max_y), 0), SENSITIVITY)
|
|
|
|
if x != px or y != py:
|
|
px = x
|
|
py = y
|
|
snap_x, snap_y = [0], [0]
|
|
for db in display_buttons:
|
|
if db.name == widget.name:
|
|
continue
|
|
|
|
val = db.x * config["view-scale"]
|
|
if val not in snap_x:
|
|
snap_x.append(val)
|
|
|
|
val = (db.x + db.logical_width) * config["view-scale"]
|
|
if val not in snap_x:
|
|
snap_x.append(val)
|
|
|
|
val = db.y * config["view-scale"]
|
|
if val not in snap_y:
|
|
snap_y.append(val)
|
|
|
|
val = (db.y + db.logical_height) * config["view-scale"]
|
|
if val not in snap_y:
|
|
snap_y.append(val)
|
|
|
|
snap_h, snap_v = None, None
|
|
for value in snap_x:
|
|
if abs(x - value) < snap_threshold_scaled:
|
|
snap_h = value
|
|
break
|
|
|
|
for value in snap_x:
|
|
w = widget.logical_width * config["view-scale"]
|
|
if abs(w + x - value) < snap_threshold_scaled:
|
|
snap_h = value - w
|
|
break
|
|
|
|
for value in snap_y:
|
|
if abs(y - value) < snap_threshold_scaled:
|
|
snap_v = value
|
|
break
|
|
|
|
for value in snap_y:
|
|
h = widget.logical_height * config["view-scale"]
|
|
if abs(h + y - value) < snap_threshold_scaled:
|
|
snap_v = value - h
|
|
break
|
|
|
|
# Just in case ;)
|
|
if snap_h and snap_h < 0:
|
|
snap_h = 0
|
|
|
|
if snap_v and snap_v < 0:
|
|
snap_v = 0
|
|
|
|
if snap_h is None and snap_v is None:
|
|
fixed.move(widget, x, y)
|
|
widget.x = round(x / config["view-scale"])
|
|
widget.y = round(y / config["view-scale"])
|
|
else:
|
|
|
|
if snap_h is not None and snap_v is not None:
|
|
fixed.move(widget, snap_h, snap_v)
|
|
widget.x = round(snap_h / config["view-scale"])
|
|
widget.y = round(snap_v / config["view-scale"])
|
|
|
|
elif snap_h is not None:
|
|
fixed.move(widget, snap_h, y)
|
|
widget.x = round(snap_h / config["view-scale"])
|
|
widget.y = round(y / config["view-scale"])
|
|
|
|
elif snap_v is not None:
|
|
fixed.move(widget, x, snap_v)
|
|
widget.x = round(x / config["view-scale"])
|
|
widget.y = round(snap_v / config["view-scale"])
|
|
|
|
update_form_from_widget(widget)
|
|
|
|
|
|
def update_form_from_widget(widget):
|
|
form_name.set_text(widget.name)
|
|
if len(widget.description) > 48:
|
|
form_description.set_text(f"{widget.description[:47]}(…)")
|
|
else:
|
|
form_description.set_text(widget.description)
|
|
form_dpms.set_active(widget.dpms)
|
|
form_adaptive_sync.set_active(widget.adaptive_sync)
|
|
form_custom_mode.set_active(widget.custom_mode)
|
|
form_view_scale.set_value(config["view-scale"]) # not really from the widget, but from the global value
|
|
form_use_desc.set_active(config["use-desc"])
|
|
form_x.set_value(widget.x)
|
|
form_y.set_value(widget.y)
|
|
form_width.set_value(widget.physical_width)
|
|
form_height.set_value(widget.physical_height)
|
|
form_scale.set_value(widget.scale)
|
|
form_scale_filter.set_active_id(widget.scale_filter)
|
|
form_refresh.set_value(widget.refresh)
|
|
if form_ten_bit:
|
|
form_ten_bit.set_active(widget.ten_bit)
|
|
if form_mirror:
|
|
form_mirror.remove_all()
|
|
form_mirror.append("", voc["none"])
|
|
for key in outputs:
|
|
if key != widget.name:
|
|
form_mirror.append(key, key)
|
|
form_mirror.set_active_id(widget.mirror)
|
|
form_mirror.show_all()
|
|
|
|
global on_mode_changed_silent
|
|
on_mode_changed_silent = True
|
|
|
|
form_modes.remove_all()
|
|
active = ""
|
|
for mode in widget.modes:
|
|
m = "{}x{}@{}Hz".format(mode["width"], mode["height"], mode["refresh"] / 1000, mode[
|
|
"refresh"] / 1000, widget.refresh)
|
|
form_modes.append(m, m)
|
|
# This is just to set active_id
|
|
|
|
if mode["width"] == widget.physical_width and mode["height"] == widget.physical_height and mode[
|
|
"refresh"] / 1000 == widget.refresh:
|
|
active = m
|
|
if active:
|
|
form_modes.set_active_id(active)
|
|
|
|
form_transform.set_active_id(widget.transform)
|
|
|
|
on_mode_changed_silent = False
|
|
|
|
|
|
class DisplayButton(Gtk.Button):
|
|
def __init__(self, name, description, x, y, physical_width, physical_height, transform, scale, scale_filter,
|
|
refresh, modes, active, dpms, adaptive_sync_status, ten_bit, custom_mode_status, focused, monitor,
|
|
mirror=""):
|
|
super().__init__()
|
|
# Output properties
|
|
self.name = name
|
|
self.description = description
|
|
self.x = x
|
|
self.y = y
|
|
self.physical_width = physical_width
|
|
self.physical_height = physical_height
|
|
self.transform = transform
|
|
self.scale = scale
|
|
self.scale_filter = scale_filter
|
|
self.refresh = refresh
|
|
self.modes = []
|
|
for m in modes:
|
|
if m not in self.modes:
|
|
self.modes.append(m)
|
|
# self.modes = modes
|
|
self.active = active
|
|
self.dpms = dpms
|
|
self.adaptive_sync = adaptive_sync_status == "enabled" # converts "enabled | disabled" to bool
|
|
self.custom_mode = custom_mode_status
|
|
self.focused = focused
|
|
self.mirror = mirror
|
|
self.ten_bit = ten_bit
|
|
|
|
# Button properties
|
|
self.selected = False
|
|
self.set_can_focus(False)
|
|
self.set_events(EvMask)
|
|
self.connect("button_press_event", on_button_press_event)
|
|
self.connect("motion_notify_event", on_motion_notify_event)
|
|
self.set_always_show_image(True)
|
|
self.set_label(self.name)
|
|
|
|
self.rescale_transform()
|
|
self.set_property("name", "output")
|
|
|
|
self.indicator = Indicator(monitor, name, round(self.physical_width * config["view-scale"]),
|
|
round(self.physical_height * config["view-scale"]), config["indicator-timeout"])
|
|
|
|
self.show()
|
|
|
|
@property
|
|
def logical_width(self):
|
|
if is_rotated(self.transform):
|
|
return self.physical_height / self.scale
|
|
else:
|
|
return self.physical_width / self.scale
|
|
|
|
@property
|
|
def logical_height(self):
|
|
if is_rotated(self.transform):
|
|
return self.physical_width / self.scale
|
|
else:
|
|
return self.physical_height / self.scale
|
|
|
|
def select(self):
|
|
self.selected = True
|
|
self.set_property("name", "selected-output")
|
|
global selected_output_button
|
|
selected_output_button = self
|
|
|
|
def unselect(self):
|
|
self.set_property("name", "output")
|
|
|
|
def rescale_transform(self):
|
|
self.set_size_request(round(self.logical_width * config["view-scale"]),
|
|
round(self.logical_height * config["view-scale"]))
|
|
|
|
def on_active_check_button_toggled(self, w):
|
|
self.active = w.get_active()
|
|
if not self.active:
|
|
self.set_property("name", "inactive-output")
|
|
else:
|
|
if self == selected_output_button:
|
|
self.set_property("name", "selected-output")
|
|
else:
|
|
self.set_property("name", "output")
|
|
|
|
|
|
def on_view_scale_changed(*args):
|
|
config["view-scale"] = round(form_view_scale.get_value(), 2)
|
|
|
|
global snap_threshold_scaled
|
|
snap_threshold_scaled = round(config["snap-threshold"] * config["view-scale"] * 10)
|
|
|
|
for b in display_buttons:
|
|
b.rescale_transform()
|
|
fixed.move(b, b.x * config["view-scale"], b.y * config["view-scale"])
|
|
|
|
save_json(config, os.path.join(config_dir, "config"))
|
|
|
|
|
|
def on_transform_changed(*args):
|
|
if selected_output_button:
|
|
transform = form_transform.get_active_id()
|
|
selected_output_button.transform = transform
|
|
selected_output_button.rescale_transform()
|
|
|
|
|
|
def on_ten_bit_toggled(check_btn):
|
|
if selected_output_button:
|
|
selected_output_button.ten_bit = check_btn.get_active()
|
|
|
|
|
|
def on_dpms_toggled(widget):
|
|
if selected_output_button:
|
|
selected_output_button.dpms = widget.get_active()
|
|
|
|
|
|
def on_use_desc_toggled(widget):
|
|
config["use-desc"] = widget.get_active()
|
|
save_json(config, os.path.join(config_dir, "config"))
|
|
|
|
|
|
def on_adaptive_sync_toggled(widget):
|
|
if selected_output_button:
|
|
selected_output_button.adaptive_sync = widget.get_active()
|
|
|
|
|
|
def on_custom_mode_toggle(widget):
|
|
if selected_output_button:
|
|
outputs = set(config["custom-mode"])
|
|
turned_on = widget.get_active()
|
|
selected_output_button.custom_mode = turned_on
|
|
if turned_on:
|
|
outputs.add(selected_output_button.name)
|
|
else:
|
|
outputs.discard(selected_output_button.name)
|
|
config["custom-mode"] = tuple(outputs)
|
|
|
|
|
|
def on_pos_x_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.x = round(widget.get_value())
|
|
fixed.move(selected_output_button, selected_output_button.x * config["view-scale"],
|
|
selected_output_button.y * config["view-scale"])
|
|
|
|
|
|
def on_pos_y_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.y = round(widget.get_value())
|
|
fixed.move(selected_output_button, selected_output_button.x * config["view-scale"],
|
|
selected_output_button.y * config["view-scale"])
|
|
|
|
|
|
def on_width_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.physical_width = round(widget.get_value())
|
|
selected_output_button.rescale_transform()
|
|
|
|
|
|
def on_height_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.physical_height = round(widget.get_value())
|
|
selected_output_button.rescale_transform()
|
|
|
|
|
|
def on_scale_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.scale = widget.get_value()
|
|
selected_output_button.rescale_transform()
|
|
|
|
|
|
def on_scale_filter_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.scale_filter = widget.get_active_id()
|
|
|
|
|
|
def on_refresh_changed(widget):
|
|
if selected_output_button:
|
|
selected_output_button.refresh = widget.get_value()
|
|
|
|
update_form_from_widget(selected_output_button)
|
|
|
|
|
|
def on_mode_changed(widget):
|
|
if selected_output_button and not on_mode_changed_silent:
|
|
mode = selected_output_button.modes[widget.get_active()]
|
|
selected_output_button.physical_width = mode["width"]
|
|
selected_output_button.physical_height = mode["height"]
|
|
selected_output_button.refresh = mode["refresh"] / 1000
|
|
selected_output_button.rescale_transform()
|
|
|
|
update_form_from_widget(selected_output_button)
|
|
|
|
|
|
def on_mirror_selected(widget):
|
|
if selected_output_button and widget.get_active_id() is not None:
|
|
selected_output_button.mirror = widget.get_active_id()
|
|
|
|
|
|
def on_apply_button(widget):
|
|
global outputs_activity
|
|
apply_settings(display_buttons, outputs_activity, outputs_path, use_desc=config["use-desc"])
|
|
# save config file
|
|
save_json(config, os.path.join(config_dir, "config"))
|
|
|
|
|
|
def on_output_toggled(check_btn, name):
|
|
global outputs_activity
|
|
outputs_activity[name] = check_btn.get_active()
|
|
|
|
|
|
def on_toggle_button(btn):
|
|
i3 = Connection()
|
|
global outputs_activity
|
|
for key in outputs_activity:
|
|
toggle = "enable" if outputs_activity[key] else "disable"
|
|
cmd = "output {} {}".format(key, toggle)
|
|
i3.command(cmd)
|
|
|
|
# If the output has just been turned back on, Gdk.Display.get_default() may need some time
|
|
GLib.timeout_add(1000, create_display_buttons)
|
|
|
|
|
|
def create_display_buttons():
|
|
global display_buttons
|
|
for item in display_buttons:
|
|
item.destroy()
|
|
display_buttons = []
|
|
|
|
global outputs
|
|
outputs = list_outputs()
|
|
for key in outputs:
|
|
item = outputs[key]
|
|
custom_mode = key in config["custom-mode"]
|
|
b = DisplayButton(key, item["description"], item["x"], item["y"], round(item["physical-width"]),
|
|
round(item["physical-height"]),
|
|
item["transform"], item["scale"], item["scale_filter"], item["refresh"], item["modes"],
|
|
item["active"], item["dpms"], item["adaptive_sync_status"], item["ten_bit"], custom_mode,
|
|
item["focused"], item["monitor"], mirror=item["mirror"])
|
|
|
|
display_buttons.append(b)
|
|
|
|
fixed.put(b, round(item["x"] * config["view-scale"]), round(item["y"] * config["view-scale"]))
|
|
|
|
display_buttons[0].select()
|
|
update_form_from_widget(display_buttons[0])
|
|
|
|
|
|
class Indicator(Gtk.Window):
|
|
def __init__(self, monitor, name, width, height, timeout):
|
|
super().__init__()
|
|
self.timeout = timeout
|
|
self.monitor = monitor
|
|
self.set_property("name", "indicator")
|
|
|
|
GtkLayerShell.init_for_window(self)
|
|
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
|
|
if monitor:
|
|
GtkLayerShell.set_monitor(self, monitor)
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
self.add(box)
|
|
label = Gtk.Label()
|
|
box.set_property("name", "indicator-label")
|
|
label.set_text(name)
|
|
box.pack_start(label, True, True, 10)
|
|
|
|
self.set_size_request(width, height)
|
|
if self.timeout > 0:
|
|
self.show_up(self.timeout * 2)
|
|
|
|
def show_up(self, timeout=None):
|
|
if self.timeout > 0 and self.monitor:
|
|
self.show_all()
|
|
if timeout:
|
|
GLib.timeout_add(timeout, self.hide)
|
|
else:
|
|
GLib.timeout_add(self.timeout, self.hide)
|
|
|
|
|
|
def handle_keyboard(window, event):
|
|
if event.type == Gdk.EventType.KEY_RELEASE and event.keyval == Gdk.KEY_Escape:
|
|
window.close()
|
|
|
|
|
|
def create_workspaces_window(btn):
|
|
global sway_config_dir
|
|
global workspaces
|
|
workspaces = load_workspaces(os.path.join(sway_config_dir, "workspaces"), use_desc=config["use-desc"])
|
|
old_workspaces = workspaces.copy()
|
|
global dialog_win
|
|
if dialog_win:
|
|
dialog_win.destroy()
|
|
dialog_win = Gtk.Window()
|
|
dialog_win.set_resizable(False)
|
|
dialog_win.set_modal(True)
|
|
dialog_win.connect("key-release-event", handle_keyboard)
|
|
grid = Gtk.Grid()
|
|
for prop in ["margin_start", "margin_end", "margin_top", "margin_bottom"]:
|
|
grid.set_property(prop, 10)
|
|
grid.set_column_spacing(12)
|
|
grid.set_row_spacing(12)
|
|
dialog_win.add(grid)
|
|
global num_ws
|
|
global outputs
|
|
last_row = 0
|
|
for i in range(num_ws):
|
|
lbl = Gtk.Label()
|
|
lbl.set_text("workspace {} output ".format(i + 1))
|
|
grid.attach(lbl, 0, i, 1, 1)
|
|
combo = Gtk.ComboBoxText()
|
|
for key in outputs:
|
|
if not config["use-desc"]:
|
|
combo.append(key, key)
|
|
else:
|
|
desc = "{}".format(outputs[key]["description"])
|
|
combo.append(desc, desc)
|
|
if i + 1 in workspaces:
|
|
combo.set_active_id(workspaces[i + 1])
|
|
combo.connect("changed", on_ws_combo_changed, i + 1)
|
|
grid.attach(combo, 1, i, 1, 1)
|
|
last_row = i
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
grid.attach(box, 0, last_row + 1, 2, 1)
|
|
|
|
btn_apply = Gtk.Button()
|
|
btn_apply.set_label(voc["apply"])
|
|
if sway_config_dir:
|
|
btn_apply.connect("clicked", on_workspaces_apply_btn, dialog_win, old_workspaces)
|
|
else:
|
|
btn_apply.set_sensitive(False)
|
|
btn_apply.set_tooltip_text("Config dir not found")
|
|
box.pack_end(btn_apply, False, False, 0)
|
|
|
|
btn_close = Gtk.Button()
|
|
btn_close.set_label(voc["close"])
|
|
btn_close.connect("clicked", close_dialog, dialog_win)
|
|
box.pack_end(btn_close, False, False, 6)
|
|
|
|
dialog_win.show_all()
|
|
|
|
|
|
def create_workspaces_window_hypr(btn):
|
|
global workspaces
|
|
workspaces = load_workspaces_hypr(
|
|
os.path.join(hypr_config_dir, "workspaces.conf"), num_ws=num_ws)
|
|
eprint("WS->Mon:", workspaces)
|
|
old_workspaces = workspaces.copy()
|
|
global dialog_win
|
|
if dialog_win:
|
|
dialog_win.destroy()
|
|
dialog_win = Gtk.Window()
|
|
dialog_win.set_resizable(False)
|
|
dialog_win.set_modal(True)
|
|
dialog_win.connect("key-release-event", handle_keyboard)
|
|
grid = Gtk.Grid()
|
|
for prop in ["margin_start", "margin_end", "margin_top", "margin_bottom"]:
|
|
grid.set_property(prop, 10)
|
|
grid.set_column_spacing(12)
|
|
grid.set_row_spacing(6)
|
|
dialog_win.add(grid)
|
|
global outputs
|
|
last_row = 0
|
|
for i in range(num_ws):
|
|
lbl = Gtk.Label()
|
|
if config["use-desc"]:
|
|
lbl.set_markup("Workspace rule: <b>workspace={},monitor:desc:</b>".format(i + 1))
|
|
else:
|
|
lbl.set_markup("Workspace rule: <b>workspace={},monitor:</b>".format(i + 1))
|
|
lbl.set_property("halign", Gtk.Align.END)
|
|
grid.attach(lbl, 0, i, 1, 1)
|
|
combo = Gtk.ComboBoxText()
|
|
for key in outputs:
|
|
if not config["use-desc"]:
|
|
combo.append(key, key)
|
|
else:
|
|
desc = "{}".format(outputs[key]["description"])
|
|
combo.append(desc, desc)
|
|
if i + 1 in workspaces:
|
|
combo.set_active_id(workspaces[i + 1])
|
|
combo.connect("changed", on_ws_combo_changed, i + 1)
|
|
|
|
grid.attach(combo, 1, i, 1, 1)
|
|
|
|
last_row += 1
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
grid.attach(box, 0, last_row + 1, 2, 1)
|
|
|
|
btn_apply = Gtk.Button()
|
|
btn_apply.set_label(voc["apply"])
|
|
if hypr_config_dir:
|
|
btn_apply.connect("clicked", on_workspaces_apply_btn_hypr, dialog_win, old_workspaces)
|
|
else:
|
|
btn_apply.set_sensitive(False)
|
|
btn_apply.set_tooltip_text("Config dir not found")
|
|
box.pack_end(btn_apply, False, False, 0)
|
|
|
|
btn_close = Gtk.Button()
|
|
btn_close.set_label(voc["close"])
|
|
btn_close.connect("clicked", close_dialog, dialog_win)
|
|
box.pack_end(btn_close, False, False, 6)
|
|
|
|
dialog_win.show_all()
|
|
|
|
|
|
def on_ws_combo_changed(combo, ws_num):
|
|
global workspaces
|
|
workspaces[ws_num] = combo.get_active_id()
|
|
|
|
|
|
def close_dialog(w, win):
|
|
win.close()
|
|
|
|
|
|
def on_workspaces_apply_btn(w, win, old_workspaces):
|
|
global workspaces
|
|
if workspaces != old_workspaces:
|
|
save_workspaces(workspaces, os.path.join(sway_config_dir, "workspaces"), use_desc=config["use-desc"])
|
|
notify("Workspaces assignment", "Restart sway for changes to take effect")
|
|
|
|
close_dialog(w, win)
|
|
|
|
|
|
def on_workspaces_apply_btn_hypr(w, win, old_workspaces):
|
|
global workspaces
|
|
if workspaces != old_workspaces:
|
|
workspace_conf_file = os.path.join(hypr_config_dir, "workspaces.conf")
|
|
text_file = open(workspace_conf_file, "w")
|
|
|
|
now = datetime.datetime.now()
|
|
line = "# Generated by nwg-displays on {} at {}. Do not edit manually.\n".format(
|
|
datetime.datetime.strftime(now, '%Y-%m-%d'),
|
|
datetime.datetime.strftime(now, '%H:%M:%S'))
|
|
text_file.write(line + "\n")
|
|
|
|
monitors_with_default_workspace = []
|
|
for ws in workspaces:
|
|
mon = workspaces[ws]
|
|
if not config["use-desc"]:
|
|
line = "workspace={},monitor:{}".format(ws, mon)
|
|
else:
|
|
line = "workspace={},monitor:desc:{}".format(ws, mon)
|
|
|
|
if mon not in monitors_with_default_workspace:
|
|
line += ",default:true"
|
|
monitors_with_default_workspace.append(mon)
|
|
|
|
text_file.write(line + "\n")
|
|
|
|
text_file.close()
|
|
notify("Workspaces assignment", "Restart Hyprland for changes to take effect")
|
|
|
|
close_dialog(w, win)
|
|
|
|
|
|
def apply_settings(display_buttons, outputs_activity, outputs_path, use_desc=False):
|
|
now = datetime.datetime.now()
|
|
lines = ["# Generated by nwg-displays on {} at {}. Do not edit manually.\n".format(
|
|
datetime.datetime.strftime(now, '%Y-%m-%d'),
|
|
datetime.datetime.strftime(now, '%H:%M:%S'))]
|
|
cmds = []
|
|
db_names = []
|
|
# just active outputs have their buttons
|
|
if os.getenv("SWAYSOCK"):
|
|
for db in display_buttons:
|
|
name = db.name if not use_desc else db.description
|
|
db_names.append(name)
|
|
|
|
lines.append('output "%s" {' % name)
|
|
cmd = 'output "{}"'.format(name)
|
|
|
|
custom_mode_str = "--custom" if db.custom_mode else ""
|
|
lines.append(
|
|
" mode {} {}x{}@{}Hz".format(custom_mode_str, db.physical_width, db.physical_height, db.refresh))
|
|
cmd += " mode {} {}x{}@{}Hz".format(custom_mode_str, db.physical_width, db.physical_height, db.refresh)
|
|
|
|
lines.append(" pos {} {}".format(db.x, db.y))
|
|
cmd += " pos {} {}".format(db.x, db.y)
|
|
|
|
lines.append(" transform {}".format(db.transform))
|
|
cmd += " transform {}".format(db.transform)
|
|
|
|
lines.append(" scale {}".format(db.scale))
|
|
cmd += " scale {}".format(db.scale)
|
|
|
|
lines.append(" scale_filter {}".format(db.scale_filter))
|
|
cmd += " scale_filter {}".format(db.scale_filter)
|
|
|
|
a_s = "on" if db.adaptive_sync else "off"
|
|
lines.append(" adaptive_sync {}".format(a_s))
|
|
cmd += " adaptive_sync {}".format(a_s)
|
|
|
|
dpms = "on" if db.dpms else "off"
|
|
lines.append(" dpms {}".format(dpms))
|
|
cmd += " dpms {}".format(dpms)
|
|
|
|
lines.append("}")
|
|
cmds.append(cmd)
|
|
|
|
if not use_desc:
|
|
for key in outputs_activity:
|
|
if key not in db_names:
|
|
lines.append('output "{}" disable'.format(key))
|
|
cmds.append('output "{}" disable'.format(key))
|
|
else:
|
|
for key in outputs_activity:
|
|
desc = inactive_output_description(key)
|
|
if desc not in db_names:
|
|
lines.append('output "{}" disable'.format(desc))
|
|
cmds.append('output "{}" disable'.format(desc))
|
|
|
|
print("[Saving]")
|
|
for line in lines:
|
|
print(line)
|
|
|
|
# Check if the outputs file exists
|
|
if os.path.isfile(outputs_path):
|
|
# Load a backup to restore settings if needed
|
|
backup = load_text_file(outputs_path).splitlines()
|
|
else:
|
|
backup = []
|
|
|
|
save_list_to_text_file(lines, outputs_path)
|
|
|
|
print("[Executing]")
|
|
for cmd in cmds:
|
|
print(cmd)
|
|
|
|
i3 = Connection()
|
|
for cmd in cmds:
|
|
i3.command(cmd)
|
|
|
|
create_confirm_win(backup, outputs_path)
|
|
|
|
elif os.getenv("HYPRLAND_INSTANCE_SIGNATURE"):
|
|
transforms = {"normal": 0, "90": 1, "180": 2, "270": 3, "flipped": 4, "flipped-90": 5, "flipped-180": 6,
|
|
"flipped-270": 7}
|
|
for db in display_buttons:
|
|
name = db.name if not use_desc else "desc:{}".format(db.description.replace("#", "##"))
|
|
db_names.append(name)
|
|
|
|
line = "monitor={},{}x{}@{},{}x{},{}".format(name, db.physical_width, db.physical_height, db.refresh, db.x, db.y, db.scale)
|
|
if db.mirror:
|
|
line += ",mirror,{}".format(db.mirror)
|
|
if db.ten_bit:
|
|
line += ",bitdepth,10"
|
|
|
|
lines.append(line)
|
|
if db.transform != "normal":
|
|
lines.append("monitor={},transform,{}".format(name, transforms[db.transform]))
|
|
|
|
# avoid looking up the hardware name
|
|
if db.name in outputs_activity and not outputs_activity[db.name]:
|
|
lines.append("monitor={},disable".format(name))
|
|
|
|
cmd = "on" if db.dpms else "off"
|
|
hyprctl(f"dispatch dpms {cmd} {db.name}")
|
|
|
|
print("[Saving]")
|
|
for line in lines:
|
|
print(line)
|
|
|
|
backup = []
|
|
if os.path.isfile(outputs_path):
|
|
backup = load_text_file(outputs_path).splitlines()
|
|
save_list_to_text_file(lines, outputs_path)
|
|
create_confirm_win(backup, outputs_path)
|
|
|
|
def create_confirm_win(backup, path):
|
|
global confirm_win
|
|
if confirm_win:
|
|
confirm_win.destroy()
|
|
confirm_win = Gtk.Window()
|
|
confirm_win.set_property("name", "popup")
|
|
|
|
GtkLayerShell.init_for_window(confirm_win)
|
|
GtkLayerShell.set_layer(confirm_win, GtkLayerShell.Layer.OVERLAY)
|
|
# GtkLayerShell.set_keyboard_mode(confirm_win, GtkLayerShell.KeyboardMode.ON_DEMAND)
|
|
|
|
confirm_win.set_resizable(False)
|
|
confirm_win.set_modal(True)
|
|
grid = Gtk.Grid()
|
|
grid.set_column_spacing(12)
|
|
grid.set_row_spacing(12)
|
|
grid.set_column_homogeneous(True)
|
|
grid.set_property("margin", 12)
|
|
confirm_win.add(grid)
|
|
lbl = Gtk.Label.new("{}?".format(voc["keep-current-settings"]))
|
|
grid.attach(lbl, 0, 0, 2, 1)
|
|
|
|
global counter
|
|
counter = config["confirm-timeout"]
|
|
|
|
cnt_lbl = Gtk.Label.new(str(counter))
|
|
grid.attach(cnt_lbl, 0, 1, 2, 1)
|
|
btn_restore = Gtk.Button.new_with_label(voc["restore"])
|
|
|
|
btn_restore.connect("clicked", restore_old_settings, backup, path)
|
|
|
|
grid.attach(btn_restore, 0, 2, 1, 1)
|
|
btn_keep = Gtk.Button.new_with_label(voc["keep"])
|
|
btn_keep.connect("clicked", keep_current_settings)
|
|
grid.attach(btn_keep, 1, 2, 1, 1)
|
|
|
|
confirm_win.show_all()
|
|
|
|
global src_tag
|
|
src_tag = GLib.timeout_add_seconds(1, count_down, cnt_lbl, backup, path)
|
|
|
|
|
|
def count_down(label, backup, path):
|
|
global counter
|
|
if counter > 0:
|
|
counter -= 1
|
|
label.set_text(str(counter))
|
|
return True
|
|
|
|
restore_old_settings(None, backup, path)
|
|
|
|
|
|
def keep_current_settings(btn):
|
|
if src_tag > 0:
|
|
GLib.Source.remove(src_tag)
|
|
confirm_win.close()
|
|
|
|
|
|
def restore_old_settings(btn, backup, path):
|
|
print("Restoring old settings...")
|
|
if src_tag > 0:
|
|
GLib.Source.remove(src_tag)
|
|
|
|
if os.getenv("SWAYSOCK"):
|
|
save_list_to_text_file(backup, path)
|
|
|
|
# Parse backup file back to commands and execute them
|
|
single_line = ""
|
|
# omit comments & empty lines
|
|
for line in backup:
|
|
if not line.startswith("#") and line:
|
|
single_line += line
|
|
# remove "{"
|
|
single_line = single_line.replace("{", "")
|
|
# convert multiple spaces into single
|
|
single_line = ' '.join(single_line.split())
|
|
cmds = single_line.split("}")
|
|
# execute line by line
|
|
i3 = Connection()
|
|
for cmd in cmds:
|
|
if cmd:
|
|
i3.command(cmd)
|
|
|
|
confirm_win.close()
|
|
create_display_buttons()
|
|
|
|
elif os.getenv("HYPRLAND_INSTANCE_SIGNATURE"):
|
|
save_list_to_text_file(backup, path)
|
|
confirm_win.close()
|
|
# Don't execute any command here, just save the file and wait for Hyprland to notice and apply the change.
|
|
# Let's give it some time to do it before refreshing UI.
|
|
GLib.timeout_add(2000, create_display_buttons)
|
|
|
|
|
|
def main():
|
|
GLib.set_prgname('nwg-displays')
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
if sway:
|
|
parser.add_argument("-o",
|
|
"--outputs_path",
|
|
type=str,
|
|
default="{}/outputs".format(sway_config_dir),
|
|
help="path to save Outputs config to, default: {}".format(
|
|
"{}/outputs".format(sway_config_dir)))
|
|
|
|
parser.add_argument("-n",
|
|
"--num_ws",
|
|
type=int,
|
|
default=8,
|
|
help="number of Workspaces in use, default: 8")
|
|
|
|
elif hypr:
|
|
parser.add_argument("-m",
|
|
"--monitors_path",
|
|
type=str,
|
|
default="{}/monitors.conf".format(hypr_config_dir),
|
|
help="path to save the monitors.conf file to, default: {}".format(
|
|
"{}/monitors.conf".format(hypr_config_dir)))
|
|
|
|
parser.add_argument("-n",
|
|
"--num_ws",
|
|
type=int,
|
|
default=10,
|
|
help="number of Workspaces in use, default: 10")
|
|
|
|
parser.add_argument("-v",
|
|
"--version",
|
|
action="version",
|
|
version="%(prog)s version {}".format(__version__),
|
|
help="display version information")
|
|
args = parser.parse_args()
|
|
|
|
load_vocabulary()
|
|
|
|
global outputs_path
|
|
if sway:
|
|
if os.path.isdir(sway_config_dir):
|
|
outputs_path = args.outputs_path
|
|
else:
|
|
eprint("sway config directory not found!")
|
|
outputs_path = ""
|
|
elif hypr:
|
|
if os.path.isdir(hypr_config_dir):
|
|
outputs_path = args.monitors_path
|
|
else:
|
|
eprint("Hyprland config directory not found!")
|
|
outputs_path = ""
|
|
|
|
global num_ws
|
|
num_ws = args.num_ws
|
|
if sway:
|
|
print("Number of workspaces: {}".format(num_ws))
|
|
|
|
config_file = os.path.join(config_dir, "config")
|
|
global config
|
|
if not os.path.isfile(config_file):
|
|
# migrate old config file, if not yet migrated
|
|
if os.path.isfile(os.path.join(old_config_dir, "config")):
|
|
print("Migrating config to the proper path...")
|
|
os.rename(old_config_dir, config_dir)
|
|
else:
|
|
if not os.path.isdir(config_dir):
|
|
os.makedirs(config_dir, exist_ok=True)
|
|
print("'{}' file not found, creating default".format(config_file))
|
|
save_json(config, config_file)
|
|
else:
|
|
config = load_json(config_file)
|
|
|
|
if config_keys_missing(config, config_file):
|
|
config = load_json(config_file)
|
|
|
|
eprint("Settings: {}".format(config))
|
|
|
|
global snap_threshold_scaled
|
|
snap_threshold_scaled = config["snap-threshold"]
|
|
|
|
builder = Gtk.Builder()
|
|
builder.add_from_file(os.path.join(dir_name, "resources/main.glade"))
|
|
|
|
window = builder.get_object("window")
|
|
screen = Gdk.Screen.get_default()
|
|
provider = Gtk.CssProvider()
|
|
style_context = Gtk.StyleContext()
|
|
style_context.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
try:
|
|
file = os.path.join(dir_name, "resources/style.css")
|
|
provider.load_from_path(file)
|
|
except:
|
|
sys.stderr.write("ERROR: {} file not found, using GTK styling\n".format(os.path.join(dir_name,
|
|
"resources/style.css")))
|
|
|
|
window.connect("key-release-event", handle_keyboard)
|
|
window.connect('destroy', Gtk.main_quit)
|
|
|
|
builder.get_object("lbl-modes").set_label("{}:".format(voc["modes"]))
|
|
builder.get_object("lbl-position-x").set_label("{}:".format(voc["position-x"]))
|
|
builder.get_object("lbl-refresh").set_label("{}:".format(voc["refresh"]))
|
|
builder.get_object("lbl-scale").set_label("{}:".format(voc["scale"]))
|
|
builder.get_object("lbl-scale-filter").set_label("{}:".format(voc["scale-filter"]))
|
|
builder.get_object("lbl-size").set_label("{}:".format(voc["size"]))
|
|
builder.get_object("lbl-transform").set_label("{}:".format(voc["transform"]))
|
|
builder.get_object("lbl-zoom").set_label("{}:".format(voc["zoom"]))
|
|
|
|
global form_name
|
|
form_name = builder.get_object("name")
|
|
|
|
global form_description
|
|
form_description = builder.get_object("description")
|
|
|
|
global form_dpms
|
|
form_dpms = builder.get_object("dpms")
|
|
form_dpms.set_tooltip_text(voc["dpms-tooltip"])
|
|
form_dpms.connect("toggled", on_dpms_toggled)
|
|
# if sway:
|
|
# form_dpms.set_tooltip_text(voc["dpms-tooltip"])
|
|
# form_dpms.connect("toggled", on_dpms_toggled)
|
|
# else:
|
|
# form_dpms.set_sensitive(False)
|
|
|
|
global form_adaptive_sync
|
|
form_adaptive_sync = builder.get_object("adaptive-sync")
|
|
if sway:
|
|
form_adaptive_sync.set_label(voc["adaptive-sync"])
|
|
form_adaptive_sync.set_tooltip_text(voc["adaptive-sync-tooltip"])
|
|
form_adaptive_sync.connect("toggled", on_adaptive_sync_toggled)
|
|
else:
|
|
form_adaptive_sync.set_sensitive(False)
|
|
|
|
global form_custom_mode
|
|
form_custom_mode = builder.get_object("custom-mode")
|
|
if sway:
|
|
form_custom_mode.set_label(voc["custom-mode"])
|
|
form_custom_mode.set_tooltip_text(voc["custom-mode-tooltip"])
|
|
form_custom_mode.connect("toggled", on_custom_mode_toggle)
|
|
else:
|
|
form_custom_mode.set_sensitive(False)
|
|
|
|
global form_view_scale
|
|
form_view_scale = builder.get_object("view-scale")
|
|
form_view_scale.set_tooltip_text(voc["view-scale-tooltip"])
|
|
adj = Gtk.Adjustment(lower=0.1, upper=0.6, step_increment=0.05, page_increment=0.1, page_size=0.1)
|
|
form_view_scale.configure(adj, 1, 2)
|
|
form_view_scale.connect("changed", on_view_scale_changed)
|
|
|
|
global form_x
|
|
form_x = builder.get_object("x")
|
|
adj = Gtk.Adjustment(lower=0, upper=60000, step_increment=1, page_increment=10, page_size=1)
|
|
form_x.configure(adj, 1, 0)
|
|
form_x.connect("value-changed", on_pos_x_changed)
|
|
|
|
global form_y
|
|
form_y = builder.get_object("y")
|
|
adj = Gtk.Adjustment(lower=0, upper=40000, step_increment=1, page_increment=10, page_size=1)
|
|
form_y.configure(adj, 1, 0)
|
|
form_y.connect("value-changed", on_pos_y_changed)
|
|
|
|
global form_width
|
|
form_width = builder.get_object("width")
|
|
adj = Gtk.Adjustment(lower=0, upper=7680, step_increment=1, page_increment=10, page_size=1)
|
|
form_width.configure(adj, 1, 0)
|
|
form_width.connect("value-changed", on_width_changed)
|
|
|
|
global form_height
|
|
form_height = builder.get_object("height")
|
|
adj = Gtk.Adjustment(lower=0, upper=4320, step_increment=1, page_increment=10, page_size=1)
|
|
form_height.configure(adj, 1, 0)
|
|
form_height.connect("value-changed", on_height_changed)
|
|
|
|
global form_scale
|
|
form_scale = builder.get_object("scale")
|
|
adj = Gtk.Adjustment(lower=0.1, upper=10, step_increment=0.1, page_increment=10, page_size=1)
|
|
form_scale.configure(adj, 0.1, 6)
|
|
form_scale.connect("value-changed", on_scale_changed)
|
|
|
|
global form_scale_filter
|
|
form_scale_filter = builder.get_object("scale-filter")
|
|
if sway:
|
|
form_scale_filter.set_tooltip_text(voc["scale-filter-tooltip"])
|
|
form_scale_filter.connect("changed", on_scale_filter_changed)
|
|
else:
|
|
form_scale_filter.set_sensitive(False)
|
|
|
|
global form_refresh
|
|
form_refresh = builder.get_object("refresh")
|
|
adj = Gtk.Adjustment(lower=1, upper=1200, step_increment=1, page_increment=10, page_size=1)
|
|
form_refresh.configure(adj, 1, 3)
|
|
form_refresh.connect("changed", on_refresh_changed)
|
|
|
|
global form_modes
|
|
form_modes = builder.get_object("modes")
|
|
form_modes.set_tooltip_text(voc["modes-tooltip"])
|
|
form_modes.connect("changed", on_mode_changed)
|
|
|
|
global form_use_desc
|
|
form_use_desc = builder.get_object("use-desc")
|
|
form_use_desc.set_label("{}".format(voc["use-desc"]))
|
|
form_use_desc.set_tooltip_text("{}".format(voc["use-desc-tooltip"]))
|
|
form_use_desc.connect("toggled", on_use_desc_toggled)
|
|
|
|
global form_transform
|
|
form_transform = builder.get_object("transform")
|
|
form_transform.set_tooltip_text(voc["transform-tooltip"])
|
|
form_transform.connect("changed", on_transform_changed)
|
|
|
|
global form_wrapper_box
|
|
form_wrapper_box = builder.get_object("wrapper-box")
|
|
|
|
global form_workspaces
|
|
form_workspaces = builder.get_object("workspaces")
|
|
form_workspaces.set_label(voc["workspaces"])
|
|
form_workspaces.set_tooltip_text(voc["workspaces-tooltip"])
|
|
if sway:
|
|
form_workspaces.connect("clicked", create_workspaces_window)
|
|
elif hypr:
|
|
form_workspaces.connect("clicked", create_workspaces_window_hypr)
|
|
|
|
global form_close
|
|
form_close = builder.get_object("close")
|
|
form_close.set_label(voc["close"])
|
|
form_close.connect("clicked", Gtk.main_quit)
|
|
form_close.grab_focus()
|
|
|
|
global form_apply
|
|
form_apply = builder.get_object("apply")
|
|
form_apply.set_label(voc["apply"])
|
|
if (sway and sway_config_dir) or (hypr and hypr_config_dir):
|
|
form_apply.connect("clicked", on_apply_button)
|
|
else:
|
|
form_apply.set_sensitive(False)
|
|
form_apply.set_tooltip_text("Config dir not found")
|
|
|
|
global form_version
|
|
form_version = builder.get_object("version")
|
|
form_version.set_text("v{}".format(__version__))
|
|
|
|
wrapper = builder.get_object("wrapper")
|
|
wrapper.set_property("name", "wrapper")
|
|
|
|
global fixed
|
|
fixed = builder.get_object("fixed")
|
|
|
|
create_display_buttons()
|
|
|
|
global outputs_activity
|
|
outputs_activity = list_outputs_activity()
|
|
lbl = Gtk.Label()
|
|
lbl.set_text("{}:".format(voc["active"]))
|
|
form_wrapper_box.pack_start(lbl, False, False, 3)
|
|
for key in outputs_activity:
|
|
cb = Gtk.CheckButton()
|
|
cb.set_label(key)
|
|
cb.set_active(outputs_activity[key])
|
|
cb.connect("toggled", on_output_toggled, key)
|
|
form_wrapper_box.pack_start(cb, False, False, 3)
|
|
|
|
btn = Gtk.Button.new_with_label(voc["toggle"])
|
|
if sway:
|
|
btn.set_tooltip_text(voc["toggle-tooltip"])
|
|
btn.connect("clicked", on_toggle_button)
|
|
form_wrapper_box.pack_start(btn, False, False, 3)
|
|
else:
|
|
btn.destroy()
|
|
|
|
if hypr:
|
|
grid = builder.get_object("grid")
|
|
|
|
global form_ten_bit
|
|
form_ten_bit = Gtk.CheckButton.new_with_label(voc["10-bit-support"])
|
|
form_ten_bit.set_tooltip_text(voc["10-bit-support-tooltip"])
|
|
form_ten_bit.connect("toggled", on_ten_bit_toggled)
|
|
grid.attach(form_ten_bit, 5, 4, 1, 1)
|
|
|
|
lbl = Gtk.Label.new("Mirror:")
|
|
lbl.set_property("halign", Gtk.Align.END)
|
|
grid.attach(lbl, 6, 4, 1, 1)
|
|
|
|
global form_mirror
|
|
form_mirror = Gtk.ComboBoxText()
|
|
form_mirror.connect("changed", on_mirror_selected)
|
|
grid.attach(form_mirror, 7, 4, 1, 1)
|
|
|
|
if display_buttons:
|
|
update_form_from_widget(display_buttons[0])
|
|
display_buttons[0].select()
|
|
|
|
screen = Gdk.Screen.get_default()
|
|
provider = Gtk.CssProvider()
|
|
style_context = Gtk.StyleContext()
|
|
style_context.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
css = b""" #popup { border-radius: 6px; border: solid 1px; border-color: #f00 } """
|
|
provider.load_from_data(css)
|
|
|
|
window.show_all()
|
|
|
|
# Gtk.Fixed does not respect expand properties. That's why we need
|
|
# to scale the window automagically if opened as a floating_con
|
|
Gdk.threads_add_timeout(GLib.PRIORITY_LOW, 100, scale_if_floating)
|
|
|
|
Gtk.main()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|