M winrustler/ui/__main__.py +65 -69
@@ 4,16 4,15 @@
import sys
import os.path
import logging
-import functools
import attr
from PyQt5.QtCore import (
QObject,
pyqtSignal,
+ pyqtSlot,
Qt,
QTimer,
- QSettings,
)
from PyQt5.QtGui import (
QIcon,
@@ 33,6 32,7 @@ from PyQt5.QtWidgets import (
from winrustler.core import REGISTRY
from winrustler.winapi import WindowDiscovery, get_window_title
from winrustler.ui.winapi import get_window_icon, WinHooker
+from winrustler.ui.state import save_window_state, restore_window_state
VERSION = 420
ABOUT_TEXT = """\
@@ 57,20 57,8 @@ def icon(filename):
return QIcon(os.path.join(RES_DIR, filename))
-def log_exceptions(fn):
- @functools.wraps(fn)
- def inner(*args, **kwargs):
- try:
- fn(*args, **kwargs)
- except (SystemExit, KeyboardInterrupt):
- raise
- except:
- logger.exception("Unhandled exception in %r", fn)
- return inner
-
-
@attr.s(frozen=True)
-class Suggestion(object):
+class History(object):
search = attr.ib()
rustle = attr.ib()
@@ 79,20 67,18 @@ class RustlerWindow(QDialog):
rustle = pyqtSignal(object)
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, winset, *args, **kwargs):
super().__init__(*args, **kwargs)
+ self.setAttribute(Qt.WA_DeleteOnClose)
self.setWindowTitle("WinRustler")
- from .widgets import WindowSelect, MoveControls, FadeControls#, MatchSelect
+ from .widgets.select import WindowSelect
+ from .widgets.rustle import MoveControls, FadeControls
self._select = WindowSelect(self)
- #from PyQt5.QtWidgets import QPushButton
- #self._match_select = QPushButton(icon('1f984.png'), "", self)
- #menu = QMenu(self._match_select)
- #menu.addAction("hi")
- #self._match_select.setMenu(menu)
-
- #self._match = WindowMatch(self)
+ from PyQt5.QtWidgets import QPushButton
+ self._match_select = QPushButton(icon('1f984.png'), "", self, shortcut="Alt+m")
+ self._match_select.clicked.connect(self._show_match)
#from PyQt5.QtWidgets import QTabWidget
#self._window_tab = QTabWidget(self)
@@ 111,50 97,45 @@ class RustlerWindow(QDialog):
self._bb = QDialogButtonBox(self)
self._bb.accepted.connect(self.accept)
self._bb.rejected.connect(self.reject)
- self._bb.clicked.connect(self._on_clicked)
+ self._bb.clicked.connect(self._bb_on_clicked)
self._apply = self._bb.addButton(QDialogButtonBox.Apply)
self._apply_and_close = self._bb.addButton('&Apply && Close', QDialogButtonBox.AcceptRole)
self._close = self._bb.addButton(QDialogButtonBox.Close)
- #from PyQt5.QtWidgets import QFormLayout
- #self._layout = QFormLayout(self)
- #self._layout.addRow(self._description)
- ##self._layout.addRow("&Window", self._window_select)
- ##self._layout.addRow("&Left", self._x)
- ##self._layout.addRow("&Top", self._y)
- #self._layout.addRow(self._move_viewport)
- #self._layout.addRow(self._bb)
- #self._layout.setSizeConstraint(QFormLayout.SetFixedSize)
from PyQt5.QtWidgets import QVBoxLayout
self._layout = QVBoxLayout(self)
from PyQt5.QtWidgets import QHBoxLayout
self._select_layout = QHBoxLayout()
self._select_layout.addWidget(self._select, stretch=1)
- #self._select_layout.addWidget(self._match_select)
- #self._layout.addWidget(self._window_tab)
+ self._select_layout.addWidget(self._match_select)
self._layout.addLayout(self._select_layout)
- #self._layout.addWidget(self._select)
- #self._layout.addWidget(self._match_select)
self._layout.addWidget(self._function_tab)
self._layout.addWidget(self._bb)
self._layout.setSizeConstraint(QVBoxLayout.SetFixedSize)
- #self.setLayout(self._layout)
self._select.updated.connect(self._refresh_engagement)
- #self._match.updated.connect(self._refresh_engagement)
self._move.updated.connect(self._refresh_engagement)
self._refresh_engagement()
- def sync_windows(self, *args):
- self._select.sync_windows(*args)
+ self.winset = winset
+ self.winset.tell_and_connect(self.sync_windows)
+
+ @pyqtSlot(object, object)
+ def sync_windows(self, *args, **kwargs):
+ """
+ This needs to exist so that it can be a slot and Qt disconnects it
+ properly, otherwise PyQt start holding reference to things and fucks
+ everything up.
+ """
+ self._select.sync_windows(*args, **kwargs)
def _refresh_engagement(self):
is_acceptable = self.request() is not None
self._apply.setEnabled(is_acceptable)
self._apply_and_close.setEnabled(is_acceptable)
- def _on_clicked(self, button):
+ def _bb_on_clicked(self, button):
from PyQt5.QtWidgets import QDialogButtonBox
if button == self._apply:
self.rustle.emit(self.request())
@@ 166,6 147,14 @@ class RustlerWindow(QDialog):
else:
raise NotImplementedError()
+ def _show_match(self):
+ #from .widgets import ComposeMatch
+ from .widgets.match import MatchDialog
+ m = MatchDialog(self.winset, parent=self)
+ #m = ComposeMatch(self.winset)
+ #app.tell_windows_and_connect(m.sync_windows)
+ print(m.exec_())
+
def request(self):
hwnd = self._select.hwnd
tab = self._function_tab.currentWidget()
@@ 174,24 163,20 @@ class RustlerWindow(QDialog):
return tab.window_request(hwnd)
def showEvent(self, event):
- settings = QSettings("WinRustler Corp.", "WinRustler")
- geometry = settings.value("RustlerWindow/geometry")
- if geometry is not None:
- self.restoreGeometry(geometry)
+ restore_window_state(self)
return super().showEvent(event)
- def closeEvent(self, event):
- settings = QSettings("WinRustler Corp.", "WinRustler")
- settings.setValue("RustlerWindow/geometry", self.saveGeometry())
- return super().closeEvent(event)
+ def hideEvent(self, event):
+ save_window_state(self)
+ return super().hideEvent(event)
class RustlerTray(QSystemTrayIcon):
rustle = pyqtSignal(object)
- def __init__(self, app, *args, **kwargs):
- super().__init__(app, *args, **kwargs)
+ def __init__(self, winset, *args, **kwargs):
+ super().__init__(winset, *args, **kwargs)
self.rustle_icon = icon('1f412.png')
self.about_icon = icon('1f49f.png')
self.exit_icon = QIcon()
@@ 206,7 191,7 @@ class RustlerTray(QSystemTrayIcon):
self.activated.connect(self._icon_clicky)
#self.suggest = []
- self.app = app
+ self.winset = winset
self.window = None
self.setIcon(self.rustle_icon)
@@ 242,11 227,9 @@ class RustlerTray(QSystemTrayIcon):
def show_window(self):
if self.window is None:
- self.window = RustlerWindow(self.app)
- self.window.setAttribute(Qt.WA_DeleteOnClose)
+ self.window = RustlerWindow(self.winset)
self.window.rustle.connect(self.rustle)
self.window.destroyed.connect(self._window_destroyed)
- self.app.tell_windows_and_connect(self.window.sync_windows)
self.window.show()
self.window.raise_()
self.window.activateWindow()
@@ 291,6 274,26 @@ class SuggestiveRustles(QObject):
return Suggestion(search, rustle)
+class WindowSet(QObject):
+ """ This is very much like the WindowDiscovery but it has a signal ...
+ """
+
+ discovered = pyqtSignal(set, set)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.data = set()
+
+ def tell_and_connect(self, slot):
+ slot(self.data, ())
+ self.discovered.connect(slot)
+
+ def sync(self, adds, removes):
+ self.data.update(adds)
+ self.data.difference_update(removes)
+ self.discovered.emit(adds, removes)
+
+
class WinRustlerApp(QApplication):
#rustle = pyqtSignal(object)
@@ 298,11 301,12 @@ class WinRustlerApp(QApplication):
suggested = pyqtSignal(object)
# Tells you of hwnds that are newly added and removed.
# For use with something like `SyncToComboBox.sync()`.
- windows_discovered = pyqtSignal(set, set)
+ #windows_discovered = pyqtSignal(set, set)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.windows = WindowDiscovery(self.windows_discovered.emit)
+ self.winset = WindowSet(self)
+ self.windisc = WindowDiscovery(self.winset.sync)
self.discovery_timer = QTimer(self, interval=200, singleShot=True) # refresh debounce
self.hooker = WinHooker(self)
# Use a windows event hook to determine when we might want to update
@@ 310,14 314,10 @@ class WinRustlerApp(QApplication):
self.hooker.event.connect(self.discovery_timer.start)
# When this debounce timer fires, we tell the window discovery to do
# update its list of windows.
- self.discovery_timer.timeout.connect(self.windows.refresh)
+ self.discovery_timer.timeout.connect(self.windisc.refresh)
self.suggesty = SuggestiveRustles(self)
- def tell_windows_and_connect(self, slot):
- slot(app.windows.discovered, set())
- self.windows_discovered.connect(slot)
-
def event(self, e):
# This is here just so we can go into the interpreter in case SIGINT.
return super().event(e)
@@ 354,7 354,7 @@ if __name__ == "__main__":
qt_args = [sys.argv[0]] + args.qt_args
app = WinRustlerApp(qt_args, quitOnLastWindowClosed=False)
app.startTimer(100) # So the interpreter can handle SIGINT
- tray = RustlerTray(app)
+ tray = RustlerTray(app.winset)
tray.show()
tray.rustle.connect(app.do_rustling)
app.rustled.connect(tray.show_rustle_message)
@@ 362,10 362,6 @@ if __name__ == "__main__":
app.setWindowIcon(tray.icon())
if args.show:
- from .widgets import ComposeMatch
- #m = ComposeMatch()
- #m.show()
- #app.tell_windows_and_connect(m.sync_windows)
tray.show_window()
print("Lets go!")
A => winrustler/ui/debug.py +16 -0
@@ 0,0 1,16 @@
+import functools
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def log_exceptions(fn):
+ @functools.wraps(fn)
+ def inner(*args, **kwargs):
+ try:
+ return fn(*args, **kwargs)
+ except (SystemExit, KeyboardInterrupt):
+ raise
+ except:
+ logger.exception("Unhandled exception in %r", fn)
+ return inner
R winrustler/ui/models.py => +0 -0
A => winrustler/ui/res/1f40a.png +0 -0
A => winrustler/ui/res/1f984.png +0 -0
A => winrustler/ui/state.py +27 -0
@@ 0,0 1,27 @@
+import logging
+
+from PyQt5.QtCore import QSettings
+
+from winrustler.ui.debug import log_exceptions
+
+logger = logging.getLogger(__name__)
+
+
+@log_exceptions
+def save_window_state(window):
+ classname = type(window).__name__
+ assert classname
+ settings = QSettings("WinRustler Corp.", "WinRustler")
+ logger.debug("Saving %r", classname + "/geometry")
+ settings.setValue(classname + "/geometry", window.saveGeometry())
+
+
+@log_exceptions
+def restore_window_state(window):
+ classname = type(window).__name__
+ assert classname
+ settings = QSettings("WinRustler Corp.", "WinRustler")
+ logger.debug("Loading %r", classname + "/geometry")
+ geometry = settings.value(classname + "/geometry")
+ if geometry is not None:
+ window.restoreGeometry(geometry)
R winrustler/ui/widgets.py => +0 -226
@@ 1,226 0,0 @@
-import logging
-import textwrap
-
-from PyQt5.QtCore import (
- QObject,
- pyqtSignal,
- Qt,
-)
-from PyQt5.QtGui import (
- QPixmap,
- QIcon,
-)
-from PyQt5.QtWidgets import (
- qApp,
- QApplication,
- QWidget,
- QSystemTrayIcon,
- QMenu,
- QDialog,
- QMainWindow,
- QMessageBox,
- QAction,
- QSizePolicy,
- QVBoxLayout,
- QGridLayout,
- QFormLayout,
-)
-
-from PyQt5.QtWinExtras import (
- QtWin,
-)
-
-from winrustler.winapi import get_window_title, test_window_match
-from winrustler.ui.winapi import get_window_icon
-
-logger = logging.getLogger(__name__)
-
-class SyncToStandardItemModel(object):
-
- def __init__(self, model, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.model = model
- self.added_items = {}
-
- def __call__(self, adds, removes):
- from PyQt5.QtGui import QStandardItem
- logger.debug("Resyncing %s, adds=%r, removes=%r", self.model, adds, removes)
- for hwnd in removes:
- item = self.added_items.pop(hwnd)
- row = self.model.indexFromItem(item).row()
- assert [item] == self.model.takeRow(row)
- for hwnd in adds:
- # FIXME CURRENT WINDOW SYNCRONIZIATION STATE MEMES
- item = QStandardItem(get_window_icon(hwnd), get_window_title(hwnd))
- item.setData(hwnd, Qt.UserRole)
- self.model.appendRow(item)
- self.added_items[hwnd] = item
-
-
-class WindowSelect(QWidget):
- """
- Input:
- Call sync_windows() (SyncToStandardItemModel.__call__()) to tell it about the
- hwnds that are going on.
-
- Output:
- The hwnd() function for the currently selected hwnd.
- Emits updated() when this changes.
- """
-
- updated = pyqtSignal()
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- from PyQt5.QtWidgets import QComboBox
- from PyQt5.QtGui import QStandardItemModel
- from PyQt5.QtCore import QSortFilterProxyModel
- self._select = QComboBox(self)
- self._proxy = QSortFilterProxyModel(self._select)
- self._model = QStandardItemModel(0, 1, self._proxy)
- self._proxy.setSourceModel(self._model)
- self._select.setModel(self._proxy)
- self._select.currentIndexChanged.connect(self.updated)
-
- self._layout = QVBoxLayout(self)
- self._layout.setContentsMargins(0, 0, 0, 0)
- self._layout.addWidget(self._select)
- self.setLayout(self._layout)
-
- self.sync_windows = SyncToStandardItemModel(self._model)
-
- def hwnd(self):
- return self._select.currentData()
-
-
-#class MatchSelect(QAction):
-#
-# def __init__
-# pass
-
-
-class ComposeMatch(QWidget):
- """
- Input:
- Call sync_windows() (SyncToStandardItemModel.__call__()) to tell it about the
- hwnds that are going on.
-
- Output:
- The filter property will be a the user filter as a string.
- Emits updated() when this changes.
- """
-
- updated = pyqtSignal()
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- from PyQt5.QtWidgets import QComboBox, QListView, QLineEdit, QLabel
- from PyQt5.QtGui import QStandardItemModel
- from PyQt5.QtCore import QSortFilterProxyModel
-
- help_text = textwrap.dedent("""\
- <p>Filter on the window title. The filter is a case insensitive
- <a href='https://docs.python.org/3/library/re.html'>regular
- expression</a>.</p>
- """)
- self._help = QLabel(help_text, self, openExternalLinks=True)
- self._filter = QLineEdit(self, placeholderText="Filter...")
- self._list = QListView(self)
-
- self._proxy = QSortFilterProxyModel(self._list)
- self._model = QStandardItemModel(0, 1, self._proxy)
- self._proxy.setSourceModel(self._model)
- self._list.setModel(self._proxy)
-
- self._filter.textEdited.connect(self._refilter)
- self._filter.textEdited.connect(self.updated)
-
- self._layout = QVBoxLayout(self)
- self._layout.setContentsMargins(0, 0, 0, 0)
- self._layout.addWidget(self._help)
- self._layout.addWidget(self._filter)
- self._layout.addWidget(self._list)
- self.setLayout(self._layout)
-
- self.sync_windows = SyncToStandardItemModel(self._model)
-
- def _refilter(self, pattern):
- items = [self._model.item(row) for row in range(self._model.rowCount())]
- #hwnds = [item.data(role=Qt.UserRole) for item in items]
- for item in items:
- title, test = test_window_match(item.data(role=Qt.UserRole), pattern)
- item.setText(title) # In case it changed??
- item.setEnabled(test)
-
- @property
- def filter(self):
- return self._filter.text()
-
-
-from winrustler.mover import MoveWindow
-
-class MoveControls(QWidget):
-
- updated = pyqtSignal()
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- from PyQt5.QtWidgets import QSpinBox, QCheckBox, QLabel
- self._description = QLabel("<p>Move a window by setting the the top-left \
- of the window some number of pixels away from the top-left of \
- the desktop.</p>", self)
- self._x = QSpinBox(self, value=0, minimum=-2**16, maximum=2**16)
- self._y = QSpinBox(self, value=0, minimum=-2**16, maximum=2**16)
- self._move_viewport = QCheckBox("Set position of &internal viewport rather than the window frame", self)
- self._move_viewport.setCheckState(Qt.Checked)
-
- from PyQt5.QtWidgets import QFormLayout
- self._layout = QFormLayout(self)
- self._layout.addRow(self._description)
- self._layout.addRow("&Left", self._x)
- self._layout.addRow("&Top", self._y)
- self._layout.addRow(self._move_viewport)
- self.setLayout(self._layout)
-
- def window_request(self, hwnd):
- return MoveWindow(hwnd, self._x.value(), self._y.value(),
- self._move_viewport.checkState() == Qt.Checked)
-
-
-from winrustler.fader import FadeWindow
-
-class FadeControls(QWidget):
-
- DESCRIPTION_TEMPLATE = "<p>Set a window's opacity to {opacity}. At zero, it \
- is transparent; at 255 it is opaque.</p>"
-
- updated = pyqtSignal()
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- from PyQt5.QtWidgets import QSlider, QLabel, QHBoxLayout
- self._description = QLabel(parent=self)
- self._opacity = QSlider(Qt.Horizontal, parent=self, minimum=0, maximum=255)#, tickInterval=127, tickPosition=QSlider.TicksBelow)
- # This can't be a kwarg, probably because PyQt applies those in no
- # particular order and this should happen after setting the maximum.
- self._opacity.setValue(255)
-
- from PyQt5.QtWidgets import QFormLayout
- self._layout = QFormLayout(self)
- self._layout.addRow(self._description)
- self._layout.addRow("&Opacity", self._opacity)
- self.setLayout(self._layout)
-
- self._opacity.valueChanged.connect(self.updated)
- self.updated.connect(self._refresh_engagement)
- self._refresh_engagement()
-
- def _refresh_engagement(self):
- self._description.setText(self.DESCRIPTION_TEMPLATE.format(opacity=self._opacity.value()))
-
- def window_request(self, hwnd):
- return FadeWindow(hwnd, self._opacity.value())
A => winrustler/ui/widgets/match.py +106 -0
@@ 0,0 1,106 @@
+import logging
+import textwrap
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSortFilterProxyModel
+from PyQt5.QtWidgets import QWidget, QComboBox, QListView, QLineEdit, QLabel
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
+from PyQt5.QtGui import QStandardItemModel
+
+from winrustler.winapi import test_window_match
+from winrustler.ui.widgets.sync import SyncToStandardItemModel
+from winrustler.ui.state import save_window_state, restore_window_state
+
+logger = logging.getLogger(__name__)
+
+
+class ComposeMatch(QWidget):
+ """
+ Input:
+ Call sync_windows() (SyncToStandardItemModel.__call__()) to tell it about the
+ hwnds that are going on.
+
+ Output:
+ The filter property will be a the user filter as a string.
+ Emits updated() when this changes.
+ """
+
+ updated = pyqtSignal()
+
+ def __init__(self, winset, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.winset = winset
+
+ help_text = textwrap.dedent("""\
+ <p>Filter on the window title. The filter is a case insensitive
+ <a href='https://docs.python.org/3/library/re.html'>regular
+ expression</a>.</p>
+ """)
+ self._help = QLabel(help_text, self, openExternalLinks=True)
+ self._filter = QLineEdit(self, placeholderText="Filter...")
+ self._list = QListView(self)
+
+ self._proxy = QSortFilterProxyModel(self._list)
+ self._model = QStandardItemModel(0, 1, self._proxy)
+ self._proxy.setSourceModel(self._model)
+ self._list.setModel(self._proxy)
+
+ self._filter.textEdited.connect(self._refilter)
+ self._filter.textEdited.connect(self.updated)
+
+ self._layout = QVBoxLayout(self)
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._layout.addWidget(self._help)
+ self._layout.addWidget(self._filter)
+ self._layout.addWidget(self._list)
+ self.setLayout(self._layout)
+
+ self._sync = SyncToStandardItemModel(self._model)
+ self.winset.tell_and_connect(self.sync_windows)
+
+ @pyqtSlot(object, object)
+ def sync_windows(self, *args, **kwargs): # Because slots and reference counting
+ self._sync(*args, **kwargs)
+
+ def _refilter(self, pattern):
+ items = [self._model.item(row) for row in range(self._model.rowCount())]
+ #hwnds = [item.data(role=Qt.UserRole) for item in items]
+ for item in items:
+ title, test = test_window_match(item.data(role=Qt.UserRole), pattern)
+ item.setText(title) # In case it changed??
+ item.setEnabled(test)
+
+ @property
+ def filter(self):
+ return self._filter.text()
+
+
+class MatchDialog(QDialog):
+
+ def __init__(self, winset, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.setAttribute(Qt.WA_DeleteOnClose)
+ self.setWindowTitle("Window Match")
+
+ self._match = ComposeMatch(winset, parent=self)
+
+ self._bb = QDialogButtonBox(self)
+ self._bb.accepted.connect(self.accept)
+ self._bb.rejected.connect(self.reject)
+ #self._bb.clicked.connect(self._bb_on_clicked)
+
+ self._apply = self._bb.addButton(QDialogButtonBox.Apply)
+ #self._apply_and_close = self._bb.addButton('&Apply && Close', QDialogButtonBox.AcceptRole)
+ self._close = self._bb.addButton(QDialogButtonBox.Close)
+
+ self._layout = QVBoxLayout(self)
+ self._layout.addWidget(self._match)
+ self._layout.addWidget(self._bb)
+
+ def showEvent(self, event):
+ restore_window_state(self)
+ return super().showEvent(event)
+
+ def hideEvent(self, event):
+ save_window_state(self)
+ return super().hideEvent(event)
+
A => winrustler/ui/widgets/rustle.py +74 -0
@@ 0,0 1,74 @@
+import logging
+
+from PyQt5.QtCore import pyqtSignal, Qt
+from PyQt5.QtWidgets import (
+ QCheckBox,
+ QFormLayout,
+ QFormLayout,
+ QLabel,
+ QSlider,
+ QSpinBox,
+ QWidget,
+)
+
+from winrustler.mover import MoveWindow
+from winrustler.fader import FadeWindow
+
+
+class MoveControls(QWidget):
+
+ updated = pyqtSignal()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._description = QLabel("<p>Move a window by setting the the top-left \
+ of the window some number of pixels away from the top-left of \
+ the desktop.</p>", self)
+ self._x = QSpinBox(self, value=0, minimum=-2**16, maximum=2**16)
+ self._y = QSpinBox(self, value=0, minimum=-2**16, maximum=2**16)
+ self._move_viewport = QCheckBox("Set position of &internal viewport rather than the window frame", self)
+ self._move_viewport.setCheckState(Qt.Checked)
+
+ self._layout = QFormLayout(self)
+ self._layout.addRow(self._description)
+ self._layout.addRow("&Left", self._x)
+ self._layout.addRow("&Top", self._y)
+ self._layout.addRow(self._move_viewport)
+ self.setLayout(self._layout)
+
+ def window_request(self, hwnd):
+ return MoveWindow(hwnd, self._x.value(), self._y.value(),
+ self._move_viewport.checkState() == Qt.Checked)
+
+
+class FadeControls(QWidget):
+
+ DESCRIPTION_TEMPLATE = "<p>Set a window's opacity to {opacity}. At zero, it \
+ is transparent; at 255 it is opaque.</p>"
+
+ updated = pyqtSignal()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._description = QLabel(parent=self)
+ self._opacity = QSlider(Qt.Horizontal, parent=self, minimum=0, maximum=255)#, tickInterval=127, tickPosition=QSlider.TicksBelow)
+ # This can't be a kwarg, probably because PyQt applies those in no
+ # particular order and this should happen after setting the maximum.
+ self._opacity.setValue(255)
+
+ self._layout = QFormLayout(self)
+ self._layout.addRow(self._description)
+ self._layout.addRow("&Opacity", self._opacity)
+ self.setLayout(self._layout)
+
+ self._opacity.valueChanged.connect(self.updated)
+ self.updated.connect(self._refresh_engagement)
+ self._refresh_engagement()
+
+ def _refresh_engagement(self):
+ self._description.setText(self.DESCRIPTION_TEMPLATE.format(opacity=self._opacity.value()))
+
+ def window_request(self, hwnd):
+ return FadeWindow(hwnd, self._opacity.value())
A => winrustler/ui/widgets/select.py +48 -0
@@ 0,0 1,48 @@
+import logging
+
+from PyQt5.QtCore import (
+ QObject,
+ pyqtSignal,
+ Qt,
+ QSortFilterProxyModel,
+)
+from PyQt5.QtWidgets import QWidget, QComboBox, QVBoxLayout
+from PyQt5.QtGui import QStandardItemModel
+
+from winrustler.ui.widgets.sync import SyncToStandardItemModel
+
+logger = logging.getLogger(__name__)
+
+
+class WindowSelect(QWidget):
+ """
+ Input:
+ Call sync_windows() (SyncToStandardItemModel.__call__()) to tell it about the
+ hwnds that are going on.
+
+ Output:
+ The hwnd() function for the currently selected hwnd.
+ Emits updated() when this changes.
+ """
+
+ updated = pyqtSignal()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._select = QComboBox(self)
+ self._proxy = QSortFilterProxyModel(self._select)
+ self._model = QStandardItemModel(0, 1, self._proxy)
+ self._proxy.setSourceModel(self._model)
+ self._select.setModel(self._proxy)
+ self._select.currentIndexChanged.connect(self.updated)
+
+ self._layout = QVBoxLayout(self)
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._layout.addWidget(self._select)
+ self.setLayout(self._layout)
+
+ self.sync_windows = SyncToStandardItemModel(self._model)
+
+ def hwnd(self):
+ return self._select.currentData()
A => winrustler/ui/widgets/sync.py +36 -0
@@ 0,0 1,36 @@
+import logging
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtCore import pyqtSlot
+
+from winrustler.winapi import get_window_title
+from winrustler.ui.winapi import get_window_icon
+
+logger = logging.getLogger(__name__)
+
+
+class SyncToStandardItemModel(object):
+
+ def __init__(self, model, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.model = model
+ self.added_items = {}
+
+ def __call__(self, adds, removes):
+ from PyQt5.QtGui import QStandardItem
+ logger.debug("Resyncing %s, adds=%r, removes=%r", self.model, adds, removes)
+ try:
+ for hwnd in removes:
+ item = self.added_items.pop(hwnd)
+ row = self.model.indexFromItem(item).row()
+ assert [item] == self.model.takeRow(row)
+ for hwnd in adds:
+ # FIXME CURRENT WINDOW SYNCRONIZIATION STATE MEMES
+ item = QStandardItem(get_window_icon(hwnd), get_window_title(hwnd))
+ item.setData(hwnd, Qt.UserRole)
+ self.model.appendRow(item)
+ self.added_items[hwnd] = item
+ except (SystemExit, KeyboardInterrupt):
+ raise
+ except:
+ logger.exception("Uh oh...")