4
""" curses_misc.py: Module for various widgets that are used throughout
8
# Copyright (C) 2008-2009 Andrew Psaltis
10
# This program is free software; you can redistribute it and/or modify
11
# it under the terms of the GNU General Public License as published by
12
# the Free Software Foundation; either version 2 of the License, or
13
# (at your option) any later version.
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
# GNU General Public License for more details.
20
# You should have received a copy of the GNU General Public License
21
# along with this program; if not, write to the Free Software
22
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
27
# Uses code that is towards the bottom
28
def error(ui,parent,message):
29
"""Shows an error dialog (or something that resembles one)"""
33
dialog = TextDialog(message,6,40,('important',"ERROR"))
34
return dialog.run(ui,parent)
37
# Although I could have made this myself pretty easily, just want to give credit
39
# http://excess.org/urwid/browser/contrib/trunk/rbreu_filechooser.py
40
class SelText(urwid.Text):
41
"""A selectable text widget. See urwid.Text."""
44
"""Make widget selectable."""
48
def keypress(self, size, key):
49
"""Don't handle any keys."""
52
# ListBox that can't be selected.
53
class NSelListBox(urwid.ListBox):
57
# This class is annoying. :/
58
class DynWrap(urwid.AttrWrap):
60
Makes an object have mutable selectivity. Attributes will change like
64
sensitive = current selectable state
65
attrs = tuple of (attr_sens,attr_not_sens)
66
attrfoc = attributes when in focus, defaults to nothing
69
def __init__(self,w,sensitive=True,attrs=('editbx','editnfc'),focus_attr='editfc'):
71
self._sensitive = sensitive
73
cur_attr = attrs[0] if sensitive else attrs[1]
75
self.__super.__init__(w,cur_attr,focus_attr)
77
def get_sensitive(self):
78
return self._sensitive
79
def set_sensitive(self,state):
81
self.set_attr(self._attrs[0])
83
self.set_attr(self._attrs[1])
84
self._sensitive = state
85
property(get_sensitive,set_sensitive)
89
def set_attrs(self,attrs):
91
property(get_attrs,set_attrs)
94
return self._sensitive
96
# Just an Edit Dynwrapped to the most common specifications
97
class DynEdit(DynWrap):
98
def __init__(self,caption='',edit_text='',sensitive=True,attrs=('editbx','editnfc'),focus_attr='editfc'):
99
caption = ('editcp',caption + ': ')
100
edit = urwid.Edit(caption,edit_text)
101
self.__super.__init__(edit,sensitive,attrs,focus_attr)
103
# Just an IntEdit Dynwrapped to the most common specifications
104
class DynIntEdit(DynWrap):
105
def __init__(self,caption='',edit_text='',sensitive=True,attrs=('editbx','editnfc'),focus_attr='editfc'):
106
caption = ('editcp',caption + ':')
107
edit = urwid.IntEdit(caption,edit_text)
108
self.__super.__init__(edit,sensitive,attrs,focus_attr)
110
class DynRadioButton(DynWrap):
111
def __init__(self,group,label,state='first True',on_state_change=None, user_data=None, sensitive=True, attrs=('body','editnfc'),focus_attr='body'):
112
#caption = ('editcp',caption + ':')
113
button = urwid.RadioButton(group,label,state,on_state_change,user_data)
114
self.__super.__init__(button,sensitive,attrs,focus_attr)
116
class MaskingEditException(Exception):
119
# Password-style edit
120
class MaskingEdit(urwid.Edit):
123
"always" : everything is a '*' all of the time
124
"no_focus" : everything is a '*' only when not in focus
125
"off" : everything is always unmasked
126
mask_char = the single character that masks all other characters in the field
128
def __init__(self, caption = "", edit_text = "", multiline = False,
129
align = 'left', wrap = 'space', allow_tab = False,
130
edit_pos = None, layout=None, mask_mode="always",mask_char='*'):
131
self.mask_mode = mask_mode
132
if len(mask_char) > 1:
133
raise MaskingEditException('Masks of more than one character are not supported!')
134
self.mask_char = mask_char
135
self.__super.__init__(caption,edit_text,multiline,align,wrap,allow_tab,edit_pos,layout)
137
def get_caption(self):
139
def get_mask_mode(self):
140
return self.mask_mode
141
def set_mask_mode(self,mode):
142
self.mask_mode = mode
144
def get_masked_text(self):
145
return self.mask_char*len(self.get_edit_text())
147
def render(self,(maxcol,), focus=False):
149
Render edit widget and return canvas. Include cursor when in
152
# If we aren't masking anything ATM, then act like an Edit.
154
if self.mask_mode == "off" or (self.mask_mode == 'no_focus' and focus == True):
155
canv = self.__super.render((maxcol,),focus)
156
# The cache messes this thing up, because I am totally changing what
161
# Else, we have a slight mess to deal with...
162
self._shift_view_to_cursor = not not focus # force bool
164
text, attr = self.get_text()
165
text = text[:len(self.caption)]+self.get_masked_text()
166
trans = self.get_line_translation( maxcol, (text,attr) )
167
canv = urwid.canvas.apply_text_layout(text, attr, trans, maxcol)
170
canv = urwid.CompositeCanvas(canv)
171
canv.cursor = self.get_cursor_coords((maxcol,))
175
# Tabbed interface, mostly for use in the Preferences Dialog
176
class TabColumns(urwid.WidgetWrap):
178
titles_dict = dictionary of tab_contents (a SelText) : tab_widget (box)
179
attr = normal attributes
180
attrsel = attribute when active
182
# FIXME Make the bottom_part optional
183
def __init__(self,tab_str,tab_wid,title,bottom_part=None,attr=('body','focus'),
184
attrsel='tab active', attrtitle='header'):
185
#self.bottom_part = bottom_part
186
#title_wid = urwid.Text((attrtitle,title),align='right')
189
text,trash = w.get_text()
190
column_list.append(('fixed',len(text),w))
191
column_list.append(urwid.Text((attrtitle,title),align='right'))
193
self.tab_map = dict(zip(tab_str,tab_wid))
194
self.active_tab = tab_str[0]
195
self.columns = urwid.Columns(column_list,dividechars=1)
196
#walker = urwid.SimpleListWalker([self.columns,tab_wid[0]])
197
#self.listbox = urwid.ListBox(walker)
198
self.gen_pile(tab_wid[0],True)
199
self.frame = urwid.Frame(self.pile)
200
self.__super.__init__(self.frame)
202
# Make the pile in the middle
203
def gen_pile(self,lbox,firstrun=False):
204
self.pile = urwid.Pile([
205
('fixed',1,urwid.Filler(self.columns,'top')),
206
urwid.Filler(lbox,'top',height=('relative',99)),
207
#('fixed',1,urwid.Filler(self.bottom_part,'bottom'))
210
self.frame.set_body(self.pile)
211
self.set_w(self.frame)
213
def selectable(self):
216
def keypress(self,size,key):
217
if key == "meta [" or key == "meta ]":
218
self._w.get_body().set_focus(0)
219
newK = 'left' if key[-1] == '[' else 'right'
220
self.keypress(size,newK)
221
self._w.get_body().set_focus(1)
223
key = self._w.keypress(size,key)
224
wid = self.pile.get_focus().get_body()
225
if wid == self.columns:
226
# lw = self.listbox.body
228
self.active_tab.set_attr('body')
229
self.columns.get_focus().set_attr('tab active')
230
self.active_tab = self.columns.get_focus()
231
self.gen_pile(self.tab_map[self.active_tab])
234
# self.listbox.body = lw
235
def mouse_event(self,size,event,button,x,y,focus):
236
wid = self.pile.get_focus().get_body()
237
if wid == self.columns:
238
self.active_tab.set_attr('body')
240
self._w.mouse_event(size,event,button,x,y,focus)
241
if wid == self.columns:
242
self.active_tab.set_attr('body')
243
self.columns.get_focus().set_attr('tab active')
244
self.active_tab = self.columns.get_focus()
245
self.gen_pile(self.tab_map[self.active_tab])
248
### Combo box code begins here
249
class ComboBoxException(Exception):
252
# A "combo box" of SelTexts
253
# I based this off of the code found here:
254
# http://excess.org/urwid/browser/contrib/trunk/rbreu_menus.py
255
# This is a hack/kludge. It isn't without quirks, but it more or less works.
256
# We need to wait for changes in urwid's Canvas controls before we can actually
257
# make a real ComboBox.
258
class ComboBox(urwid.WidgetWrap):
259
"""A ComboBox of text objects"""
260
class ComboSpace(urwid.WidgetWrap):
261
"""The actual menu-like space that comes down from the ComboBox"""
262
def __init__(self,list,body,ui,show_first,pos=(0,0),attr=('body','focus')):
265
list : stuff to include in the combobox
267
show_first: index of the element in the list to pick first
268
pos : a tuple of (row,col) where to put the list
269
attr : a tuple of (attr_no_focus,attr_focus)
272
#Calculate width and height of the menu widget:
276
if len(entry) > width:
278
content = [urwid.AttrWrap(SelText(w), attr[0], attr[1])
280
self._listbox = urwid.ListBox(content)
281
self._listbox.set_focus(show_first)
283
overlay = urwid.Overlay(self._listbox, body, ('fixed left', pos[0]),
284
width + 2, ('fixed top', pos[1]), height)
285
self.__super.__init__(overlay)
287
def show(self,ui,display):
289
dim = ui.get_cols_rows()
295
ui.draw_screen(dim, self.render(dim, True))
297
keys = ui.get_input()
299
if "window resize" in keys:
300
dim = ui.get_cols_rows()
304
(wid,pos) = self._listbox.get_focus()
305
(text,attr) = wid.get_text()
309
#Send key to underlying widget:
310
self._w.keypress(dim, k)
314
def __init__(self,label='',list=[],attrs=('body','editnfc'),focus_attr='focus',use_enter=True,focus=0,callback=None,user_args=None):
316
label : bit of text that preceeds the combobox. If it is "", then
318
list : stuff to include in the combobox
321
row : where this object is to be found onscreen
322
focus : index of the element in the list to pick first
323
callback : function that takes (combobox,sel_index,user_args=None)
324
user_args : user_args in the callback
327
self.DOWN_ARROW = ' vvv'
328
self.label = urwid.Text(label)
330
self.focus_attr = focus_attr
333
str,trash = self.label.get_text()
336
#w,sensitive=True,attrs=('editbx','editnfc'),focus_attr='editfc')
337
self.cbox = DynWrap(SelText(self.DOWN_ARROW),attrs=attrs,focus_attr=focus_attr)
338
# Unicode will kill me sooner or later. ^_^
340
w = urwid.Columns([('fixed',len(str),self.label),self.cbox],dividechars=1)
342
w = urwid.Columns([self.cbox])
343
self.__super.__init__(w)
345
# We need this to pick our keypresses
346
self.use_enter = use_enter
350
# The callback and friends
351
self.callback = callback
352
self.user_args = user_args
354
# Widget references to simplify some things
358
def set_list(self,list):
361
def set_focus(self,index):
363
self.cbox.set_w(SelText(self.list[index]+self.DOWN_ARROW))
365
self.overlay._listbox.set_focus(index)
367
def rebuild_combobox(self):
368
self.build_combobox(self.parent,self.ui,self.row)
369
def build_combobox(self,parent,ui,row):
370
str,trash = self.label.get_text()
372
self.cbox = DynWrap(SelText([self.list[self.focus]+self.DOWN_ARROW]),
373
attrs=self.attrs,focus_attr=self.focus_attr)
375
w = urwid.Columns([('fixed',len(str),self.label),self.cbox],
377
self.overlay = self.ComboSpace(self.list,parent,ui,self.focus,
378
pos=(len(str)+1,row))
380
w = urwid.Columns([self.cbox])
381
self.overlay = self.ComboSpace(self.list,parent,ui,self.focus,
389
# If we press space or enter, be a combo box!
390
def keypress(self,size,key):
391
activate = key == ' '
393
activate = activate or key == 'enter'
395
# Die if the user didn't prepare the combobox overlay
396
if self.overlay == None:
397
raise ComboBoxException('ComboBox must be built before use!')
398
retval = self.overlay.show(self.ui,self.parent)
400
self.set_focus(self.list.index(retval))
401
#self.cbox.set_w(SelText(retval+' vvv'))
402
if self.callback != None:
403
self.callback(self,self.overlay._listbox.get_focus()[1],
405
return self._w.keypress(size,key)
407
def selectable(self):
408
return self.cbox.selectable()
412
return self.overlay._listbox.get_focus()
414
return None,self.focus
416
def get_sensitive(self):
417
return self.cbox.get_sensitive()
418
def set_sensitive(self,state):
419
self.cbox.set_sensitive(state)
421
# This is a h4x3d copy of some of the code in Ian Ward's dialog.py example.
422
class DialogExit(Exception):
425
class Dialog2(urwid.WidgetWrap):
426
def __init__(self, text, height,width, body=None ):
427
self.width = int(width)
429
self.width = ('relative', 80)
430
self.height = int(height)
432
self.height = ('relative', 80)
436
# fill space with nothing
437
body = urwid.Filler(urwid.Divider(),'top')
439
self.frame = urwid.Frame( body, focus_part='footer')
441
self.frame.header = urwid.Pile( [urwid.Text(text,align='right'),
446
# pad area around listbox
447
#w = urwid.Padding(w, ('fixed left',2), ('fixed right',2))
448
#w = urwid.Filler(w, ('fixed top',1), ('fixed bottom',1))
449
#w = urwid.AttrWrap(w, 'body')
451
# buttons: tuple of name,exitcode
452
def add_buttons(self, buttons):
454
for name, exitcode in buttons:
455
b = urwid.Button( name, self.button_press )
456
b.exitcode = exitcode
457
b = urwid.AttrWrap( b, 'body','focus' )
459
self.buttons = urwid.GridFlow(l, 10, 3, 1, 'center')
460
self.frame.footer = urwid.Pile( [ urwid.Divider(),
461
self.buttons ], focus_item = 1)
463
def button_press(self, button):
464
raise DialogExit(button.exitcode)
466
def run(self,ui,parent):
467
ui.set_mouse_tracking()
468
size = ui.get_cols_rows()
469
overlay = urwid.Overlay(urwid.LineBox(self.view),
470
parent, 'center', self.width,
471
'middle', self.height)
474
canvas = overlay.render( size, focus=True )
475
ui.draw_screen( size, canvas )
478
keys = ui.get_input()
480
if urwid.is_mouse_event(k):
481
event, button, col, row = k
482
overlay.mouse_event( size,
483
event, button, col, row,
486
if k == 'window resize':
487
size = ui.get_cols_rows()
488
k = self.view.keypress( size, k )
492
self.unhandled_key( size, k)
493
except DialogExit, e:
494
return self.on_exit( e.args[0] )
496
def on_exit(self, exitcode):
499
def unhandled_key(self, size, key):
502
# Simple dialog with text in it and "OK"
503
class TextDialog(Dialog2):
504
def __init__(self, text, height, width, header=None,align='left'):
505
l = [urwid.Text(text)]
507
# l.append( urwid.Text( line,align=align))
508
body = urwid.ListBox(l)
509
body = urwid.AttrWrap(body, 'body')
511
Dialog2.__init__(self, header, height+2, width+2, body)
512
self.add_buttons([('OK',1)])
515
def unhandled_key(self, size, k):
516
if k in ('up','page up','down','page down'):
517
self.frame.set_focus('body')
518
self.view.keypress( size, k )
519
self.frame.set_focus('footer')
521
class InputDialog(Dialog2):
522
def __init__(self, text, height, width,ok_name='OK',edit_text=''):
523
self.edit = urwid.Edit(wrap='clip',edit_text=edit_text)
524
body = urwid.ListBox([self.edit])
525
body = urwid.AttrWrap(body, 'editbx','editfc')
527
Dialog2.__init__(self, text, height, width, body)
529
self.frame.set_focus('body')
530
self.add_buttons([(ok_name,0),('Cancel',-1)])
532
def unhandled_key(self, size, k):
533
if k in ('up','page up'):
534
self.frame.set_focus('body')
535
if k in ('down','page down'):
536
self.frame.set_focus('footer')
538
# pass enter to the "ok" button
539
self.frame.set_focus('footer')
540
self.view.keypress( size, k )
542
def on_exit(self, exitcode):
543
return exitcode, self.edit.get_edit_text()
546
class ClickCols(urwid.WidgetWrap):
547
def __init__(self,items,callback=None,args=None):
548
cols = urwid.Columns(items)
549
self.__super.__init__(cols)
550
self.callback = callback
552
def mouse_event(self,size,event,button,x,y,focus):
553
if event == "mouse press":
554
# The keypress dealie in wicd-curses.py expects a list of keystrokes
555
self.callback([self.args])
557
# htop-style menu menu-bar on the bottom of the screen
558
class OptCols(urwid.WidgetWrap):
559
# tuples = [(key,desc)], on_event gets passed a key
560
# attrs = (attr_key,attr_desc)
561
# handler = function passed the key of the "button" pressed
562
# mentions of 'left' and right will be converted to <- and -> respectively
563
def __init__(self,tuples,handler,attrs=('body','infobar'),debug=False):
564
# Find the longest string. Keys for this bar should be no greater than
565
# 2 characters long (e.g., -> for left)
568
# newmax = len(i[0])+len(i[1])
569
# if newmax > maxlen:
572
# Construct the texts
575
# callbacks map the text contents to its assigned callback.
578
splitcmd = cmd[0].split()
580
for part in splitcmd:
584
# If anyone has a problem with this, they can bother me
590
elif part == 'right':
594
elif part == 'enter':
599
#theText = urwid.Text([(attrs[0],cmd[0]),(attrs[1],cmd[1])])
601
callback = self.debugClick
606
#self.callbacks.append(cmd[2])
608
('fixed',len(key)+1,urwid.Text((attrs[0],key+':')) ),
609
urwid.AttrWrap(urwid.Text(cmd[1]),attrs[1])],
611
#if i != len(tuples)-1:
612
# textList.append(('fixed',maxlen,col))
613
#else: # The last one
617
self.debug = urwid.Text("DEBUG_MODE")
618
textList.append(('fixed',10,self.debug))
620
cols = urwid.Columns(textList)
621
self.__super.__init__(cols)
622
def debugClick(self,args):
623
self.debug.set_text(args)
625
def mouse_event(self,size,event,button,x,y,focus):
626
# Widgets are evenly long (as of current), so...
627
return self._w.mouse_event(size,event,button,x,y,focus)
632
widsize = (size[0]-10)/len(self.callbacks)
634
widsize = size[0]/len(self.callbacks)
638
if self.callbacks[widnum] == None:
640
self.debug.set_text(text)
641
elif self.callbacks[widnum] != None:
642
self.callbacks[widnum]()