chiark / gitweb /
xscsize.c, etc.: Report geometry of individual monitors.
[xtoys] / xtoys.py
1 ### -*-python-*-
2 ###
3 ### Utility module for xtoys Python programs
4 ###
5 ### (c) 2007 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Edgeware X tools collection.
11 ###
12 ### X tools is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
16 ###
17 ### X tools is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 ### GNU General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with X tools; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 ###--------------------------------------------------------------------------
27 ### External dependencies.
28
29 import os as OS
30 import optparse as O
31 from sys import stdin, stdout, exit, argv
32
33 import pygtk
34 pygtk.require('2.0')
35 import gtk as GTK
36 GDK = GTK.gdk
37 import gobject as GO
38 del pygtk
39
40 ###--------------------------------------------------------------------------
41 ### Reasons for living.
42
43 _reasons = 0
44
45 def addreason():
46   """Add a reason."""
47   global _reasons
48   _reasons += 1
49
50 def delreason():
51   """Drop a reason.  When reasons reach zero, the main loop stops."""
52   global _reasons
53   _reasons -= 1
54   if _reasons == 0:
55     GTK.main_quit()
56
57 ###--------------------------------------------------------------------------
58 ### General utilities.
59
60 def make_optparse(options, **kw):
61   """
62   Construct an option parser object.
63
64   The KW are keyword arguments to be passed to OptionParser.  The OPTIONS are
65   a list of (SHORT, LONG, KW) triples; the SHORT and LONG strings do /not/
66   have leading dashes.
67   """
68   op = O.OptionParser(**kw)
69   for short, long, kw in options:
70     names = ['--%s' % long]
71     if short is not None:
72       names.append('-%s' % short)
73     op.add_option(*names, **kw)
74   return op
75
76 ###--------------------------------------------------------------------------
77 ### Message boxes.
78
79 class MessageButton (object):
80   """
81   An object storing information about a button in a Message.
82   """
83
84   def __init__(me, label, value = None, *options):
85     """
86     Initialize a button definition.
87
88     If LABEL is a tuple, then it should have the form
89     (LABEL, VALUE, OPTIONS...); the initialization parser is applied to its
90     contents.  This is not done recursively.
91
92     If LABEL is a string, and VALUE and OPTIONS are omitted, then it is
93     parsed as OPT:OPT:...:LABEL, where OPT is either an option (see below) or
94     `=VALUE'.  Only one VALUE may be given.
95
96     The LABEL is the label to put on the button, or the GTK stock id.  The
97     VALUE is the value to return from Message.ask if the button is chosen.
98     The OPTIONS are:
99
100       * 'default': this is the default button
101       * 'cancel': this is the cancel button
102     """
103     if value is not None or len(options) > 0:
104       me._doinit(label, value, *options)
105     elif isinstance(label, tuple):
106       me._doinit(*label)
107     else:
108       i = 0
109       options = []
110       while 0 <= i < len(label):
111         if label[i] == '!':
112           i += 1
113           break
114         j = label.find(':', i)
115         if j < 0:
116           break
117         if label[i] == '=':
118           if value is not None:
119             raise ValueError, 'Duplicate value in button spec %s' % label
120           value = label[i + 1:j]
121         else:
122           options.append(label[i:j])
123         i = j + 1
124       label = label[i:]
125       me._doinit(label, value, *options)
126
127   def _doinit(me, label, value = None, *options):
128     """
129     Does the work of processing the initialization parameters.
130     """
131     me.label = label
132     if value is None:
133       me.value = label
134     else:
135       me.value = value
136     me.defaultp = me.cancelp = False
137     for opt in options:
138       if opt == 'default':
139         me.defaultp = True
140       elif opt == 'cancel':
141         me.cancelp = True
142       else:
143         raise ValueError, 'unknown button option %s' % opt
144
145 class Message (GTK.MessageDialog):
146   """
147   A simple message-box window: contains text and some buttons.
148
149   See __init__ for the usage instructions.
150   """
151
152   ## Mapping from Pythonic strings to GTK constants.
153   dboxtype = {'info': GTK.MESSAGE_INFO,
154               'warning': GTK.MESSAGE_WARNING,
155               'question': GTK.MESSAGE_QUESTION,
156               'error': GTK.MESSAGE_ERROR}
157
158   def __init__(me,
159                title = None,
160                type = 'info',
161                message = '',
162                headline = None,
163                buttons = [],
164                markupp = False):
165     """
166     Report a message to the user and get a response back.
167
168     The TITLE is placed in the window's title bar.
169
170     The TYPE controls what kind of icon is placed in the window; it should be
171     one of 'info', 'warning', 'question' or 'error'.
172
173     The MESSAGE is the string which should be displayed.  It may have
174     multiple lines.  There may also be a HEADLINE message.  The messages are
175     parsed for Pango markup if MARKUPP is set.
176
177     The BUTTONS are a list of buttons to show, right to left.  Each one
178     should be a string which may either be a GTK stock tag or a plain label
179     string.  This may be prefixed, optionally, by `!' to ignore other prefix
180     characters, `+' to make this button the default, or `:' to make this the
181     `cancel' button, chosen by pressing escape.  If no button is marked as
182     cancel, we just use the first.
183     """
184
185     ## Initialize superclasses.
186     GTK.MessageDialog.__init__(me, type = me.dboxtype.get(type))
187
188     ## Set the title.
189     if title is None:
190       title = OS.path.basename(argv[0])
191     me.set_title(title)
192
193     ## Add the message strings.
194     me.set_property('use-markup', markupp)
195     if headline is None:
196       me.set_property('text', message)
197     else:
198       me.set_property('text', headline)
199       me.set_property('secondary-text', message)
200       me.set_property('secondary-use-markup', markupp)
201
202     ## Include the buttons.
203     if len(buttons) == 0:
204       buttons = [MessageButton('gtk-ok', True, 'default', 'cancel')]
205     else:
206       buttons = buttons[:]
207       buttons.reverse()
208     me.buttons = [isinstance(b, MessageButton) and b or MessageButton(b)
209                   for b in buttons]
210     cancel = -1
211     default = -1
212     for i in xrange(len(buttons)):
213       button = me.buttons[i]
214       label = button.label
215       if GTK.stock_lookup(label) is None:
216         b = GTK.Button(label = label)
217       else:
218         b = GTK.Button(stock = label)
219       if button.defaultp:
220         default = i
221       if button.cancelp:
222         cancel = i
223       b.set_flags(b.flags() | GTK.CAN_DEFAULT)
224       button.widget = b
225       me.add_action_widget(b, i)
226
227     ## Choose default buttons.
228     if cancel == -1:
229       cancel = 0
230     if default == -1:
231       default = len(me.buttons) - 1
232     me.cancel = cancel
233     me.default = default
234
235     ## Connect up the handlers and go.
236     me.connect('key-press-event', me.keypress, me.buttons[cancel])
237     me.buttons[default].widget.grab_default()
238
239   def keypress(me, win, event, cancel):
240     """
241     Handle key-press events.
242
243     If the EVENT was an escape-press, then activate the CANCEL button.
244     """
245     key = GDK.keyval_name(event.keyval)
246     if key != 'Escape':
247       return False
248     cancel.widget.activate()
249     return True
250
251   def ask(me):
252     """
253     Display the message, and wait for a response.
254
255     The return value is the label of the button which was chosen.
256     """
257     me.show_all()
258     r = me.run()
259     if r < 0:
260       r = me.cancel
261     me.hide()
262     return me.buttons[r].value
263
264 ###----- That's all, folks --------------------------------------------------