diff options
Diffstat (limited to 'services/console/lib/purk/widgets.py')
-rw-r--r-- | services/console/lib/purk/widgets.py | 811 |
1 files changed, 811 insertions, 0 deletions
diff --git a/services/console/lib/purk/widgets.py b/services/console/lib/purk/widgets.py new file mode 100644 index 0000000..1ad3cb8 --- /dev/null +++ b/services/console/lib/purk/widgets.py @@ -0,0 +1,811 @@ +import codecs + +import gobject +import gtk +import gtk.gdk +import pango + +from conf import conf +import parse_mirc +import windows + +import servers + +# Window activity Constants +HILIT = 'h' +TEXT ='t' +EVENT = 'e' + +ACTIVITY_MARKUP = { + HILIT: "<span style='italic' foreground='#00F'>%s</span>", + TEXT: "<span foreground='#ca0000'>%s</span>", + EVENT: "<span foreground='#363'>%s</span>", + } + +# This holds all tags for all windows ever +tag_table = gtk.TextTagTable() + +link_tag = gtk.TextTag('link') +link_tag.set_property('underline', pango.UNDERLINE_SINGLE) + +indent_tag = gtk.TextTag('indent') +indent_tag.set_property('indent', -20) + +tag_table.add(link_tag) +tag_table.add(indent_tag) + +#FIXME: MEH hates dictionaries, they remind him of the bad words +styles = {} + +def style_me(widget, style): + widget.set_style(styles.get(style)) + +def set_style(widget_name, style): + if style: + # FIXME: find a better way... + dummy = gtk.Label() + dummy.set_style(None) + + def apply_style_fg(value): + dummy.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse(value)) + + def apply_style_bg(value): + dummy.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(value)) + + def apply_style_font(value): + dummy.modify_font(pango.FontDescription(value)) + + style_functions = ( + ('fg', apply_style_fg), + ('bg', apply_style_bg), + ('font', apply_style_font), + ) + + for name, f in style_functions: + if name in style: + f(style[name]) + + style = dummy.rc_get_style() + else: + style = None + + styles[widget_name] = style + +def menu_from_list(alist): + while alist and not alist[-1]: + alist.pop(-1) + + last = None + for item in alist: + if item != last: + if item: + if len(item) == 2: + name, function = item + + menuitem = gtk.ImageMenuItem(name) + + elif len(item) == 3: + name, stock_id, function = item + + if isinstance(stock_id, bool): + menuitem = gtk.CheckMenuItem(name) + menuitem.set_active(stock_id) + else: + menuitem = gtk.ImageMenuItem(stock_id) + + if isinstance(function, list): + submenu = gtk.Menu() + for subitem in menu_from_list(function): + submenu.append(subitem) + menuitem.set_submenu(submenu) + + else: + menuitem.connect("activate", lambda a, f: f(), function) + + yield menuitem + + else: + yield gtk.SeparatorMenuItem() + + last = item + +class Nicklist(gtk.TreeView): + def click(self, event): + if event.button == 3: + x, y = event.get_coords() + + (data,), path, x, y = self.get_path_at_pos(int(x), int(y)) + + c_data = self.events.data(window=self.win, data=self[data], menu=[]) + + self.events.trigger("ListRightClick", c_data) + + if c_data.menu: + menu = gtk.Menu() + for item in menu_from_list(c_data.menu): + menu.append(item) + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + elif event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: + x, y = event.get_coords() + + (data,), path, x, y = self.get_path_at_pos(int(x), int(y)) + + self.events.trigger("ListDoubleClick", window=self.win, target=self[data]) + + def __getitem__(self, pos): + return self.get_model()[pos][0] + + def __setitem__(self, pos, name_markup): + realname, markedupname, sortkey = name_markup + + self.get_model()[pos] = realname, markedupname, sortkey + + def __len__(self): + return len(self.get_model()) + + def index(self, item): + for i, x in enumerate(self): + if x == item: + return i + + return -1 + + def append(self, realname, markedupname, sortkey): + self.get_model().append((realname, markedupname, sortkey)) + + def insert(self, pos, realname, markedupname, sortkey): + self.get_model().insert(pos, (realname, markedupname, sortkey)) + + def replace(self, names): + self.set_model(gtk.ListStore(str, str, str)) + + self.insert_column_with_attributes( + 0, '', gtk.CellRendererText(), markup=1 + ).set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + for name in names: + self.append(*name) + + self.get_model().set_sort_column_id(2, gtk.SORT_ASCENDING) + + def remove(self, realname): + index = self.index(realname) + + if index == -1: + raise ValueError + + self.get_model().remove(self.get_model().iter_nth_child(None, index)) + + def clear(self): + self.get_model().clear() + + def __iter__(self): + return (r[0] for r in self.get_model()) + + def __init__(self, window, core): + self.win = window + self.core = core + self.events = core.events + + gtk.TreeView.__init__(self) + + self.replace(()) + + self.set_headers_visible(False) + self.set_property("fixed-height-mode", True) + self.connect("button-press-event", Nicklist.click) + self.connect_after("button-release-event", lambda *a: True) + + style_me(self, "nicklist") + +# Label used to display/edit your current nick on a network +class NickEditor(gtk.EventBox): + def nick_change(self, entry): + oldnick, newnick = self.label.get_text(), entry.get_text() + + if newnick and newnick != oldnick: + self.events.run('nick %s' % newnick, self.win, self.win.network) + + self.win.input.grab_focus() + + def update(self, nick=None): + self.label.set_text(nick or self.win.network.me) + + def to_edit_mode(self, widget, event): + if self.label not in self.get_children(): + return + + if getattr(event, 'button', None) == 3: + c_data = self.events.data(window=self.win, menu=[]) + self.events.trigger("NickEditMenu", c_data) + + if c_data.menu: + menu = gtk.Menu() + for item in menu_from_list(c_data.menu): + menu.append(item) + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + else: + entry = gtk.Entry() + entry.set_text(self.label.get_text()) + entry.connect('activate', self.nick_change) + entry.connect('focus-out-event', self.to_show_mode) + + self.remove(self.label) + self.add(entry) + self.window.set_cursor(None) + + entry.show() + entry.grab_focus() + + def to_show_mode(self, widget, event): + self.remove(widget) + self.add(self.label) + self.win.input.grab_focus() + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM)) + + def __init__(self, window, core): + gtk.EventBox.__init__(self) + self.events = core.events + self.win = window + + self.label = gtk.Label() + self.label.set_padding(5, 0) + self.add(self.label) + + self.connect("button-press-event", self.to_edit_mode) + + self.update() + + self.connect( + "realize", + lambda *a: self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM)) + ) + +# The entry which you type in to send messages +class TextInput(gtk.Entry): + # Generates an input event + def entered_text(self, ctrl): + #for a in globals(): + # print a + #print events.__file__ + #self.core.run_command(self.text) + for line in self.text.splitlines(): + if line: + e_data = self.events.data( + window=self.win, network=self.win.network, + text=line, ctrl=ctrl + ) + self.events.trigger('Input', e_data) + if not e_data.done: + self.events.run(line, self.win, self.win.network) + + self.text = '' + + def _set_selection(self, s): + if s: + self.select_region(*s) + else: + self.select_region(self.cursor, self.cursor) + + #some nice toys for the scriptors + text = property(gtk.Entry.get_text, gtk.Entry.set_text) + cursor = property(gtk.Entry.get_position, gtk.Entry.set_position) + selection = property(gtk.Entry.get_selection_bounds, _set_selection) + + def insert(self, text): + self.do_insert_at_cursor(self, text) + + #hack to stop it selecting the text when we focus + def do_grab_focus(self): + temp = self.text, (self.selection or (self.cursor,)*2) + self.text = '' + gtk.Entry.do_grab_focus(self) + self.text, self.selection = temp + + def keypress(self, event): + keychar = ( + (gtk.gdk.CONTROL_MASK, '^'), + (gtk.gdk.SHIFT_MASK, '+'), + (gtk.gdk.MOD1_MASK, '!') + ) + + key = '' + for keymod, char in keychar: + # we make this an int, because otherwise it leaks + if int(event.state) & keymod: + key += char + key += gtk.gdk.keyval_name(event.keyval) + + self.events.trigger('KeyPress', key=key, string=event.string, window=self.win) + + if key == "^Return": + self.entered_text(True) + + up = gtk.gdk.keyval_from_name("Up") + down = gtk.gdk.keyval_from_name("Down") + tab = gtk.gdk.keyval_from_name("Tab") + + return event.keyval in (up, down, tab) + + def __init__(self, window, core): + gtk.Entry.__init__(self) + self.events = core.events + self.core = core + self.win = window + + # we don't want key events to propogate so we stop them in connect_after + self.connect('key-press-event', TextInput.keypress) + self.connect_after('key-press-event', lambda *a: True) + + self.connect('activate', TextInput.entered_text, False) + +gobject.type_register(TextInput) + +def prop_to_gtk(textview, (prop, val)): + if val == parse_mirc.BOLD: + val = pango.WEIGHT_BOLD + + elif val == parse_mirc.UNDERLINE: + val = pango.UNDERLINE_SINGLE + + return {prop: val} + +def word_from_pos(text, pos): + if text[pos] == ' ': + return ' ', pos, pos+1 + + else: + fr = text[:pos].split(" ")[-1] + to = text[pos:].split(" ")[0] + + return fr + to, pos - len(fr), pos + len(to) + +def get_iter_at_coords(view, x, y): + return view.get_iter_at_location( + *view.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(x), int(y)) + ) + +def get_event_at_iter(view, iter, core): + buffer = view.get_buffer() + + line_strt = buffer.get_iter_at_line(iter.get_line()) + line_end = line_strt.copy() + line_end.forward_lines(1) + + pos = iter.get_line_offset() + + #Caveat: text must be a unicode string, not utf-8 encoded; otherwise our + # offsets will be off when we use anything outside 7-bit ascii + #gtk.TextIter.get_text returns unicode but gtk.TextBuffer.get_text does not + text = line_strt.get_text(line_end).rstrip("\n") + + word, fr, to = word_from_pos(text, pos) + + return core.events.data( + window=view.win, pos=pos, text=text, + target=word, target_fr=fr, target_to=to, + ) + +class TextOutput(gtk.TextView): + def copy(self): + startend = self.get_buffer().get_selection_bounds() + + tagsandtext = [] + if startend: + start, end = startend + + while not start.equal(end): + tags_at_iter = {} + for tag in start.get_tags(): + try: + tagname, tagval = eval(tag.get_property('name')) + tags_at_iter[tagname] = tagval + except NameError: + continue + + tagsandtext.append((dict(tags_at_iter), start.get_char())) + start.forward_char() + + text = parse_mirc.unparse_mirc(tagsandtext) + + gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD).set_text(text) + gtk.clipboard_get(gtk.gdk.SELECTION_PRIMARY).set_text(text) + + return text + + def clear(self): + self.get_buffer().set_text('') + + def get_y(self): + rect = self.get_visible_rect() + return rect.y + + def set_y(self,y): + iter = self.get_iter_at_location(0, y) + if self.get_iter_location(iter).y < y: + self.forward_display_line(iter) + yalign = float(self.get_iter_location(iter).y-y)/self.height + self.scroll_to_iter(iter, 0, True, 0, yalign) + + self.check_autoscroll() + + def get_ymax(self): + buffer = self.get_buffer() + return sum(self.get_line_yrange(buffer.get_end_iter())) - self.height + + def get_height(self): + return self.get_visible_rect().height + + y = property(get_y, set_y) + ymax = property(get_ymax) + height = property(get_height) + + # the unknowing print weird things to our text widget function + def write(self, text, line_ending='\n', fg=None): + if not isinstance(text, unicode): + try: + text = codecs.utf_8_decode(text)[0] + except: + text = codecs.latin_1_decode(text)[0] + tags, text = parse_mirc.parse_mirc(text) + + if fg: + tags.append({'data': ("foreground", isinstance(fg, basestring) and ('#%s'%fg) or parse_mirc.get_mirc_color(fg)), 'from': 0, 'to': len(text)}) + + buffer = self.get_buffer() + + cc = buffer.get_char_count() + + buffer.insert_with_tags_by_name( + buffer.get_end_iter(), + text + line_ending, + 'indent' + ) + + for tag in tags: + tag_name = str(tag['data']) + + if not tag_table.lookup(tag_name): + buffer.create_tag(tag_name, **prop_to_gtk(self, tag['data'])) + + buffer.apply_tag_by_name( + tag_name, + buffer.get_iter_at_offset(tag['from'] + cc), + buffer.get_iter_at_offset(tag['to'] + cc) + ) + + def popup(self, menu): + hover_iter = get_iter_at_coords(self, *self.hover_coords) + + menuitems = [] + if not hover_iter.ends_line(): + c_data = get_event_at_iter(self, hover_iter) + c_data.menu = [] + + self.events.trigger("RightClick", c_data) + + menuitems = c_data.menu + + if not menuitems: + c_data = self.events.data(menu=[]) + self.events.trigger("MainMenu", c_data) + + menuitems = c_data.menu + + for child in menu.get_children(): + menu.remove(child) + + for item in menu_from_list(menuitems): + menu.append(item) + + menu.show_all() + + def mousedown(self, event): + if event.button == 3: + self.hover_coords = event.get_coords() + + def mouseup(self, event): + if not self.get_buffer().get_selection_bounds(): + if event.button == 1: + hover_iter = get_iter_at_coords(self, event.x, event.y) + + if not hover_iter.ends_line(): + c_data = get_event_at_iter(self, hover_iter, self.core) + + self.events.trigger("Click", c_data) + + if self.is_focus(): + self.win.focus() + + def clear_hover(self, _event=None): + buffer = self.get_buffer() + + for fr, to in self.linking: + buffer.remove_tag_by_name( + "link", + buffer.get_iter_at_mark(fr), + buffer.get_iter_at_mark(to) + ) + + self.linking = set() + self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(None) + + def hover(self, event): + if self.linking: + self.clear_hover() + + hover_iter = get_iter_at_coords(self, event.x, event.y) + + if not hover_iter.ends_line(): + h_data = get_event_at_iter(self, hover_iter, self.core) + h_data.tolink = set() + + self.events.trigger("Hover", h_data) + + if h_data.tolink: + buffer = self.get_buffer() + + offset = buffer.get_iter_at_line( + hover_iter.get_line() + ).get_offset() + + for fr, to in h_data.tolink: + fr = buffer.get_iter_at_offset(offset + fr) + to = buffer.get_iter_at_offset(offset + to) + + buffer.apply_tag_by_name("link", fr, to) + + self.linking.add( + (buffer.create_mark(None, fr), + buffer.create_mark(None, to)) + ) + + self.get_window( + gtk.TEXT_WINDOW_TEXT + ).set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) + + self.get_pointer() + + def scroll(self, _allocation=None): + if self.autoscroll: + def do_scroll(): + self.scroller.value = self.scroller.upper - self.scroller.page_size + self._scrolling = False + + if not self._scrolling: + self._scrolling = gobject.idle_add(do_scroll) + + def check_autoscroll(self, *args): + def set_to_scroll(): + self.autoscroll = self.scroller.value + self.scroller.page_size >= self.scroller.upper + + gobject.idle_add(set_to_scroll) + + def __init__(self, core, window, buffer=None): + if not buffer: + buffer = gtk.TextBuffer(tag_table) + + gtk.TextView.__init__(self, buffer) + self.core = core + self.events = core.events + self.win = window + + self.set_size_request(0, -1) + + self.set_wrap_mode(gtk.WRAP_WORD_CHAR) + self.set_editable(False) + self.set_cursor_visible(False) + + self.set_property("left-margin", 3) + self.set_property("right-margin", 3) + + self.linking = set() + + self.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK) + self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK) + + self.connect('populate-popup', TextOutput.popup) + self.connect('motion-notify-event', TextOutput.hover) + self.connect('button-press-event', TextOutput.mousedown) + self.connect('button-release-event', TextOutput.mouseup) + self.connect_after('button-release-event', lambda *a: True) + self.connect('leave-notify-event', TextOutput.clear_hover) + + self.hover_coords = 0, 0 + + self.autoscroll = True + self._scrolling = False + self.scroller = gtk.Adjustment() + + def setup_scroll(self, _adj, vadj): + self.scroller = vadj + + if vadj: + def set_scroll(adj): + self.autoscroll = adj.value + adj.page_size >= adj.upper + + vadj.connect("value-changed", set_scroll) + + self.connect("set-scroll-adjustments", setup_scroll) + self.connect("size-allocate", TextOutput.scroll) + + def set_cursor(widget): + self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(None) + + self.connect("realize", set_cursor) + + style_me(self, "view") + +class WindowLabel(gtk.EventBox): + def update(self): + title = self.win.get_title() + + for escapes in (('&','&'), ('<','<'), ('>','>')): + title = title.replace(*escapes) + + for a_type in (HILIT, TEXT, EVENT): + if a_type in self.win.activity: + title = ACTIVITY_MARKUP[a_type] % title + break + + self.label.set_markup(title) + + def tab_popup(self, event): + if event.button == 3: # right click + c_data = self.events.data(window=self.win, menu=[]) + self.events.trigger("WindowMenu", c_data) + + c_data.menu += [ + None, + ("Close", gtk.STOCK_CLOSE, self.win.close), + ] + + menu = gtk.Menu() + for item in menu_from_list(c_data.menu): + menu.append(item) + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + def __init__(self, window, core): + gtk.EventBox.__init__(self) + self.core = core + self.events = core.events + + self.win = window + self.connect("button-press-event", WindowLabel.tab_popup) + + self.label = gtk.Label() + self.add(self.label) + + self.update() + self.show_all() + +class FindBox(gtk.HBox): + def remove(self, *args): + self.parent.remove(self) + self.win.focus() + + def clicked(self, button, search_down=False): + text = self.textbox.get_text() + + if not text: + return + + buffer = self.win.output.get_buffer() + + if buffer.get_selection_bounds(): + if button == self.down: + _, cursor_iter = buffer.get_selection_bounds() + else: + cursor_iter, _ = buffer.get_selection_bounds() + else: + cursor_iter = buffer.get_end_iter() + + if search_down: + cursor = cursor_iter.forward_search( + text, gtk.TEXT_SEARCH_VISIBLE_ONLY + ) + else: + cursor = cursor_iter.backward_search( + text, gtk.TEXT_SEARCH_VISIBLE_ONLY + ) + + if not cursor: + return + + fr, to = cursor + + if button == self.up: + buffer.place_cursor(fr) + self.win.output.scroll_to_iter(fr, 0) + elif button == self.down: + buffer.place_cursor(to) + self.win.output.scroll_to_iter(to, 0) + + buffer.select_range(*cursor) + + cursor_iter = buffer.get_iter_at_mark(buffer.get_insert()) + + def __init__(self, window): + gtk.HBox.__init__(self) + + self.win = window + + self.up = gtk.Button(stock='gtk-go-up') + self.down = gtk.Button(stock='gtk-go-down') + + self.up.connect('clicked', self.clicked) + self.down.connect('clicked', self.clicked, True) + + self.up.set_property('can_focus', False) + self.down.set_property('can_focus', False) + + self.textbox = gtk.Entry() + + self.textbox.connect('focus-out-event', self.remove) + self.textbox.connect('activate', self.clicked) + + self.pack_start(gtk.Label('Find:'), expand=False) + self.pack_start(self.textbox) + + self.pack_start(self.up, expand=False) + self.pack_start(self.down, expand=False) + + self.show_all() + +#class UrkUITabs(gtk.Window): +class UrkUITabs(object): + def __init__(self, core): + # threading stuff + gtk.gdk.threads_init() + self.core = core + self.events = core.events + self.tabs = gtk.Notebook() + self.tabs.set_property( + "tab-pos", + conf.get("ui-gtk/tab-pos", gtk.POS_BOTTOM) + ) + + self.tabs.set_scrollable(True) + self.tabs.set_property("can-focus", False) + + self.box = gtk.VBox(False) + self.box.pack_end(self.tabs) + + def __iter__(self): + return iter(self.tabs.get_children()) + + def __len__(self): + return self.tabs.get_n_pages() + + def exit(self, *args): + self.events.trigger("Exit") + gtk.main_level() and gtk.main_quit() + + def get_active(self): + return self.tabs.get_nth_page(self.tabs.get_current_page()) + + def set_active(self, window): + self.tabs.set_current_page(self.tabs.page_num(window)) + + def add(self, window): + for pos in reversed(range(self.tabs.get_n_pages())): + if self.tabs.get_nth_page(pos).network == window.network: + break + else: + pos = self.tabs.get_n_pages() - 1 + + self.tabs.insert_page(window, WindowLabel(window, self.core), pos+1) + + def remove(self, window): + self.tabs.remove_page(self.tabs.page_num(window)) + + def update(self, window): + self.tabs.get_tab_label(window).update() + + def show_all(self): + self.box.show_all() |