class ScrollingDetector(GObject.GObject): ''' The scrolling detector sends signals when a scrolled window is scrolled and when a scrolled window stops scrolling. Only one `scroll-start` signal will be emitted until scrolling stops. The `scroll-start` signal is emitted when scrolling begins and The `scroll-end` signal is emitted when scrolling ends Neither of these two signals have any arguments Args: scrolled_window (Gtk.ScrolledWindow): A GTK scrolled window object for which scrolling is to be detected timeout (int): time in milliseconds to establish the interval for which scrolling is detected ''' scroll_start_signal = GObject.Signal('scroll-start') scroll_end_signal = GObject.Signal('scroll-end') def __init__(self, scrolled_window, timeout=100): self._scrolled_window = scrolled_window self._timeout = timeout self.is_scrolling = False self._prev_value = 0 self.connect_scrolled_window() GObject.GObject.__init__(self) def connect_scrolled_window(self): ''' Connects scrolling detector to a scrolled window. Detects scrolling when the vertical scrollbar adjustment value is changed Should be used to link an instance of a scrolling detector to a Scrolled Window, after setting scrolled_window ''' adj = self._scrolled_window.get_vadjustment() adj.connect('value-changed', self._value_changed_cb) def _check_scroll_cb(self, adj): if (adj.props.value == self._prev_value): self.is_scrolling = False self.scroll_end_signal.emit() return False self._prev_value = adj.props.value return True def _value_changed_cb(self, adj): if (self.is_scrolling): return self.is_scrolling = True self.scroll_start_signal.emit() self._prev_value = adj.props.value GLib.timeout_add(self._timeout, self._check_scroll_cb, adj)
class Foo(GObject.GObject): my_acc_signal = GObject.Signal(return_type=GObject.TYPE_INT, flags=GObject.SignalFlags.RUN_LAST, accumulator=my_accumulator, accu_data="accum data") my_other_acc_signal = GObject.Signal( return_type=GObject.TYPE_BOOLEAN, flags=GObject.SignalFlags.RUN_LAST, accumulator=GObject.signal_accumulator_true_handled) my_acc_first_wins = GObject.Signal( return_type=GObject.TYPE_BOOLEAN, flags=GObject.SignalFlags.RUN_LAST, accumulator=GObject.signal_accumulator_first_wins)
class Decorated(GObject.GObject): value = 0 @GObject.Signal def pushed(self): """this will push""" self.value += 1 @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST) def pulled(self): self.value -= 1 stomped = GObject.Signal('stomped', arg_types=(int, ), doc='this will stomp') unnamed = GObject.Signal()
class NormalCommentRow(CommentRow): refresh = GObject.Signal('refresh') def __init__(self, api: RedditAPI, data, depth, toplevel_cv): super().__init__(data, depth) self._api = api self._toplevel_cv = toplevel_cv self._top = None self._sub = None self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add(self._box) self._box.show() def get_sub(self): return self._sub def do_event(self, event): if self._top is not None: return self._top.do_event(event) def recurse(self): self._top = posttopbar.PostTopBar(self._api, self.data, self._toplevel_cv) self._top.hide_toggled.connect(self.__hide_toggled_cb) self._box.add(self._top) self._top.show() self._label = newmarkdown.make_html_widget(self.data['body_html']) self._box.add(self._label) self._label.show() self._sub = None self._revealer = None if self.data.get('replies'): self._revealer = Gtk.Revealer( transition_type=Gtk.RevealerTransitionType.SLIDE_DOWN, reveal_child=True) self._box.add(self._revealer) self._revealer.show() self._sub = _CommentsView(self._api, self.data['replies']['data']['children'], self._toplevel_cv, depth=self.depth + 1) self._revealer.add(self._sub) self._sub.show() else: self._top.expand.hide() def __hide_toggled_cb(self, top, hidden): if self._revealer is not None: self._revealer.props.reveal_child = hidden def do_activated(self): if self._revealer is not None: rc = not self._revealer.props.reveal_child self._revealer.props.reveal_child = rc self._top.expand.props.active = not rc
class ColorPicker(EventIcon): color_changed_signal = GObject.Signal('color-changed', arg_types=([object])) def __init__(self, picker): EventIcon.__init__(self, icon_name='computer-xo', pixel_size=style.LARGE_ICON_SIZE) self._picker = picker self._color = None self.connect('activate', self.__activate_cb, picker) def set_color(self, color): if self._picker == _PREVIOUS_FILL_COLOR: self._color = XoColor(_get_previous_fill_color(color)) elif self._picker == _PREVIOUS_STROKE_COLOR: self._color = XoColor(_get_previous_stroke_color(color)) elif self._picker == _NEXT_FILL_COLOR: self._color = XoColor(_get_next_fill_color(color)) elif self._picker == _NEXT_STROKE_COLOR: self._color = XoColor(_get_next_stroke_color(color)) else: self._color = color self.props.xo_color = self._color color = GObject.Property(type=object, setter=set_color) def __activate_cb(self, button, picker): if picker != _CURRENT_COLOR: self.color_changed_signal.emit(self._color)
class GenderPicker(EventIcon): gender_changed_signal = GObject.Signal('gender-changed', arg_types=([str])) def __init__(self, color, gender): EventIcon.__init__(self, icon_name='%s-6' % (gender), pixel_size=style.XLARGE_ICON_SIZE) self._gender = gender self._color = color self.set_gender() self.connect('button_press_event', self.__pressed_cb) def set_color(self, color, gender): self._color = color self.set_gender(gender) def set_gender(self, gender=''): if gender is not '' and self._gender == gender: self.props.xo_color = self._color else: self.props.xo_color = _NOCOLOR gender = GObject.property(type=object, setter=set_gender) def __pressed_cb(self, button, event): self.gender_changed_signal.emit(self._gender)
class TokenManager(GObject.GObject): ''' This class manages a single account token. ''' value_changed = GObject.Signal('value-changed') user_name = None is_anonymous = False def __init__(self): super().__init__() def refresh(self, done_callback: typing.Callable[[], typing.Any]): ''' Request a refresh of the token, if applicable to this type of account ''' raise NotImplementedError() def wrap_path(self, path: str) -> str: ''' Take a path and put the domain part in front of it ''' raise NotImplementedError() def add_message_headers(self, msg: Soup.Message): ''' Mutate the soup message and add any required headers to signify this token ''' raise NotImplementedError() def serialize(self) -> dict: raise NotImplementedError()
class LoadMoreCommentsRow(CommentRow): got_more_comments = GObject.Signal('got-more-comments', arg_types=[object]) def __init__(self, api: RedditAPI, data, depth, toplevel_cv): super().__init__(data, depth) self._api = api self._toplevel_cv = toplevel_cv self._more_button = None def recurse(self): self._more_button = Gtk.Button.new_with_label( 'Show {} more comments...'.format(self.data['count'])) self._more_button.connect('clicked', self.__load_more_cb) self._more_button.get_style_context().add_class('load-more') self.add(self._more_button) self._more_button.show() def do_focus_in_event(self, event): if self._more_button is not None: self._more_button.grab_focus() def __load_more_cb(self, button): button.props.sensitive = False self._api.load_more(self._toplevel_cv.get_link_name(), self.data, self.__loaded_more_cb) def __loaded_more_cb(self, comments): self.got_more_comments.emit(comments)
class Observable(GObject.GObject): changed_signal = GObject.Signal('changed') def serialize(self) -> PopoType: raise NotImplimentedError() def deserialize(self, value: PopoType): raise NotImplimentedError()
class SubmitWindow(GObject.GObject): done = GObject.Signal('done', arg_types=[str, str]) def __init__(self, api: RedditAPI, sub=''): GObject.GObject.__init__(self) self._api = api self._b = Gtk.Builder.new_from_resource( '/today/sam/reddit-is-gtk/submit-window.ui') self.window = self._b.get_object('submit-window') self._b.get_object('sub-entry').props.text = sub self._b.get_object('submit-button').connect( 'clicked', self.__submit_clicked_cb) def show(self): self.window.show() def __submit_clicked_cb(self, button): submit = self._b.get_object('submit-button') submit.props.label = 'Submitting...' submit.props.sensitive = False data = {'title': self._b.get_object('title-entry').props.text, 'sr': self._b.get_object('sub-entry').props.text} stack = self._b.get_object('link-self-stack') if stack.props.visible_child_name == 'link': data['kind'] = 'link' data['url'] = self._b.get_object('link-entry').props.text else: data['kind'] = 'self' buf = self._b.get_object('self-textview').props.buffer data['text'] = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False) self._api.submit(data, self.__submit_done_cb) def __submit_done_cb(self, data): data = data['json'] if data.get('errors'): errors = data['errors'] error_name, error_text, error_name_lower = errors[0] error = self._b.get_object('error-label') error.props.label = error_text error.show() submit = self._b.get_object('submit-button') submit.props.sensitive = True submit.props.label = 'Submit' else: uri = data['data']['url'] self.done.emit(self._b.get_object('sub-entry').props.text, uri) self.window.hide() self.window.destroy()
class AgePicker(Gtk.Grid): age_changed_signal = GObject.Signal('age-changed', arg_types=([int])) def __init__(self, color, gender, age): Gtk.Grid.__init__(self) self._color = color self._gender = gender self._age = age if self._gender is '': # Used for graphic only; does not set user's gender preference. self._gender = 'female' self._icon = EventIcon(icon_name='%s-%d' % (self._gender, self._age), pixel_size=style.LARGE_ICON_SIZE) self._icon.connect('button-press-event', self.__pressed_cb) self.attach(self._icon, 0, 0, 1, 1) self._icon.show() label = Gtk.Label() label.set_text(AGE_LABELS[self._age]) self.attach(label, 0, 1, 1, 1) label.show() self.set_age() def set_color(self, color, age): self._color = color self.set_age(age) def set_age(self, age=None): if age in AGES: age_index = AGES.index(age) else: age_index = None if age_index == self._age: self._icon.props.xo_color = self._color else: self._icon.props.xo_color = _NOCOLOR self._icon.show() age = GObject.property(type=object, setter=set_age) def set_gender(self, gender): self._icon.set_icon_name('%s-%d' % (gender, self._age)) self._icon.show() gender = GObject.property(type=object, setter=set_gender) def __pressed_cb(self, button, event): self.age_changed_signal.emit(self._age)
class GenderPicker(Gtk.Grid): gender_changed_signal = GObject.Signal('gender-changed', arg_types=([str])) def __init__(self): Gtk.Grid.__init__(self) self.set_row_spacing(style.DEFAULT_SPACING) self.set_column_spacing(style.DEFAULT_SPACING) self._gender = load_gender() self._buttons = [] self._nocolor = XoColor('#010101,#ffffff') self._color = XoColor() for i, gender in enumerate(GENDERS): self._buttons.append( EventIcon(pixel_size=style.XLARGE_ICON_SIZE, icon_name='%s-6' % (gender))) self._buttons[-1].connect('button-press-event', self._button_press_cb, i) self.attach(self._buttons[-1], i * 2, 0, 1, 1) self._buttons[-1].show() self.reset_button = EventIcon(pixel_size=style.SMALL_ICON_SIZE, icon_name='entry-cancel') self.reset_button.connect('button-press-event', self._reset_button_press_cb) self.attach(self.reset_button, 1, 0, 1, 1) self.reset_button.xo_color = XoColor('#010101,#a0a0a0') self.reset_button.show() def _reset_button_press_cb(self, widget, event): self._set_gender('') for i in range(len(GENDERS)): self._buttons[i].xo_color = self._nocolor def _button_press_cb(self, widget, event, gender_index): if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: self._set_gender(GENDERS[gender_index]) self._buttons[gender_index].xo_color = self._color self._buttons[1 - gender_index].xo_color = self._nocolor def get_gender(self): return self._gender def _set_gender(self, gender): self.gender_changed_signal.emit(gender) self._gender = gender def update_color(self, color): self._color = color if self._gender in GENDERS: self._buttons[GENDERS.index(self._gender)].xo_color = self._color
class Server(GObject.GObject): session_started = GObject.Signal("session-started", arg_types=(object, )) def _incoming_connection_cb(self, service, connection, user_data): session = Session(connection) self.session_started.emit(session) session.read_data() def start(self): service = Gio.SocketService() service.connect("incoming", self._incoming_connection_cb) return service.add_any_inet_port(None)
class ScrollingDetector(GObject.GObject): """ ScollingDetector emit signals when a ScrolledWindow starts and finish scrolling. Other widets can use that information to avoid do performance expensive operations. """ scroll_start_signal = GObject.Signal('scroll-start') scroll_end_signal = GObject.Signal('scroll-end') def __init__(self, scrolled_window, timeout=100): self._scrolled_window = scrolled_window self._timeout = timeout self.is_scrolling = False self._prev_value = 0 self.connect_scrolled_window() GObject.GObject.__init__(self) def connect_scrolled_window(self): adj = self._scrolled_window.get_vadjustment() adj.connect('value-changed', self._value_changed_cb) def _check_scroll_cb(self, adj): if (adj.props.value == self._prev_value): self.is_scrolling = False self.scroll_end_signal.emit() return False self._prev_value = adj.props.value return True def _value_changed_cb(self, adj): if (self.is_scrolling): return self.is_scrolling = True self.scroll_start_signal.emit() self._prev_value = adj.props.value GLib.timeout_add(self._timeout, self._check_scroll_cb, adj)
class _ReplyPopoverContents(Gtk.Box): posted = GObject.Signal('posted', arg_types=[str]) def __init__(self, api: RedditAPI, data, header_bar=None, **kwargs): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) self.data = data self._api = api sw = Gtk.ScrolledWindow() sw.set_size_request(500, 300) self.add(sw) self._textview = Gtk.TextView() self._textview.props.wrap_mode = Gtk.WrapMode.WORD self._textview.set_size_request(500, 300) self._textview.connect('event', self.__event_cb) sw.add(self._textview) self._done = Gtk.Button(label='Post Reply') self._done.connect('clicked', self.__done_clicked_cb) if header_bar is not None: header_bar.pack_end(self._done) self._done.get_style_context().add_class('suggested-action') self._done.show() else: self.add(self._done) self.show_all() def __event_cb(self, textview, event): shortcuts = {'<Ctrl>Return': (self.__done_clicked_cb, [None])} return process_shortcuts(shortcuts, event) def __done_clicked_cb(self, button): self._done.props.label = 'Sending...' self._done.props.sensitive = False b = self._textview.props.buffer text = b.get_text(b.get_start_iter(), b.get_end_iter(), False) self._api.reply(self.data['name'], text, self.__reply_done_cb) def __reply_done_cb(self, data): new_id = data['json']['data']['things'][0]['data']['id'] self.posted.emit(new_id) parent = self.get_parent() if not isinstance(parent, Gtk.Popover): parent = self.get_toplevel() assert isinstance(parent, Gtk.Dialog) parent.hide() parent.destroy() self.destroy()
class BaseComponent(GObject.GObject): updated_signal = GObject.Signal('updated') def __init__(self, **props): super().__init__() def update(self, updated_list=[]): pass def destroy(self): pass def get_widgets(self): return []
class TabAdd(Gtk.HBox): __gtype_name__ = 'BrowseTabAdd' tab_added = GObject.Signal('tab-added', arg_types=[str]) def __init__(self): GObject.GObject.__init__(self) add_tab_icon = Icon(icon_name='add') button = Gtk.Button() button.drag_dest_set(0, [], 0) button.props.relief = Gtk.ReliefStyle.NONE button.props.focus_on_click = False icon_box = Gtk.HBox() icon_box.pack_start(add_tab_icon, True, False, 0) button.add(icon_box) button.connect('clicked', self.__button_clicked_cb) button.connect('drag-data-received', self.__drag_cb) button.connect('drag-motion', self.__drag_motion_cb) button.connect('drag-drop', self.__drag_drop_cb) button.set_name('browse-tab-add') self.pack_start(button, True, True, 0) add_tab_icon.show() icon_box.show() button.show() def __drag_motion_cb(self, widget, context, x, y, time): Gdk.drag_status(context, Gdk.DragAction.MOVE, time) return True def __drag_drop_cb(self, widget, context, x, y, time): context_targets = context.list_targets() for target in context_targets: if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): widget.drag_get_data(context, target, time) return True def __drag_cb(self, widget, drag_context, x, y, data, info, time): uris = data.get_uris() for uri in uris: self.tab_added.emit(uri) def __button_clicked_cb(self, button): self.tab_added.emit(None)
class AddNewBar(Gtk.Box): activate = GObject.Signal('activate', arg_types=[str]) def __init__(self, placeholder=None): Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) self._button = EventIcon(icon_name='list-add') self._button.connect('button-release-event', self.__button_release_event_cb) self._button.fill_color = style.COLOR_TOOLBAR_GREY.get_svg() self._button.set_tooltip(_('Add New')) self.pack_start(self._button, False, True, 0) self._button.show() self._entry = iconentry.IconEntry() self._entry.connect('key-press-event', self.__key_press_cb) if placeholder is None: placeholder = _('Add new entry') self._entry.set_placeholder_text(placeholder) self._entry.add_clear_button() self.pack_start(self._entry, True, True, 0) self._entry.show() def get_entry(self): return self._entry def get_button(self): return self._button def __key_press_cb(self, window, event): if event.keyval == Gdk.KEY_Return: return self._maybe_activate() def __button_release_event_cb(self, button, event): self._maybe_activate() def _maybe_activate(self): if self._entry.props.text: self.activate.emit(self._entry.props.text) self._entry.props.text = '' return True
class MoreItemRow(Gtk.ListBoxRow): load_more = GObject.Signal('load-more', arg_types=[str]) def __init__(self, after): Gtk.ListBoxRow.__init__(self) self._after = after if after is not None: b = Gtk.Button(label='Load More') else: b = Gtk.Button(label='End of Listing') b.props.sensitive = False b.connect('clicked', self.__clicked_cb) self.add(b) b.show() def __clicked_cb(self, button): self.hide() self.load_more.emit(self._after) self.destroy()
class EmptyView(Gtk.Bin): action = GObject.Signal('action') def __init__(self, title, action=None): Gtk.Bin.__init__(self) builder = Gtk.Builder.new_from_resource( '/today/sam/reddit-is-gtk/empty-view.ui') self._g = builder.get_object self.add(self._g('box')) self.get_child().show() self._g('title').props.label = title if action is not None: self._g('action').props.label = action self._g('action').props.visible = True self._g('action').connect('clicked', self.__action_clicked_cb) def __action_clicked_cb(self, button): self.action.emit()
class LoadFullCommentsRow(Gtk.ListBoxRow): load_full = GObject.Signal('load_full') def __init__(self): Gtk.ListBoxRow.__init__(self) self._ib = Gtk.InfoBar(message_type=Gtk.MessageType.QUESTION) self._ib.connect('response', self.__response_cb) self.add(self._ib) self._ib.show() label = Gtk.Label(label='Showing only a subset of comments') self._ib.get_content_area().add(label) label.show() self._button = self._ib.add_button('Show All Comments', 1) def grab_focus(self): self._button.grab_focus() def __response_cb(self, ib, response): self.load_full.emit() self._button.props.label = 'Loading...'
class MoreItemsRow(Gtk.ListBoxRow): load_more = GObject.Signal('load-more', arg_types=[str]) def __init__(self, after): Gtk.ListBoxRow.__init__(self) self.is_loading_state = False self._after = after if after is not None: self._btn = Gtk.Button(label='Load More') else: self._btn = Gtk.Button( label='End of Listing', sensitive=False, ) self._btn.connect('clicked', self.__clicked_cb) self.add(self._btn) self._btn.show() def __clicked_cb(self, button): self.activate() def activate(self): self.load_more.emit(self._after) def show_loading_state(self): self.is_loading_state = True self._btn.remove(self._btn.get_child()) spinner = Gtk.Spinner() spinner.start() self._btn.add(spinner) spinner.show() self._btn.props.sensitive = False
class Session(GObject.GObject): message_received = GObject.Signal("message-received", arg_types=(object, )) def __init__(self, connection): GObject.GObject.__init__(self) self._connection = connection self._request = StringIO() self._message = MessageBuffer() self._parse_g = None self._ready = False self._send_queue = deque() self._sending = False def _response_write_cb(self, stream, result, user_data): stream.write_bytes_finish(result) self._ready = True def _do_handshake(self): self._request.seek(0) response = protocol.make_handshake(self._request) stream = self._connection.get_output_stream() stream.write_bytes_async(GLib.Bytes.new(response.encode("utf-8")), GLib.PRIORITY_DEFAULT, None, self._response_write_cb, None) def read_data(self): stream = self._connection.get_input_stream() stream.read_bytes_async(8192, GLib.PRIORITY_DEFAULT, None, self._read_data_cb, None) def _read_data_cb(self, stream, result, user_data): data = stream.read_bytes_finish(result).get_data() logger.debug("Got data, length %d" % len(data)) if not data: return if self._ready: self._message.append(data) while self._message.available > 0: if self._parse_g is None: self._parse_g = protocol.parse_message(self._message) parsed_message = self._parse_g.next() if parsed_message: self._parse_g = None received = None if parsed_message.tp == protocol.OPCODE_TEXT: received = Message(Message.TYPE_TEXT, parsed_message.data) logger.debug("Received text message %s" % received.data) elif parsed_message.tp == protocol.OPCODE_BINARY: received = Message(Message.TYPE_BINARY, parsed_message.data) logger.debug("Received binary message, length %s" % len(received.data)) if received: self.message_received.emit(received) else: break else: self._request.write(data) if data.endswith("\r\n\r\n"): self._do_handshake() self.read_data() def _message_write_cb(self, stream, result, callback): written = stream.write_bytes_finish(result) if callback: callback(written) self._sending = False self._send_from_queue() def send_message(self, message, callback=None, binary=False): if binary: logger.debug("Sending binary message, length %s" % len(message)) else: logger.debug("Sending text message %s" % message) protocol_message = protocol.make_message(message, binary) self._send_queue.append((protocol_message, callback)) self._send_from_queue() def _send_from_queue(self): if self._sending: return if not self._send_queue: return stream = self._connection.get_output_stream() message, callback = self._send_queue.popleft() stream.write_bytes_async(GLib.Bytes.new(message), GLib.PRIORITY_DEFAULT, None, self._message_write_cb, callback) self._sending = True
class _IntroBox(Gtk.VBox): done_signal = GObject.Signal('done', arg_types=([object])) PAGE_NAME = 0 PAGE_COLOR = 1 PAGE_GENDER = 2 PAGE_AGE = 3 PAGE_FIRST = min(PAGE_NAME, PAGE_COLOR, PAGE_GENDER, PAGE_AGE) PAGE_LAST = max(PAGE_NAME, PAGE_COLOR, PAGE_GENDER, PAGE_AGE) def __init__(self, start_on_age_page): Gtk.VBox.__init__(self) self.set_border_width(style.zoom(30)) self._page = self.PAGE_NAME self._name_page = _NamePage(self) self._color_page = _ColorPage() self._gender_page = _GenderPage() self._age_page = _AgePage(None) self._current_page = None self._next_button = None settings = Gio.Settings('org.sugarlabs.user') default_nick = settings.get_string('default-nick') if default_nick != 'disabled': self._page = self.PAGE_COLOR if default_nick == 'system': pwd_entry = pwd.getpwuid(os.getuid()) default_nick = (pwd_entry.pw_gecos.split(',')[0] or pwd_entry.pw_name) self._name_page.set_name(default_nick) # XXX should also consider whether or not there is a nick nick = settings.get_string('nick') if start_on_age_page and nick: self._page = self.PAGE_AGE self._setup_page() def _setup_page(self): for child in self.get_children(): self.remove(child) def _setup_name_page(self): self._current_page = self._name_page def _setup_color_page(self): self._current_page = self._color_page def _setup_gender_page(self): if self._color_page.get_color() is not None: self._gender_page.update_color(self._color_page.get_color()) self._current_page = self._gender_page def _setup_age_page(self): if self._gender_page.get_gender() is not None: self._age_page.update_gender(self._gender_page.get_gender()) if self._color_page.get_color() is not None: self._age_page.update_color(self._color_page.get_color()) self._current_page = self._age_page setup_methods = { self.PAGE_NAME: _setup_name_page, self.PAGE_COLOR: _setup_color_page, self.PAGE_GENDER: _setup_gender_page, self.PAGE_AGE: _setup_age_page } setup_methods[self._page](self) self.pack_start(self._current_page, True, True, 0) self._current_page.show() button_box = Gtk.HButtonBox() if self._page == self.PAGE_FIRST: button_box.set_layout(Gtk.ButtonBoxStyle.END) else: button_box.set_layout(Gtk.ButtonBoxStyle.EDGE) back_button = Gtk.Button(_('Back')) image = Icon(icon_name='go-left') back_button.set_image(image) back_button.connect('clicked', self._back_activated_cb) button_box.pack_start(back_button, True, True, 0) back_button.show() self._next_button = Gtk.Button() image = Icon(icon_name='go-right') self._next_button.set_image(image) if self._page == self.PAGE_LAST: self._next_button.set_label(_('Done')) self._next_button.connect('clicked', self._done_activated_cb) else: self._next_button.set_label(_('Next')) self._next_button.connect('clicked', self._next_activated_cb) self._current_page.activate() self._update_next_button() button_box.pack_start(self._next_button, True, True, 0) self._next_button.show() self._current_page.connect('notify::valid', self._page_valid_changed_cb) self.pack_start(button_box, False, True, 0) button_box.show() def _update_next_button(self): self._next_button.set_sensitive(self._current_page.props.valid) def _page_valid_changed_cb(self, page, pspec): self._update_next_button() def _back_activated_cb(self, widget): self.back() def back(self): if self._page != self.PAGE_FIRST: self._page -= 1 self._setup_page() def _next_activated_cb(self, widget): self.next() def next(self): if self._current_page.props.valid: if self._page == self.PAGE_LAST: self.done() else: self._page += 1 self._setup_page() def _done_activated_cb(self, widget): self.done() def done(self): user_profile = UserProfile() user_profile.nickname = self._name_page.get_name() user_profile.colors = self._color_page.get_color() user_profile.gender = self._gender_page.get_gender() user_profile.age = self._age_page.get_age() self.done_signal.emit(user_profile)
class IncomingFileTransfer(_BaseFileTransfer): ''' An incoming file transfer from another buddy. You need to first accept the transfer (either to memory or to a file). Then you need to listen to the state and wait until the transfer is completed. Then you can read the file that it was saved to, or access the :class:`Gio.MemoryOutputStream` from the `output` property. The `output` property is different depending on how the file was accepted. If the file was accepted to a file on the file system, it is a string representing the path to the file. If the file was accepted to memory, it is a :class:`Gio.MemoryOutputStream`. ''' ready = GObject.Signal('ready', arg_types=[object]) def __init__(self, connection, object_path, props): _BaseFileTransfer.__init__(self) channel = {} proxy = dbus.Bus().get_object(connection.bus_name, object_path) channel[PROPERTIES_IFACE] = dbus.Interface(proxy, PROPERTIES_IFACE) channel[CHANNEL] = dbus.Interface(proxy, CHANNEL) channel[CHANNEL_TYPE_FILE_TRANSFER] = dbus.Interface( proxy, CHANNEL_TYPE_FILE_TRANSFER) self.set_channel(channel) self.connect('notify::state', self.__notify_state_cb) self._destination_path = None self._output_stream = None self._socket_address = None self._socket = None self._splicer = None def accept_to_file(self, destination_path): ''' Accept the file transfer and write it to a new file. The file must already exist. Args: destination_path (str): the path where a new file will be created and saved to ''' if os.path.exists(destination_path): raise ValueError('Destination path already exists: %r' % destination_path) self._destination_path = destination_path self._accept() def accept_to_memory(self): ''' Accept the file transfer. Once the state is FT_STATE_OPEN, a :class:`Gio.MemoryOutputStream` accessible via the output prop. ''' self._destination_path = None self._accept() def _accept(self): channel_ft = self.channel[CHANNEL_TYPE_FILE_TRANSFER] self._socket_address = channel_ft.AcceptFile( SOCKET_ADDRESS_TYPE_UNIX, SOCKET_ACCESS_CONTROL_LOCALHOST, '', 0, byte_arrays=True) def __notify_state_cb(self, file_transfer, pspec): _logger.debug('__notify_state_cb %r', self.props.state) if self.props.state == FT_STATE_OPEN: # Need to hold a reference to the socket so that python doesn't # close the fd when it goes out of scope self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._socket.connect(self._socket_address) input_stream = Gio.UnixInputStream.new(self._socket.fileno(), True) if self._destination_path is not None: destination_file = Gio.File.new_for_path( self._destination_path) if self.initial_offset == 0: self._output_stream = destination_file.create( Gio.FileCreateFlags.PRIVATE, None) else: self._output_stream = destination_file.append_to() else: if hasattr(Gio.MemoryOutputStream, 'new_resizable'): self._output_stream = \ Gio.MemoryOutputStream.new_resizable() else: self._output_stream = Gio.MemoryOutputStream() self._output_stream.splice_async( input_stream, Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | Gio.OutputStreamSpliceFlags.CLOSE_TARGET, GLib.PRIORITY_LOW, None, self.__splice_done_cb, None) def __splice_done_cb(self, output_stream, res, user): _logger.debug('__splice_done_cb') self.ready.emit(self._destination_path or self._output_stream) @GObject.Property def output(self): return self._destination_path or self._output_stream
class CollabWrapper(GObject.GObject): ''' The wrapper provides a high level abstraction over the collaboration system. The wrapper deals with setting up the channels, encoding and decoding messages, initialization and alerting the caller to the status. An activity instance is initially private, but may be shared. Once shared, an instance will remain shared for as long as the activity runs. On stop, the journal will preserve the instance as shared, and on resume the instance will be shared again. When the caller shares an activity instance, they are the leader, and other buddies may join. The instance is now a shared activity. When the caller joins a shared activity, the leader will call `get_data`, and the caller's `set_data` will be called with the result. The `joined` signal is emitted when the caller joins a shared activity. One or more `buddy_joined` signals will be emitted before this signal. The signal is not emitted to the caller who first shared the activity. There are no arguments. The `buddy_joined` signal is emitted when another buddy joins the shared activity. At least one will be emitted before the `joined` signal. The caller will never be mentioned, but is assumed to be part of the set. The signal passes a :class:`sugar3.presence.buddy.Buddy` as the only argument. The `buddy_left` signal is emitted when another user leaves the shared activity. The signal is not emitted during quit. The signal passes a :class:`sugar3.presence.buddy.Buddy` as the only argument. Any buddy may call `post` to send a message to all buddies. Each buddy will receive a `message` signal. The `message` signal is emitted when a `post` is received from any buddy. The signal has two arguments. The first is a :class:`sugar3.presence.buddy.Buddy`. The second is the message. Any buddy may call `send_file_memory` or `send_file_file` to transfer a file to all buddies. A description is to be given. Each buddy will receive an `incoming_file` signal. The `incoming_file` signal is emitted when a file transfer is received. The signal has two arguments. The first is a :class:`IncomingFileTransfer`. The second is the description. ''' message = GObject.Signal('message', arg_types=[object, object]) joined = GObject.Signal('joined') buddy_joined = GObject.Signal('buddy_joined', arg_types=[object]) buddy_left = GObject.Signal('buddy_left', arg_types=[object]) incoming_file = GObject.Signal('incoming_file', arg_types=[object, object]) def __init__(self, activity): _logger.debug('__init__') GObject.GObject.__init__(self) self.activity = activity self.shared_activity = activity.shared_activity self._leader = False self._init_waiting = False self._text_channel = None self._owner = presenceservice.get_instance().get_owner() def setup(self): ''' Setup must be called so that the activity can join or share if appropriate. .. note:: As soon as setup is called, any signal, `get_data` or `set_data` call may occur. This means that the activity must have set up enough so these functions can work. For example, call setup at the end of the activity `__init__` function. ''' _logger.debug('setup') # Some glue to know if we are launching, joining, or resuming # a shared activity. if self.shared_activity: # We're joining the activity. self.activity.connect("joined", self.__joined_cb) if self.activity.get_shared(): _logger.debug('calling _joined_cb') self.__joined_cb(self) else: _logger.debug('Joining activity...') self._alert(_('Joining activity...'), _('Please wait for the connection...')) else: self._leader = True if not self.activity.metadata or self.activity.metadata.get( 'share-scope', SCOPE_PRIVATE) == \ SCOPE_PRIVATE: # We are creating a new activity instance. _logger.debug('Off-line') else: # We are sharing an old activity instance. _logger.debug('On-line') self._alert(_('Resuming shared activity...'), _('Please wait for the connection...')) self.activity.connect('shared', self.__shared_cb) def _alert(self, title, msg=None): a = NotifyAlert() a.props.title = title a.props.msg = msg self.activity.add_alert(a) a.connect('response', lambda a, r: self.activity.remove_alert(a)) a.show() def __shared_cb(self, sender): ''' Callback for when activity is shared. ''' _logger.debug('__shared_cb') # FIXME: may be called twice, but we should only act once self.shared_activity = self.activity.shared_activity self._setup_text_channel() self._listen_for_channels() def __joined_cb(self, sender): '''Callback for when an activity is joined.''' _logger.debug('__joined_cb') self.shared_activity = self.activity.shared_activity if not self.shared_activity: return self._setup_text_channel() self._listen_for_channels() self._init_waiting = True self.post({'action': ACTION_INIT_REQUEST}) for buddy in self.shared_activity.get_joined_buddies(): self.buddy_joined.emit(buddy) self.joined.emit() def _setup_text_channel(self): ''' Set up a text channel to use for collaboration. ''' _logger.debug('_setup_text_channel') self._text_channel = _TextChannelWrapper( self.shared_activity.telepathy_text_chan, self.shared_activity.telepathy_conn) # Tell the text channel what callback to use for incoming # text messages. self._text_channel.set_received_callback(self.__received_cb) # Tell the text channel what callbacks to use when buddies # come and go. self.shared_activity.connect('buddy-joined', self.__buddy_joined_cb) self.shared_activity.connect('buddy-left', self.__buddy_left_cb) def _listen_for_channels(self): _logger.debug('_listen_for_channels') conn = self.shared_activity.telepathy_conn conn.connect_to_signal('NewChannels', self.__new_channels_cb) def __new_channels_cb(self, channels): _logger.debug('__new_channels_cb') conn = self.shared_activity.telepathy_conn for path, props in channels: if props[CHANNEL + '.Requested']: continue # This channel was requested by me channel_type = props[CHANNEL + '.ChannelType'] if channel_type == CHANNEL_TYPE_FILE_TRANSFER: self._handle_ft_channel(conn, path, props) def _handle_ft_channel(self, conn, path, props): _logger.debug('_handle_ft_channel') ft = IncomingFileTransfer(conn, path, props) if ft.description == ACTION_INIT_RESPONSE: ft.connect('ready', self.__ready_cb) ft.accept_to_memory() else: desc = json.loads(ft.description) self.incoming_file.emit(ft, desc) def __ready_cb(self, ft, stream): _logger.debug('__ready_cb') if self._init_waiting: stream.close(None) # FIXME: The data prop seems to just be the raw pointer gbytes = stream.steal_as_bytes() data = gbytes.get_data() _logger.debug('Got init data from buddy: %r', data) data = json.loads(data) self.activity.set_data(data) self._init_waiting = False def __received_cb(self, buddy, msg): '''Process a message when it is received.''' _logger.debug('__received_cb') action = msg.get('action') if action == ACTION_INIT_REQUEST: if self._leader: data = self.activity.get_data() if data is not None: data = json.dumps(data) OutgoingBlobTransfer(buddy, self.shared_activity.telepathy_conn, data.encode(), self.get_client_name(), ACTION_INIT_RESPONSE, ACTIVITY_FT_MIME) return if buddy: nick = buddy.props.nick else: nick = '???' _logger.debug('Received message from %s: %r', nick, msg) self.message.emit(buddy, msg) def send_file_memory(self, buddy, data, description): ''' Send a one to one file transfer from memory to a buddy. The buddy will get the file transfer and description through the `incoming_transfer` signal. Args: buddy (sugar3.presence.buddy.Buddy), buddy to send to. data (str), the data to send. description (object), a json encodable description for the transfer. This will be given to the `incoming_transfer` signal at the buddy. ''' OutgoingBlobTransfer(buddy, self.shared_activity.telepathy_conn, data, self.get_client_name(), json.dumps(description), ACTIVITY_FT_MIME) def send_file_file(self, buddy, path, description): ''' Send a one to one file transfer from a filesystem path to a given buddy. The buddy will get the file transfer and description through the `incoming_transfer` signal. Args: buddy (sugar3.presence.buddy.Buddy), buddy to send to. path (str), path of the file containing the data to send. description (object), a json encodable description for the transfer. This will be given to the `incoming_transfer` signal at the buddy. ''' OutgoingFileTransfer(buddy, self.shared_activity.telepathy_conn, path, self.get_client_name(), json.dumps(description), ACTIVITY_FT_MIME) def post(self, msg): ''' Send a message to all buddies. If the activity is not shared, no message is sent. Args: msg (object): json encodable object to send, eg. :class:`dict` or :class:`str`. ''' if self._text_channel is not None: self._text_channel.post(msg) def __buddy_joined_cb(self, sender, buddy): '''A buddy joined.''' self.buddy_joined.emit(buddy) def __buddy_left_cb(self, sender, buddy): '''A buddy left.''' self.buddy_left.emit(buddy) def get_client_name(self): ''' Get the name of the activity's telepathy client. Returns: str, telepathy client name ''' return CLIENT + '.' + self.activity.get_bundle_id() @GObject.property def leader(self): ''' Boolean of if this client is the leader in this activity. The way the leader is decided may change, however there should only ever be one leader for an activity. ''' return self._leader @GObject.property def owner(self): ''' Ourselves, :class:`sugar3.presence.buddy.Owner` ''' return self._owner
class CollabWrapper(GObject.GObject): ''' The collaboration wrapper provides a high level abstraction over the collaboration system. The wrapper deals with setting up the channels, encoding and decoding messages, initialization and alerting the user to the status. When a user joins the activity, it will query the leader for the contents. The leader will return the result of the activity's `get_data` function which will be passed to the `set_data` function on the new user's computer. The `message` signal is called when a message is received from a buddy. It has 2 arguments. The first is the buddy, as a :class:`sugar3.presence.buddy.Buddy`. The second is the decoded content of the message, same as that posted by the other instance. The `joined` signal is emitted when the buddy joins a running activity. If the user shares and activity, the joined signal is not emitted. By the time this signal is emitted, the channels will be setup so all messages will flow through. The `buddy_joined` and `buddy_left` signals are emitted when another user joins or leaves the activity. They both a :class:`sugar3.presence.buddy.Buddy` as their only argument. The `incoming_file` signal is emitted when a file transfer is received from a buddy. The first argument is the object representing the transfer, as a :class:`sugar3.presence.filetransfer.IncomingFileTransfer`. The seccond argument is the description, as passed to the `send_file_*` function on the sender's client ''' message = GObject.Signal('message', arg_types=[object, object]) joined = GObject.Signal('joined') buddy_joined = GObject.Signal('buddy_joined', arg_types=[object]) buddy_left = GObject.Signal('buddy_left', arg_types=[object]) incoming_file = GObject.Signal('incoming_file', arg_types=[object, object]) def __init__(self, activity): GObject.GObject.__init__(self) self.activity = activity self.shared_activity = activity.shared_activity self._leader = False self._init_waiting = False self._text_channel = None def setup(self): ''' Setup must be called to so that the activity can join or share if appropriate. .. note:: As soon as setup is called, any signal, `get_data` or `set_data` call must be made. This means that your activity must have set up enough so these functions can work. For example, place this at the end of the activity's `__init__` function. ''' # Some glue to know if we are launching, joining, or resuming # a shared activity. if self.shared_activity: # We're joining the activity. self.activity.connect("joined", self.__joined_cb) if self.activity.get_shared(): _logger.debug('calling _joined_cb') self.__joined_cb(self) else: _logger.debug('Joining activity...') self._alert(_('Joining activity...'), _('Please wait for the connection...')) else: if not self.activity.metadata or self.activity.metadata.get( 'share-scope', SCOPE_PRIVATE) == \ SCOPE_PRIVATE: # We are creating a new activity instance. _logger.debug('Off-line') else: # We are sharing an old activity instance. _logger.debug('On-line') self._alert(_('Resuming shared activity...'), _('Please wait for the connection...')) self.activity.connect('shared', self.__shared_cb) def _alert(self, title, msg=None): a = NotifyAlert() a.props.title = title a.props.msg = msg self.activity.add_alert(a) a.connect('response', lambda a, r: self.activity.remove_alert(a)) a.show() def __shared_cb(self, sender): ''' Callback for when activity is shared. ''' self.shared_activity = self.activity.shared_activity self._setup_text_channel() self._listen_for_channels() self._leader = True _logger.debug('I am sharing...') def __joined_cb(self, sender): '''Callback for when an activity is joined.''' self.shared_activity = self.activity.shared_activity if not self.shared_activity: return self._setup_text_channel() self._listen_for_channels() self._init_waiting = True self.post({'action': ACTION_INIT_REQUEST}) _logger.debug('I joined a shared activity.') self.joined.emit() def _setup_text_channel(self): ''' Set up a text channel to use for collaboration. ''' self._text_channel = _TextChannelWrapper( self.shared_activity.telepathy_text_chan, self.shared_activity.telepathy_conn) # Tell the text channel what callback to use for incoming # text messages. self._text_channel.set_received_callback(self.__received_cb) # Tell the text channel what callbacks to use when buddies # come and go. self.shared_activity.connect('buddy-joined', self.__buddy_joined_cb) self.shared_activity.connect('buddy-left', self.__buddy_left_cb) def _listen_for_channels(self): conn = self.shared_activity.telepathy_conn conn.connect_to_signal('NewChannels', self.__new_channels_cb) def __new_channels_cb(self, channels): conn = self.shared_activity.telepathy_conn for path, props in channels: if props[CHANNEL + '.Requested']: continue # This channel was requested by me channel_type = props[CHANNEL + '.ChannelType'] if channel_type == CHANNEL_TYPE_FILE_TRANSFER: self._handle_ft_channel(conn, path, props) def _handle_ft_channel(self, conn, path, props): ft = IncomingFileTransfer(conn, path, props) if ft.description == ACTION_INIT_RESPONSE: ft.connect('notify::state', self.__notify_ft_state_cb) ft.accept_to_memory() else: desc = json.loads(ft.description) self.incoming_file.emit(ft, desc) def __notify_ft_state_cb(self, ft, pspec): if ft.props.state == FT_STATE_COMPLETED and self._init_waiting: stream = ft.props.output stream.close(None) # FIXME: The data prop seems to just be the raw pointer gbytes = stream.steal_as_bytes() data = gbytes.get_data() logging.debug('Got init data from buddy: %s', data) data = json.loads(data) self.activity.set_data(data) self._init_waiting = False def __received_cb(self, buddy, msg): '''Process a message when it is received.''' action = msg.get('action') if action == ACTION_INIT_REQUEST and self._leader: data = self.activity.get_data() data = json.dumps(data) OutgoingBlobTransfer(buddy, self.shared_activity.telepathy_conn, data, self.get_client_name(), ACTION_INIT_RESPONSE, ACTIVITY_FT_MIME) return if buddy: nick = buddy.props.nick else: nick = '???' _logger.debug('Received message from %s: %r', nick, msg) self.message.emit(buddy, msg) def send_file_memory(self, buddy, data, description): ''' Send a 1-to-1 transfer from memory to a given buddy. They will get the file transfer and description through the `incoming_transfer` signal. Args: buddy (sugar3.presence.buddy.Buddy), buddy to offer the transfer to data (str), the data to offer to the buddy via the transfer description (object), a json encodable description for the transfer. This will be given to the `incoming_transfer` signal of the transfer ''' OutgoingBlobTransfer(buddy, self.shared_activity.telepathy_conn, data, self.get_client_name(), json.dumps(description), ACTIVITY_FT_MIME) def send_file_file(self, buddy, path, description): ''' Send a 1-to-1 transfer from a file to a given buddy. They will get the file transfer and description through the `incoming_transfer` signal. Args: buddy (sugar3.presence.buddy.Buddy), buddy to offer the transfer to path (str), path of the file to send to the buddy description (object), a json encodable description for the transfer. This will be given to the `incoming_transfer` signal of the transfer ''' OutgoingFileTransfer(buddy, self.shared_activity.telepathy_conn, path, self.get_client_name(), json.dumps(description), ACTIVITY_FT_MIME) def post(self, msg): ''' Broadcast a message to the other buddies if the activity is shared. If it is not shared, the message will not be send at all. Args: msg (object): json encodable object to send to the other buddies, eg. :class:`dict` or :class:`str`. ''' if self._text_channel is not None: self._text_channel.post(msg) def __buddy_joined_cb(self, sender, buddy): '''A buddy joined.''' self.buddy_joined.emit(buddy) def __buddy_left_cb(self, sender, buddy): '''A buddy left.''' self.buddy_left.emit(buddy) def get_client_name(self): ''' Get the name of the activity's telepathy client. Returns: str, telepathy client name ''' return CLIENT + '.' + self.activity.get_bundle_id()
class AgePicker(Gtk.Grid): age_changed_signal = GObject.Signal('age-changed', arg_types=([int])) def __init__(self, gender, page=None): Gtk.Grid.__init__(self) self.set_row_spacing(style.DEFAULT_SPACING) self.set_column_spacing(style.DEFAULT_SPACING) self._group_labels = get_group_labels() self._page = page self._gender = gender self._age = self.get_age() self._pickers = [] self._nocolor = XoColor('#010101,#ffffff') self._color = XoColor() if self._gender not in GENDERS: self._gender = 'female' gender_index = GENDERS.index(self._gender) age_index = age_to_index(self._age) width = Gdk.Screen.width() num_ages = len(self._group_labels.AGES) for i in range(num_ages): self._pickers.append( Picker(self._group_labels.ICONS[i][gender_index], _(self._group_labels.LABELS[i]))) self._pickers[i].connect(self._button_activate_cb, i) self._fixed = Gtk.Fixed() fixed_size = width - 4 * style.GRID_CELL_SIZE self._fixed.set_size_request(fixed_size, -1) self.attach(self._fixed, 0, 0, 1, 1) self._fixed.show() self._age_adj = Gtk.Adjustment(value=age_index, lower=0, upper=num_ages - 1, step_incr=1, page_incr=3, page_size=0) self._age_adj.connect('value-changed', self.__age_adj_changed_cb) self._age_slider = Gtk.HScale() self._age_slider.set_draw_value(False) self._age_slider.set_adjustment(self._age_adj) self.attach(self._age_slider, 0, 1, 1, 1) for i in range(num_ages): self._fixed.put(self._pickers[i], 0, 0) self._configure(width) Gdk.Screen.get_default().connect('size-changed', self._configure_cb) def _configure_cb(self, event=None): width = Gdk.Screen.width() self._configure(width) def _configure(self, width): fixed_size = width - 4 * style.GRID_CELL_SIZE self._fixed.set_size_request(fixed_size, -1) num_ages = len(self._group_labels.AGES) dx = int((fixed_size - style.LARGE_ICON_SIZE) / (num_ages - 1)) for i in range(num_ages): self._fixed.move(self._pickers[i], dx * i, 0) if num_ages + 2 < width / style.LARGE_ICON_SIZE: for i in range(num_ages): self._pickers[i].show_all() self._age_slider.hide() else: self._age_slider.show() value = self._age_adj.get_value() self._set_age_picker(int(value + 0.5)) def get_label(self): return self._group_labels.GROUP_LABEL def _set_age_picker(self, age_index): for i in range(len(self._group_labels.AGES)): if i == age_index: self._pickers[i].show_all() else: self._pickers[i].hide_all() self._do_selected(age_index) def __age_adj_changed_cb(self, widget): value = self._age_adj.get_value() self._set_age_picker(int(value + 0.5)) def _do_selected(self, age_index): if self._age is not None: i = age_to_index(self._age) self._pickers[i].set_color(self._nocolor) self._set_age(self._group_labels.AGES[age_index]) self._pickers[age_index].set_color(self._color) def _button_activate_cb(self, widget, age_index): self._do_selected(age_index) def get_age(self): if self._page is None: return load_age() elif hasattr(self, '_age'): if self._age is None: return None i = age_to_index(self._age) return self._group_labels.AGES[i] return None def _set_age(self, age): if self._page is None: if age != self._age: self.age_changed_signal.emit(age) else: self._page.set_valid(True) self._age = age def update_color(self, color): self._color = color if self._age is not None: i = age_to_index(self._age) self._pickers[i].set_color(self._color) def update_gender(self, gender): self._gender = gender if self._gender in GENDERS: gender_index = GENDERS.index(self._gender) else: gender_index = 0 for i in range(len(self._group_labels.AGES)): self._pickers[i].set_icon( self._group_labels.ICONS[i][gender_index])
class AboutMe(SectionView): age_changed_signal = GObject.Signal('age-changed', arg_types=([int])) gender_changed_signal = GObject.Signal('gender-changed', arg_types=([str])) def __init__(self, model, alerts): SectionView.__init__(self) self._model = model self.restart_alerts = alerts if alerts else set() self.props.is_deferrable = False self._nick_sid = 0 self._color_valid = True self._nick_valid = True self._color = None self._gender = '' self._age = None self.set_border_width(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) self._color = XoColor(self._model.get_color()) self._original_nick = self._model.get_nick() self._setup_color() self._setup_nick() self._setup_gender() self._setup_age() self._update_pickers(self._color) self._nick_entry.set_text(self._original_nick) self._color_valid = True self._nick_valid = True self.needs_restart = False self._nick_entry.connect('changed', self.__nick_changed_cb) for picker in self._pickers.values(): picker.connect('color-changed', self.__color_changed_cb) self._gender_pickers.connect('gender-changed', self.__gender_changed_cb) self._age_pickers.connect('age-changed', self.__age_changed_cb) def _setup_nick(self): grid = Gtk.Grid() grid.set_row_spacing(style.DEFAULT_SPACING) grid.set_column_spacing(style.DEFAULT_SPACING) self._nick_entry = Gtk.Entry() self._nick_entry.set_width_chars(25) grid.attach(self._nick_entry, 0, 0, 1, 1) self._nick_entry.show() alert_grid = Gtk.Grid() self._nick_alert = InlineAlert() alert_grid.attach(self._nick_alert, 0, 0, 1, 1) if 'nick' in self.restart_alerts: self._nick_alert.props.msg = self.restart_msg self._nick_alert.show() center_in_panel = Gtk.Alignment.new(0.5, 0, 0, 0) center_in_panel.add(grid) grid.show() center_alert = Gtk.Alignment.new(0.5, 0, 0, 0) center_alert.add(alert_grid) alert_grid.show() self.pack_start(center_in_panel, False, False, 0) self.pack_start(center_alert, False, False, 0) center_in_panel.show() center_alert.show() def _setup_color(self): grid = Gtk.Grid() grid.set_row_spacing(style.DEFAULT_SPACING) grid.set_column_spacing(style.DEFAULT_SPACING) self._color_alert = None self._pickers = { _PREVIOUS_FILL_COLOR: ColorPicker(_PREVIOUS_FILL_COLOR), _NEXT_FILL_COLOR: ColorPicker(_NEXT_FILL_COLOR), _CURRENT_COLOR: ColorPicker(_CURRENT_COLOR), _NEXT_STROKE_COLOR: ColorPicker(_NEXT_STROKE_COLOR), _PREVIOUS_STROKE_COLOR: ColorPicker(_PREVIOUS_STROKE_COLOR), } label_color = Gtk.Label(label=_('Click to change your color:')) label_color.modify_fg(Gtk.StateType.NORMAL, style.COLOR_SELECTION_GREY.get_gdk_color()) grid.attach(label_color, 0, 0, 3, 1) label_color.show() current = 0 for picker_index in sorted(self._pickers.keys()): if picker_index == _CURRENT_COLOR: left_separator = Gtk.SeparatorToolItem() grid.attach(left_separator, current, 1, 1, 1) left_separator.show() current += 1 picker = self._pickers[picker_index] picker.show() grid.attach(picker, current, 1, 1, 1) current += 1 if picker_index == _CURRENT_COLOR: right_separator = Gtk.SeparatorToolItem() right_separator.show() grid.attach(right_separator, current, 1, 1, 1) current += 1 label_color_error = Gtk.Label() grid.attach(label_color_error, 0, 2, 3, 1) label_color_error.show() self._color_alert = InlineAlert() grid.attach(self._color_alert, 0, 3, 3, 1) if 'color' in self.restart_alerts: self._color_alert.props.msg = self.restart_msg self._color_alert.show() center_in_panel = Gtk.Alignment.new(0.5, 0, 0, 0) center_in_panel.add(grid) grid.show() self.pack_start(center_in_panel, False, False, 0) center_in_panel.show() def _setup_gender(self): self._saved_gender = load_gender() self._gender_pickers = GenderPicker() grid = Gtk.Grid() grid.set_row_spacing(style.DEFAULT_SPACING) grid.set_column_spacing(style.DEFAULT_SPACING) label_gender = Gtk.Label(label=_('Select gender:')) label_gender.modify_fg(Gtk.StateType.NORMAL, style.COLOR_SELECTION_GREY.get_gdk_color()) grid.attach(label_gender, 0, 0, 1, 1) label_gender.show() grid.attach(self._gender_pickers, 0, 1, 1, 1) self._gender_pickers.show() center_in_panel = Gtk.Alignment.new(0.5, 0, 0, 0) center_in_panel.add(grid) grid.show() self.pack_start(center_in_panel, False, False, 0) center_in_panel.show() def _setup_age(self): self._saved_age = load_age() grid = Gtk.Grid() grid.set_row_spacing(style.DEFAULT_SPACING) grid.set_column_spacing(style.DEFAULT_SPACING) self._age_pickers = AgePicker(self._saved_gender) center_in_panel = Gtk.Alignment.new(0.5, 0, 0, 0) center_in_panel.add(self._age_pickers) self._age_pickers.show() label = self._age_pickers.get_label() label_age = Gtk.Label(label=_(label)) label_age.modify_fg(Gtk.StateType.NORMAL, style.COLOR_SELECTION_GREY.get_gdk_color()) left_align = Gtk.Alignment.new(0, 0, 0, 0) left_align.add(label_age) label_age.show() grid.attach(left_align, 0, 0, 1, 1) left_align.show() grid.attach(center_in_panel, 0, 1, 1, 1) center_in_panel.show() center_in_panel = Gtk.Alignment.new(0.5, 0, 0, 0) center_in_panel.add(grid) grid.show() self.pack_start(center_in_panel, False, False, 0) center_in_panel.show() def setup(self): pass def undo(self): self._model.undo() self._nick_alert.hide() self._color_alert.hide() # Undo gender or age changes save_gender(self._saved_gender) save_age(self._saved_age) def _update_pickers(self, color): for picker in self._pickers.values(): picker.props.color = color self._gender_pickers.update_color(color) self._age_pickers.update_color(color) def _validate(self): if self._nick_valid and self._color_valid: self.props.is_valid = True else: self.props.is_valid = False def __nick_changed_cb(self, widget, data=None): if self._nick_sid: GLib.source_remove(self._nick_sid) self._nick_sid = GLib.timeout_add(self._APPLY_TIMEOUT, self.__nick_timeout_cb, widget) def __nick_timeout_cb(self, widget): self._nick_sid = 0 if widget.get_text() == self._model.get_nick(): self.restart_alerts.remove('nick') if not self.restart_alerts: self.needs_restart = False self._nick_alert.hide() return False try: self._model.set_nick(widget.get_text()) except ValueError, detail: self._nick_alert.props.msg = detail self._nick_valid = False self._nick_alert.show() else:
class SessionManager(GObject.GObject): shutdown_signal = GObject.Signal('shutdown') MODE_LOGOUT = 0 MODE_SHUTDOWN = 1 MODE_REBOOT = 2 SHUTDOWN_TIMEOUT = 1 MAX_SHUTDOWN_TRIES = 10 def __init__(self): GObject.GObject.__init__(self) address = SugarExt.xsmp_init() os.environ['SESSION_MANAGER'] = address SugarExt.xsmp_run() self.session = SugarExt.Session.create_global() self._shell_model = shell.get_model() self._shutdown_tries = 0 self._logout_mode = None def start(self): self.session.start() self.session.connect('shutdown_completed', self.__shutdown_completed_cb) def initiate_shutdown(self, logout_mode): self._logout_mode = logout_mode self.shutdown_signal.emit() self.session.initiate_shutdown() def cancel_shutdown(self): self.session.cancel_shutdown() self._shutdown_tries = 0 self._logout_mode = None def __shutdown_completed_cb(self, session): if self._logout_mode is not None: if self._try_shutdown(): GObject.timeout_add(self.SHUTDOWN_TIMEOUT, self._try_shutdown) def _try_shutdown(self): if len(self._shell_model) > 0: self._shutdown_tries += 1 if self._shutdown_tries < self.MAX_SHUTDOWN_TRIES: # returning True, the timeout_add_seconds will try # again in the specified seconds return True self.shutdown_completed() return False def logout(self): self.initiate_shutdown(self.MODE_LOGOUT) def shutdown(self): self.initiate_shutdown(self.MODE_SHUTDOWN) def reboot(self): self.initiate_shutdown(self.MODE_REBOOT) def shutdown_completed(self): if self._logout_mode != self.MODE_LOGOUT: bus = dbus.SystemBus() if have_systemd(): try: proxy = bus.get_object('org.freedesktop.login1', '/org/freedesktop/login1') pm = dbus.Interface(proxy, 'org.freedesktop.login1.Manager') if self._logout_mode == self.MODE_SHUTDOWN: pm.PowerOff(False) elif self._logout_mode == self.MODE_REBOOT: pm.Reboot(True) except: logging.exception('Can not stop sugar') self.session.cancel_shutdown() return else: CONSOLEKIT_DBUS_PATH = '/org/freedesktop/ConsoleKit/Manager' try: proxy = bus.get_object('org.freedesktop.ConsoleKit', CONSOLEKIT_DBUS_PATH) pm = dbus.Interface(proxy, 'org.freedesktop.ConsoleKit.Manager') if self._logout_mode == self.MODE_SHUTDOWN: pm.Stop() elif self._logout_mode == self.MODE_REBOOT: pm.Restart() except: logging.exception('Can not stop sugar') self.session.cancel_shutdown() return SugarExt.xsmp_shutdown() Gtk.main_quit()