Commit e7fa9146 authored by chrysn's avatar chrysn

added trunk



git-svn-id: http://svn.amsuess.com/svn/tools/arandr/trunk@121 84c1553d-868a-485e-9ebb-c7de0e225ff1
parents
This diff is collapsed.
README
arandr
setup.py
screenlayout/__init__.py
screenlayout/auxiliary.py
screenlayout/demo.py
screenlayout/gui.py
screenlayout/metacity.py
screenlayout/snap.py
screenlayout/widget.py
screenlayout/xrandr.py
COPYING
TODO
screenlayout/data/gpl-3.txt
data/arandr.desktop
data/arandr.1
data/arandr.1.txt
data/po/de.po
==========================
ARandR: Another XRandR GUI
==========================
Design intent
-------------
Provide a simple visual front end for XRandR_ 1.2, client side X only (no xorg.conf involved, no pre-1.2 options).
Features
--------
* Full controll over positioning (instead of plain "left of") with edge snapping
* Saving configurations as executable shell scripts (configurations can be loaded without using this program)
* Configuration files can be edited to include additional payload (like xsetwacom_ commands tablet PC users need when rotating), which is preserved when editing
* Metacity keybinding integration:
* Saved configurations can be bound to arbitrary keys via metacity's custom commands.
* Several layouts can be bound to one key; they are cycled through. (Useful for "rotate" buttons on tablet PCs.)
* Main widget separated from packaged application (to facilitate integration with existing solutions)
Status
------
Works for me, hardly tested on other systems. (If this program had a controlled release cycle, the current version would be called "alpha".)
See TODO_ for planned features.
Installation
------------
* Python Setuptools style::
sudo easy_install http://svn.amsuess.com/svn/tools/arandr/
arandr
* Debian package_::
wget http://christian.amsuess.com/tools/arandr/files/arandr_0.0-1_all.deb
sudo dpkg -i arandr_0.0-1_all.deb
arandr
* Directly from SVN::
svn co http://svn.amsuess.com/svn/tools/arandr/
cd arandr
./run.py
Dependencies
------------
* xrandr
* setuptools_ (Debian_/Ubuntu_: ``python-setuptools``)
Bugs / Caveats
--------------
* Not tested in many environments
* Changes while running are not caught (no HAL events!)
About
-----
Copyright (c) chrysn_ <chrysn@fsfe.org> 2008, published under GPLv3_
Inspired by the `dual head sketch`_ in the ThinkWiki_.
.. _XRandR: http://www.x.org/wiki/Projects/XRandR
.. _xsetwacom: http://linuxwacom.sourceforge.net/index.php/howto/xsetwacom
.. _TODO: ./TODO
.. _setuptools: http://pypi.python.org/pypi/setuptools
.. _package: http://christian.amsuess.com/tools/arandr/files/arandr_0.0-1_all.deb
.. _Debian: http://packages.debian.org/etch/python-setuptools
.. _Ubuntu: http://packages.ubuntu.com/gutsy/python-setuptools
.. _chrysn: http://christian.amsuess.com
.. _GPLv3: http://www.gnu.org/licenses/gpl-3.0.txt
.. _`dual head sketch`: http://www.thinkwiki.org/wiki/Image:Intel-DualHead.png
.. _ThinkWiki: http://thinkwiki.org/
* add option to make layout default on startup
* implement missing xrandr features: reflect, additional modes (for forcibly applying saved layouts so they are already active when an output is connected), fb, fbmm/dpi
* indicate connection status
* call xrandr without blocking
* add command line args (loading configurations, using other $DISPLAY internally)
* receive notifications on changes (XRRScreenChangeNotify)
* integrate related settings: (might end up writing a complete configuration manager, which is not my intention)
* xsetwacom
* gnome-panel layout
#!/usr/bin/env python
"""Run ARandR GUI"""
from screenlayout.gui import main
main()
.\" Man page generated from reStructeredText.
.TH arandr 1 "2008-06-03" "" ""
.SH NAME
arandr \- visual front end for XRandR 1.2
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level magin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.SH SYNOPSIS
\fBarandr\fP
.SH DESCRIPTION
ARandR is a visual front end for XRandR 1.2 (per display options), which
provides full controll over positioning, saving and loading to/from shell
scripts and easy integration with other applications.
.SH SEE ALSO
\fBman 1 xrandr\fP
.\" As long as rst2man is not yet widespread, download it from http://docutils.sourceforge.net/sandbox/manpage-writer/.
.\" The ready-made arandr.1 will be in the package as long as rst2man is not packaged.
.SH AUTHOR
chrysn <chrysn@fsfe.org>
.\" Generated by docutils manpage writer on 2008-06-03 16:40.
.\"
=========
arandr
=========
-------------------------------
visual front end for XRandR 1.2
-------------------------------
:Author: chrysn <chrysn@fsfe.org>
:Date: 2008-06-03
:Manual section: 1
SYNOPSIS
=========
``arandr``
DESCRIPTION
===========
ARandR is a visual front end for XRandR 1.2 (per display options), which
provides full controll over positioning, saving and loading to/from shell
scripts and easy integration with other applications.
SEE ALSO
========
``man 1 xrandr``
.. As long as rst2man is not yet widespread, download it from
http://docutils.sourceforge.net/sandbox/manpage-writer/.
The ready-made arandr.1 will be in the package as long as rst2man is not
packaged.
[Desktop Entry]
Name=ARandR
GenericName=Screen Settings
GenericName[de]=Bildschirmeinstellungen
Icon=display
Exec=arandr
Terminal=false
Type=Application
Categories=Settings;HardwareSettings;
StartupNotify=true
# ARandR
# Copyright (C) 2008 chrysn <chrysn@fsfe.org
# This file is distributed under the same license as the arandr package.
# chrysn <chrysn@fsfe.org>, 2008.
msgid ""
msgstr ""
"Project-Id-Version: arandr 0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-06-03 18:29+0200\n"
"PO-Revision-Date: 2008-06-03 18:38+0100\n"
"Last-Translator: chrysn <chrysn@fsfe.org>\n"
"Language-Team: German\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: screenlayout/gui.py:91
msgid "_Layout"
msgstr "_Layout"
#: screenlayout/gui.py:102
msgid "_View"
msgstr "_Ansicht"
#: screenlayout/gui.py:104
msgid "_Outputs"
msgstr "_Ausgabegeräte"
#: screenlayout/gui.py:105
msgid "Dummy"
msgstr "Dummy"
#: screenlayout/gui.py:107
msgid "_System"
msgstr "_System"
#: screenlayout/gui.py:108
msgid "_Keybindings (Metacity)"
msgstr "_Tastenkombinationen (Metacity)"
#: screenlayout/gui.py:110
msgid "_Help"
msgstr "_Hilfe"
#: screenlayout/gui.py:114
msgid "1:4"
msgstr "1:4"
#: screenlayout/gui.py:115
msgid "1:8"
msgstr "1:8"
#: screenlayout/gui.py:116
msgid "1:16"
msgstr "1:16"
#: screenlayout/gui.py:160
msgid "Script Properties"
msgstr "Skript-Eigenschaften"
#: screenlayout/gui.py:172
msgid "Script"
msgstr "Skript"
#: screenlayout/gui.py:188
#, python-format
msgid "XRandR failed: %s"
msgstr "XRandR fehlgeschlagen: %s"
#: screenlayout/gui.py:198
msgid "Open Layout"
msgstr "Layout öffnen"
#: screenlayout/gui.py:210
msgid "Save Layout"
msgstr "Layout speichern"
#: screenlayout/gui.py:261
msgid "Keybindings (via Metacity)"
msgstr "Tastenkombinationen (über Metacity)"
#: screenlayout/gui.py:283
msgid "ARandR Screen Layout Editor"
msgstr "ARandR Bildschirmlayout-Editor"
#: screenlayout/gui.py:286
msgid "Another XRandR GUI (1.2 only)"
msgstr "Another XRandR GUI (nur 1.2)"
#: screenlayout/metacity.py:32
msgid "Accelerator"
msgstr "Kombination"
#: screenlayout/metacity.py:33
msgid "Action"
msgstr "Aktion"
#: screenlayout/metacity.py:91
msgid "disabled"
msgstr "deaktiviert"
#: screenlayout/metacity.py:106
msgid "New accelerator..."
msgstr "Neue Kombination..."
#: screenlayout/metacity.py:168
msgid "no action"
msgstr "keine Aktion"
#: screenlayout/metacity.py:198
msgid "incompatible configuration"
msgstr "Konfiguration nicht verwendbar."
#: screenlayout/metacity.py:205
msgid "other application"
msgstr "andere Anwendung"
#: screenlayout/widget.py:42
msgid "Your configuration does not include an active monitor. Do you want to apply the configuration?"
msgstr "Diese Konfiguration beinhaltet keinen aktiven Monitor. Soll sie angewandt werden?"
#: screenlayout/widget.py:256
msgid "Active"
msgstr "Aktiv"
#: screenlayout/widget.py:274
#, python-format
msgid "Setting this resolution is not possible here: %s"
msgstr "Setzen der Bildschirmauflösung unmöglich: %s"
#: screenlayout/widget.py:287
#, python-format
msgid "This orientation is not possible here: %s"
msgstr "Drehung nicht möglich: %s"
#: screenlayout/widget.py:293
msgid "Resolution"
msgstr "Auflösung"
#: screenlayout/widget.py:295
msgid "Orientation"
msgstr "Drehung"
#: screenlayout/xrandr.py:196
msgid "A part of an output is outside the virtual screen."
msgstr "Ein Teil des Ausgabegeräts liegt außerhalb des Virtual-Bereichs."
#: screenlayout/xrandr.py:199
msgid "An output is outside the virtual screen."
msgstr "Ein Ausgabegerät liegt außerhalb des Virtual-Bereichs."
"""Exceptions and generic classes"""
from math import pi
class FileLoadError(Exception): pass
class FileSyntaxError(FileLoadError): pass
class InadequateConfiguration(Exception): pass
class BetterList(list):
"""List that can be split like a string"""
def indices(self, item):
i = -1
while True:
try:
i = self.index(item, i+1)
except ValueError:
break
yield i
def split(self, item):
indices = list(self.indices(item))
yield self[:indices[0]]
for x in (self[a+1:b] for (a,b) in zip(indices[:-1], indices[1:])):
yield x
yield self[indices[-1]+1:]
class Size(tuple):
def __new__(cls, arg):
if isinstance(arg, basestring):
arg = [int(x) for x in arg.split("x")]
arg = tuple(arg)
assert len(arg)==2
return super(Size, cls).__new__(cls, arg)
width = property(lambda self:self[0])
height = property(lambda self:self[1])
def __str__(self):
return "%dx%d"%self
class Position(tuple):
def __new__(cls, arg):
if isinstance(arg, basestring):
arg = [int(x) for x in arg.split("x")]
arg = tuple(arg)
assert len(arg)==2
return super(Position, cls).__new__(cls, arg)
left = property(lambda self:self[0])
top = property(lambda self:self[1])
def __str__(self):
return "%dx%d"%self
class Geometry(tuple):
def __new__(cls, width, height=None, left=None, top=None):
if isinstance(width, basestring):
width,rest = width.split("x")
height,left,top = rest.split("+")
return super(Geometry, cls).__new__(cls, (int(width), int(height), int(left), int(top)))
def __str__(self):
return "%dx%d+%d+%d"%self
width = property(lambda self:self[0])
height = property(lambda self:self[1])
left = property(lambda self:self[2])
top = property(lambda self:self[3])
position = property(lambda self:Position(self[2:4]))
size = property(lambda self:Size(self[0:2]))
class Rotation(str):
def __init__(self, original_me):
if self not in ('left','right','normal','inverted'):
raise Exception("No know rotation.")
is_odd = property(lambda self: self in ('left','right'))
_angles = {'left':pi/2,'inverted':pi,'right':3*pi/2,'normal':0}
angle = property(lambda self: Rotation._angles[self])
def __repr__(self):
return '<Rotation %s>'%self
LEFT = Rotation('left')
RIGHT = Rotation('right')
INVERTED = Rotation('inverted')
NORMAL = Rotation('normal')
ROTATIONS = (NORMAL, RIGHT, INVERTED, LEFT)
This diff is collapsed.
import gtk
from . import widget
def main():
w = gtk.Window()
w.connect('destroy',gtk.main_quit)
r = widget.ARandRWidget()
r.load_from_x()
b = gtk.Button("Reload")
b.connect('clicked', lambda *args: r.load_from_x())
b2 = gtk.Button("Apply")
b2.connect('clicked', lambda *args: r.save_to_x())
v = gtk.VBox()
w.add(v)
v.add(r)
v.add(b)
v.add(b2)
w.set_title('Simple ARandR Widget Demo')
w.show_all()
gtk.main()
# This Python file uses the following encoding: utf-8
"""Main GUI for ARandR"""
import os, stat
import gtk, gobject
from . import widget
from .metacity import MetacityWidget
#import os
#os.environ['DISPLAY']=':0.0'
import gettext
gettext.install('arandr')
def actioncallback(function):
"""Wrapper around a function that is intended to be used both as a callback from a gtk.Action and as a normal function.
Functions taking no arguments will never be given any, functions taking one argument (callbacks for radio actions) will be given the value of the action or just the argument.
A first argument called 'self' is passed through."""
import inspect
argnames = inspect.getargspec(function)[0]
if argnames[0] == 'self':
has_self = True
argnames.pop(0)
else:
has_self = False
assert len(argnames) in (0,1)
def wrapper(*args):
args_in = list(args)
args_out = []
if has_self:
args_out.append(args_in.pop(0))
if len(argnames) == len(args_in): # called directly
args_out.extend(args_in)
elif len(argnames)+1 == len(args_in):
if len(argnames):
args_out.append(args_in[1].props.value)
else: raise TypeError("Arguments don't match")
return function(*args_out)
wrapper.__name__ = function.__name__
wrapper.__doc__ = function.__doc__
return wrapper
class Application(object):
uixml = """
<ui>
<menubar name="MenuBar">
<menu action="Layout">
<menuitem action="New" />
<menuitem action="Open" />
<menuitem action="SaveAs" />
<separator />
<menuitem action="Apply" />
<menuitem action="LayoutSettings" />
<separator />
<menuitem action="Quit" />
</menu>
<menu action="View">
<menuitem action="Zoom4" />
<menuitem action="Zoom8" />
<menuitem action="Zoom16" />
</menu>
<menu action="Outputs" name="Outputs">
<menuitem action="OutputsDummy" />
</menu>
<menu action="System">
<menuitem action="Metacity" />
</menu>
<menu action="Help">
<menuitem action="About" />
</menu>
</menubar>
<toolbar name="ToolBar">
<toolitem action="Apply" />
<separator />
<toolitem action="New" />
<toolitem action="Open" />
<toolitem action="SaveAs" />
</toolbar>
</ui>
"""
def __init__(self):
self.window = window = gtk.Window()
window.props.title = "Screen Layout Editor"
# actions
actiongroup = gtk.ActionGroup('default')
actiongroup.add_actions([
("Layout", None, _("_Layout")),
("New", gtk.STOCK_NEW, None, None, None, self.do_new),
("Open", gtk.STOCK_OPEN, None, None, None, self.do_open),
("SaveAs", gtk.STOCK_SAVE_AS, None, None, None, self.do_save_as),
("Apply", gtk.STOCK_APPLY, None, '<Control>Return', None, self.do_apply),
("LayoutSettings", gtk.STOCK_PROPERTIES, None, '<Alt>Return', None, self.do_open_properties),
("Quit", gtk.STOCK_QUIT, None, None, None, gtk.main_quit),
("View", None, _("_View")),
("Outputs", None, _("_Outputs")),
("OutputsDummy", None, _("Dummy")),
("System", None, _("_System")),
("Metacity", None, _("_Keybindings (Metacity)"), None, None, self.do_open_metacity),
("Help", None, _("_Help")),
("About", gtk.STOCK_ABOUT, None, None, None, self.about),
])
actiongroup.add_radio_actions([
("Zoom4", None, _("1:4"), None, None, 4),
("Zoom8", None, _("1:8"), None, None, 8),
("Zoom16", None, _("1:16"), None, None, 16),
], 8, self.set_zoom)
window.connect('destroy', gtk.main_quit)
# uimanager
self.uimanager = gtk.UIManager()
accelgroup = self.uimanager.get_accel_group()
window.add_accel_group(accelgroup)
self.uimanager.insert_action_group(actiongroup, 0)
self.uimanager.add_ui_from_string(self.uixml)
# widget
self.widget = widget.ARandRWidget()
self.filetemplate = self.widget.load_from_x()
self.widget.connect('changed', self._widget_changed)
self._widget_changed(self.widget)
# window layout
vbox = gtk.VBox()
menubar = self.uimanager.get_widget('/MenuBar')
vbox.pack_start(menubar, expand=False)
toolbar = self.uimanager.get_widget('/ToolBar')
vbox.pack_start(toolbar, expand=False)
vbox.add(self.widget)
window.add(vbox)
window.show_all()
self.gconf = None
#################### actions ####################
@actioncallback
def set_zoom(self, value): # don't use directly: state is not pushed back to action.
self.widget.factor = value
self.window.resize(1,1)
@actioncallback
def do_open_properties(self):
d = gtk.Dialog(_("Script Properties"), None, gtk.DIALOG_MODAL, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
d.set_default_size(300,400)
script_editor = gtk.TextView()
script_buffer = script_editor.get_buffer()
script_buffer.set_text("\n".join(self.filetemplate))
script_editor.props.editable = False
#wacom_options = gtk.Label("FIXME")
nb = gtk.Notebook()
#nb.append_page(wacom_options, gtk.Label(_("Wacom options")))
nb.append_page(script_editor, gtk.Label(_("Script")))
d.vbox.pack_start(nb)
d.show_all()
d.run()
d.destroy()
@actioncallback
def do_apply(self):
if self.widget.abort_if_unsafe():
return
try:
self.widget.save_to_x()
except Exception, e:
d = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("XRandR failed: %s")%e)
d.run()
d.destroy()
@actioncallback
def do_new(self):
self.filetemplate = self.widget.load_from_x()
@actioncallback
def do_open(self):
d = self._new_file_dialog(_("Open Layout"), gtk.FILE_CHOOSER_ACTION_OPEN)
result = d.run()
filenames = d.get_filenames()
d.destroy()
if result == gtk.RESPONSE_ACCEPT:
assert len(filenames) == 1
f = filenames[0]
self.filetemplate = self.widget.load_from_file(f)
@actioncallback
def do_save_as(self):
d = self._new_file_dialog(_("Save Layout"), gtk.FILE_CHOOSER_ACTION_SAVE)
d.props.do_overwrite_confirmation = True
result = d.run()
filenames = d.get_filenames()
d.destroy()
if result == gtk.RESPONSE_ACCEPT:
assert len(filenames) == 1
f = filenames[0]
if not f.endswith('.sh'): f = f + '.sh'
self.widget.save_to_file(f, self.filetemplate)
def _new_file_dialog(self, title, type):
d = gtk.FileChooserDialog(title, None, type)
d.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
d.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT)
layoutdir = os.path.expanduser('~/.screenlayout/')
try:
os.makedirs(layoutdir)
except OSError:
pass
d.set_current_folder(layoutdir)
f = gtk.FileFilter()
f.set_name('Shell script (Layout file)')
f.add_pattern('*.sh')
d.add_filter(f)
return d
def reset(self, widget):
self.widget.reload()
self._widget_changed(widget)
#################### widget maintenance ####################
def _widget_changed(self, widget):
self._populate_outputs()
def _populate_outputs(self):
w = self.uimanager.get_widget('/MenuBar/Outputs')
w.props.submenu = self.widget.contextmenu()
#################### metacity ####################
@actioncallback
def do_open_metacity(self):
d = gtk.Window()
d.props.modal = True
d.props.title = _("Keybindings (via Metacity)")