Select Git revision
Jiří Kalvoda authored
qt.py 55.33 KiB
#!/usr/bin/env python3
import sys
import os
import threading
from collections import defaultdict
import binascii
import Xlib
import Xlib.display
import Xlib.X
import argparse
import i3ipc
import time
import traceback
import signal
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import re
import random
import string
import types
import subprocess
from contextlib import contextmanager
from woman.shared import *
from woman.lib import *
import woman.help as help
from woman.constants import *
from woman.qt_util import *
class NoSutchKey(Exception):
def __init__(self, key, mod):
self.key_str = key_to_str(key, mod)
super().__init__(f"No sutch key {self.key_str}")
def qt_main():
shared.init_thread()
app = QApplication(sys.argv)
primary_screen = app.primaryScreen()
def qt_workspace_widget_func(master, slave, f):
if slave is None or (slave, master) == GUI_WORKSPACE:
return
if slave > MAX_MASTERED_SLAVE:
master = None
return f(m_win._workspaces[(master, slave)])
SCREENSHOTS_SIZES = [120, 150, 200, 250, 300, 400, 500, 750, 1000]
class Tasker(QObject):
do = pyqtSignal(object)
func = {}
def f(self, *arg):
self.do.emit(arg)
@pyqtSlot(object)
def _run(self, w):
try:
self.func[w[0]](*w[1:])
except Exception:
traceback.print_exc()
def __init__(self, parent = None):
super().__init__(parent)
self.do.connect(self._run)
def func_add(f):
self.func[f.__name__] = f
return f
@func_add
def ws_removed(master, slave):
qt_workspace_widget_func(master, slave, lambda x: x.removed())
@func_add
def ws_changed(master, slave):
qt_workspace_widget_func(master, slave, lambda x: x.screenshot_changed())
@func_add
def ws_exist(master, slave):
qt_workspace_widget_func(master, slave, lambda x: x.make_exist())
@func_add
def ws_metadata_changed(master, slave):
qt_workspace_widget_func(master, slave, lambda x: x.metadata_changed())
@func_add
def ws_swap(a_master, a_slave, b_master, b_slave):
qt_workspace_widget_func(a_master, a_slave,
lambda x: qt_workspace_widget_func(b_master, b_slave, lambda y: x.swapped(y)))
qt_workspace_widget_func(a_master, a_slave, lambda x: x.metadata_changed())
qt_workspace_widget_func(b_master, b_slave, lambda x: x.metadata_changed())
@func_add
def ws_rename(a_master, a_slave, b_master, b_slave):
qt_workspace_widget_func(a_master, a_slave,
lambda x: qt_workspace_widget_func(b_master, b_slave, lambda y: x.moved(y)))
@func_add
def screenshot_and_goto(n_master, n_slave, master, slave):
qt_workspace_widget_func(master, slave, lambda x: x.make_screenshot())
shared.i3_cmd(f'workspace {workspace(n_master, n_slave)}')
if (n_master, n_slave) == GUI_WORKSPACE:
m_win.move_to_gui_workspace()
@func_add
def screenshot(master, slave):
qt_workspace_widget_func(master, slave, lambda x: x.make_screenshot())
@func_add
def gui_focused():
m_win.end_get_continuing_key()
m_win.load_i3_tree()
with shared.lock:
w = get_workspace()
m_win.focus_workspace(*w)
shared.qt_thread_tasker = Tasker()
class TmpWin(QWidget):
def __init__(self):
self.title = "i3-woman-daemon-tmp-window"
super().__init__()
self.setWindowTitle(self.title)
self.show()
self.id = int(self.winId())
shared.i3_cmd(f'[id={self.id}] move container to workspace tmp')
class I3NodeWidget(QFrame, DragFeature):
def __init__(self, t, workspace_widget, is_root, parent=None):
super().__init__(parent)
self.is_root = is_root
self.workspace_widget = workspace_widget
self.container_id = t.id
self.shortcut = None
self.setObjectName(f"{id(self)}");
self.setAcceptDrops(True)
self.dropPlace = None # 0: top 1: mid 2: bottom
self.ignore_drop = False
self.marked = None
@contextmanager
def marked_context(self, color):
try:
self.marked = color
self.redraw()
yield None
finally:
self.marked = None
self.redraw()
def focus(self):
shared.i3_cmd(f'[con_id={self.container_id}] focus')
def wrap_if_is_root(self):
if self.is_root:
self.is_root = False
master, slave = (self.workspace_widget.master, self.workspace_widget.slave)
shared.i3_cmd(f'[con_id={self.container_id}] move container to workspace tmp')
# ID is changed, need reload
t = shared.i3.value.get_tree()
def go(x):
if x.type == "workspace":
print(x.name)
if x.name == "tmp":
self.container_id = x.nodes[0].id
else:
for y in x.nodes:
go(y)
go(t)
shared.i3_cmd(f'[con_id={self.container_id}] move container to workspace {workspace(master, slave)}')
for f in self.workspace_widget._tree.floating_nodes:
shared.i3_cmd(f'[con_id={f.container_id}] move container to workspace {workspace(master, slave)}')
return self
def move_to_workspace(self, master, slave, expand=False):
self.workspace_widget.screenshot_changed()
m_win._workspaces[(master, slave)].screenshot_changed()
if expand and type(self) == I3InnerNodeWidget:
marks = []
for (i, it) in enumerate(self.nodes):
shared.i3_cmd(f'[con_id={it.container_id}] mark tmp_from_{i}')
marks.append(f"tmp_from_{i}")
shared.i3_cmd(f'[con_mark="{"|".join(marks)}"] move container to to workspace {workspace(master, slave)}')
else:
shared.i3_cmd(f'[con_id={self.container_id}] move container to workspace {workspace(master, slave)}')
def move_to(self, target):
self.workspace_widget.screenshot_changed()
target.workspace_widget.screenshot_changed()
shared.i3_cmd(f'[con_id={target.container_id}] mark tmp')
shared.i3_cmd(f'[con_id={self.container_id}] move container to mark tmp')
def move(self, target, swap=False, before=False, expand=False, new_container=None):
self.workspace_widget.screenshot_changed()
target.workspace_widget.screenshot_changed()
if swap:
self.wrap_if_is_root()
target.wrap_if_is_root()
shared.i3_cmd(f'[con_id={self.container_id}] mark tmp')
shared.i3_cmd(f'[con_id={target.container_id}] swap container with mark tmp')
else:
if new_container is not None:
target.put_in_new_container(None if new_container is True else new_container)
else:
target.wrap_if_is_root()
if expand and type(self) != I3InnerNodeWidget:
if before:
self.move(target, expand=True)
target.move(self.nodes[0])
else:
marks = []
for (i, it) in enumerate(self.nodes):
shared.i3_cmd(f'[con_id={it.container_id}] mark tmp_from_{i}')
marks.append(f"tmp_from_{i}")
if type(target) == I3WindowNodeWidget:
tmp_win = TmpWin()
shared.i3_cmd(f'[con_id={target.container_id}] swap container with id {tmp_win.id}')
shared.i3_cmd(f'[id={tmp_win.id}] mark tmp')
else:
shared.i3_cmd(f'[con_id={target.container_id}] mark tmp')
shared.i3_cmd(f'[con_mark="{"|".join(marks)}"] move container to mark tmp')
if type(target) == I3WindowNodeWidget:
shared.i3_cmd(f'[con_id={target.container_id}] swap container with id {tmp_win.id}')
else:
if before:
self.move(target)
target.move(self)
else:
if type(target) == I3WindowNodeWidget:
self.move_to(target)
else:
tmp_win = TmpWin()
shared.i3_cmd(f'[con_id={target.container_id}] swap container with id {tmp_win.id}')
shared.i3_cmd(f'[id={tmp_win.id}] mark tmp')
shared.i3_cmd(f'[con_id={self.container_id}] move container to mark tmp')
try:
shared.i3_cmd(f'[con_id={target.container_id}] swap container with id {tmp_win.id}')
except I3CmdException:
pass # Could fail whem moving the only window from container to upper container
def do_float(self, state=None):
self.workspace_widget.screenshot_changed()
shared.i3_cmd(f'[con_id={self.container_id}] floating { {None:"toggle", True:"enable", False:"disable"}[state] }')
def put_in_new_container(self, orientation=None):
if orientation is None:
shared.i3_cmd(f'[con_id={self.container_id}] split toggle')
elif orientation == "splith":
shared.i3_cmd(f'[con_id={self.container_id}] split horizontal')
elif orientation == "splitv":
shared.i3_cmd(f'[con_id={self.container_id}] split vertical')
else:
shared.i3_cmd(f'[con_id={self.container_id}] split vertical')
shared.i3_cmd(f'[con_id={self.container_id}] layout {orientation}')
def clicked(self, pos, buttons):
try:
m_win.end_get_continuing_key()
if buttons == Qt.LeftButton:
self.focus()
if buttons == Qt.MiddleButton:
self.do_float()
m_win.load_i3_tree()
except I3CmdException as e:
m_win.cmd_msg(e.error, QColor(255, 100, 100))
traceback.print_exc()
m_win.load_i3_tree()
def drag(self, pos, buttons):
d = QDrag(self)
mimeData = QMimeData()
d.setMimeData(mimeData)
self.marked = QColor(255,0,0)
self.redraw()
dropAction = d.exec()
self.marked = None
self.redraw()
@qtoverride
def dragEnterEvent(self, event):
s = event.source()
if s is not None and isinstance(s, WorkspaceWidget) or isinstance(s, I3NodeWidget):
self.ignore_drop = False
x = self
while x is not None:
if x == s:
self.ignore_drop = True
x = x.parentWidget()
event.acceptProposedAction()
@qtoverride
def dropEvent(self, event):
m_win.end_get_continuing_key()
if self.ignore_drop:
return
s = event.source()
if isinstance(s, WorkspaceWidget) or isinstance(s, I3NodeWidget):
m_win.cmd_msg_clean()
button = s.press_event_buttons
try:
if button == Qt.MiddleButton:
s.move(self, swap=True)
else:
s.move(self,
expand=s.press_event_buttons == Qt.RightButton,
**[{"before":True}, {"new_container": True}, {}][self.drop_place]
)
# [s.expand_before, lambda self: s.move_to_new_container_with(self, expand=True), s.expand_after][self.drop_place](self)
#[s.move_before, s.move_to_new_container_with, s.move_after][self.drop_place](self)
m_win.load_i3_tree()
self.setStyleSheet(f"")
self.redraw()
except I3CmdException as e:
m_win.cmd_msg(e.error, QColor(255, 100, 100))
traceback.print_exc()
m_win.load_i3_tree()
@qtoverride
def dragLeaveEvent(self, event):
self.setStyleSheet(f"")
self.redraw()
@qtoverride
def dragMoveEvent(self, event):
event.acceptProposedAction()
if self.ignore_drop:
return
y = event.answerRect().center().y()
h = self.height()
if y < h//4:
self.drop_place = 0
elif y >= h - h//4:
self.drop_place = 2
else:
self.drop_place = 1
s = event.source()
if s is not None and isinstance(s, WorkspaceWidget) or isinstance(s, I3NodeWidget):
button = s.press_event_buttons
if button == Qt.MiddleButton:
style = "background-color: green;"
else:
style = ["border-top: 3px solid green;", "background-color: green;", "border:no; border-bottom: 3px solid green;"][self.drop_place]
self.setStyleSheet(f"#{id(self)} {{ {style} }}");
class I3WindowNodeWidget(I3NodeWidget):
def __init__(self, t, workspace_widget, is_root, parent=None):
super().__init__(t, workspace_widget, is_root, parent)
self._hlay = no_space(QHBoxLayout(self))
self.title = t.name
self.urgent = t.urgent
self.title_with_find = None
self._title = QLabel(self)
self.setAutoFillBackground(True)
self.setLayout(self._hlay)
self._hlay.addWidget(self._title)
self.win_id = t.window
self.redraw()
self._title.setTextInteractionFlags(Qt.NoTextInteraction)
def quit_windows(self, force=False):
if force:
r = subprocess.run(['xkill', '-id', str(self.win_id)])
if r.returncode != 0:
m_win.cmd_msg(f"xkill return {r.returncode}", QColor(255, 100, 100))
else:
shared.i3_cmd(f'[con_id={self.container_id}] kill')
def redraw(self):
p = QPalette()
if self.title_with_find is not None:
t = self.title_with_find
p.setColor(self.backgroundRole(), QColor(255, 255, 0))
else:
t = self.title
if self.urgent:
p.setColor(self.foregroundRole(), QColor(255, 0, 0))
else:
p.setColor(self.foregroundRole(), QColor(0, 0, 0))
if self.marked is not None:
p.setColor(self.backgroundRole(), self.marked)
if self.shortcut:
self._title.setText(f"[<b>{self.shortcut[0]}</b>{self.shortcut[1:]}] {t}")
else:
self._title.setText(self.title)
self.setPalette(p)
class I3InnerNodeWidget(I3NodeWidget):
def __init__(self, t, workspace_widget, is_root, parent=None):
super().__init__(t, workspace_widget, is_root, parent)
self._hlay = no_space(QHBoxLayout(self))
self._head = QLabelOpeningBracket(self)
self._list = no_space(QVBoxLayout())
self._head.setTextInteractionFlags(Qt.NoTextInteraction)
self.setAutoFillBackground(True)
self.setLayout(self._hlay)
self._hlay.addWidget(self._head)
self._hlay.addItem(self._list)
self._head.setFixedWidth(13)
self.layout = t.layout
self.nodes = [i3_tree_widget_create(i, workspace_widget, self) for i in t.nodes]
for i in self.nodes:
self._list.addWidget(i)
self.redraw()
def quit_windows(self, *arg, **kvarg):
for i in self.nodes:
i.quit_windows(*arg, **kvarg)
def expand(self):
if isinstance(self.parentWidget(), I3InnerNodeWidget):
self.workspace_widget.screenshot_changed()
tmp_win = TmpWin()
shared.i3_cmd(f'[con_id={self.container_id}] swap container with id {tmp_win.id}')
marks = []
for (i, it) in enumerate(self.nodes):
shared.i3_cmd(f'[con_id={it.container_id}] mark tmp_from_{i}')
marks.append(f"tmp_from_{i}")
shared.i3_cmd(f'[id={tmp_win.id}] mark tmp')
shared.i3_cmd(f'[con_mark="{"|".join(marks)}"] move container to mark tmp')
def redraw(self):
p = QPalette()
p.setColor(self._head.foregroundRole(), QColor(0, 0, 0))
if self.marked is not None:
p.setColor(self.backgroundRole(), self.marked)
if self.shortcut:
self._head.setText(f"<b>{self.shortcut[0]}</b>{self.shortcut[1:]}")
else:
self._head.setText({
"splith": "H",
"splitv": "V",
"tabbed": "T",
"stacked": "S"
}.get(self.layout, "?"))
self.setPalette(p)
def change_layout(self, new_layout=None):
if new_layout is None:
new_layout = {
"splith": "splitv",
"splitv": "tabbed",
"tabbed": "stacked",
"stacked": "splith",
}.get(self.layout, "splith")
shared.i3_cmd(f'[con_id={self.nodes[0].container_id}] layout {new_layout}')
self.layout = new_layout
self.redraw()
def clicked(self, pos, buttons):
if buttons == Qt.RightButton:
self.change_layout()
else:
super().clicked(pos, buttons)
def i3_tree_widget_create(t, workspace_widget, parent, is_root=False):
if t.nodes:
return I3InnerNodeWidget(t, workspace_widget, is_root, parent)
else:
return I3WindowNodeWidget(t, workspace_widget, is_root, parent)
class I3TreeWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.workspace_widget = parent
self._lay = no_space(QVBoxLayout(self))
self.setLayout(self._lay)
self.root_node = None
self.floating_nodes = []
self.all_inner_nides = []
self.all_window_nodes = []
self.find_matchs = []
self.container_id = None
def clear(self):
while (i := self._lay.takeAt(0)):
i.widget().hide()
self.all_inner_nides = []
self.all_window_nodes = []
self.find_matchs = []
self.container_id = None
self.all_inner_nides = []
self.root_node = None
self.floating_nodes = []
def set_tree(self, t):
self.clear()
self.container_id = t.id
def go(x):
if isinstance(x, I3WindowNodeWidget):
self.all_window_nodes.append(x)
if isinstance(x, I3InnerNodeWidget):
self.all_inner_nides.append(x)
for i in x.nodes:
go(i)
r = i3_tree_widget_create(t, self.workspace_widget, self, is_root=True)
self.root_node = r
go(r)
self._lay.addWidget(r)
for i in t.floating_nodes:
for j in i.nodes:
f = i3_tree_widget_create(j, self.workspace_widget, self)
go(f)
self._lay.addWidget(f)
self.floating_nodes.append(f)
def find(self, r):
for win in self.all_window_nodes:
t = win.title
m = r.search(t)
if m:
span = m.span()
win.title_with_find = t[:span[0]] + "<b>" + t[span[0]:span[1]] + "</b>" + t[span[1]:]
win.redraw()
self.find_matchs.append((win, m))
def clear_find(self):
for win,m in self.find_matchs:
win.title_with_find = None
win.redraw()
self.find_matchs = []
class WorkspaceWidget(QFrame, DragFeature):
screenshot = None
screenshot_is_old = False
exist = False
def __init__(self, master, slave, parent):
super().__init__(parent)
self.workspace_widget = self # for compatibility with I3NodeWidget
self.setFrameShape(QFrame.Box)
self.master = master
self.slave = slave
self._lay = no_space(QVBoxLayout(self))
self._name = QLabel(self)
self._screenshot = QLabel(self)
self._tree = I3TreeWidget(self)
self.setAutoFillBackground(True)
self._name.setAutoFillBackground(True)
self._lay.addWidget(self._name)
self._lay.addWidget(self._screenshot)
self._lay.addWidget(self._tree)
self._lay.addStretch()
self.setLayout(self._lay)
self._name.setAlignment(Qt.AlignCenter)
self.focused = False
self.on_primary_output = False
self.name_color = QColor(255, 255, 255)
self.marked = None
self.metadata_changed()
self.setAcceptDrops(True)
self._name.setStyleSheet("font-weight: bold");
@contextmanager
def marked_context(self, color):
try:
self.marked = color
self.redraw()
yield None
finally:
self.marked = None
self.redraw()
def redraw_pic(self):
w = m_win.screenshot_size
self.setMaximumSize(w,2*w+1000)
if self.exist:
if self.screenshot:
s = self.screenshot.scaled(w, 2*w, Qt.KeepAspectRatio)
else:
s = QPixmap(w, w*9//16)
s.fill(QColor(0,0,0))
if self.screenshot_is_old or not self.screenshot:
painter = QPainter(s)
red = QPen(QColor(255, 0, 0), 5)
painter.setPen(red)
painter.drawLine(0, 0, s.width(), s.height())
painter.drawLine(0, s.height(), s.width(), 0)
painter.end()
self._screenshot.setPixmap(s)
self._screenshot.setScaledContents(False)
else:
self._screenshot.clear()
def make_screenshot(self):
self.exist = True
with shared.lock:
try:
o = shared.output_of_workspace[self.master][self.slave]
rect = shared.outputs[o].rect
except KeyError:
traceback.print_exc()
return
self.screenshot = primary_screen.grabWindow(QApplication.desktop().winId(), rect.x, rect.y, rect.width, rect.height)
self.screenshot_is_old = False
self.redraw_pic()
def screenshot_changed(self):
self.screenshot_is_old = True
self.exist = True
self.redraw_pic()
def make_exist(self):
self.exist = True
self.redraw_pic()
def removed(self):
self.screenshot = None
self.exist = False
self.redraw_pic()
def redraw(self):
p = QPalette()
p_name = QPalette()
p_name.setColor(self._name.foregroundRole(), self.name_color)
if self.focused:
p.setColor(QPalette.WindowText, QColor(255, 0, 0))
p.setColor(self.backgroundRole(), QColor(255, 200, 200))
else:
if self.on_primary_output is not None:
if self.on_primary_output:
p_name.setColor(self._name.backgroundRole(), QColor(255, 255, 255))
else:
p_name.setColor(self._name.backgroundRole(), QColor(180, 180, 180))
if self.marked is not None:
p.setColor(self.backgroundRole(), self.marked)
self.setPalette(p)
self._name.setPalette(p_name)
def swapped(self, other):
tmp = self.screenshot_is_old
self.screenshot_is_old = other.screenshot_is_old
other.screenshot_is_old = tmp
tmp = self.screenshot
self.screenshot = other.screenshot
other.screenshot = tmp
other.redraw_pic()
self.redraw_pic()
def moved(self, other):
other.screenshot_is_old = self.screenshot_is_old
other.screenshot = self.screenshot
other.redraw_pic()
self.redraw_pic()
def metadata_changed(self):
self._name.setText(f"{workspace(self.master, self.slave)}")
self.name_color = QColor(0, 0, 0)
with shared.lock:
try:
self.on_primary_output = shared.outputs[shared.output_of_workspace[self.master][self.slave]].primary
except KeyError:
self.on_primary_output = None
if (self.master, self.slave) in shared.workspace_on.values():
self.name_color = QColor(0,255,0)
elif self.master is not None:
if shared.slave_for[self.master] == self.slave:
self.name_color = QColor(255,0,0)
else:
for x in shared.slave_on_for.values():
if x is not None and x[self.master] == self.slave:
self.name_color = QColor(0,0,255)
self.redraw()
def parse_i3_tree(self, t):
self._tree.set_tree(t)
def focus(self):
shared.i3_cmd(f'workspace {workspace(self.master, self.slave)}')
def move_to_otput(self, output="next"):
shared.i3_cmd(f"move workspace to output {output}")
def move(self, target, swap=False, before=False, expand=False, new_container=None):
if (w := self.root_i3_tree_widget()) is not None:
w.move(target, swap=swap, before=before, expand=expand, new_container=new_container)
for f in self._tree.floating_nodes:
f.move_to_workspace(target.workspace_widget.master, target.workspace_widget.slave)
def move_to_workspace(self, master, slave, expand=False):
if (w := self.root_i3_tree_widget()) is not None:
w.move_to_workspace(master, slave, expand=expand)
for f in self._tree.floating_nodes:
f.move_to_workspace(master, slave)
def quit_windows(self, *arg, **kvarg):
if self._tree.root_node is not None:
self._tree.root_node.quit_windows(*arg, **kvarg)
for f in self._tree.floating_nodes:
f.quit_windows(*arg, **kvarg)
def clicked(self, pos, buttons):
if buttons == Qt.RightButton:
try:
self.move_to_otput()
except I3CmdException as e:
m_win.cmd_msg(e.error, QColor(255, 100, 100))
traceback.print_exc()
else:
self.focus()
def root_i3_tree_widget(self):
r = self._tree.root_node
if isinstance(r, I3WindowNodeWidget): return r
if r is None: return None
if len(r.nodes) == 1:
return r.nodes[0]
return r
@qtoverride
def dragEnterEvent(self, event):
s = event.source()
if s is not None and isinstance(s, WorkspaceWidget) or isinstance(s, I3NodeWidget):
event.acceptProposedAction()
@qtoverride
def dropEvent(self, event):
s = event.source()
if isinstance(s, WorkspaceWidget) or isinstance(s, I3NodeWidget):
m_win.cmd_msg_clean()
button = s.press_event_buttons
try:
s.move_to_workspace(self.master, self.slave, expand=button == Qt.RightButton)
m_win.load_i3_tree()
except I3CmdException as e:
m_win.cmd_msg(e.error, QColor(255, 100, 100))
traceback.print_exc()
m_win.load_i3_tree()
class MainWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("i3-woman")
self.screenshot_size = 300
self._lay = no_space(QVBoxLayout(self))
self._scroll = QNoArrowScrollArea(self)
self._scroll_widget = QWidget(self)
self._scroll_lay = no_space(QVBoxLayout(self._scroll_widget))
self._master_widgets = {i: QWidget(self) for i in GUI_MASTERS}
self._master_lays = {i: FlowLayout(self._master_widgets[i]) for i in GUI_MASTERS}
self._workspaces = {(master,slave): WorkspaceWidget(master, slave, self) for (master, slave) in GUI_WORKSPACES_ORDER}
self._bar_lay = QHBoxLayout()
self._help_label = QPushButton(self)
self._find_input = QLineEdit(self)
self._find_msg = QLabel(self)
self._cmd_msg = QLabel(self)
self._cmd_msg.setFixedWidth(200)
self._cmd_msg.setAutoFillBackground(True)
self._help_label.clicked.connect(self._find_msg_clicked)
self._scroll.setWidgetResizable(True)
#self.scroll_widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
for i in GUI_MASTERS:
self._scroll_lay.addWidget(self._master_widgets[i])
self._master_widgets[i].setLayout(self._master_lays[i])
self._master_lays[i].setSpacing(0)
self._master_lays[i].setContentsMargins(0, 0, 0, 0)
for ((master, slave), widget) in self._workspaces.items():
self._master_lays[master].addWidget(widget)
self._find_input.setPlaceholderText("Find")
self._find_input.textChanged.connect(self.find_changed)
self._find_msg.setAutoFillBackground(True)
self._help_label.setText("Help (?)")
self._help_label.setFocusPolicy(Qt.NoFocus)
self._scroll_widget.setLayout(self._scroll_lay)
self._scroll.setWidget(self._scroll_widget)
self._bar_lay.addWidget(self._help_label)
self._bar_lay.addWidget(self._find_input)
self._bar_lay.addWidget(self._find_msg)
self._bar_lay.addWidget(self._cmd_msg)
self._lay.addWidget(self._scroll)
self._lay.addItem(self._bar_lay)
self.setLayout(self._lay)
self.focused_master = None
self.focused_slave = None
self.focused_widget = None
self.find_regex = None
self.find_error = None
self.find_matchs = []
self.get_continuing_key = None
self.get_continuing_key_readed_keys = None
self.cmd_msg_show_get_continuing_key = False
self._help_window = TextShowWidget(help.qt)
@pyqtSlot(bool)
def _find_msg_clicked(self, x):
self.show_help()
def focus_workspace(self, n_master, n_slave):
def f(x):
if self.focused_widget:
self.focused_widget.focused = False
self.focused_widget.redraw()
if n_slave <= MAX_MASTERED_SLAVE:
self.focused_master = n_master
self.focused_slave = n_slave
self.focused_widget = x
self._scroll.ensureWidgetVisible(x)
x.focused = True
x.redraw()
if self.find_regex:
self.set_find_msg()
qt_workspace_widget_func(n_master, n_slave, f)
def change_focused_forkspace(self, *arg, **kvarg):
with shared.lock:
try:
w = get_workspace(*arg, **kvarg, old_master=self.focused_master, old_slave=self.focused_slave, gui=True)
except WorkspaceOutOfRange:
m_win.cmd_msg("No more workspaces", QColor(255, 0, 0))
return
self.focus_workspace(*w)
def load_i3_tree(self):
def ppr(x, t, fl=False):
for y in x.nodes:
ppr(y, t+1)
for y in x.floating_nodes:
ppr(y, t+1, True)
t = shared.i3.value.get_tree()
# ppr(t, 0)
for w in self._workspaces.values():
w._tree.clear()
def go(x):
if x.type == "workspace":
qt_workspace_widget_func(*parse_workspace(x.name), lambda y: y._tree.set_tree(x))
else:
for y in x.nodes:
go(y)
go(t)
if self.find_regex:
for w in self._workspaces.values():
w._tree.find(self.find_regex)
def set_screenshot_size(self, val):
self.screenshot_size = val
for i in self._workspaces.values():
i.redraw_pic()
self.focus_workspace(self.focused_master, self.focused_slave)
def clear_find(self):
self.find_regex = None
self.find_error = None
for w in self._workspaces.values():
w._tree.clear_find()
def find(self, s):
self.clear_find()
try:
self.find_regex = re.compile(s, re.IGNORECASE)
except re.error as e:
self.find_error = e
self.set_find_msg()
return
find_error = None
for w in self._workspaces.values():
w._tree.find(self.find_regex)
for i in GUI_WORKSPACES_ORDER:
w = self._workspaces[i]
if len(w._tree.find_matchs) > 0:
self.change_focused_forkspace(w.master, w.slave)
break
self.set_find_msg()
def set_find_msg(self):
p = QPalette()
if self.find_error:
self._find_msg.setText(str(self.find_error))
p.setColor(self._find_msg.backgroundRole(), QColor(255,100,0))
elif self.find_regex:
tot_ws = 0
tot_win = 0
act_ws = None
act_win = None
act_win_to = None
for i in GUI_WORKSPACES_ORDER:
w = self._workspaces[i]
l = len(w._tree.find_matchs)
if l:
tot_ws += 1
if i == (self.focused_widget.master, self.focused_widget.slave):
act_ws = tot_ws
act_win = tot_win + 1
act_win_to = tot_win + l
tot_win += l
if tot_ws:
self._find_msg.setText(f"{act_ws}/{tot_ws} window {act_win}-{act_win_to}/{tot_win}")
p.setColor(self._find_msg.backgroundRole(), QColor(0,255,0))
else:
self._find_msg.setText("Not found")
p.setColor(self._find_msg.backgroundRole(), QColor(255,0,0))
else:
self._find_msg.setText("")
p.setColor(self._find_msg.backgroundRole(), QColor(0,0,0,0))
self._find_msg.setPalette(p)
def find_next(self, direction):
index = GUI_WORKSPACES_ORDER.index((self.focused_widget.master, self.focused_widget.slave))
def cmd_msg(self, msg, bg_color=None, is_get_continuing_key=False):
self.cmd_msg_show_get_continuing_key = is_get_continuing_key
self._cmd_msg.setText(msg)
p = QPalette()
if bg_color is not None:
p.setColor(self._cmd_msg.backgroundRole(), bg_color)
self._cmd_msg.setPalette(p)
def cmd_msg_clean(self):
if not self.cmd_msg_show_get_continuing_key:
self.cmd_msg("")
def end_get_continuing_key(self):
if self.get_continuing_key is not None:
self.get_continuing_key_readed_keys = None
if self.cmd_msg_show_get_continuing_key:
self.cmd_msg("")
get_continuing_key = self.get_continuing_key # Prevent cyclic recursion
self.get_continuing_key = None
get_continuing_key.close()
def append_key_to_cmd_msg(self, key, mod):
self.get_continuing_key_readed_keys += key_to_str(key, mod)
self.cmd_msg(self.get_continuing_key_readed_keys + "...", QColor(200,200,255), is_get_continuing_key=True)
@pyqtSlot()
def find_changed(self):
s = self._find_input.text()
if len(s) >= 3:
self.find(s)
else:
self.clear_find()
@qtoverride
def closeEvent(self, event):
event.ignore()
@qtoverride
def keyPressEvent(self, event):
MOD_KEYS = (16781694, 16777248, 16777249, 16777299, 16777251, 16777400)
SHIFT = Qt.ShiftModifier
CTRL = Qt.ControlModifier
F_KEYS = {v: k+1 for k, v in enumerate([
16777264, 16777265, 16777266, 16777267, 16777268,
16777269, 16777270, 16777271, 16777272, 16777273,
16777274, 16777275
])}
NUM_KEYS = {ord('`'):0,
**{ord(str(i)): i for i in range(1, 10)},
ord('0'): 10, ord('-'): 11, ord('='): 12,
}
LEFT_KEYS = {ord('H'), Qt.Key_Left}
UP_KEYS = {ord('J'), Qt.Key_Up}
DOWN_KEYS = {ord('K'), Qt.Key_Down}
RIGHT_KEYS = {ord('L'), Qt.Key_Right}
ESCAPE = 16777216;
print(event, type(event), event.text(), event.key(), int(event.modifiers()))
mod = int(event.modifiers())
key = int(event.key())
mod_to_show = mod
key_to_show = key
event.accept()
if key not in MOD_KEYS:
self.cmd_msg_clean()
async def get_container_or_workspace(filter=lambda x: True, pre_filter=lambda x: True, get_workspace=True):
master = self.focused_master
if master is None:
master = MIN_MASTER
by_priority = [[] for i in range(2*2*3)]
shortcuts = {}
def priority(m, s):
if m == self.focused_master:
if s == self.focused_slave:
return 0
return 1
return 2
for (m, s), w in self._workspaces.items():
for nd in w._tree.all_window_nodes:
if pre_filter(nd):
by_priority[(0 if nd.title_with_find else 6) + 2*priority(m, s)].append(nd)
for nd in w._tree.all_inner_nides:
if pre_filter(nd):
by_priority[6 + 2*priority(m, s)+1].append(nd)
gen = get_shortcuts([len(x) for x in by_priority], WORKSPACES_KEY.values())
for x in by_priority:
for nd in x:
shortcut = next(gen)
if filter(nd):
nd.shortcut = shortcut
shortcuts_node = shortcuts
for c in nd.shortcut[:-1]:
shortcuts_node.setdefault(c, {})
shortcuts_node = shortcuts_node[c]
shortcuts_node[nd.shortcut[-1]] = nd
nd.redraw()
try:
while True:
key, mod = await continuing_key()
if get_workspace:
if mod == 0 and key in F_KEYS:
master = F_KEYS[key]
continue
if mod == 0 and key in ORD_TO_WORKSPACE:
return self._workspaces[parse_workspace(ORD_TO_WORKSPACE[key])]
elif mod == 0 and key in NUM_KEYS:
slave = NUM_KEYS[key]
if slave > MAX_MASTERED_SLAVE:
master = None
return self._workspaces[(master, slave)]
if mod != 0 or not (ord('A') <= key and key <= ord('Z')):
raise NoSutchKey(key, mod)
c = chr(key).lower()
if c not in shortcuts:
raise NoSutchKey(key, mod)
for (i, nd) in shortcuts.items():
if i == c:
def go(x):
if type(x) == dict:
for i in x.values(): go(i)
else:
x.shortcut = x.shortcut[1:]
x.redraw()
go(nd)
else:
def go(x):
if type(x) == dict:
for i in x.values(): go(i)
else:
x.shortcut = None
x.redraw()
go(nd)
shortcuts = shortcuts[c]
if type(shortcuts) != dict:
return shortcuts
finally:
if shortcuts is not None:
def go(x):
if type(x) == dict:
for i in x.values():
go(i)
else:
x.shortcut = None
x.redraw()
go(shortcuts)
@types.coroutine
def continuing_key():
x = yield None
return x
def add_continuing_key(key, mod):
try:
self.get_continuing_key.send((key, mod))
except I3CmdException as e:
self.cmd_msg(e.error, QColor(255, 100, 100))
traceback.print_exc()
m_win.load_i3_tree()
except NoSutchKey as e:
self.cmd_msg(f"No sutch key {self.get_continuing_key_readed_keys}{e.key_str}", QColor(255, 100, 100))
self.get_continuing_key_readed_keys = None
self.get_continuing_key = None
except (StopIteration, GeneratorExit):
self.get_continuing_key_readed_keys = None
if self.cmd_msg_show_get_continuing_key:
self.cmd_msg("")
self.get_continuing_key = None
else:
self.append_key_to_cmd_msg(key, mod)
async def ck_move(nd_from):
def filter(nd):
while nd is not None:
if nd == nd_from:
return False
nd = nd.parentWidget()
return True
nd_to = await get_container_or_workspace(filter=filter)
with nd_to.marked_context(QColor(0,255,0)):
if type(nd_to) == WorkspaceWidget:
nd_from.move_to_workspace(nd_to.master, nd_to.slave)
else:
key, mod = await continuing_key()
if mod == 0 and key == ord('N'):
nd_from.move(nd_to)
elif mod == 0 and key == ord('M'):
nd_from.move(nd_to, expand=True)
elif mod == 0 and key == ord('I'):
nd_from.move(nd_to, before=True)
elif mod == 0 and key == ord('O'):
nd_from.move(nd_to, expand=True, before=True)
elif mod in [0, SHIFT] and key == ord('H'):
nd_from.move(nd_to, new_container="splith", before=mod==SHIFT)
elif mod in [0, SHIFT] and key == ord('V'):
nd_from.move(nd_to, new_container="splitv", before=mod==SHIFT)
elif mod in [0, SHIFT] and key == ord('T'):
nd_from.move(nd_to, new_container="tabbed", before=mod==SHIFT)
elif mod in [0, SHIFT] and key == ord('S'):
nd_from.move(nd_to, new_container="stacked", before=mod==SHIFT)
elif mod in [0, SHIFT] and key == ord('J'):
nd_from.move(nd_to, new_container="splith", before=mod==SHIFT, expand=True)
elif mod in [0, SHIFT] and key == ord('B'):
nd_from.move(nd_to, new_container="splitv", before=mod==SHIFT, expand=True)
elif mod in [0, SHIFT] and key == ord('Y'):
nd_from.move(nd_to, new_container="tabbed", before=mod==SHIFT, expand=True)
elif mod in [0, SHIFT] and key == ord('D'):
nd_from.move(nd_to, new_container="stacked", before=mod==SHIFT, expand=True)
elif mod in [0] and key == ord('C'):
nd_from.move(nd_to, swap=True)
else:
raise NoSutchKey(key, mod)
self.load_i3_tree()
self.cmd_msg("OK")
async def keyPressEvent_main(key, mod):
if mod == 0 and self.get_continuing_key is not None and key == ESCAPE:
self.end_get_continuing_key()
elif self.get_continuing_key is not None:
if key not in MOD_KEYS:
add_continuing_key(key, mod)
elif key == ord('?') and mod == SHIFT:
self.show_help()
elif mod == 0 and key == ord('F'):
nd = await get_container_or_workspace()
nd.focus()
elif mod == 0 and key == ord('G'):
nd = await get_container_or_workspace()
nd.move_to_workspace(self.focused_master, self.focused_slave)
self.load_i3_tree()
self.cmd_msg("OK")
elif mod == 0 and key == ord('M'):
nd_from = await get_container_or_workspace()
with nd_from.marked_context(QColor(255, 0, 0)):
await ck_move(nd_from)
elif mod == 0 and key == ord('C'):
nd = await get_container_or_workspace()
with nd.marked_context(QColor(255, 0, 0)):
key, mod = await continuing_key()
if mod == 0 and key == ord('M'):
await ck_move(nd)
elif type(nd) == I3InnerNodeWidget and mod == 0 and key == ord('V'):
nd.change_layout("splitv")
elif type(nd) == I3InnerNodeWidget and mod == 0 and key == ord('H'):
nd.change_layout("splitv")
elif type(nd) == I3InnerNodeWidget and mod == 0 and key == ord('T'):
nd.change_layout("tabbed")
elif type(nd) == I3InnerNodeWidget and mod == 0 and key == ord('S'):
nd.change_layout("stacked")
elif type(nd) == I3InnerNodeWidget and mod == 0 and key == ord('E'):
nd.expand()
self.load_i3_tree()
elif type(nd) == I3InnerNodeWidget and mod == 0 and key == ord('F'):
nd.focus()
elif mod == 0 and key == ord('A'):
w = nd.workspace_widget
self.focus_workspace(w.master, w.slave)
elif mod == 0 and key == ord('Q'):
key, mod = await continuing_key()
if key == ord("Y") and mod in [0, SHIFT]:
nd.quit_windows()
self.load_i3_tree()
else:
raise NoSutchKey(key, mod)
elif mod == 0 and key == ord('K'):
key, mod = await continuing_key()
if key == ord("Y") and mod in [SHIFT]:
nd.quit_windows(force=True)
self.load_i3_tree()
else:
raise NoSutchKey(key, mod)
else:
raise NoSutchKey(key, mod)
elif mod == 0 and key == ord('R'): # Undocumented
for i in self._workspaces.values():
i.metadata_changed()
elif mod == 0 and key == ord('T'):
m_win.end_get_continuing_key()
self.load_i3_tree()
elif mod == 0 and key == ord('O'):
self.focused_widget.move_to_otput()
elif mod == 0 and key == ord('W'):
try:
self.set_screenshot_size([x for x in SCREENSHOTS_SIZES if x > self.screenshot_size][0])
except IndexError:
pass
elif mod == 0 and key == ord('E'):
try:
self.set_screenshot_size([x for x in SCREENSHOTS_SIZES if x < self.screenshot_size][-1])
except IndexError:
pass
elif mod == 0 and key in ORD_TO_WORKSPACE:
self.change_focused_forkspace(workspace_cmd=ORD_TO_WORKSPACE[key])
elif mod == 0 and key in NUM_KEYS:
self.change_focused_forkspace(slave_cmd=NUM_KEYS[key])
elif mod == 0 and key in F_KEYS:
self.change_focused_forkspace(master_cmd=F_KEYS[key])
elif mod == 0 and key in LEFT_KEYS:
self.change_focused_forkspace(slave_cmd="prev-skip")
elif mod == SHIFT and key in LEFT_KEYS:
self.change_focused_forkspace(slave_cmd="prev-limit")
elif mod == 0 and key in RIGHT_KEYS:
self.change_focused_forkspace(slave_cmd="next-skip")
elif mod == SHIFT and key in RIGHT_KEYS:
self.change_focused_forkspace(slave_cmd="next-limit")
elif mod == 0 and key in UP_KEYS:
self.change_focused_forkspace(master_cmd="prev-skip")
elif mod == SHIFT and key in UP_KEYS:
self.change_focused_forkspace(master_cmd="prev")
elif mod == 0 and key in DOWN_KEYS:
self.change_focused_forkspace(master_cmd="next-skip")
elif mod == SHIFT and key in DOWN_KEYS:
self.change_focused_forkspace(master_cmd="next")
elif mod == CTRL and key in LEFT_KEYS:
self.find_next(-1)
elif mod == CTRL and key in RIGHT_KEYS:
self.find_next(1)
elif mod == SHIFT and key == ord("N"):
self.find_next(-1)
elif mod == 0 and key == ord("N"):
self.find_next(1)
elif mod == 0 and key == ord('/'):
self._find_input.setFocus()
elif mod == 0 and key == ESCAPE: # ESCAPE
with shared.lock:
w = get_workspace()
shared.i3_cmd(f'workspace {workspace(*w)}')
elif mod == 0 and key == 16777220: # ENTER
shared.i3_cmd(f'workspace {workspace(self.focused_master, self.focused_slave)}')
else:
if key not in MOD_KEYS:
raise NoSutchKey(key, mod)
async def keyPressEvent_find(key, mod):
if mod == 0 and key == 16777216: # ESCAPE
self.clear_find()
self._find_input.setText("")
self.find_error = None
self.set_find_msg()
self._scroll.setFocus()
elif mod == 0 and key == 16777220: # ENTER
self._scroll.setFocus()
elif mod == 0 and key == Qt.Key_Up:
self.find_next(-1)
elif mod == 0 and key == Qt.Key_Down:
self.find_next(1)
else:
nonlocal mod_to_show
nonlocal key_to_show
mod_to_show = mod & ~CTRL
key_to_show = key
await keyPressEvent_main(key, mod & ~CTRL)
async def main():
if app.focusWidget() == self._find_input:
await keyPressEvent_find(key, mod)
else:
await keyPressEvent_main(key, mod)
try:
x = main()
x.send(None)
except StopIteration:
pass
except NoSutchKey as e:
traceback.print_exc()
self.cmd_msg(f"No sutch key {e.key_str}", QColor(255, 100, 100))
except I3CmdException as e:
self.end_get_continuing_key()
self.cmd_msg(e.error, QColor(255, 100, 100))
traceback.print_exc()
m_win.load_i3_tree()
else:
self.end_get_continuing_key()
self.get_continuing_key = x
self.get_continuing_key_readed_keys = ""
self.append_key_to_cmd_msg(key, mod)
def show_help(self):
self._help_window.show()
def move_to_gui_workspace(self):
shared.i3_cmd(f'[id={int(self.winId())}] move container to workspace 0')
m_win = MainWindow()
m_win.show()
m_win.move_to_gui_workspace()
with shared.lock:
load_workspaces()
app.exec_()