diff --git a/autogen.sh b/autogen.sh index 6d3283adaa..739e7f8d90 100755 --- a/autogen.sh +++ b/autogen.sh @@ -12,4 +12,4 @@ intltoolize --force autoreconf -i cd "$olddir" -"$srcdir/configure" --enable-maintainer-mode "$@" --disable-python-byte-compile +"$srcdir/configure" --enable-maintainer-mode "$@" diff --git a/bin/sugar-backlight-setup b/bin/sugar-backlight-setup index 4bebccfc0b..a686242b7c 100644 --- a/bin/sugar-backlight-setup +++ b/bin/sugar-backlight-setup @@ -19,6 +19,8 @@ import os import sys from functools import cmp_to_key +import gi +gi.require_version('GUdev', '1.0') from gi.repository import GUdev diff --git a/bin/sugar-control-panel b/bin/sugar-control-panel index 00196d51fd..154bb6a383 100644 --- a/bin/sugar-control-panel +++ b/bin/sugar-control-panel @@ -15,6 +15,9 @@ # along with this program. If not, see . import sys +import gi + +gi.require_version('Gtk', '4.0') from jarabe import config diff --git a/bin/sugar-erase-bundle b/bin/sugar-erase-bundle index 67c5a840a2..4c35c9da28 100755 --- a/bin/sugar-erase-bundle +++ b/bin/sugar-erase-bundle @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import os -import sys +import argparse from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) @@ -8,20 +7,22 @@ DBusGMainLoop(set_as_default=True) from jarabe.model import bundleregistry -def cmd_help(): - print('Usage: sugar-erase-bundle bundle_id [...] \n\n\ - Erase activity bundles\n') +def main(): + parser = argparse.ArgumentParser(description="Erase activity bundles") + parser.add_argument("bundles", nargs="+", metavar="bundle_id", + help="Activity bundles to erase") + args = parser.parse_args() + registry = bundleregistry.get_registry() -if len(sys.argv) < 2: - cmd_help() - exit(2) + for name in args.bundles: + bundle = registry.get_bundle(name) + if not bundle: + print('Cannot find %s bundle.' % name) + continue + + registry.uninstall(bundle, delete_profile=True) -for name in sys.argv[1:]: - registry = bundleregistry.get_registry() - bundle = registry.get_bundle(name) - if not bundle: - print('Cannot find %s bundle.' % name) - continue - registry.uninstall(bundle, delete_profile=True) +if __name__ == '__main__': + main() diff --git a/bin/sugar-install-bundle b/bin/sugar-install-bundle index b1fc2cb6bf..33a836b7c1 100755 --- a/bin/sugar-install-bundle +++ b/bin/sugar-install-bundle @@ -1,6 +1,6 @@ #!/usr/bin/env python3 +import argparse import os -import sys from sugar4.bundle.activitybundle import ActivityBundle @@ -8,21 +8,22 @@ from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) -def cmd_help(): - print('Usage: sugar-install-bundle activitybundle.xo [...] \n\n\ - Install activity bundles\n') +def main(): + parser = argparse.ArgumentParser(description="Install activity bundles") + parser.add_argument("bundles", nargs="+", metavar="activitybundle.xo", + help="Activity bundles to install") + args = parser.parse_args() + for name in args.bundles: + bundle = ActivityBundle(name) + path = bundle.install() -if len(sys.argv) < 2: - cmd_help() - sys.exit(2) + # make sure that the BundleRegistry will add this bundle + # by triggering a GFileMonitor ATTRIBUTE_CHANGED event. + os.utime(path, None) -for name in sys.argv[1:]: - bundle = ActivityBundle(name) - path = bundle.install() + print("%s: '%s' installed." % (parser.prog, name)) - # make sure that the BundleRegistry will add this bundle - # by triggering a GFileMonitor ATTRIBUTE_CHANGED event. - os.utime(path, None) - print("%s: '%s' installed." % (sys.argv[0], name)) +if __name__ == '__main__': + main() diff --git a/bin/sugar-launch b/bin/sugar-launch index e28a0e2afc..3d009a3c6b 100644 --- a/bin/sugar-launch +++ b/bin/sugar-launch @@ -20,6 +20,9 @@ import sys import dbus import argparse +import gi +gi.require_version('Gtk', '4.0') + from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) @@ -72,8 +75,8 @@ def _get_interpreter(exec_file): if not abs_path: return exec_file - f = open(abs_path) - line = f.readline(100) + with open(abs_path) as f: + line = f.readline(100) if line.startswith('#!'): cmds = line[2:].strip().split(' ') cmds.append(abs_path) @@ -92,5 +95,10 @@ if args.debug: cmd_args.extend(_get_interpreter(act_args.pop(0))) cmd_args.extend(act_args) +env = activityfactory.get_environment(activity) +if 'WAYLAND_DISPLAY' in os.environ: + env['WAYLAND_DISPLAY'] = os.environ['WAYLAND_DISPLAY'] + env['GDK_BACKEND'] = 'wayland' + os.chdir(str(activity.get_path())) -os.execvpe(cmd_args[0], cmd_args, activityfactory.get_environment(activity)) +os.execvpe(cmd_args[0], cmd_args, env) diff --git a/bin/sugar.in b/bin/sugar.in index b7e8d60e66..0b951183b7 100644 --- a/bin/sugar.in +++ b/bin/sugar.in @@ -32,12 +32,8 @@ if test -z "$SUGAR_ACTIVITIES_HIDDEN"; then export SUGAR_ACTIVITIES_HIDDEN="$sugardatadir/activities.hidden" fi -export GTK2_RC_FILES="@prefix@/share/sugar/data/sugar-$SUGAR_SCALING.gtkrc" - -if ! test -f "$GTK2_RC_FILES"; then - echo "sugar: ERROR: Gtk theme for scaling $SUGAR_SCALING not available in path $GTK2_RC_FILES" - exit 1 -fi +export GDK_BACKEND="wayland" +export GTK_THEME="Sugar" # Set default language export LANG="${LANG:-en_US.utf8}" diff --git a/configure.ac b/configure.ac index 1c52f0481b..f51bb2a948 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ AC_INIT([Sugar],[0.121],[],[sugar]) -AC_PREREQ([2.59]) +AC_PREREQ([2.72]) AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_SRCDIR([configure.ac]) @@ -25,7 +25,7 @@ AC_SUBST([GETTEXT_PACKAGE]) AM_GLIB_GNU_GETTEXT AC_ARG_ENABLE(update-mimedb, - AC_HELP_STRING([--disable-update-mimedb], + AS_HELP_STRING([--disable-update-mimedb], [disable the update-mime-database after install [default=no]]),, enable_update_mimedb=yes) AM_CONDITIONAL(ENABLE_UPDATE_MIMEDB, test x$enable_update_mimedb = xyes) diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am index 6f0b172867..cad84fd2e8 100644 --- a/data/icons/Makefile.am +++ b/data/icons/Makefile.am @@ -11,6 +11,8 @@ sugar_DATA = \ module-network.svg \ module-power.svg \ module-updater.svg \ - module-webaccount.svg + module-webaccount.svg \ + list-add.svg \ + list-remove.svg EXTRA_DIST = $(sugar_DATA) diff --git a/extensions/cpsection/aboutcomputer/model.py b/extensions/cpsection/aboutcomputer/model.py index 98630ad05d..5065b35770 100644 --- a/extensions/cpsection/aboutcomputer/model.py +++ b/extensions/cpsection/aboutcomputer/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,7 +27,6 @@ from jarabe import config from jarabe.model.network import get_wireless_interfaces - _OFW_TREE = '/ofw' _PROC_TREE = '/proc/device-tree' _DMI_DIRECTORY = '/sys/class/dmi/id' @@ -234,7 +233,7 @@ def _read_file(path): def get_license(): license_file = os.path.join(config.data_path, 'GPLv3') - lang = os.environ['LANG'] + lang = os.environ.get('LANG', 'en_US.UTF-8') if lang.endswith('UTF-8'): lang = lang[:-6] diff --git a/extensions/cpsection/aboutcomputer/view.py b/extensions/cpsection/aboutcomputer/view.py index 1e3f605a76..67b34a2100 100644 --- a/extensions/cpsection/aboutcomputer/view.py +++ b/extensions/cpsection/aboutcomputer/view.py @@ -1,4 +1,4 @@ -# coding=utf-8 +# coding=utf-8 # Copyright (C) 2008, OLPC # Copyright (C) 2009 Simon Schampijer # @@ -15,11 +15,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ -from gi.repository import Gtk, Pango -from gi.repository import Gdk +from gi.repository import Gtk from sugar4.graphics import style @@ -33,19 +31,22 @@ def __init__(self, model, alerts=None): self._model = model - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) - self._group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + self._group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) scrollwindow = Gtk.ScrolledWindow() scrollwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.pack_start(scrollwindow, True, True, 0) - scrollwindow.show() + scrollwindow.set_vexpand(True) + scrollwindow.set_hexpand(True) + self.append(scrollwindow) self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - scrollwindow.add_with_viewport(self._vbox) - self._vbox.show() + scrollwindow.set_child(self._vbox) self._setup_identity() @@ -55,78 +56,70 @@ def __init__(self, model, alerts=None): def create_information_box(self, label_text, value_text): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) label = Gtk.Label(label=label_text) - label.set_alignment(1, 0) - label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box.pack_start(label, False, True, 0) + label.set_halign(Gtk.Align.END) + style.apply_css_to_widget(label, "* { color: %s; }" % style.COLOR_SELECTION_GREY.get_html()) + box.append(label) self._group.add_widget(label) - label.show() value = Gtk.Label(label=value_text) - value.set_alignment(0, 0) - box.pack_start(value, False, True, 0) - value.show() - box.show() + value.set_halign(Gtk.Align.START) + value.set_selectable(True) + box.append(value) return box def _setup_identity(self): separator_identity = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(separator_identity, False, True, 0) - separator_identity.show() + self._vbox.append(separator_identity) label_identity = Gtk.Label(label=_('Identity')) - label_identity.set_alignment(0, 0) - self._vbox.pack_start(label_identity, False, True, 0) - label_identity.show() + label_identity.set_halign(Gtk.Align.START) + self._vbox.append(label_identity) vbox_identity = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox_identity.set_border_width(style.DEFAULT_SPACING * 2) + vbox_identity.set_margin_top(style.DEFAULT_SPACING * 2) + vbox_identity.set_margin_bottom(style.DEFAULT_SPACING * 2) + vbox_identity.set_margin_start(style.DEFAULT_SPACING * 2) + vbox_identity.set_margin_end(style.DEFAULT_SPACING * 2) vbox_identity.set_spacing(style.DEFAULT_SPACING) hardware_model = self._model.get_hardware_model() if hardware_model: - vbox_identity.pack_start( - self.create_information_box(_('Model:'), hardware_model), - False, True, 0) + vbox_identity.append( + self.create_information_box(_('Model:'), hardware_model)) - vbox_identity.pack_start( + vbox_identity.append( self.create_information_box(_('Serial Number:'), - self._model.get_serial_number()), - False, True, 0) + self._model.get_serial_number())) - self._vbox.pack_start(vbox_identity, False, True, 0) - vbox_identity.show() + self._vbox.append(vbox_identity) def _setup_software(self): separator_software = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(separator_software, False, True, 0) - separator_software.show() + self._vbox.append(separator_software) label_software = Gtk.Label(label=_('Software')) - label_software.set_alignment(0, 0) - self._vbox.pack_start(label_software, False, True, 0) - label_software.show() + label_software.set_halign(Gtk.Align.START) + self._vbox.append(label_software) box_software = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_software.set_border_width(style.DEFAULT_SPACING * 2) + box_software.set_margin_top(style.DEFAULT_SPACING * 2) + box_software.set_margin_bottom(style.DEFAULT_SPACING * 2) + box_software.set_margin_start(style.DEFAULT_SPACING * 2) + box_software.set_margin_end(style.DEFAULT_SPACING * 2) box_software.set_spacing(style.DEFAULT_SPACING) - box_software.pack_start( + box_software.append( self.create_information_box(_('Build:'), - self._model.get_build_number()), - False, True, 0) + self._model.get_build_number())) - box_software.pack_start( + box_software.append( self.create_information_box(_('Sugar:'), - config.version), - False, True, 0) + config.version)) - box_software.pack_start( + box_software.append( self.create_information_box(_('Firmware:'), - self._model.get_firmware_number()), - False, True, 0) + self._model.get_firmware_number())) - box_software.pack_start( + box_software.append( self.create_information_box(_('Wireless Firmware:'), - self._model.get_wireless_firmware()), - False, True, 0) + self._model.get_wireless_firmware())) days_from_last_update = self._model.days_from_last_update() if days_from_last_update >= 0: @@ -135,35 +128,33 @@ def _setup_software(self): else: msg = _('Today') - box_software.pack_start( - self.create_information_box(_('Last system update:'), msg), - False, True, 0) + box_software.append( + self.create_information_box(_('Last system update:'), msg)) - self._vbox.pack_start(box_software, False, True, 0) - box_software.show() + self._vbox.append(box_software) def _setup_copyright(self): separator_copyright = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(separator_copyright, False, True, 0) - separator_copyright.show() + self._vbox.append(separator_copyright) label_copyright = Gtk.Label(label=_('Copyright and License')) - label_copyright.set_alignment(0, 0) - self._vbox.pack_start(label_copyright, False, True, 0) - label_copyright.show() + label_copyright.set_halign(Gtk.Align.START) + self._vbox.append(label_copyright) vbox_copyright = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox_copyright.set_border_width(style.DEFAULT_SPACING * 2) + vbox_copyright.set_margin_top(style.DEFAULT_SPACING * 2) + vbox_copyright.set_margin_bottom(style.DEFAULT_SPACING * 2) + vbox_copyright.set_margin_start(style.DEFAULT_SPACING * 2) + vbox_copyright.set_margin_end(style.DEFAULT_SPACING * 2) vbox_copyright.set_spacing(style.DEFAULT_SPACING) copyright_text = '© 2006-2020 One Laptop per Child Association Inc,' \ ' Sugar Labs Inc, Red Hat Inc, Collabora Ltd and' \ ' Contributors.' label_copyright = Gtk.Label(label=copyright_text) - label_copyright.set_alignment(0, 0) - label_copyright.set_size_request(Gdk.Screen.width() / 2, -1) - label_copyright.set_line_wrap(True) - label_copyright.show() - vbox_copyright.pack_start(label_copyright, False, True, 0) + label_copyright.set_halign(Gtk.Align.START) + label_copyright.set_wrap(True) + label_copyright.set_max_width_chars(80) + vbox_copyright.append(label_copyright) # TRANS: The word "Sugar" should not be translated. info_text = _('Sugar is the graphical user interface that you are' @@ -172,33 +163,28 @@ def _setup_copyright(self): ' change it and/or distribute copies of it under' ' certain conditions described therein.') label_info = Gtk.Label(label=info_text) - label_info.set_alignment(0, 0) + label_info.set_halign(Gtk.Align.START) label_info.set_max_width_chars(80) - label_info.set_line_wrap(True) - label_info.set_size_request(Gdk.Screen.width() / 2, -1) - label_info.show() - vbox_copyright.pack_start(label_info, False, True, 0) + label_info.set_wrap(True) + vbox_copyright.append(label_info) expander = Gtk.Expander(label=_('Full license:')) expander.connect('notify::expanded', self.license_expander_cb) - expander.show() - vbox_copyright.pack_start(expander, False, True, 0) + vbox_copyright.append(expander) # display secondary licenses, if any for license_text in self._model.get_secondary_licenses(): label_license = Gtk.Label(label=license_text) - label_license.set_alignment(0, 0) - label_license.set_line_wrap(True) - label_license.set_size_request(Gdk.Screen.width() / 2, -1) - label_license.show() + label_license.set_halign(Gtk.Align.START) + label_license.set_wrap(True) + label_license.set_max_width_chars(80) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - vbox_copyright.pack_start(separator, False, True, 0) - separator.show() - vbox_copyright.pack_start(label_license, False, True, 0) + vbox_copyright.append(separator) + vbox_copyright.append(label_license) - self._vbox.pack_start(vbox_copyright, True, True, 0) - vbox_copyright.show() + vbox_copyright.set_vexpand(True) + self._vbox.append(vbox_copyright) def license_expander_cb(self, expander, param_spec): # load/destroy the license viewer on-demand, to avoid storing the @@ -206,10 +192,11 @@ def license_expander_cb(self, expander, param_spec): if expander.get_expanded(): view_license = Gtk.TextView() view_license.set_editable(False) + view_license.set_wrap_mode(Gtk.WrapMode.WORD) view_license.get_buffer().set_text(self._model.get_license()) - fd = Pango.FontDescription('Monospace') - view_license.modify_font(fd) - view_license.show() - expander.add(view_license) + view_license.add_css_class('monospace') + expander.set_child(view_license) else: - expander.get_child().destroy() + child = expander.get_child() + if child: + expander.set_child(None) diff --git a/extensions/cpsection/aboutme/__init__.py b/extensions/cpsection/aboutme/__init__.py index 124bfd7066..e61a806775 100644 --- a/extensions/cpsection/aboutme/__init__.py +++ b/extensions/cpsection/aboutme/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,11 +13,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ from sugar4 import profile + CLASS = 'AboutMe' ICON = 'module-about_me' TITLE = _('About Me') diff --git a/extensions/cpsection/aboutme/model.py b/extensions/cpsection/aboutme/model.py index 87dea08c4d..445f179ec7 100644 --- a/extensions/cpsection/aboutme/model.py +++ b/extensions/cpsection/aboutme/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2010-14, Sugar Labs # Copyright (C) 2010-14, Walter Bender # diff --git a/extensions/cpsection/aboutme/view.py b/extensions/cpsection/aboutme/view.py index eb658acfdd..08a1b36f38 100644 --- a/extensions/cpsection/aboutme/view.py +++ b/extensions/cpsection/aboutme/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # Copyright (C) 2010-14, Sugar Labs # Copyright (C) 2010-14, Walter Bender # Copyright (C) 2014, Ignacio Rodriguez @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gi.repository import GObject from gi.repository import GLib @@ -31,7 +30,6 @@ from jarabe.intro.agepicker import AgePicker, save_age, load_age from jarabe.intro.genderpicker import GenderPicker, save_gender, load_gender - _STROKE_COLOR = 0 _FILL_COLOR = 1 _NOCOLOR = XoColor('#010101,#FFFFFF') @@ -110,7 +108,6 @@ def _previous_index(current_index): def _get_current_index(color): return colors.index([color.stroke, color.fill]) - _PREVIOUS_FILL_COLOR = 0 _NEXT_FILL_COLOR = 1 _CURRENT_COLOR = 2 @@ -169,7 +166,10 @@ def __init__(self, model, alerts): self._gender = '' self._age = None - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) self._color = XoColor(self._model.get_color()) @@ -204,27 +204,21 @@ def _setup_nick(self): 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() + self._nick_alert.set_visible(True) + else: + self._nick_alert.set_visible(False) - center_alert = Gtk.Alignment.new(0.5, 0, 0, 0) - center_alert.add(alert_grid) - alert_grid.show() + grid.set_halign(Gtk.Align.CENTER) + alert_grid.set_halign(Gtk.Align.CENTER) - self.pack_start(center_in_panel, False, False, 0) - self.pack_start(center_alert, False, False, 0) - center_in_panel.show() - center_alert.show() + self.append(grid) + self.append(alert_grid) def _setup_color(self): grid = Gtk.Grid() @@ -242,46 +236,38 @@ def _setup_color(self): } label_color = Gtk.Label(label=_('Click to change your color:')) - label_color.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) + style.apply_css_to_widget(label_color, "* { color: %s; }" % style.COLOR_SELECTION_GREY.get_html()) 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() + left_separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) 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() + right_separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) 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._color_alert.set_visible(True) + else: + self._color_alert.set_visible(False) - self.pack_start(center_in_panel, False, False, 0) - center_in_panel.show() + grid.set_halign(Gtk.Align.CENTER) + self.append(grid) def _setup_gender(self): self._saved_gender = load_gender() @@ -293,20 +279,13 @@ def _setup_gender(self): 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()) + style.apply_css_to_widget(label_gender, "* { color: %s; }" % style.COLOR_SELECTION_GREY.get_html()) 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() + grid.set_halign(Gtk.Align.CENTER) + self.append(grid) def _setup_age(self): self._saved_age = load_age() @@ -316,37 +295,26 @@ def _setup_age(self): 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() + style.apply_css_to_widget(label_age, "* { color: %s; }" % style.COLOR_SELECTION_GREY.get_html()) + label_age.set_halign(Gtk.Align.START) + grid.attach(label_age, 0, 0, 1, 1) + + self._age_pickers.set_halign(Gtk.Align.CENTER) + grid.attach(self._age_pickers, 0, 1, 1, 1) + + grid.set_halign(Gtk.Align.CENTER) + self.append(grid) def setup(self): pass def undo(self): self._model.undo() - self._nick_alert.hide() - self._color_alert.hide() + self._nick_alert.set_visible(False) + self._color_alert.set_visible(False) # Undo gender or age changes save_gender(self._saved_gender) @@ -374,29 +342,31 @@ def __nick_timeout_cb(self, widget): self._nick_sid = 0 if widget.get_text() == self._model.get_nick(): - self.restart_alerts.remove('nick') + if 'nick' in self.restart_alerts: + self.restart_alerts.remove('nick') if not self.restart_alerts: self.needs_restart = False - self._nick_alert.hide() + self._nick_alert.set_visible(False) return False try: self._model.set_nick(widget.get_text()) except ValueError as detail: self._nick_alert.props.msg = detail self._nick_valid = False - self._nick_alert.show() + self._nick_alert.set_visible(True) else: self._nick_valid = True if widget.get_text() == self._original_nick: - self.restart_alerts.remove('nick') + if 'nick' in self.restart_alerts: + self.restart_alerts.remove('nick') if not self.restart_alerts: self.needs_restart = False - self._nick_alert.hide() + self._nick_alert.set_visible(False) else: self._nick_alert.props.msg = self.restart_msg self.needs_restart = True self.restart_alerts.add('nick') - self._nick_alert.show() + self._nick_alert.set_visible(True) self._validate() return False @@ -408,7 +378,7 @@ def __color_changed_cb(self, colorpicker, color): self.restart_alerts.add('color') self._validate() - self._color_alert.show() + self._color_alert.set_visible(True) self._update_pickers(color) return False diff --git a/extensions/cpsection/background/__init__.py b/extensions/cpsection/background/__init__.py index 60eaefe798..3b4743fc3f 100644 --- a/extensions/cpsection/background/__init__.py +++ b/extensions/cpsection/background/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ + CLASS = 'Background' ICON = 'image-x-generic' TITLE = _('Background') diff --git a/extensions/cpsection/background/model.py b/extensions/cpsection/background/model.py index 6fa7584938..e1e6ca4dd2 100644 --- a/extensions/cpsection/background/model.py +++ b/extensions/cpsection/background/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 Agustin Zubiaga +# Copyright (C) 2012 Agustin Zubiaga # Copyright (C) 2013 Sugar Labs # # This program is free software: you can redistribute it and/or modify @@ -42,7 +42,6 @@ def get_background_image_path(): settings = Gio.Settings.new(BACKGROUND_DIR) return settings.get_string(BACKGROUND_IMAGE_PATH_KEY) - PREVIOUS_BACKGROUND_IMAGE_PATH = get_background_image_path() @@ -67,7 +66,6 @@ def get_background_alpha_level(): alpha = 1.0 return alpha - PREVIOUS_BACKGROUND_ALPHA_LEVEL = get_background_alpha_level() diff --git a/extensions/cpsection/background/view.py b/extensions/cpsection/background/view.py index 6ef78afbfe..f6553f70d4 100644 --- a/extensions/cpsection/background/view.py +++ b/extensions/cpsection/background/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 Agustin Zubiaga +# Copyright (C) 2012 Agustin Zubiaga # Copyright (C) 2013 Sugar Labs # # This program is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import os from gi.repository import Gtk @@ -41,40 +40,24 @@ def __init__(self, model, alerts=None): self.connect('realize', self.__realize_cb) self.connect('unrealize', self.__unrealize_cb) - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) label_box = Gtk.Box() label_bg = Gtk.Label(label=_('Select a background:')) - label_bg.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - label_bg.show() - label_box.pack_start(label_bg, False, True, 0) - label_box.show() - self.pack_start(label_box, False, True, 1) - - clear_button = Gtk.Button() - clear_button.set_label(_('Clear background')) - clear_button.connect('clicked', self._clear_clicked_cb) - clear_button.show() - self.pack_end(clear_button, False, True, 0) + style.apply_css_to_widget(label_bg, "* { color: %s; }" % style.COLOR_SELECTION_GREY.get_html()) + label_box.append(label_bg) + self.append(label_box) scrolled_window = Gtk.ScrolledWindow() - scrolled_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + scrolled_window.set_has_frame(True) scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self.pack_start(scrolled_window, True, True, 0) - scrolled_window.show() - - self._store = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - - self._icon_view = Gtk.IconView.new_with_model(self._store) - self._icon_view.set_selection_mode(Gtk.SelectionMode.SINGLE) - self._icon_view.connect('selection-changed', self._background_selected) - self._icon_view.set_pixbuf_column(0) - self._icon_view.grab_focus() - scrolled_window.add(self._icon_view) - self._icon_view.show() + scrolled_window.set_vexpand(True) + self.append(scrolled_window) alpha = self._model.get_background_alpha_level() @@ -94,19 +77,31 @@ def __init__(self, model, alerts=None): button.set_icon_name(icon_name) button.value = value button.props.active = value == alpha - button.show() - alpha_box.pack_start(button, False, True, 0) + alpha_box.append(button) alpha_buttons.append(button) for button in alpha_buttons: button.connect('toggled', self._set_alpha_cb) - alpha_alignment = Gtk.Alignment() - alpha_alignment.set(0.5, 0, 0, 0) - alpha_alignment.add(alpha_box) - alpha_box.show() - self.pack_start(alpha_alignment, False, False, 0) - alpha_alignment.show() + alpha_box.set_halign(Gtk.Align.CENTER) + self.append(alpha_box) + + clear_button = Gtk.Button() + clear_button.set_label(_('Clear background')) + clear_button.connect('clicked', self._clear_clicked_cb) + clear_button.set_valign(Gtk.Align.END) + self.append(clear_button) + + self._flow_box = Gtk.FlowBox() + self._flow_box.set_selection_mode(Gtk.SelectionMode.SINGLE) + self._flow_box.set_valign(Gtk.Align.START) + self._flow_box.set_max_children_per_line(6) + self._flow_box.set_column_spacing(6) + self._flow_box.set_row_spacing(6) + self._flow_box.set_homogeneous(True) + self._flow_box.connect('selected-children-changed', self._background_selected) + self._flow_box.grab_focus() + scrolled_window.set_child(self._flow_box) self._paths_list = [] @@ -132,7 +127,14 @@ def _append_to_store(self, file_paths): except GLib.GError: pass else: - self._store.append([pixbuf, file_path]) + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + picture = Gtk.Picture.new_for_paintable(texture) + picture.set_size_request(style.XLARGE_ICON_SIZE, style.XLARGE_ICON_SIZE) + + child = Gtk.FlowBoxChild() + child.set_child(picture) + child._image_path = file_path + self._flow_box.append(child) self._paths_list.append(file_path) self._append_to_store_sid = GLib.idle_add(self._append_to_store, @@ -140,10 +142,7 @@ def _append_to_store(self, file_paths): else: self._select_background() self._images_loaded = True - window = self.get_window() - if window is not None: - window.set_cursor(None) - Gdk.flush() + self.set_cursor(None) self._append_to_store_sid = None def _cancel_append_to_store(self): @@ -153,51 +152,39 @@ def _cancel_append_to_store(self): def __realize_cb(self, widget): if self._images_loaded: - self.get_window().set_cursor(None) + self.set_cursor(None) else: - self.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) - Gdk.flush() + self.set_cursor(Gdk.Cursor.new_from_name('wait', None)) def __unrealize_cb(self, widget): - self.get_window().set_cursor(None) - Gdk.flush() + self.set_cursor(None) def _set_alpha_cb(self, widget): if widget.get_active(): self._model.set_background_alpha_level(widget.value) - def _get_selected_path(self, widget): - try: - iter_ = self._store.get_iter(widget.get_selected_items()[0]) - image_path = self._store.get(iter_, 1)[0] - - return image_path, iter_ - except: - return None - - def _background_selected(self, widget): - selected = self._get_selected_path(widget) - - if selected is None: + def _background_selected(self, flow_box): + selected = flow_box.get_selected_children() + if not selected: return - - image_path, _iter = selected - iter_ = self._store.get_iter(widget.get_selected_items()[0]) - image_path = self._store.get(iter_, 1)[0] + + child = selected[0] + image_path = child._image_path self._model.set_background_image_path(image_path) def _select_background(self): background = self._model.get_background_image_path() if background in self._paths_list: - self._icon_view.select_path( - Gtk.TreePath.new_from_string( - '%s' % self._paths_list.index(background))) + index = self._paths_list.index(background) + child = self._flow_box.get_child_at_index(index) + if child: + self._flow_box.select_child(child) def _clear_clicked_cb(self, widget, event=None): self._model.set_background_image_path(None) def setup(self): - self.show_all() + pass def apply(self): self._cancel_append_to_store() diff --git a/extensions/cpsection/backup/backends/backend_tools.py b/extensions/cpsection/backup/backends/backend_tools.py index 409e081f99..3ef4d50d94 100644 --- a/extensions/cpsection/backup/backends/backend_tools.py +++ b/extensions/cpsection/backup/backends/backend_tools.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Martin Abente Lahaye - tch@sugarlabs.org +# Copyright (C) 2013, Martin Abente Lahaye - tch@sugarlabs.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/extensions/cpsection/backup/backends/volume.py b/extensions/cpsection/backup/backends/volume.py index 20d1d744a8..5706606c27 100644 --- a/extensions/cpsection/backup/backends/volume.py +++ b/extensions/cpsection/backup/backends/volume.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Martin Abente Lahaye - tch@sugarlabs.org +# Copyright (C) 2013, Martin Abente Lahaye - tch@sugarlabs.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/extensions/cpsection/backup/backupmanager.py b/extensions/cpsection/backup/backupmanager.py index 2f0285bb20..9fec142d00 100644 --- a/extensions/cpsection/backup/backupmanager.py +++ b/extensions/cpsection/backup/backupmanager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 Gonzalo Odiard +# Copyright (c) 2013 Gonzalo Odiard # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,7 +26,6 @@ BACKENDS_MODULE = 'cpsection.backup.backends' - OPERATION_BACKUP = 'backup' OPERATION_RESTORE = 'restore' diff --git a/extensions/cpsection/backup/view.py b/extensions/cpsection/backup/view.py index c4fbb59a78..602b700d16 100644 --- a/extensions/cpsection/backup/view.py +++ b/extensions/cpsection/backup/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, SugarLabs +# Copyright (C) 2013, SugarLabs # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,6 @@ from sugar4.graphics import style from sugar4.graphics.xocolor import XoColor - from jarabe.controlpanel.sectionview import SectionView from .backupmanager import BackupManager @@ -45,56 +44,58 @@ def __init__(self, model, alerts=None): # add the initial panel self.set_canvas(SelectBackupRestorePanel(self)) self.grab_focus() - self.show_all() self.manager = BackupManager() def set_canvas(self, canvas): - if len(self.get_children()) > 0: - self.remove(self.get_children()[0]) + child = self.get_first_child() + if child: + self.remove(child) if canvas: - self.add(canvas) + self.append(canvas) def undo(self): - if self.get_children()[0].__class__ == OperationPanel: - operation_panel = self.get_children()[0] - if operation_panel._operator is not None: - operation_panel._operator.cancel() + child = self.get_first_child() + if child and child.__class__ == OperationPanel: + if child._operator is not None: + child._operator.cancel() -class _BackupButton(Gtk.EventBox): +class _BackupButton(Gtk.Box): __gproperties__ = { 'icon-name': (str, None, None, None, GObject.ParamFlags.READWRITE), 'pixel-size': (object, None, None, GObject.ParamFlags.READWRITE), 'title': (str, None, None, None, GObject.ParamFlags.READWRITE), } + __gsignals__ = { + 'clicked': (GObject.SignalFlags.RUN_FIRST, None, ()), + } def __init__(self, **kwargs): self._icon_name = None self._pixel_size = style.GRID_CELL_SIZE - self._xo_color = None self._title = 'No Title' - Gtk.EventBox.__init__(self, **kwargs) + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, **kwargs) + + self.add_css_class('sectionicon-transparent') - self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._icon = Icon(icon_name=self._icon_name, pixel_size=self._pixel_size, xo_color=XoColor('#000000,#000000')) - self._vbox.pack_start(self._icon, expand=False, fill=False, padding=0) + self.append(self._icon) self._label = Gtk.Label(label=self._title) - self._vbox.pack_start(self._label, expand=False, fill=False, padding=0) + self.append(self._label) - self._vbox.set_spacing(style.DEFAULT_SPACING) - self.set_visible_window(False) - self.set_app_paintable(True) - self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) + self.set_spacing(style.DEFAULT_SPACING) - self.add(self._vbox) - self._vbox.show() - self._label.show() - self._icon.show() + self.click_gesture = Gtk.GestureClick.new() + self.click_gesture.connect("pressed", self.__pressed_cb) + self.add_controller(self.click_gesture) + + def __pressed_cb(self, gesture, n_press, x, y): + self.emit("clicked") def get_icon(self): return self._icon @@ -131,38 +132,38 @@ def __init__(self, view): icon_name='backup-backup', title=_('Save the contents of your Journal'), pixel_size=style.GRID_CELL_SIZE) - self.backup_btn.connect('button-press-event', + self.backup_btn.connect('clicked', self.__backup_button_press_cb) - hbox.pack_start(self.backup_btn, False, False, style.DEFAULT_SPACING) + self.backup_btn.set_margin_start(style.DEFAULT_SPACING) + hbox.append(self.backup_btn) self.restore_btn = _BackupButton( icon_name='backup-restore', title=_('Restore the contents of your Journal'), pixel_size=style.GRID_CELL_SIZE) - self.restore_btn.connect('button-press-event', + self.restore_btn.connect('clicked', self.__restore_button_press_cb) - hbox.pack_start(self.restore_btn, False, False, style.DEFAULT_SPACING) + self.restore_btn.set_margin_start(style.DEFAULT_SPACING) + hbox.append(self.restore_btn) hbox.set_valign(Gtk.Align.CENTER) hbox.set_halign(Gtk.Align.CENTER) - self.add(hbox) - self.show_all() + self.append(hbox) - def __backup_button_press_cb(self, button, event): + def __backup_button_press_cb(self, button): operation_panel = OperationPanel(OPERATION_BACKUP, self._view) self._view.set_canvas(operation_panel) - def __restore_button_press_cb(self, button, event): + def __restore_button_press_cb(self, button): operation_panel = OperationPanel(OPERATION_RESTORE, self._view) self._view.set_canvas(operation_panel) -class OperationPanel(Gtk.Grid): +class OperationPanel(Gtk.Box): def __init__(self, operation, view): - Gtk.Grid.__init__(self) - self.set_orientation(Gtk.Orientation.VERTICAL) - self.set_row_spacing(style.DEFAULT_SPACING) + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(style.DEFAULT_SPACING) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.CENTER) @@ -172,49 +173,38 @@ def __init__(self, operation, view): _icon = Icon(icon_name='backup-%s' % operation, pixel_size=style.XLARGE_ICON_SIZE) - self.add(_icon) - _icon.show() + self.append(_icon) self._message_label = Gtk.Label() - self._message_label.set_line_wrap(True) + self._message_label.set_wrap(True) self._message_label.set_width_chars(40) - self._message_label.set_single_line_mode(False) - align = Gtk.Alignment.new(0.5, 0.5, 0, 0) - align.set_padding(0, 0, style.GRID_CELL_SIZE * 2, - style.GRID_CELL_SIZE * 2) - align.show() - align.add(self._message_label) - self.add(align) - self._message_label.show() + self._message_label.set_halign(Gtk.Align.CENTER) + self._message_label.set_valign(Gtk.Align.CENTER) + self._message_label.set_margin_bottom(style.GRID_CELL_SIZE * 2) + self._message_label.set_margin_end(style.GRID_CELL_SIZE * 2) + self.append(self._message_label) self._options_combo = Gtk.ComboBox() cell = Gtk.CellRendererText() self._options_combo.pack_start(cell, True) self._options_combo.add_attribute(cell, 'text', 0) - self.add(self._options_combo) + self.append(self._options_combo) self._progress_bar = Gtk.ProgressBar() - self.add(self._progress_bar) - self._progress_bar.set_size_request( - Gdk.Screen.width() - style.GRID_CELL_SIZE * 6, -1) + self._progress_bar.set_hexpand(True) + self.append(self._progress_bar) self._confirm_restore_chkbtn = Gtk.CheckButton() - align = Gtk.Alignment() - align.set_padding(0, 0, style.GRID_CELL_SIZE * 2, - style.GRID_CELL_SIZE * 2) - align.show() - align.add(self._confirm_restore_chkbtn) - self.add(align) - - btn_box = Gtk.ButtonBox() - btn_box.show() - self._continue_btn = Gtk.Button(_('Continue')) - btn_box.add(self._continue_btn) - self.add(btn_box) + self._confirm_restore_chkbtn.set_margin_bottom(style.GRID_CELL_SIZE * 2) + self._confirm_restore_chkbtn.set_margin_end(style.GRID_CELL_SIZE * 2) + self.append(self._confirm_restore_chkbtn) + + btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self._continue_btn = Gtk.Button(label=_('Continue')) + btn_box.append(self._continue_btn) + self.append(btn_box) self._continue_btn_handler_id = 0 - self.show() - # check if there are activities running # and request close them if any. if self._view.manager.need_stop_activities(): @@ -254,7 +244,7 @@ def _ask_options(self, message, options, continue_cb): options_store.append([option['description'], option['value']]) self._options_combo.set_model(options_store) - self._options_combo.show() + self._options_combo.set_visible(True) if self._continue_btn_handler_id != 0: self._continue_btn.disconnect(self._continue_btn_handler_id) @@ -262,13 +252,13 @@ def _ask_options(self, message, options, continue_cb): self._continue_btn_handler_id = self._continue_btn.connect( 'clicked', continue_cb) self._continue_btn.set_label(_('Continue')) - self._continue_btn.show() + self._continue_btn.set_visible(True) def _show_error_message(self, message): self._message_label.set_text(message) - self._options_combo.hide() + self._options_combo.set_visible(False) self._continue_btn.set_label(_('Retry')) - self._continue_btn.show() + self._continue_btn.set_visible(True) if self._continue_btn_handler_id != 0: self._continue_btn.disconnect(self._continue_btn_handler_id) self._continue_btn_handler_id = self._continue_btn.connect( @@ -321,10 +311,10 @@ def _request_restore_confirmation(self): 'In order to do this, my Journal will first be emptied of all ' 'its content; then the restored content will be added.')) self._confirm_restore_chkbtn.set_label(_('Accept')) - self._confirm_restore_chkbtn.show() - self._options_combo.hide() + self._confirm_restore_chkbtn.set_visible(True) + self._options_combo.set_visible(False) self._continue_btn.set_label(_('Confirm')) - self._continue_btn.show() + self._continue_btn.set_visible(True) if self._continue_btn_handler_id != 0: self._continue_btn.disconnect(self._continue_btn_handler_id) self._continue_btn_handler_id = self._continue_btn.connect( @@ -333,8 +323,8 @@ def _request_restore_confirmation(self): def __confirm_restore_cb(self, button): if self._confirm_restore_chkbtn.get_active(): self._view.props.is_cancellable = False - self._confirm_restore_chkbtn.hide() - self._continue_btn.hide() + self._confirm_restore_chkbtn.set_visible(False) + self._continue_btn.set_visible(False) self._message_label.set_text('') self._internal_start_operation() self._view.needs_restart = True @@ -362,10 +352,10 @@ def _assign_parameter_backend(self, button): def __operation_started_cb(self, backend): self._message_label.set_text(_('Operation started')) - self._options_combo.hide() - self._continue_btn.hide() + self._options_combo.set_visible(False) + self._continue_btn.set_visible(False) self._progress_bar.set_fraction(0.0) - self._progress_bar.show() + self._progress_bar.set_visible(True) def __operation_progress_cb(self, backend, fraction): self._progress_bar.set_fraction(fraction) diff --git a/extensions/cpsection/datetime/__init__.py b/extensions/cpsection/datetime/__init__.py index 527078b320..e469292a88 100644 --- a/extensions/cpsection/datetime/__init__.py +++ b/extensions/cpsection/datetime/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ + CLASS = 'TimeZone' ICON = 'module-date_and_time' TITLE = _('Date & Time') diff --git a/extensions/cpsection/datetime/model.py b/extensions/cpsection/datetime/model.py index 94a0576f28..a8c4914e62 100644 --- a/extensions/cpsection/datetime/model.py +++ b/extensions/cpsection/datetime/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, 2008 One Laptop Per Child +# Copyright (C) 2007, 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -60,10 +60,16 @@ def read_all_timezones(fn=_zone_tab): timezones.append(tz) return timezones +_initial_timezone = None + def get_timezone(): + global _initial_timezone settings = Gio.Settings.new('org.sugarlabs.date') - return settings.get_string('timezone') + tz = settings.get_string('timezone') + if _initial_timezone is None: + _initial_timezone = tz + return tz def print_timezone(): @@ -100,5 +106,10 @@ def set_timezone(timezone): return 1 +def undo(): + global _initial_timezone + if _initial_timezone is not None: + set_timezone(_initial_timezone) + # inilialize the docstrings for the timezone _initialize() diff --git a/extensions/cpsection/datetime/view.py b/extensions/cpsection/datetime/view.py index a122d16371..0b3eeaa217 100644 --- a/extensions/cpsection/datetime/view.py +++ b/extensions/cpsection/datetime/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gi.repository import GObject from gi.repository import GLib @@ -26,6 +25,11 @@ from jarabe.controlpanel.inlinealert import InlineAlert +CLASS = 'TimeZone' +ICON = 'module-date_and_time' +TITLE = _('Date & Time') + + class TimeZone(SectionView): def __init__(self, model, alerts): SectionView.__init__(self) @@ -35,54 +39,57 @@ def __init__(self, model, alerts): self._zone_sid = 0 self._cursor_change_handler = None - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) - self.connect('realize', self.__realize_cb) + self.connect('map', self.__map_cb) self._entry = iconentry.IconEntry() self._entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, 'entry-search') self._entry.add_clear_button() - self.pack_start(self._entry, False, False, 0) - self._entry.show() + self.append(self._entry) self._scrolled_window = Gtk.ScrolledWindow() self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self._scrolled_window.set_shadow_type(Gtk.ShadowType.IN) + self._scrolled_window.set_has_frame(True) self._store = Gtk.ListStore(GObject.TYPE_STRING) zones = model.read_all_timezones() for zone in zones: self._store.append([zone]) - self._treeview = Gtk.TreeView(self._store) + self._treeview = Gtk.TreeView(model=self._store) self._treeview.set_search_entry(self._entry) self._treeview.set_search_equal_func(self._search, None) self._treeview.set_search_column(0) - self._scrolled_window.add(self._treeview) - self._treeview.show() + self._scrolled_window.set_child(self._treeview) - self._timezone_column = Gtk.TreeViewColumn(_('Timezone')) + self._timezone_column = Gtk.TreeViewColumn(title=_('Timezone')) self._cell = Gtk.CellRendererText() self._timezone_column.pack_start(self._cell, True) self._timezone_column.add_attribute(self._cell, 'text', 0) self._timezone_column.set_sort_column_id(0) self._treeview.append_column(self._timezone_column) - self.pack_start(self._scrolled_window, True, True, 0) - self._scrolled_window.show() + self._scrolled_window.set_vexpand(True) + self.append(self._scrolled_window) self._zone_alert_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) - self.pack_start(self._zone_alert_box, False, False, 0) + self.append(self._zone_alert_box) self._zone_alert = InlineAlert() - self._zone_alert_box.pack_start(self._zone_alert, True, True, 0) + self._zone_alert.set_hexpand(True) + self._zone_alert_box.append(self._zone_alert) if 'zone' in self.restart_alerts: self._zone_alert.props.msg = self.restart_msg - self._zone_alert.show() - self._zone_alert_box.show() + self._zone_alert.set_visible(True) + else: + self._zone_alert.set_visible(False) self.setup() @@ -90,9 +97,10 @@ def setup(self): zone = self._model.get_timezone() for row in self._store: if zone == row[0]: - self._treeview.set_cursor(row.path, self._timezone_column, + path = self._store.get_path(row.iter) + self._treeview.set_cursor(path, self._timezone_column, False) - self._treeview.scroll_to_cell(row.path, self._timezone_column, + self._treeview.scroll_to_cell(path, self._timezone_column, True, 0.5, 0.5) break @@ -103,9 +111,9 @@ def setup(self): def undo(self): self._treeview.disconnect(self._cursor_change_handler) self._model.undo() - self._zone_alert.hide() + self._zone_alert.set_visible(False) - def __realize_cb(self, widget): + def __map_cb(self, widget): self._entry.grab_focus() def _search(self, model, column, key, iterator, data=None): @@ -118,20 +126,21 @@ def __zone_changed_cd(self, treeview, data=None): list_, row = treeview.get_selection().get_selected() if not row: return False - if self._model.get_timezone() == self._store.get_value(row, 0): + timezone_val = self._store.get_value(row, 0) + if self._model.get_timezone() == timezone_val: return False if self._zone_sid: GLib.source_remove(self._zone_sid) self._zone_sid = GLib.timeout_add(self._APPLY_TIMEOUT, - self.__zone_timeout_cb, row) + self.__zone_timeout_cb, timezone_val) return True - def __zone_timeout_cb(self, row): + def __zone_timeout_cb(self, timezone_val): self._zone_sid = 0 - self._model.set_timezone(self._store.get_value(row, 0)) + self._model.set_timezone(timezone_val) self.restart_alerts.append('zone') self.needs_restart = True self._zone_alert.props.msg = self.restart_msg - self._zone_alert.show() + self._zone_alert.set_visible(True) return False diff --git a/extensions/cpsection/frame/__init__.py b/extensions/cpsection/frame/__init__.py index 94cc56ce16..c41c7411fd 100644 --- a/extensions/cpsection/frame/__init__.py +++ b/extensions/cpsection/frame/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ + CLASS = 'Frame' ICON = 'module-frame' TITLE = _('Frame') diff --git a/extensions/cpsection/frame/model.py b/extensions/cpsection/frame/model.py index 6955942204..39ad5ea6bd 100644 --- a/extensions/cpsection/frame/model.py +++ b/extensions/cpsection/frame/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/extensions/cpsection/frame/view.py b/extensions/cpsection/frame/view.py index 95fbfad73d..a13b8a1887 100644 --- a/extensions/cpsection/frame/view.py +++ b/extensions/cpsection/frame/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gi.repository import GLib from gettext import gettext as _ @@ -22,7 +21,6 @@ from jarabe.controlpanel.sectionview import SectionView - _never = _('never') _instantaneous = _('instantaneous') _seconds_label = _('%s seconds') @@ -42,99 +40,98 @@ def __init__(self, model, alerts): self._trigger_size_change_handler = None self.restart_alerts = alerts - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) - self._group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + self._group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self.pack_start(separator, False, True, 0) + self.append(separator) label = Gtk.Label(label=_('Activation Delay')) - label.set_alignment(0, 0) - self.pack_start(label, False, True, 0) + label.set_halign(Gtk.Align.START) + self.append(label) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box.set_border_width(style.DEFAULT_SPACING * 2) + box.set_margin_top(style.DEFAULT_SPACING * 2) + box.set_margin_bottom(style.DEFAULT_SPACING * 2) + box.set_margin_start(style.DEFAULT_SPACING * 2) + box.set_margin_end(style.DEFAULT_SPACING * 2) box.set_spacing(style.DEFAULT_SPACING) - box.pack_start(self._setup_corner(), False, True, 0) - box.pack_start(self._setup_edge(), False, True, 0) + box.append(self._setup_corner()) + box.append(self._setup_edge()) - self.pack_start(box, False, True, 0) + self.append(box) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self.pack_start(separator, False, True, 0) + self.append(separator) label = Gtk.Label(label=_('Activation Area')) - label.set_alignment(0, 0) - self.pack_start(label, False, True, 0) + label.set_halign(Gtk.Align.START) + self.append(label) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box.set_border_width(style.DEFAULT_SPACING * 2) + box.set_margin_top(style.DEFAULT_SPACING * 2) + box.set_margin_bottom(style.DEFAULT_SPACING * 2) + box.set_margin_start(style.DEFAULT_SPACING * 2) + box.set_margin_end(style.DEFAULT_SPACING * 2) box.set_spacing(style.DEFAULT_SPACING) - box.pack_start(self._setup_trigger(), False, True, 0) + box.append(self._setup_trigger()) - self.pack_start(box, False, True, 0) - self.show_all() + self.append(box) self.setup() def _setup_corner(self): box_delay = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) label_delay = Gtk.Label(label=_('Corner')) - label_delay.set_alignment(1, 0.75) - label_delay.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_delay.pack_start(label_delay, False, True, 0) + label_delay.set_halign(Gtk.Align.END) + box_delay.append(label_delay) self._group.add_widget(label_delay) adj = Gtk.Adjustment(value=100, lower=0, upper=_MAX_DELAY, - step_incr=100, page_incr=100, page_size=0) - self._corner_delay_slider = Gtk.HScale() - self._corner_delay_slider.set_adjustment(adj) + step_increment=100, page_increment=100, page_size=0) + self._corner_delay_slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj) self._corner_delay_slider.set_digits(0) - self._corner_delay_slider.connect('format-value', - self.__corner_delay_format_cb) - box_delay.pack_start(self._corner_delay_slider, True, True, 0) + self._corner_delay_slider.set_hexpand(True) + self._corner_delay_slider.set_format_value_func(self.__corner_delay_format_cb) + box_delay.append(self._corner_delay_slider) return box_delay def _setup_edge(self): box_delay = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) label_delay = Gtk.Label(label=_('Edge')) - label_delay.set_alignment(1, 0.75) - label_delay.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_delay.pack_start(label_delay, False, True, 0) + label_delay.set_halign(Gtk.Align.END) + box_delay.append(label_delay) self._group.add_widget(label_delay) adj = Gtk.Adjustment(value=100, lower=0, upper=_MAX_DELAY, - step_incr=100, page_incr=100, page_size=0) - self._edge_delay_slider = Gtk.HScale() - self._edge_delay_slider.set_adjustment(adj) + step_increment=100, page_increment=100, page_size=0) + self._edge_delay_slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj) self._edge_delay_slider.set_digits(0) - self._edge_delay_slider.connect('format-value', - self.__edge_delay_format_cb) - box_delay.pack_start(self._edge_delay_slider, True, True, 0) + self._edge_delay_slider.set_hexpand(True) + self._edge_delay_slider.set_format_value_func(self.__edge_delay_format_cb) + box_delay.append(self._edge_delay_slider) return box_delay def _setup_trigger(self): box_trigger = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) label_trigger = Gtk.Label(label=_('Size')) - label_trigger.set_alignment(1, 0.75) - label_trigger.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_trigger.pack_start(label_trigger, False, True, 0) + label_trigger.set_halign(Gtk.Align.END) + box_trigger.append(label_trigger) self._group.add_widget(label_trigger) adj = Gtk.Adjustment(value=1, lower=1, upper=style.GRID_CELL_SIZE, - step_incr=1, page_incr=1, page_size=0) - self._trigger_size_slider = Gtk.HScale() - self._trigger_size_slider.set_adjustment(adj) + step_increment=1, page_increment=1, page_size=0) + self._trigger_size_slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj) self._trigger_size_slider.set_digits(0) - self._trigger_size_slider.connect('format-value', - self.__trigger_size_format_cb) - box_trigger.pack_start(self._trigger_size_slider, True, True, 0) + self._trigger_size_slider.set_hexpand(True) + self._trigger_size_slider.set_format_value_func(self.__trigger_size_format_cb) + box_trigger.append(self._trigger_size_slider) return box_trigger def setup(self): @@ -208,7 +205,7 @@ def __trigger_size_changed_cb(self, scale, data=None): def __trigger_size_timeout_cb(self, scale): self._trigger_size_sid = 0 if scale.get_value() == self._model.get_trigger_size(): - return + return False self._model.set_trigger_size(scale.get_value()) return False @@ -240,4 +237,4 @@ def apply(self): self.__edge_delay_timeout_cb(self._edge_delay_slider) if self._trigger_size_sid: GLib.source_remove(self._trigger_size_sid) - self.__trigger_size_timeout_cb(self._trigger_size_sid) + self.__trigger_size_timeout_cb(self._trigger_size_slider) diff --git a/extensions/cpsection/keyboard/__init__.py b/extensions/cpsection/keyboard/__init__.py index a4555dc1ff..ac920b8b58 100644 --- a/extensions/cpsection/keyboard/__init__.py +++ b/extensions/cpsection/keyboard/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009, OLPC +# Copyright (C) 2009, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ + CLASS = 'Keyboard' ICON = 'module-keyboard' TITLE = _('Keyboard') diff --git a/extensions/cpsection/keyboard/model.py b/extensions/cpsection/keyboard/model.py index 07f48a5bc9..8049bfb4ed 100644 --- a/extensions/cpsection/keyboard/model.py +++ b/extensions/cpsection/keyboard/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013 Sugar Labs +# Copyright (C) 2013 Sugar Labs # Copyright (C) 2009 OLPC # Author: Sayamindu Dasgupta # @@ -16,78 +16,175 @@ # along with this program. If not, see . # +import os +import logging +import xml.etree.ElementTree as ET +import gi +gi.require_version('Xkl', '1.0') from gi.repository import Xkl from gi.repository import Gio +from jarabe.config import data_path + _GROUP_NAME = 'grp' # The XKB name for group switch options _KEYBOARD_DIR = 'org.sugarlabs.peripherals.keyboard' _LAYOUTS_KEY = 'layouts' _OPTIONS_KEY = 'options' _MODEL_KEY = 'model' +_EVDEV_XML_PATH = '/usr/share/X11/xkb/rules/evdev.xml' class KeyboardManager(object): - def __init__(self, display): - self._engine = Xkl.Engine.get_instance(display) - self._configregistry = Xkl.ConfigRegistry.get_instance(self._engine) - self._configregistry.load(False) - self._configrec = Xkl.ConfigRec() - self._configrec.get_from_server(self._engine) + def __init__(self, display): self._settings = Gio.Settings.new(_KEYBOARD_DIR) + self._engine = None + self._configrec = None - def _populate_one(self, config_registry, item, store): - store.append([item.get_description(), item.get_name()]) + if display is not None: + try: + self._engine = Xkl.Engine.get_instance(display) + self._configrec = Xkl.ConfigRec() + self._configrec.get_from_server(self._engine) + except Exception: + logging.exception('Could not initialize Xkl engine') + self._engine = None + self._configrec = None - def _populate_two(self, config_registry, item, subitem, store): - layout = item.get_name() - if subitem: - description = '%s, %s' % (subitem.get_description(), - item.get_description()) - variant = subitem.get_name() - else: - description = 'Default layout, %s' % item.get_description() - variant = '' - store.append([description, ('%s(%s)' % (layout, variant))]) + self._models = [] + self._languages = {} + self._layouts_for_lang = {} + self._options = [] + self._load_xkb_registry() + + def _load_xkb_registry(self): + lang_names = {} + iso_file = os.path.join(data_path, 'ISO-639-2_utf-8.txt') + if os.path.exists(iso_file): + try: + with open(iso_file, 'r', encoding='utf-8') as f: + for line in f: + parts = line.strip().split('|') + if len(parts) >= 4: + eng_name = parts[3].split(';')[0].strip() + if parts[0]: + lang_names[parts[0]] = eng_name + if parts[1]: + lang_names[parts[1]] = eng_name + except Exception: + logging.exception('Failed to parse ISO-639-2 file') + + if not os.path.exists(_EVDEV_XML_PATH): + return + + try: + tree = ET.parse(_EVDEV_XML_PATH) + root = tree.getroot() + + for elem in root.iter(): + if elem.tag.startswith('{'): + elem.tag = elem.tag.split('}', 1)[1] + + model_list = root.find('modelList') + if model_list is not None: + for model_node in model_list.findall('model'): + config = model_node.find('configItem') + if config is not None: + name_node = config.find('name') + desc_node = config.find('description') + if name_node is not None and desc_node is not None: + self._models.append([desc_node.text, name_node.text]) + self._models.sort() + + layout_list = root.find('layoutList') + if layout_list is not None: + for layout_node in layout_list.findall('layout'): + config = layout_node.find('configItem') + if config is not None: + layout_name = config.find('name') + layout_desc = config.find('description') + if layout_name is not None and layout_desc is not None: + l_name = layout_name.text + l_desc = layout_desc.text + + lang_list = config.find('languageList') + lang_codes = [] + if lang_list is not None: + for lang_node in lang_list.findall('iso639Id'): + lang_codes.append(lang_node.text) + + variants = [[l_desc, '%s()' % l_name]] + + variant_list = layout_node.find('variantList') + if variant_list is not None: + for var_node in variant_list.findall('variant'): + var_config = var_node.find('configItem') + if var_config is not None: + v_name = var_config.find('name') + v_desc = var_config.find('description') + if v_name is not None and v_desc is not None: + variants.append([ + '%s, %s' % (v_desc.text, l_desc), + '%s(%s)' % (l_name, v_name.text) + ]) + + for lang in lang_codes: + desc = lang_names.get(lang, lang.upper()) + self._languages[lang] = desc + if lang not in self._layouts_for_lang: + self._layouts_for_lang[lang] = [] + self._layouts_for_lang[lang].extend(variants) + + for lang in self._layouts_for_lang: + self._layouts_for_lang[lang].sort() + + option_list = root.find('optionList') + if option_list is not None: + for group in option_list.findall('group'): + config = group.find('configItem') + if config is not None: + name_node = config.find('name') + if name_node is not None and name_node.text == _GROUP_NAME: + for opt in group.findall('option'): + opt_config = opt.find('configItem') + if opt_config is not None: + opt_name = opt_config.find('name') + opt_desc = opt_config.find('description') + if opt_name is not None and opt_desc is not None: + self._options.append([opt_desc.text, opt_name.text]) + self._options.sort() + + except Exception: + logging.exception('Failed to parse evdev.xml') def get_models(self): """Return list of supported keyboard models""" - models = [] - self._configregistry.foreach_model(self._populate_one, models) - models.sort() - return models + return self._models def get_languages(self): """Return list of supported keyboard languages""" - languages = [] - self._configregistry.foreach_language(self._populate_one, languages) + languages = [[desc, code] for code, desc in self._languages.items()] languages.sort() return languages def get_layouts_for_language(self, language): """Return list of supported keyboard layouts for a given language""" - layouts = [] - self._configregistry.foreach_language_variant(language, - self._populate_two, - layouts) - layouts.sort() - return layouts + return self._layouts_for_lang.get(language, []) def get_options_group(self): """Return list of supported options for switching keyboard group""" - options = [] - self._configregistry.foreach_option(_GROUP_NAME, self._populate_one, - options) - options.sort() - return options + return self._options def get_current_model(self): """Return the enabled keyboard model""" model = self._settings.get_string(_MODEL_KEY) if not model: - model = self._configrec.model - self.set_model(model) + if self._configrec is not None: + model = self._configrec.model + self.set_model(model) + else: + model = 'pc105' return model def get_current_layouts(self): @@ -96,28 +193,33 @@ def get_current_layouts(self): if layouts: return layouts - layouts = self._configrec.layouts - variants = self._configrec.variants + if self._configrec is not None: + layouts = self._configrec.layouts + variants = self._configrec.variants - layout_list = [] - i = 0 - for layout in layouts: - if len(variants) <= i or variants[i] == '': - layout_list.append('%s(%s)' % (layout, '')) - else: - layout_list.append('%s(%s)' % (layout, variants[i])) - i += 1 + layout_list = [] + i = 0 + for layout in layouts: + if len(variants) <= i or variants[i] == '': + layout_list.append('%s(%s)' % (layout, '')) + else: + layout_list.append('%s(%s)' % (layout, variants[i])) + i += 1 - self.set_layouts(layout_list) - return layout_list + self.set_layouts(layout_list) + return layout_list + return ['us'] def get_current_option_group(self): """Return the enabled option for switching keyboard group""" options = self._settings.get_strv(_OPTIONS_KEY) if not options: - options = self._configrec.options - self.set_option_group(options) + if self._configrec is not None: + options = self._configrec.options + self.set_option_group(options) + else: + options = [] for option in options: if option.startswith(_GROUP_NAME): @@ -127,15 +229,18 @@ def get_current_option_group(self): def get_max_layouts(self): """Return the maximum number of layouts supported simultaneously""" - return self._engine.get_max_num_groups() + if self._engine is not None: + return self._engine.get_max_num_groups() + return 4 def set_model(self, model): """Sets the supplied keyboard model""" if model is None or not model: return self._settings.set_string(_MODEL_KEY, model) - self._configrec.set_model(model) - self._configrec.activate(self._engine) + if self._configrec is not None: + self._configrec.set_model(model) + self._configrec.activate(self._engine) def set_option_group(self, option_group): """Sets the supplied option for switching keyboard group""" @@ -148,8 +253,9 @@ def set_option_group(self, option_group): options = [option_group] self._settings.set_strv(_OPTIONS_KEY, options) - self._configrec.set_options(options) - self._configrec.activate(self._engine) + if self._configrec is not None: + self._configrec.set_options(options) + self._configrec.activate(self._engine) def set_layouts(self, layouts): """Sets the supplied keyboard layouts (with variants)""" @@ -158,11 +264,12 @@ def set_layouts(self, layouts): self._settings.set_strv(_LAYOUTS_KEY, layouts) - layouts_list = [] - variants_list = [] - for layout in layouts: - layouts_list.append(layout.split('(')[0]) - variants_list.append(layout.split('(')[1][:-1]) - self._configrec.set_layouts(layouts_list) - self._configrec.set_variants(variants_list) - self._configrec.activate(self._engine) + if self._configrec is not None: + layouts_list = [] + variants_list = [] + for layout in layouts: + layouts_list.append(layout.split('(')[0]) + variants_list.append(layout.split('(')[1][:-1]) + self._configrec.set_layouts(layouts_list) + self._configrec.set_variants(variants_list) + self._configrec.activate(self._engine) diff --git a/extensions/cpsection/keyboard/view.py b/extensions/cpsection/keyboard/view.py index c0675e419d..1318f9046a 100644 --- a/extensions/cpsection/keyboard/view.py +++ b/extensions/cpsection/keyboard/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Sugar Labs +# Copyright (C) 2013, Sugar Labs # Copyright (C) 2009, OLPC # Author: Sayamindu Dasgupta # @@ -15,12 +15,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import os import locale from gi.repository import Gtk -from gi.repository import GdkX11 +from gi.repository import Gdk from gi.repository import GObject from gi.repository import GLib from gi.repository import Pango @@ -34,7 +33,8 @@ from jarabe.controlpanel.sectionview import SectionView from jarabe.config import data_path -CLASS = 'Language' + +CLASS = 'Keyboard' ICON = 'module-keyboard' TITLE = _('Keyboard') @@ -76,18 +76,22 @@ class LayoutCombo(Gtk.Box): def __init__(self, keyboard_manager, n): GObject.GObject.__init__(self) + self.set_orientation(Gtk.Orientation.HORIZONTAL) self._keyboard_manager = keyboard_manager self._index = n - self.set_border_width(style.DEFAULT_SPACING) + self.set_margin_top(style.DEFAULT_SPACING) + self.set_margin_bottom(style.DEFAULT_SPACING) + self.set_margin_start(style.DEFAULT_SPACING) + self.set_margin_end(style.DEFAULT_SPACING) self.set_spacing(style.DEFAULT_SPACING) - label = Gtk.Label(label=' %s ' % str(n + 1)) + label = Gtk.Label(label=' %s ' % + (style.COLOR_SELECTION_GREY.get_svg(), str(n + 1))) label.set_use_markup(True) - label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - label.set_alignment(0.5, 0.5) - self.pack_start(label, False, True, 0) + label.set_halign(Gtk.Align.CENTER) + label.set_valign(Gtk.Align.CENTER) + self.append(label) self._klang_store = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING) @@ -102,7 +106,8 @@ def __init__(self, keyboard_manager, n): cell.props.ellipsize_set = True self._klang_combo.pack_start(cell, True) self._klang_combo.add_attribute(cell, 'text', 1) - self.pack_start(self._klang_combo, expand=True, fill=True, padding=0) + self._klang_combo.set_hexpand(True) + self.append(self._klang_combo) self._kvariant_store = None self._kvariant_combo = Gtk.ComboBox(model=None) @@ -114,8 +119,8 @@ def __init__(self, keyboard_manager, n): cell.props.ellipsize_set = True self._kvariant_combo.pack_start(cell, True) self._kvariant_combo.add_attribute(cell, 'text', 1) - self.pack_start(self._kvariant_combo, expand=True, fill=True, - padding=0) + self._kvariant_combo.set_hexpand(True) + self.append(self._kvariant_combo) self._klang_combo.set_active(self._index) @@ -156,7 +161,11 @@ def _look_for_lang_and_layout(self, lang, layout): def get_layout(self): """Gets the selected layout (with variant)""" it = self._kvariant_combo.get_active_iter() + if it is None: + return '' model = self._kvariant_combo.get_model() + if model is None: + return '' return model.get(it, 0)[0] def _set_kvariant_store(self, lang): @@ -170,13 +179,21 @@ def _set_kvariant_store(self, lang): def _klang_combo_changed_cb(self, combobox): it = combobox.get_active_iter() + if it is None: + return model = combobox.get_model() + if model is None: + return lang = model.get(it, 0)[0] self._set_kvariant_store(lang) def _kvariant_combo_changed_cb(self, combobox): it = combobox.get_active_iter() + if it is None: + return model = combobox.get_model() + if model is None: + return layout = model.get(it, 0)[0] self.emit('selection-changed', layout, self._index) @@ -196,26 +213,50 @@ def __init__(self, model, alerts): self._group_switch_option = None self._selected_group_switch_option = None - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) - self._layout_table = Gtk.Table(rows=4, columns=2, homogeneous=False) + self._layout_table = Gtk.Grid() + self._layout_table.set_column_spacing(style.DEFAULT_SPACING) + self._layout_table.set_row_spacing(style.DEFAULT_SPACING) _build_ISO_639_dictionary() - self._keyboard_manager = model.KeyboardManager( - GdkX11.x11_get_default_xdisplay()) + display_manager = Gdk.DisplayManager.get() + xdisplay = None + for display in display_manager.list_displays(): + if hasattr(display, 'get_xdisplay'): + xdisplay = display.get_xdisplay() + break + + if xdisplay is None: + display_name = os.environ.get('DISPLAY') + if display_name: + try: + x11_display = display_manager.open_display(display_name) + if x11_display is not None and hasattr(x11_display, 'get_xdisplay'): + xdisplay = x11_display.get_xdisplay() + except Exception: + pass + + if xdisplay is None: + logging.warning('Keyboard: Could not get X11 display, ' + 'keyboard settings may not work.') + self._keyboard_manager = model.KeyboardManager(xdisplay) self._layout_combo_list = [] self._layout_addremovebox_list = [] scrollwindow = Gtk.ScrolledWindow() scrollwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.pack_start(scrollwindow, True, True, 0) - scrollwindow.show() + scrollwindow.set_vexpand(True) + self.append(scrollwindow) self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - scrollwindow.add_with_viewport(self._vbox) + scrollwindow.set_child(self._vbox) self.__kmodel_sid = None self.__layout_sid = None @@ -225,21 +266,20 @@ def __init__(self, model, alerts): self._setup_layouts() self._setup_group_switch_option() - self._vbox.show() - def _setup_kmodel(self): """Adds the controls for changing the keyboard model""" separator_kmodel = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(separator_kmodel, False, True, 0) - separator_kmodel.show_all() + self._vbox.append(separator_kmodel) label_kmodel = Gtk.Label(label=_('Keyboard Model')) - label_kmodel.set_alignment(0, 0) - self._vbox.pack_start(label_kmodel, False, True, 0) - label_kmodel.show_all() + label_kmodel.set_halign(Gtk.Align.START) + self._vbox.append(label_kmodel) box_kmodel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_kmodel.set_border_width(style.DEFAULT_SPACING * 2) + box_kmodel.set_margin_top(style.DEFAULT_SPACING * 2) + box_kmodel.set_margin_bottom(style.DEFAULT_SPACING * 2) + box_kmodel.set_margin_start(style.DEFAULT_SPACING * 2) + box_kmodel.set_margin_end(style.DEFAULT_SPACING * 2) box_kmodel.set_spacing(style.DEFAULT_SPACING) kmodel_store = Gtk.ListStore(GObject.TYPE_STRING, @@ -261,9 +301,8 @@ def _setup_kmodel(self): kmodel_combo.set_active_iter(row.iter) break - box_kmodel.pack_start(kmodel_combo, False, True, 0) - self._vbox.pack_start(box_kmodel, False, True, 0) - box_kmodel.show_all() + box_kmodel.append(kmodel_combo) + self._vbox.append(box_kmodel) kmodel_combo.connect('changed', self.__kmodel_changed_cb) @@ -275,11 +314,16 @@ def __kmodel_changed_cb(self, combobox): combobox) def __kmodel_timeout_cb(self, combobox): + self.__kmodel_sid = None it = combobox.get_active_iter() + if it is None: + return False model = combobox.get_model() + if model is None: + return False self._selected_kmodel = model.get(it, 0)[0] if self._selected_kmodel == self._keyboard_manager.get_current_model(): - return + return False try: self._keyboard_manager.set_model(self._selected_kmodel) except Exception: @@ -291,16 +335,17 @@ def _setup_group_switch_option(self): """Adds the controls for changing the group switch option of keyboard """ separator_group_option = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(separator_group_option, False, True, 0) - separator_group_option.show_all() + self._vbox.append(separator_group_option) label_group_option = Gtk.Label(label=_('Key(s) to change layout')) - label_group_option.set_alignment(0, 0) - self._vbox.pack_start(label_group_option, False, True, 0) - label_group_option.show_all() + label_group_option.set_halign(Gtk.Align.START) + self._vbox.append(label_group_option) box_group_option = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_group_option.set_border_width(style.DEFAULT_SPACING * 2) + box_group_option.set_margin_top(style.DEFAULT_SPACING * 2) + box_group_option.set_margin_bottom(style.DEFAULT_SPACING * 2) + box_group_option.set_margin_start(style.DEFAULT_SPACING * 2) + box_group_option.set_margin_end(style.DEFAULT_SPACING * 2) box_group_option.set_spacing(style.DEFAULT_SPACING) group_option_store = Gtk.ListStore(GObject.TYPE_STRING, @@ -329,9 +374,8 @@ def _setup_group_switch_option(self): if not found: group_option_combo.set_active(0) - box_group_option.pack_start(group_option_combo, False, True, 0) - self._vbox.pack_start(box_group_option, False, True, 0) - box_group_option.show_all() + box_group_option.append(group_option_combo) + self._vbox.append(box_group_option) group_option_combo.connect('changed', self.__group_switch_changed_cb) @@ -342,12 +386,17 @@ def __group_switch_changed_cb(self, combobox): _APPLY_TIMEOUT, self.__group_switch_timeout_cb, combobox) def __group_switch_timeout_cb(self, combobox): + self.__group_switch_sid = None it = combobox.get_active_iter() + if it is None: + return False model = combobox.get_model() + if model is None: + return False self._selected_group_switch_option = model.get(it, 0)[0] if self._selected_group_switch_option == \ self._keyboard_manager.get_current_option_group(): - return + return False try: self._keyboard_manager.set_option_group( self._selected_group_switch_option) @@ -359,73 +408,79 @@ def __group_switch_timeout_cb(self, combobox): def _setup_layouts(self): """Adds the controls for changing the keyboard layouts""" separator_klayout = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(separator_klayout, False, True, 0) - separator_klayout.show_all() + self._vbox.append(separator_klayout) label_klayout = Gtk.Label(label=_('Keyboard Layout(s)')) - label_klayout.set_alignment(0, 0) - label_klayout.show_all() - self._vbox.pack_start(label_klayout, False, True, 0) + label_klayout.set_halign(Gtk.Align.START) + self._vbox.append(label_klayout) self._klayouts = self._keyboard_manager.get_current_layouts() for i in range(0, self._keyboard_manager.get_max_layouts()): add_remove_box = self.__create_add_remove_box() self._layout_addremovebox_list.append(add_remove_box) - self._layout_table.attach(add_remove_box, 1, 2, i, i + 1) + self._layout_table.attach(add_remove_box, 1, i, 1, 1) layout_combo = LayoutCombo(self._keyboard_manager, i) layout_combo.connect('selection-changed', self.__layout_combo_selection_changed_cb) self._layout_combo_list.append(layout_combo) - self._layout_table.attach(layout_combo, 0, 1, i, i + 1) + self._layout_table.attach(layout_combo, 0, i, 1, 1) if i < len(self._klayouts): - layout_combo.show_all() layout_combo.select_layout(self._klayouts[i]) + else: + layout_combo.set_visible(False) - self._vbox.pack_start(self._layout_table, False, True, 0) - self._layout_table.set_size_request( - self._vbox.get_size_request()[0], -1) - self._layout_table.show() + self._vbox.append(self._layout_table) self._update_klayouts() def __determine_add_remove_box_visibility(self): i = 1 for box in self._layout_addremovebox_list: if not i == len(self._selected_klayouts): - box.props.visible = False + box.set_visible(False) else: - box.show_all() + box.set_visible(True) + add = box.get_first_child() + remove = add.get_next_sibling() + if add: + add.set_visible(True) + if remove: + remove.set_visible(True) + if i == 1: # First row - no need for showing remove btn - add, remove = box.get_children() - remove.props.visible = False + if remove: + remove.set_visible(False) if i == self._keyboard_manager.get_max_layouts(): # Last row - no need for showing add btn - add, remove = box.get_children() - add.props.visible = False + if add: + add.set_visible(False) i += 1 def __create_add_remove_box(self): - """Creates Gtk.Hbox with add/remove buttons""" - add_icon = Icon(icon_name='list-add') + """Creates Gtk.Box with add/remove buttons""" + add_icon = Icon(icon_name='list-add', + pixel_size=style.STANDARD_ICON_SIZE) add_button = Gtk.Button() - add_button.set_image(add_icon) + add_button.add_css_class('flat') + add_button.set_child(add_icon) add_button.connect('clicked', self.__add_button_clicked_cb) - remove_icon = Icon(icon_name='list-remove') + remove_icon = Icon(icon_name='list-remove', + pixel_size=style.STANDARD_ICON_SIZE) remove_button = Gtk.Button() - remove_button.set_image(remove_icon) + remove_button.add_css_class('flat') + remove_button.set_child(remove_icon) remove_button.connect('clicked', self.__remove_button_clicked_cb) - add_remove_box = Gtk.HButtonBox() - add_remove_box.set_layout(Gtk.ButtonBoxStyle.START) + add_remove_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) add_remove_box.set_spacing(10) - add_remove_box.pack_start(add_button, True, True, 0) - add_remove_box.pack_start(remove_button, True, True, 0) + add_remove_box.append(add_button) + add_remove_box.append(remove_button) return add_remove_box @@ -433,19 +488,21 @@ def __layout_combo_selection_changed_cb(self, combo, layout, index): self._update_klayouts() def __add_button_clicked_cb(self, button): - self._layout_combo_list[len(self._selected_klayouts)].show_all() + self._layout_combo_list[len(self._selected_klayouts)].set_visible(True) self._update_klayouts() def __remove_button_clicked_cb(self, button): - self._layout_combo_list[len(self._selected_klayouts) - 1].hide() + self._layout_combo_list[len(self._selected_klayouts) - 1].set_visible(False) self._update_klayouts() def _update_klayouts(self): """Responds to any changes in the keyboard layout options""" self._selected_klayouts = [] for combo in self._layout_combo_list: - if combo.props.visible: - self._selected_klayouts.append(combo.get_layout()) + if combo.get_visible(): + layout = combo.get_layout() + if layout: + self._selected_klayouts.append(layout) self.__determine_add_remove_box_visibility() @@ -455,6 +512,7 @@ def _update_klayouts(self): self.__layout_timeout_cb) def __layout_timeout_cb(self): + self.__layout_sid = None if self._selected_klayouts == \ self._keyboard_manager.get_current_layouts(): return diff --git a/extensions/cpsection/language/__init__.py b/extensions/cpsection/language/__init__.py index 88a28bf7ac..bd58e9d0d1 100644 --- a/extensions/cpsection/language/__init__.py +++ b/extensions/cpsection/language/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ + CLASS = 'Language' ICON = 'module-language' TITLE = _('Language') diff --git a/extensions/cpsection/language/model.py b/extensions/cpsection/language/model.py index 45fc73a77a..d9d6ad2e47 100644 --- a/extensions/cpsection/language/model.py +++ b/extensions/cpsection/language/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, 2008 One Laptop Per Child +# Copyright (C) 2007, 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -242,7 +242,6 @@ } - def read_all_languages(): fdp = subprocess.Popen(['locale', '-av'], stdout=subprocess.PIPE) lines = fdp.stdout.read().decode('utf-8', 'ignore').split('\n') @@ -270,7 +269,7 @@ def read_all_languages(): else: territory = title.split()[-1] if locale_str.endswith('utf8') and len(lang): - locales.append((LANGUAGES[lang], territory, locale_str)) + locales.append((LANGUAGES.get(lang, lang), territory, locale_str)) locales.sort() return locales @@ -389,10 +388,13 @@ def set_languages(languages): def set_languages_list(languages): """Set the system language using a list of preferred languages""" colon = ':' + languages = [lang for lang in languages if lang is not None] + if not languages: + return language_env = colon.join(languages) + lang_env = languages[0].strip('\n') _write_i18n(lang_env, language_env) - # inilialize the docstrings for the language _initialize() diff --git a/extensions/cpsection/language/view.py b/extensions/cpsection/language/view.py index 73fdd9f79c..e9b9f0131b 100644 --- a/extensions/cpsection/language/view.py +++ b/extensions/cpsection/language/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # Copyright (C) 2009, Simon Schampijer # Copyright (C) 2014, Walter Bender # @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import logging from gi.repository import Gtk @@ -76,37 +75,43 @@ def __init__(self, model, alerts): else: self._country_dict[language].append([code, country]) - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) explanation = gettext.gettext('Add languages in the order you prefer.' ' If a translation is not available,' ' the next in the list will be used.') self._text = Gtk.Label(label=explanation) - self._text.set_line_wrap(True) - self._text.set_alignment(0, 0) - self.pack_start(self._text, False, False, 0) - self._text.show() + self._text.set_wrap(True) + self._text.set_halign(Gtk.Align.START) + self.append(self._text) scrolled = Gtk.ScrolledWindow() scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scrolled.show() - self.pack_start(scrolled, True, True, 0) + scrolled.set_vexpand(True) + self.append(scrolled) - self._table = Gtk.Table(rows=2, columns=4, homogeneous=False) - self._table.set_border_width(style.DEFAULT_SPACING * 2) - self._table.show() - scrolled.add_with_viewport(self._table) + self._table = Gtk.Grid() + self._table.set_margin_top(style.DEFAULT_SPACING * 2) + self._table.set_margin_bottom(style.DEFAULT_SPACING * 2) + self._table.set_margin_start(style.DEFAULT_SPACING * 2) + self._table.set_margin_end(style.DEFAULT_SPACING * 2) + scrolled.set_child(self._table) self._lang_alert_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) - self.pack_start(self._lang_alert_box, False, True, 0) + self.append(self._lang_alert_box) self._lang_alert = InlineAlert() - self._lang_alert_box.pack_start(self._lang_alert, True, True, 0) + self._lang_alert.set_hexpand(True) + self._lang_alert_box.append(self._lang_alert) if 'lang' in self.restart_alerts: self._lang_alert.props.msg = self.restart_msg - self._lang_alert.show() - self._lang_alert_box.show() + self._lang_alert.set_visible(True) + else: + self._lang_alert.set_visible(False) self.setup() @@ -117,15 +122,10 @@ def _add_row(self, locale_code=None): self._selected_lang_count += 1 - self._table.resize(self._selected_lang_count * 2, 3) - label = Gtk.Label(label=str(self._selected_lang_count)) - label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) self._labels.append(label) self._attach_to_table(label, 0, 1, self._selected_lang_count * 2 - 1, xpadding=1, ypadding=1) - label.show() locale_language = None locale_country = None @@ -173,8 +173,7 @@ def _add_row(self, locale_code=None): self._language_buttons.append(new_language_button) self._attach_to_table( - new_language_button, 1, 2, self._selected_lang_count * 2 - 1, - yoptions=Gtk.AttachOptions.SHRINK) + new_language_button, 1, 2, self._selected_lang_count * 2 - 1) self._language_widgets.append(new_language_widget) self._attach_to_table(new_language_widget, 1, 2, @@ -184,8 +183,7 @@ def _add_row(self, locale_code=None): self._country_buttons.append(new_country_button) self._attach_to_table( - new_country_button, 2, 3, self._selected_lang_count * 2 - 1, - yoptions=Gtk.AttachOptions.SHRINK) + new_country_button, 2, 3, self._selected_lang_count * 2 - 1) self._country_widgets.append(new_country_widget) self._attach_to_table(new_country_widget, 2, 3, @@ -198,20 +196,17 @@ def _add_row(self, locale_code=None): self._attach_to_table(add_remove_box, 3, 4, self._selected_lang_count * 2 - 1) - add_remove_box.show_all() - if self._selected_lang_count > 1: previous_add_removes = self._add_remove_boxes[-2] - previous_add_removes.hide() + previous_add_removes.set_visible(False) # Hide the Remove button if the new added row is the only # language. elif self._selected_lang_count == 1: - add_button_, remove_button = add_remove_box.get_children() - remove_button.props.visible = False - - new_language_button.show() - new_country_button.show() + add_button_ = add_remove_box.get_first_child() + remove_button = add_button_.get_next_sibling() + if remove_button: + remove_button.set_visible(False) def _build_country_list(self, language, idx=None): country_list = [] @@ -229,15 +224,12 @@ def _build_country_list(self, language, idx=None): def _attach_to_table(self, widget, left, right, above, xpadding=style.DEFAULT_SPACING, - ypadding=style.DEFAULT_SPACING, - yoptions=Gtk.AttachOptions.FILL): - self._table.attach(widget, left, right, - above, - above + 1, - xoptions=Gtk.AttachOptions.FILL, - yoptions=yoptions, - xpadding=xpadding, - ypadding=ypadding) + ypadding=style.DEFAULT_SPACING): + widget.set_margin_start(xpadding) + widget.set_margin_end(xpadding) + widget.set_margin_top(ypadding) + widget.set_margin_bottom(ypadding) + self._table.attach(widget, left, above, right - left, 1) def _delete_last_row(self): """Deletes the last two rows of the table""" @@ -245,41 +237,42 @@ def _delete_last_row(self): self._selected_lang_count -= 1 label = self._labels.pop() - label.destroy() + self._table.remove(label) add_remove_box = self._add_remove_boxes.pop() - add_remove_box.destroy() + self._table.remove(add_remove_box) language_button = self._language_buttons.pop() - language_button.destroy() + self._table.remove(language_button) country_button = self._country_buttons.pop() - country_button.destroy() + self._table.remove(country_button) language_widget = self._language_widgets.pop() - language_widget.destroy() + self._table.remove(language_widget) country_widget = self._country_widgets.pop() - country_widget.destroy() + self._table.remove(country_widget) # Remove language code associated with last row self._country_codes.pop() - self._table.resize(self._selected_lang_count * 2, 3) - if self._selected_lang_count < 1: return - self._add_remove_boxes[-1].show_all() + self._add_remove_boxes[-1].set_visible(True) # Hide or show the Remove button in the new last row, # depending if it is the only language. add_remove_box = self._add_remove_boxes[-1] - add_button_, remove_button = add_remove_box.get_children() + add_button_ = add_remove_box.get_first_child() + remove_button = add_button_.get_next_sibling() if self._selected_lang_count == 1: - remove_button.props.visible = False + if remove_button: + remove_button.set_visible(False) else: - remove_button.props.visible = True + if remove_button: + remove_button.set_visible(True) def setup(self): for locale in self._selected_locales: @@ -291,29 +284,32 @@ def _delete_all_rows(self): def undo(self): self._model.undo() - self._lang_alert.hide() + self._lang_alert.set_visible(False) self._delete_all_rows() def _create_add_remove_box(self): - """Creates Gtk.Hbox with add/remove buttons""" - add_icon = Icon(icon_name='list-add') + """Creates Gtk.Box with add/remove buttons""" + add_icon = Icon(icon_name='list-add', + pixel_size=style.STANDARD_ICON_SIZE) add_button = Gtk.Button() - add_button.set_image(add_icon) + add_button.add_css_class('flat') + add_button.set_child(add_icon) add_button.connect('clicked', self.__add_button_clicked_cb) - remove_icon = Icon(icon_name='list-remove') + remove_icon = Icon(icon_name='list-remove', + pixel_size=style.STANDARD_ICON_SIZE) remove_button = Gtk.Button() - remove_button.set_image(remove_icon) + remove_button.add_css_class('flat') + remove_button.set_child(remove_icon) remove_button.connect('clicked', self.__remove_button_clicked_cb) - add_remove_box = Gtk.HButtonBox() - add_remove_box.set_layout(Gtk.ButtonBoxStyle.START) + add_remove_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) add_remove_box.set_spacing(10) - add_remove_box.pack_start(add_button, True, True, 0) - add_remove_box.pack_start(remove_button, True, True, 0) + add_remove_box.append(add_button) + add_remove_box.append(remove_button) return add_remove_box @@ -325,7 +321,7 @@ def __remove_button_clicked_cb(self, button): self._delete_last_row() self._check_change() - def _language_changed(self, widget, event, item): + def _language_changed(self, gesture, n_press, x, y, item): i = item['index'] for language_key in list(self._language_dict.keys()): @@ -338,11 +334,11 @@ def _language_changed(self, widget, event, item): self._language_buttons[i].button_cb() self._country_buttons[i].set_widget_label( - _translate_country(self._country_dict[language_key][0][1])), + _translate_country(self._country_dict[language_key][0][1])) self._country_codes[i] = self._country_dict[language_key][0][0] old_country_widget = self._country_widgets[i] - old_country_widget.destroy() + self._table.remove(old_country_widget) new_country_widget = set_palette_list(new_country_list) self._country_buttons[i].set_widget(new_country_widget) @@ -353,7 +349,7 @@ def _language_changed(self, widget, event, item): self._update_country(new_country_list[0]) - def _country_changed(self, widget, event, item): + def _country_changed(self, gesture, n_press, x, y, item): self._update_country(item) def _update_country(self, item): @@ -374,7 +370,7 @@ def _check_change(self): self.needs_restart = False if 'lang' in self.restart_alerts: self.restart_alerts.remove('lang') - self._lang_alert.hide() + self._lang_alert.set_visible(False) if self._lang_sid: GLib.source_remove(self._lang_sid) self._model.undo() @@ -393,77 +389,68 @@ def __lang_timeout_cb(self, codes): self.restart_alerts.append('lang') self.needs_restart = True self._lang_alert.props.msg = self.restart_msg - self._lang_alert.show() + self._lang_alert.set_visible(True) except IOError as e: logging.exception('Error writing i18n config %s', e) self.undo() self._lang_alert.props.msg = gettext.gettext( 'Error writing language configuration (%s)') % e - self._lang_alert.show() + self._lang_alert.set_visible(True) self.props.is_valid = False return False -class FilterToolItem(Gtk.ToolItem): +class FilterToolItem(Gtk.Box): def __init__(self, primary_icon, secondary_icon, default_label, widget): - Gtk.ToolItem.__init__(self) - - self.set_size_request(style.GRID_CELL_SIZE * 3, -1) + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) self._widget = widget self._visible = False grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) - self.add(grid) - grid.show() + self.append(grid) self._primary_icon = Icon(icon_name=primary_icon) self._secondary_icon = Icon(icon_name=secondary_icon) self._button = Gtk.Button() - self._button.set_image(self._primary_icon) - self._primary_icon.show() - self._secondary_icon.show() + self._button.add_css_class('flat') + self._button.set_child(self._primary_icon) grid.attach(self._button, 0, 0, 1, 1) - self._button.show() self._button.connect('clicked', self.button_cb) - event_box = Gtk.EventBox() self._label_widget = Gtk.Label() - self._label_widget.set_alignment(0.0, 0.5) + self._label_widget.set_halign(Gtk.Align.START) + self._label_widget.set_valign(Gtk.Align.CENTER) self._label_widget.set_use_markup(True) self.set_widget_label(default_label) - event_box.add(self._label_widget) - self._label_widget.show() - grid.attach(event_box, 1, 0, 1, 1) - event_box.show() - # Allow clicking on the label in addition to the button - event_box.set_events(Gdk.EventMask.TOUCH_MASK) - event_box.connect('touch-event', self._touch_event_cb) + + click = Gtk.GestureClick.new() + click.connect('pressed', self._touch_event_cb) + self._label_widget.add_controller(click) + grid.attach(self._label_widget, 1, 0, 1, 1) def set_widget(self, widget): - self._widget.destroy() self._widget = widget if self._visible: - self._widget.show() + self._widget.set_visible(True) else: - self._widget.hide() + self._widget.set_visible(False) def is_visible(self): return self._visible - def _touch_event_cb(self, widget, event): - if event.type in [Gdk.EventType.TOUCH_BEGIN]: - self.button_cb(widget) + def _touch_event_cb(self, gesture, n, x, y): + self.button_cb() def button_cb(self, widget=None): if self._visible: - self._widget.hide() - self._button.set_image(self._primary_icon) + self._widget.set_visible(False) + self._button.set_child(self._primary_icon) else: - self._widget.show() - self._button.set_image(self._secondary_icon) + self._widget.set_visible(True) + self._button.set_child(self._secondary_icon) self._visible = not self._visible def set_widget_label(self, label): @@ -477,12 +464,7 @@ class BlackLabel(PaletteMenuItem): ''' Label in palette menu item with black text on white background ''' def __init__(self, text_label=None): - PaletteMenuItem.__init__(self, text_label=None, text_maxlen=0) - - self.id_enter_notify_cb = self.connect('enter-notify-event', - self.__enter_notify_cb) - self.id_leave_notify_cb = self.connect('leave-notify-event', - self.__leave_notify_cb) + PaletteMenuItem.__init__(self, text_label="", text_maxlen=0) self.set_label(text_label) def set_label(self, text_label): @@ -491,23 +473,22 @@ def set_label(self, text_label): text_label + '' self.label.set_markup(text) - def __enter_notify_cb(self, widget, event): - self.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_HIGHLIGHT.get_gdk_color()) - - def __leave_notify_cb(self, widget, event): - self.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - def set_palette_list(palette_list): menu_item = BlackLabel(text_label='English') - req2 = menu_item.get_preferred_size()[1] - item_width = req2.width - item_height = req2.height + style.DEFAULT_PADDING - - palette_width = int(Gdk.Screen.width() / 2) - palette_height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 3 + item_width = style.GRID_CELL_SIZE * 3 + item_height = style.GRID_CELL_SIZE + style.DEFAULT_PADDING + + from gi.repository import Gdk + display = Gdk.Display.get_default() + monitors = display.get_monitors() + if monitors.get_n_items() > 0: + geometry = monitors.get_item(0).get_geometry() + palette_width = geometry.width + palette_height = geometry.height + else: + palette_width = 800 + palette_height = 600 nx = min(3, int(palette_width / item_width)) ny = min(8, int(palette_height / item_height), len(palette_list)) @@ -525,14 +506,13 @@ def set_palette_list(palette_list): grid = Gtk.Grid() grid.set_row_spacing(style.DEFAULT_PADDING) grid.set_column_spacing(0) - grid.set_border_width(0) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_size_request(nx * item_width, ny * item_height) - scrolled_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN) - scrolled_window.add_with_viewport(grid) - grid.show() + scrolled_window.set_has_frame(True) + scrolled_window.set_child(grid) + scrolled_window.set_visible(False) x = 0 y = 0 @@ -540,13 +520,14 @@ def set_palette_list(palette_list): for item in palette_list: menu_item = BlackLabel(item['label']) - menu_item.connect('button-release-event', item['callback'], item) + gesture = Gtk.GestureClick.new() + gesture.connect('released', item['callback'], item) + menu_item.add_controller(gesture) + grid.attach(menu_item, x, y, 1, 1) x += 1 if x == nx: x = 0 y += 1 - menu_item.show() - return scrolled_window diff --git a/extensions/cpsection/modemconfiguration/model.py b/extensions/cpsection/modemconfiguration/model.py index 9586189456..abd246adc0 100755 --- a/extensions/cpsection/modemconfiguration/model.py +++ b/extensions/cpsection/modemconfiguration/model.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- encoding: utf-8 -*- # Copyright (C) 2009 Paraguay Educa, Martin Abente # Copyright (C) 2013 Miguel González # @@ -17,7 +17,7 @@ import logging import locale -from xml.etree.cElementTree import ElementTree +from xml.etree.ElementTree import ElementTree from gettext import gettext as _ import dbus @@ -26,7 +26,6 @@ from jarabe.model import network - PROVIDERS_PATH = "/usr/share/mobile-broadband-provider-info/"\ "serviceproviders.xml" @@ -206,8 +205,7 @@ def get(self, country_code): try: return self._data[country_code] except KeyError: - raise KeyError('Not found country name for code "%s"' - % country_code) + return country_code # Fallback to code if name not found class ServiceProvidersParser(object): diff --git a/extensions/cpsection/modemconfiguration/view.py b/extensions/cpsection/modemconfiguration/view.py index 6627ab78f7..ae739beca7 100644 --- a/extensions/cpsection/modemconfiguration/view.py +++ b/extensions/cpsection/modemconfiguration/view.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- encoding: utf-8 -*- # Copyright (C) 2009 Paraguay Educa, Martin Abente # Copyright (C) 2010 Andrés Ambrois # Copyright (C) 2012 Ajay Garg @@ -27,7 +27,6 @@ from jarabe.controlpanel.sectionview import SectionView - from .model import ServiceProvidersError APPLY_TIMEOUT = 1000 @@ -41,23 +40,19 @@ def _create_providers_list_store(items): class EntryWithLabel(Gtk.Box): - __gtype_name__ = 'SugarEntryWithLabel' def __init__(self, label_text): Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) self.label = Gtk.Label(label=label_text) - self.label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - self.label.set_alignment(1, 0.5) - self.pack_start(self.label, False, True, 0) - self.label.show() + self.label.set_halign(Gtk.Align.END) + self.label.set_valign(Gtk.Align.CENTER) + self.append(self.label) self._entry = Gtk.Entry() self._entry.set_max_length(25) self._entry.set_width_chars(25) - self.pack_start(self._entry, False, True, 0) - self._entry.show() + self.append(self._entry) def get_entry(self): return self._entry @@ -73,53 +68,58 @@ def __init__(self, model, alerts=None): self.restart_alerts = alerts self._timeout_sid = 0 - self.set_border_width(style.DEFAULT_SPACING) + self.set_margin_top(style.DEFAULT_SPACING) + self.set_margin_bottom(style.DEFAULT_SPACING) + self.set_margin_start(style.DEFAULT_SPACING) + self.set_margin_end(style.DEFAULT_SPACING) self.set_spacing(style.DEFAULT_SPACING) - self._label_grp = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) - self._combo_grp = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + self._label_grp = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) + self._combo_grp = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) scrolled_win = Gtk.ScrolledWindow() scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self.add(scrolled_win) - scrolled_win.show() + scrolled_win.set_vexpand(True) + self.append(scrolled_win) main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) - main_box.set_border_width(style.DEFAULT_SPACING) - scrolled_win.add_with_viewport(main_box) - main_box.show() + main_box.set_margin_top(style.DEFAULT_SPACING) + main_box.set_margin_bottom(style.DEFAULT_SPACING) + main_box.set_margin_start(style.DEFAULT_SPACING) + main_box.set_margin_end(style.DEFAULT_SPACING) + scrolled_win.set_child(main_box) explanation = _('You will need to provide the following information' ' to set up a mobile broadband connection to a' ' cellular (3G) network.') self._text = Gtk.Label(label=explanation) - self._text.set_line_wrap(True) - self._text.set_alignment(0, 0) - main_box.pack_start(self._text, True, False, 0) - self._text.show() + self._text.set_wrap(True) + self._text.set_halign(Gtk.Align.START) + main_box.append(self._text) self._upper_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) - self._upper_box.set_border_width(style.DEFAULT_SPACING) - main_box.pack_start(self._upper_box, True, False, 0) - self._upper_box.show() + self._upper_box.set_margin_top(style.DEFAULT_SPACING) + self._upper_box.set_margin_bottom(style.DEFAULT_SPACING) + self._upper_box.set_margin_start(style.DEFAULT_SPACING) + self._upper_box.set_margin_end(style.DEFAULT_SPACING) + main_box.append(self._upper_box) country_store = Gtk.ListStore(str, object) - country_store.append([]) + country_store.append(['', None]) provider_store = Gtk.ListStore(str, object) - provider_store.append([]) + provider_store.append(['', None]) plan_store = Gtk.ListStore(str, object) - plan_store.append([]) + plan_store.append(['', None]) self._country_combo = self._add_combo(country_store, _('Country:')) self.provider_combo = self._add_combo(provider_store, _('Provider:')) self.plan_combo = self._add_combo(plan_store, _('Plan:')) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - main_box.pack_start(separator, True, False, 0) - separator.show() + main_box.append(separator) try: self.service_providers = self._model.ServiceProviders() @@ -152,50 +152,48 @@ def __init__(self, model, alerts=None): self.plan_combo.connect("changed", self._plan_selected_cb) lower_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) - lower_box.set_border_width(style.DEFAULT_SPACING) - main_box.pack_start(lower_box, True, False, 0) - lower_box.show() + lower_box.set_margin_top(style.DEFAULT_SPACING) + lower_box.set_margin_bottom(style.DEFAULT_SPACING) + lower_box.set_margin_start(style.DEFAULT_SPACING) + lower_box.set_margin_end(style.DEFAULT_SPACING) + main_box.append(lower_box) self._username_entry = EntryWithLabel(_('Username:')) self._username_entry.entry.connect('changed', self.__entry_changed_cb) self._label_grp.add_widget(self._username_entry.label) self._combo_grp.add_widget(self._username_entry.entry) - lower_box.pack_start(self._username_entry, False, True, 0) - self._username_entry.show() + lower_box.append(self._username_entry) self._password_entry = EntryWithLabel(_('Password:')) self._password_entry.entry.connect('changed', self.__entry_changed_cb) self._label_grp.add_widget(self._password_entry.label) self._combo_grp.add_widget(self._password_entry.entry) - lower_box.pack_start(self._password_entry, False, True, 0) - self._password_entry.show() + lower_box.append(self._password_entry) self._number_entry = EntryWithLabel(_('Number:')) self._number_entry.entry.connect('changed', self.__entry_changed_cb) self._label_grp.add_widget(self._number_entry.label) self._combo_grp.add_widget(self._number_entry.entry) - lower_box.pack_start(self._number_entry, False, True, 0) - self._number_entry.show() + lower_box.append(self._number_entry) self._apn_entry = EntryWithLabel(_('Access Point Name (APN):')) self._apn_entry.entry.connect('changed', self.__entry_changed_cb) self._label_grp.add_widget(self._apn_entry.label) self._combo_grp.add_widget(self._apn_entry.entry) - lower_box.pack_start(self._apn_entry, False, True, 0) - self._apn_entry.show() + lower_box.append(self._apn_entry) self._pin_entry = EntryWithLabel(_('Personal Identity Number (PIN):')) self._pin_entry.entry.connect('changed', self.__entry_changed_cb) self._label_grp.add_widget(self._pin_entry.label) self._combo_grp.add_widget(self._pin_entry.entry) - lower_box.pack_start(self._pin_entry, False, True, 0) - self._pin_entry.show() + lower_box.append(self._pin_entry) self.setup() def _add_combo(self, store, label_text=''): - label = Gtk.Label(label_text) - label.set_alignment(1, 0.5) + label = Gtk.Label(label=label_text) + label.set_halign(Gtk.Align.END) + label.set_valign(Gtk.Align.CENTER) self._label_grp.add_widget(label) combo = Gtk.ComboBox() @@ -209,13 +207,10 @@ def _add_combo(self, store, label_text=''): combo.add_attribute(renderer_text, "text", 0) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) - box.pack_start(label, False, True, 0) - label.show() - box.pack_start(combo, False, True, 0) - combo.show() + box.append(label) + box.append(combo) - self._upper_box.pack_start(box, False, True, 0) - box.show() + self._upper_box.append(box) return combo def undo(self): @@ -291,7 +286,7 @@ def _plan_selected_cb(self, combo): self.service_providers.set_plan(plan.idx) plan = self.service_providers.get_plan() - self._username_entry.entry.set_text(plan.username) - self._password_entry.entry.set_text(plan.password) - self._number_entry.entry.set_text(plan.number) - self._apn_entry.entry.set_text(plan.apn) + self._username_entry.entry.set_text(plan.username or '') + self._password_entry.entry.set_text(plan.password or '') + self._number_entry.entry.set_text(plan.number or '') + self._apn_entry.entry.set_text(plan.apn or '') diff --git a/extensions/cpsection/network/model.py b/extensions/cpsection/network/model.py index bbeb4ff39c..63744e0142 100644 --- a/extensions/cpsection/network/model.py +++ b/extensions/cpsection/network/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2014 Sugar Labs, Frederick Grose # # This program is free software: you can redistribute it and/or modify @@ -23,7 +23,6 @@ from jarabe.model import network - KEYWORDS = ['network', 'jabber', 'radio', 'server', 'social', 'help'] @@ -134,5 +133,4 @@ def set_publish_information(value): settings.set_boolean('publish-gadget', value) return 0 - nm_client = NM.Client() diff --git a/extensions/cpsection/network/view.py b/extensions/cpsection/network/view.py index 778578ef52..4158876b1b 100644 --- a/extensions/cpsection/network/view.py +++ b/extensions/cpsection/network/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # Copyright (C) 2014, Sugar Labs, Frederick Grose # # This program is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gi.repository import Gdk from gettext import gettext as _ @@ -31,6 +30,7 @@ import os + CLASS = 'Network' ICON = 'module-network' TITLE = _('Network') @@ -100,7 +100,6 @@ def prop_changed(widget, param): if not (flags & Gio.SettingsBindFlags.NO_SENSITIVITY): self.bind_writable(key, widget, "sensitive", False) - Gio.Settings.bind_with_convert = bind_with_convert Gio.Settings.__setitem__ = __setitem__ @@ -131,14 +130,13 @@ class SettingBox(Gtk.Box): def __init__(self, name, size_group=None): Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) - label = Gtk.Label(name) - label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - label.set_alignment(1, 0.5) + label = Gtk.Label(label=name) + label.add_css_class('dim-label') + label.set_xalign(1) + label.set_yalign(0.5) if size_group is not None: size_group.add_widget(label) - self.pack_start(label, False, False, 0) - label.show() + self.append(label) class ComboSettingBox(Gtk.Box): @@ -155,14 +153,14 @@ def __init__(self, name, setting, setting_key, Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) setting_box = SettingBox(name, size_group) - self.pack_start(setting_box, False, False, 0) - setting_box.show() + self.append(setting_box) model = Gtk.ListStore(str, str, object) combo_box = Gtk.ComboBox(model=model) combo_box.connect('changed', self.__combo_changed_cb) - setting_box.pack_start(combo_box, True, True, 0) - combo_box.show() + setting_box.append(combo_box) + combo_box.set_hexpand(True) + combo_box.set_vexpand(True) cell_renderer = Gtk.CellRendererText() cell_renderer.props.ellipsize = Pango.EllipsizeMode.MIDDLE @@ -172,8 +170,7 @@ def __init__(self, name, setting, setting_key, combo_box.props.id_column = 1 self._settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._settings_box.show() - self.pack_start(self._settings_box, False, False, 0) + self.append(self._settings_box) for optset in option_sets: model.append(optset) @@ -184,12 +181,14 @@ def __init__(self, name, setting, setting_key, def __combo_changed_cb(self, combobox): giter = combobox.get_active_iter() new_box = combobox.get_model().get(giter, 2)[0] - current_box = self._settings_box.get_children() + current_box = [] + child = self._settings_box.get_first_child() + if child: + current_box.append(child) if current_box: self._settings_box.remove(current_box[0]) - self._settings_box.add(new_box) - new_box.show() + self._settings_box.append(new_box) class OptionalSettingsBox(Gtk.Box): @@ -206,9 +205,10 @@ def __init__(self, name, setting, setting_key, contents_box): check_button = Gtk.CheckButton() check_button.props.label = name check_button.connect('toggled', self.__button_toggled_cb, contents_box) - check_button.show() - self.pack_start(check_button, True, True, 0) - self.pack_start(contents_box, False, False, 0) + self.append(check_button) + check_button.set_hexpand(True) + check_button.set_vexpand(True) + self.append(contents_box) setting.bind(setting_key, check_button, 'active', Gio.SettingsBindFlags.DEFAULT) @@ -224,19 +224,19 @@ class HostPortSettingBox(SettingBox): def __init__(self, name, alert, setting, size_group=None): SettingBox.__init__(self, name, size_group) - self.pack_start(alert, False, True, 0) - alert.hide() + self.append(alert) + alert.set_visible(False) host_entry = Gtk.Entry() - self.pack_start(host_entry, True, True, 0) - host_entry.show() + self.append(host_entry) + host_entry.set_hexpand(True) + host_entry.set_vexpand(True) setting.bind('host', host_entry, 'text', Gio.SettingsBindFlags.DEFAULT) # port number 0 means n/a port_entry = NumberEntry() - self.pack_start(port_entry, False, False, 0) - port_entry.show() + self.append(port_entry) setting.bind_with_convert('port', port_entry, "text", @@ -257,8 +257,9 @@ def __init__(self, name, setting, setting_key, size_group=None, SettingBox.__init__(self, name, size_group) entry = Gtk.Entry() - self.pack_start(entry, True, True, 0) - entry.show() + self.append(entry) + entry.set_hexpand(True) + entry.set_vexpand(True) if password_field: entry.set_visibility(False) @@ -276,8 +277,9 @@ def __init__(self, name, setting, setting_key, SettingBox.__init__(self, name, size_group) entry = Gtk.Entry() - self.pack_start(entry, True, True, 0) - entry.show() + self.append(entry) + entry.set_hexpand(True) + entry.set_vexpand(True) if password_field: entry.set_visibility(False) @@ -306,179 +308,179 @@ def __init__(self, model, alerts): self._proxy_settings = {} self._proxy_inline_alerts = {} - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + + self.set_margin_end(style.DEFAULT_SPACING * 2) + + self.set_margin_top(style.DEFAULT_SPACING * 2) + + self.set_margin_bottom(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) - group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) self._radio_alert_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) scrolled = Gtk.ScrolledWindow() scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.add(scrolled) - scrolled.show() + scrolled.set_hexpand(True) + scrolled.set_vexpand(True) + self.append(scrolled) workspace = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - scrolled.add_with_viewport(workspace) - workspace.show() + viewport = Gtk.Viewport() + viewport.set_child(workspace) + scrolled.set_child(viewport) separator_wireless = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - workspace.pack_start(separator_wireless, False, True, 0) - separator_wireless.show() + workspace.append(separator_wireless) label_wireless = Gtk.Label(label=_('Wireless')) - label_wireless.set_alignment(0, 0) - workspace.pack_start(label_wireless, False, True, 0) - label_wireless.show() + label_wireless.set_xalign(0) + label_wireless.set_yalign(0) + workspace.append(label_wireless) box_wireless = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_wireless.set_border_width(style.DEFAULT_SPACING * 2) + box_wireless.set_margin_start(style.DEFAULT_SPACING * 2) + box_wireless.set_margin_end(style.DEFAULT_SPACING * 2) + box_wireless.set_margin_top(style.DEFAULT_SPACING * 2) + box_wireless.set_margin_bottom(style.DEFAULT_SPACING * 2) box_wireless.set_spacing(style.DEFAULT_SPACING) radio_info = Gtk.Label(label=_('The wireless radio may be turned' ' off to save battery life.')) - radio_info.set_alignment(0, 0) - radio_info.set_line_wrap(True) - radio_info.show() - box_wireless.pack_start(radio_info, False, True, 0) + radio_info.set_xalign(0) + radio_info.set_yalign(0) + radio_info.set_wrap(True) + box_wireless.append(radio_info) box_radio = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) self._button = Gtk.CheckButton() - self._button.set_alignment(0, 0) - box_radio.pack_start(self._button, False, True, 0) - self._button.show() + self._button.set_halign(Gtk.Align.START) + self._button.set_valign(Gtk.Align.START) + box_radio.append(self._button) label_radio = Gtk.Label(label=_('Radio')) - label_radio.set_alignment(0, 0.5) - box_radio.pack_start(label_radio, False, True, 0) - label_radio.show() + label_radio.set_xalign(0) + label_radio.set_yalign(0.5) + box_radio.append(label_radio) - box_wireless.pack_start(box_radio, False, True, 0) - box_radio.show() + box_wireless.append(box_radio) self._radio_alert = InlineAlert() - self._radio_alert_box.pack_start(self._radio_alert, False, True, 0) - box_radio.pack_end(self._radio_alert_box, False, True, 0) - self._radio_alert_box.show() + self._radio_alert.set_visible(False) + self._radio_alert_box.append(self._radio_alert) + box_radio.append(self._radio_alert_box) + self._radio_alert_box.set_hexpand(True) + self._radio_alert_box.set_halign(Gtk.Align.END) if 'radio' in self.restart_alerts: self._radio_alert.props.msg = self.restart_msg - self._radio_alert.show() - wireless_info = Gtk.Label( - label=_('Discard wireless connections if' + wireless_info = Gtk.Label(label=_('Discard wireless connections if' ' you have trouble connecting to the network')) - wireless_info.set_alignment(0, 0) - wireless_info.set_line_wrap(True) - wireless_info.show() - box_wireless.pack_start(wireless_info, False, True, 0) + wireless_info.set_xalign(0) + wireless_info.set_yalign(0) + wireless_info.set_wrap(True) + box_wireless.append(wireless_info) box_clear_wireless = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) self._clear_wireless_button = Gtk.Button() self._clear_wireless_button.set_label( _('Discard wireless connections')) - box_clear_wireless.pack_start( - self._clear_wireless_button, False, True, 0) + box_clear_wireless.append( + self._clear_wireless_button) if not self._model.have_wireless_networks(): self._clear_wireless_button.set_sensitive(False) - self._clear_wireless_button.show() - box_wireless.pack_start(box_clear_wireless, False, True, 0) - box_clear_wireless.show() + box_wireless.append(box_clear_wireless) - workspace.pack_start(box_wireless, False, True, 0) - box_wireless.show() + workspace.append(box_wireless) separator_mesh = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - workspace.pack_start(separator_mesh, False, False, 0) - separator_mesh.show() + workspace.append(separator_mesh) label_mesh = Gtk.Label(label=_('Collaboration')) - label_mesh.set_alignment(0, 0) - workspace.pack_start(label_mesh, False, True, 0) - label_mesh.show() + label_mesh.set_xalign(0) + label_mesh.set_yalign(0) + workspace.append(label_mesh) box_mesh = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_mesh.set_border_width(style.DEFAULT_SPACING * 2) + box_mesh.set_margin_start(style.DEFAULT_SPACING * 2) + box_mesh.set_margin_end(style.DEFAULT_SPACING * 2) + box_mesh.set_margin_top(style.DEFAULT_SPACING * 2) + box_mesh.set_margin_bottom(style.DEFAULT_SPACING * 2) box_mesh.set_spacing(style.DEFAULT_SPACING) - server_info = Gtk.Label(_("The server is the equivalent of what" + server_info = Gtk.Label(label=_("The server is the equivalent of what" " room you are in; people on the same server" " will be able to see each other, even when" " they aren't on the same network.")) - server_info.set_alignment(0, 0) - server_info.set_line_wrap(True) - box_mesh.pack_start(server_info, False, True, 0) - server_info.show() + server_info.set_xalign(0) + server_info.set_yalign(0) + server_info.set_wrap(True) + box_mesh.append(server_info) box_server = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) label_server = Gtk.Label(label=_('Server:')) - label_server.set_alignment(1, 0.5) - label_server.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_server.pack_start(label_server, False, True, 0) + label_server.add_css_class('dim-label') + label_server.set_xalign(1) + label_server.set_yalign(0.5) + box_server.append(label_server) group.add_widget(label_server) - label_server.show() self._entry = Gtk.Entry() self._entry.set_alignment(0) - self._entry.set_size_request(int(Gdk.Screen.width() / 3), -1) - box_server.pack_start(self._entry, False, True, 0) - self._entry.show() - box_mesh.pack_start(box_server, False, True, 0) - box_server.show() - - social_help_info = Gtk.Label( - _('Social Help is a forum that lets you connect with developers' + self._entry.set_hexpand(True) + box_server.append(self._entry) + box_mesh.append(box_server) + + social_help_info = Gtk.Label(label=_('Social Help is a forum that lets you connect with developers' ' and discuss Sugar Activities. Changing servers means' ' discussions will happen in a different place with' ' different people.')) - social_help_info.set_alignment(0, 0) - social_help_info.set_line_wrap(True) - box_mesh.pack_start(social_help_info, False, True, 0) - social_help_info.show() + social_help_info.set_xalign(0) + social_help_info.set_yalign(0) + social_help_info.set_wrap(True) + box_mesh.append(social_help_info) social_help_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) social_help_label = Gtk.Label(label=_('Social Help Server:')) - social_help_label.set_alignment(1, 0.5) - social_help_label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - social_help_box.pack_start(social_help_label, False, True, 0) + social_help_label.add_css_class('dim-label') + social_help_label.set_xalign(1) + social_help_label.set_yalign(0.5) + social_help_box.append(social_help_label) group.add_widget(social_help_label) - social_help_label.show() self._social_help_entry = Gtk.Entry() self._social_help_entry.set_alignment(0) - self._social_help_entry.set_size_request( - int(Gdk.Screen.width() / 3), -1) - social_help_box.pack_start(self._social_help_entry, False, True, 0) - self._social_help_entry.show() - box_mesh.pack_start(social_help_box, False, True, 0) - social_help_box.show() + self._social_help_entry.set_hexpand(True) + social_help_box.append(self._social_help_entry) + box_mesh.append(social_help_box) - workspace.pack_start(box_mesh, False, True, 0) - box_mesh.show() + workspace.append(box_mesh) separator_proxy = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - workspace.pack_start(separator_proxy, False, False, 0) - separator_proxy.show() + workspace.append(separator_proxy) self._add_proxy_section(workspace) self.setup() def _add_proxy_section(self, workspace): - label_proxy = Gtk.Label(_('Proxy')) - label_proxy.set_alignment(0, 0) - workspace.pack_start(label_proxy, False, True, 0) - label_proxy.show() + label_proxy = Gtk.Label(label=_('Proxy')) + label_proxy.set_xalign(0) + label_proxy.set_yalign(0) + workspace.append(label_proxy) box_proxy = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_proxy.set_border_width(style.DEFAULT_SPACING * 2) + box_proxy.set_margin_start(style.DEFAULT_SPACING * 2) + box_proxy.set_margin_end(style.DEFAULT_SPACING * 2) + box_proxy.set_margin_top(style.DEFAULT_SPACING * 2) + box_proxy.set_margin_bottom(style.DEFAULT_SPACING * 2) box_proxy.set_spacing(style.DEFAULT_SPACING) - workspace.pack_start(box_proxy, False, True, 0) - box_proxy.show() + workspace.append(box_proxy) self._proxy_alert = Alert() self._proxy_alert.props.title = _('Error') self._proxy_alert.props.msg = _('Proxy settings cannot be verified') - box_proxy.pack_start(self._proxy_alert, False, False, 0) + box_proxy.append(self._proxy_alert) self._proxy_alert.connect('response', self._response_cb) - self._proxy_alert.hide() + self._proxy_alert.set_visible(False) # GSettings schemas for proxy: schemas = ['org.sugarlabs.system.proxy', @@ -500,7 +502,7 @@ def _add_proxy_section(self, workspace): self._proxy_settings[schema] = proxy_setting self._proxy_inline_alerts[schema] = alert - size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) automatic_proxy_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) manual_proxy_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) @@ -514,8 +516,7 @@ def _add_proxy_section(self, workspace): _('Method:'), self._proxy_settings['org.sugarlabs.system.proxy'], 'mode', option_sets, size_group) - box_proxy.pack_start(box_mode, False, False, 0) - box_mode.show() + box_proxy.append(box_mode) url_box = StringSettingBox( _('Configuration URL:'), @@ -523,46 +524,47 @@ def _add_proxy_section(self, workspace): 'autoconfig-url', size_group) - automatic_proxy_box.pack_start(url_box, True, True, 0) - url_box.show() + automatic_proxy_box.append(url_box) + + url_box.set_hexpand(True) + + url_box.set_vexpand(True) wpad_help_text = _('Web Proxy Autodiscovery is used when a' ' Configuration URL is not provided. This is not' ' recommended for untrusted public networks.') - automatic_proxy_help = Gtk.Label(wpad_help_text) - automatic_proxy_help.set_alignment(0, 0) - automatic_proxy_help.set_line_wrap(True) - automatic_proxy_help.show() - automatic_proxy_box.pack_start(automatic_proxy_help, True, True, 0) + automatic_proxy_help = Gtk.Label(label=wpad_help_text) + automatic_proxy_help.set_xalign(0) + automatic_proxy_help.set_yalign(0) + automatic_proxy_help.set_wrap(True) + automatic_proxy_box.append(automatic_proxy_help) + automatic_proxy_help.set_hexpand(True) + automatic_proxy_help.set_vexpand(True) # HTTP Section schema = 'org.sugarlabs.system.proxy.http' box_http = HostPortSettingBox( _('HTTP Proxy:'), self._proxy_inline_alerts[schema], self._proxy_settings[schema], size_group) - manual_proxy_box.pack_start(box_http, False, False, 0) - box_http.show() + manual_proxy_box.append(box_http) auth_contents_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) auth_box = OptionalSettingsBox( _('Use authentication'), self._proxy_settings[schema], 'use-authentication', auth_contents_box) - manual_proxy_box.pack_start(auth_box, False, False, 0) - auth_box.show() + manual_proxy_box.append(auth_box) proxy_http_setting = Gio.Settings.new(schema) proxy_http_setting.delay() box_username = StringSettingBox( _('Username:'), self._proxy_settings[schema], 'authentication-user', size_group) - auth_contents_box.pack_start(box_username, False, False, 0) - box_username.show() + auth_contents_box.append(box_username) box_password = StringSettingBox( _('Password:'), self._proxy_settings[schema], 'authentication-password', size_group, password_field=True) - auth_contents_box.pack_start(box_password, False, False, 0) - box_password.show() + auth_contents_box.append(box_password) # HTTPS Section schema = 'org.sugarlabs.system.proxy.https' @@ -570,8 +572,7 @@ def _add_proxy_section(self, workspace): _('HTTPS Proxy:'), self._proxy_inline_alerts[schema], self._proxy_settings[schema], size_group) - manual_proxy_box.pack_start(box_https, False, False, 0) - box_https.show() + manual_proxy_box.append(box_https) # FTP Section schema = 'org.sugarlabs.system.proxy.ftp' @@ -579,8 +580,7 @@ def _add_proxy_section(self, workspace): _('FTP Proxy:'), self._proxy_inline_alerts[schema], self._proxy_settings[schema], size_group) - manual_proxy_box.pack_start(box_ftp, False, False, 0) - box_ftp.show() + manual_proxy_box.append(box_ftp) # SOCKS Section schema = 'org.sugarlabs.system.proxy.socks' @@ -588,15 +588,13 @@ def _add_proxy_section(self, workspace): _('SOCKS Proxy:'), self._proxy_inline_alerts[schema], self._proxy_settings[schema], size_group) - manual_proxy_box.pack_start(box_socks, False, False, 0) - box_socks.show() + manual_proxy_box.append(box_socks) box_ignore = StringSettingBox_with_convert( _('Ignore Hosts:'), self._proxy_settings['org.sugarlabs.system.proxy'], 'ignore-hosts', type_as_to_string, string_to_type_as, size_group) - manual_proxy_box.pack_start(box_ignore, False, False, 0) - box_ignore.show() + manual_proxy_box.append(box_ignore) def setup(self): self._entry.set_text(self._start_jabber) @@ -606,7 +604,7 @@ def setup(self): radio_state = self._model.get_radio() except self._model.ReadError as detail: self._radio_alert.props.msg = detail - self._radio_alert.show() + self._radio_alert.set_visible(True) else: self._button.set_active(radio_state) @@ -619,16 +617,16 @@ def setup(self): 'clicked', self.__wireless_configuration_reset_cb) def _response_cb(self, alert, response_id): - if response_id is Gtk.ResponseType.APPLY: - self._proxy_alert.hide() + if response_id == Gtk.ResponseType.APPLY: + self._proxy_alert.set_visible(False) self._apply_proxy_settings() self.show_restart_alert = True self.emit('add-alert') - elif response_id is Gtk.ResponseType.CANCEL: + elif response_id == Gtk.ResponseType.CANCEL: self.undo() self._proxy_alert.remove_button(Gtk.ResponseType.APPLY) self._proxy_alert.remove_button(Gtk.ResponseType.CANCEL) - self._proxy_alert.hide() + self._proxy_alert.set_visible(False) self.emit('set-toolbar-sensitivity', True) def _ping_servers(self): @@ -642,7 +640,7 @@ def _ping_servers(self): non_blank_host_name_counter += 1 response = os.system("ping -c 1 -W 1 " + hostname) if (response): - self._proxy_inline_alerts[schema].show() + self._proxy_inline_alerts[schema].set_visible(True) response_to_return = False if non_blank_host_name_counter == 0: response_to_return = False @@ -651,7 +649,6 @@ def _ping_servers(self): def _verify_settings(self): self._proxy_alert.props.title = _('Please Wait!') self._proxy_alert.props.msg = _('Proxy settings are being verified.') - self._proxy_alert.show() flag_all_true = True g_proxy_schema = self._proxy_settings['org.sugarlabs.system.proxy'] g_mode = Gio.Settings.get_string(g_proxy_schema, 'mode') @@ -664,7 +661,7 @@ def _verify_settings(self): flag_all_true = self._ping_servers() if flag_all_true: self.show_restart_alert = True - self._proxy_alert.hide() + self._proxy_alert.set_visible(False) self._apply_proxy_settings() else: self._proxy_alert.props.title = _('Error!') @@ -675,11 +672,9 @@ def _verify_settings(self): icon = Icon(icon_name='dialog-cancel') self._proxy_alert.add_button(Gtk.ResponseType.APPLY, _('Break my internet'), icon) - icon.show() icon = Icon(icon_name='dialog-ok') self._proxy_alert.add_button(Gtk.ResponseType.CANCEL, _('Reset'), icon) - icon.show() def _apply_proxy_settings(self): for setting in list(self._proxy_settings.values()): @@ -703,11 +698,11 @@ def apply(self): def undo(self): self._button.disconnect(self._radio_change_handler) - self._radio_alert.hide() + self._radio_alert.set_visible(False) for setting in list(self._proxy_settings.values()): setting.revert() for alert in list(self._proxy_inline_alerts.values()): - alert.hide() + alert.set_visible(False) def _validate(self): if self._radio_valid: @@ -721,6 +716,7 @@ def __radio_toggled_cb(self, widget, data=None): self._model.set_radio(radio_state) except self._model.ReadError as detail: self._radio_alert.props.msg = detail + self._radio_alert.set_visible(True) self._radio_valid = False else: self._radio_valid = True diff --git a/extensions/cpsection/power/__init__.py b/extensions/cpsection/power/__init__.py index 0311aeadc0..aee59ebd57 100644 --- a/extensions/cpsection/power/__init__.py +++ b/extensions/cpsection/power/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,13 +13,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ import os POWERD_FLAG_DIR = '/etc/powerd/flags' -if os.access(POWERD_FLAG_DIR, os.W_OK): - CLASS = 'Power' + + +CLASS = 'Power' ICON = 'module-power' TITLE = _('Power') KEYWORDS = ['automatic', 'power', 'suspend', 'battery'] diff --git a/extensions/cpsection/power/model.py b/extensions/cpsection/power/model.py index 34ee824189..4804773c32 100644 --- a/extensions/cpsection/power/model.py +++ b/extensions/cpsection/power/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -40,9 +40,7 @@ def __str__(self): def using_powerd(): - # directory exists if powerd running, and it's recent - # enough to be controllable. - return os.access(POWERD_FLAG_DIR, os.W_OK) + return False def get_automatic_pm(): @@ -75,15 +73,21 @@ def set_automatic_pm(enabled): return # ohmd - bus = dbus.SystemBus() - proxy = bus.get_object(OHM_SERVICE_NAME, OHM_SERVICE_PATH) - keystore = dbus.Interface(proxy, OHM_SERVICE_IFACE) + try: + bus = dbus.SystemBus() + proxy = bus.get_object(OHM_SERVICE_NAME, OHM_SERVICE_PATH) + keystore = dbus.Interface(proxy, OHM_SERVICE_IFACE) + + if enabled == 'on' or enabled == 1: + keystore.SetKey('suspend.automatic_pm', 1) + elif enabled == 'off' or enabled == 0: + keystore.SetKey('suspend.automatic_pm', 0) + except dbus.exceptions.DBusException: + _logger.warning('Could not connect to %s. Falling back to GSettings.' % OHM_SERVICE_NAME) if enabled == 'on' or enabled == 1: - keystore.SetKey('suspend.automatic_pm', 1) enabled = True elif enabled == 'off' or enabled == 0: - keystore.SetKey('suspend.automatic_pm', 0) enabled = False else: raise ValueError(_('Error in automatic pm argument, use on/off.')) diff --git a/extensions/cpsection/power/view.py b/extensions/cpsection/power/view.py index 590d2fbc72..da14e359b9 100644 --- a/extensions/cpsection/power/view.py +++ b/extensions/cpsection/power/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gettext import gettext as _ @@ -32,65 +31,63 @@ def __init__(self, model, alerts): self._automatic_pm_valid = True self._automatic_pm_change_handler = None - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + + self.set_margin_end(style.DEFAULT_SPACING * 2) + + self.set_margin_top(style.DEFAULT_SPACING * 2) + + self.set_margin_bottom(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) - group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) self._automatic_pm_alert_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) separator_pm = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self.pack_start(separator_pm, False, True, 0) - separator_pm.show() + self.append(separator_pm) label_pm = Gtk.Label(label=_('Power management')) - label_pm.set_alignment(0, 0) - self.pack_start(label_pm, False, True, 0) - label_pm.show() + label_pm.set_xalign(0) + label_pm.set_yalign(0) + self.append(label_pm) box_pm = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - box_pm.set_border_width(style.DEFAULT_SPACING * 2) + box_pm.set_margin_start(style.DEFAULT_SPACING * 2) + box_pm.set_margin_end(style.DEFAULT_SPACING * 2) + box_pm.set_margin_top(style.DEFAULT_SPACING * 2) + box_pm.set_margin_bottom(style.DEFAULT_SPACING * 2) box_pm.set_spacing(style.DEFAULT_SPACING) box_automatic_pm = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) - label_automatic_pm = Gtk.Label( - label=_('Automatic power management (increases battery life)')) - label_automatic_pm.set_alignment(0, 0.5) + label_automatic_pm = Gtk.Label(label=_('Automatic power management (increases battery life)')) + label_automatic_pm.set_xalign(0) + label_automatic_pm.set_yalign(0.5) self._automatic_button = Gtk.CheckButton() - self._automatic_button.set_alignment(0, 0) - box_automatic_pm.pack_start(self._automatic_button, False, True, 0) - box_automatic_pm.pack_start(label_automatic_pm, False, True, 0) - self._automatic_button.show() - label_automatic_pm.show() + self._automatic_button.set_halign(Gtk.Align.START) + self._automatic_button.set_valign(Gtk.Align.START) + box_automatic_pm.append(self._automatic_button) + box_automatic_pm.append(label_automatic_pm) group.add_widget(label_automatic_pm) - box_pm.pack_start(box_automatic_pm, False, True, 0) - box_automatic_pm.show() + box_pm.append(box_automatic_pm) self._automatic_pm_alert = InlineAlert() label_automatic_pm_error = Gtk.Label() group.add_widget(label_automatic_pm_error) - self._automatic_pm_alert_box.pack_start(label_automatic_pm_error, - expand=False, fill=True, - padding=0) - label_automatic_pm_error.show() - self._automatic_pm_alert_box.pack_start(self._automatic_pm_alert, - expand=False, fill=True, - padding=0) - box_pm.pack_end(self._automatic_pm_alert_box, False, True, 0) - self._automatic_pm_alert_box.show() + self._automatic_pm_alert_box.append(label_automatic_pm_error) + self._automatic_pm_alert_box.append(self._automatic_pm_alert) + box_pm.append(self._automatic_pm_alert_box) if 'automatic_pm' in self.restart_alerts: self._automatic_pm_alert.props.msg = self.restart_msg - self._automatic_pm_alert.show() - self.pack_start(box_pm, False, True, 0) - box_pm.show() + self.append(box_pm) self.setup() def setup(self): try: automatic_state = self._model.get_automatic_pm() - except Exception as detail: - self._automatic_pm_alert.props.msg = detail - self._automatic_pm_alert.show() + except self._model.ReadError as detail: + self._automatic_pm_alert.props.msg = str(detail) + self._automatic_pm_alert.set_visible(True) else: self._automatic_button.set_active(automatic_state) @@ -101,8 +98,9 @@ def setup(self): def undo(self): self._automatic_button.disconnect(self._automatic_pm_change_handler) - self._model.undo() - self._automatic_pm_alert.hide() + if hasattr(self._model, 'undo'): + self._model.undo() + self._automatic_pm_alert.set_visible(False) def _validate(self): if self._automatic_pm_valid: @@ -116,7 +114,8 @@ def __automatic_pm_toggled_cb(self, widget, data=None): self._model.set_automatic_pm(state) except Exception as detail: print(detail) - self._automatic_pm_alert.props.msg = detail + self._automatic_pm_alert.props.msg = str(detail) + self._automatic_pm_alert.set_visible(True) else: self._automatic_pm_valid = True diff --git a/extensions/cpsection/updater/__init__.py b/extensions/cpsection/updater/__init__.py index ad06fc3776..f86f97eb81 100644 --- a/extensions/cpsection/updater/__init__.py +++ b/extensions/cpsection/updater/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ + CLASS = 'ActivityUpdater' ICON = 'module-updater' TITLE = _('Software Update') diff --git a/extensions/cpsection/updater/view.py b/extensions/cpsection/updater/view.py index 216db55006..9fb5acdcf3 100644 --- a/extensions/cpsection/updater/view.py +++ b/extensions/cpsection/updater/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, One Laptop Per Child +# Copyright (C) 2008, One Laptop Per Child # Copyright (C) 2009, Tomeu Vizoso # # This program is free software: you can redistribute it and/or modify @@ -14,13 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ from gettext import ngettext import locale import logging -from gi.repository import GObject +from gi.repository import GLib from gi.repository import Gtk from sugar4.graphics import style @@ -49,28 +48,28 @@ def __init__(self, model, alerts): self.__finished_cb) self.set_spacing(style.DEFAULT_SPACING) - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) self._top_label = Gtk.Label() - self._top_label.set_line_wrap(True) + self._top_label.set_wrap(True) self._top_label.set_justify(Gtk.Justification.LEFT) self._top_label.props.xalign = 0 - self.pack_start(self._top_label, False, True, 0) - self._top_label.show() + self.append(self._top_label) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self.pack_start(separator, False, True, 0) - separator.show() + self.append(separator) self._bottom_label = Gtk.Label() - self._bottom_label.set_line_wrap(True) + self._bottom_label.set_wrap(True) self._bottom_label.set_justify(Gtk.Justification.LEFT) self._bottom_label.props.xalign = 0 self._bottom_label.set_markup( _('Software updates correct errors, eliminate security ' 'vulnerabilities, and provide new features.')) - self.pack_start(self._bottom_label, False, True, 0) - self._bottom_label.show() + self.append(self._bottom_label) self._update_box = None self._progress_pane = None @@ -82,7 +81,7 @@ def __init__(self, model, alerts): updater.STATE_UPDATING): self._switch_to_progress_pane() self._progress_pane.set_message(_('Update in progress...')) - self.connect('destroy', self.__destroy_cb) + self.connect('unrealize', self.__destroy_cb) def __destroy_cb(self, widget): self._model.disconnect(self._id_progresss) @@ -92,10 +91,10 @@ def __destroy_cb(self, widget): self._model.clean() def _switch_to_update_box(self, updates): - if self._update_box in self.get_children(): + if self._update_box and self._update_box.get_parent() == self: return - if self._progress_pane in self.get_children(): + if self._progress_pane and self._progress_pane.get_parent() == self: self.remove(self._progress_pane) self._progress_pane = None @@ -108,11 +107,12 @@ def _switch_to_update_box(self, updates): 'clicked', self.__install_button_clicked_cb) - self.pack_start(self._update_box, expand=True, fill=True, padding=0) - self._update_box.show() + self.append(self._update_box) + self._update_box.set_hexpand(True) + self._update_box.set_vexpand(True) def _switch_to_progress_pane(self): - if self._progress_pane in self.get_children(): + if self._progress_pane and self._progress_pane.get_parent() == self: return if self._model.get_state() == updater.STATE_CHECKING: @@ -121,7 +121,7 @@ def _switch_to_progress_pane(self): top_message = _('Installing updates...') self._top_label.set_markup('%s' % top_message) - if self._update_box in self.get_children(): + if self._update_box and self._update_box.get_parent() == self: self.remove(self._update_box) self._update_box = None @@ -131,16 +131,15 @@ def _switch_to_progress_pane(self): 'clicked', self.__cancel_button_clicked_cb) - self.pack_start( - self._progress_pane, expand=True, fill=False, padding=0) - self._progress_pane.show() + self.append(self._progress_pane) + self._progress_pane.set_hexpand(True) def _clear_center(self): - if self._progress_pane in self.get_children(): + if self._progress_pane and self._progress_pane.get_parent() == self: self.remove(self._progress_pane) self._progress_pane = None - if self._update_box in self.get_children(): + if self._update_box and self._update_box.get_parent() == self: self.remove(self._update_box) self._update_box = None @@ -169,7 +168,7 @@ def __updates_available_cb(self, model, updates): 'You can install %s updates', available_updates) top_message = top_message % available_updates - top_message = GObject.markup_escape_text(top_message) + top_message = GLib.markup_escape_text(top_message) self._top_label.set_markup('%s' % top_message) @@ -206,7 +205,7 @@ def __finished_cb(self, model, installed_updates, failed_updates, top_message = ngettext('%s update was installed', '%s updates were installed', num_installed) top_message = top_message % num_installed - top_message = GObject.markup_escape_text(top_message) + top_message = GLib.markup_escape_text(top_message) self._top_label.set_markup('%s' % top_message) self._clear_center() @@ -221,28 +220,33 @@ class ProgressPane(Gtk.Box): def __init__(self): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) self.set_spacing(style.DEFAULT_PADDING) - self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_margin_start(style.DEFAULT_SPACING * 2) + self.set_margin_end(style.DEFAULT_SPACING * 2) + self.set_margin_top(style.DEFAULT_SPACING * 2) + self.set_margin_bottom(style.DEFAULT_SPACING * 2) self._progress = Gtk.ProgressBar() - self.pack_start(self._progress, True, True, 0) - self._progress.show() + self.append(self._progress) + self._progress.set_hexpand(True) + self._progress.set_vexpand(True) self._label = Gtk.Label() - self._label.set_line_wrap(True) + self._label.set_wrap(True) self._label.set_property('xalign', 0.5) - self._label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_BUTTON_GREY.get_gdk_color()) - self.pack_start(self._label, True, True, 0) - self._label.show() - alignment_box = Gtk.Alignment.new(xalign=0.5, yalign=0.5, - xscale=0, yscale=0) - self.pack_start(alignment_box, True, True, 0) - alignment_box.show() + self.append(self._label) + self._label.set_hexpand(True) + self._label.set_vexpand(True) + + alignment_box = Gtk.Box() + alignment_box.set_halign(Gtk.Align.CENTER) + alignment_box.set_valign(Gtk.Align.CENTER) + self.append(alignment_box) + alignment_box.set_hexpand(True) + alignment_box.set_vexpand(True) - self.cancel_button = Gtk.Button(stock=Gtk.STOCK_CANCEL) - alignment_box.add(self.cancel_button) - self.cancel_button.show() + self.cancel_button = Gtk.Button(label=_('Cancel')) + alignment_box.append(self.cancel_button) def set_message(self, message): self._label.set_text(message) @@ -261,36 +265,35 @@ def __init__(self, updates): scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy( Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self.pack_start(scrolled_window, True, True, 0) - scrolled_window.show() + self.append(scrolled_window) + scrolled_window.set_hexpand(True) + scrolled_window.set_vexpand(True) self._update_list = UpdateList(updates) self._update_list.props.model.connect('row-changed', self.__row_changed_cb) - scrolled_window.add(self._update_list) - self._update_list.show() + scrolled_window.set_child(self._update_list) bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) bottom_box.set_spacing(style.DEFAULT_SPACING) - self.pack_start(bottom_box, False, True, 0) - bottom_box.show() + self.append(bottom_box) self._size_label = Gtk.Label() self._size_label.props.xalign = 0 self._size_label.set_justify(Gtk.Justification.LEFT) - bottom_box.pack_start(self._size_label, True, True, 0) - self._size_label.show() + bottom_box.append(self._size_label) + self._size_label.set_hexpand(True) + self._size_label.set_vexpand(True) - self.refresh_button = Gtk.Button(stock=Gtk.STOCK_REFRESH) - bottom_box.pack_start(self.refresh_button, False, True, 0) - self.refresh_button.show() + self.refresh_button = Gtk.Button(label=_('Refresh')) + bottom_box.append(self.refresh_button) - self.install_button = Gtk.Button(_('Install selected')) - self.install_button.props.image = Icon( - icon_name='emblem-downloads', - pixel_size=style.SMALL_ICON_SIZE) - bottom_box.pack_start(self.install_button, False, True, 0) - self.install_button.show() + self.install_button = Gtk.Button() + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) + box.append(Icon(icon_name='emblem-downloads', pixel_size=style.SMALL_ICON_SIZE)) + box.append(Gtk.Label(label=_('Install selected'))) + self.install_button.set_child(box) + bottom_box.append(self.install_button) self._update_total_size_label() @@ -326,7 +329,7 @@ class UpdateList(Gtk.TreeView): def __init__(self, updates): list_model = UpdateListModel(updates) - Gtk.TreeView.__init__(self, list_model) + Gtk.TreeView.__init__(self, model=list_model) self.set_reorderable(False) self.set_enable_search(False) @@ -335,7 +338,6 @@ def __init__(self, updates): toggle_renderer = Gtk.CellRendererToggle() toggle_renderer.props.activatable = True toggle_renderer.props.xpad = style.DEFAULT_PADDING - toggle_renderer.props.indicator_size = style.zoom(26) toggle_renderer.connect('toggled', self.__toggled_cb) toggle_column = Gtk.TreeViewColumn() diff --git a/extensions/cpsection/webaccount/__init__.py b/extensions/cpsection/webaccount/__init__.py index a5e249c0a1..222012ad3d 100644 --- a/extensions/cpsection/webaccount/__init__.py +++ b/extensions/cpsection/webaccount/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender +# Copyright (C) 2013, Walter Bender # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,12 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ from pkgutil import extend_path __path__ = extend_path(__path__, __name__) + CLASS = 'WebServicesConfig' ICON = 'module-webaccount' TITLE = _('Web Services') diff --git a/extensions/cpsection/webaccount/view.py b/extensions/cpsection/webaccount/view.py index 130f3b8778..6af6749f52 100644 --- a/extensions/cpsection/webaccount/view.py +++ b/extensions/cpsection/webaccount/view.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales +# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ from gi.repository import GLib @@ -54,7 +53,6 @@ def __init__(self, model, alerts): fill_color=style.COLOR_TRANSPARENT.get_svg()) grid.attach(icon, 0, 0, 1, 1) - icon.show() label = Gtk.Label() label.set_justify(Gtk.Justification.CENTER) @@ -65,23 +63,25 @@ def __init__(self, model, alerts): _('No web services are installed.\n' 'Please visit %s for more details.' % 'http://wiki.sugarlabs.org/go/WebServices')))) - label.show() grid.attach(label, 0, 1, 1, 1) - alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1) - alignment.add(grid) - grid.show() - - self.add(alignment) - alignment.show() + grid.set_halign(Gtk.Align.CENTER) + grid.set_valign(Gtk.Align.CENTER) + grid.set_hexpand(True) + grid.set_vexpand(True) + + self.append(grid) return grid.set_row_spacing(style.DEFAULT_SPACING * 4) grid.set_column_spacing(style.DEFAULT_SPACING * 4) - grid.set_border_width(style.DEFAULT_SPACING * 2) + grid.set_margin_start(style.DEFAULT_SPACING * 2) + grid.set_margin_end(style.DEFAULT_SPACING * 2) + grid.set_margin_top(style.DEFAULT_SPACING * 2) + grid.set_margin_bottom(style.DEFAULT_SPACING * 2) grid.set_column_homogeneous(True) - width = Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE + width = 800 - 2 * style.GRID_CELL_SIZE nx = int(width / (style.GRID_CELL_SIZE + style.DEFAULT_SPACING * 4)) self._service_config_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) @@ -91,49 +91,46 @@ def __init__(self, model, alerts): for service in services: service_grid = Gtk.Grid() icon = CanvasIcon(icon_name=service.get_icon_name()) - icon.show() - service_grid.attach(icon, x, y, 1, 1) + service_grid.attach(icon, 0, 0, 1, 1) - icon.connect('activate', service.config_service_cb, None, - self._service_config_box) + gesture = Gtk.GestureClick() + gesture.connect('pressed', self._on_service_clicked, service) + icon.add_controller(gesture) label = Gtk.Label() label.set_justify(Gtk.Justification.CENTER) name = get_service_name(service) label.set_markup(name) - service_grid.attach(label, x, y + 1, 1, 1) - label.show() + service_grid.attach(label, 0, 1, 1, 1) grid.attach(service_grid, x, y, 1, 1) - service_grid.show() x += 1 if x == nx: x = 0 y += 1 - alignment = Gtk.Alignment.new(0.5, 0, 0, 0) - alignment.add(grid) - grid.show() + grid.set_halign(Gtk.Align.CENTER) + grid.set_valign(Gtk.Align.START) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.pack_start(alignment, False, False, 0) - alignment.show() + vbox.append(grid) scrolled = Gtk.ScrolledWindow() - vbox.pack_start(scrolled, True, True, 0) + vbox.append(scrolled) + scrolled.set_hexpand(True) + scrolled.set_vexpand(True) - self.add(vbox) + self.append(vbox) scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scrolled.show() workspace = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - scrolled.add_with_viewport(workspace) - workspace.show() + scrolled.set_child(workspace) - workspace.add(self._service_config_box) - workspace.show_all() - vbox.show() + workspace.append(self._service_config_box) def undo(self): pass + + def _on_service_clicked(self, gesture, n_press, x, y, service): + service.config_service_cb(gesture.get_widget(), None, self._service_config_box) diff --git a/extensions/cpsection/webaccount/web_service.py b/extensions/cpsection/webaccount/web_service.py index 998acb120d..3a56f18e09 100644 --- a/extensions/cpsection/webaccount/web_service.py +++ b/extensions/cpsection/webaccount/web_service.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales +# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ class WebService(): def get_icon_name(self): - raise "Not implemented" + raise NotImplementedError("Not implemented") def config_service_cb(self, widget, event, container): - raise "Not implemented" + raise NotImplementedError("Not implemented") diff --git a/extensions/deviceicon/audio.py b/extensions/deviceicon/audio.py index e1b7c12487..f53b74ecc8 100644 --- a/extensions/deviceicon/audio.py +++ b/extensions/deviceicon/audio.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 Martin Dengler +# Copyright (C) 2008 Martin Dengler # Copyright (C) 2014 Emil Dudev # Copyright (C) 2014 Walter Bender # Copyright (C) 2014 Gonzalo Odiard @@ -98,68 +98,62 @@ def __init__(self, text, icon_name, device): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) self._device = device - self._ok_icon = Icon(icon_name='dialog-ok') - self._cancel_icon = Icon(icon_name='dialog-cancel') + self._button_icon = Icon(icon_name='dialog-cancel') icon = Icon(pixel_size=style.SMALL_ICON_SIZE) icon.props.icon_name = icon_name icon.props.xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), style.COLOR_BUTTON_GREY.get_svg())) - icon.show() - label = Gtk.Label(text) - label.show() + label = Gtk.Label(label=text) grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) grid.attach(icon, 0, 0, 1, 1) grid.attach(label, 1, 0, 1, 1) - grid.show() - alignment = Gtk.Alignment() - alignment.set(0.5, 0, 0, 0) - alignment.add(grid) - alignment.show() - - self.add(alignment) + alignment = Gtk.Box() + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.START) + alignment.append(grid) + self.append(alignment) adjustment = Gtk.Adjustment( value=device.props.level, lower=0, upper=100 + sound.VOLUME_STEP, - step_incr=sound.VOLUME_STEP, - page_incr=sound.VOLUME_STEP, + step_increment=sound.VOLUME_STEP, + page_increment=sound.VOLUME_STEP, page_size=sound.VOLUME_STEP) self._adjustment = adjustment - hscale = Gtk.HScale() - hscale.props.draw_value = False + hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + hscale.set_draw_value(False) hscale.set_adjustment(adjustment) hscale.set_digits(0) hscale.set_size_request(style.GRID_CELL_SIZE * 4, -1) - hscale.show() button = Gtk.Button() - button.props.relief = Gtk.ReliefStyle.NONE - button.props.focus_on_click = False + button.set_has_frame(False) + button.set_focusable(False) + button.set_child(self._button_icon) button.connect('clicked', self.__muted_clicked_cb) - button.show() self._button = button grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) grid.attach(hscale, 0, 0, 1, 1) grid.attach(button, 1, 0, 1, 1) - grid.show() - alignment = Gtk.Alignment() - alignment.set(0.5, 0, 0, 0) - alignment.set_padding(0, 0, style.DEFAULT_SPACING, - style.DEFAULT_SPACING) - alignment.add(grid) - alignment.show() + alignment = Gtk.Box() + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.START) + alignment.set_margin_bottom(style.DEFAULT_SPACING) + alignment.set_margin_start(style.DEFAULT_SPACING) + alignment.set_margin_end(style.DEFAULT_SPACING) + alignment.append(grid) - self.add(alignment) + self.append(alignment) self._adjustment_hid = \ self._adjustment.connect('value-changed', @@ -172,9 +166,9 @@ def update_level(self): def update_muted(self): if self._device.props.muted: - self._button.set_image(self._ok_icon) + self._button_icon.props.icon_name = 'dialog-ok' else: - self._button.set_image(self._cancel_icon) + self._button_icon.props.icon_name = 'dialog-cancel' def __level_adjusted_cb(self, device, data=None): value = self._adjustment.props.value @@ -209,21 +203,17 @@ def __init__(self, primary_text, output_text, input_text, output_model, self._capture_manager = AudioManagerWidget(input_text, 'media-audio-input', input_model) - self._capture_manager.show() separator = PaletteMenuItemSeparator() - separator.show() self._speaker_manager = AudioManagerWidget(output_text, 'speaker-100', output_model) - self._speaker_manager.show() self._box = PaletteMenuBox() self._box.append_item(self._capture_manager, 0, 0) self._box.append_item(separator, 0, 0) self._box.append_item(self._speaker_manager, 0, 0) - self._box.show() self.set_content(self._box) diff --git a/extensions/deviceicon/battery.py b/extensions/deviceicon/battery.py index 32ee4f15fb..9bb469169d 100644 --- a/extensions/deviceicon/battery.py +++ b/extensions/deviceicon/battery.py @@ -38,7 +38,6 @@ from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.model.session import get_session_manager - _ICON_NAME = 'battery' _STATUS_CHARGING = 0 @@ -87,10 +86,10 @@ def _update_info(self): current_level = self._model.props.level xo_color = self._color badge_name = None - self.show() + self.set_visible(True) if not self._model.props.present: - self.hide() + self.set_visible(False) status = _STATUS_NOT_PRESENT elif self._model.props.charging: @@ -128,22 +127,22 @@ def __init__(self, primary_text): self._progress_widget = PaletteMenuBox() self.set_content(self._progress_widget) - self._progress_widget.show() inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) self._progress_widget.append_item(inner_box, vertical_padding=0) - inner_box.show() self._progress_bar = Gtk.ProgressBar() self._progress_bar.set_size_request( style.zoom(style.GRID_CELL_SIZE * 4), -1) - inner_box.pack_start(self._progress_bar, True, True, 0) - self._progress_bar.show() + self._progress_bar.set_hexpand(True) + self._progress_bar.set_vexpand(True) + inner_box.append(self._progress_bar) self._status_label = Gtk.Label() - inner_box.pack_start(self._status_label, True, True, 0) - self._status_label.show() + self._status_label.set_hexpand(True) + self._status_label.set_vexpand(True) + inner_box.append(self._status_label) def set_info(self, percentage, seconds, status): self._level = percentage @@ -305,14 +304,11 @@ def __check_capacity(self): def setup(tray): - bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM) - up_proxy = bus.get_object('org.freedesktop.UPower', - '/org/freedesktop/UPower') - upower = dbus.Interface(up_proxy, 'org.freedesktop.UPower') - - for device_path in upower.EnumerateDevices(): - device = bus.get_object('org.freedesktop.UPower', device_path) - device_prop_iface = dbus.Interface(device, dbus.PROPERTIES_IFACE) - device_type = device_prop_iface.Get(_UP_DEVICE_IFACE, 'Type') - if device_type == _UP_TYPE_BATTERY: - tray.add_device(DeviceView(device_path)) + try: + client = UPowerGlib.Client.new_full(None) + devices = client.get_devices() + for device in devices: + if device.kind == UPowerGlib.DeviceKind.BATTERY: + tray.add_device(DeviceView(device.get_object_path())) + except Exception as e: + logging.warning("Could not setup battery device: %s", e) diff --git a/extensions/deviceicon/display.py b/extensions/deviceicon/display.py index 55a13ac18e..0904a1a925 100644 --- a/extensions/deviceicon/display.py +++ b/extensions/deviceicon/display.py @@ -91,27 +91,25 @@ def __init__(self, text, icon_name): icon.props.icon_name = icon_name icon.props.xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), style.COLOR_BUTTON_GREY.get_svg())) - icon.show() - label = Gtk.Label(text) - label.show() + label = Gtk.Label(label=text) grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) grid.attach(icon, 0, 0, 1, 1) grid.attach(label, 1, 0, 1, 1) - grid.show() - alignment = Gtk.Alignment() - alignment.set(0.5, 0, 0, 0) - alignment.add(grid) - alignment.show() - self.add(alignment) + alignment = Gtk.Box() + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.START) + alignment.append(grid) + self.append(alignment) - alignment = Gtk.Alignment() - alignment.set(0.5, 0, 0, 0) - alignment.set_padding(0, 0, style.DEFAULT_SPACING, - style.DEFAULT_SPACING) + alignment = Gtk.Box() + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.START) + alignment.set_margin_start(style.DEFAULT_SPACING) + alignment.set_margin_end(style.DEFAULT_SPACING) self._model = brightness.get_instance() self._model_changed_hid = \ @@ -123,8 +121,8 @@ def __init__(self, text, icon_name): value=self._model.get_brightness(), lower=0, upper=self._model.get_max_brightness() + 1, - step_incr=self._model.get_step_amount(), - page_incr=self._model.get_step_amount(), + step_increment=self._model.get_step_amount(), + page_increment=self._model.get_step_amount(), page_size=self._model.get_step_amount()) self._adjustment = adjustment @@ -132,23 +130,20 @@ def __init__(self, text, icon_name): self._adjustment_hid = \ self._adjustment.connect('value-changed', self.__adjusted_cb) - hscale = Gtk.HScale() - hscale.props.draw_value = False + hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + hscale.set_draw_value(False) hscale.set_adjustment(adjustment) hscale.set_digits(0) hscale.set_size_request(style.GRID_CELL_SIZE * 4, -1) - alignment.add(hscale) - hscale.show() + alignment.append(hscale) else: self._progress_bar = Gtk.ProgressBar() self._progress_bar.set_size_request( style.zoom(style.GRID_CELL_SIZE * 4), -1) - alignment.props.top_padding = style.DEFAULT_PADDING - alignment.add(self._progress_bar) - self._progress_bar.show() + alignment.set_margin_top(style.DEFAULT_PADDING) + alignment.append(self._progress_bar) - alignment.show() - self.add(alignment) + self.append(alignment) def __brightness_changed_cb(self, model, value): self.update(value) @@ -187,10 +182,8 @@ def __init__(self): self._screenshot = PaletteMenuItem(_('Take a screenshot')) icon = Icon(icon_name='camera-external', pixel_size=style.SMALL_ICON_SIZE) - self._screenshot.set_image(icon) - icon.show() + self._screenshot.set_icon_widget(icon) self._screenshot.connect('activate', self.__screenshot_cb) - self._screenshot.show() self._box = PaletteMenuBox() @@ -200,7 +193,6 @@ def __init__(self): self._add_brightness_manager() self._box.append_item(self._screenshot, 0, 0) - self._box.show() self.set_content(self._box) self.connect('popup', self.__popup_cb) @@ -208,10 +200,8 @@ def __init__(self): def _add_brightness_manager(self): self._brightness_manager = BrightnessManagerWidget(_('Brightness'), 'brightness-100') - self._brightness_manager.show() separator = PaletteMenuItemSeparator() - separator.show() self._box.append_item(self._brightness_manager, 0, 0) self._box.append_item(separator, 0, 0) @@ -222,14 +212,14 @@ def __popup_cb(self, palette): def __screenshot_cb(self, palette): frame_ = frame.get_view() - frame_.hide() + frame_.set_visible(False) GLib.idle_add(self.__take_screenshot_cb, frame_) def __take_screenshot_cb(self, frame_): if frame_.is_visible(): return True take_screenshot() - frame_.show() + frame_.set_visible(True) return False diff --git a/extensions/deviceicon/frame.py b/extensions/deviceicon/frame.py index d5495d8c39..36a7e1e196 100644 --- a/extensions/deviceicon/frame.py +++ b/extensions/deviceicon/frame.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012, OLPC +# Copyright (C) 2012, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,6 +16,8 @@ import logging from gettext import gettext as _ +from gi.repository import Gtk + from sugar4 import profile from sugar4.graphics.tray import TrayIcon from sugar4.graphics.palette import Palette @@ -46,7 +48,9 @@ def __init__(self): TrayIcon.__init__(self, icon_name=_ICON_NAME, xo_color=self._color) self._input_method = Maliit.InputMethod() - self.connect('button-release-event', self.__button_release_event_cb) + click_controller = Gtk.GestureClick() + click_controller.connect('released', self.__button_release_event_cb) + self.add_controller(click_controller) self.set_palette_invoker(FrameWidgetInvoker(self)) def create_palette(self): @@ -54,10 +58,10 @@ def create_palette(self): palette.set_group_id('frame') return palette - def __button_release_event_cb(self, widget, event): + def __button_release_event_cb(self, gesture, n_press, x, y): self._input_method.show() frame = jarabe.frame.get_view() - frame.hide() + frame.set_visible(False) def setup(tray): diff --git a/extensions/deviceicon/network.py b/extensions/deviceicon/network.py index 55ab523db4..cbe3c5e66d 100644 --- a/extensions/deviceicon/network.py +++ b/extensions/deviceicon/network.py @@ -43,7 +43,6 @@ from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.view.pulsingicon import PulsingIcon - IP_ADDRESS_TEXT_TEMPLATE = _('IP address: %s') _GSM_STATE_NOT_READY = 0 @@ -87,38 +86,46 @@ def __init__(self, primary_text): self._channel_label = Gtk.Label() self._channel_label.props.xalign = 0.0 - self._channel_label.show() self._ip_address_label = Gtk.Label() self._ip_address_label.props.xalign = 0.0 - self._ip_address_label.show() self._info = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._disconnect_item = PaletteMenuItem(_('Disconnect')) icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='media-eject') - self._disconnect_item.set_image(icon) + self._disconnect_item.set_icon_widget(icon) self._disconnect_item.connect('activate', self.__disconnect_activate_cb) - self._info.add(self._disconnect_item) + self._info.append(self._disconnect_item) separator = PaletteMenuItemSeparator() - self._info.pack_start(separator, True, True, 0) + self._info.append(separator) def _padded(child, xalign=0, yalign=0.5): - padder = Gtk.Alignment.new(xalign=xalign, yalign=yalign, - xscale=1, yscale=0.33) - padder.set_padding(style.DEFAULT_SPACING, - style.DEFAULT_SPACING, - style.DEFAULT_SPACING, - style.DEFAULT_SPACING) - padder.add(child) + padder = Gtk.Box() + padder.set_hexpand(True) + padder.set_vexpand(True) + padder.set_margin_top(style.DEFAULT_SPACING) + padder.set_margin_bottom(style.DEFAULT_SPACING) + padder.set_margin_start(style.DEFAULT_SPACING) + padder.set_margin_end(style.DEFAULT_SPACING) + child.set_hexpand(True) + if xalign == 0: + child.set_halign(Gtk.Align.START) + elif xalign == 0.5: + child.set_halign(Gtk.Align.CENTER) + if yalign == 0: + child.set_valign(Gtk.Align.START) + elif yalign == 0.5: + child.set_valign(Gtk.Align.CENTER) + padder.append(child) return padder - self._info.pack_start(_padded(self._channel_label), True, True, 0) - self._info.pack_start(_padded(self._ip_address_label), True, True, 0) - self._info.show_all() + self._info.append(_padded(self._channel_label)) + self._info.append(_padded(self._ip_address_label)) + self._info.set_visible(True) def set_connecting(self): self.props.secondary_text = _('Connecting...') @@ -127,7 +134,7 @@ def _set_connected(self, iaddress): self.set_content(self._info) self.props.secondary_text = _('Connected') self._set_ip_address(iaddress) - self._disconnect_item.show() + self._disconnect_item.set_visible(True) def set_connected_with_frequency(self, frequency, iaddress): self._set_connected(iaddress) @@ -140,7 +147,7 @@ def set_connected_with_channel(self, channel, iaddress): def set_disconnected(self): self.props.primary_text = _('No wireless connection') self.props.secondary_text = '' - self._disconnect_item.hide() + self._disconnect_item.set_visible(False) self.set_content(None) def __disconnect_activate_cb(self, menuitem): @@ -174,25 +181,34 @@ def __init__(self): self._speed_label = Gtk.Label() self._speed_label.props.xalign = 0.0 - self._speed_label.show() self._ip_address_label = Gtk.Label() self._info = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) def _padded(child, xalign=0, yalign=0.5): - padder = Gtk.Alignment.new(xalign=xalign, yalign=yalign, - xscale=1, yscale=0.33) - padder.set_padding(style.DEFAULT_SPACING, - style.DEFAULT_SPACING, - style.DEFAULT_SPACING, - style.DEFAULT_SPACING) - padder.add(child) + padder = Gtk.Box() + padder.set_hexpand(True) + padder.set_vexpand(True) + padder.set_margin_top(style.DEFAULT_SPACING) + padder.set_margin_bottom(style.DEFAULT_SPACING) + padder.set_margin_start(style.DEFAULT_SPACING) + padder.set_margin_end(style.DEFAULT_SPACING) + child.set_hexpand(True) + if xalign == 0: + child.set_halign(Gtk.Align.START) + elif xalign == 0.5: + child.set_halign(Gtk.Align.CENTER) + if yalign == 0: + child.set_valign(Gtk.Align.START) + elif yalign == 0.5: + child.set_valign(Gtk.Align.CENTER) + padder.append(child) return padder - self._info.pack_start(_padded(self._speed_label), True, True, 0) - self._info.pack_start(_padded(self._ip_address_label), True, True, 0) - self._info.show_all() + self._info.append(_padded(self._speed_label)) + self._info.append(_padded(self._ip_address_label)) + self._info.set_visible(True) self.set_content(self._info) self.props.secondary_text = _('Connected') @@ -231,58 +247,65 @@ def __init__(self): self._toggle_state_item = PaletteMenuItem('') self._toggle_state_item.connect('activate', self.__toggle_state_cb) - self.info_box.pack_start(self._toggle_state_item, True, True, 0) - self._toggle_state_item.show() + self.info_box.append(self._toggle_state_item) self.error_title_label = Gtk.Label(label="") - self.error_title_label.set_alignment(0, 0.5) - self.error_title_label.set_line_wrap(True) - self.info_box.pack_start(self.error_title_label, True, True, 0) - self.error_description_label = Gtk.Label(label="") - self.error_description_label.set_alignment(0, 0.5) - self.error_description_label.set_line_wrap(True) - self.info_box.pack_start(self.error_description_label, True, True, 0) + self.error_title_label.set_halign(Gtk.Align.START) + self.error_title_label.set_valign(Gtk.Align.CENTER) + self.error_title_label.set_wrap(True) + self.error_title_label.set_hexpand(True) + self.error_title_label.set_vexpand(True) + self.info_box.append(self.error_title_label) + self.error_description_label = Gtk.Label() + self.error_description_label.set_halign(Gtk.Align.START) + self.error_description_label.set_valign(Gtk.Align.CENTER) + self.error_description_label.set_wrap(True) + self.error_description_label.set_hexpand(True) + self.error_description_label.set_vexpand(True) + self.info_box.append(self.error_description_label) self.connection_info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) icon = Icon(icon_name='data-upload', pixel_size=style.SMALL_ICON_SIZE) - self.connection_info_box.pack_start(icon, True, True, 0) - icon.show() + self.connection_info_box.append(icon) self._data_label_up = Gtk.Label() self._data_label_up.props.xalign = 0.0 label_alignment = self._add_widget_with_padding(self._data_label_up) - self.connection_info_box.pack_start(label_alignment, True, True, 0) - self._data_label_up.show() - label_alignment.show() + self.connection_info_box.append(label_alignment) icon = Icon(icon_name='data-download', pixel_size=style.SMALL_ICON_SIZE) - self.connection_info_box.pack_start(icon, True, True, 0) - icon.show() + self.connection_info_box.append(icon) self._data_label_down = Gtk.Label() self._data_label_down.props.xalign = 0.0 label_alignment = self._add_widget_with_padding(self._data_label_down) - self.connection_info_box.pack_start(label_alignment, True, True, 0) - self._data_label_down.show() - label_alignment.show() - - self.info_box.pack_start(self.connection_info_box, True, True, 0) + self.connection_info_box.append(label_alignment) - self.info_box.show() + self.info_box.append(self.connection_info_box) self.set_content(self.info_box) self.update_state(_GSM_STATE_NOT_READY) def _add_widget_with_padding(self, child, xalign=0, yalign=0.5): - alignment = Gtk.Alignment.new(xalign=xalign, yalign=yalign, - xscale=1, yscale=0.33) - alignment.set_padding(style.DEFAULT_SPACING, - style.DEFAULT_SPACING, - style.DEFAULT_SPACING, - style.DEFAULT_SPACING) - alignment.add(child) - return alignment + padder = Gtk.Box() + padder.set_hexpand(True) + padder.set_vexpand(True) + padder.set_margin_top(style.DEFAULT_SPACING) + padder.set_margin_bottom(style.DEFAULT_SPACING) + padder.set_margin_start(style.DEFAULT_SPACING) + padder.set_margin_end(style.DEFAULT_SPACING) + child.set_hexpand(True) + if xalign == 0: + child.set_halign(Gtk.Align.START) + elif xalign == 0.5: + child.set_halign(Gtk.Align.CENTER) + if yalign == 0: + child.set_valign(Gtk.Align.START) + elif yalign == 0.5: + child.set_valign(Gtk.Align.CENTER) + padder.append(child) + return padder def update_state(self, state, reason=0): self._current_state = state @@ -299,14 +322,14 @@ def _update_label_and_text(self, reason=0): self.props.secondary_text = _('Disconnected') icon = Icon(icon_name='dialog-ok', pixel_size=style.SMALL_ICON_SIZE) - self._toggle_state_item.set_image(icon) + self._toggle_state_item.set_icon_widget(icon) elif self._current_state == _GSM_STATE_CONNECTING: self._toggle_state_item.set_label(_('Cancel')) self.props.secondary_text = _('Connecting...') icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - self._toggle_state_item.set_image(icon) + self._toggle_state_item.set_icon_widget(icon) elif self._current_state == _GSM_STATE_CONNECTED: self._failed_connection = False @@ -314,7 +337,7 @@ def _update_label_and_text(self, reason=0): self.update_connection_time() icon = Icon(icon_name='media-eject', pixel_size=style.SMALL_ICON_SIZE) - self._toggle_state_item.set_image(icon) + self._toggle_state_item.set_icon_widget(icon) elif self._current_state == _GSM_STATE_FAILED: message_error = self._get_error_by_nm_reason(reason) @@ -327,8 +350,8 @@ def __toggle_state_cb(self, menuitem): if self._current_state == _GSM_STATE_NOT_READY: pass elif self._current_state == _GSM_STATE_DISCONNECTED: - self.error_title_label.hide() - self.error_description_label.hide() + self.error_title_label.set_visible(False) + self.error_description_label.set_visible(False) self.emit('gsm-connect') elif self._current_state == _GSM_STATE_CONNECTING: self.emit('gsm-disconnect') @@ -345,11 +368,11 @@ def add_alert(self, error, suggestion): title = _('Error: %s') % error self.error_title_label.set_markup('%s' % title) - self.error_title_label.show() + self.error_title_label.set_visible(True) message = _('Suggestion: %s') % suggestion self.error_description_label.set_text(message) - self.error_description_label.show() + self.error_description_label.set_visible(True) def update_connection_time(self, connection_time=None): if connection_time is not None: @@ -411,7 +434,6 @@ def __init__(self, device): self._icon.props.base_color = self._inactive_color self.set_icon_widget(self._icon) - self._icon.show() self.set_palette_invoker(FrameWidgetInvoker(self)) self.props.hide_tooltip_on_click = False @@ -620,7 +642,6 @@ def __init__(self, device, state): self._icon.props.base_color = self._inactive_color self.set_icon_widget(self._icon) - self._icon.show() self.set_palette_invoker(FrameWidgetInvoker(self)) self.props.hide_tooltip_on_click = False @@ -847,7 +868,7 @@ def _update_state(self, state, old_state, reason): self._palette.update_connection_time() self._palette.update_stats(0, 0) if self._palette is not None: - self._palette.connection_info_box.show() + self._palette.connection_info_box.set_visible(True) elif state is network.NM_DEVICE_STATE_DISCONNECTED: gsm_state = _GSM_STATE_DISCONNECTED @@ -855,7 +876,7 @@ def _update_state(self, state, old_state, reason): if self._connection_time_handler is not None: GLib.source_remove(self._connection_time_handler) if self._palette is not None: - self._palette.connection_info_box.hide() + self._palette.connection_info_box.set_visible(False) elif state in [network.NM_DEVICE_STATE_UNMANAGED, network.NM_DEVICE_STATE_UNAVAILABLE, diff --git a/extensions/deviceicon/speech.py b/extensions/deviceicon/speech.py index 77fb8bd745..9cc259e30b 100644 --- a/extensions/deviceicon/speech.py +++ b/extensions/deviceicon/speech.py @@ -28,7 +28,6 @@ from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.model import speech - _ICON_NAME = 'microphone' @@ -62,18 +61,16 @@ def __init__(self, primary_text, manager): box = PaletteMenuBox() self.set_content(box) - box.show() self._play_icon = Icon(icon_name='player_play') self._pause_icon = Icon(icon_name='player_pause') self._play_pause_menu = PaletteMenuItem( icon_name='player_play', text_label=_('Say selected text'), - accelerator='Shift+Alt+S') - self._play_pause_menu.set_image(self._play_icon) + accelerator='s') + self._play_pause_menu.set_icon_widget(self._play_icon) self._play_pause_menu.connect('activate', self.__play_activated_cb) box.append_item(self._play_pause_menu) - self._play_pause_menu.show() self._stop_menu = PaletteMenuItem(icon_name='player_stop', text_label=_('Stop playback')) @@ -83,37 +80,32 @@ def __init__(self, primary_text, manager): separator = PaletteMenuItemSeparator() box.append_item(separator) - separator.show() - pitch_label = Gtk.Label(_('Pitch')) + pitch_label = Gtk.Label(label=_('Pitch')) box.append_item(pitch_label, vertical_padding=0) - pitch_label.show() self._adj_pitch = Gtk.Adjustment(value=self._manager.get_pitch(), lower=self._manager.MIN_PITCH, upper=self._manager.MAX_PITCH) - hscale_pitch = Gtk.HScale() + hscale_pitch = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) hscale_pitch.set_adjustment(self._adj_pitch) hscale_pitch.set_draw_value(False) box.append_item(hscale_pitch, vertical_padding=0) - hscale_pitch.show() - rate_label = Gtk.Label(_('Rate')) + rate_label = Gtk.Label(label=_('Rate')) box.append_item(rate_label, vertical_padding=0) - rate_label.show() self._adj_rate = Gtk.Adjustment(value=self._manager.get_rate(), lower=self._manager.MIN_RATE, upper=self._manager.MAX_RATE) - hscale_rate = Gtk.HScale() + hscale_rate = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) hscale_rate.set_adjustment(self._adj_rate) hscale_rate.set_draw_value(False) box.append_item(hscale_rate, vertical_padding=0) - hscale_rate.show() self._adj_pitch.connect('value-changed', self.__adj_pitch_changed_cb) self._adj_rate.connect('value-changed', self.__adj_rate_changed_cb) @@ -137,17 +129,17 @@ def __stop_activated_cb(self, widget): def _set_menu_state(self, manager, signal): if signal == 'play': - self._play_pause_menu.set_image(self._pause_icon) + self._play_pause_menu.set_icon_widget(self._pause_icon) self._play_pause_menu.set_label(_('Pause playback')) self._stop_menu.set_sensitive(True) elif signal == 'pause': - self._play_pause_menu.set_image(self._play_icon) + self._play_pause_menu.set_icon_widget(self._play_icon) self._play_pause_menu.set_label(_('Say selected text')) self._stop_menu.set_sensitive(True) elif signal == 'stop': - self._play_pause_menu.set_image(self._play_icon) + self._play_pause_menu.set_icon_widget(self._play_icon) self._play_pause_menu.set_label(_('Say selected text')) self._stop_menu.set_sensitive(False) diff --git a/extensions/deviceicon/touchpad.py b/extensions/deviceicon/touchpad.py index 0e2866553a..6bab675318 100644 --- a/extensions/deviceicon/touchpad.py +++ b/extensions/deviceicon/touchpad.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010, Walter Bender, Sugar Labs +# Copyright (C) 2010, Walter Bender, Sugar Labs # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gettext import gettext as _ import os @@ -52,7 +51,9 @@ def __init__(self): TrayIcon.__init__(self, icon_name=icon_name, xo_color=color) self.set_palette_invoker(FrameWidgetInvoker(self)) - self.connect('button-release-event', self.__button_release_event_cb) + click_controller = Gtk.GestureClick() + click_controller.connect('released', self.__button_release_event_cb) + self.add_controller(click_controller) def create_palette(self): """ Create a palette for this icon; called by the Sugar framework @@ -61,11 +62,11 @@ def create_palette(self): self.palette.set_group_id('frame') return self.palette - def __button_release_event_cb(self, widget, event): + def __button_release_event_cb(self, gesture, n_press, x, y): """ Callback for button release event; used to invoke touchpad-mode change. """ self.palette.toggle_mode() - return True + gesture.set_state(Gtk.EventSequenceState.CLAIMED) class ResourcePalette(Palette): @@ -81,10 +82,9 @@ def __init__(self, primary_text, icon): self.set_content(vbox) self._status_text = Gtk.Label() - vbox.pack_start(self._status_text, True, True, style.DEFAULT_PADDING) - self._status_text.show() - - vbox.show() + self._status_text.set_margin_top(style.DEFAULT_PADDING) + self._status_text.set_margin_bottom(style.DEFAULT_PADDING) + vbox.append(self._status_text) self._mode = _read_touchpad_mode() self._update() diff --git a/extensions/deviceicon/volume.py b/extensions/deviceicon/volume.py index 4aeeaf34da..3dd8a23aeb 100644 --- a/extensions/deviceicon/volume.py +++ b/extensions/deviceicon/volume.py @@ -32,7 +32,6 @@ from jarabe.view.palettes import VolumePalette from jarabe.frame.frameinvoker import FrameWidgetInvoker - _icons = {} volume_monitor = None @@ -45,7 +44,7 @@ def __init__(self, mount): self._mount = mount self._icon_name = get_mount_icon_name(mount, - Gtk.IconSize.LARGE_TOOLBAR) + Gtk.IconSize.LARGE) # TODO: retrieve the colors from the owner of the device color = get_mount_color(self._mount) @@ -63,13 +62,10 @@ def create_palette(self): icon = Icon(icon_name=self._icon_name, pixel_size=style.SMALL_ICON_SIZE, xo_color=color) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) menu_item.connect('activate', self.__show_contents_cb) - palette.content_box.pack_start(menu_item, True, True, 0) - palette.content_box.reorder_child(menu_item, 0) - menu_item.show() + palette.content_box.prepend(menu_item) return palette @@ -113,7 +109,7 @@ def _mount(volume, tray): if volume.get_mount() is None and volume.can_mount(): # TODO: pass None as mount_operation, or better, SugarMountOperation flags = 0 - mount_operation = Gtk.MountOperation(parent=tray.get_toplevel()) + mount_operation = Gtk.MountOperation(parent=tray.get_root()) cancellable = None user_data = None volume.mount(flags, mount_operation, cancellable, _mount_cb, user_data) diff --git a/extensions/globalkey/brightness.py b/extensions/globalkey/brightness.py index 7f0a82d069..2f8413a1b0 100644 --- a/extensions/globalkey/brightness.py +++ b/extensions/globalkey/brightness.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Martin Abente Lahaye +# Copyright (C) 2015 Martin Abente Lahaye # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/extensions/globalkey/screenshot.py b/extensions/globalkey/screenshot.py index ee84dfad0b..e4621c87c4 100644 --- a/extensions/globalkey/screenshot.py +++ b/extensions/globalkey/screenshot.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2009 Simon Schampijer, James Zaki # # This program is free software: you can redistribute it and/or modify @@ -16,7 +16,7 @@ from jarabe.model.screenshot import take_screenshot -BOUND_KEYS = ['1', 'Print'] +BOUND_KEYS = ['1', 'Print'] def handle_key_press(key): diff --git a/extensions/globalkey/speech.py b/extensions/globalkey/speech.py index a159db792b..d0866ac754 100644 --- a/extensions/globalkey/speech.py +++ b/extensions/globalkey/speech.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011 One Laptop Per Child +# Copyright (C) 2011 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,7 +15,7 @@ from jarabe.model import speech -BOUND_KEYS = ['s'] +BOUND_KEYS = ['s'] def handle_key_press(key): diff --git a/extensions/globalkey/viewhelp.py b/extensions/globalkey/viewhelp.py index f969d450eb..4d749b7355 100644 --- a/extensions/globalkey/viewhelp.py +++ b/extensions/globalkey/viewhelp.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013 Kalpa Welivitigoda +# Copyright (C) 2013 Kalpa Welivitigoda # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ from jarabe.view.viewhelp import setup_view_help from jarabe.model import shell -BOUND_KEYS = ['h'] +BOUND_KEYS = ['h'] def handle_key_press(key): diff --git a/extensions/globalkey/viewsource.py b/extensions/globalkey/viewsource.py index 236a51d571..2d3499a42a 100644 --- a/extensions/globalkey/viewsource.py +++ b/extensions/globalkey/viewsource.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer # # This program is free software: you can redistribute it and/or modify @@ -17,8 +17,7 @@ from jarabe.view.viewsource import setup_view_source from jarabe.model import shell - -BOUND_KEYS = ['XF86KbdLightOnOff', 'v'] +BOUND_KEYS = ['XF86KbdLightOnOff', 'v'] def handle_key_press(key): diff --git a/src/jarabe/apisocket.py b/src/jarabe/apisocket.py index d2afc4fd5a..8be4206065 100644 --- a/src/jarabe/apisocket.py +++ b/src/jarabe/apisocket.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Daniel Narvaez +# Copyright (C) 2013, Daniel Narvaez # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -68,7 +68,7 @@ def get_xo_color(self, request): self._client.send_result(request, [color_string.split(",")]) def close(self, request): - self._activity.get_window().close(GLib.get_current_time()) + self._activity.get_window().close() self._client.send_result(request, []) @@ -90,9 +90,9 @@ def _session_manager_shutdown_cb(self, event): self._client.send_notification("activity.stop") def show_object_chooser(self, request): - chooser = ObjectChooser(self._activity) + chooser = ObjectChooser(self._activity.get_window()) chooser.connect('response', self._chooser_response_cb, request) - chooser.show() + chooser.set_visible(True) def _chooser_response_cb(self, chooser, response_id, request): if response_id == Gtk.ResponseType.ACCEPT: @@ -101,7 +101,7 @@ def _chooser_response_cb(self, chooser, response_id, request): else: self._client.send_result(request, [None]) - chooser.destroy() + chooser.close() class DatastoreAPI(API): diff --git a/src/jarabe/controlpanel/cmd.py b/src/jarabe/controlpanel/cmd.py index 2d71dd7ec4..119152a8ae 100644 --- a/src/jarabe/controlpanel/cmd.py +++ b/src/jarabe/controlpanel/cmd.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, 2008 One Laptop Per Child +# Copyright (C) 2007, 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ from jarabe import config - _RESTART = 1 _same_option_warning = _('sugar-control-panel: WARNING, found more than one' diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py index 43d441a172..b94cd3a298 100644 --- a/src/jarabe/controlpanel/gui.py +++ b/src/jarabe/controlpanel/gui.py @@ -26,6 +26,7 @@ from sugar4.graphics.icon import Icon from sugar4.graphics import style from sugar4.graphics.alert import Alert, TimeoutAlert +from sugar4.graphics.xocolor import XoColor from jarabe.model.session import get_session_manager from jarabe.controlpanel.toolbar import MainToolbar @@ -36,22 +37,56 @@ _logger = logging.getLogger('ControlPanel') +def _set_css_bg(widget, color): + if color == style.COLOR_BLACK: + widget.set_css_classes(['controlpanel-bg-black']) + elif color == style.COLOR_WHITE: + widget.set_css_classes(['controlpanel-bg-white']) + else: + widget.set_css_classes([]) + +# Inject CSS for backgrounds +provider = Gtk.CssProvider() +css = b''' +.controlpanel-bg-black { background-color: %s; } +.controlpanel-bg-white { background-color: %s; } +.sectionicon-transparent { background-color: transparent; } +.sectionicon-label { color: white; } +.toolbar-title { color: white; font-weight: bold; } +''' % (style.COLOR_BLACK.get_html().encode('utf-8'), + style.COLOR_WHITE.get_html().encode('utf-8')) +provider.load_from_data(css) +Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + class ControlPanel(Gtk.Window): __gtype_name__ = 'SugarControlPanel' - def __init__(self, window_id): + def __init__(self, window_id=None): self.parent_window_id = window_id - Gtk.Window.__init__(self) + super().__init__() self._calculate_max_columns() - self.set_border_width(style.LINE_WIDTH) - self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + self.set_margin_start(style.LINE_WIDTH) + self.set_margin_end(style.LINE_WIDTH) + self.set_margin_top(style.LINE_WIDTH) + self.set_margin_bottom(style.LINE_WIDTH) self.set_decorated(False) self.set_resizable(False) self.set_modal(True) + self.set_focusable(True) + self.add_css_class('controlpanel-bg-black') - self.set_can_focus(True) - self.connect('key-press-event', self.__key_press_event_cb) + # Set transient parent window + shell_model = shell.get_model() + if shell_model and shell_model._main_window: + self.set_transient_for(shell_model._main_window) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) self._toolbar = None self._canvas = None @@ -64,120 +99,173 @@ def __init__(self, window_id): self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(self._hbox, True, True, 0) - self._hbox.show() + self._hbox.set_hexpand(True) + self._hbox.set_vexpand(True) + self._vbox.append(self._hbox) + self._hbox.set_visible(True) - self._main_view = Gtk.EventBox() - self._hbox.pack_start(self._main_view, True, True, 0) - self._main_view.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_BLACK.get_gdk_color()) - self._main_view.show() + self._main_view = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self._main_view.set_hexpand(True) + self._main_view.set_vexpand(True) + self._hbox.append(self._main_view) + _set_css_bg(self._main_view, style.COLOR_BLACK) + self._main_view.set_visible(True) - self.add(self._vbox) - self._vbox.show() + self.set_child(self._vbox) + self._vbox.set_visible(True) self.connect('realize', self.__realize_cb) + self.connect('destroy', self.__destroy_cb) self._options = self._get_options() self._current_option = None self._setup_main() self._setup_section() self._show_main_view() - Gdk.Screen.get_default().connect( - 'size-changed', self.__size_changed_cb) + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + monitors.get_item(0).connect('notify::geometry', self.__size_changed_cb) self._busy_count = 0 self._selected = [] def __realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - window = self.get_window() - window.set_accept_focus(True) - if self.parent_window_id: - attributes = Gdk.WindowAttr() - attributes.window_type = Gdk.WindowType.FOREIGN - parent = Gdk.Window.new(None, attributes, None) - window.set_transient_for(parent) - # the modal windows counter is updated to disable hot keys - SL#4601 + self._has_modal = True shell.get_model().push_modal() - def __size_changed_cb(self, event): + def __destroy_cb(self, widget): + if getattr(self, '_has_modal', False): + self._has_modal = False + shell.get_model().pop_modal() + + def __size_changed_cb(self, event, pspec=None): self._calculate_max_columns() def busy(self): if self._busy_count == 0: - self._old_cursor = self.get_window().get_cursor() - self._set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) + self.set_cursor(Gdk.Cursor.new_from_name("wait")) self._busy_count += 1 def unbusy(self): self._busy_count -= 1 if self._busy_count == 0: - self._set_cursor(self._old_cursor) - - def _set_cursor(self, cursor): - self.get_window().set_cursor(cursor) - Gdk.flush() + self.set_cursor(None) def add_alert(self, alert): - self._vbox.pack_start(alert, False, False, 0) - self._vbox.reorder_child(alert, 2) + self._alert = alert + if self._separator: + self._vbox.insert_child_after(alert, self._separator) + else: + self._vbox.prepend(alert) def remove_alert(self, alert): self._vbox.remove(alert) def grab_focus(self): - # overwrite grab focus in order to grab focus on the view - self._main_view.get_child().grab_focus() + if self._main_view.get_first_child(): + self._main_view.get_first_child().grab_focus() + + def _get_screen_width(self): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + return monitors.get_item(0).get_geometry().width + return 1024 + + def _get_screen_height(self): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + return monitors.get_item(0).get_geometry().height + return 768 def _calculate_max_columns(self): - self._max_columns = int(0.285 * (float(Gdk.Screen.width()) / - style.GRID_CELL_SIZE - 3)) + sw = self._get_screen_width() + sh = self._get_screen_height() + self._max_columns = int(0.285 * (float(sw) / style.GRID_CELL_SIZE - 3)) offset = style.GRID_CELL_SIZE - width = Gdk.Screen.width() - offset * 2 - height = Gdk.Screen.height() - offset * 2 + width = sw - offset * 2 + height = sh - offset * 2 self.set_size_request(width, height) - if hasattr(self, '_table'): - for child in self._table.get_children(): - child.destroy() + if hasattr(self, '_table') and self._table is not None: + while self._table.get_first_child() is not None: + self._table.remove(self._table.get_first_child()) self._setup_options() def _set_canvas(self, canvas): - if self._canvas in self._main_view: + if self._canvas == canvas: + return + if self._canvas and self._canvas.get_parent() == self._main_view: self._main_view.remove(self._canvas) if canvas: - self._main_view.add(canvas) + if canvas.get_parent() is not None and canvas.get_parent() != self._main_view: + canvas.get_parent().remove(canvas) + if canvas.get_parent() != self._main_view: + canvas.set_hexpand(True) + canvas.set_vexpand(True) + self._main_view.append(canvas) self._canvas = canvas def _set_toolbar(self, toolbar): + if self._toolbar == toolbar: + return if self._toolbar: - self._vbox.remove(self._toolbar) - self._vbox.pack_start(toolbar, False, False, 0) - self._vbox.reorder_child(toolbar, 0) + if self._toolbar.get_parent() == self._vbox: + self._vbox.remove(self._toolbar) + self._toolbar = toolbar + if not toolbar: + if self._separator and self._separator.get_parent() == self._vbox: + self._vbox.remove(self._separator) + return + + if toolbar.get_parent() is not None and toolbar.get_parent() != self._vbox: + toolbar.get_parent().remove(toolbar) + + if toolbar.get_parent() != self._vbox: + self._vbox.insert_child_after(toolbar, None) + if not self._separator: self._separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - self._vbox.pack_start(self._separator, False, False, 0) - self._vbox.reorder_child(self._separator, 1) - self._separator.show() + self._separator.set_visible(True) + + if self._separator.get_parent() is not None: + self._separator.get_parent().remove(self._separator) + + self._vbox.insert_child_after(self._separator, toolbar) def _setup_main(self): self._main_toolbar = MainToolbar() - self._table = Gtk.Table() - self._table.set_col_spacings(style.GRID_CELL_SIZE) - self._table.set_row_spacings(style.GRID_CELL_SIZE) - self._table.set_border_width(style.GRID_CELL_SIZE) + self._table = Gtk.Grid() + self._table.set_column_spacing(style.GRID_CELL_SIZE) + self._table.set_row_spacing(style.GRID_CELL_SIZE) + self._table.set_margin_start(style.GRID_CELL_SIZE) + self._table.set_margin_end(style.GRID_CELL_SIZE) + self._table.set_margin_top(style.GRID_CELL_SIZE) + self._table.set_margin_bottom(style.GRID_CELL_SIZE) + self._table.set_column_homogeneous(True) + self._table.set_row_homogeneous(True) + self._table.set_halign(Gtk.Align.CENTER) + self._table.set_valign(Gtk.Align.CENTER) self._scrolledwindow = Gtk.ScrolledWindow() - self._scrolledwindow.set_can_focus(False) + self._scrolledwindow.set_focusable(False) self._scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self._scrolledwindow.add_with_viewport(self._table) - child = self._scrolledwindow.get_child() - child.modify_bg( - Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color()) + self._scrolledwindow.set_hexpand(True) + self._scrolledwindow.set_vexpand(True) + + self._viewport = Gtk.Viewport() + self._viewport.set_child(self._table) + self._scrolledwindow.set_child(self._viewport) + + _set_css_bg(self._scrolledwindow, style.COLOR_BLACK) self._setup_options() self._main_toolbar.connect('stop-clicked', @@ -205,18 +293,16 @@ def _setup_options(self): title=self._options[option]['title'], xo_color=self._options[option]['color'], pixel_size=style.GRID_CELL_SIZE) - sectionicon.connect('button-press-event', + sectionicon.connect('option-clicked', self.__select_option_cb, option) - sectionicon.show() + sectionicon.set_visible(True) if option == 'aboutme': - self._table.attach(sectionicon, 0, 1, 0, 1) + self._table.attach(sectionicon, 0, 0, 1, 1) elif option == 'aboutcomputer': - self._table.attach(sectionicon, 1, 2, 0, 1) + self._table.attach(sectionicon, 1, 0, 1, 1) else: - self._table.attach(sectionicon, - column, column + 1, - row, row + 1) + self._table.attach(sectionicon, column, row, 1, 1) column += 1 if column == self._max_columns: column = 0 @@ -226,31 +312,28 @@ def _setup_options(self): def _show_main_view(self): if self._section_view is not None: - self._section_view.destroy() self._section_view = None self._set_toolbar(self._main_toolbar) - self._main_toolbar.show() + self._main_toolbar.set_visible(True) self._set_canvas(self._scrolledwindow) - self._main_view.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_BLACK.get_gdk_color()) - self._table.show() - self._scrolledwindow.show() + _set_css_bg(self._main_view, style.COLOR_BLACK) + self._table.set_visible(True) + self._scrolledwindow.set_visible(True) entry = self._main_toolbar.get_entry() entry.set_text('') - entry.connect('icon-press', self.__clear_icon_pressed_cb) self.grab_focus() - def __key_press_event_cb(self, window, event): - if event.keyval == Gdk.KEY_Return: + def __key_press_event_cb(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Return: if len(self._selected) == 1: self.show_section_view(self._selected[0]) return True - if event.keyval == Gdk.KEY_Escape: + if keyval == Gdk.KEY_Escape: if self._toolbar == self._main_toolbar: self.__stop_clicked_cb(None) - self.destroy() + self.close() else: self.__cancel_clicked_cb(None) return True @@ -264,7 +347,7 @@ def __key_press_event_cb(self, window, event): entry.grab_focus() return False - def __clear_icon_pressed_cb(self, entry, icon_pos, event): + def __clear_icon_pressed_cb(self, entry, icon_pos, event=None): self.grab_focus() def _update(self, query): @@ -291,12 +374,11 @@ def show_section_view(self, option): self._set_toolbar(self._section_toolbar) icon = self._section_toolbar.get_icon() - icon.set_from_icon_name(self._options[option]['icon'], - Gtk.IconSize.LARGE_TOOLBAR) + icon.set_icon_name(self._options[option]['icon']) icon.props.xo_color = self._options[option]['color'] title = self._section_toolbar.get_title() title.set_text(self._options[option]['title']) - self._section_toolbar.show() + self._section_toolbar.set_visible(True) self._current_option = option @@ -312,7 +394,8 @@ def show_section_view(self, option): self._options[option]['alerts']) self._set_canvas(self._section_view) - self._section_view.show() + self._section_view.set_visible(True) + _set_css_bg(self._main_view, style.COLOR_WHITE) finally: self.unbusy() @@ -326,8 +409,7 @@ def show_section_view(self, option): self.__create_restart_alert_cb) self._section_view.connect('set-toolbar-sensitivity', self.__set_toolbar_sensitivity_cb) - self._main_view.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) + _set_css_bg(self._main_view, style.COLOR_WHITE) def set_section_view_auto_close(self): """Automatically close the control panel if there is "nothing to do" @@ -354,7 +436,8 @@ def _get_options(self): options[item]['view'] = view_class options[item]['icon'] = getattr(mod, 'ICON', item) options[item]['title'] = getattr(mod, 'TITLE', item) - options[item]['color'] = getattr(mod, 'COLOR', None) + color = getattr(mod, 'COLOR', None) + options[item]['color'] = color keywords = getattr(mod, 'KEYWORDS', []) keywords.append(options[item]['title'].lower()) if item not in keywords: @@ -398,20 +481,20 @@ def __create_restart_alert_cb(self, widget=None, event=None): icon = Icon(icon_name='dialog-cancel') alert.add_button(Gtk.ResponseType.CANCEL, _('Cancel changes'), icon) - icon.show() + icon.set_visible(True) if self._section_view.props.is_deferrable: icon = Icon(icon_name='dialog-ok') alert.add_button(Gtk.ResponseType.ACCEPT, _('Later'), icon) - icon.show() + icon.set_visible(True) icon = Icon(icon_name='system-restart') alert.add_button(Gtk.ResponseType.APPLY, _('Restart now'), icon) - icon.show() + icon.set_visible(True) self.add_alert(alert) alert.connect('response', self.__response_cb) - alert.show() + alert.set_visible(True) def __response_cb(self, alert, response_id): self.remove_alert(alert) @@ -440,7 +523,7 @@ def __quit_timeout_cb(self): alert.connect('response', self.__quit_accept_cb) self.add_alert(alert) - alert.show() + alert.set_visible(True) def __quit_accept_cb(self, alert, response_id): self.remove_alert(alert) @@ -452,18 +535,20 @@ def __quit_accept_cb(self, alert, response_id): self.busy() get_session_manager().shutdown_completed() - def __select_option_cb(self, button, event, option): + def __select_option_cb(self, button, option): self.show_section_view(option) def __search_changed_cb(self, maintoolbar, query): self._update(query) def __stop_clicked_cb(self, widget): - shell.get_model().pop_modal() - self.destroy() + if getattr(self, '_has_modal', False): + self._has_modal = False + shell.get_model().pop_modal() + self.close() def __close_request_cb(self, widget, event=None): - self.destroy() + self.close() def __valid_section_cb(self, section_view, pspec): section_is_valid = section_view.props.is_valid @@ -502,14 +587,17 @@ def undo(self): except Exception as detail: _logger.debug('Error undo option: %s', detail) - if hasattr(ControlPanel, 'set_css_name'): ControlPanel.set_css_name('controlpanel') -class _SectionIcon(Gtk.EventBox): +class _SectionIcon(Gtk.Box): __gtype_name__ = 'SugarSectionIcon' + __gsignals__ = { + 'option-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])), + } + __gproperties__ = { 'icon-name': (str, None, None, None, GObject.ParamFlags.READWRITE), 'pixel-size': (object, None, None, GObject.ParamFlags.READWRITE), @@ -523,28 +611,36 @@ def __init__(self, **kwargs): self._xo_color = None self._title = 'No Title' - Gtk.EventBox.__init__(self, **kwargs) + super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs) + + # Ensure the box is pickable for clicks even in transparent areas + self.add_css_class('sectionicon-transparent') + self.set_halign(Gtk.Align.CENTER) + self.set_valign(Gtk.Align.CENTER) + self.set_hexpand(True) + self.set_vexpand(True) - self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._icon = Icon(icon_name=self._icon_name, pixel_size=self._pixel_size, xo_color=self._xo_color) - self._vbox.pack_start(self._icon, expand=False, fill=False, padding=0) + self.append(self._icon) self._label = Gtk.Label(label=self._title) - self._label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - self._vbox.pack_start(self._label, expand=False, fill=False, padding=0) - - self._vbox.set_spacing(style.DEFAULT_SPACING) - self.set_visible_window(False) - self.set_app_paintable(True) - self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) - - self.add(self._vbox) - self._vbox.show() - self._label.show() - self._icon.show() + self._label.add_css_class('sectionicon-label') + + self.append(self._label) + + self.set_spacing(style.DEFAULT_SPACING) + + click = Gtk.GestureClick() + click.connect('pressed', self._on_clicked) + self.add_controller(click) + + self._label.set_visible(True) + self._icon.set_visible(True) + + def _on_clicked(self, gesture, n_press, x, y): + self.emit('option-clicked') def get_icon(self): return self._icon diff --git a/src/jarabe/controlpanel/inlinealert.py b/src/jarabe/controlpanel/inlinealert.py index f756c4a95a..37dacae75a 100644 --- a/src/jarabe/controlpanel/inlinealert.py +++ b/src/jarabe/controlpanel/inlinealert.py @@ -13,10 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from gi.repository import Gtk from gi.repository import GObject - +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import Pango from sugar4.graphics import style from sugar4.graphics.icon import Icon @@ -51,20 +51,21 @@ def __init__(self, **kwargs): self._msg_label = Gtk.Label() self._msg_label.set_max_width_chars(150) self._msg_label.set_ellipsize(style.ELLIPSIZE_MODE_DEFAULT) - self._msg_label.set_alignment(0, 0.5) - self._msg_label.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) + self._msg_label.set_halign(Gtk.Align.START) + self._msg_label.set_valign(Gtk.Align.CENTER) + self._msg_label.add_css_class('inline-alert-label') - Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, **kwargs) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL, **kwargs) self.set_spacing(style.DEFAULT_SPACING) - self.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - - self.pack_start(self._icon, False, False, 0) - self.pack_start(self._msg_label, False, False, 0) - self._msg_label.show() - self._icon.show() + + self.add_css_class('sugar-inline-alert') + + self.append(self._icon) + self.append(self._msg_label) + self._msg_label.set_visible(True) + self._icon.set_visible(True) + self.set_visible(True) def do_set_property(self, pspec, value): if pspec.name == 'msg': diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py index 33552d7dd4..3f642a917d 100644 --- a/src/jarabe/controlpanel/sectionview.py +++ b/src/jarabe/controlpanel/sectionview.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, OLPC +# Copyright (C) 2008, OLPC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import GObject from gi.repository import Gtk from gettext import gettext as _ @@ -40,7 +39,8 @@ class SectionView(Gtk.Box): _APPLY_TIMEOUT = 1000 def __init__(self): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.add_css_class('cpanelsectionview') self._is_valid = True self._is_cancellable = True self._is_deferrable = True @@ -72,7 +72,3 @@ def do_get_property(self, pspec): def undo(self): """Undo here the changes that have been made in this section.""" pass - - -if hasattr(SectionView, 'set_css_name'): - SectionView.set_css_name('cpanelsectionview') diff --git a/src/jarabe/controlpanel/toolbar.py b/src/jarabe/controlpanel/toolbar.py index c75604d354..b97a1ab348 100644 --- a/src/jarabe/controlpanel/toolbar.py +++ b/src/jarabe/controlpanel/toolbar.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, 2008 One Laptop Per Child +# Copyright (C) 2007, 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,14 +21,13 @@ def _(msg): return gettext.dgettext('sugar', msg) - from sugar4.graphics.icon import Icon from sugar4.graphics.toolbutton import ToolButton from sugar4.graphics import iconentry from sugar4.graphics import style -class MainToolbar(Gtk.Toolbar): +class MainToolbar(Gtk.Box): """ Main toolbar of the control panel """ __gtype_name__ = 'MainToolbar' @@ -43,45 +42,40 @@ class MainToolbar(Gtk.Toolbar): } def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self.add_css_class('toolbar') self._add_separator() - tool_item = Gtk.ToolItem() - self.insert(tool_item, -1) - tool_item.show() - self._search_entry = iconentry.IconEntry() - self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, - 'entry-search') - self._search_entry.add_clear_button() - self._search_entry.set_width_chars(25) + tool_item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.append(tool_item) + tool_item.set_visible(True) + self._search_entry = Gtk.SearchEntry() text = _('Search in %s') % _('Settings') self._search_entry.set_placeholder_text(text) - self._search_entry.connect('changed', self.__search_entry_changed_cb) - tool_item.add(self._search_entry) - self._search_entry.show() + self._search_entry.connect('search-changed', self.__search_entry_changed_cb) + tool_item.append(self._search_entry) + self._search_entry.set_visible(True) self._add_separator(True) self.stop = ToolButton(icon_name='dialog-cancel') - self.stop.set_tooltip(_('Done')) + self.stop.set_tooltip_text(_('Done')) self.stop.connect('clicked', self.__stop_clicked_cb) - self.stop.show() - self.insert(self.stop, -1) - self.stop.show() + self.stop.set_visible(True) + self.append(self.stop) def get_entry(self): return self._search_entry def _add_separator(self, expand=False): - separator = Gtk.SeparatorToolItem() - separator.props.draw = False + spacer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) if expand: - separator.set_expand(True) + spacer.set_hexpand(True) else: - separator.set_size_request(style.DEFAULT_SPACING, -1) - self.insert(separator, -1) - separator.show() + spacer.set_size_request(style.DEFAULT_SPACING, -1) + self.append(spacer) + spacer.set_visible(True) def __search_entry_changed_cb(self, search_entry): self.emit('search-changed', search_entry.props.text) @@ -90,7 +84,7 @@ def __stop_clicked_cb(self, button): self.emit('stop-clicked') -class SectionToolbar(Gtk.Toolbar): +class SectionToolbar(Gtk.Box): """ Toolbar of the sections of the control panel """ __gtype_name__ = 'SectionToolbar' @@ -105,7 +99,8 @@ class SectionToolbar(Gtk.Toolbar): } def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self.add_css_class('toolbar') self._add_separator() @@ -115,21 +110,22 @@ def __init__(self): self._add_separator() self._title = Gtk.Label() + self._title.add_css_class('toolbar-title') self._add_widget(self._title) self._add_separator(True) - self.cancel_button = ToolButton('dialog-cancel') - self.cancel_button.set_tooltip(_('Cancel')) + self.cancel_button = ToolButton(icon_name='dialog-cancel') + self.cancel_button.set_tooltip_text(_('Cancel')) self.cancel_button.connect('clicked', self.__cancel_button_clicked_cb) - self.insert(self.cancel_button, -1) - self.cancel_button.show() + self.append(self.cancel_button) + self.cancel_button.set_visible(True) - self.accept_button = ToolButton('dialog-ok') - self.accept_button.set_tooltip(_('Ok')) + self.accept_button = ToolButton(icon_name='dialog-ok') + self.accept_button.set_tooltip_text(_('Ok')) self.accept_button.connect('clicked', self.__accept_button_clicked_cb) - self.insert(self.accept_button, -1) - self.accept_button.show() + self.append(self.accept_button) + self.accept_button.set_visible(True) def get_icon(self): return self._icon @@ -138,24 +134,24 @@ def get_title(self): return self._title def _add_separator(self, expand=False): - separator = Gtk.SeparatorToolItem() - separator.props.draw = False + spacer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) if expand: - separator.set_expand(True) + spacer.set_hexpand(True) else: - separator.set_size_request(style.DEFAULT_SPACING, -1) - self.insert(separator, -1) - separator.show() + spacer.set_size_request(style.DEFAULT_SPACING, -1) + self.append(spacer) + spacer.set_visible(True) def _add_widget(self, widget, expand=False): - tool_item = Gtk.ToolItem() - tool_item.set_expand(expand) + tool_item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + if expand: + tool_item.set_hexpand(True) - tool_item.add(widget) - widget.show() + tool_item.append(widget) + widget.set_visible(True) - self.insert(tool_item, -1) - tool_item.show() + self.append(tool_item) + tool_item.set_visible(True) def __cancel_button_clicked_cb(self, widget, data=None): self.emit('cancel-clicked') diff --git a/src/jarabe/desktop/activitieslist.py b/src/jarabe/desktop/activitieslist.py index 18c87f0f79..dcb50c4d11 100644 --- a/src/jarabe/desktop/activitieslist.py +++ b/src/jarabe/desktop/activitieslist.py @@ -44,6 +44,15 @@ from jarabe.util.normalize import normalize_string +def _get_children(box): + children = [] + child = box.get_first_child() + while child: + children.append(child) + child = child.get_next_sibling() + return children + + class ActivitiesTreeView(Gtk.TreeView): __gtype_name__ = 'SugarActivitiesTreeView' @@ -53,35 +62,34 @@ class ActivitiesTreeView(Gtk.TreeView): } def __init__(self): - Gtk.TreeView.__init__(self) + super().__init__() self.set_can_focus(False) self._query = '' self.set_headers_visible(False) - self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.TOUCH_MASK | - Gdk.EventMask.BUTTON_RELEASE_MASK) selection = self.get_selection() - selection.set_mode(Gtk.SelectionMode.NONE) + selection.set_mode(Gtk.SelectionMode.SINGLE) self._model = ListModel() self._model.set_visible_func(self.__model_visible_cb) self.set_model(self._model) self._favorite_columns = [] + self._favorite_cells = [] for i in range(desktop.get_number_of_views()): - self.fav_column = Gtk.TreeViewColumn() - self.cell_favorite = CellRendererFavorite(i) - self.cell_favorite.connect('clicked', self.__favorite_clicked_cb) - self.fav_column.pack_start(self.cell_favorite, True) - self.fav_column.set_cell_data_func(self.cell_favorite, - self.__favorite_set_data_cb) - self.append_column(self.fav_column) - - self._favorite_columns.append(self.fav_column) + fav_column = Gtk.TreeViewColumn() + cell_favorite = CellRendererFavorite(i) + cell_favorite.connect('clicked', self.__favorite_clicked_cb) + fav_column.pack_start(cell_favorite, True) + fav_column.set_cell_data_func(cell_favorite, + self.__favorite_set_data_cb) + self.append_column(fav_column) + self._favorite_columns.append(fav_column) + self._favorite_cells.append(cell_favorite) self.cell_icon = CellRendererActivityIcon() + self.cell_icon.connect('clicked', self.__activity_icon_clicked_cb) column = Gtk.TreeViewColumn() column.pack_start(self.cell_icon, True) @@ -94,7 +102,6 @@ def __init__(self): cell_text = Gtk.CellRendererText() cell_text.props.ellipsize = style.ELLIPSIZE_MODE_DEFAULT cell_text.props.ellipsize_set = True - column = Gtk.TreeViewColumn() column.props.sizing = Gtk.TreeViewColumnSizing.GROW_ONLY column.props.expand = True @@ -139,23 +146,12 @@ def __init__(self): self._invoker = TreeViewInvoker() self._invoker.attach_treeview(self) - self.button_press_handler = None - self.button_reslease_handler = None - self.icon_clicked_handler = None - self.row_activated_handler = None - if hasattr(self.props, 'activate_on_single_click'): - # Gtk+ 3.8 and later - self.props.activate_on_single_click = True - self.row_activated_handler = self.connect('row-activated', - self.__row_activated_cb) - else: - self.icon_clicked_handler = self.cell_icon.connect( - 'clicked', self.__icon_clicked_cb) - self.button_press_handler = self.connect( - 'button-press-event', self.__button_press_cb) - self.button_reslease_handler = self.connect( - 'button-release-event', self.__button_release_cb) - self._row_activated_armed_path = None + self.props.activate_on_single_click = True + self.row_activated_handler = self.connect('row-activated', + self.__row_activated_cb) + + def __activity_icon_clicked_cb(self, cell, path): + self._start_activity(path) def __favorite_set_data_cb(self, column, cell, model, tree_iter, data): favorite = \ @@ -174,61 +170,25 @@ def __favorite_clicked_cb(self, cell, path): not row[self._model.column_favorites[cell.favorite_view]], cell.favorite_view) - def __icon_clicked_cb(self, cell, path): - """ - A click on activity icon cell is to start an activity. - """ - logging.debug('__icon_clicked_cb') - self._start_activity(path) - def __row_activated_cb(self, treeview, path, col): """ A click on cells other than the favorite toggle is to start an - activity. Gtk+ 3.8 and later. + activity. """ logging.debug('__row_activated_cb') - if col is not treeview.get_column(0): + + columns = treeview.get_columns() + try: + col_idx = columns.index(col) + except ValueError: + col_idx = -1 + + num_favorite_views = desktop.get_number_of_views() + if 0 <= col_idx < num_favorite_views: + pass + else: self._start_activity(path) - def __button_to_path(self, event, event_type): - if event.window != self.get_bin_window() or \ - event.button != 1 or \ - event.type != event_type: - return None - - pos = self.get_path_at_pos(int(event.x), int(event.y)) - if pos is None: - return None - - path, column, x_, y_ = pos - if column == self._icon_column: - return None - - if column in self._favorite_columns: - return None - - return path - - def __button_press_cb(self, widget, event): - logging.debug('__button_press_cb') - path = self.__button_to_path(event, Gdk.EventType.BUTTON_PRESS) - if path is None: - return - - self._row_activated_armed_path = path - - def __button_release_cb(self, widget, event): - logging.debug('__button_release_cb') - path = self.__button_to_path(event, Gdk.EventType.BUTTON_RELEASE) - if path is None: - return - - if self._row_activated_armed_path != path: - return - - self._start_activity(path) - self._row_activated_armed_path = None - def _start_activity(self, path): model = self.get_model() row = model[path] @@ -236,7 +196,11 @@ def _start_activity(self, path): registry = bundleregistry.get_registry() bundle = registry.get_bundle(row[self._model.column_bundle_id]) - misc.launch(bundle) + if bundle: + misc.launch(bundle) + else: + logging.warning('Bundle not found in registry: %s', + row[self._model.column_bundle_id]) def set_filter(self, query): """Set a new query and refilter the model, return the number @@ -310,7 +274,8 @@ def connect_to_scroller(self, scrolled): scrolled.connect('scroll-start', self._scroll_start_cb) scrolled.connect('scroll-end', self._scroll_end_cb) self.cell_icon.connect_to_scroller(scrolled) - self.cell_favorite.connect_to_scroller(scrolled) + for cell in self._favorite_cells: + cell.connect_to_scroller(scrolled) def _scroll_start_cb(self, event): self._invoker.detach() @@ -342,8 +307,10 @@ def __init__(self): self._model = Gtk.ListStore() self._model.set_column_types(column_types) self._model_filter = self._model.filter_new() - Gtk.TreeModelSort.__init__(self, model=self._model_filter) + super().__init__(model=self._model_filter) self.set_sort_column_id(self.column_title, Gtk.SortType.ASCENDING) + + self._registry_sids = [] GLib.idle_add(self.__connect_to_bundle_registry_cb) @@ -351,9 +318,17 @@ def __connect_to_bundle_registry_cb(self): registry = bundleregistry.get_registry() for info in registry: self._add_activity(info) - registry.connect('bundle-added', self.__activity_added_cb) - registry.connect('bundle-changed', self.__activity_changed_cb) - registry.connect('bundle-removed', self.__activity_removed_cb) + self._registry_sids.append(registry.connect('bundle-added', self.__activity_added_cb)) + self._registry_sids.append(registry.connect('bundle-changed', self.__activity_changed_cb)) + self._registry_sids.append(registry.connect('bundle-removed', self.__activity_removed_cb)) + + def do_dispose(self): + registry = bundleregistry.get_registry() + for sid in self._registry_sids: + if registry.handler_is_connected(sid): + registry.disconnect(sid) + self._registry_sids = [] + super().do_dispose() def __activity_added_cb(self, activity_registry, activity_info): self._add_activity(activity_info) @@ -438,6 +413,8 @@ def __init__(self, favorite_view): self.props.height = style.GRID_CELL_SIZE self.props.size = style.SMALL_ICON_SIZE self.props.icon_name = desktop.get_favorite_icons()[favorite_view] + self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.stroke_color = style.COLOR_TRANSPARENT.get_svg() self.props.mode = Gtk.CellRendererMode.ACTIVATABLE @@ -457,54 +434,48 @@ def __init__(self): self.props.size = style.STANDARD_ICON_SIZE self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() - self.props.mode = Gtk.CellRendererMode.ACTIVATABLE + self.props.mode = Gtk.CellRendererMode.INERT prelit_color = profile.get_color() self.props.prelit_stroke_color = prelit_color.get_stroke_color() self.props.prelit_fill_color = prelit_color.get_fill_color() -class ClearMessageBox(Gtk.EventBox): +class ClearMessageBox(Gtk.Box): def __init__(self, message, button_callback): - Gtk.EventBox.__init__(self) - - self.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) + super().__init__(orientation=Gtk.Orientation.VERTICAL) - alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1) - self.add(alignment) - alignment.show() + self.set_name("clear-message-box") box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - alignment.add(box) - box.show() + box.set_halign(Gtk.Align.CENTER) + box.set_valign(Gtk.Align.CENTER) + box.set_vexpand(True) + self.append(box) icon = Icon(pixel_size=style.LARGE_ICON_SIZE, icon_name='system-search', stroke_color=style.COLOR_BUTTON_GREY.get_svg(), fill_color=style.COLOR_TRANSPARENT.get_svg()) - box.pack_start(icon, expand=True, fill=False, padding=0) - icon.show() + box.append(icon) label = Gtk.Label() - color = style.COLOR_BUTTON_GREY.get_html() + color = style.COLOR_BLACK.get_html() label.set_markup('%s' % ( color, GLib.markup_escape_text(message))) - box.pack_start(label, expand=True, fill=False, padding=0) - label.show() + box.append(label) - button_box = Gtk.HButtonBox() - button_box.set_layout(Gtk.ButtonBoxStyle.CENTER) - box.pack_start(button_box, False, True, 0) - button_box.show() + button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + button_box.set_halign(Gtk.Align.CENTER) + box.append(button_box) button = Gtk.Button(label=_('Clear search')) button.connect('clicked', button_callback) - button.props.image = Icon(icon_name='dialog-cancel', - pixel_size=style.SMALL_ICON_SIZE) - button_box.pack_start(button, expand=True, fill=False, padding=0) - button.show() + button_icon = Icon(icon_name='dialog-cancel', + pixel_size=style.SMALL_ICON_SIZE) + button.set_child(button_icon) + button_box.append(button) class ActivitiesList(Gtk.Box): @@ -517,22 +488,24 @@ class ActivitiesList(Gtk.Box): def __init__(self): logging.debug('STARTUP: Loading the activities list') - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self._scrolled_window = Gtk.ScrolledWindow() self._scrolled_window.set_can_focus(False) self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self._scrolled_window.set_shadow_type(Gtk.ShadowType.NONE) - self._scrolled_window.connect('key-press-event', - self.__key_press_event_cb) - self.pack_start(self._scrolled_window, True, True, 0) - self._scrolled_window.show() + self._scrolled_window.set_has_frame(False) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_pressed_cb) + self._scrolled_window.add_controller(key_controller) + + self._scrolled_window.set_vexpand(True) + self.append(self._scrolled_window) self._tree_view = ActivitiesTreeView() self._tree_view.connect('erase-activated', self.__erase_activated_cb) - self._scrolled_window.add(self._tree_view) - self._tree_view.show() + self._scrolled_window.set_child(self._tree_view) scrolling_detector = ScrollingDetector(self._scrolled_window) self._tree_view.connect_to_scroller(scrolling_detector) @@ -555,15 +528,15 @@ def set_filter(self, query): self._hide_clear_message() def __desktop_view_icons_changed_cb(self, model): - self._tree_view.destroy() + # Unparent the old tree view before replacing. self._tree_view = ActivitiesTreeView() self._tree_view.connect('erase-activated', self.__erase_activated_cb) - self._scrolled_window.add(self._tree_view) - self._tree_view.show() + self._scrolled_window.set_child(self._tree_view) - def __key_press_event_cb(self, scrolled_window, event): - keyname = Gdk.keyval_name(event.keyval) + def __key_pressed_cb(self, controller, keyval, keycode, state): + keyname = Gdk.keyval_name(keyval) + scrolled_window = controller.get_widget() vadjustment = scrolled_window.props.vadjustment if keyname == 'Up': if vadjustment.props.value > vadjustment.props.lower: @@ -580,38 +553,39 @@ def __key_press_event_cb(self, scrolled_window, event): return True def _show_clear_message(self): - if self._clear_message_box in self.get_children(): + children = _get_children(self) + if self._clear_message_box in children: return - if self._scrolled_window in self.get_children(): + if self._scrolled_window in children: self.remove(self._scrolled_window) self._clear_message_box = ClearMessageBox( message=_('No matching activities'), button_callback=self.__clear_button_clicked_cb) - self.pack_end(self._clear_message_box, True, True, 0) - self._clear_message_box.show() + self._clear_message_box.set_vexpand(True) + self.append(self._clear_message_box) def __clear_button_clicked_cb(self, button): self.emit('clear-clicked') def _hide_clear_message(self): - if self._scrolled_window in self.get_children(): + children = _get_children(self) + if self._scrolled_window in children: return - if self._clear_message_box in self.get_children(): + if self._clear_message_box in children: self.remove(self._clear_message_box) self._clear_message_box = None - self.pack_end(self._scrolled_window, True, True, 0) - self._scrolled_window.show() + self._scrolled_window.set_vexpand(True) + self.append(self._scrolled_window) def add_alert(self, alert): if self._alert is not None: self.remove_alert() self._alert = alert - self.pack_start(alert, False, True, 0) - self.reorder_child(alert, 0) + self.prepend(alert) def remove_alert(self): self.remove(self._alert) @@ -680,12 +654,10 @@ def __init__(self, activity_info): self._favorite_icons.append( Icon(icon_name=desktop.get_favorite_icons()[i], pixel_size=style.SMALL_ICON_SIZE)) - self._favorite_items[i].set_image(self._favorite_icons[i]) - self._favorite_icons[i].show() + self._favorite_items[i].set_icon_widget(self._favorite_icons[i]) self._favorite_items[i].connect( 'activate', self.__change_favorite_activate_cb, i) self.menu_box.append_item(self._favorite_items[i]) - self._favorite_items[i].show() if activity_info.is_user_activity(): self._add_erase_option(registry, activity_info) @@ -698,23 +670,23 @@ def __init__(self, activity_info): self.__activity_changed_cb, i)) self._update_favorite_item(i) - self.menu_box.connect('destroy', self.__destroy_cb) + def do_dispose(self): + registry = bundleregistry.get_registry() + for sid in self._activity_changed_sid: + if registry.handler_is_connected(sid): + registry.disconnect(sid) + self._activity_changed_sid = [] + super().do_dispose() def _add_erase_option(self, registry, activity_info): menu_item = PaletteMenuItem(_('Erase'), 'list-remove') menu_item.connect('activate', self.__erase_activate_cb) self.menu_box.append_item(menu_item) - menu_item.show() if not os.access(activity_info.get_path(), os.W_OK) or \ registry.is_activity_protected(self._bundle_id): menu_item.props.sensitive = False - def __destroy_cb(self, palette): - registry = bundleregistry.get_registry() - for i in range(desktop.get_number_of_views()): - registry.disconnect(self._activity_changed_sid[i]) - def _update_favorite_item(self, favorite_view): if self._favorites[favorite_view]: self._favorite_items[favorite_view].set_label(_('Remove favorite')) diff --git a/src/jarabe/desktop/activitychooser.py b/src/jarabe/desktop/activitychooser.py index 3cebce1960..c6c6ec527d 100644 --- a/src/jarabe/desktop/activitychooser.py +++ b/src/jarabe/desktop/activitychooser.py @@ -32,29 +32,26 @@ from jarabe.util.normalize import normalize_string -class TitleBox(Gtk.Toolbar): +class TitleBox(Gtk.Box): def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) self.close_button = ToolButton(icon_name='dialog-cancel') self.close_button.set_tooltip(_('Close')) - self.insert(self.close_button, -1) - self.close_button.show() + self.append(self.close_button) + self.close_button.set_visible(True) self._label = Gtk.Label() - self._label.set_alignment(0, 0.5) + self._label.set_halign(Gtk.Align.START) + self._label.set_valign(Gtk.Align.CENTER) + self._label.set_hexpand(True) - tool_item = Gtk.ToolItem() - tool_item.set_expand(True) - tool_item.add(self._label) - self._label.show() - self.insert(tool_item, 0) - tool_item.show() + self.append(self._label) + self._label.set_visible(True) def set_title(self, title): self._label.set_markup('%s' % title) - self._label.show() - + self._label.set_visible(True) _AUTOSEARCH_TIMEOUT = 1000 @@ -70,18 +67,40 @@ class ActivityChooser(Gtk.Window): } def __init__(self): - Gtk.Window.__init__(self) + super().__init__() self.set_decorated(False) - self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) - self.set_border_width(style.LINE_WIDTH) self.set_resizable(False) self.set_modal(True) - self.set_can_focus(True) + self.set_focusable(True) + + # Set transient parent window + from jarabe.model import shell as _shell + shell_model = _shell.get_model() + if shell_model and shell_model._main_window: + self.set_transient_for(shell_model._main_window) + + display = Gdk.Display.get_default() + width = 1024 - style.GRID_CELL_SIZE * 2 + height = 768 - style.GRID_CELL_SIZE * 2 + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geo = monitors.get_item(0).get_geometry() + width = geo.width - style.GRID_CELL_SIZE * 2 + height = geo.height - style.GRID_CELL_SIZE * 2 + + self.set_default_size( + width * 3 / 4, + height * 2 / 3) self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.add(self._vbox) - self._vbox.show() + self._vbox.set_margin_start(style.LINE_WIDTH) + self._vbox.set_margin_end(style.LINE_WIDTH) + self._vbox.set_margin_top(style.LINE_WIDTH) + self._vbox.set_margin_bottom(style.LINE_WIDTH) + self.set_child(self._vbox) + self._vbox.set_visible(True) self._title_box = TitleBox() self._title_box.close_button.connect( @@ -89,33 +108,40 @@ def __init__(self): self.__close_button_clicked_cb) self._title_box.set_size_request(-1, style.GRID_CELL_SIZE) - self._vbox.pack_start(self._title_box, False, True, 0) - self._title_box.show() + self._vbox.append(self._title_box) + self._title_box.set_visible(True) - self.set_size_request( - (Gdk.Screen.height() - style.GRID_CELL_SIZE * 3) * 3 / 4, - (Gdk.Screen.height() - style.GRID_CELL_SIZE * 2) * 2 / 3) - self.connect('key-press-event', self.__key_press_event_cb) + key_controller = Gtk.EventControllerKey() + key_controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) + self.connect('realize', self.__realize_cb) self._list_view = ActivitiesList() self.search_bar = SearchBar() - self._vbox.pack_start(self.search_bar, False, False, 0) + self._vbox.append(self.search_bar) self.search_bar.connect('query-changed', self.__toolbar_query_changed_cb) - self.search_bar.search_entry.connect('key-press-event', - self.__key_press_event_cb) + + search_key_controller = Gtk.EventControllerKey() + search_key_controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + search_key_controller.connect('key-pressed', self.__key_press_event_cb) + self.search_bar.search_entry.add_controller(search_key_controller) self.search_bar.search_entry.grab_focus() + self._scrolled_window = Gtk.ScrolledWindow() self._scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self._scrolled_window.set_vexpand(True) + self._scrolled_window.set_hexpand(True) - self._scrolled_window.add(self._list_view) + self._scrolled_window.set_child(self._list_view) - self._vbox.pack_start(self._scrolled_window, True, True, 0) + self._vbox.append(self._scrolled_window) - self._list_view.show() + self._list_view.set_visible(True) self._list_view.connect('clear-clicked', self.__activitylist_clear_clicked_cb, self.search_bar) @@ -126,39 +152,22 @@ def __init__(self): self.tree_view.fav_column.set_visible(False) self.tree_view.version_column.set_visible(False) + # Disconnect default row-activated handler to prevent double launch if self.tree_view.row_activated_handler: self.tree_view.disconnect(self.tree_view.row_activated_handler) - if self.tree_view.button_press_handler: - self.tree_view.disconnect(self.tree_view.button_press_handler) - if self.tree_view.button_reslease_handler: - self.tree_view.disconnect(self.tree_view.button_reslease_handler) - if self.tree_view.icon_clicked_handler: - self.tree_view.disconnect(self.tree_view.icon_clicked_handler) - - if hasattr(self.tree_view.props, 'activate_on_single_click'): - # Gtk+ 3.8 and later - self.tree_view.props.activate_on_single_click = True - self.tree_view.connect('row-activated', self.__row_activated_cb) - else: - self.tree_view.cell_icon.connect('clicked', - self.__icon_clicked_cb) - self.tree_view.connect('button-press-event', - self.__button_press_cb) - self.tree_view.connect('button-release-event', - self.__button_release_cb) - self._row_activated_armed_path = None + self.tree_view.row_activated_handler = None - self.show() + self.tree_view.props.activate_on_single_click = True + self.tree_view.connect('row-activated', self.__row_activated_cb) + + self.set_visible(True) def __close_button_clicked_cb(self, button): - self.destroy() + shell.get_model().pop_modal() + self.close() def __realize_cb(self, widget): shell.get_model().push_modal() - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - window = self.get_window() - window.set_accept_focus(True) - shell.get_model().push_modal() def __toolbar_query_changed_cb(self, toolbar, query): self._query = normalize_string(query.decode('utf-8')) @@ -167,32 +176,38 @@ def __toolbar_query_changed_cb(self, toolbar, query): toolbar.search_entry._icon_selected = \ self._list_view.get_activities_selected() - # verify if one off the selected names is a perfect match - # this is needed by th case of activities with names contained + # verify if one of the selected names is a perfect match + # this is needed by the case of activities with names contained # in other activities like 'Paint' and 'MusicPainter' + activity = None for activity in self._list_view.get_activities_selected(): if activity['name'].upper() == query.upper(): toolbar.search_entry._icon_selected = [activity] break # Don't change the selection if the entry has been autocompleted - if len(toolbar.search_entry._icon_selected) == 1 \ - and not toolbar.search_entry.get_text() == activity['name']: - pos = toolbar.search_entry.get_position() - toolbar.search_entry.set_text( - toolbar.search_entry._icon_selected[0]['name']) - toolbar.search_entry.select_region(pos, -1) - - def __key_press_event_cb(self, widget, event): - keyname = Gdk.keyval_name(event.keyval) + # if activity is not None and len(toolbar.search_entry._icon_selected) == 1 \ + # and not toolbar.search_entry.get_text() == activity['name']: + # pos = toolbar.search_entry.get_position() + # toolbar.search_entry.set_text( + # toolbar.search_entry._icon_selected[0]['name']) + # toolbar.search_entry.select_region(pos, -1) + + def __key_press_event_cb(self, controller, keyval, keycode, state): + keyname = Gdk.keyval_name(keyval) if keyname == 'Escape': - self.destroy() + shell.get_model().pop_modal() + self.close() + return True - if not self.search_bar.search_entry.has_focus(): - self.search_bar.search_entry.grab_focus() + search_entry = self.search_bar.search_entry + has_focus = search_entry.has_focus() or search_entry.get_focus_child() is not None + if not has_focus: + search_entry.grab_focus() + widget = controller.get_widget() if widget == self.search_bar.search_entry: - if event.keyval == Gdk.KEY_Return: + if keyval == Gdk.KEY_Return: model = self.tree_view.get_model() if len(model) > 1: return True @@ -202,8 +217,10 @@ def __key_press_event_cb(self, widget, event): activity_id = activityfactory.create_activity_id() self.emit('activity-selected', bundle_id, activity_id) - self.destroy() + shell.get_model().pop_modal() + self.close() return True + return False def __activitylist_clear_clicked_cb(self, list_view, toolbar): toolbar.clear_query() @@ -215,35 +232,8 @@ def _got_row_tree_view(self, row): bundle_id = row[self.tree_view._model.column_bundle_id] activity_id = activityfactory.create_activity_id() self.emit('activity-selected', bundle_id, activity_id) - self.destroy() - - def __button_press_cb(self, widget, event): - path = self.tree_view.__button_to_path(event, - Gdk.EventType.BUTTON_PRESS) - if path is None: - return - - self._row_activated_armed_path = path - - def __button_release_cb(self, widget, event): - path = self.tree_view.__button_to_path(event, - Gdk.EventType.BUTTON_PRESS) - if path is None: - return - - if self._row_activated_armed_path != path: - return - - model = self.tree_view.get_model() - row = model[path] - self._got_row_tree_view(row) - self._row_activated_armed_path = None - - def __icon_clicked_cb(self, tree_view, path): - model = tree_view.get_model() - row = model[path] - self._got_row_tree_view(row) - return True + shell.get_model().pop_modal() + self.close() def __row_activated_cb(self, treeview, path, col): if col is not treeview.get_column(0): @@ -253,7 +243,7 @@ def __row_activated_cb(self, treeview, path, col): return True -class SearchBar(Gtk.Toolbar): +class SearchBar(Gtk.Box): ''' New Toolbar below the Titlebox of sugar3.graphics PopWindow. This toolbar contains textentry for search. @@ -267,42 +257,34 @@ class SearchBar(Gtk.Toolbar): } def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) self._query = None self._autosearch_timer = None - # self.set_border_width(10) - tool_item = Gtk.ToolItem() - self.insert(tool_item, -1) - tool_item.set_expand(True) - tool_item.show() - - self.search_entry = iconentry.IconEntry() - self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, - 'entry-search') - self.search_entry.set_can_focus(True) - self.search_entry.add_clear_button() + + self.search_entry = Gtk.SearchEntry() + self.search_entry.set_focusable(True) self.search_entry.set_width_chars(20) self.search_entry.connect('activate', self._entry_activated_cb) - self.search_entry.connect('changed', self._entry_changed_cb) + self.search_entry.connect('search-changed', self._entry_changed_cb) + self.search_entry.set_hexpand(True) - tool_item.add(self.search_entry) - self.search_entry.show() + self.append(self.search_entry) + self.search_entry.set_visible(True) self._add_separator() def clear_query(self): self.search_entry.props.text = '' def _add_separator(self, expand=False): - separator = Gtk.SeparatorToolItem() - separator.props.draw = False + separator = Gtk.Box() if expand: - separator.set_expand(True) + separator.set_hexpand(True) else: separator.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) - self.insert(separator, -1) - separator.show() + self.append(separator) + separator.set_visible(True) def _entry_activated_cb(self, entry): if self._autosearch_timer: @@ -315,7 +297,7 @@ def _entry_activated_cb(self, entry): def _entry_changed_cb(self, entry): if not entry.props.text: - entry.activate() + entry.emit('activate') return if self._autosearch_timer: @@ -326,5 +308,5 @@ def _entry_changed_cb(self, entry): def _autosearch_timer_cb(self): logging.debug('_autosearch_timer_cb') self._autosearch_timer = None - self.search_entry.activate() + self.search_entry.emit('activate') return False diff --git a/src/jarabe/desktop/favoriteslayout.py b/src/jarabe/desktop/favoriteslayout.py index 3b4289bca5..835f6461eb 100644 --- a/src/jarabe/desktop/favoriteslayout.py +++ b/src/jarabe/desktop/favoriteslayout.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2010 Sugar Labs # # This program is free software: you can redistribute it and/or modify @@ -19,20 +19,51 @@ import hashlib from gettext import gettext as _ +from gi.repository import Gtk from gi.repository import Gdk +import gi +gi.require_version('Gsk', '4.0') +gi.require_version('Graphene', '1.0') +from gi.repository import Gsk, Graphene from sugar4.graphics import style from jarabe.model import bundleregistry from jarabe.desktop.grid import Grid - _logger = logging.getLogger('FavoritesLayout') _CELL_SIZE = 4 _BASE_SCALE = 1000 +class _SizeRequest: + def __init__(self, w, h): + self.width = w + self.height = h + + +def get_child_size_request(child): + min_w, nat_w, _, _ = child.measure(Gtk.Orientation.HORIZONTAL, -1) + min_h, nat_h, _, _ = child.measure(Gtk.Orientation.VERTICAL, -1) + return _SizeRequest(nat_w, nat_h) + + +def allocate_child(child, x, y, width, height): + transform = Gsk.Transform().translate(Graphene.Point().init(x, y)) + child.allocate(width, height, -1, transform) + + +def _get_screen_height(): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + monitor = monitors.get_item(0) + return monitor.get_geometry().height + return 768 + + class Layout(object): def __init__(self): @@ -56,6 +87,7 @@ def setup(self, allocation, owner_icon, activity_icon=None): if self._grid is not None: if self._width == allocation.width and \ self._height == allocation.height: + self._allocate_owner_icon(allocation, owner_icon, activity_icon) return self._width = allocation.width self._height = allocation.height @@ -68,7 +100,7 @@ def setup(self, allocation, owner_icon, activity_icon=None): def _allocate_owner_icon(self, allocation, owner_icon, activity_icon): # add owner icon to the grid, precisely centered on the screen # if not None, add an activity icon directly below the owner icon - owner_request = owner_icon.size_request() + owner_request = get_child_size_request(owner_icon) owner_width, owner_height = owner_request.width, owner_request.height height = allocation.height + allocation.y width = allocation.width @@ -87,40 +119,44 @@ def _allocate_owner_icon(self, allocation, owner_icon, activity_icon): # calculate x coordinate and create allocation owner_icon_allocation = Gdk.Rectangle() - owner_icon_allocation.x = (width - owner_width) / 2 - owner_icon_allocation.y = allocation.y + y + owner_icon_allocation.x = int((width - owner_width) / 2) + owner_icon_allocation.y = int(allocation.y + y) owner_icon_allocation.width = owner_width owner_icon_allocation.height = owner_height - owner_icon.size_allocate(owner_icon_allocation) + + allocate_child(owner_icon, owner_icon_allocation.x, owner_icon_allocation.y, owner_icon_allocation.width, owner_icon_allocation.height) # Determine grid coordinates and add to grid - owner_grid_width, owner_grid_height = \ - self._get_child_grid_size(owner_icon) - grid_x = int(owner_icon_allocation.x / float(_CELL_SIZE)) - grid_y = int(owner_icon_allocation.y / float(_CELL_SIZE)) - self._grid.add(owner_icon, owner_grid_width, owner_grid_height, - grid_x, grid_y, locked=True) + if not self._grid.is_in_grid(owner_icon): + owner_grid_width, owner_grid_height = \ + self._get_child_grid_size(owner_icon) + grid_x = int(owner_icon_allocation.x / float(_CELL_SIZE)) + grid_y = int(owner_icon_allocation.y / float(_CELL_SIZE)) + self._grid.add(owner_icon, owner_grid_width, owner_grid_height, + grid_x, grid_y, locked=True) if activity_icon is None: return # Position the current activity below the XO icon # FIXME must ensure we cross into next grid cell here.. - activity_request = activity_icon.size_request() + activity_request = get_child_size_request(activity_icon) activity_icon_allocation = Gdk.Rectangle() - activity_icon_allocation.x = (width - activity_request.width) / 2 - activity_icon_allocation.y = owner_icon_allocation.y + owner_height + activity_icon_allocation.x = int((width - activity_request.width) / 2) + activity_icon_allocation.y = int(owner_icon_allocation.y + owner_height) activity_icon_allocation.width = activity_request.width activity_icon_allocation.height = activity_request.height - activity_icon.size_allocate(activity_icon_allocation) + + allocate_child(activity_icon, activity_icon_allocation.x, activity_icon_allocation.y, activity_icon_allocation.width, activity_icon_allocation.height) # Determine grid coordinates and add to grid - activity_grid_width, activity_grid_height = \ - self._get_child_grid_size(activity_icon) - x = int(activity_icon_allocation.x / float(_CELL_SIZE)) - y = int(activity_icon_allocation.y / float(_CELL_SIZE)) - self._grid.add(activity_icon, activity_grid_width, - activity_grid_height, x, y, locked=True) + if not self._grid.is_in_grid(activity_icon): + activity_grid_width, activity_grid_height = \ + self._get_child_grid_size(activity_icon) + x = int(activity_icon_allocation.x / float(_CELL_SIZE)) + y = int(activity_icon_allocation.y / float(_CELL_SIZE)) + self._grid.add(activity_icon, activity_grid_width, + activity_grid_height, x, y, locked=True) def allocate_children(self, allocation, children): pass @@ -130,37 +166,22 @@ def move_icon(self, child, x, y, allocation): def move(self, child, x, y, allocation=None): self._grid.move(child, x / _CELL_SIZE, y / _CELL_SIZE, locked=True) - child_request = child.size_request() - rect = self._grid.get_child_rect(child) - - child_allocation = Gdk.Rectangle() - child_allocation.x = int(round(rect.x * _CELL_SIZE)) - child_allocation.y = int(round(rect.y * _CELL_SIZE)) - child_allocation.width = child_request.width - child_allocation.height = child_request.height - child.size_allocate(child_allocation) + child.queue_allocate() def _get_child_grid_size(self, child): - request = child.size_request() + request = get_child_size_request(child) width = math.ceil(request.width / _CELL_SIZE) height = math.ceil(request.height / _CELL_SIZE) return int(width), int(height) def __grid_child_changed_cb(self, grid, child, allocation): - request = child.size_request() - rect = self._grid.get_child_rect(child) - child_allocation = Gdk.Rectangle() - child_allocation.x = int(round(rect.x * _CELL_SIZE)) - child_allocation.y = int(round(rect.y * _CELL_SIZE)) + allocation.y - child_allocation.width = request.width - child_allocation.height = request.height - child.size_allocate(child_allocation) + child.queue_allocate() class SpreadLayout(ViewLayout): def __init__(self): - ViewLayout.__init__(self) + super().__init__() def remove(self, child): if self._grid is None: @@ -190,14 +211,14 @@ def allocate_children(self, allocation, children): self._grid.add(child, width, height, x, y, locked=False) - requisition = child.get_preferred_size()[0] + requisition = get_child_size_request(child) rect = self._grid.get_child_rect(child) child_allocation = Gdk.Rectangle() child_allocation.x = int(round(rect.x * _CELL_SIZE)) child_allocation.y = int(round(rect.y * _CELL_SIZE)) + allocation.y child_allocation.width = requisition.width child_allocation.height = requisition.height - child.size_allocate(child_allocation) + allocate_child(child, child_allocation.x, child_allocation.y, child_allocation.width, child_allocation.height) class RandomLayout(SpreadLayout): @@ -216,7 +237,7 @@ class RandomLayout(SpreadLayout): """String used to identify this layout in home view dropdown palette.""" def __init__(self): - SpreadLayout.__init__(self) + super().__init__() self.fixed_positions = {} def _add_fixed_position(self, icon, allocation, locked=False): @@ -238,7 +259,7 @@ def _add_fixed_position(self, icon, allocation, locked=False): def allocate_children(self, allocation, children): for child in children: - child_requisition = child.size_request() + child_requisition = get_child_size_request(child) if not self._grid.is_in_grid(child): self._add_fixed_position(child, allocation) @@ -257,20 +278,20 @@ def allocate_children(self, allocation, children): y = None if x is None or y is None: - self._grid.add(child, child_requisition.width / _CELL_SIZE, - child_requisition.height / _CELL_SIZE) + self._grid.add(child, int(child_requisition.width / _CELL_SIZE), + int(child_requisition.height / _CELL_SIZE)) else: - self._grid.add(child, child_requisition.width / _CELL_SIZE, - child_requisition.height / _CELL_SIZE, - x / _CELL_SIZE, y / _CELL_SIZE) + self._grid.add(child, int(child_requisition.width / _CELL_SIZE), + int(child_requisition.height / _CELL_SIZE), + int(x / _CELL_SIZE), int(y / _CELL_SIZE)) rect = self._grid.get_child_rect(child) child_allocation = Gdk.Rectangle() child_allocation.x = int(round(rect.x * _CELL_SIZE)) - child_allocation.y = int(round(rect.y * _CELL_SIZE)) + allocation.y + child_allocation.y = int(round(rect.y * _CELL_SIZE)) child_allocation.width = child_requisition.width child_allocation.height = child_requisition.height - child.size_allocate(child_allocation) + allocate_child(child, child_allocation.x, child_allocation.y, child_allocation.width, child_allocation.height) def move_icon(self, child, x, y, allocation): ViewLayout.move(self, child, x, y) @@ -287,7 +308,6 @@ def move_icon(self, child, x, y, allocation): y * allocation.height / float(_BASE_SCALE)) self.fixed_positions[child] = (x, y) - _MINIMUM_RADIUS = style.XLARGE_ICON_SIZE / 2 + style.DEFAULT_SPACING _RING_SPACING_FACTOR = 0.95 _SPIRAL_SPACING_FACTOR = 0.75 @@ -310,7 +330,7 @@ class RingLayout(ViewLayout): """String used to identify this layout in home view dropdown palette.""" def __init__(self): - ViewLayout.__init__(self) + super().__init__() self._spiral_mode = False def _calculate_radius_and_icon_size(self, children_count): @@ -369,7 +389,7 @@ def _convert_from_polar_to_cartesian(self, angle, radius, icon_size, width, def _calculate_maximum_radius(self, icon_size): """ Return the maximum radius including encroachment. """ - r = (Gdk.Screen.height() - style.GRID_CELL_SIZE) / 2 - \ + r = (_get_screen_height() - style.GRID_CELL_SIZE) / 2 - \ style.DEFAULT_SPACING return r - (icon_size * _MAXIMUM_RADIUS_PADDING_FACTOR) @@ -417,15 +437,14 @@ def allocate_children(self, allocation, children): y += icon_size / 2 child.set_size(icon_size) - new_width = child.get_preferred_width()[0] - new_height = child.get_preferred_height()[0] + new_width = child.measure(Gtk.Orientation.HORIZONTAL, -1)[1] + new_height = child.measure(Gtk.Orientation.VERTICAL, -1)[1] child_allocation = Gdk.Rectangle() - child_allocation.x = allocation.x + x - child_allocation.y = allocation.y + y + child_allocation.x = int(allocation.x + x) + child_allocation.y = int(allocation.y + y) child_allocation.width = new_width child_allocation.height = new_height - child.size_allocate(child_allocation) - + allocate_child(child, child_allocation.x, child_allocation.y, child_allocation.width, child_allocation.height) _SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75 """Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced.""" @@ -472,7 +491,7 @@ class SunflowerLayout(RingLayout): """String used to identify this layout in home view dropdown palette.""" def __init__(self): - RingLayout.__init__(self) + super().__init__() self.skipped_indices = [] def _calculate_radius_and_icon_size(self, children_count): @@ -540,7 +559,7 @@ class BoxLayout(RingLayout): """String used to identify this layout in home view dropdown palette.""" def __init__(self): - RingLayout.__init__(self) + super().__init__() def _calculate_position(self, radius, icon_size, index, children_count, width, height, sin=None, cos=None): @@ -586,7 +605,7 @@ class TriangleLayout(RingLayout): """String used to identify this layout in home view dropdown palette.""" def __init__(self): - RingLayout.__init__(self) + super().__init__() def _calculate_radius_and_icon_size(self, children_count): # use slightly larger minimum radius than parent, because sides diff --git a/src/jarabe/desktop/favoritesview.py b/src/jarabe/desktop/favoritesview.py index de58d74f67..8f2d95a3c1 100644 --- a/src/jarabe/desktop/favoritesview.py +++ b/src/jarabe/desktop/favoritesview.py @@ -57,8 +57,6 @@ _logger = logging.getLogger('FavoritesView') -_ICON_DND_TARGET = ('activity-icon', Gtk.TargetFlags.SAME_WIDGET, 0) - LAYOUT_MAP = {favoriteslayout.RingLayout.key: favoriteslayout.RingLayout, favoriteslayout.BoxLayout.key: favoriteslayout.BoxLayout, favoriteslayout.TriangleLayout.key: @@ -77,12 +75,13 @@ class FavoritesBox(Gtk.Box): __gtype_name__ = 'SugarFavoritesBox' def __init__(self, favorite_view): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self.favorite_view = favorite_view self._view = FavoritesView(self) - self.pack_start(self._view, True, True, 0) - self._view.show() + self.append(self._view) + self._view.set_vexpand(True) + self._view.set_hexpand(True) self._alert = None @@ -94,14 +93,13 @@ def set_resume_mode(self, resume_mode): def grab_focus(self): # overwrite grab focus in order to grab focus from the parent - self._view.grab_focus() + return self._view.grab_focus() def add_alert(self, alert): if self._alert is not None: self.remove_alert() self._alert = alert - self.pack_start(alert, False, True, 0) - self.reorder_child(alert, 0) + self.prepend(alert) def remove_alert(self): self.remove(self._alert) @@ -128,22 +126,10 @@ def __init__(self, box): activity_icon=current_activity) self.set_can_focus(False) - self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.POINTER_MOTION_HINT_MASK) - self.drag_dest_set(0, [], 0) - - # Drag and drop is set only for the Random layout. This is - # the flag that enables or disables it. + # Drag and drop is set only for the Random layout. self._dragging_mode = False + self._drop_target = None - self._drag_motion_hid = None - self._drag_drop_hid = None - self._drag_data_received_hid = None - - self._dragging = False - self._pressed_button = None - self._press_start_x = 0 - self._press_start_y = 0 self._hot_x = None self._hot_y = None self._last_clicked_icon = None @@ -156,7 +142,9 @@ def __init__(self, box): favorites_settings = get_settings(self._box.favorite_view) favorites_settings.changed.connect(self.__settings_changed_cb) - self._set_layout(favorites_settings.layout) + layout_set = self._set_layout(favorites_settings.layout) + if layout_set: + self.set_layout(self._layout) def __settings_changed_cb(self, **kwargs): favorites_settings = get_settings(self._box.favorite_view) @@ -172,23 +160,21 @@ def __settings_changed_cb(self, **kwargs): def _set_layout(self, layout): if layout not in LAYOUT_MAP: - logging.warn('Unknown favorites layout: %r', layout) + logging.warning('Unknown favorites layout: %r', layout) layout = favoriteslayout.RingLayout.key assert layout in LAYOUT_MAP if self._layout is not None and self._dragging_mode: - self.disconnect(self._drag_motion_hid) - self.disconnect(self._drag_drop_hid) - self.disconnect(self._drag_data_received_hid) + if self._drop_target: + self.remove_controller(self._drop_target) + self._drop_target = None if layout == favoriteslayout.RandomLayout.key: self._dragging_mode = True - self._drag_motion_hid = self.connect( - 'drag-motion', self.__drag_motion_cb) - self._drag_drop_hid = self.connect( - 'drag-drop', self.__drag_drop_cb) - self._drag_data_received_hid = self.connect( - 'drag-data-received', self.__drag_data_received_cb) + self._drop_target = Gtk.DropTarget.new(type=GObject.TYPE_STRING, actions=Gdk.DragAction.MOVE) + self._drop_target.connect('motion', self.__drag_motion_cb) + self._drop_target.connect('drop', self.__drag_drop_cb) + self.add_controller(self._drop_target) else: self._dragging_mode = False @@ -197,97 +183,62 @@ def _set_layout(self, layout): layout = property(None, _set_layout) - def do_add(self, child): + def add(self, child): if child != self._owner_icon and child != self._activity_icon: self._children.append(child) - child.connect('button-press-event', self.__button_press_cb) - child.connect('button-release-event', self.__button_release_cb) - child.connect('motion-notify-event', self.__motion_notify_event_cb) - child.connect('drag-begin', self.__drag_begin_cb) - if child.get_realized(): - child.set_parent_window(self.get_parent_window()) - child.set_parent(self) - - def __button_release_cb(self, widget, event): - if self._dragging: - return True - return False + + drag_source = Gtk.DragSource.new() + drag_source.set_actions(Gdk.DragAction.MOVE) + drag_source.connect("prepare", self.__drag_prepare_cb, child) + drag_source.connect("drag-begin", self.__drag_begin_cb, child) + child.add_controller(drag_source) - def __button_press_cb(self, widget, event): - if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: - self._last_clicked_icon = widget - self._pressed_button = event.button - self._press_start_x = event.x - self._press_start_y = event.y - return False + child.set_parent(self) - def __motion_notify_event_cb(self, widget, event): + def __drag_prepare_cb(self, source, x, y, child): if not self._dragging_mode: - return False - - # if the mouse button is not pressed, no drag should occurr - if not event.get_state() & Gdk.ModifierType.BUTTON1_MASK: - self._pressed_button = None - return False - - if event.is_hint: - x, y, state_ = event.window.get_pointer() - else: - x = event.x - y = event.y - - if widget.drag_check_threshold(int(self._press_start_x), - int(self._press_start_y), - int(x), - int(y)): - self._dragging = True - target_entry = Gtk.TargetEntry.new(*_ICON_DND_TARGET) - target_list = Gtk.TargetList.new([target_entry]) - widget.drag_begin(target_list, - Gdk.DragAction.MOVE, - 1, - event) - return False + return None + self._last_clicked_icon = child + v = GObject.Value(GObject.TYPE_STRING, "activity-icon") + return Gdk.ContentProvider.new_for_value(v) - def __drag_begin_cb(self, widget, context): + def __drag_begin_cb(self, source, drag, child): if not self._dragging_mode: - return False - - pixbuf = GdkPixbuf.Pixbuf.new_from_file(widget.props.file_name) - - self._hot_x = pixbuf.props.width / 2 - self._hot_y = pixbuf.props.height / 2 - Gtk.drag_set_icon_pixbuf(context, pixbuf, self._hot_x, self._hot_y) - - def __drag_motion_cb(self, widget, context, x, y, time): + return + if not child.props.file_name: + return + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(child.props.file_name) + self._hot_x = pixbuf.get_width() / 2 + self._hot_y = pixbuf.get_height() / 2 + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + source.set_icon(texture, self._hot_x, self._hot_y) + except Exception as e: + logging.error("Failed to create drag icon: %s", e) + self._hot_x = 0 + self._hot_y = 0 + + def __drag_motion_cb(self, target, x, y): if self._last_clicked_icon is not None: - Gdk.drag_status(context, context.get_suggested_action(), time) - return True - return False + return Gdk.DragAction.MOVE + return Gdk.DragAction(0) - def __drag_drop_cb(self, widget, context, x, y, time): + def __drag_drop_cb(self, target, value, x, y): if self._last_clicked_icon is not None: - target = Gdk.Atom.intern_static_string(_ICON_DND_TARGET[0]) - self.drag_get_data(context, target, time) + allocation = Gdk.Rectangle() + allocation.x = 0 + allocation.y = 0 + allocation.width = self.get_width() + allocation.height = self.get_height() self._layout.move_icon(self._last_clicked_icon, - x - self._hot_x, y - self._hot_y, - self.get_allocation()) + x - self._hot_x, y - self._hot_y, allocation) - self._pressed_button = None - self._press_start_x = None - self._press_start_y = None self._hot_x = None self._hot_y = None self._last_clicked_icon = None - self._dragging = False - return True return False - def __drag_data_received_cb(self, widget, context, x, y, selection_data, - info, time): - Gdk.drop_finish(context, success=True, time_=time) - def __connect_to_bundle_registry_cb(self): registry = bundleregistry.get_registry() @@ -312,7 +263,6 @@ def _add_activity(self, activity_info): icon.props.pixel_size = style.STANDARD_ICON_SIZE # icon.set_resume_mode(self._resume_mode) self.add(icon) - icon.show() def __activity_added_cb(self, activity_registry, activity_info): registry = bundleregistry.get_registry() @@ -493,15 +443,11 @@ def __palette_activate_cb(self, palette): def __palette_entry_activate_cb(self, palette, metadata): self._resume(metadata) - def do_get_preferred_width(self): - width = CanvasIcon.do_get_preferred_width(self)[0] - width += ActivityIcon._BORDER_WIDTH * 2 - return (width, width) - - def do_get_preferred_height(self): - height = CanvasIcon.do_get_preferred_height(self)[0] - height += ActivityIcon._BORDER_WIDTH * 2 - return (height, height) + def do_measure(self, orientation, for_size): + min_size, nat_size, min_base, nat_base = super().do_measure(orientation, for_size) + min_size += ActivityIcon._BORDER_WIDTH * 2 + nat_size += ActivityIcon._BORDER_WIDTH * 2 + return (min_size, nat_size, min_base, nat_base) def __button_activate_cb(self, icon): self._activate() @@ -582,15 +528,17 @@ def __init__(self, activity_info, journal_entries): xo_color=color) menu_item.connect('activate', self.__resume_entry_cb, entry) menu_items.append(menu_item) - menu_item.show() if journal_entries: separator = PaletteMenuItemSeparator() menu_items.append(separator) - separator.show() for i in range(0, len(menu_items)): - self.menu_box.pack_start(menu_items[i], True, True, 0) + menu_items[i].set_hexpand(True) + menu_items[i].set_vexpand(True) + menu_items[i].set_halign(Gtk.Align.FILL) + menu_items[i].set_valign(Gtk.Align.FILL) + self.menu_box.append(menu_items[i]) def __resume_entry_cb(self, menu_item, entry): if entry is not None: @@ -605,17 +553,30 @@ def __init__(self): self._home_model = shell.get_model() self._home_activity = self._home_model.get_active_activity() - if self._home_activity is not None: - self._update() + if self._home_activity is None: + self._home_activity = self._get_journal_activity() + + self._update() self._home_model.connect('active-activity-changed', self.__active_activity_changed_cb) self.connect_after('activate', self.__activate_cb) + def _get_journal_activity(self): + for activity in self._home_model._activities: + if activity.is_journal(): + return activity + return None + def __activate_cb(self, icon): - window = self._home_model.get_active_activity().get_window() - window.activate(Gtk.get_current_event_time()) + active_activity = self._home_model.get_active_activity() + if active_activity is None: + # No activity open: reveal the Journal directly. + from jarabe.journal import journalactivity + journalactivity.get_journal().reveal() + else: + self._home_model.activate_activity(active_activity) def _update(self): if self._home_activity is not None: @@ -625,33 +586,51 @@ def _update(self): if self._home_activity.is_journal(): if self._unbusy(): GLib.timeout_add(100, self._unbusy) + else: + self.props.xo_color = get_owner_instance().props.color self.props.pixel_size = style.STANDARD_ICON_SIZE if self.palette is not None: - self.palette.destroy() + if hasattr(self.palette, "popdown"): + self.palette.popdown(immediate=True) self.palette = None def _unbusy(self): - if self.get_window(): - import jarabe.desktop.homewindow - jarabe.desktop.homewindow.get_instance().unbusy() + import jarabe.desktop.homewindow + hw = jarabe.desktop.homewindow.get_instance() + if hw and hw.get_root(): + hw.unbusy() return False return True def create_palette(self): + if self._home_activity is None: + # Journal may not be registered yet – initialize it now. + from jarabe.journal import journalactivity as _ja + _ja.get_journal() + self._home_activity = self._get_journal_activity() + if self._home_activity is not None: if self._home_activity.is_journal(): palette = JournalPalette(self._home_activity) else: palette = CurrentActivityPalette(self._home_activity) self.connect_to_palette_pop_events(palette) - else: - palette = None - return palette + return palette + + return None def __active_activity_changed_cb(self, home_model, home_activity): - self._home_activity = home_activity + if home_activity is None: + self._home_activity = self._get_journal_activity() + if self._home_activity is None: + # journal not yet registered – initialize it + from jarabe.journal import journalactivity as _ja + _ja.get_journal() + self._home_activity = self._get_journal_activity() + else: + self._home_activity = home_activity self._update() @@ -666,15 +645,6 @@ class OwnerIcon(BuddyIcon): def __init__(self, size): BuddyIcon.__init__(self, buddy=get_owner_instance(), pixel_size=size) - # This is a workaround to skip the callback for - # enter-notify-event in the parent class the first time. - def __enter_notify_event_cb(icon, event): - self.unset_state_flags(Gtk.StateFlags.PRELIGHT) - self.disconnect(self._enter_notify_hid) - - self._enter_notify_hid = self.connect('enter-notify-event', - __enter_notify_event_cb) - def create_palette(self): palette = BuddyMenu(get_owner_instance()) @@ -688,9 +658,13 @@ def create_palette(self): text = _('Register again') register_menu = PaletteMenuItem(text, 'media-record') - register_menu.connect('activate', self.__register_activate_cb) - palette.menu_box.pack_end(register_menu, True, True, 0) - register_menu.show() + register_menu.connect('item-activated', self.__register_activate_cb) + register_menu.set_hexpand(True) + register_menu.set_vexpand(True) + register_menu.set_halign(Gtk.Align.FILL) + register_menu.set_valign(Gtk.Align.FILL) + palette.menu_box.append_item(register_menu) + register_menu.set_visible(True) self.connect_to_palette_pop_events(palette) diff --git a/src/jarabe/desktop/friendview.py b/src/jarabe/desktop/friendview.py index f4dbd54c4d..c502cf4020 100644 --- a/src/jarabe/desktop/friendview.py +++ b/src/jarabe/desktop/friendview.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2010 Collabora Ltd. # # This program is free software: you can redistribute it and/or modify @@ -26,7 +26,7 @@ class FriendView(Gtk.Box): def __init__(self, buddy, **kwargs): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs) # round icon sizes to an even number so that it can be accurately # centered in a larger bounding box also of even dimensions @@ -35,10 +35,13 @@ def __init__(self, buddy, **kwargs): self._buddy = buddy self._buddy_icon = BuddyIcon(buddy) self._buddy_icon.props.pixel_size = size - self.add(self._buddy_icon) - self._buddy_icon.show() + self.append(self._buddy_icon) self._activity_icon = CanvasIcon(pixel_size=size) + # Always keep the activity icon in the box; use set_visible() to + # show/hide it. + self._activity_icon.set_visible(False) + self.append(self._activity_icon) self._update_activity() self._buddy.connect('notify::current-activity', @@ -54,9 +57,8 @@ def _get_new_icon_name(self, ps_activity): return None def _remove_activity_icon(self): - if self._activity_icon.get_visible(): - self._activity_icon.hide() - self.remove(self._activity_icon) + # Hide the widget. + self._activity_icon.set_visible(False) def __buddy_notify_current_activity_cb(self, buddy, pspec): self._update_activity() @@ -73,9 +75,8 @@ def _update_activity(self): if name: self._activity_icon.props.file_name = name self._activity_icon.props.xo_color = self._buddy.props.color - if not self._activity_icon.get_visible(): - self.add(self._activity_icon) - self._activity_icon.show() + # Make visible; widget is always a child of the box. + self._activity_icon.set_visible(True) else: self._remove_activity_icon() diff --git a/src/jarabe/desktop/grid.py b/src/jarabe/desktop/grid.py index 48b32d4eed..4248df9eee 100644 --- a/src/jarabe/desktop/grid.py +++ b/src/jarabe/desktop/grid.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 Red Hat, Inc. +# Copyright (C) 2007 Red Hat, Inc. # Copyright (C) 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify @@ -20,23 +20,29 @@ from gi.repository import Gdk from gi.repository import GLib +import gi +gi.require_version('SugarExt', '2.0') from gi.repository import SugarExt - _PLACE_TRIALS = 20 _MAX_WEIGHT = 255 _REFRESH_RATE = 200 _MAX_COLLISIONS_PER_REFRESH = 20 -class Grid(SugarExt.Grid): +class GridWeight(SugarExt.Grid): + def __init__(self): + GObject.GObject.__init__(self) + + +class Grid(GridWeight): __gsignals__ = { 'child-changed': (GObject.SignalFlags.RUN_FIRST, None, ([GObject.TYPE_PYOBJECT])), } def __init__(self, width, height): - GObject.GObject.__init__(self) + GridWeight.__init__(self) self._children = [] self._child_rects = {} @@ -193,12 +199,22 @@ def __solve_collisions_cb(self): return True + def _intersect_rects(self, rect1, rect2): + x = max(rect1.x, rect2.x) + y = max(rect1.y, rect2.y) + w = min(rect1.x + rect1.width, rect2.x + rect2.width) - x + h = min(rect1.y + rect1.height, rect2.y + rect2.height) - y + intersection = Gdk.Rectangle() + if w > 0 and h > 0: + intersection.x, intersection.y, intersection.width, intersection.height = x, y, w, h + return True, intersection + return False, intersection + def _detect_collisions(self, child): collision_found = False child_rect = self._child_rects[child] for c in self._children: - intersects_, intersection = Gdk.rectangle_intersect( - child_rect, self._child_rects[c]) + intersects_, intersection = self._intersect_rects(child_rect, self._child_rects[c]) if c != child and intersection.width > 0: collision_found = True if (c not in self._locked_children and diff --git a/src/jarabe/desktop/groupbox.py b/src/jarabe/desktop/groupbox.py index 98df01d234..4044e53183 100644 --- a/src/jarabe/desktop/groupbox.py +++ b/src/jarabe/desktop/groupbox.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,13 +37,12 @@ def __init__(self, toolbar): # Round off icon size to an even number to ensure that the icon owner_icon = BuddyIcon(get_owner_instance(), style.LARGE_ICON_SIZE & ~1) - ViewContainer.__init__(self, layout, owner_icon) - self.set_can_focus(False) + super().__init__(layout, owner_icon) + self.set_focusable(False) self._query = '' toolbar.connect('query-changed', self._toolbar_query_changed_cb) - toolbar.search_entry.connect('icon-press', - self.__clear_icon_pressed_cb) + self._friends = {} friends_model = friends.get_model() @@ -58,7 +57,6 @@ def add_friend(self, buddy_info): icon = FriendView(buddy_info) self.add(icon) self._friends[buddy_info.get_key()] = icon - icon.show() def _friend_added_cb(self, data_model, buddy_info): self.add_friend(buddy_info) @@ -67,7 +65,6 @@ def _friend_removed_cb(self, data_model, key): icon = self._friends[key] self.remove(icon) del self._friends[key] - icon.destroy() def _toolbar_query_changed_cb(self, toolbar, query): self._query = normalize_string(query) @@ -75,5 +72,5 @@ def _toolbar_query_changed_cb(self, toolbar, query): if hasattr(icon, 'set_filter'): icon.set_filter(self._query) - def __clear_icon_pressed_cb(self, entry, icon_pos, event): + def __clear_icon_pressed_cb(self, entry, icon_pos): self.grab_focus() diff --git a/src/jarabe/desktop/homebackgroundbox.py b/src/jarabe/desktop/homebackgroundbox.py index b9c00f51e3..24610cfc3b 100644 --- a/src/jarabe/desktop/homebackgroundbox.py +++ b/src/jarabe/desktop/homebackgroundbox.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 Agustin Zubiaga +# Copyright (C) 2012 Agustin Zubiaga # Copyright (C) 2013 Sugar Labs # # This program is free software: you can redistribute it and/or modify @@ -21,6 +21,7 @@ from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import Gio +from gi.repository import Graphene BACKGROUND_DIR = 'org.sugarlabs.user.background' BACKGROUND_IMAGE_PATH_KEY = 'image-path' @@ -56,29 +57,39 @@ def get_background_alpha_level(): class HomeBackgroundBox(Gtk.Box): def __init__(self): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self._background_pixbuf = None self._update_background_image() - self.connect('draw', self.__draw_cb) self._settings = Gio.Settings.new(BACKGROUND_DIR) self._settings.connect('changed', self.__conf_changed_cb, None) - def __draw_cb(self, widget, context): - if self._background_pixbuf is None: - return - - alloc = widget.get_allocation() - - if self._background_pixbuf.get_width() != alloc.width or \ - self._background_pixbuf.get_height() != alloc.height: - self._background_pixbuf = self._background_pixbuf.scale_simple( - alloc.width, - alloc.height, - GdkPixbuf.InterpType.TILES) - Gdk.cairo_set_source_pixbuf(context, self._background_pixbuf, 0, 0) - alpha = get_background_alpha_level() - context.paint_with_alpha(alpha) + def do_snapshot(self, snapshot): + if self._background_pixbuf is not None: + width = self.get_width() + height = self.get_height() + + if (self._background_pixbuf.get_width() != width or + self._background_pixbuf.get_height() != height): + scaled = self._background_pixbuf.scale_simple( + width, height, GdkPixbuf.InterpType.TILES) + else: + scaled = self._background_pixbuf + + texture = Gdk.Texture.new_for_pixbuf(scaled) + alpha = get_background_alpha_level() + + rect = Graphene.Rect() + rect.init(0, 0, width, height) + + snapshot.push_opacity(alpha) + snapshot.append_texture(texture, rect) + snapshot.pop() + + child = self.get_first_child() + while child is not None: + self.snapshot_child(child, snapshot) + child = child.get_next_sibling() def __conf_changed_cb(self, settings, key, data): self._update_background_image() diff --git a/src/jarabe/desktop/homebox.py b/src/jarabe/desktop/homebox.py index bbd5cbe1bf..61ec984ef3 100644 --- a/src/jarabe/desktop/homebox.py +++ b/src/jarabe/desktop/homebox.py @@ -25,23 +25,41 @@ from jarabe.model import desktop +def _get_children(box): + children = [] + child = box.get_first_child() + while child: + children.append(child) + child = child.get_next_sibling() + return children + + class HomeBox(Gtk.Box): __gtype_name__ = 'SugarHomeBox' def __init__(self, toolbar): logging.debug('STARTUP: Loading the home view') - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self._favorites_views_indicies = [] for i in range(desktop.get_number_of_views()): self._favorites_views_indicies.append(i) self._list_view_index = self._favorites_views_indicies[-1] + 1 + self._view_stack = Gtk.Stack() + self._view_stack.set_transition_type(Gtk.StackTransitionType.NONE) + self._view_stack.set_vexpand(True) + self.append(self._view_stack) + self._favorites_boxes = [] for i in range(desktop.get_number_of_views()): - self._favorites_boxes.append(FavoritesBox(i)) + box = FavoritesBox(i) + self._favorites_boxes.append(box) + self._view_stack.add_named(box, f"favorite_{i}") + self._list_view = ActivitiesList() + self._view_stack.add_named(self._list_view, "list") self._desktop_model = desktop.get_model() self._desktop_model.connect('desktop-view-icons-changed', @@ -50,18 +68,25 @@ def __init__(self, toolbar): toolbar.search_entry._icon_selected = [] toolbar.connect('query-changed', self.__toolbar_query_changed_cb) toolbar.connect('view-changed', self.__toolbar_view_changed_cb) - toolbar.search_entry.connect('key-press-event', - self.__search_entry_key_press_event_cb) - toolbar.search_entry.connect('icon-press', - self.__clear_icon_pressed_cb) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', + self.__search_entry_key_pressed_cb) + toolbar.search_entry.add_controller(key_controller) + + self._list_view.connect('clear-clicked', self.__activitylist_clear_clicked_cb, toolbar) - self._set_view(self._favorites_views_indicies[0]) + self._last_view = self._favorites_views_indicies[0] self._query = '' self._resume_mode = Gio.Settings.new( 'org.sugarlabs.user').get_boolean('resume-activity') + # Show the default view after all state is initialised + self._set_view(self._favorites_views_indicies[0]) + + def __desktop_view_icons_changed_cb(self, model): number_of_views = desktop.get_number_of_views() @@ -91,87 +116,87 @@ def __toolbar_query_changed_cb(self, toolbar, query): toolbar.search_entry._icon_selected = \ self._list_view.get_activities_selected() - # verify if one off the selected names is a perfect match - # this is needed by th case of activities with names contained + # verify if one of the selected names is a perfect match + # this is needed by the case of activities with names contained # in other activities like 'Paint' and 'MusicPainter' + activity = None for activity in self._list_view.get_activities_selected(): if activity['name'].upper() == query.upper(): toolbar.search_entry._icon_selected = [activity] break # Don't change the selection if the entry has been autocompleted - if len(toolbar.search_entry._icon_selected) == 1 \ + if activity is not None and len(toolbar.search_entry._icon_selected) == 1 \ and not toolbar.search_entry.get_text() == activity['name']: pos = toolbar.search_entry.get_position() toolbar.search_entry.set_text( toolbar.search_entry._icon_selected[0]['name']) toolbar.search_entry.select_region(pos, -1) + if not query and self._resume_mode: + self._list_view.get_activities_selected() + else: + self.set_resume_mode(False) + + # when query has any string, then set search view else revert + if query: + self._set_view(self._list_view_index) + else: + self._set_view(self._last_view) + def __toolbar_view_changed_cb(self, toolbar, view): - self._set_view(view) + self.set_view(view) - def __search_entry_key_press_event_cb(self, entry, event): + def __search_entry_key_pressed_cb(self, controller, keyval, keycode, state): # wherever a single item is selected in a desktop view, # launch the activity on pressing return - if event.keyval == Gdk.KEY_Return and len(entry._icon_selected) == 1: + entry = controller.get_widget() + if keyval == Gdk.KEY_Return and len(entry._icon_selected) == 1: self._list_view.run_activity(entry._icon_selected[0]['bundle_id'], self._resume_mode) entry._icon_selected = [] self.set_resume_mode(self._resume_mode) + return True + + return False def __activitylist_clear_clicked_cb(self, widget, toolbar): - toolbar.clear_query() + toolbar.search_entry.set_text('') + toolbar.search_entry.grab_focus() - def __clear_icon_pressed_cb(self, entry, icon_pos, event): + def __clear_icon_pressed_cb(self, entry, icon_pos): self.grab_focus() def grab_focus(self): - # overwrite grab focus to be able to grab focus on the - # views which are packed inside a box - children = self.get_children() - if self._list_view in children: - self._list_view.grab_focus() - else: - for i in range(desktop.get_number_of_views()): - if self._favorites_boxes[i] in children: - self._favorites_boxes[i].grab_focus() + super().grab_focus() + + def set_view(self, view): + if view in self._favorites_views_indicies: + self._last_view = view + self._set_view(view) def _set_view(self, view): if view in self._favorites_views_indicies: favorite = self._favorites_views_indicies.index(view) - - children = self.get_children() - if self._list_view in children: - self.remove(self._list_view) - else: - for i in range(desktop.get_number_of_views()): - if i != favorite and self._favorites_boxes[i] in children: - self.remove(self._favorites_boxes[i]) - - if self._favorites_boxes[favorite] not in children: - self.add(self._favorites_boxes[favorite]) - self._favorites_boxes[favorite].show() - self._favorites_boxes[favorite].grab_focus() + self._view_stack.set_visible_child_name(f"favorite_{favorite}") + self._favorites_boxes[favorite].grab_focus() elif view == self._list_view_index: - children = self.get_children() - for i in range(desktop.get_number_of_views()): - if self._favorites_boxes[i] in children: - self.remove(self._favorites_boxes[i]) - - if self._list_view not in children: - self.add(self._list_view) - self._list_view.show() - self._list_view.grab_focus() + self._view_stack.set_visible_child_name("list") + self._list_view.grab_focus() else: raise ValueError('Invalid view: %r' % view) _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes def resume(self): - pass + visible = self._view_stack.get_visible_child() + if hasattr(visible, 'resume'): + visible.resume() def suspend(self): - pass + visible = self._view_stack.get_visible_child() + if hasattr(visible, 'suspend'): + visible.suspend() def set_resume_mode(self, resume_mode, favorite_view=0): self._resume_mode = resume_mode diff --git a/src/jarabe/desktop/homewindow.py b/src/jarabe/desktop/homewindow.py index 7fb44468c2..c1a654ea70 100644 --- a/src/jarabe/desktop/homewindow.py +++ b/src/jarabe/desktop/homewindow.py @@ -35,7 +35,6 @@ from jarabe.model import shell from jarabe import config - _HOME_PAGE = 0 _GROUP_PAGE = 1 _MESH_PAGE = 2 @@ -44,78 +43,96 @@ _instance = None -class HomeWindow(Gtk.ApplicationWindow): +def _get_children(box): + children = [] + child = box.get_first_child() + while child: + children.append(child) + child = child.get_next_sibling() + return children + + +class HomeWindow(Gtk.Box): def __init__(self): logging.debug('STARTUP: Loading the desktop window') - Gtk.ApplicationWindow.__init__(self) - self.set_has_resize_grip(False) - - accel_group = Gtk.AccelGroup() - self.sugar_accel_group = accel_group - self.add_accel_group(accel_group) + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.add_css_class('home-window') + self.add_css_class('background') self._active = False self._fully_obscured = True - screen = self.get_screen() - screen.connect('size-changed', self.__screen_size_changed_cb) - self.set_default_size(screen.get_width(), - screen.get_height()) + display = Gdk.Display.get_default() + self._monitors = display.get_monitors() + self._monitors_hid = self._monitors.connect('items-changed', self.__screen_size_changed_cb) + self._monitor_geom_hid = None + self._current_monitor = None + if self._monitors.get_n_items() > 0: + self._current_monitor = self._monitors.get_item(0) + self._monitor_geom_hid = self._current_monitor.connect('notify::geometry', self.__screen_size_changed_cb) + geom = self._current_monitor.get_geometry() + self.set_size_request(geom.width, geom.height) icons_path = os.path.join(config.data_path, 'icons') - Gtk.IconTheme.get_for_screen(screen).append_search_path(icons_path) - - self.__screen_size_changed_cb(None) + icon_theme = Gtk.IconTheme.get_for_display(display) + icon_theme.add_search_path(icons_path) - self.realize() self._busy_count = 0 self.busy() - self.set_type_hint(Gdk.WindowTypeHint.DESKTOP) - self.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) + key_controller = Gtk.EventControllerKey.new() + key_controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + key_controller.connect('key-pressed', self.__key_pressed_cb) + key_controller.connect('key-released', self.__key_released_cb) + self.add_controller(key_controller) - self.add_events(Gdk.EventMask.VISIBILITY_NOTIFY_MASK | - Gdk.EventMask.BUTTON_PRESS_MASK) - self.connect('visibility-notify-event', - self._visibility_notify_event_cb) - self.connect('map-event', self.__map_event_cb) - self.connect('key-press-event', self.__key_press_event_cb) - self.connect('key-release-event', self.__key_release_event_cb) + focus_controller = Gtk.EventControllerFocus.new() + focus_controller.connect('leave', self.__focus_out_event_cb) + self.add_controller(focus_controller) self._box = HomeBackgroundBox() + self._box.set_hexpand(True) self._toolbar = ViewToolbar() - self._box.pack_start(self._toolbar, False, True, 0) - self._toolbar.show() + self._toolbar.add_css_class('toolbar') + self._box.append(self._toolbar) self._alert = None + self._view_stack = Gtk.Stack() + self._view_stack.set_transition_type(Gtk.StackTransitionType.NONE) + self._view_stack.set_vexpand(True) + self._box.append(self._view_stack) + self._home_box = HomeBox(self._toolbar) - self._box.pack_start(self._home_box, True, True, 0) - self._home_box.show() + self._view_stack.add_named(self._home_box, 'home') self._toolbar.show_view_buttons() - # Loads the Gsettings value for activity 'resume-mode' setting = Gio.Settings.new('org.sugarlabs.user') self._resume_mode = setting.get_boolean('resume-activity') self._home_box.set_resume_mode(self._resume_mode) self._group_box = GroupBox(self._toolbar) + self._view_stack.add_named(self._group_box, 'group') + self._mesh_box = MeshBox(self._toolbar) + self._view_stack.add_named(self._mesh_box, 'mesh') + self._transition_box = TransitionBox() + self._view_stack.add_named(self._transition_box, 'transition') - self.add(self._box) - self._box.show() + self._view_stack.set_visible_child_name('home') + + self.append(self._box) self._transition_box.connect('completed', self._transition_completed_cb) - shell.get_model().zoom_level_changed.connect( + self._zoom_hid = shell.get_model().zoom_level_changed.connect( self.__zoom_level_changed_cb) - self._alt_timeout_sid = None + self._alt_held = False def add_alert(self, alert): self._alert = alert @@ -128,8 +145,8 @@ def remove_alert(self, alert): def _show_alert(self): if self._alert: - self._box.pack_start(self._alert, False, False, 0) - self._box.reorder_child(self._alert, 1) + self._box.append(self._alert) + self._box.reorder_child_after(self._alert, self._toolbar) def _hide_alert(self): if self._alert: @@ -143,24 +160,16 @@ def _deactivate_view(self, level): elif level == ShellModel.ZOOM_MESH: self._mesh_box.suspend() - def __screen_size_changed_cb(self, screen): - screen = Gdk.Screen.get_default() - n = screen.get_number() - rect = screen.get_monitor_geometry(n) - geometry = Gdk.Geometry() - geometry.max_width = geometry.base_width = geometry.min_width = \ - rect.width - geometry.max_height = geometry.base_height = geometry.min_height = \ - rect.height - geometry.width_inc = geometry.height_inc = geometry.min_aspect = \ - geometry.max_aspect = 1 - hints = Gdk.WindowHints(Gdk.WindowHints.ASPECT | - Gdk.WindowHints.BASE_SIZE | - Gdk.WindowHints.MAX_SIZE | - Gdk.WindowHints.MIN_SIZE) - workarea = screen.get_monitor_workarea(n) - self.move(workarea.x, workarea.y) - self.set_geometry_hints(None, geometry, hints) + def __screen_size_changed_cb(self, *args): + if self._monitors.get_n_items() > 0: + monitor = self._monitors.get_item(0) + if self._current_monitor != monitor: + if self._current_monitor and self._monitor_geom_hid: + self._current_monitor.disconnect(self._monitor_geom_hid) + self._current_monitor = monitor + self._monitor_geom_hid = monitor.connect('notify::geometry', self.__screen_size_changed_cb) + geom = monitor.get_geometry() + self.set_size_request(geom.width, geom.height) def _activate_view(self, level): if level == ShellModel.ZOOM_HOME: @@ -168,65 +177,50 @@ def _activate_view(self, level): elif level == ShellModel.ZOOM_MESH: self._mesh_box.resume() - def _visibility_notify_event_cb(self, window, event): - fully_obscured = ( - event.get_state() == Gdk.VisibilityState.FULLY_OBSCURED) - if self._fully_obscured == fully_obscured: - return - self._fully_obscured = fully_obscured - - if fully_obscured: - self._deactivate_view(shell.get_model().zoom_level) - else: - self._activate_view(shell.get_model().zoom_level) - - def __is_alt(self, event): - # When shift is on, becomes - shift = (event.state & Gdk.ModifierType.SHIFT_MASK) == 1 - return event.keyval in [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R] or \ - event.keyval in [Gdk.KEY_Meta_L, Gdk.KEY_Meta_R] and shift + def __is_alt(self, keyval, state): + # with Shift held, Alt reports as Meta + shift = (state & Gdk.ModifierType.SHIFT_MASK) != 0 + return keyval in [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R] or \ + keyval in [Gdk.KEY_Meta_L, Gdk.KEY_Meta_R] and shift - def __key_press_event_cb(self, window, event): - if self.__is_alt(event) and not self._alt_timeout_sid: + def __key_pressed_cb(self, controller, keyval, keycode, state): + if self.__is_alt(keyval, state) and not getattr(self, '_alt_held', False): self._home_box.set_resume_mode(not self._resume_mode) - self._alt_timeout_sid = GLib.timeout_add(100, - self.__alt_timeout_cb) + self._alt_held = True - if not self._toolbar.search_entry.props.has_focus: - self._toolbar.search_entry.grab_focus() + _modifier_mask = (Gdk.ModifierType.CONTROL_MASK | + Gdk.ModifierType.ALT_MASK | + Gdk.ModifierType.SUPER_MASK | + Gdk.ModifierType.META_MASK) + has_modifier = bool(state & _modifier_mask) - return False + unicode_char = Gdk.keyval_to_unicode(keyval) + is_printable = unicode_char >= 0x20 and unicode_char != 0x7F - def __key_release_event_cb(self, window, event): - if self.__is_alt(event) and self._alt_timeout_sid: - self._home_box.set_resume_mode(self._resume_mode) - GLib.source_remove(self._alt_timeout_sid) - self._alt_timeout_sid = None + if is_printable and not has_modifier: + search_entry = self._toolbar.search_entry + if search_entry is None: + return False - return False + has_focus = bool(search_entry.get_state_flags() & Gtk.StateFlags.FOCUS_WITHIN) - def __alt_timeout_cb(self): - display = Gdk.Display.get_default() - screen_, x_, y_, modmask = display.get_pointer() - if modmask & Gdk.ModifierType.MOD1_MASK: - return True + if not has_focus: + search_entry.grab_focus() - self._home_box.set_resume_mode(self._resume_mode) - if self._alt_timeout_sid: - GLib.source_remove(self._alt_timeout_sid) - self._alt_timeout_sid = None + return False + + def __key_released_cb(self, controller, keyval, keycode, state): + if self.__is_alt(keyval, state) and getattr(self, '_alt_held', False): + self._home_box.set_resume_mode(self._resume_mode) + self._alt_held = False return False - def __map_event_cb(self, widget, event): - # have to make the desktop window active - # since metacity doesn't make it on startup - timestamp = event.get_time() - window = self.get_window() - if not timestamp: - timestamp = Gtk.get_current_event_time() - window.focus(timestamp) + def __focus_out_event_cb(self, controller): + if getattr(self, '_alt_held', False): + self._home_box.set_resume_mode(self._resume_mode) + self._alt_held = False def __zoom_level_changed_cb(self, **kwargs): old_level = kwargs['old_level'] @@ -238,11 +232,7 @@ def __zoom_level_changed_cb(self, **kwargs): if old_level != ShellModel.ZOOM_ACTIVITY and \ new_level != ShellModel.ZOOM_ACTIVITY: self._hide_alert() - children = self._box.get_children() - if len(children) >= 2: - self._box.remove(children[1]) - self._box.pack_start(self._transition_box, True, True, 0) - self._transition_box.show() + self._view_stack.set_visible_child_name('transition') if new_level == ShellModel.ZOOM_HOME: end_size = style.XLARGE_ICON_SIZE @@ -270,47 +260,57 @@ def _update_view(self, level): return self._hide_alert() - children = self._box.get_children() - if len(children) >= 2: - self._box.remove(children[1]) if level == ShellModel.ZOOM_HOME: - self._box.pack_start(self._home_box, True, True, 0) - self._home_box.show() + self._view_stack.set_visible_child_name('home') self._toolbar.clear_query() self._toolbar.set_placeholder_text_for_view(_('Home')) self._toolbar.show_view_buttons() + GLib.idle_add(self._home_box.grab_focus) elif level == ShellModel.ZOOM_GROUP: - self._box.pack_start(self._group_box, True, True, 0) - self._group_box.show() + self._view_stack.set_visible_child_name('group') self._toolbar.clear_query() self._toolbar.set_placeholder_text_for_view(_('Group')) - self._toolbar.hide_view_buttons() + self._toolbar.show_view_buttons() + GLib.idle_add(self._group_box.grab_focus) elif level == ShellModel.ZOOM_MESH: - self._box.pack_start(self._mesh_box, True, True, 0) - self._mesh_box.show() + self._view_stack.set_visible_child_name('mesh') self._toolbar.clear_query() self._toolbar.set_placeholder_text_for_view(_('Neighborhood')) self._toolbar.hide_view_buttons() + GLib.idle_add(self._mesh_box.grab_focus) self._show_alert() + def do_dispose(self): + if hasattr(self, '_zoom_hid'): + shell.get_model().zoom_level_changed.disconnect(self._zoom_hid) + del self._zoom_hid + if hasattr(self, '_monitors_hid') and self._monitors: + self._monitors.disconnect(self._monitors_hid) + del self._monitors_hid + if hasattr(self, '_monitor_geom_hid') and self._monitor_geom_hid and self._current_monitor: + self._current_monitor.disconnect(self._monitor_geom_hid) + self._monitor_geom_hid = None + self._current_monitor = None + super().do_dispose() + def get_home_box(self): return self._home_box def busy(self): if self._busy_count == 0: - self._old_cursor = self.get_window().get_cursor() - self._set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) + self._old_cursor = self.get_cursor() + self._set_cursor(Gdk.Cursor.new_from_name('wait')) self._busy_count += 1 def unbusy(self): self._busy_count -= 1 if self._busy_count == 0: + # Restore previous cursor — None means system default (arrow) self._set_cursor(self._old_cursor) def _set_cursor(self, cursor): - self.get_window().set_cursor(cursor) - Gdk.flush() + self.set_cursor(cursor) def get_instance(): @@ -318,3 +318,4 @@ def get_instance(): if not _instance: _instance = HomeWindow() return _instance + diff --git a/src/jarabe/desktop/keydialog.py b/src/jarabe/desktop/keydialog.py index f1f0d7b357..5a3449b8e3 100644 --- a/src/jarabe/desktop/keydialog.py +++ b/src/jarabe/desktop/keydialog.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2009 One Laptop per Child # # This program is free software: you can redistribute it and/or modify @@ -18,13 +18,12 @@ from gettext import gettext as _ from gi.repository import Gtk -from gi.repository import Gdk + import dbus -from sugar4.graphics import style -from jarabe.model import network +from jarabe.model import network IW_AUTH_ALG_OPEN_SYSTEM = 'open' IW_AUTH_ALG_SHARED_KEY = 'shared' @@ -64,8 +63,9 @@ def hash_passphrase(passphrase): elif len(passphrase) < 64: while len(passphrase) < 64: passphrase += passphrase[:64 - len(passphrase)] - passphrase = hashlib.md5(passphrase).digest() - return string_to_hex(passphrase)[:26] + # Use hexdigest() directly: digest() returns raw bytes that are not + # guaranteed to be valid UTF-8, causing UnicodeDecodeError in Python 3. + return hashlib.md5(passphrase.encode('utf-8')).hexdigest()[:26] class CanceledKeyRequestError(dbus.DBusException): @@ -78,7 +78,8 @@ def __init__(self): class KeyDialog(Gtk.Dialog): def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response): - Gtk.Dialog.__init__(self, flags=Gtk.DialogFlags.MODAL) + super().__init__() + self.set_modal(True) self.set_title('Wireless Key Required') self._response = response @@ -89,30 +90,38 @@ def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response): self._rsn_flags = rsn_flags self._dev_caps = dev_caps + # Set spacing once for all children in the content area + self.get_content_area().set_spacing(6) + display_name = network.ssid_to_display_name(ssid) label = Gtk.Label(label=_("A wireless encryption key is required for\n" " the wireless network '%s'.") % (display_name, )) - self.vbox.pack_start(label, True, True, 0) + label.set_hexpand(True) + label.set_vexpand(True) + self.get_content_area().append(label) + label.set_visible(True) - self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK) + self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) + self.add_button(_("OK"), Gtk.ResponseType.OK) self.set_default_response(Gtk.ResponseType.OK) def add_key_entry(self): self._entry = Gtk.Entry(visibility=True) self._entry.connect('changed', self._update_response_sensitivity) self._entry.connect('activate', self.__entry_activate_cb) - self.vbox.pack_start(self._entry, True, True, 0) - self.vbox.set_spacing(6) + self._entry.set_hexpand(True) + self._entry.set_vexpand(True) + self.get_content_area().append(self._entry) + self._entry.set_visible(True) - button = Gtk.CheckButton(_("Show Password")) - button.props.draw_indicator = True - button.props.active = self._entry.get_visibility() + button = Gtk.CheckButton(label=_("Show Password")) + button.set_active(self._entry.get_visibility()) button.connect("toggled", self.__button_toggled_cb) - self.vbox.pack_start(button, True, True, 0) - - self.vbox.show_all() + button.set_hexpand(True) + button.set_vexpand(True) + self.get_content_area().append(button) + button.set_visible(True) self._update_response_sensitivity() self._entry.grab_focus() @@ -137,68 +146,84 @@ def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response): dev_caps, response) # WEP key type - self.key_store = Gtk.ListStore(str, int) - self.key_store.append(['Passphrase (128-bit)', WEP_PASSPHRASE]) - self.key_store.append(['Hex (40/128-bit)', WEP_HEX]) - self.key_store.append(['ASCII (40/128-bit)', WEP_ASCII]) - - self.key_combo = Gtk.ComboBox(model=self.key_store) - cell = Gtk.CellRendererText() - self.key_combo.pack_start(cell, True) - self.key_combo.add_attribute(cell, 'text', 0) - self.key_combo.set_active(0) - self.key_combo.connect('changed', self.__key_combo_changed_cb) + self.key_combo = Gtk.DropDown.new_from_strings([ + _('Passphrase (128-bit)'), + _('Hex (40/128-bit)'), + _('ASCII (40/128-bit)') + ]) + self.key_combo.connect('notify::selected', self.__key_combo_changed_cb) + self.key_combo.set_hexpand(True) + self.key_combo.set_vexpand(True) hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(Gtk.Label(_('Key Type:')), True, True, 0) - hbox.pack_start(self.key_combo, True, True, 0) - hbox.show_all() - self.vbox.pack_start(hbox, True, True, 0) + label1 = Gtk.Label(label=_('Key Type:')) + label1.set_hexpand(True) + label1.set_vexpand(True) + hbox.append(label1) + label1.set_visible(True) + + hbox.append(self.key_combo) + self.key_combo.set_visible(True) + + hbox.set_hexpand(True) + hbox.set_vexpand(True) + self.get_content_area().append(hbox) + hbox.set_visible(True) # Key entry field self.add_key_entry() # WEP authentication mode - self.auth_store = Gtk.ListStore(str, str) - self.auth_store.append(['Open System', IW_AUTH_ALG_OPEN_SYSTEM]) - self.auth_store.append(['Shared Key', IW_AUTH_ALG_SHARED_KEY]) - - self.auth_combo = Gtk.ComboBox(model=self.auth_store) - cell = Gtk.CellRendererText() - self.auth_combo.pack_start(cell, True) - self.auth_combo.add_attribute(cell, 'text', 0) - self.auth_combo.set_active(0) - - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.pack_start(Gtk.Label(_('Authentication Type:')), True, True, 0) - hbox.pack_start(self.auth_combo, True, True, 0) - hbox.show_all() - - self.vbox.pack_start(hbox, True, True, 0) - - def __key_combo_changed_cb(self, widget): + self.auth_combo = Gtk.DropDown.new_from_strings([ + _('Open System'), + _('Shared Key') + ]) + self.auth_combo.set_hexpand(True) + self.auth_combo.set_vexpand(True) + + hbox2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + label2 = Gtk.Label(label=_('Authentication Type:')) + label2.set_hexpand(True) + label2.set_vexpand(True) + hbox2.append(label2) + label2.set_visible(True) + + hbox2.append(self.auth_combo) + self.auth_combo.set_visible(True) + + hbox2.set_hexpand(True) + hbox2.set_vexpand(True) + self.get_content_area().append(hbox2) + hbox2.set_visible(True) + + def __key_combo_changed_cb(self, widget, pspec): self._update_response_sensitivity() def _get_security(self): key = self._entry.get_text() - it = self.key_combo.get_active_iter() - (key_type, ) = self.key_store.get(it, 1) + selected_key = self.key_combo.get_selected() + if selected_key == 0: + key_type = WEP_PASSPHRASE + elif selected_key == 1: + key_type = WEP_HEX + else: + key_type = WEP_ASCII if key_type == WEP_PASSPHRASE: key = hash_passphrase(key) elif key_type == WEP_ASCII: key = string_to_hex(key) - it = self.auth_combo.get_active_iter() - (auth_alg, ) = self.auth_store.get(it, 1) + selected_auth = self.auth_combo.get_selected() + auth_alg = IW_AUTH_ALG_OPEN_SYSTEM if selected_auth == 0 else IW_AUTH_ALG_SHARED_KEY return (key, auth_alg) def print_security(self): (key, auth_alg) = self._get_security() print('Key: %s' % key) - print('Auth: %d' % auth_alg) + print('Auth: %s' % auth_alg) def create_security(self): (key, auth_alg) = self._get_security() @@ -207,8 +232,14 @@ def create_security(self): def _update_response_sensitivity(self, ignored=None): key = self._entry.get_text() - it = self.key_combo.get_active_iter() - (key_type, ) = self.key_store.get(it, 1) + + selected_key = self.key_combo.get_selected() + if selected_key == 0: + key_type = WEP_PASSPHRASE + elif selected_key == 1: + key_type = WEP_HEX + else: + key_type = WEP_ASCII valid = False if key_type == WEP_PASSPHRASE: @@ -233,21 +264,24 @@ def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response): dev_caps, response) self.add_key_entry() - self.store = Gtk.ListStore(str) - self.store.append([_('WPA & WPA2 Personal')]) - - self.combo = Gtk.ComboBox(model=self.store) - cell = Gtk.CellRendererText() - self.combo.pack_start(cell, True) - self.combo.add_attribute(cell, 'text', 0) - self.combo.set_active(0) + self.combo = Gtk.DropDown.new_from_strings([_('WPA & WPA2 Personal')]) + self.combo.set_hexpand(True) + self.combo.set_vexpand(True) self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.hbox.pack_start(Gtk.Label(_('Wireless Security:')), True, True, 0) - self.hbox.pack_start(self.combo, True, True, 0) - self.hbox.show_all() - - self.vbox.pack_start(self.hbox, True, True, 0) + label = Gtk.Label(label=_('Wireless Security:')) + label.set_hexpand(True) + label.set_vexpand(True) + self.hbox.append(label) + label.set_visible(True) + + self.hbox.append(self.combo) + self.combo.set_visible(True) + + self.hbox.set_hexpand(True) + self.hbox.set_vexpand(True) + self.get_content_area().append(self.hbox) + self.hbox.set_visible(True) def _get_security(self): return self._entry.get_text() @@ -285,10 +319,7 @@ def create(ssid, flags, wpa_flags, rsn_flags, dev_caps, response): dev_caps, response) key_dialog.connect('response', _key_dialog_response_cb) - key_dialog.show_all() - width, height = key_dialog.get_size() - key_dialog.move(Gdk.Screen.width() / 2 - width / 2, - style.GRID_CELL_SIZE * 2) + key_dialog.set_visible(True) def _key_dialog_response_cb(key_dialog, response_id): @@ -298,8 +329,8 @@ def _key_dialog_response_cb(key_dialog, response_id): secrets = key_dialog.create_security() if response_id in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.NONE, - Gtk.ResponseType.DELETE_EVENT]: - # key dialog dialog was canceled; send the error back to NM + Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT]: + # key dialog was canceled; send the error back to NM response.set_error(CanceledKeyRequestError()) elif response_id == Gtk.ResponseType.OK: if not secrets: @@ -308,4 +339,4 @@ def _key_dialog_response_cb(key_dialog, response_id): else: raise RuntimeError('Unhandled key dialog response %d' % response_id) - key_dialog.destroy() + key_dialog.close() diff --git a/src/jarabe/desktop/meshbox.py b/src/jarabe/desktop/meshbox.py index 9c73f80941..21f5efdc00 100644 --- a/src/jarabe/desktop/meshbox.py +++ b/src/jarabe/desktop/meshbox.py @@ -46,7 +46,6 @@ from jarabe.model.adhoc import get_adhoc_manager_instance from jarabe.journal import misc - _FILTERED_ALPHA = 0.33 @@ -82,19 +81,19 @@ def create_palette(self): item = PaletteMenuItem(_('Resume')) icon = Icon( pixel_size=style.SMALL_ICON_SIZE, icon_name='activity-start') - item.set_image(icon) - item.connect('activate', self.__palette_item_clicked_cb) + item.set_icon_widget(icon) + item.connect('item-activated', self.__palette_item_clicked_cb) menu_box.append_item(item) elif not private and is_joinable: item = PaletteMenuItem(_('Join')) icon = Icon( pixel_size=style.SMALL_ICON_SIZE, icon_name='activity-start') - item.set_image(icon) - item.connect('activate', self.__palette_item_clicked_cb) + item.set_icon_widget(icon) + item.connect('item-activated', self.__palette_item_clicked_cb) menu_box.append_item(item) + menu_box.set_visible(True) palette.set_content(menu_box) - menu_box.show_all() self.connect_to_palette_pop_events(palette) return palette @@ -117,7 +116,6 @@ def __init__(self, model): self._icons = {} self._icon = self._create_icon() - self._icon.show() self.add_icon(self._icon, center=True) self._icon.palette_invoker.cache_palette = False @@ -147,13 +145,11 @@ def _add_buddy(self, buddy): icon = BuddyIcon(buddy, style.STANDARD_ICON_SIZE) self._icons[buddy.props.key] = icon self.add_icon(icon) - icon.show() def __buddy_removed_cb(self, activity, buddy): icon = self._icons[buddy.props.key] del self._icons[buddy.props.key] self.remove(icon) - icon.destroy() def set_filter(self, query): text_to_check = self._model.bundle.get_name().lower() + \ @@ -180,7 +176,7 @@ class DeviceObserver(GObject.GObject): } def __init__(self, device): - GObject.GObject.__init__(self) + super().__init__() self._bus = dbus.SystemBus() self.device = device @@ -369,7 +365,7 @@ def __init__(self, toolbar): owner_icon = BuddyIcon(get_owner_instance(), style.STANDARD_ICON_SIZE & ~1) ViewContainer.__init__(self, layout, owner_icon) - self.set_can_focus(False) + self.set_focusable(False) self.wireless_networks = {} self._adhoc_manager = None @@ -384,8 +380,7 @@ def __init__(self, toolbar): self._query = '' toolbar.connect('query-changed', self._toolbar_query_changed_cb) - toolbar.search_entry.connect('icon-press', - self.__clear_icon_pressed_cb) + for buddy_model in self._model.get_buddies(): self._add_buddy(buddy_model) @@ -423,7 +418,6 @@ def _add_buddy(self, buddy_model): return icon = BuddyIcon(buddy_model) self.add(icon) - icon.show() if hasattr(icon, 'set_filter'): icon.set_filter(self._query) @@ -448,7 +442,6 @@ def __buddy_notify_current_activity_cb(self, buddy_model, pspec): def _add_activity(self, activity_model): icon = ActivityView(activity_model) self.add(icon) - icon.show() if hasattr(icon, 'set_filter'): icon.set_filter(self._query) @@ -471,7 +464,6 @@ def _add_ap_to_network(self, ap): icon = WirelessNetworkView(ap) self.wireless_networks[hash_value] = icon self.add(icon) - icon.show() if hasattr(icon, 'set_filter'): icon.set_filter(self._query) @@ -565,13 +557,11 @@ def remove_adhoc_networks(self): def _add_adhoc_network_icon(self, channel): icon = SugarAdhocView(channel) self.add(icon) - icon.show() self._adhoc_networks.append(icon) def _add_olpc_mesh_icon(self, mesh_mgr, channel): icon = OlpcMeshView(mesh_mgr, channel) self.add(icon) - icon.show() self._mesh.append(icon) def enable_olpc_mesh(self, mesh_device): @@ -616,5 +606,5 @@ def _toolbar_query_changed_cb(self, toolbar, query): if hasattr(icon, 'set_filter'): icon.set_filter(self._query) - def __clear_icon_pressed_cb(self, entry, icon_pos, event): + def __clear_icon_pressed_cb(self, entry, icon_pos): self.grab_focus() diff --git a/src/jarabe/desktop/networkviews.py b/src/jarabe/desktop/networkviews.py index 5d1b5c8965..b791bb387c 100644 --- a/src/jarabe/desktop/networkviews.py +++ b/src/jarabe/desktop/networkviews.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer # Copyright (C) 2009-2010 One Laptop per Child # @@ -42,7 +42,6 @@ from jarabe.model.network import WirelessSecurity from jarabe.model.adhoc import get_adhoc_manager_instance - _AP_ICON_NAME = 'network-wireless' _OLPC_MESH_ICON_NAME = 'network-mesh' @@ -52,7 +51,14 @@ class WirelessNetworkView(EventPulsingIcon): def __init__(self, initial_ap): - EventPulsingIcon.__init__(self, pixel_size=style.STANDARD_ICON_SIZE, + if initial_ap.mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(initial_ap.ssid): + icon_name = 'network-adhoc-1' # Default channel + else: + icon_name = get_icon_state(_AP_ICON_NAME, initial_ap.strength) + + EventPulsingIcon.__init__(self, icon_name=icon_name, + pixel_size=style.STANDARD_ICON_SIZE, cache=True) self._bus = dbus.SystemBus() self._access_points = {initial_ap.model.object_path: initial_ap} @@ -126,27 +132,24 @@ def _create_palette(self): self.menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._connect_item = PaletteMenuItem(_('Connect')) - icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='dialog-ok') - self._connect_item.set_image(icon) - self._connect_item.connect('activate', self.__connect_activate_cb) - self.menu_box.add(self._connect_item) + self._connect_item = PaletteMenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('item-activated', self.__connect_activate_cb) + self.menu_box.append(self._connect_item) + self._connect_item.set_visible(True) - self._disconnect_item = PaletteMenuItem(_('Disconnect')) - icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='media-eject') - self._disconnect_item.set_image(icon) + self._disconnect_item = PaletteMenuItem(_('Disconnect'), 'media-eject') self._disconnect_item.connect( - 'activate', self.__disconnect_activate_cb) - self.menu_box.add(self._disconnect_item) + 'item-activated', self.__disconnect_activate_cb) + self.menu_box.append(self._disconnect_item) + self._disconnect_item.set_visible(True) - self._forget_item = PaletteMenuItem(_('Forget')) - icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='list-remove') - self._forget_item.set_image(icon) - self._forget_item.connect('activate', self.__forget_activate_cb) - self.menu_box.add(self._forget_item) + self._forget_item = PaletteMenuItem(_('Forget'), 'list-remove') + self._forget_item.connect('item-activated', self.__forget_activate_cb) + self.menu_box.append(self._forget_item) + self._forget_item.set_visible(True) p.set_content(self.menu_box) - self.menu_box.show_all() + self.menu_box.set_visible(True) self.connect_to_palette_pop_events(p) @@ -270,21 +273,21 @@ def _update_state(self): state == network.NM_DEVICE_STATE_NEED_AUTH or \ state == network.NM_DEVICE_STATE_IP_CONFIG: if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() + self._disconnect_item.set_visible(True) + self._connect_item.set_visible(False) self._palette.props.secondary_text = _('Connecting...') self.props.pulsing = True elif state == network.NM_DEVICE_STATE_ACTIVATED: network.set_connected() if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() + self._disconnect_item.set_visible(True) + self._connect_item.set_visible(False) self._palette.props.secondary_text = _('Connected') self.props.pulsing = False else: if self._disconnect_item: - self._disconnect_item.hide() - self._connect_item.show() + self._disconnect_item.set_visible(False) + self._connect_item.set_visible(True) self._palette.props.secondary_text = None self.props.pulsing = False @@ -292,9 +295,9 @@ def _update_color(self): self.props.base_color = self._color if self._filtered: self.props.pulsing = False - self.props.alpha = _FILTERED_ALPHA + self.set_opacity(_FILTERED_ALPHA) else: - self.props.alpha = 1.0 + self.set_opacity(1.0) def __disconnect_activate_cb(self, item): ap_paths = list(self._access_points.keys()) @@ -529,22 +532,19 @@ def _create_palette(self): self.menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._connect_item = PaletteMenuItem(_('Connect')) - icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='dialog-ok') - self._connect_item.set_image(icon) - self._connect_item.connect('activate', self.__connect_activate_cb) - self.menu_box.add(self._connect_item) + self._connect_item = PaletteMenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('item-activated', self.__connect_activate_cb) + self.menu_box.append(self._connect_item) + self._connect_item.set_visible(True) - self._disconnect_item = PaletteMenuItem(_('Disconnect')) - icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='media-eject') - self._disconnect_item.set_image(icon) + self._disconnect_item = PaletteMenuItem(_('Disconnect'), 'media-eject') self._disconnect_item.connect( - 'activate', self.__disconnect_activate_cb) - self.menu_box.add(self._disconnect_item) + 'item-activated', self.__disconnect_activate_cb) + self.menu_box.append(self._disconnect_item) + self._disconnect_item.set_visible(False) palette_.set_content(self.menu_box) - self.menu_box.show_all() - self._disconnect_item.hide() + self.menu_box.set_visible(True) self.connect_to_palette_pop_events(palette_) @@ -575,20 +575,20 @@ def __state_changed_cb(self, adhoc_manager, channel, device_state): if (state >= network.NM_DEVICE_STATE_PREPARE) and \ (state <= network.NM_DEVICE_STATE_IP_CONFIG): if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() + self._disconnect_item.set_visible(True) + self._connect_item.set_visible(False) self._palette.props.secondary_text = _('Connecting...') self.props.pulsing = True elif state == network.NM_DEVICE_STATE_ACTIVATED: if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() + self._disconnect_item.set_visible(True) + self._connect_item.set_visible(False) self._palette.props.secondary_text = _('Connected') self.props.pulsing = False else: if self._disconnect_item: - self._disconnect_item.hide() - self._connect_item.show() + self._disconnect_item.set_visible(False) + self._connect_item.set_visible(True) self._palette.props.secondary_text = None self.props.pulsing = False self._update_color() @@ -597,9 +597,9 @@ def _update_color(self): self.props.base_color = self._state_color if self._filtered: self.props.pulsing = False - self.alpha = _FILTERED_ALPHA + self.set_opacity(_FILTERED_ALPHA) else: - self.alpha = 1.0 + self.set_opacity(1.0) def __members_changed_cb(self, adhoc_manager, channel, has_members): if channel == self._channel: @@ -673,14 +673,13 @@ def _create_palette(self): self.menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._connect_item = PaletteMenuItem(_('Connect')) - icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name='dialog-ok') - self._connect_item.set_image(icon) - self._connect_item.connect('activate', self.__connect_activate_cb) - self.menu_box.add(self._connect_item) + self._connect_item = PaletteMenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('item-activated', self.__connect_activate_cb) + self.menu_box.append(self._connect_item) + self._connect_item.set_visible(True) _palette.set_content(self.menu_box) - self.menu_box.show_all() + self.menu_box.set_visible(True) return _palette @@ -720,29 +719,29 @@ def _update(self): network.NM_DEVICE_STATE_NEED_AUTH, network.NM_DEVICE_STATE_IP_CONFIG]: if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() + self._disconnect_item.set_visible(True) + self._connect_item.set_visible(False) self._palette.props.secondary_text = _('Connecting...') self.props.pulsing = True elif state == network.NM_DEVICE_STATE_ACTIVATED: if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() + self._disconnect_item.set_visible(True) + self._connect_item.set_visible(False) self._palette.props.secondary_text = _('Connected') self.props.pulsing = False else: if self._disconnect_item: - self._disconnect_item.hide() - self._connect_item.show() + self._disconnect_item.set_visible(False) + self._connect_item.set_visible(True) self._palette.props.secondary_text = None self.props.pulsing = False def _update_color(self): self.props.base_color = profile.get_color() if self._filtered: - self.alpha = _FILTERED_ALPHA + self.set_opacity(_FILTERED_ALPHA) else: - self.alpha = 1.0 + self.set_opacity(1.0) def __connect_activate_cb(self, icon): self._connect() diff --git a/src/jarabe/desktop/schoolserver.py b/src/jarabe/desktop/schoolserver.py index 89b7ea5efc..599b5a2fe4 100644 --- a/src/jarabe/desktop/schoolserver.py +++ b/src/jarabe/desktop/schoolserver.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, 2008 One Laptop Per Child +# Copyright (C) 2007, 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/jarabe/desktop/snowflakelayout.py b/src/jarabe/desktop/snowflakelayout.py index 0ea36e37ba..2cefd1301e 100644 --- a/src/jarabe/desktop/snowflakelayout.py +++ b/src/jarabe/desktop/snowflakelayout.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,49 +17,34 @@ from gi.repository import Gtk from gi.repository import Gdk +import gi +gi.require_version('Gsk', '4.0') +gi.require_version('Graphene', '1.0') +from gi.repository import Gsk, Graphene from sugar4.graphics import style - _BASE_DISTANCE = style.zoom(25) _CHILDREN_FACTOR = style.zoom(3) -class SnowflakeLayout(Gtk.Container): +class SnowflakeLayout(Gtk.Widget): __gtype_name__ = 'SugarSnowflakeLayout' def __init__(self): - Gtk.Container.__init__(self) - self.set_has_window(False) + super().__init__() self._nflakes = 0 self._children = {} - def do_realize(self): - self.set_realized(True) - self.set_window(self.get_parent_window()) + def do_dispose(self): for child in list(self._children.keys()): - child.set_parent_window(self.get_parent_window()) - self.queue_resize() + child.unparent() + self._children.clear() + self._nflakes = 0 - def do_add(self, child): - if child.get_realized(): - child.set_parent_window(self.get_parent_window()) + def add(self, child): child.set_parent(self) - def do_forall(self, include_internals, callback): - for child in list(self._children.keys()): - callback(child) - - def do_remove(self, child): - child.unparent() - - def add_icon(self, child, center=False): - if not center: - self._nflakes += 1 - - self._children[child] = center - self.add(child) - def remove(self, child): if child not in self._children: return @@ -68,44 +53,40 @@ def remove(self, child): self._nflakes -= 1 del self._children[child] - self.remove(child) + child.unparent() - def do_get_preferred_size(self): - size = self._calculate_size() - requisition = Gtk.Requisition() - requisition.width = size - requisition.height = size - return (requisition, requisition) + def add_icon(self, child, center=False): + if not center: + self._nflakes += 1 - def do_get_preferred_width(self): - size = self._calculate_size() - return (size, size) + self._children[child] = center + self.add(child) - def do_get_preferred_height(self): - size = self._calculate_size() - return (size, size) + def _child_size(self, child): + _, nat_w, _, _ = child.measure(Gtk.Orientation.HORIZONTAL, -1) + _, nat_h, _, _ = child.measure(Gtk.Orientation.VERTICAL, -1) + return nat_w, nat_h - def do_size_allocate(self, allocation): - self.set_allocation(allocation) + def do_measure(self, orientation, for_size): + size = self._calculate_size() + return (size, size, -1, -1) + def do_size_allocate(self, width, height, baseline): r = self._get_radius() index = 0 for child, centered in list(self._children.items()): - child_request = child.size_request() - child_width, child_height = \ - child_request.width, child_request.height + child_width, child_height = self._child_size(child) + rect = Gdk.Rectangle() - rect.x = 0 - rect.y = 0 rect.width = child_width rect.height = child_height - width = allocation.width - child_width - height = allocation.height - child_height + w = width - child_width + h = height - child_height if centered: - rect.x = allocation.x + width / 2 - rect.y = allocation.y + height / 2 + rect.x = w / 2 + rect.y = h / 2 else: angle = 2 * math.pi * index / self._nflakes @@ -115,30 +96,30 @@ def do_size_allocate(self, allocation): dx = math.cos(angle) * r dy = math.sin(angle) * r - rect.x = int(allocation.x + width / 2 + dx) - rect.y = int(allocation.y + height / 2 + dy) + rect.x = int(w / 2 + dx) + rect.y = int(h / 2 + dy) index += 1 - child.size_allocate(rect) + transform = Gsk.Transform.new().translate(Graphene.Point().init(rect.x, rect.y)) + child.allocate(rect.width, rect.height, -1, transform) def _get_radius(self): radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes) for child, centered in list(self._children.items()): if centered: - child_request = child.size_request() - child_width, child_height = \ - child_request.width, child_request.height + child_width, child_height = self._child_size(child) radius += max(child_width, child_height) / 2 - return radius def _calculate_size(self): thickness = 0 for child in list(self._children.keys()): - child_request = child.size_request() - child_width, child_height = \ - child_request.width, child_request.height + child_width, child_height = self._child_size(child) thickness = max(thickness, max(child_width, child_height)) return self._get_radius() * 2 + thickness + + def do_snapshot(self, snapshot): + for child in list(self._children.keys()): + self.snapshot_child(child, snapshot) diff --git a/src/jarabe/desktop/transitionbox.py b/src/jarabe/desktop/transitionbox.py index bd3dc8d151..fa0419eb08 100644 --- a/src/jarabe/desktop/transitionbox.py +++ b/src/jarabe/desktop/transitionbox.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/jarabe/desktop/viewcontainer.py b/src/jarabe/desktop/viewcontainer.py index 3a2d2dd33a..f20cf5510c 100644 --- a/src/jarabe/desktop/viewcontainer.py +++ b/src/jarabe/desktop/viewcontainer.py @@ -16,16 +16,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from gi.repository import Gtk +from gi.repository import Gdk, Gtk +# We subclass Gtk.Widget and manage children manually using +# set_parent() / unparent() and a custom LayoutManager. -class ViewContainer(Gtk.Container): + +class ViewContainer(Gtk.Widget): __gtype_name__ = 'SugarViewContainer' def __init__(self, layout, owner_icon, activity_icon=None, **kwargs): - Gtk.Container.__init__(self, **kwargs) - self.set_has_window(False) - self.set_can_focus(True) + super().__init__(**kwargs) + self.set_focusable(True) self._activity_icon = None self._owner_icon = None @@ -36,22 +38,20 @@ def __init__(self, layout, owner_icon, activity_icon=None, **kwargs): if owner_icon: self._owner_icon = owner_icon - self.add(self._owner_icon) - self._owner_icon.show() + self._owner_icon.set_parent(self) if activity_icon: self._activity_icon = activity_icon - self.add(self._activity_icon) - self._activity_icon.show() + self._activity_icon.set_parent(self) - def do_add(self, child): + def add(self, child): + """Add a child widget to this container.""" if child != self._owner_icon and child != self._activity_icon: self._children.append(child) - if child.get_realized(): - child.set_parent_window(self.get_parent_window()) child.set_parent(self) - def do_remove(self, child): + def remove(self, child): + """Remove a child widget from this container.""" was_visible = child.get_visible() if child in self._children: self._children.remove(child) @@ -60,23 +60,60 @@ def do_remove(self, child): if was_visible and self.get_visible(): self.queue_resize() - def do_size_allocate(self, allocation): - self.set_allocation(allocation) + def do_measure(self, orientation, for_size): + min_size = 0 + nat_size = 0 + for child in self.get_children(): + child_min, child_nat, _, _ = child.measure(orientation, for_size) + min_size = max(min_size, child_min) + nat_size = max(nat_size, child_nat) + return (min_size, nat_size, -1, -1) + + def do_size_allocate(self, width, height, baseline): + if not self._layout: + return + + allocation = Gdk.Rectangle() + allocation.x = 0 + allocation.y = 0 + allocation.width = width + allocation.height = height + if self._owner_icon: self._layout.setup(allocation, self._owner_icon, self._activity_icon) self._layout.allocate_children(allocation, self._children) - def do_forall(self, include_internals, callback): - for child in self._children: - callback(child) + def do_snapshot(self, snapshot): + if not self._layout: + return + for child in self.get_children(): + self.snapshot_child(child, snapshot) + + def get_children(self): + all_children = list(self._children) if self._owner_icon: - callback(self._owner_icon) + all_children.append(self._owner_icon) if self._activity_icon: - callback(self._activity_icon) + all_children.append(self._activity_icon) + return all_children def set_layout(self, layout): + """Set the layout manager, removing all current children.""" for child in self.get_children(): self.remove(child) self._layout = layout + + def do_dispose(self): + while self._children: + child = self._children.pop() + child.unparent() + if self._owner_icon: + self._owner_icon.unparent() + self._owner_icon = None + if self._activity_icon: + self._activity_icon.unparent() + self._activity_icon = None + super().do_dispose() + diff --git a/src/jarabe/desktop/viewtoolbar.py b/src/jarabe/desktop/viewtoolbar.py index 4dacca7c94..0d1dab942f 100644 --- a/src/jarabe/desktop/viewtoolbar.py +++ b/src/jarabe/desktop/viewtoolbar.py @@ -30,13 +30,16 @@ from sugar4.graphics import iconentry from sugar4.graphics.radiotoolbutton import RadioToolButton + from jarabe.desktop import favoritesview from jarabe.model import desktop _AUTOSEARCH_TIMEOUT = 1000 +# Gtk.Box + regular buttons would require toolkit changes too. + -class ViewToolbar(Gtk.Toolbar): +class ViewToolbar(Gtk.Box): __gtype_name__ = 'SugarViewToolbar' __gsignals__ = { @@ -47,7 +50,8 @@ class ViewToolbar(Gtk.Toolbar): } def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self.add_css_class('sugar-toolbar') self._favorites_views_indicies = [] for i in range(desktop.get_number_of_views()): @@ -63,10 +67,6 @@ def __init__(self): self._add_separator() - tool_item = Gtk.ToolItem() - self.insert(tool_item, -1) - tool_item.show() - self.search_entry = iconentry.IconEntry() self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, 'entry-search') @@ -75,8 +75,9 @@ def __init__(self): self.search_entry.set_width_chars(25) self.search_entry.connect('activate', self._entry_activated_cb) self.search_entry.connect('changed', self._entry_changed_cb) - tool_item.add(self.search_entry) - self.search_entry.show() + + self.append(self.search_entry) + self.search_entry.set_visible(True) self._add_separator(expand=True) @@ -84,20 +85,23 @@ def __init__(self): self._favorites_buttons = [] for i in range(desktop.get_number_of_views()): self._add_favorites_button(i) - toolitem = Gtk.ToolItem() - toolitem.add(self._button_box) - self.insert(toolitem, -1) - self._button_box.show() - toolitem.show() self._list_button = RadioToolButton(icon_name='view-list') - self._list_button.props.group = self._favorites_buttons[0] + self._list_button.set_group(self._favorites_buttons[0]) self._list_button.props.tooltip = _('List view') self._list_button.props.accelerator = \ _('%d' % (len(self._favorites_views_indicies) + 1)) self._list_view_toggle_id = self._list_button.connect( 'toggled', self.__view_button_toggled_cb, self._list_view_index) - self.insert(self._list_button, -1) + self._button_box.append(self._list_button) + + if self._favorites_buttons: + self._favorites_buttons[0].set_active(True) + + toolitem = Gtk.Box() + toolitem.append(self._button_box) + toolitem.set_visible(True) + self.append(toolitem) self._add_separator() @@ -108,19 +112,19 @@ def _add_favorites_button(self, i): self.__view_button_toggled_cb, self._favorites_views_indicies[i]) if i > 0: - self._favorites_buttons[i].props.group = self._favorites_buttons[0] - self._button_box.add(self._favorites_buttons[i]) - self._favorites_buttons[i].show() + self._favorites_buttons[i].set_group(self._favorites_buttons[0]) + self._button_box.append(self._favorites_buttons[i]) + self._favorites_buttons[i].set_visible(True) def show_view_buttons(self): for i in range(desktop.get_number_of_views()): - self._favorites_buttons[i].show() - self._list_button.show() + self._favorites_buttons[i].set_visible(True) + self._list_button.set_visible(True) def hide_view_buttons(self): for i in range(desktop.get_number_of_views()): - self._favorites_buttons[i].hide() - self._list_button.hide() + self._favorites_buttons[i].set_visible(False) + self._list_button.set_visible(False) def clear_query(self): self.search_entry.props.text = '' @@ -130,15 +134,14 @@ def set_placeholder_text_for_view(self, view_name): self.search_entry.set_placeholder_text(text) def _add_separator(self, expand=False): - separator = Gtk.SeparatorToolItem() - separator.props.draw = False + separator = Gtk.Box() + separator.set_visible(True) if expand: - separator.set_expand(True) + separator.set_hexpand(True) else: separator.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) - self.insert(separator, -1) - separator.show() + self.append(separator) def _entry_activated_cb(self, entry): if self._autosearch_timer: @@ -153,7 +156,7 @@ def _entry_activated_cb(self, entry): def _entry_changed_cb(self, entry): if not entry.props.text: - entry.activate() + entry.emit('activate') return if self._autosearch_timer: @@ -164,7 +167,7 @@ def _entry_changed_cb(self, entry): def _autosearch_timer_cb(self): logging.debug('_autosearch_timer_cb') self._autosearch_timer = None - self.search_entry.activate() + self.search_entry.emit('activate') return False def __view_button_toggled_cb(self, button, view): @@ -180,7 +183,7 @@ def __desktop_view_icons_changed_cb(self, model): n = len(self._favorites_views_indicies) self._favorites_views_indicies.append(n) self._add_favorites_button(n) - self._favorites_buttons[n].show() + self._favorites_buttons[n].set_visible(True) elif number_of_views < len(self._favorites_views_indicies): for i in range(len(self._favorites_views_indicies) - number_of_views): @@ -188,10 +191,10 @@ def __desktop_view_icons_changed_cb(self, model): logging.debug('removing FavoritesButton %d' % (n)) button = self._favorites_buttons[n] self._favorites_buttons.remove(button) - button.destroy() + self._button_box.remove(button) self._favorites_views_indicies.remove( self._favorites_views_indicies[n]) - self._button_box.show() + self._button_box.set_visible(True) self._list_view_index = number_of_views self._list_button.props.accelerator = \ @@ -199,35 +202,42 @@ def __desktop_view_icons_changed_cb(self, model): self._list_button.disconnect(self._list_view_toggle_id) self._list_view_toggle_id = self._list_button.connect( 'toggled', self.__view_button_toggled_cb, self._list_view_index) - self._list_button.show() + self._list_button.set_visible(True) class FavoritesButton(RadioToolButton): __gtype_name__ = 'SugarFavoritesButton' def __init__(self, favorite_view): - RadioToolButton.__init__(self) + super().__init__() self.props.tooltip = desktop.get_view_labels()[favorite_view] self.props.accelerator = _('%d' % (favorite_view + 1)) - self.props.group = None + self.set_group(None) self.props.icon_name = desktop.get_view_icons()[favorite_view] favorites_settings = favoritesview.get_settings(favorite_view) self._layout = favorites_settings.layout - # someday, this will be a Gtk.Table() layouts_grid = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - layout_item = None + prev_layout_item = None for layoutid, layoutclass in sorted(favoritesview.LAYOUT_MAP.items()): layout_item = RadioToolButton(icon_name=layoutclass.icon_name, - group=layout_item, active=False) + active=False) + if prev_layout_item: + layout_item.set_group(prev_layout_item) + else: + layout_item.set_group(None) + prev_layout_item = layout_item if layoutid == self._layout: layout_item.set_active(True) - layouts_grid.pack_start(layout_item, True, False, 0) + layout_item.set_visible(True) + layout_item.set_hexpand(True) + layout_item.set_halign(Gtk.Align.CENTER) + layouts_grid.append(layout_item) layout_item.connect('toggled', self.__layout_activate_cb, layoutid, favorite_view) - layouts_grid.show_all() + layouts_grid.set_visible(True) self.props.palette.set_content(layouts_grid) def __layout_activate_cb(self, menu_item, layout, favorite_view): diff --git a/src/jarabe/frame/__init__.py b/src/jarabe/frame/__init__.py index a3076f5541..2b7ae1eabc 100644 --- a/src/jarabe/frame/__init__.py +++ b/src/jarabe/frame/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007, Red Hat, Inc. +# Copyright (C) 2006-2007, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,7 +15,6 @@ from jarabe.frame.frame import Frame - _view = None diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py index ddd6f00b02..a2030bed86 100644 --- a/src/jarabe/frame/activitiestray.py +++ b/src/jarabe/frame/activitiestray.py @@ -22,6 +22,7 @@ from gi.repository import GObject from gi.repository import Gio +from gi.repository import GLib from gi.repository import Gtk from gi.repository import Gdk @@ -52,7 +53,6 @@ from jarabe.frame.notification import NotificationPulsingIcon from jarabe.frame.clipboardmenu import ClipboardMenu from jarabe.frame.clipboardobject import ClipboardObject -from jarabe.frame.clipboardtray import ContextMap from jarabe.frame.clipboardobject import Format import jarabe.frame from jarabe.frame import clipboard @@ -61,7 +61,7 @@ class ActivityButton(RadioToolButton): def __init__(self, home_activity, group): - RadioToolButton.__init__(self, group=group) + super().__init__(group=group) self.set_palette_invoker(FrameWidgetInvoker(self)) self.palette_invoker.cache_palette = False @@ -69,7 +69,6 @@ def __init__(self, home_activity, group): self._home_activity = home_activity self._notify_launch_hid = None - self._context_map = ContextMap() self._cb_object = None self._icon = NotificationPulsingIcon() self._icon.props.base_color = home_activity.get_icon_color() @@ -77,28 +76,19 @@ def __init__(self, home_activity, group): XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TOOLBAR_GREY.get_svg())) if self._home_activity.is_journal(): - self._icon.drag_dest_set(Gtk.DestDefaults.ALL, - [], Gdk.DragAction.COPY) - self._icon.drag_dest_add_text_targets() - self._icon.drag_dest_add_image_targets() - self._icon.drag_dest_add_uri_targets() - self._icon.connect('drag_drop', self.__drag_drop_cb) - self._icon.connect('drag_data_received', - self.__journal_drag_data_received_cb) + from gi.repository import GdkPixbuf + self._drop_target = Gtk.DropTarget.new(type=GObject.TYPE_STRING, actions=Gdk.DragAction.COPY) + self._drop_target.set_gtypes([GObject.TYPE_STRING, Gdk.FileList.__gtype__, GdkPixbuf.Pixbuf.__gtype__]) + self._drop_target.connect('drop', self.__drag_drop_cb) + self._icon.add_controller(self._drop_target) if home_activity.get_icon_path(): self._icon.props.file = home_activity.get_icon_path() else: - window = home_activity.get_window() - - if not window.get_icon_is_fallback(): - pixbuf = window.get_icon() - self._icon.pixbuf = pixbuf - else: - self._icon.props.icon_name = 'image-missing' + self._icon.props.icon_name = 'image-missing' self.set_icon_widget(self._icon) - self._icon.show() + self._icon.set_visible(True) if home_activity.props.launch_status == shell.Activity.LAUNCHING: self._icon.props.pulsing = True @@ -107,42 +97,30 @@ def __init__(self, home_activity, group): elif home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: self._on_failed_launch() - def __drag_drop_cb(self, widget, context, x, y, time): - context_targets = context.list_targets() - if not self._context_map.has_context(context): - self._context_map.add_context(context, 0, len(context_targets)) - - for target in context_targets: - if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): - widget.drag_get_data(context, target, time) - - cb_menu = ClipboardMenu(self._cb_object) - cb_menu._copy_to_journal() - - logging.debug('Saved to Journal') - return True - - def __journal_drag_data_received_cb(self, widget, context, x, y, selection, - targetType, time): + def __drag_drop_cb(self, target, value, x, y): data = None - if selection.get_pixbuf() is not None: - data = selection.get_pixbuf() - if len(selection.get_uris()) != 0: - data = selection.get_uris()[0].encode() - if selection.get_text() is not None: - data = selection.get_text().encode() - if data is None: - data = selection.get_data() - - self._cb_object = ClipboardObject(0, "") - - mime_type = selection.get_data_type().name() - cb_format = Format(mime_type, data, on_disk=False) - self._cb_object.add_format(cb_format) - - logging.debug('ActivityTray: got data for target') - - Gtk.drag_finish(context, True, True, Gtk.get_current_event_time()) + mime_type = "text/plain" + if isinstance(value, str): + data = value.encode() + elif hasattr(value, 'get_files'): + files = value.get_files() + if files: + data = files[0].get_uri().encode() + mime_type = "text/uri-list" + elif hasattr(value, 'savev'): + data = value + mime_type = "image/png" + + if data: + self._cb_object = ClipboardObject(0, "") + cb_format = Format(mime_type, data, on_disk=False) + self._cb_object.add_format(cb_format) + + cb_menu = ClipboardMenu(self._cb_object) + cb_menu._copy_to_journal() + logging.debug('Saved to Journal') + return True + return False def create_palette(self): if self._home_activity.is_journal(): @@ -184,12 +162,11 @@ class InviteButton(ToolButton): } def __init__(self, invite): - ToolButton.__init__(self) + super().__init__() self._invite = invite self.connect('clicked', self.__clicked_cb) - self.connect('destroy', self.__destroy_cb) bundle_registry = bundleregistry.get_registry() bundle = bundle_registry.get_bundle(invite.get_bundle_id()) @@ -201,17 +178,19 @@ def __init__(self, invite): else: self._icon.props.icon_name = 'image-missing' self.set_icon_widget(self._icon) - self._icon.show() + self._icon.set_visible(True) + invoker = FrameWidgetInvoker(self) palette = InvitePalette(invite) - palette.props.invoker = FrameWidgetInvoker(self) + invoker.palette = palette palette.set_group_id('frame') palette.connect('remove-invite', self.__remove_invite_cb) - self.set_palette(palette) + self.set_palette_invoker(invoker) self._notif_icon = NotificationIcon() - self._notif_icon.connect('button-release-event', - self.__button_release_event_cb) + click = Gtk.GestureClick.new() + click.connect('released', self.__button_release_event_cb) + self._notif_icon.add_controller(click) self._notif_icon.props.xo_color = invite.get_color() if bundle is not None: @@ -222,7 +201,7 @@ def __init__(self, invite): frame = jarabe.frame.get_view() frame.add_notification(self._notif_icon, Gtk.CornerType.TOP_LEFT) - def __button_release_event_cb(self, icon, event): + def __button_release_event_cb(self, gesture, n_press, x, y): if self._notif_icon is not None: frame = jarabe.frame.get_view() frame.remove_notification(self._notif_icon) @@ -236,11 +215,12 @@ def __clicked_cb(self, button): def __remove_invite_cb(self, palette): self.emit('remove-invite') - def __destroy_cb(self, button): + def do_unroot(self): if self._notif_icon is not None: frame = jarabe.frame.get_view() frame.remove_notification(self._notif_icon) self._notif_icon = None + super().do_unroot() class InvitePalette(Palette): @@ -257,17 +237,17 @@ def __init__(self, invite): self.menu_box = PaletteMenuBox() self.set_content(self.menu_box) - self.menu_box.show() + self.menu_box.set_visible(True) menu_item = PaletteMenuItem(_('Join'), icon_name='dialog-ok') menu_item.connect('activate', self.__join_activate_cb) self.menu_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) menu_item = PaletteMenuItem(_('Decline'), icon_name='dialog-cancel') menu_item.connect('activate', self.__decline_activate_cb) self.menu_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) bundle_id = invite.get_bundle_id() @@ -337,7 +317,7 @@ def __notification_received_cb(self, **kwargs): button = NotificationButton(name) button.set_icon(icon) - button.show() + button.set_visible(True) self.add_item(button) self._buttons_by_name[name] = button @@ -361,17 +341,18 @@ def __buffer_cleared_cb(self, **kwargs): def __activity_added_cb(self, home_model, home_activity): logging.debug('__activity_added_cb: %r', home_activity) - if self.get_children(): - group = self.get_children()[0] - else: - group = None + group = None + for child in self.get_children(): + if isinstance(child, ActivityButton): + group = child + break button = ActivityButton(home_activity, group) self.add_item(button) self._buttons[home_activity] = button self._buttons_by_name[home_activity.get_activity_id()] = button button.connect('clicked', self.__activity_clicked_cb, home_activity) - button.show() + button.set_visible(True) def __activity_removed_cb(self, home_model, home_activity): logging.debug('__activity_removed_cb: %r', home_activity) @@ -381,16 +362,15 @@ def __activity_removed_cb(self, home_model, home_activity): del self._buttons_by_name[home_activity.get_activity_id()] def _activate_activity(self, home_activity): + if home_activity not in self._buttons: + return + button = self._buttons[home_activity] self._freeze_button_clicks = True - button.props.active = True + button.set_active(True) self._freeze_button_clicks = False - self.scroll_to_item(button) - # Redraw immediately. - # The widget may not be realized yet, and then there is no window. - while Gtk.events_pending(): - Gtk.main_iteration() + GLib.idle_add(self.scroll_to_item, button) def __activity_changed_cb(self, home_model, home_activity): logging.debug('__activity_changed_cb: %r', home_activity) @@ -413,13 +393,13 @@ def __tabbing_activity_changed_cb(self, home_model, home_activity): self._activate_activity(home_activity) def __activity_clicked_cb(self, button, home_activity): - if not self._freeze_button_clicks and button.props.active: + if not self._freeze_button_clicks and button.get_active(): logging.debug('ActivitiesTray.__activity_clicked_cb') window = home_activity.get_window() if window: - window.activate(Gtk.get_current_event_time()) frame = jarabe.frame.get_view() frame.hide() + window.present() def __remove_invite_cb(self, icon, invite): self._invites.remove_invite(invite) @@ -435,12 +415,12 @@ def _add_invite(self, invite): item = InviteButton(invite) item.connect('remove-invite', self.__remove_invite_cb, invite) self.add_item(item) - item.show() + item.set_visible(True) self._invite_to_item[invite] = item def _remove_invite(self, invite): self.remove_item(self._invite_to_item[invite]) - self._invite_to_item[invite].destroy() + self._invite_to_item[invite].unparent() del self._invite_to_item[invite] def __new_file_transfer_cb(self, **kwargs): @@ -453,7 +433,7 @@ def __new_file_transfer_cb(self, **kwargs): button = OutgoingTransferButton(file_transfer) self.add_item(button) - button.show() + button.set_visible(True) class BaseTransferButton(ToolButton): @@ -461,22 +441,23 @@ class BaseTransferButton(ToolButton): """ def __init__(self, file_transfer): - ToolButton.__init__(self) + super().__init__() self.file_transfer = file_transfer file_transfer.connect('notify::state', self.__notify_state_cb) icon = Icon() self.props.icon_widget = icon - icon.show() + icon.set_visible(True) self.notif_icon = NotificationIcon() - self.notif_icon.connect('button-release-event', - self.__button_release_event_cb) + click = Gtk.GestureClick.new() + click.connect('released', self.__button_release_event_cb) + self.notif_icon.add_controller(click) self.connect('clicked', self.__button_clicked_cb) - def __button_release_event_cb(self, icon, event): + def __button_release_event_cb(self, gesture, n_press, x, y): if self.notif_icon is not None: frame = jarabe.frame.get_view() frame.remove_notification(self.notif_icon) @@ -488,7 +469,7 @@ def __button_clicked_cb(self, button): def remove(self): frame = jarabe.frame.get_view() frame.remove_notification(self.notif_icon) - self.props.parent.remove(self) + self.unparent() def __notify_state_cb(self, file_transfer, pspec): logging.debug('_update state: %r %r', file_transfer.props.state, @@ -512,7 +493,8 @@ def __init__(self, file_transfer): file_transfer.connect('notify::transferred-bytes', self.__notify_transferred_bytes_cb) - icons = Gio.content_type_get_icon(file_transfer.mime_type).props.names + content_type = Gio.content_type_from_mime_type(file_transfer.mime_type) + icons = Gio.content_type_get_icon(content_type).get_names() icons.append('application-octet-stream') for icon_name in icons: icon_name = 'transfer-from-%s' % icon_name @@ -533,7 +515,9 @@ def __init__(self, file_transfer): def create_palette(self): palette = IncomingTransferPalette(self.file_transfer) palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) - palette.props.invoker = FrameWidgetInvoker(self) + invoker = FrameWidgetInvoker(self) + invoker.palette = palette + self.set_palette_invoker(invoker) palette.set_group_id('frame') return palette @@ -588,7 +572,8 @@ class OutgoingTransferButton(BaseTransferButton): def __init__(self, file_transfer): BaseTransferButton.__init__(self, file_transfer) - icons = Gio.content_type_get_icon(file_transfer.mime_type).props.names + content_type = Gio.content_type_from_mime_type(file_transfer.mime_type) + icons = Gio.content_type_get_icon(content_type).get_names() icons.append('application-octet-stream') for icon_name in icons: icon_name = 'transfer-to-%s' % icon_name @@ -609,7 +594,9 @@ def __init__(self, file_transfer): def create_palette(self): palette = OutgoingTransferPalette(self.file_transfer) palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) - palette.props.invoker = FrameWidgetInvoker(self) + invoker = FrameWidgetInvoker(self) + invoker.palette = palette + self.set_palette_invoker(invoker) palette.set_group_id('frame') return palette @@ -702,80 +689,80 @@ def __notify_state_cb(self, file_transfer, pspec): def _update(self): box = PaletteMenuBox() self.set_content(box) - box.show() + box.set_visible(True) logging.debug('_update state: %r', self.file_transfer.props.state) if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING: menu_item = PaletteMenuItem(_('Accept')) icon = Icon(icon_name='dialog-ok', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__accept_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) menu_item = PaletteMenuItem(_('Decline')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__decline_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) separator = PaletteMenuItemSeparator() box.append_item(separator) - separator.show() + separator.set_visible(True) inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) box.append_item(inner_box, vertical_padding=0) - inner_box.show() + inner_box.set_visible(True) if self.file_transfer.description: text = self.file_transfer.description.replace('\n', ' ') label = Gtk.Label(label=text) label.set_max_width_chars(style.MENU_WIDTH_CHARS) label.set_ellipsize(style.ELLIPSIZE_MODE_DEFAULT) - inner_box.add(label) - label.show() + inner_box.append(label) + label.set_visible(True) mime_type = self.file_transfer.mime_type type_description = mime.get_mime_description(mime_type) size = self._format_size(self.file_transfer.file_size) label = Gtk.Label(label='%s (%s)' % (size, type_description)) - inner_box.add(label) - label.show() + inner_box.append(label) + label.set_visible(True) elif self.file_transfer.props.state in \ [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: menu_item = PaletteMenuItem(_('Cancel')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__cancel_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) separator = PaletteMenuItemSeparator() box.append_item(separator) - separator.show() + separator.set_visible(True) inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) box.append_item(inner_box, vertical_padding=0) - inner_box.show() + inner_box.set_visible(True) self.progress_bar = Gtk.ProgressBar() - inner_box.add(self.progress_bar) - self.progress_bar.show() + inner_box.append(self.progress_bar) + self.progress_bar.set_visible(True) self.progress_label = Gtk.Label(label='') - inner_box.add(self.progress_label) - self.progress_label.show() + inner_box.append(self.progress_label) + self.progress_label.set_visible(True) self.update_progress() @@ -783,11 +770,11 @@ def _update(self): menu_item = PaletteMenuItem(_('Dismiss')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__dismiss_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) self.update_progress() @@ -797,21 +784,21 @@ def _update(self): menu_item = PaletteMenuItem(_('Dismiss')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__dismiss_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) box.append_item(inner_box, vertical_padding=0) - inner_box.show() + inner_box.set_visible(True) text = _('The other participant canceled the file transfer') label = Gtk.Label(label=text) - inner_box.add(label) - label.show() + inner_box.append(label) + label.set_visible(True) def __accept_activate_cb(self, menu_item): # TODO: figure out the best place to get rid of that temp file @@ -871,66 +858,66 @@ def _update(self): box = PaletteMenuBox() self.set_content(box) - box.show() + box.set_visible(True) if new_state == filetransfer.FT_STATE_PENDING: menu_item = PaletteMenuItem(_('Cancel')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__cancel_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) separator = PaletteMenuItemSeparator() box.append_item(separator) - separator.show() + separator.set_visible(True) inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) box.append_item(inner_box, vertical_padding=0) - inner_box.show() + inner_box.set_visible(True) if self.file_transfer.description: label = Gtk.Label(label=self.file_transfer.description) - inner_box.add(label) - label.show() + inner_box.append(label) + label.set_visible(True) mime_type = self.file_transfer.mime_type type_description = mime.get_mime_description(mime_type) size = self._format_size(self.file_transfer.file_size) label = Gtk.Label(label='%s (%s)' % (size, type_description)) - inner_box.add(label) - label.show() + inner_box.append(label) + label.set_visible(True) elif new_state in [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: menu_item = PaletteMenuItem(_('Cancel')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__cancel_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) separator = PaletteMenuItemSeparator() box.append_item(separator) - separator.show() + separator.set_visible(True) inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) box.append_item(inner_box, vertical_padding=0) - inner_box.show() + inner_box.set_visible(True) self.progress_bar = Gtk.ProgressBar() - inner_box.add(self.progress_bar) - self.progress_bar.show() + inner_box.append(self.progress_bar) + self.progress_bar.set_visible(True) self.progress_label = Gtk.Label(label='') - inner_box.add(self.progress_label) - self.progress_label.show() + inner_box.append(self.progress_label) + self.progress_label.set_visible(True) self.update_progress() @@ -939,11 +926,11 @@ def _update(self): menu_item = PaletteMenuItem(_('Dismiss')) icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) menu_item.connect('activate', self.__dismiss_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) self.update_progress() diff --git a/src/jarabe/frame/clipboard.py b/src/jarabe/frame/clipboard.py index 315aece02f..922c47e4e2 100644 --- a/src/jarabe/frame/clipboard.py +++ b/src/jarabe/frame/clipboard.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006, Red Hat, Inc. +# Copyright (C) 2006, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,14 +20,12 @@ import tempfile from gi.repository import GObject -from gi.repository import Gtk from gi.repository import Gdk from sugar4 import mime from jarabe.frame.clipboardobject import ClipboardObject, Format - _instance = None @@ -35,17 +33,17 @@ class Clipboard(GObject.GObject): __gsignals__ = { 'object-added': (GObject.SignalFlags.RUN_FIRST, None, - ([object])), + (object,)), 'object-deleted': (GObject.SignalFlags.RUN_FIRST, None, - ([GObject.TYPE_INT64])), + (GObject.TYPE_INT64,)), 'object-selected': (GObject.SignalFlags.RUN_FIRST, None, - ([int])), + (GObject.TYPE_INT64,)), 'object-state-changed': (GObject.SignalFlags.RUN_FIRST, None, - ([object])), + (object,)), } def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._objects = {} self._next_id = 0 @@ -99,7 +97,7 @@ def delete_object(self, object_id): cb_object = self._objects.pop(object_id) cb_object.destroy() if not self._objects: - gtk_clipboard = Gtk.Display.get_default().get_clipboard() + gtk_clipboard = Gdk.Display.get_default().get_clipboard() gtk_clipboard.set_content(None) self.emit('object-deleted', object_id) logging.debug('Deleted object with object_id %r', object_id) @@ -137,7 +135,7 @@ def _process_object(self, cb_object): elif 'text/unicode' in list(formats.keys()): self.add_object_format( cb_object.get_id(), 'text/plain', - data=formats['UTF8_STRING'].get_data(), on_disk=False) + data=formats['text/unicode'].get_data(), on_disk=False) def get_object(self, object_id): logging.debug('Clipboard.get_object') diff --git a/src/jarabe/frame/clipboardicon.py b/src/jarabe/frame/clipboardicon.py index 2be3218cc2..b9bcaf3713 100644 --- a/src/jarabe/frame/clipboardicon.py +++ b/src/jarabe/frame/clipboardicon.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, One Laptop Per Child # # This program is free software: you can redistribute it and/or modify @@ -18,8 +18,8 @@ from gi.repository import Gtk from gi.repository import Gdk -from gi.repository import SugarExt from gi.repository import GLib +from gi.repository import GObject from sugar4.graphics.radiotoolbutton import RadioToolButton from sugar4.graphics.icon import Icon @@ -40,8 +40,10 @@ class ClipboardIcon(RadioToolButton): def __init__(self, cb_object, group): RadioToolButton.__init__(self, group=group) - self.props.palette_invoker = FrameWidgetInvoker(self) - self.palette_invoker.props.toggle_palette = True + invoker = FrameWidgetInvoker(self) + self.set_palette_invoker(invoker) + if hasattr(invoker, '_toggle_palette'): + invoker._toggle_palette = True self._cb_object = cb_object self.owns_clipboard = False @@ -54,17 +56,22 @@ def __init__(self, cb_object, group): color = profile.get_color() self._icon.props.xo_color = color self.set_icon_widget(self._icon) - self._icon.show() + self._icon.set_visible(True) cb_service = clipboard.get_instance() cb_service.connect('object-state-changed', self._object_state_changed_cb) cb_service.connect('object-selected', self._object_selected_cb) - child = self.get_child() - child.connect('drag-data-get', self._drag_data_get_cb) self.connect('notify::active', self._notify_active_cb) + self._clipboard = Gdk.Display.get_default().get_clipboard() + self._clipboard.connect('notify::content', self._on_clipboard_content_changed) + + def _on_clipboard_content_changed(self, clipboard, pspec): + if not getattr(self, '_setting_clipboard', False): + self.owns_clipboard = False + def create_palette(self): palette = ClipboardMenu(self._cb_object) palette.set_group_id('frame') @@ -73,15 +80,6 @@ def create_palette(self): def get_object_id(self): return self._cb_object.get_id() - def _drag_data_get_cb(self, widget, context, selection, target_type, - event_time): - target_atom = selection.get_target() - target_name = target_atom.name() - logging.debug('_drag_data_get_cb: requested target %s', target_name) - data = self._cb_object.get_formats()[target_name].get_data() - selection.set(target_atom, 8, data) - GLib.source_remove(self._timeout_id) - def _put_in_clipboard(self): logging.debug('ClipboardIcon._put_in_clipboard') @@ -89,22 +87,32 @@ def _put_in_clipboard(self): raise ValueError('Object is not complete, cannot be put into the' ' clipboard.') - clipboard = Gdk.Display.get_default().get_clipboard() + gdk_clipboard = Gdk.Display.get_default().get_clipboard() try: - content_provider = Gdk.ContentProvider.new_for_bytes( - self._cb_object.get_mime_type(), - self._cb_object) - stored = clipboard.set_content(content_provider) - # this is empty as I don't yet know what exceptions can be raised here. - except: - stored = SugarExt.clipboard_set_with_data( - self._cb_object.get_mime_type(), - self._cb_object) - - if not stored: - logging.error('GtkClipboard.set_with_data failed!') - else: - self.owns_clipboard = True + formats = self._cb_object.get_formats() + providers = [] + + for fmt_name, format_ in formats.items(): + data = format_.get_data() + if isinstance(data, str): + if fmt_name == 'text/plain': + v = GObject.Value(GObject.TYPE_STRING, data) + providers.append(Gdk.ContentProvider.new_for_value(v)) + else: + gbytes = GLib.Bytes.new(data.encode('utf-8')) + providers.append(Gdk.ContentProvider.new_for_bytes(fmt_name, gbytes)) + elif isinstance(data, bytes): + gbytes = GLib.Bytes.new(data) + providers.append(Gdk.ContentProvider.new_for_bytes(fmt_name, gbytes)) + + if providers: + provider = Gdk.ContentProvider.new_union(providers) + self._setting_clipboard = True + gdk_clipboard.set_content(provider) + self._setting_clipboard = False + self.owns_clipboard = True + except Exception as e: + logging.error('Failed to put in clipboard: %s', e) def _object_state_changed_cb(self, cb_service, cb_object): if cb_object != self._cb_object: @@ -117,11 +125,11 @@ def _object_state_changed_cb(self, cb_service, cb_object): else: self._icon.props.icon_name = 'application-octet-stream' - child = self.get_child() - child.connect('drag-begin', self._drag_begin_cb) - child.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, - self._get_targets(), - Gdk.DragAction.COPY) + if not hasattr(self, '_drag_source'): + self._drag_source = Gtk.DragSource.new() + self._drag_source.connect('prepare', self._on_drag_prepare) + self._drag_source.connect('drag-begin', self._drag_begin_cb) + self.add_controller(self._drag_source) if cb_object.get_percent() == 100: self.props.sensitive = True @@ -150,23 +158,41 @@ def show_notification(self): frame = jarabe.frame.get_view() self._timeout_id = frame.add_notification( self._notif_icon, Gtk.CornerType.BOTTOM_LEFT) - self._notif_icon.connect('drag-data-get', self._drag_data_get_cb) - self._notif_icon.connect('drag-begin', self._drag_begin_cb) - self._notif_icon.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, - self._get_targets(), - Gdk.DragAction.COPY) - - def _drag_begin_cb(self, widget, context): - # TODO: We should get the pixbuf from the icon, with colors, etc. - frame = jarabe.frame.get_view() - self._timeout_id = GLib.timeout_add( - jarabe.frame.frame.NOTIFICATION_DURATION, - lambda: frame.remove_notification(self._notif_icon)) - icon_theme = Gtk.IconTheme.get_default() - pixbuf = icon_theme.load_icon(self._icon.props.icon_name, - style.STANDARD_ICON_SIZE, 0) - Gtk.drag_set_icon_pixbuf(context, pixbuf, hot_x=pixbuf.props.width / 2, - hot_y=pixbuf.props.height / 2) + + notif_drag_source = Gtk.DragSource.new() + notif_drag_source.connect('prepare', self._on_drag_prepare) + notif_drag_source.connect('drag-begin', self._drag_begin_cb) + self._notif_icon.add_controller(notif_drag_source) + + def _on_drag_prepare(self, source, x, y): + formats = self._cb_object.get_formats() + providers = [] + for fmt_name, format_ in formats.items(): + data = format_.get_data() + if isinstance(data, str): + if fmt_name == 'text/plain': + v = GObject.Value(GObject.TYPE_STRING, data) + providers.append(Gdk.ContentProvider.new_for_value(v)) + else: + gbytes = GLib.Bytes.new(data.encode('utf-8')) + providers.append(Gdk.ContentProvider.new_for_bytes(fmt_name, gbytes)) + elif isinstance(data, bytes): + gbytes = GLib.Bytes.new(data) + providers.append(Gdk.ContentProvider.new_for_bytes(fmt_name, gbytes)) + + if providers: + return Gdk.ContentProvider.new_union(providers) + + return None + + def _drag_begin_cb(self, source, drag): + if hasattr(self, '_timeout_id') and self._timeout_id > 0: + GLib.source_remove(self._timeout_id) + self._timeout_id = 0 + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + paintable = icon_theme.lookup_icon(self._icon.props.icon_name, None, style.STANDARD_ICON_SIZE, 1, 0, 0) + if paintable: + source.set_icon(paintable, 0, 0) def _notify_active_cb(self, widget, pspec): if self.props.active: diff --git a/src/jarabe/frame/clipboardmenu.py b/src/jarabe/frame/clipboardmenu.py index 040c2266f3..5046372b65 100644 --- a/src/jarabe/frame/clipboardmenu.py +++ b/src/jarabe/frame/clipboardmenu.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2007, One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ import os import logging from gi.repository import Gio +from gi.repository import GLib from gi.repository import Gtk @@ -30,7 +31,7 @@ from sugar4 import mime from sugar4 import env from sugar4 import profile -from sugar4.activity.i18n import pgettext +from gettext import pgettext from jarabe.frame import clipboard from jarabe.journal import misc @@ -45,6 +46,13 @@ def __init__(self, cb_object): self._cb_object = cb_object self.set_group_id('frame') + + self.action_group = Gio.SimpleActionGroup.new() + self.menu.insert_action_group('clip', self.action_group) + + open_action = Gio.SimpleAction.new('open-activity', GLib.VariantType('s')) + open_action.connect('activate', self._on_open_activity_activated) + self.action_group.add_action(open_action) cb_service = clipboard.get_instance() cb_service.connect('object-state-changed', @@ -52,14 +60,14 @@ def __init__(self, cb_object): self._remove_item = MenuItem(pgettext('Clipboard', 'Remove'), 'list-remove') - self._remove_item.connect('activate', self._remove_item_activate_cb) + self._remove_item.connect('clicked', self._remove_item_activate_cb) self.menu.append(self._remove_item) - self._remove_item.show() + self._remove_item.set_visible(True) self._open_item = MenuItem(_('Open'), 'zoom-activity') - self._open_item.connect('activate', self._open_item_activate_cb) + self._open_item.connect('clicked', self._open_item_activate_cb) self.menu.append(self._open_item) - self._open_item.show() + self._open_item.set_visible(True) self._journal_item = MenuItem(_('Keep')) color = profile.get_color() @@ -68,26 +76,25 @@ def __init__(self, cb_object): xo_color=color) self._journal_item.set_image(icon) - self._journal_item.connect('activate', self._journal_item_activate_cb) + self._journal_item.connect('clicked', self._journal_item_activate_cb) self.menu.append(self._journal_item) - self._journal_item.show() + self._journal_item.set_visible(True) self._update() def _update_open_submenu(self): activities = self._get_activities() logging.debug('_update_open_submenu: %r', activities) - child = self._open_item.get_child() if activities is None or len(activities) <= 1: - child.set_text(_('Open')) + self._open_item.set_text(_('Open')) if self._open_item.get_submenu() is not None: self._open_item.set_submenu(None) return - child.set_text(_('Open with')) + self._open_item.set_text(_('Open with')) + submenu = Gio.Menu.new() self._open_item.set_submenu(submenu) - submenu.show() for service_name in activities: registry = bundleregistry.get_registry() @@ -96,11 +103,9 @@ def _update_open_submenu(self): if not activity_info: logging.warning('Activity %s is unknown.', service_name) - item = Gio.MenuItem(activity_info.get_name()) - item.connect('activate', self._open_submenu_item_activate_cb, - service_name) + item = Gio.MenuItem.new(activity_info.get_name(), 'clip.open-activity') + item.set_action_and_target_value('clip.open-activity', GLib.Variant('s', service_name)) submenu.append_item(item) - item.show() def _update_items_visibility(self): activities = self._get_activities() @@ -153,6 +158,10 @@ def _open_item_activate_cb(self, menu_item): misc.resume(jobject.metadata, self._get_activities()[0]) jobject.destroy() + def _on_open_activity_activated(self, action, parameter): + service_name = parameter.get_string() + self._open_submenu_item_activate_cb(None, service_name) + def _open_submenu_item_activate_cb(self, menu_item, service_name): logging.debug('_open_submenu_item_activate_cb') percent = self._cb_object.get_percent() @@ -176,11 +185,9 @@ def _write_to_temp_file(self, data, is_pixbuf): f, file_path = tempfile.mkstemp(dir=tmp_dir) try: - if not is_pixbuf: - os.write(f, data) - else: - _type = self._cb_object.get_mime_type().split('/').pop() - data.savev(file_path, _type, [], []) + if hasattr(data, 'get_data'): + data = data.get_data() + os.write(f, data) finally: os.close(f) return file_path diff --git a/src/jarabe/frame/clipboardobject.py b/src/jarabe/frame/clipboardobject.py index b58dea5f71..73501a9458 100644 --- a/src/jarabe/frame/clipboardobject.py +++ b/src/jarabe/frame/clipboardobject.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2007, One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ import urllib.parse from gi.repository import Gio from gi.repository import Gtk +from gi.repository import Gdk from gettext import gettext as _ from sugar4 import mime @@ -59,21 +60,16 @@ def get_icon(self): return generic_type.icon icons = Gio.content_type_get_icon(mime_type) - icon_name = None if icons is not None: - icon_theme = Gtk.IconTheme.get_default() - for icon_name in icons.props.names: - icon_info = ( - icon_theme.lookup_icon(icon_name, - Gtk.IconSize.LARGE_TOOLBAR, 0)) - if icon_info is not None: - del icon_info + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + for icon_name in icons.get_names(): + if icon_theme.has_icon(icon_name): return icon_name return 'application-octet-stream' def get_preview(self): - for mime_type in ['UTF8_STRING']: + for mime_type in ['UTF8_STRING', 'text/plain']: if mime_type in self._formats: return self._formats[mime_type].get_data() return '' diff --git a/src/jarabe/frame/clipboardpanelwindow.py b/src/jarabe/frame/clipboardpanelwindow.py index 671f27981a..1c32173826 100644 --- a/src/jarabe/frame/clipboardpanelwindow.py +++ b/src/jarabe/frame/clipboardpanelwindow.py @@ -17,13 +17,19 @@ from urllib.parse import urlparse import hashlib -from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GLib from jarabe.frame.framewindow import FrameWindow from jarabe.frame.clipboardtray import ClipboardTray -from jarabe.frame import clipboard +import jarabe.frame.clipboard as clipboard + +# Clipboard target names that carry no data content. +_SKIP_TARGETS = frozenset([ + 'TIMESTAMP', 'TARGETS', 'MULTIPLE', 'SAVE_TARGETS', + 'DELETE', 'INSERT_SELECTION', 'INSERT_PROPERTY', +]) class ClipboardPanelWindow(FrameWindow): @@ -33,63 +39,118 @@ def __init__(self, frame, orientation): self._frame = frame - # Listening for new clipboard objects - # NOTE: we need to keep a reference to Gdk.Clipboard in order to keep - # listening to it. - self._clipboard = Gtk.Display.get_default().get_clipboard() + # Listening for new clipboard objects. + self._clipboard = Gdk.Display.get_default().get_clipboard() self._clipboard.connect('changed', self._owner_change_cb) self._clipboard_tray = ClipboardTray() - self._clipboard_tray.show() + self._clipboard_tray.set_visible(True) self.append(self._clipboard_tray) - # Receiving dnd drops - self.drag_dest_set(0, [], 0) - self.connect('drag-motion', self._clipboard_tray.drag_motion_cb) - self.connect('drag-leave', self._clipboard_tray.drag_leave_cb) - self.connect('drag-drop', self._clipboard_tray.drag_drop_cb) - self.connect('drag-data-received', - self._clipboard_tray.drag_data_received_cb) - - def _owner_change_cb(self, clipboard): + def _owner_change_cb(self, cb): logging.debug('owner_change_cb') if self._clipboard_tray.owns_clipboard(): return - cb_service = clipboard.get_instance() - - content = clipboard.get_content() - - if not content: + formats = cb.get_formats() + if formats is None: return - formats = content.ref_formats() - - if formats.is_empty(): + mime_types = formats.get_mime_types() + if not mime_types: return - mime_types = formats.get_mime_types() - content_is_uri = False - for mime_type in mime_types: - if mime_type == 'text/uri-list': - content_is_uri = True - mt = mime_type - - if content_is_uri: - uri = content.get_value() - filename = uri[len('file://'):].strip() - md5 = self._md5_for_file(filename) - data_hash = hash(md5) - else: - data_hash = hash(content.get_value()) - - key = cb_service.add_object(name="", data_hash=data_hash) - if key is None: + # Filter out internal/control targets. + targets = [t for t in mime_types if t not in _SKIP_TARGETS] + if not targets: return - cb_service.set_object_percent(key, percent=0) - self._add_content(key, content, mt) - cb_service.set_object_percent(key, percent=100) + + cb_service = clipboard.get_instance() + + # Read all targets asynchronously; track how many are still pending. + state = {'pending': len(targets), 'key': None, 'data_hash': None, 'cancelled': False} + for mime_type in targets: + cb.read_async( + [mime_type], + GLib.PRIORITY_DEFAULT, + None, + self._read_mime_cb, + (cb_service, mime_type, state)) + + # ------------------------------------------------------------------ + # Async reading chain + # ------------------------------------------------------------------ + + def _read_mime_cb(self, cb, result, user_data): + cb_service, mime_type, state = user_data + try: + stream, _ = cb.read_finish(result) + if stream: + stream.read_bytes_async( + 65536, + GLib.PRIORITY_DEFAULT, + None, + self._read_bytes_cb, + (cb_service, mime_type, state, stream)) + return + except Exception as e: + logging.debug('Could not read clipboard mime %s: %s', mime_type, e) + + # Count this target as done even on failure. + self._check_complete(cb_service, state) + + def _read_bytes_cb(self, stream, result, user_data): + cb_service, mime_type, state, _ = user_data + try: + raw = stream.read_bytes_finish(result) + data = raw.get_data() if raw else None + if data and not state['cancelled']: + on_disk = False + if mime_type == 'text/uri-list': + # Extract first URI and compute data hash + text = data.decode('utf-8', errors='replace') + uris = [u.strip() for u in text.splitlines() + if u.strip() and not u.strip().startswith('#')] + if uris: + uri = uris[0] + scheme = urlparse(uri).scheme + on_disk = (scheme == 'file') + if on_disk and state['data_hash'] is None: + filename = urlparse(uri).path + state['data_hash'] = hash( + self._md5_for_file(filename)) + data = uri + else: + if state['data_hash'] is None: + state['data_hash'] = hash(data) + # Decode text/* to str; keep binary as bytes. + if mime_type.startswith('text/'): + try: + data = data.decode('utf-8', errors='replace') + except Exception: + pass + + if state['key'] is None: + # Create the clipboard object now that we have a hash + state['key'] = cb_service.add_object(name='', data_hash=state['data_hash']) + if state['key'] is None: + state['cancelled'] = True + else: + cb_service.set_object_percent(state['key'], percent=0) + + if not state['cancelled']: + cb_service.add_object_format( + state['key'], mime_type, data, on_disk=on_disk) + except Exception as e: + logging.debug('Error reading bytes for %s: %s', mime_type, e) + + self._check_complete(cb_service, state) + + def _check_complete(self, cb_service, state): + state['pending'] -= 1 + if state['pending'] <= 0 and state['key'] is not None and not state['cancelled']: + cb_service.set_object_percent(state['key'], percent=100) def _md5_for_file(self, file_name): '''Calculate md5 for file data @@ -98,37 +159,13 @@ def _md5_for_file(self, file_name): ''' block_size = 8192 md5 = hashlib.md5() - f = open(file_name, 'r') - while True: - data = f.read(block_size) - if not data: - break - md5.update(data) - f.close() + try: + with open(file_name, 'rb') as f: + while True: + data = f.read(block_size) + if not data: + break + md5.update(data) + except OSError as e: + logging.warning('md5_for_file failed for %s: %s', file_name, e) return md5.digest() - - def _add_content(self, key, content, mime_type): - result, value = content.get_value() - if not result: - logging.warning('no data for content %s.', - mime_type) - return - - logging.debug('adding type ' + mime_type + '.') - cb_service = clipboard.get_instance() - if mime_type == 'text/uri-list': - uri = content.get_value() - - scheme, netloc_, path_, parameters_, query_, fragment_ = \ - urlparse(uri) - on_disk = (scheme == 'file') - - cb_service.add_object_format(key, - mime_type, - uri, - on_disk) - else: - cb_service.add_object_format(key, - mime_type, - content.get_value(), - on_disk=False) diff --git a/src/jarabe/frame/clipboardtray.py b/src/jarabe/frame/clipboardtray.py index fe39ab2ae6..ccf7676604 100644 --- a/src/jarabe/frame/clipboardtray.py +++ b/src/jarabe/frame/clipboardtray.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2007, One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,6 +17,9 @@ from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GLib +from gi.repository import GObject +from gi.repository import Gio from sugar4.graphics import tray from sugar4.graphics import style @@ -25,49 +28,55 @@ from jarabe.frame.clipboardicon import ClipboardIcon -class ContextMap(object): - """Maps a drag context to the clipboard object involved in the dragging.""" - - def __init__(self): - self._context_map = {} - - def add_context(self, context, object_id, data_types): - """Establishes the mapping. data_types will serve us for reference- - counting this mapping. - """ - self._context_map[context] = [object_id, data_types] - - def get_object_id(self, context): - """Retrieves the object_id associated with context. - Will release the association when this function was called as many - times as the number of data_types that this clipboard object contains. - """ - [object_id, data_types_left] = self._context_map[context] - - data_types_left = data_types_left - 1 - if data_types_left == 0: - del self._context_map[context] - else: - self._context_map[context] = [object_id, data_types_left] - - return object_id - - def has_context(self, context): - return context in self._context_map +def _get_screen_height(): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + return monitors.get_item(0).get_geometry().height + return 768 class ClipboardTray(tray.VTray): - MAX_ITEMS = Gdk.Screen.height() // style.GRID_CELL_SIZE - 2 + MAX_ITEMS = _get_screen_height() // style.GRID_CELL_SIZE - 2 def __init__(self): tray.VTray.__init__(self, align=tray.ALIGN_TO_END) self._icons = {} - self._context_map = ContextMap() cb_service = clipboard.get_instance() - cb_service.connect('object-added', self._object_added_cb) - cb_service.connect('object-deleted', self._object_deleted_cb) + self._object_added_hid = cb_service.connect('object-added', self._object_added_cb) + self._object_deleted_hid = cb_service.connect('object-deleted', self._object_deleted_cb) + + builder = Gdk.ContentFormatsBuilder.new() + builder.add_mime_type('journal-object-id') + builder.add_mime_type('text/uri-list') + builder.add_mime_type('text/plain') + builder.add_mime_type('text/html') + builder.add_mime_type('image/png') + builder.add_mime_type('image/jpeg') + builder.add_mime_type('image/gif') + builder.add_mime_type('image/svg+xml') + builder.add_gtype(GObject.TYPE_STRING) + formats = builder.to_formats() + + self._drop_target = Gtk.DropTargetAsync.new(formats, actions=Gdk.DragAction.COPY | Gdk.DragAction.MOVE) + self._drop_target.connect('drop', self._on_drop_async_cb) + self.add_controller(self._drop_target) + + def do_dispose(self): + cb_service = clipboard.get_instance() + if hasattr(self, '_object_added_hid'): + cb_service.disconnect(self._object_added_hid) + del self._object_added_hid + if hasattr(self, '_object_deleted_hid'): + cb_service.disconnect(self._object_deleted_hid) + del self._object_deleted_hid + if getattr(self, '_drop_target', None) is not None: + self.remove_controller(self._drop_target) + self._drop_target = None + super().do_dispose() def owns_clipboard(self): for icon in list(self._icons.values()): @@ -75,125 +84,119 @@ def owns_clipboard(self): return True return False - def _add_selection(self, object_id, selection): - if not selection.get_data(): - return - - selection_data = selection.get_data() - - selection_type_atom = selection.get_data_type() - selection_type = selection_type_atom.name() - - logging.debug('ClipboardTray: adding type %r', selection_type) - - cb_service = clipboard.get_instance() - if selection_type == 'text/uri-list': - uris = selection.get_uris() - if len(uris) > 1: - raise NotImplementedError('Multiple uris in text/uri-list' - ' still not supported.') - - cb_service.add_object_format(object_id, - selection_type, - uris[0], - on_disk=True) - else: - cb_service.add_object_format(object_id, - selection_type, - selection_data, - on_disk=False) - def _object_added_cb(self, cb_service, cb_object): + group = None if self._icons: group = list(self._icons.values())[0] - else: - group = None icon = ClipboardIcon(cb_object, group) self.add_item(icon) - icon.show() + icon.set_visible(True) self._icons[cb_object.get_id()] = icon - objects_to_delete = self.get_children()[:-self.MAX_ITEMS] - for icon in objects_to_delete: - logging.debug('ClipboardTray: deleting surplus object') - cb_service = clipboard.get_instance() - cb_service.delete_object(icon.get_object_id()) + # Enforce MAX_ITEMS + children = [] + child = self.get_first_child() + while child: + children.append(child) + child = child.get_next_sibling() + + if len(children) > self.MAX_ITEMS: + objects_to_delete = children[:-self.MAX_ITEMS] + for icon_to_delete in objects_to_delete: + logging.debug('ClipboardTray: deleting surplus object') + cb_service = clipboard.get_instance() + cb_service.delete_object(icon_to_delete.get_object_id()) logging.debug('ClipboardTray: %r was added', cb_object.get_id()) def _object_deleted_cb(self, cb_service, object_id): - icon = self._icons[object_id] - self.remove_item(icon) - del self._icons[object_id] - # select the last available icon - if self._icons: - last_icon = self.get_children()[-1] - last_icon.props.active = True + icon = self._icons.get(object_id) + if icon: + self.remove_item(icon) + del self._icons[object_id] + + # select the last available icon if the deleted one was active + if icon.props.active and self._icons: + children = [] + child = self.get_first_child() + while child: + children.append(child) + child = child.get_next_sibling() + if children: + last_icon = children[-1] + if hasattr(last_icon, 'set_active'): + last_icon.set_active(True) logging.debug('ClipboardTray: %r was deleted', object_id) - def drag_motion_cb(self, widget, context, x, y, time): - logging.debug('ClipboardTray._drag_motion_cb') - - if self._internal_drag(context): - Gdk.drag_status(context, Gdk.DragAction.MOVE, time) - else: - Gdk.drag_status(context, Gdk.DragAction.COPY, time) - self.props.drag_active = True - - return True - - def drag_leave_cb(self, widget, context, time): - self.props.drag_active = False - - def drag_drop_cb(self, widget, context, x, y, time): - logging.debug('ClipboardTray._drag_drop_cb') - - if self._internal_drag(context): - # TODO: We should move the object within the clipboard here - if not self._context_map.has_context(context): - Gdk.drop_finish(context, False, Gtk.get_current_event_time()) + def _on_drop_async_cb(self, target, drop, x, y): + logging.debug('ClipboardTray._on_drop_async_cb') + formats = drop.get_formats() + mime_types = formats.get_mime_types() + if not mime_types: return False - + + skip_targets = frozenset(['TIMESTAMP', 'TARGETS', 'MULTIPLE', 'SAVE_TARGETS', 'DELETE', 'INSERT_SELECTION', 'INSERT_PROPERTY']) + valid_targets = [t for t in mime_types if t not in skip_targets] + if not valid_targets: + return False + cb_service = clipboard.get_instance() object_id = cb_service.add_object(name="") - - context_targets = context.list_targets() - self._context_map.add_context(context, object_id, len(context_targets)) - - for target in context_targets: - if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): - widget.drag_get_data(context, target, time) - - cb_service.set_object_percent(object_id, percent=100) - + cb_service.set_object_percent(object_id, percent=0) + + state = {'pending': len(valid_targets), 'object_id': object_id} + for mime_type in valid_targets: + drop.read_async([mime_type], GLib.PRIORITY_DEFAULT, None, self._on_read_raw_cb, (mime_type, state)) return True - def drag_data_received_cb(self, widget, context, x, y, selection, - targetType, time): - logging.debug('ClipboardTray: got data for target %r', - selection.get_target()) - - object_id = self._context_map.get_object_id(context) + def _on_read_raw_cb(self, drop, result, user_data): + mime_type, state = user_data try: - if selection is None: - logging.warn('ClipboardTray: empty selection for target %s', - selection.get_target()) - else: - self._add_selection(object_id, selection) - - finally: - # If it's the last target to be processed, finish - # the dnd transaction - if not self._context_map.has_context(context): - Gdk.drop_finish(context, True, Gtk.get_current_event_time()) - - def _internal_drag(self, context): - source_widget = Gtk.drag_get_source_widget(context) - if source_widget is None: - return False - view_ancestor = source_widget.get_ancestor(Gtk.Viewport) - if view_ancestor is self._viewport: - return True - return False + stream, out_mime = drop.read_finish(result) + if stream: + stream.read_bytes_async(65536, GLib.PRIORITY_DEFAULT, None, self._on_bytes_read_cb, (stream, mime_type, state)) + return + except Exception as e: + logging.error("Failed to read raw drop: %s", e) + self._check_complete(state) + + def _on_bytes_read_cb(self, stream, result, user_data): + stream, mime_type, state = user_data + try: + bytes_data = stream.read_bytes_finish(result) + data = bytes_data.get_data() if bytes_data else None + if data: + cb_service = clipboard.get_instance() + + if mime_type == 'text/uri-list': + try: + text = data.decode('utf-8').strip() + lines = [line.strip() for line in text.splitlines() if line.strip() and not line.startswith('#')] + if lines: + uri = lines[0] + f = Gio.File.new_for_uri(uri) + path = f.get_path() + if path: + cb_service.add_object_format(state['object_id'], mime_type, path, on_disk=True) + self._check_complete(state) + return + except Exception: + pass + + if mime_type.startswith('text/') or mime_type == 'journal-object-id': + try: + data = data.decode('utf-8') + except Exception: + pass + cb_service.add_object_format(state['object_id'], mime_type, data, on_disk=False) + except Exception as e: + logging.error("Failed to read dropped bytes: %s", e) + self._check_complete(state) + + def _check_complete(self, state): + state['pending'] -= 1 + if state['pending'] <= 0: + cb_service = clipboard.get_instance() + cb_service.set_object_percent(state['object_id'], percent=100) diff --git a/src/jarabe/frame/devicestray.py b/src/jarabe/frame/devicestray.py index c3c2136abe..6e72a7ddfd 100644 --- a/src/jarabe/frame/devicestray.py +++ b/src/jarabe/frame/devicestray.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -31,23 +31,24 @@ def __init__(self): if f.endswith('.py') and not f.startswith('__'): module_name = f[:-3] try: - mod = importlib.import_module('deviceicon.' + module_name, globals()) + mod = importlib.import_module('deviceicon.' + module_name) mod.setup(self) - except Exception: - logging.exception('Exception while loading extension:') + except Exception as e: + logging.error(f'Exception while loading extension {module_name}: {e}', exc_info=True) def add_device(self, view): index = 0 relative_index = getattr(view, 'FRAME_POSITION_RELATIVE', -1) + for item in self.get_children(): - current_relative_index = getattr(item, 'FRAME_POSITION_RELATIVE', - 0) + current_relative_index = getattr(item, 'FRAME_POSITION_RELATIVE', 0) if current_relative_index >= relative_index: index += 1 else: break + self.add_item(view, index=index) - view.show() + view.set_visible(True) def remove_device(self, view): self.remove_item(view) diff --git a/src/jarabe/frame/eventarea.py b/src/jarabe/frame/eventarea.py index 35d78cdce6..c86d91ecb9 100644 --- a/src/jarabe/frame/eventarea.py +++ b/src/jarabe/frame/eventarea.py @@ -1,26 +1,12 @@ -# Copyright (C) 2007, Red Hat, Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository import GObject -from gi.repository import GLib -#from gi.repository import Wnck +import logging -from sugar4.graphics import style +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GObject, GLib, Gdk + +HAS_LAYER_SHELL = False +from sugar4.graphics import style _MAX_DELAY = 1000 @@ -36,13 +22,21 @@ class EventArea(GObject.GObject): } def __init__(self, settings): - GObject.GObject.__init__(self) + super().__init__() + self._hover = False + self._is_active = False self._sids = {} self._boxes = {} self._tags = {} + + self._edge_delay = _MAX_DELAY + self._corner_delay = _MAX_DELAY + + + for tag in _BOXES: box = self._box(tag) self._tags[box] = tag @@ -51,54 +45,94 @@ def __init__(self, settings): settings.connect('changed', self._settings_changed_cb) self._settings_changed_cb(settings, None) - # Figure out a replacement for this. - #screen = Wnck.Screen.get_default() - #screen.connect('window-stacking-changed', - # self._window_stacking_changed_cb) - def _box(self, tag): - box = Gtk.Invisible() - box.connect('enter-notify-event', self._enter_notify_cb) - box.connect('leave-notify-event', self._leave_notify_cb) - box.drag_dest_set(0, [], 0) - box.connect('drag-motion', self._drag_motion_cb) - box.connect('drag-leave', self._drag_leave_cb) - box.realize() - return box + window = Gtk.Box() + window.add_css_class('transparent-window') + + provider = Gtk.CssProvider() + provider.load_from_string('.transparent-window, .transparent-window:hover { background-color: rgba(0, 0, 0, 0.01); }') + window.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + if tag == 'nw': + window.set_valign(Gtk.Align.START) + window.set_halign(Gtk.Align.START) + elif tag == 'ne': + window.set_valign(Gtk.Align.START) + window.set_halign(Gtk.Align.END) + elif tag == 'se': + window.set_valign(Gtk.Align.END) + window.set_halign(Gtk.Align.END) + elif tag == 'sw': + window.set_valign(Gtk.Align.END) + window.set_halign(Gtk.Align.START) + elif tag == 'n': + window.set_valign(Gtk.Align.START) + window.set_halign(Gtk.Align.FILL) + elif tag == 's': + window.set_valign(Gtk.Align.END) + window.set_halign(Gtk.Align.FILL) + elif tag == 'e': + window.set_valign(Gtk.Align.FILL) + window.set_halign(Gtk.Align.END) + elif tag == 'w': + window.set_valign(Gtk.Align.FILL) + window.set_halign(Gtk.Align.START) + + + controller = Gtk.EventControllerMotion() + controller.connect('enter', self._enter_notify_cb, window) + controller.connect('leave', self._leave_notify_cb, window) + window.add_controller(controller) + + # Drop support + drop_target = Gtk.DropTargetAsync.new(None, Gdk.DragAction.COPY) + drop_target.connect('accept', self._drag_accept_cb, window) + drop_target.connect('drag-enter', self._drag_motion_cb, window) + drop_target.connect('drag-leave', self._drag_leave_cb, window) + window.add_controller(drop_target) + + return window def _settings_changed_cb(self, settings, key): self._edge_delay = min(settings.get_int('edge-delay'), _MAX_DELAY) self._corner_delay = min(settings.get_int('corner-delay'), _MAX_DELAY) ts = min(settings.get_int('trigger-size'), style.GRID_CELL_SIZE) - sw = Gdk.Screen.width() - sh = Gdk.Screen.height() if self._edge_delay == _MAX_DELAY: self._hide(_EDGES) else: - self._move('n', ts, -1, sw - ts * 2, ts + 1) - self._move('e', -1, ts, ts + 1, sh - ts * 2) - self._move('s', ts, sh - ts, sw - ts * 2, ts + 1) - self._move('w', sw - ts, ts, ts + 1, sh - ts * 2) + self._move('n', ts) + self._move('e', ts) + self._move('s', ts) + self._move('w', ts) if self._corner_delay == _MAX_DELAY: self._hide(_CORNERS) else: - self._move('nw', -1, -1, ts + 1, ts + 1) - self._move('ne', sw - ts, -1, ts + 1, ts + 1) - self._move('se', sw - ts, sh - ts, ts + 1, ts + 1) - self._move('sw', -1, sh - ts, ts + 1, ts + 1) + self._move('nw', ts) + self._move('ne', ts) + self._move('se', ts) + self._move('sw', ts) def _hide(self, tags): for tag in tags: - self._move(tag, -20, -20, 1, 1) - - def _move(self, tag, x, y, width, height): - window = self._boxes[tag].get_window() - window.set_events(Gdk.EventMask.POINTER_MOTION_MASK | - Gdk.EventMask.ENTER_NOTIFY_MASK | - Gdk.EventMask.LEAVE_NOTIFY_MASK) - window.move_resize(x, y, width, height) + self._boxes[tag].set_visible(False) + + def _move(self, tag, ts): + window = self._boxes[tag] + if tag in ['n', 's']: + window.set_size_request(-1, ts) + window.set_margin_start(ts) + window.set_margin_end(ts) + elif tag in ['e', 'w']: + window.set_size_request(ts, -1) + window.set_margin_top(ts) + window.set_margin_bottom(ts) + else: + window.set_size_request(ts, ts) + + if self._is_active: + window.set_visible(True) def _notify_enter(self): if not self._hover: @@ -110,50 +144,59 @@ def _notify_leave(self): self._hover = False self.emit('leave') - def _enter_notify_cb(self, widget, event): - if self._sids: - GLib.source_remove(widget.sid) - del self._sids[widget] + def _enter_notify_cb(self, controller, x, y, window): + if window in self._sids: + GLib.source_remove(self._sids[window]) + del self._sids[window] delay = None - if self._tags[widget] in _CORNERS: + if self._tags[window] in _CORNERS: delay = self._corner_delay - if self._tags[widget] in _EDGES: + if self._tags[window] in _EDGES: delay = self._edge_delay if delay is not None: - self._sids[widget] = GLib.timeout_add(delay, - self.__delay_cb, - widget) + self._sids[window] = GLib.timeout_add(delay, self.__delay_cb, window) - def __delay_cb(self, widget): - del self._sids[widget] + def __delay_cb(self, window): + del self._sids[window] self._notify_enter() return False - def _leave_notify_cb(self, widget, event): - if widget in self._sids: - GLib.source_remove(self._sids[widget]) - del self._sids[widget] + def _leave_notify_cb(self, controller, window): + if window in self._sids: + GLib.source_remove(self._sids[window]) + del self._sids[window] self._notify_leave() - def _drag_motion_cb(self, widget, drag_context, x, y, timestamp): - Gdk.drag_status(drag_context, 0, timestamp) - self._notify_enter() + def _drag_accept_cb(self, drop_target, drop, window): return True - def _drag_leave_cb(self, widget, drag_context, timestamp): + def _drag_motion_cb(self, drop_target, drop, x, y, window): + self._notify_enter() + return Gdk.DragAction.COPY + + def _drag_leave_cb(self, drop_target, drop, window): self._notify_leave() - return True def show(self): - for box in list(self._boxes.values()): - box.show() + self._is_active = True + if not HAS_LAYER_SHELL: + return + for tag in _BOXES: + delay = self._corner_delay if tag in _CORNERS else self._edge_delay + if delay != _MAX_DELAY: + self._boxes[tag].set_visible(True) def hide(self): + self._is_active = False + if not HAS_LAYER_SHELL: + return for box in list(self._boxes.values()): - box.hide() + box.set_visible(False) - def _window_stacking_changed_cb(self, screen): - for box in list(self._boxes.values()): - box.get_window().raise_() + def set_visible(self, visible): + if visible: + self.show() + else: + self.hide() diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py index 25150d9ea2..37330029ef 100644 --- a/src/jarabe/frame/frame.py +++ b/src/jarabe/frame/frame.py @@ -20,7 +20,6 @@ from gi.repository import GLib from gi.repository import Gio -from sugar4.graphics import animator from sugar4.graphics import style from sugar4.graphics import palettegroup from sugar4 import profile @@ -35,7 +34,6 @@ from jarabe.frame.notification import NotificationIcon, NotificationWindow from jarabe.model import notifications - TOP_RIGHT = 0 TOP_LEFT = 1 BOTTOM_RIGHT = 2 @@ -44,15 +42,15 @@ NOTIFICATION_DURATION = 5000 -class _Animation(animator.Animation): - - def __init__(self, frame, end): - start = frame.current_position - animator.Animation.__init__(self, start, end) - self._frame = frame +def _get_screen_size(): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geometry = monitors.get_item(0).get_geometry() + return geometry.width, geometry.height + return 1024, 768 - def next_frame(self, current): - self._frame.move(current) class Frame(object): @@ -69,20 +67,49 @@ def __init__(self): self._bottom_panel = None self._wanted = False - self.current_position = 0.0 - self._animator = None - + self._hide_timeout_id = 0 + self._event_area = EventArea(self.settings) self._event_area.connect('enter', self._enter_corner_cb) - self._event_area.show() - + self._event_area.connect('leave', self._leave_corner_cb) + self._top_panel = self._create_top_panel() self._bottom_panel = self._create_bottom_panel() self._left_panel = self._create_left_panel() self._right_panel = self._create_right_panel() - screen = Gdk.Screen.get_default() - screen.connect('size-changed', self._size_changed_cb) + for panel in (self._top_panel, self._bottom_panel, + self._left_panel, self._right_panel): + if panel: + controller = Gtk.EventControllerMotion.new() + controller.connect('enter', self._panel_enter_cb, panel) + controller.connect('leave', self._panel_leave_cb, panel) + panel.add_controller(controller) + + from jarabe.model import shell + shell_model = shell.get_model() + if hasattr(shell_model, '_overlay') and shell_model._overlay: + # Add event area hot corners + for box in self._event_area._boxes.values(): + shell_model._overlay.add_overlay(box) + box.set_visible(True) + + # Add frame panels + shell_model._overlay.add_overlay(self._top_panel) + shell_model._overlay.add_overlay(self._bottom_panel) + shell_model._overlay.add_overlay(self._left_panel) + shell_model._overlay.add_overlay(self._right_panel) + + + + display = Gdk.Display.get_default() + if display: + self._monitors = display.get_monitors() + if self._monitors: + self._monitors.connect('items-changed', self._size_changed_cb) + for i in range(self._monitors.get_n_items()): + monitor = self._monitors.get_item(i) + monitor.connect('notify::geometry', self._monitor_geometry_changed_cb) self._notif_by_icon = {} @@ -93,7 +120,12 @@ def __init__(self): self.__notification_cancelled_cb) def is_visible(self): - return self.current_position != 0.0 + # Check if any panel is currently revealed + for panel in (self._top_panel, self._bottom_panel, + self._left_panel, self._right_panel): + if panel and panel.get_reveal_child(): + return True + return False visible = property(is_visible, None) @@ -104,47 +136,37 @@ def toggle(self): self.hide() def hide(self): + self._cancel_hide_timeout() if not self._wanted: return - self._wanted = False - - if self._animator: - self._animator.stop() - palettegroup.popdown_all() - self._animator = animator.Animator(0.5, widget=self._top_panel) - self._animator.add(_Animation(self, 0.0)) - self._animator.start() + for panel in (self._top_panel, self._bottom_panel, + self._left_panel, self._right_panel): + if panel: + panel.set_reveal_child(False) def show(self): + self._cancel_hide_timeout() if self._wanted: return - self._wanted = True - - if self._animator: - self._animator.stop() - - self._animator = animator.Animator(0.5, widget=self._top_panel) - self._animator.add(_Animation(self, 1.0)) - self._animator.start() - - def move(self, pos): - self.current_position = pos - self._update_position() + for panel in (self._top_panel, self._bottom_panel, + self._left_panel, self._right_panel): + if panel: + panel.set_reveal_child(True) def _create_top_panel(self): panel = self._create_panel(Gtk.PositionType.TOP) zoom_toolbar = ZoomToolbar() panel.append(zoom_toolbar, expand=False) - zoom_toolbar.show() + zoom_toolbar.set_visible(True) zoom_toolbar.connect('level-clicked', self._level_clicked_cb) activities_tray = ActivitiesTray() panel.append(activities_tray) - activities_tray.show() + activities_tray.set_visible(True) return panel @@ -153,7 +175,7 @@ def _create_bottom_panel(self): devices_tray = DevicesTray() panel.append(devices_tray) - devices_tray.show() + devices_tray.set_visible(True) return panel @@ -162,56 +184,80 @@ def _create_right_panel(self): tray = FriendsTray() panel.append(tray) - tray.show() + tray.set_visible(True) return panel def _create_left_panel(self): panel = ClipboardPanelWindow(self, Gtk.PositionType.LEFT) - return panel def _create_panel(self, orientation): panel = FrameWindow(orientation) - return panel - def _move_panel(self, panel, pos, x1, y1, x2, y2): - x = (x2 - x1) * pos + x1 - y = (y2 - y1) * pos + y1 - - panel.move(int(x), int(y)) - - # FIXME we should hide and show as necessary to free memory - if not panel.props.visible: - panel.show() - def _level_clicked_cb(self, zoom_toolbar): self.hide() def _update_position(self): - screen_h = Gdk.Screen.height() - screen_w = Gdk.Screen.width() - - self._move_panel(self._top_panel, self.current_position, - 0, - self._top_panel.size, 0, 0) - - self._move_panel(self._bottom_panel, self.current_position, - 0, screen_h, 0, screen_h - self._bottom_panel.size) - - self._move_panel(self._left_panel, self.current_position, - - self._left_panel.size, 0, 0, 0) - - self._move_panel(self._right_panel, self.current_position, - screen_w, 0, screen_w - self._right_panel.size, 0) - - def _size_changed_cb(self, screen): + # With Revealer approach, animation is handled by the Revealer widget. + # This method is kept for compatibility (called on screen resize). + # Ensure panels reflect wanted state after a resize. + for panel in (self._top_panel, self._bottom_panel, + self._left_panel, self._right_panel): + if panel is None: + continue + panel.set_reveal_child(self._wanted) + + def _size_changed_cb(self, monitors, position, removed, added): + for i in range(added): + monitor = monitors.get_item(position + i) + if monitor: + monitor.connect('notify::geometry', self._monitor_geometry_changed_cb) + self._update_position() + + def _monitor_geometry_changed_cb(self, monitor, pspec): self._update_position() def _enter_corner_cb(self, event_area): + self._cancel_hide_timeout() self.toggle() + def _panel_enter_cb(self, controller, x, y, panel): + panel.hover = True + self._cancel_hide_timeout() + + def _panel_leave_cb(self, controller, panel): + panel.hover = False + self._check_auto_hide() + + def _leave_corner_cb(self, event_area): + self._check_auto_hide() + + def _cancel_hide_timeout(self): + if self._hide_timeout_id: + GLib.source_remove(self._hide_timeout_id) + self._hide_timeout_id = 0 + + def _check_auto_hide(self): + self._cancel_hide_timeout() + # Schedule auto-hide check after 250ms + self._hide_timeout_id = GLib.timeout_add(250, self._auto_hide_cb) + + def _auto_hide_cb(self): + self._hide_timeout_id = 0 + any_hover = self._event_area._hover or \ + (self._top_panel and getattr(self._top_panel, 'hover', False)) or \ + (self._bottom_panel and getattr(self._bottom_panel, 'hover', False)) or \ + (self._left_panel and getattr(self._left_panel, 'hover', False)) or \ + (self._right_panel and getattr(self._right_panel, 'hover', False)) + + if not any_hover and self._wanted: + self.hide() + return False + def notify_key_press(self): + self._cancel_hide_timeout() self.toggle() ''' @@ -227,22 +273,27 @@ def add_notification(self, icon, corner=Gtk.CornerType.TOP_LEFT, window = NotificationWindow() - screen = Gdk.Screen.get_default() if corner == Gtk.CornerType.TOP_LEFT: - window.move(0, 0) + window.set_halign(Gtk.Align.START) + window.set_valign(Gtk.Align.START) elif corner == Gtk.CornerType.TOP_RIGHT: - window.move(screen.get_width() - style.GRID_CELL_SIZE, 0) + window.set_halign(Gtk.Align.END) + window.set_valign(Gtk.Align.START) elif corner == Gtk.CornerType.BOTTOM_LEFT: - window.move(0, screen.get_height() - style.GRID_CELL_SIZE) + window.set_halign(Gtk.Align.START) + window.set_valign(Gtk.Align.END) elif corner == Gtk.CornerType.BOTTOM_RIGHT: - window.move(screen.get_width() - style.GRID_CELL_SIZE, - screen.get_height() - style.GRID_CELL_SIZE) - else: - raise ValueError('Inalid corner: %r' % corner) + window.set_halign(Gtk.Align.END) + window.set_valign(Gtk.Align.END) + + window.append(icon) + icon.set_visible(True) + window.set_visible(True) - window.add(icon) - icon.show() - window.show() + from jarabe.model import shell + shell_model = shell.get_model() + if hasattr(shell_model, '_overlay') and shell_model._overlay: + shell_model._overlay.add_overlay(window) self._notif_by_icon[icon] = window @@ -256,10 +307,15 @@ def remove_notification(self, icon): return window = self._notif_by_icon[icon] - window.destroy() + parent = window.get_parent() + if parent: + if hasattr(parent, 'remove_overlay'): + parent.remove_overlay(window) + else: + window.unparent() del self._notif_by_icon[icon] - def __button_release_event_cb(self, icon, data=None): + def __button_release_event_cb(self, gesture, n_press, x, y, icon): self.remove_notification(icon) self.show() @@ -267,7 +323,10 @@ def __notification_received_cb(self, **kwargs): logging.debug('__notification_received_cb') icon = NotificationIcon() icon.show_badge() - icon.connect('button-release-event', self.__button_release_event_cb) + + click = Gtk.GestureClick.new() + click.connect('released', self.__button_release_event_cb, icon) + icon.add_controller(click) hints = kwargs['hints'] @@ -292,6 +351,5 @@ def __notification_received_cb(self, **kwargs): self.add_notification(icon, Gtk.CornerType.TOP_LEFT, duration) def __notification_cancelled_cb(self, **kwargs): - # Do nothing for now. Our notification UI is so simple, there's no # point yet. pass diff --git a/src/jarabe/frame/frameinvoker.py b/src/jarabe/frame/frameinvoker.py index 0e62a83ebd..403047170f 100644 --- a/src/jarabe/frame/frameinvoker.py +++ b/src/jarabe/frame/frameinvoker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, Eduardo Silva +# Copyright (C) 2007, Eduardo Silva # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,10 +22,24 @@ def _get_screen_area(): frame_thickness = style.GRID_CELL_SIZE + display = Gdk.Display.get_default() + width = 1024 + height = 768 + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + max_x, max_y = 0, 0 + for i in range(monitors.get_n_items()): + geometry = monitors.get_item(i).get_geometry() + max_x = max(max_x, geometry.x + geometry.width) + max_y = max(max_y, geometry.y + geometry.height) + width = max_x + height = max_y + screen_area = Gdk.Rectangle() screen_area.x = screen_area.y = frame_thickness - screen_area.width = Gdk.Screen.width() - frame_thickness - screen_area.height = Gdk.Screen.height() - frame_thickness + screen_area.width = width - frame_thickness + screen_area.height = height - frame_thickness return screen_area @@ -33,7 +47,6 @@ def _get_screen_area(): class FrameWidgetInvoker(WidgetInvoker): def __init__(self, widget): - WidgetInvoker.__init__(self, widget, widget.get_child()) - + WidgetInvoker.__init__(self, widget, widget) self._position_hint = self.ANCHORED self._screen_area = _get_screen_area() diff --git a/src/jarabe/frame/framewindow.py b/src/jarabe/frame/framewindow.py index 5e6d018ba9..0b7f52c248 100644 --- a/src/jarabe/frame/framewindow.py +++ b/src/jarabe/frame/framewindow.py @@ -15,11 +15,32 @@ from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import Graphene +import gi + +gi.require_version('Gsk', '4.0') +from gi.repository import Gsk + +try: + gi.require_version('Gtk4LayerShell', '1.0') + from gi.repository import Gtk4LayerShell +except ValueError: + Gtk4LayerShell = None from sugar4.graphics import style -class FrameContainer(Gtk.Bin): +def _get_screen_size(): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geometry = monitors.get_item(0).get_geometry() + return geometry.width, geometry.height + return 1024, 768 + + +class FrameContainer(Gtk.Widget): """A container class for frame panel rendering. Hosts a child 'box' where frame elements can be added. Excludes grid-sized squares at each end of the frame panel, and a space alongside the inside of the screen where @@ -28,58 +49,80 @@ class FrameContainer(Gtk.Bin): __gtype_name__ = 'SugarFrameContainer' def __init__(self, position): - Gtk.Bin.__init__(self) + super().__init__() self._position = position if self.is_vertical(): - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) else: - box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.add(box) - box.show() + self._box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self._box.set_parent(self) + + def do_dispose(self): + if self._box: + self._box.unparent() + self._box = None + super().do_dispose() + + def get_child_box(self): + return self._box def is_vertical(self): return self._position in (Gtk.PositionType.LEFT, Gtk.PositionType.RIGHT) - def do_draw(self, cr): + def do_snapshot(self, snapshot): + width = self.get_width() + height = self.get_height() + + # Render CSS background (e.g. framewindow background color) + context = self.get_style_context() + snapshot.render_background(context, 0, 0, width, height) + # Draw the inner border as a rectangle - r, g, b, a = style.COLOR_BUTTON_GREY.get_rgba() - cr.set_source_rgba(r, g, b, a) + color = Gdk.RGBA() + color.parse(style.COLOR_BUTTON_GREY.get_svg()) - allocation = self.get_allocation() if self.is_vertical(): x = style.GRID_CELL_SIZE \ if self._position == Gtk.PositionType.LEFT else 0 y = style.GRID_CELL_SIZE - width = style.LINE_WIDTH - height = allocation.height - (style.GRID_CELL_SIZE * 2) + rect_width = style.LINE_WIDTH + rect_height = height - (style.GRID_CELL_SIZE * 2) else: x = style.GRID_CELL_SIZE y = style.GRID_CELL_SIZE \ if self._position == Gtk.PositionType.TOP else 0 - height = style.LINE_WIDTH - width = allocation.width - (style.GRID_CELL_SIZE * 2) - - cr.rectangle(x, y, width, height) - cr.fill() - - Gtk.Bin.do_draw(self, cr) - return False - - def do_size_request(self, req): + rect_height = style.LINE_WIDTH + rect_width = width - (style.GRID_CELL_SIZE * 2) + + if rect_width > 0 and rect_height > 0: + rect = Graphene.Rect() + rect.init(x, y, rect_width, rect_height) + snapshot.append_color(color, rect) + + if self._box: + self.snapshot_child(self._box, snapshot) + + def do_measure(self, orientation, for_size): + if self._box: + self._box.measure(orientation, for_size) + + sw, sh = _get_screen_size() if self.is_vertical(): - req.height = Gdk.Screen.height() - req.width = style.GRID_CELL_SIZE + style.LINE_WIDTH + if orientation == Gtk.Orientation.VERTICAL: + return sh, sh, -1, -1 + else: + w = style.GRID_CELL_SIZE + style.LINE_WIDTH + return w, w, -1, -1 else: - req.width = Gdk.Screen.width() - req.height = style.GRID_CELL_SIZE + style.LINE_WIDTH - - self.get_child().size_request() - - def do_size_allocate(self, allocation): - self.set_allocation(allocation) + if orientation == Gtk.Orientation.HORIZONTAL: + return sw, sw, -1, -1 + else: + h = style.GRID_CELL_SIZE + style.LINE_WIDTH + return h, h, -1, -1 + def do_size_allocate(self, width, height, baseline): # exclude grid squares at two ends of the frame # allocate remaining space to child box, minus the space needed for # drawing the border @@ -88,74 +131,118 @@ def do_size_allocate(self, allocation): allocation.x = 0 if self._position == Gtk.PositionType.LEFT \ else style.LINE_WIDTH allocation.y = style.GRID_CELL_SIZE - allocation.width = self.get_allocation().width - style.LINE_WIDTH - allocation.height = self.get_allocation().height \ - - (style.GRID_CELL_SIZE * 2) + allocation.width = max(0, width - style.LINE_WIDTH) + allocation.height = max(0, height - (style.GRID_CELL_SIZE * 2)) else: allocation.x = style.GRID_CELL_SIZE allocation.y = 0 if self._position == Gtk.PositionType.TOP \ else style.LINE_WIDTH - allocation.width = self.get_allocation().width \ - - (style.GRID_CELL_SIZE * 2) - allocation.height = self.get_allocation().height - style.LINE_WIDTH + allocation.width = max(0, width - (style.GRID_CELL_SIZE * 2)) + allocation.height = max(0, height - style.LINE_WIDTH) + + if self._box: + transform = Gsk.Transform.new().translate(Graphene.Point().init(allocation.x, allocation.y)) + self._box.allocate(allocation.width, allocation.height, baseline, transform) + - self.get_child().size_allocate(allocation) +class FrameWindow(Gtk.Revealer): + """A frame panel that slides in/out using Gtk.Revealer. + GTK3 used floating Gtk.Window objects that were moved with window.move(). + In GTK4, Gtk.Revealer is the correct widget for slide-in/out animations. + It handles clipping, animation, and positioning natively. -class FrameWindow(Gtk.Window): + The Revealer wraps a FrameContainer (the actual drawing surface). + It is added to the shell's Gtk.Overlay as an overlay child, positioned + at its respective screen edge via valign/halign. + """ __gtype_name__ = 'SugarFrameWindow' def __init__(self, position): - Gtk.Window.__init__(self) - self.set_has_resize_grip(False) + # Choose transition type based on panel edge + if position == Gtk.PositionType.TOP: + transition = Gtk.RevealerTransitionType.SLIDE_DOWN + elif position == Gtk.PositionType.BOTTOM: + transition = Gtk.RevealerTransitionType.SLIDE_UP + elif position == Gtk.PositionType.LEFT: + transition = Gtk.RevealerTransitionType.SLIDE_RIGHT + else: # RIGHT + transition = Gtk.RevealerTransitionType.SLIDE_LEFT + + super().__init__( + transition_type=transition, + transition_duration=500, + reveal_child=False, + ) + self.hover = False self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH - - accel_group = Gtk.AccelGroup() - self.sugar_accel_group = accel_group - self.add_accel_group(accel_group) - self._position = position - self.set_decorated(False) - self.connect('realize', self._realize_cb) - self.connect('enter-notify-event', self._enter_notify_cb) - self.connect('leave-notify-event', self._leave_notify_cb) + # Shortcut controller + self.shortcut_controller = Gtk.ShortcutController.new() + self.shortcut_controller.set_scope(Gtk.ShortcutScope.GLOBAL) + self.add_controller(self.shortcut_controller) + self.sugar_accel_group = self.shortcut_controller + # Hover tracking + controller = Gtk.EventControllerMotion.new() + controller.connect('enter', self._enter_notify_cb) + controller.connect('leave', self._leave_notify_cb) + self.add_controller(controller) + + # The actual drawing surface self._container = FrameContainer(position) - self.add(self._container) - self._container.show() + self._container.add_css_class('framewindow') + self.set_child(self._container) + + # Position the revealer at its screen edge via Overlay alignment + if position == Gtk.PositionType.TOP: + self.set_valign(Gtk.Align.START) + self.set_halign(Gtk.Align.FILL) + elif position == Gtk.PositionType.BOTTOM: + self.set_valign(Gtk.Align.END) + self.set_halign(Gtk.Align.FILL) + elif position == Gtk.PositionType.LEFT: + self.set_valign(Gtk.Align.FILL) + self.set_halign(Gtk.Align.START) + elif position == Gtk.PositionType.RIGHT: + self.set_valign(Gtk.Align.FILL) + self.set_halign(Gtk.Align.END) + self._update_size() - screen = Gdk.Screen.get_default() - screen.connect('size-changed', self._size_changed_cb) + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors: + monitors.connect('items-changed', self._size_changed_cb) def append(self, child, expand=True, fill=True): - self._container.get_child().pack_start(child, expand=expand, fill=fill, - padding=0) + if expand: + if self._container.is_vertical(): + child.set_vexpand(True) + else: + child.set_hexpand(True) + if not fill: + if self._container.is_vertical(): + child.set_valign(Gtk.Align.CENTER) + else: + child.set_halign(Gtk.Align.CENTER) + self._container.get_child_box().append(child) def _update_size(self): - if self._position == Gtk.PositionType.TOP \ - or self._position == Gtk.PositionType.BOTTOM: - self.resize(Gdk.Screen.width(), self.size) + sw, sh = _get_screen_size() + if self._position in (Gtk.PositionType.TOP, Gtk.PositionType.BOTTOM): + self._container.set_size_request(sw, self.size) else: - self.resize(self.size, Gdk.Screen.height()) - - def _realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DOCK) - self.get_window().set_accept_focus(False) + self._container.set_size_request(self.size, sh) - def _enter_notify_cb(self, window, event): - if event.detail != Gdk.NotifyType.INFERIOR: - self.hover = True + def _enter_notify_cb(self, controller, x, y): + self.hover = True - def _leave_notify_cb(self, window, event): - if event.detail != Gdk.NotifyType.INFERIOR: - self.hover = False + def _leave_notify_cb(self, controller): + self.hover = False - def _size_changed_cb(self, screen): + def _size_changed_cb(self, monitors, position, removed, added): self._update_size() - - -if hasattr(FrameWindow, 'set_css_name'): - FrameWindow.set_css_name('framewindow') diff --git a/src/jarabe/frame/friendstray.py b/src/jarabe/frame/friendstray.py index 2ab67cc950..40f660f54b 100644 --- a/src/jarabe/frame/friendstray.py +++ b/src/jarabe/frame/friendstray.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -56,13 +56,16 @@ def __init__(self): neighborhood.get_model().connect('activity-added', self.__neighborhood_activity_added_cb) + # always display ourselves by default + self.add_buddy(get_owner_instance()) + def add_buddy(self, buddy): if buddy.props.key in self._buddies: return icon = FriendIcon(buddy) self.add_item(icon) - icon.show() + icon.set_visible(True) self._buddies[buddy.props.key] = icon @@ -74,9 +77,8 @@ def remove_buddy(self, buddy): del self._buddies[buddy.props.key] def clear(self): - for item in self.get_children(): - self.remove_item(item) - item.destroy() + for child in self.get_children(): + self.remove_item(child) self._buddies = {} def __neighborhood_activity_added_cb(self, neighborhood_model, diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py index c7f323aae4..ad534d1e09 100644 --- a/src/jarabe/frame/notification.py +++ b/src/jarabe/frame/notification.py @@ -21,6 +21,7 @@ from gi.repository import GLib from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import Graphene from sugar4 import profile from sugar4.graphics import style @@ -44,28 +45,24 @@ class NotificationBox(Gtk.Box): ELLIPSIS_AND_BREAKS = 6 def __init__(self, name): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self._name = name self._notifications_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._notifications_box.show() self._scrolled_window = Gtk.ScrolledWindow() - self._scrolled_window.add_with_viewport(self._notifications_box) + self._scrolled_window.set_child(self._notifications_box) self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self._scrolled_window.show() separator = PaletteMenuItemSeparator() - separator.show() clear_item = PaletteMenuItem(_('Clear notifications'), 'dialog-cancel') clear_item.connect('activate', self.__clear_cb) - clear_item.show() - self.add(self._scrolled_window) - self.add(separator) - self.add(clear_item) + self.append(self._scrolled_window) + self.append(separator) + self.append(clear_item) self._service = notifications.get_service() entries = self._service.retrieve_by_name(self._name) @@ -73,44 +70,54 @@ def __init__(self, name): if entries: for entry in entries: self._add(entry['summary'], entry['body']) + else: + self.set_visible(False) - self._service.notification_received.connect( + self._notify_handler = self._service.notification_received.connect( self.__notification_received_cb) - self.connect('destroy', self.__destroy_cb) + def _get_entries(self): + entries = [] + child = self._notifications_box.get_first_child() + while child: + entries.append(child) + child = child.get_next_sibling() + return entries def _update_scrolled_size(self): - entries = self._notifications_box.get_children() + entries = self._get_entries() height = 0 for entry in entries[:self.MAX_ENTRIES]: - requests = entry.get_preferred_size() - height += requests[1].height + _min, nat_height, _min_b, _nat_b = entry.measure( + Gtk.Orientation.VERTICAL, -1) + height += nat_height self._scrolled_window.set_size_request(-1, height) def _add(self, summary, body): icon = Icon() icon.props.icon_name = 'emblem-notification' - icon.props.icon_size = Gtk.IconSize.SMALL_TOOLBAR + icon.props.pixel_size = style.SMALL_ICON_SIZE + icon.props.xo_color = \ XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), style.COLOR_BLACK.get_svg())) - icon.show() summary_label = Gtk.Label() summary_label.set_max_width_chars(style.MENU_WIDTH_CHARS) summary_label.set_ellipsize(style.ELLIPSIZE_MODE_DEFAULT) - summary_label.set_alignment(0, 0.5) + summary_label.set_xalign(0.0) + summary_label.set_yalign(0.5) summary_label.set_markup('%s' % summary) - summary_label.show() body_label = Gtk.Label() - body_label.set_alignment(0, 0.5) + body_label.set_xalign(0.0) + body_label.set_yalign(0.5) if hasattr(body_label, 'set_lines'): body_label.set_max_width_chars(style.MENU_WIDTH_CHARS) - body_label.set_line_wrap(True) + body_label.set_wrap(True) body_label.set_ellipsize(style.ELLIPSIZE_MODE_DEFAULT) body_label.set_lines(self.LINES) body_label.set_justify(Gtk.Justification.FILL) @@ -124,38 +131,43 @@ def _add(self, summary, body): body = textwrap.fill(body, width=style.MENU_WIDTH_CHARS) body_label.set_text(body) - body_label.show() grid = Gtk.Grid() - grid.set_border_width(style.DEFAULT_SPACING) grid.set_column_spacing(style.DEFAULT_SPACING) grid.set_row_spacing(0) + grid.set_margin_top(style.DEFAULT_SPACING) + grid.set_margin_bottom(style.DEFAULT_SPACING) + grid.set_margin_start(style.DEFAULT_SPACING) + grid.set_margin_end(style.DEFAULT_SPACING) grid.attach(icon, 0, 0, 1, 2) grid.attach(summary_label, 1, 0, 1, 1) grid.attach(body_label, 1, 1, 1, 1) - grid.show() - self._notifications_box.add(grid) + self._notifications_box.append(grid) self._update_scrolled_size() - self.show() def __clear_cb(self, clear_item): logging.debug('NotificationBox.__clear_cb') - for entry in self._notifications_box.get_children(): - self._notifications_box.remove(entry) + child = self._notifications_box.get_first_child() + while child: + next_child = child.get_next_sibling() + if child.get_parent() == self._notifications_box: + child.unparent() + child = next_child self._service.clear_by_name(self._name) - self.hide() + self.set_visible(False) def __notification_received_cb(self, **kwargs): logging.debug('NotificationBox.__notification_received_cb') if kwargs.get('app_name', '') == self._name: self._add(kwargs.get('summary', ''), kwargs.get('body', '')) - def __destroy_cb(self, box): - logging.debug('NotificationBox.__destroy_cb') + def do_unroot(self): + logging.debug('NotificationBox.do_unroot') service = notifications.get_service() - service.notification_received.disconnect( - self. __notification_received_cb) + if hasattr(self, '_notify_handler') and self._notify_handler: + service.notification_received.disconnect(self._notify_handler) + self._notify_handler = 0 class NotificationButton(ToolButton): @@ -168,9 +180,12 @@ def __init__(self, name): self.palette_invoker.cache_palette = False self.connect('clicked', self.__clicked_cb) + def remove_notification(self, icon): + # no-op: notifications are managed by the frame view + logging.debug('NotificationButton.remove_notification called') + def set_icon(self, icon): self._icon = icon - self._icon.show() self.set_icon_widget(self._icon) def show_badge(self): @@ -223,21 +238,25 @@ def show_badge(self): def hide_badge(self): self._badge = None - def do_draw(self, cr): - PulsingIcon.do_draw(self, cr) + def do_snapshot(self, snapshot): + PulsingIcon.do_snapshot(self, snapshot) if self._badge: - allocation = self.get_allocation() + width = self.get_width() + height = self.get_height() - # XXX assume icon is centered in its container offset = int(self.props.pixel_size / 2) - self.get_badge_size() - x = int(allocation.width / 2) + offset - y = int(allocation.height / 2) + offset - - cr.set_source_surface(self._badge, x, y) + x = int(width / 2) + offset + y = int(height / 2) + offset + + badge_size = self.get_badge_size() + rect = Graphene.Rect() + rect.init(x, y, badge_size, badge_size) + cr = snapshot.append_cairo(rect) + cr.set_source_surface(self._badge, 0, 0) cr.paint() -class NotificationIcon(Gtk.EventBox): +class NotificationIcon(Gtk.Box): __gtype_name__ = 'SugarNotificationIcon' __gproperties__ = { @@ -252,15 +271,13 @@ def __init__(self, **kwargs): self._icon = NotificationPulsingIcon() self._icon.props.pixel_size = style.STANDARD_ICON_SIZE - Gtk.EventBox.__init__(self, **kwargs) - self.props.visible_window = False + super().__init__(**kwargs) self._icon.props.pulse_color = \ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg())) self._icon.props.pulsing = True - self.add(self._icon) - self._icon.show() + self.append(self._icon) GLib.timeout_add_seconds(self._PULSE_TIMEOUT, self.__stop_pulsing_cb) @@ -305,20 +322,16 @@ def hide_badge(self): self._icon.hide_badge() -class NotificationWindow(Gtk.Window): +class NotificationWindow(Gtk.Box): __gtype_name__ = 'SugarNotificationWindow' def __init__(self, **kwargs): - - Gtk.Window.__init__(self, **kwargs) - - self.set_decorated(False) - self.set_resizable(False) - self.connect('realize', self._realize_cb) - - def _realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - self.get_window().set_accept_focus(False) - - color = Gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) - self.modify_bg(Gtk.StateType.NORMAL, color) + # strip Gtk.Window-specific kwargs not valid on Gtk.Box + box_kwargs = {k: v for k, v in kwargs.items() + if k not in ('decorated', 'resizable', 'focusable')} + super().__init__(**box_kwargs) + + self.add_css_class('notification-window') + self.add_css_class('frame-notification') + self.set_valign(Gtk.Align.START) + self.set_halign(Gtk.Align.START) diff --git a/src/jarabe/frame/zoomtoolbar.py b/src/jarabe/frame/zoomtoolbar.py index e5b18ab37f..70ea743041 100644 --- a/src/jarabe/frame/zoomtoolbar.py +++ b/src/jarabe/frame/zoomtoolbar.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2009 Simon Schampijer # # This program is free software: you can redistribute it and/or modify @@ -29,14 +29,17 @@ from jarabe.model import shell -class ZoomToolbar(Gtk.Toolbar): +class ZoomToolbar(Gtk.Box): + __gtype_name__ = 'SugarZoomToolbar' + __gsignals__ = { 'level-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])) } def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self.add_css_class('toolbar') # we shouldn't be mirrored in RTL locales self.set_direction(Gtk.TextDirection.LTR) @@ -67,29 +70,37 @@ def __init__(self): shell_model.zoom_level_changed.connect(self.__zoom_level_changed_cb) def _add_button(self, icon_name, label, accelerator, zoom_level): - if self.get_children(): - group = self.get_children()[0] - else: - group = None + group = self.get_first_child() button = RadioToolButton(icon_name=icon_name, group=group, accelerator=accelerator) - button.connect('clicked', self.__level_clicked_cb, zoom_level) - self.add(button) - button.show() + button.connect('toggled', self.__level_toggled_cb, zoom_level) + self.append(button) - palette = Palette(GLib.markup_escape_text(label)) - palette.props.invoker = FrameWidgetInvoker(button) + invoker = FrameWidgetInvoker(button) + palette = Palette(label) + invoker.palette = palette palette.set_group_id('frame') - button.set_palette(palette) + button.set_palette_invoker(invoker) return button - def __level_clicked_cb(self, button, level): + def __level_toggled_cb(self, button, level): if not button.get_active(): return - shell.get_model().set_zoom_level(level) + model = shell.get_model() + if level == shell.ShellModel.ZOOM_ACTIVITY and \ + model.get_active_activity() is None: + # No activity open: show Journal instead of the empty compositor. + from jarabe.journal import journalactivity + journalactivity.get_journal().show_journal() + # Restore the toolbar button to the current zoom level so the + # toolbar stays consistent (we didn't change the zoom level). + GLib.idle_add(self._set_zoom_level, model.zoom_level) + else: + model.set_zoom_level(level) + self.emit('level-clicked') def __zoom_level_changed_cb(self, **kwargs): @@ -98,12 +109,12 @@ def __zoom_level_changed_cb(self, **kwargs): def _set_zoom_level(self, new_level): logging.debug('new zoom level: %r', new_level) if new_level == shell.ShellModel.ZOOM_MESH: - self._mesh_button.props.active = True + self._mesh_button.set_active(True) elif new_level == shell.ShellModel.ZOOM_GROUP: - self._groups_button.props.active = True + self._groups_button.set_active(True) elif new_level == shell.ShellModel.ZOOM_HOME: - self._home_button.props.active = True + self._home_button.set_active(True) elif new_level == shell.ShellModel.ZOOM_ACTIVITY: - self._activity_button.props.active = True + self._activity_button.set_active(True) else: raise ValueError('Invalid zoom level: %r' % (new_level)) diff --git a/src/jarabe/intro/__init__.py b/src/jarabe/intro/__init__.py index 63ffc9ad15..eb965d7d24 100644 --- a/src/jarabe/intro/__init__.py +++ b/src/jarabe/intro/__init__.py @@ -1,4 +1,4 @@ -import os +import os from gi.repository import Gio diff --git a/src/jarabe/intro/agepicker.py b/src/jarabe/intro/agepicker.py index 9ae9dbe3eb..5d7292b924 100644 --- a/src/jarabe/intro/agepicker.py +++ b/src/jarabe/intro/agepicker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014, Sugar Labs +# Copyright (C) 2014, Sugar Labs # Copyright (C) 2014, Walter Bender # # This program is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gi.repository import Gdk from gi.repository import Gio @@ -34,6 +33,18 @@ from jarabe.intro.genderpicker import GENDERS _group_labels = None +_settings = None + + +def _get_settings(): + global _settings + + if _settings is None: + _settings = Gio.Settings.new('org.sugarlabs.user') + + return _settings + + _SECONDS_PER_YEAR = 365 * 24 * 60 * 60. _DEFAULT_PROMPT = _('Select grade:') _DEFAULT_LABELS = [_('Preschool'), _('Kindergarten'), _('1st Grade'), @@ -87,7 +98,7 @@ def group_label_to_age(label): def load_age(): group_labels = get_group_labels() - settings = Gio.Settings.new('org.sugarlabs.user') + settings = _get_settings() birth_timestamp = settings.get_int('birth-timestamp') if birth_timestamp == 0: @@ -109,29 +120,36 @@ def load_age(): def save_age(age): birth_timestamp = calculate_birth_timestamp(age) - settings = Gio.Settings.new('org.sugarlabs.user') - settings.set_int('birth-timestamp', birth_timestamp) + settings = _get_settings() + + if settings.get_int('birth-timestamp') != birth_timestamp: + settings.set_int('birth-timestamp', birth_timestamp) - # Record the label so we know it was set - settings.set_string('group-label', age_to_group_label(age)) + group_label = age_to_group_label(age) + if settings.get_string('group-label') != group_label: + settings.set_string('group-label', group_label) class GroupLabels(): - GROUP_LABEL = [] + GROUP_LABEL = _DEFAULT_PROMPT AGES = [] LABELS = [] ICONS = [] def __init__(self): - f = open(os.environ['SUGAR_GROUP_LABELS'], 'r') - json_data = f.read() - f.close() - group_labels = json.loads(json_data) - self.GROUP_LABEL = group_labels['group-label'] - for item in group_labels['group-items']: - self.ICONS.append([item['female-icon'], item['male-icon']]) - self.LABELS.append(_(item['label'])) - self.AGES.append(item['age']) + labels_path = os.environ.get('SUGAR_GROUP_LABELS', '/etc/sugar/group-labels') + try: + f = open(labels_path, 'r') + json_data = f.read() + f.close() + group_labels = json.loads(json_data) + self.GROUP_LABEL = group_labels.get('group-label', _DEFAULT_PROMPT) + for item in group_labels.get('group-items', []): + self.ICONS.append([item['female-icon'], item['male-icon']]) + self.LABELS.append(_(item['label'])) + self.AGES.append(item['age']) + except Exception: + self.GROUP_LABEL = _DEFAULT_PROMPT def get_group_labels(): @@ -146,27 +164,17 @@ def get_group_labels(): class Picker(Gtk.Grid): def __init__(self, icon, label): - Gtk.Grid.__init__(self) + super().__init__() self._button = EventIcon(pixel_size=style.LARGE_ICON_SIZE, icon_name=icon) self.attach(self._button, 0, 0, 1, 1) - self._button.hide() + self._button.set_visible(True) - self._label = Gtk.Label(label.replace(' ', '\n')) + self._label = Gtk.Label(label=label.replace(' ', '\n')) self._label.props.justify = Gtk.Justification.CENTER self.attach(self._label, 0, 1, 1, 1) - self._label.hide() - - def show_all(self): - self._button.show() - self._label.show() - self.show() - - def hide_all(self): - self._button.hide() - self._label.hide() - self.hide() + self._label.set_visible(True) def connect(self, callback, arg): self._button.connect('activate', callback, arg) @@ -178,12 +186,21 @@ def set_icon(self, icon): self._button.set_icon_name(icon) +def _get_screen_width(): + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + return monitors.get_item(0).get_geometry().width + return 1024 + + class AgePicker(Gtk.Grid): age_changed_signal = GObject.Signal('age-changed', arg_types=([int])) def __init__(self, gender, page=None): - Gtk.Grid.__init__(self) + super().__init__() self.set_row_spacing(style.DEFAULT_SPACING) self.set_column_spacing(style.DEFAULT_SPACING) @@ -203,7 +220,7 @@ def __init__(self, gender, page=None): gender_index = GENDERS.index(self._gender) age_index = age_to_index(self._age) - width = Gdk.Screen.width() + width = _get_screen_width() num_ages = len(self._group_labels.AGES) for i in range(num_ages): @@ -213,17 +230,17 @@ def __init__(self, gender, page=None): self._pickers[i].connect(self._button_activate_cb, i) self._fixed = Gtk.Fixed() - fixed_size = width - 4 * style.GRID_CELL_SIZE + fixed_size = max(1, 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._fixed.set_visible(True) self._age_adj = Gtk.Adjustment(value=age_index, lower=0, - upper=num_ages - 1, step_incr=1, - page_incr=3, page_size=0) + upper=num_ages - 1, step_increment=1, + page_increment=3, page_size=0) self._age_adj.connect('value-changed', self.__age_adj_changed_cb) - self._age_slider = Gtk.HScale() + self._age_slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) self._age_slider.set_draw_value(False) self._age_slider.set_adjustment(self._age_adj) self.attach(self._age_slider, 0, 1, 1, 1) @@ -233,14 +250,18 @@ def __init__(self, gender, page=None): self._configure(width) - Gdk.Screen.get_default().connect('size-changed', self._configure_cb) + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + monitors.get_item(0).connect('notify::geometry', self._configure_cb) - def _configure_cb(self, event=None): - width = Gdk.Screen.width() + def _configure_cb(self, event=None, pspec=None): + width = _get_screen_width() self._configure(width) def _configure(self, width): - fixed_size = width - 4 * style.GRID_CELL_SIZE + fixed_size = max(1, width - 4 * style.GRID_CELL_SIZE) self._fixed.set_size_request(fixed_size, -1) num_ages = len(self._group_labels.AGES) @@ -251,10 +272,10 @@ def _configure(self, width): if num_ages + 2 < width / style.LARGE_ICON_SIZE: for i in range(num_ages): - self._pickers[i].show_all() - self._age_slider.hide() + self._pickers[i].set_visible(True) + self._age_slider.set_visible(False) else: - self._age_slider.show() + self._age_slider.set_visible(True) value = self._age_adj.get_value() self._set_age_picker(int(value + 0.5)) @@ -264,9 +285,9 @@ def get_label(self): 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() + self._pickers[i].set_visible(True) else: - self._pickers[i].hide_all() + self._pickers[i].set_visible(False) self._do_selected(age_index) def __age_adj_changed_cb(self, widget): diff --git a/src/jarabe/intro/colorpicker.py b/src/jarabe/intro/colorpicker.py index 14ae4385ba..6a9711384d 100644 --- a/src/jarabe/intro/colorpicker.py +++ b/src/jarabe/intro/colorpicker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,21 +21,25 @@ from sugar4.graphics.xocolor import XoColor -class ColorPicker(Gtk.EventBox): +class ColorPicker(Gtk.Box): def __init__(self): - Gtk.EventBox.__init__(self) + super().__init__() self._xo_color = None self._xo = Icon(pixel_size=style.XLARGE_ICON_SIZE, icon_name='computer-xo') self._set_random_colors() - self.connect('button-press-event', self._button_press_cb) - self.add(self._xo) - self._xo.show() - - def _button_press_cb(self, widget, event): - if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: + + click = Gtk.GestureClick() + click.connect('pressed', self._button_press_cb) + self.add_controller(click) + + self.append(self._xo) + self._xo.set_visible(True) + + def _button_press_cb(self, gesture, n_press, x, y): + if gesture.get_current_button() == 1: self._set_random_colors() def get_color(self): diff --git a/src/jarabe/intro/genderpicker.py b/src/jarabe/intro/genderpicker.py index e545c3ffd7..776a7c79a0 100644 --- a/src/jarabe/intro/genderpicker.py +++ b/src/jarabe/intro/genderpicker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014, Sugar Labs +# Copyright (C) 2014, Sugar Labs # Copyright (C) 2014, Walter Bender # # This program is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from gi.repository import Gio from gi.repository import GObject @@ -23,7 +22,6 @@ from sugar4.graphics import style from sugar4.graphics.xocolor import XoColor - GENDERS = ['female', 'male'] @@ -45,7 +43,7 @@ class GenderPicker(Gtk.Grid): gender_changed_signal = GObject.Signal('gender-changed', arg_types=([str])) def __init__(self): - Gtk.Grid.__init__(self) + super().__init__() self.set_row_spacing(style.DEFAULT_SPACING) self.set_column_spacing(style.DEFAULT_SPACING) @@ -60,7 +58,7 @@ def __init__(self): self._buttons[-1].connect('activate', self._button_activate_cb, i) self.attach(self._buttons[-1], i * 2, 0, 1, 1) - self._buttons[-1].show() + self._buttons[-1].set_visible(True) self.reset_button = EventIcon(pixel_size=style.SMALL_ICON_SIZE, icon_name='entry-cancel') @@ -68,7 +66,7 @@ def __init__(self): self._reset_button_activate_cb) self.attach(self.reset_button, 1, 0, 1, 1) self.reset_button.xo_color = XoColor('#010101,#a0a0a0') - self.reset_button.show() + self.reset_button.set_visible(True) def _reset_button_activate_cb(self, widget): self._set_gender('') diff --git a/src/jarabe/intro/window.py b/src/jarabe/intro/window.py index 246492c1a9..2dc6eb1e30 100644 --- a/src/jarabe/intro/window.py +++ b/src/jarabe/intro/window.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -80,7 +80,7 @@ def create_profile(user_profile): logging.debug("Generating user keypair") - cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % (keypath, ) + cmd = "ssh-keygen -q -t rsa -f %s -C '' -N ''" % (keypath, ) (s, o) = subprocess.getstatusoutput(cmd) if s != 0: logging.error('Could not generate key pair: %d %s', s, o) @@ -94,7 +94,7 @@ class _Page(Gtk.Box): } def __init__(self): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self.valid = False def set_valid(self, valid): @@ -115,26 +115,26 @@ def __init__(self, intro): _Page.__init__(self) self._intro = intro - alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0) - self.pack_start(alignment, expand=True, fill=True, padding=0) - grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) - alignment.add(grid) + grid.set_row_spacing(style.DEFAULT_SPACING) + grid.set_halign(Gtk.Align.CENTER) + grid.set_valign(Gtk.Align.CENTER) + grid.set_hexpand(True) + grid.set_vexpand(True) + self.append(grid) + grid.set_visible(True) label = Gtk.Label(label=_('Name:')) grid.attach(label, 0, 0, 1, 1) - label.show() + label.set_visible(True) self._entry = Gtk.Entry() self._entry.connect('notify::text', self._text_changed_cb) self._entry.set_size_request(style.zoom(300), -1) self._entry.set_max_length(45) grid.attach(self._entry, 0, 1, 1, 1) - self._entry.show() - - grid.show() - alignment.show() + self._entry.set_visible(True) def _text_changed_cb(self, entry, pspec): valid = len(entry.props.text.strip()) > 0 @@ -155,23 +155,23 @@ class _ColorPage(_Page): def __init__(self): _Page.__init__(self) - alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0) - self.pack_start(alignment, expand=True, fill=True, padding=0) - grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) - alignment.add(grid) + grid.set_row_spacing(style.DEFAULT_SPACING) + grid.set_halign(Gtk.Align.CENTER) + grid.set_valign(Gtk.Align.CENTER) + grid.set_hexpand(True) + grid.set_vexpand(True) + self.append(grid) + grid.set_visible(True) label = Gtk.Label(label=_('Click to change color:')) grid.attach(label, 0, 0, 1, 1) - label.show() + label.set_visible(True) self._cp = colorpicker.ColorPicker() grid.attach(self._cp, 0, 1, 1, 1) - self._cp.show() - - grid.show() - alignment.show() + self._cp.set_visible(True) self._color = self._cp.get_color() self.set_valid(True) @@ -185,23 +185,23 @@ class _GenderPage(_Page): def __init__(self): _Page.__init__(self) - alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0) - self.pack_start(alignment, expand=True, fill=True, padding=0) - grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) - alignment.add(grid) + grid.set_row_spacing(style.DEFAULT_SPACING) + grid.set_halign(Gtk.Align.CENTER) + grid.set_valign(Gtk.Align.CENTER) + grid.set_hexpand(True) + grid.set_vexpand(True) + self.append(grid) + grid.set_visible(True) label = Gtk.Label(label=_('Select gender:')) grid.attach(label, 0, 0, 1, 1) - label.show() + label.set_visible(True) self._gp = genderpicker.GenderPicker() grid.attach(self._gp, 0, 1, 1, 1) - self._gp.show() - - grid.show() - alignment.show() + self._gp.set_visible(True) self._gender = self._gp.get_gender() self.set_valid(True) @@ -218,24 +218,24 @@ class _AgePage(_Page): def __init__(self, gender): _Page.__init__(self) - alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0) - self.pack_start(alignment, expand=True, fill=True, padding=0) - grid = Gtk.Grid() grid.set_column_spacing(style.DEFAULT_SPACING) - alignment.add(grid) + grid.set_row_spacing(style.DEFAULT_SPACING) + grid.set_halign(Gtk.Align.CENTER) + grid.set_valign(Gtk.Align.CENTER) + grid.set_hexpand(True) + grid.set_vexpand(True) + self.append(grid) + grid.set_visible(True) self._ap = agepicker.AgePicker(gender, self) label = Gtk.Label(label=_(self._ap.get_label())) grid.attach(label, 0, 0, 1, 1) - label.show() + label.set_visible(True) grid.attach(self._ap, 0, 1, 1, 1) - self._ap.show() - - grid.show() - alignment.show() + self._ap.set_visible(True) self._age = self._ap.get_age() @@ -258,14 +258,27 @@ class _IntroBox(Gtk.Box): PAGE_LAST = len(PAGES) - 1 def __init__(self, start_on_age_page): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) - self.set_border_width(style.zoom(30)) + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + self.set_hexpand(True) + self.set_vexpand(True) + self.set_spacing(style.DEFAULT_SPACING) + + self.set_margin_start(style.zoom(30)) + self.set_margin_end(style.zoom(30)) + self.set_margin_top(style.zoom(30)) + self.set_margin_bottom(style.zoom(30)) self._page = 0 self._name_page = _NamePage(self) self._color_page = _ColorPage() self._gender_page = _GenderPage() self._age_page = _AgePage(None) + + for page in (self._name_page, self._color_page, + self._gender_page, self._age_page): + page.connect('notify::valid', self._page_valid_changed_cb) + self._current_page = None self._next_button = None @@ -297,8 +310,8 @@ def __init__(self, start_on_age_page): self._setup_page() def _setup_page(self): - for child in self.get_children(): - self.remove(child) + while self.get_first_child() is not None: + self.remove(self.get_first_child()) def _setup_name_page(self): self._current_page = self._name_page @@ -326,49 +339,64 @@ def _setup_age_page(self): ] setup_methods[self._page](self) - self.pack_start(self._current_page, True, True, 0) - self._current_page.show() + self._current_page.set_hexpand(True) + self._current_page.set_vexpand(True) + self.append(self._current_page) + self._current_page.set_visible(True) - button_box = Gtk.HButtonBox() + button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) if self._page == self.PAGE_FIRST: - button_box.set_layout(Gtk.ButtonBoxStyle.END) + button_box.set_halign(Gtk.Align.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) + button_box.set_halign(Gtk.Align.FILL) + back_button = Gtk.Button() + back_button.add_css_class('intro-nav-button') + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) + box.append(Icon(icon_name='go-left')) + box.append(Gtk.Label(label=_('Back'))) + back_button.set_child(box) back_button.connect('clicked', self._back_activated_cb) - button_box.pack_start(back_button, True, True, 0) - back_button.show() + back_button.set_hexpand(True) + back_button.set_halign(Gtk.Align.START) + back_button.set_margin_start(style.zoom(15)) + back_button.set_margin_bottom(style.zoom(15)) + button_box.append(back_button) + back_button.set_visible(True) self._next_button = Gtk.Button() - image = Icon(icon_name='go-right') - self._next_button.set_image(image) - + self._next_button.add_css_class('intro-nav-button') + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING) + self._next_label = Gtk.Label(label=_('Next')) + box.append(self._next_label) + box.append(Icon(icon_name='go-right')) + self._next_button.set_child(box) + if self._page == self.PAGE_LAST: - self._next_button.set_label(_('Done')) + self._next_label.set_text(_('Done')) self._next_button.connect('clicked', self._done_activated_cb) else: - self._next_button.set_label(_('Next')) + self._next_label.set_text(_('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._next_button.set_hexpand(True) + self._next_button.set_halign(Gtk.Align.END) + self._next_button.set_margin_end(style.zoom(15)) + self._next_button.set_margin_bottom(style.zoom(15)) + button_box.append(self._next_button) + self._next_button.set_visible(True) - self._current_page.connect('notify::valid', - self._page_valid_changed_cb) - - self.pack_start(button_box, False, True, 0) - button_box.show() + self.append(button_box) + button_box.set_visible(True) if not self.PAGES[self._page]: next(self) def _update_next_button(self): - self._next_button.set_sensitive(self._current_page.props.valid) + if self._next_button is not None: + self._next_button.set_sensitive(self._current_page.props.valid) def _page_valid_changed_cb(self, page, pspec): self._update_next_button() @@ -416,7 +444,7 @@ def __init__(self): self.age = 0 -class IntroWindow(Gtk.Window): +class IntroWindow(Gtk.Box): __gtype_name__ = 'SugarIntroWindow' __gsignals__ = { @@ -424,20 +452,24 @@ class IntroWindow(Gtk.Window): } def __init__(self, start_on_age_page=False): - Gtk.Window.__init__(self) + super().__init__() - self.props.decorated = False - self.maximize() + self.set_hexpand(True) + self.set_vexpand(True) + self.add_css_class('intro-window') self._intro_box = _IntroBox(start_on_age_page) self._intro_box.connect('done', self._done_cb) - self.add(self._intro_box) - self._intro_box.show() - self.connect('key-press-event', self.__key_press_cb) + self.append(self._intro_box) + self._intro_box.set_visible(True) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_cb) + self.add_controller(key_controller) def _done_cb(self, box, user_profile): - self.hide() + self.set_visible(False) GLib.idle_add(self._create_profile_cb, user_profile) def _create_profile_cb(self, user_profile): @@ -446,22 +478,23 @@ def _create_profile_cb(self, user_profile): return False - def __key_press_cb(self, widget, event): - if Gdk.keyval_name(event.keyval) == 'Return': + def __key_press_cb(self, controller, keyval, keycode, state): + if Gdk.keyval_name(keyval) == 'Return': next(self._intro_box) return True - if Gdk.keyval_name(event.keyval) == 'Escape': + if Gdk.keyval_name(keyval) == 'Escape': self._intro_box.back() return True return False - -if hasattr(IntroWindow, 'set_css_name'): - IntroWindow.set_css_name('introwindow') - +IntroWindow.set_css_name('introwindow') if __name__ == '__main__': + win = Gtk.Window() w = IntroWindow() - w.show() - w.connect('destroy', Gtk.main_quit) - Gtk.main() + win.set_child(w) + win.set_visible(True) + + loop = GLib.MainLoop() + win.connect('close-request', lambda w: loop.quit()) + loop.run() diff --git a/src/jarabe/journal/bundlelauncher.py b/src/jarabe/journal/bundlelauncher.py index dc94fcd9d3..ef1097ea87 100644 --- a/src/jarabe/journal/bundlelauncher.py +++ b/src/jarabe/journal/bundlelauncher.py @@ -45,7 +45,7 @@ def get_bundle(bundle_id=None, object_id=None): bundle = bundleregistry.get_registry().get_bundle(bundle_id) if bundle is None: logging.warning('Activity with the bundle_id %s was not found', - mime_type) + bundle_id) return None return bundle diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py index b83f5e78cc..70dba8bc98 100644 --- a/src/jarabe/journal/detailview.py +++ b/src/jarabe/journal/detailview.py @@ -18,6 +18,7 @@ from gi.repository import GObject from gi.repository import Gtk +from gi.repository import Gdk from sugar4.graphics import style from sugar4.graphics.icon import Icon @@ -26,11 +27,20 @@ from jarabe.journal import model +def _set_css_bg(widget, color): + if color == style.COLOR_PANEL_GREY: + widget.set_css_classes(['detailview-bg-panel']) + elif color == style.COLOR_SELECTION_GREY: + widget.set_css_classes(['detailview-bg-selection']) + else: + widget.set_css_classes([]) + + class DetailView(Gtk.Box): __gtype_name__ = 'DetailView' __gsignals__ = { - 'go-back-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'go-back-clicked': (GObject.SignalFlags.RUN_FIRST, None, ()), } def __init__(self, journalactivity, **kwargs): @@ -38,30 +48,30 @@ def __init__(self, journalactivity, **kwargs): self._metadata = None self._expanded_entry = None - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) back_bar = BackBar() - back_bar.connect('button-release-event', - self.__back_bar_release_event_cb) - self.pack_start(back_bar, False, True, 0) - - self.show_all() + click = Gtk.GestureClick() + click.connect('released', self.__back_bar_release_event_cb) + back_bar.add_controller(click) + self.append(back_bar) + back_bar.set_visible(True) def _fav_icon_activated_cb(self, fav_icon): keep = not self._expanded_entry.get_keep() self._expanded_entry.set_keep(keep) fav_icon.props.keep = keep - def __back_bar_release_event_cb(self, back_bar, event): + def __back_bar_release_event_cb(self, gesture, n_press, x, y): self.emit('go-back-clicked') - return False def _update_view(self): if self._expanded_entry is None: self._expanded_entry = ExpandedEntry(self._journalactivity) - self.pack_start(self._expanded_entry, True, True, 0) + self._expanded_entry.set_vexpand(True) + self.append(self._expanded_entry) self._expanded_entry.set_metadata(self._metadata) - self.show_all() + self._expanded_entry.set_visible(True) def refresh(self): logging.debug('DetailView.refresh') @@ -79,40 +89,36 @@ def set_metadata(self, metadata): type=object, getter=get_metadata, setter=set_metadata) -class BackBar(Gtk.EventBox): +class BackBar(Gtk.Box): def __init__(self): - Gtk.EventBox.__init__(self) - self.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_PANEL_GREY.get_gdk_color()) - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_PADDING) - hbox.set_border_width(style.DEFAULT_PADDING) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_PADDING) + _set_css_bg(self, style.COLOR_PANEL_GREY) + self.set_margin_start(style.DEFAULT_PADDING) + self.set_margin_end(style.DEFAULT_PADDING) + self.set_margin_top(style.DEFAULT_PADDING) + self.set_margin_bottom(style.DEFAULT_PADDING) + icon = Icon(icon_name='go-previous', pixel_size=style.SMALL_ICON_SIZE, fill_color=style.COLOR_TOOLBAR_GREY.get_svg()) - hbox.pack_start(icon, False, False, 0) + self.append(icon) + icon.set_visible(True) label = Gtk.Label() label.set_text(_('Back')) - halign = Gtk.Alignment.new(0, 0.5, 0, 1) - halign.add(label) - hbox.pack_start(halign, True, True, 0) - hbox.show() - self.add(hbox) - - if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL: - # Reverse hbox children. - for child in hbox.get_children(): - hbox.reorder_child(child, 0) - - self.connect('enter-notify-event', self.__enter_notify_event_cb) - self.connect('leave-notify-event', self.__leave_notify_event_cb) - - def __enter_notify_event_cb(self, box, event): - box.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - return False - - def __leave_notify_event_cb(self, box, event): - box.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_PANEL_GREY.get_gdk_color()) - return False + label.set_halign(Gtk.Align.START) + label.set_valign(Gtk.Align.CENTER) + label.set_hexpand(True) + self.append(label) + label.set_visible(True) + + motion = Gtk.EventControllerMotion() + motion.connect('enter', self.__enter_notify_event_cb) + motion.connect('leave', self.__leave_notify_event_cb) + self.add_controller(motion) + + def __enter_notify_event_cb(self, controller, x, y): + _set_css_bg(self, style.COLOR_SELECTION_GREY) + + def __leave_notify_event_cb(self, controller): + _set_css_bg(self, style.COLOR_PANEL_GREY) diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py index f14ee0ad17..dd667bf958 100644 --- a/src/jarabe/journal/expandedentry.py +++ b/src/jarabe/journal/expandedentry.py @@ -22,6 +22,7 @@ from gi.repository import GObject from gi.repository import GLib from gi.repository import Gtk +from gi.repository import Gdk import json from sugar4.graphics import style @@ -40,37 +41,43 @@ from jarabe.journal import journalwindow -class Separator(Gtk.Box): +def _set_css_bg(widget, color): + if color == style.COLOR_PANEL_GREY: + widget.set_css_classes(['expandedentry-bg-panel']) + elif color == style.COLOR_WHITE: + widget.set_css_classes(['expandedentry-bg-white']) + else: + widget.set_css_classes([]) + +class Separator(Gtk.Box): def __init__(self, orientation): - Gtk.Box.__init__( - self, orientation=Gtk.Orientation.VERTICAL, background_color=style.COLOR_PANEL_GREY.get_gdk_color()) + super().__init__(orientation=Gtk.Orientation.VERTICAL) + _set_css_bg(self, style.COLOR_PANEL_GREY) -class BuddyList(Gtk.Alignment): +class BuddyList(Gtk.Box): def __init__(self, buddies): - Gtk.Alignment.__init__(self) - self.set(0, 0, 0, 0) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) for buddy in buddies: nick_, color = buddy icon = CanvasIcon(icon_name='computer-xo', xo_color=XoColor(color), pixel_size=style.STANDARD_ICON_SIZE) icon.set_palette(BuddyPalette(buddy)) - hbox.pack_start(icon, True, True, 0) - self.add(hbox) + icon.set_hexpand(True) + self.append(icon) class TextView(Gtk.TextView): def __init__(self): - Gtk.TextView.__init__(self) + super().__init__() text_buffer = Gtk.TextBuffer() self.set_buffer(text_buffer) - self.set_left_margin(style.DEFAULT_PADDING) + self.set_margin_start(style.DEFAULT_PADDING) self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) @@ -92,7 +99,7 @@ class CommentsView(Gtk.TreeView): COMMENT_ERASE_ICON_COLOR = 5 def __init__(self): - Gtk.TreeView.__init__(self) + super().__init__() self.set_headers_visible(False) self._store = Gtk.ListStore(str, object, str, str, str, object) self._comments = [] @@ -158,16 +165,16 @@ def _erase_comment_cb(self, widget, event): icon = Icon(icon_name='dialog-cancel') alert.add_button(Gtk.ResponseType.CANCEL, _('Cancel'), icon) - icon.show() + icon.set_visible(True) ok_icon = Icon(icon_name='dialog-ok') alert.add_button(Gtk.ResponseType.OK, erase_string, ok_icon) - ok_icon.show() + ok_icon.set_visible(True) alert.connect('response', self._erase_alert_response_cb, entry) journalwindow.get_journal_window().add_alert(alert) - alert.show() + alert.set_visible(True) def _erase_alert_response_cb(self, alert, response_id, entry): journalwindow.get_journal_window().remove_alert(alert) @@ -202,10 +209,10 @@ def __init__(self): self.props.mode = Gtk.CellRendererMode.ACTIVATABLE -class BaseExpandedEntry(GObject.GObject): +class BaseExpandedEntry(object): - def __init__(self): - # Create a header + def __init__(self, **kwargs): + super().__init__(**kwargs) self._keep_icon = None self._keep_sid = None self._icon = None @@ -215,19 +222,21 @@ def __init__(self): def create_header(self): header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + header.set_spacing(style.DEFAULT_SPACING) self._keep_icon = self._create_keep_icon() - header.pack_start(self._keep_icon, False, False, style.DEFAULT_SPACING) + header.append(self._keep_icon) self._icon_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - header.pack_start(self._icon_box, False, False, style.DEFAULT_SPACING) + header.append(self._icon_box) self._title = self._create_title() - header.pack_start(self._title, True, True, 0) + self._title.set_hexpand(True) + header.append(self._title) # TODO: create a version list popup instead of a date label self._date = self._create_date() - header.pack_start(self._date, False, False, style.DEFAULT_SPACING) + header.append(self._date) return header @@ -244,77 +253,78 @@ def _create_date(self): return date -class ExpandedEntry(Gtk.EventBox, BaseExpandedEntry): +class ExpandedEntry(Gtk.Box, BaseExpandedEntry): - def __init__(self, journalactivity): + def __init__(self, journalactivity, **kwargs): + super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs) BaseExpandedEntry.__init__(self) self._journalactivity = journalactivity - Gtk.EventBox.__init__(self) - self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.add(self._vbox) + self._vbox = self self.in_focus = False self._metadata = None self._update_title_sid = None - self.modify_bg(Gtk.StateType.NORMAL, style.COLOR_WHITE.get_gdk_color()) + _set_css_bg(self, style.COLOR_WHITE) self._header = self.create_header() - self._vbox.pack_start(self._header, False, False, - style.DEFAULT_SPACING * 2) + self._header.set_margin_bottom(style.DEFAULT_SPACING * 2) + self._vbox.append(self._header) + self._keep_sid = self._keep_icon.connect( 'toggled', self._keep_icon_toggled_cb) self._title.connect('activate', self._title_entered) - self._title.connect( - 'focus-out-event', self._focus_out_cb) - self._title.connect( - 'focus-in-event', self._focus_in_cb) - - if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL: - # Reverse header children. - for child in self._header.get_children(): - self._header.reorder_child(child, 0) - - # Create a two-column body - body_box = Gtk.EventBox() - body_box.set_border_width(style.DEFAULT_SPACING) - body_box.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - self._vbox.pack_start(body_box, True, True, 0) - body = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - body_box.add(body) + + focus_controller = Gtk.EventControllerFocus() + focus_controller.connect('leave', self._focus_out_cb) + focus_controller.connect('enter', self._focus_in_cb) + self._title.add_controller(focus_controller) + + body_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + body_box.set_margin_start(style.DEFAULT_SPACING) + body_box.set_margin_end(style.DEFAULT_SPACING) + body_box.set_margin_top(style.DEFAULT_SPACING) + body_box.set_margin_bottom(style.DEFAULT_SPACING) + _set_css_bg(body_box, style.COLOR_WHITE) + body_box.set_vexpand(True) + self._vbox.append(body_box) first_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - body.pack_start(first_column, False, False, style.DEFAULT_SPACING) + first_column.set_margin_end(style.DEFAULT_SPACING) + body_box.append(first_column) second_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - body.pack_start(second_column, True, True, 0) + second_column.set_hexpand(True) + second_column.set_vexpand(True) + body_box.append(second_column) - # First body column self._preview_box = Gtk.Frame() - style_context = self._preview_box.get_style_context() - style_context.add_class('journal-preview-box') - first_column.pack_start(self._preview_box, False, True, 0) + self._preview_box.add_css_class('journal-preview-box') + self._preview_box.set_vexpand(True) + first_column.append(self._preview_box) self._technical_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - first_column.pack_start(self._technical_box, False, False, 0) + first_column.append(self._technical_box) # Second body column description_box, self._description = self._create_description() - second_column.pack_start(description_box, True, True, - style.DEFAULT_SPACING) + description_box.set_vexpand(True) + description_box.set_margin_bottom(style.DEFAULT_SPACING) + second_column.append(description_box) tags_box, self._tags = self._create_tags() - second_column.pack_start(tags_box, True, True, - style.DEFAULT_SPACING) + tags_box.set_vexpand(True) + tags_box.set_margin_bottom(style.DEFAULT_SPACING) + second_column.append(tags_box) comments_box, self._comments = self._create_comments() - second_column.pack_start(comments_box, True, True, - style.DEFAULT_SPACING) + comments_box.set_vexpand(True) + comments_box.set_margin_bottom(style.DEFAULT_SPACING) + second_column.append(comments_box) self._buddy_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - second_column.pack_start(self._buddy_list, True, False, 0) - self.show_all() + second_column.append(self._buddy_list) + self.set_visible(True) def set_metadata(self, metadata): if self._metadata == metadata: @@ -326,30 +336,29 @@ def set_metadata(self, metadata): self._keep_icon.handler_unblock(self._keep_sid) self._icon = self._create_icon() - for child in self._icon_box.get_children(): - self._icon_box.remove(child) - # FIXME: self._icon_box.foreach(self._icon_box.remove) - self._icon_box.pack_start(self._icon, False, False, 0) + while self._icon_box.get_first_child() is not None: + self._icon_box.remove(self._icon_box.get_first_child()) + self._icon_box.append(self._icon) self._date.set_text(misc.get_date(metadata)) self._title.set_text(metadata.get('title', _('Untitled'))) if self._preview_box.get_child(): - self._preview_box.remove(self._preview_box.get_child()) - self._preview_box.add(self._create_preview()) + self._preview_box.set_child(None) + self._preview_box.set_child(self._create_preview()) - for child in self._technical_box.get_children(): - self._technical_box.remove(child) - # FIXME: self._technical_box.foreach(self._technical_box.remove) - self._technical_box.pack_start(self._create_technical(), - False, False, style.DEFAULT_SPACING) + while self._technical_box.get_first_child() is not None: + self._technical_box.remove(self._technical_box.get_first_child()) + tb = self._create_technical() + tb.set_margin_bottom(style.DEFAULT_SPACING) + self._technical_box.append(tb) - for child in self._buddy_list.get_children(): - self._buddy_list.remove(child) - # FIXME: self._buddy_list.foreach(self._buddy_list.remove) - self._buddy_list.pack_start(self._create_buddy_list(), False, False, - style.DEFAULT_SPACING) + while self._buddy_list.get_first_child() is not None: + self._buddy_list.remove(self._buddy_list.get_first_child()) + bl = self._create_buddy_list() + bl.set_margin_bottom(style.DEFAULT_SPACING) + self._buddy_list.append(bl) description = metadata.get('description', '') self._description.get_buffer().set_text(description) @@ -375,33 +384,34 @@ def _create_icon(self): def _create_preview(self): - box = Gtk.EventBox() - box.modify_bg(Gtk.StateType.NORMAL, style.COLOR_WHITE.get_gdk_color()) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + _set_css_bg(box, style.COLOR_WHITE) metadata = self._metadata pixbuf = get_preview_pixbuf(metadata.get('preview', '')) has_preview = pixbuf is not None if has_preview: - im = Gtk.Image() - im.set_from_pixbuf(pixbuf) - box.add(im) - im.show() + im = Gtk.Image.new_from_paintable(Gdk.Texture.new_for_pixbuf(pixbuf)) + box.append(im) + im.set_visible(True) else: label = Gtk.Label() label.set_text(_('No preview')) width, height = PREVIEW_SIZE[0], PREVIEW_SIZE[1] label.set_size_request(width, height) - box.add(label) - label.show() + box.append(label) + label.set_visible(True) - box.connect_after('button-release-event', - self._preview_box_button_release_event_cb) + click = Gtk.GestureClick() + click.set_propagation_phase(Gtk.PropagationPhase.BUBBLE) + click.connect('released', self._preview_box_button_release_event_cb) + box.add_controller(click) return box def _create_technical(self): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.props.spacing = style.DEFAULT_SPACING + vbox.set_spacing(style.DEFAULT_SPACING) if 'filesize' in self._metadata: filesize = self._metadata['filesize'] @@ -416,12 +426,12 @@ def _create_technical(self): for line in lines: linebox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - vbox.pack_start(linebox, False, False, 0) + vbox.append(linebox) text = Gtk.Label() text.set_markup('%s' % ( style.COLOR_BUTTON_GREY.get_html(), line)) - linebox.pack_start(text, False, False, 0) + linebox.append(text) return vbox @@ -440,74 +450,78 @@ def _format_date(self): def _create_buddy_list(self): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.props.spacing = style.DEFAULT_SPACING + vbox.set_spacing(style.DEFAULT_SPACING) text = Gtk.Label() text.set_markup('%s' % ( style.COLOR_BUTTON_GREY.get_html(), _('Participants:'))) - halign = Gtk.Alignment.new(0, 0, 0, 0) - halign.add(text) - vbox.pack_start(halign, False, False, 0) + text.set_halign(Gtk.Align.START) + vbox.append(text) if self._metadata.get('buddies'): buddies = list(json.loads(self._metadata['buddies']).values()) - vbox.pack_start(BuddyList(buddies), False, False, 0) + vbox.append(BuddyList(buddies)) return vbox return vbox def _create_scrollable(self, widget, label=None): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.props.spacing = style.DEFAULT_SPACING + vbox.set_spacing(style.DEFAULT_SPACING) if label is not None: text = Gtk.Label() text.set_markup('%s' % ( style.COLOR_BUTTON_GREY.get_html(), label)) - halign = Gtk.Alignment.new(0, 0, 0, 0) - halign.add(text) - vbox.pack_start(halign, False, False, 0) + text.set_halign(Gtk.Align.START) + vbox.append(text) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN) - scrolled_window.add(widget) - vbox.pack_start(scrolled_window, True, True, 0) + scrolled_window.set_has_frame(True) + scrolled_window.set_child(widget) + scrolled_window.set_vexpand(True) + scrolled_window.set_hexpand(True) + vbox.append(scrolled_window) return vbox def _create_description(self): widget = TextView() - widget.connect('focus-in-event', self._focus_in_cb) - widget.connect('focus-out-event', - self._description_tags_focus_out_event_cb) + focus = Gtk.EventControllerFocus() + focus.connect('enter', self._focus_in_cb) + focus.connect('leave', self._description_tags_focus_out_event_cb) + widget.add_controller(focus) return self._create_scrollable(widget, label=_('Description:')), widget def _create_tags(self): widget = TextView() - widget.connect('focus-in-event', self._focus_in_cb) - widget.connect('focus-out-event', - self._description_tags_focus_out_event_cb) + focus = Gtk.EventControllerFocus() + focus.connect('enter', self._focus_in_cb) + focus.connect('leave', self._description_tags_focus_out_event_cb) + widget.add_controller(focus) return self._create_scrollable(widget, label=_('Tags:')), widget def _create_comments(self): widget = CommentsView() widget.connect('comments-changed', self._comments_changed_cb) - widget.connect('focus-in-event', self._focus_in_cb) - widget.connect('focus-out-event', self._focus_out_cb) + focus = Gtk.EventControllerFocus() + focus.connect('enter', self._focus_in_cb) + focus.connect('leave', self._focus_out_cb) + widget.add_controller(focus) return self._create_scrollable(widget, label=_('Comments:')), widget - def _focus_in_cb(self, widget, event): + def _focus_in_cb(self, controller): self.in_focus = True - def _focus_out_cb(self, widget, event): + def _focus_out_cb(self, controller): self.in_focus = False def _title_entered(self, widget): self._update_entry() - self._title.hide() - self._title.show() + self._title.set_visible(False) + self._title.set_visible(True) def _title_notify_text_cb(self, entry, pspec): if not self._update_title_sid: @@ -530,7 +544,7 @@ def _title_changed_event_cb(self, widget): self._metadata.get('title_set_by_user', 0) ) journalwindow.get_journal_window().add_alert(alert) - alert.show() + alert.set_visible(True) self._update_entry() @@ -544,7 +558,7 @@ def _title_alert_response_cb(self, alert, response_id, old_title, old_title_set_ self._metadata['title_set_by_user'] = old_title_set_by_user self._update_entry(needs_update=True) - def _description_tags_focus_out_event_cb(self, text_view, event): + def _description_tags_focus_out_event_cb(self, controller): self._update_entry() def _comments_changed_cb(self, event, comments): @@ -564,7 +578,6 @@ def _update_entry(self, needs_update=False): self._metadata['title_set_by_user'] = '1' needs_update = True - bounds = self._tags.get_buffer().get_bounds() old_tags = self._metadata.get('tags', None) new_tags = self._tags.get_buffer().get_text(bounds[0], bounds[1], @@ -610,7 +623,7 @@ def __icon_activate_cb(self, button): alert_window=journalwindow.get_journal_window()) return True - def _preview_box_button_release_event_cb(self, button, event): + def _preview_box_button_release_event_cb(self, gesture, n_press, x, y): logging.debug('_preview_box_button_release_event_cb') misc.resume(self._metadata, alert_window=journalwindow.get_journal_window()) diff --git a/src/jarabe/journal/iconmodel.py b/src/jarabe/journal/iconmodel.py index 713b89c09d..25bb8b53b0 100644 --- a/src/jarabe/journal/iconmodel.py +++ b/src/jarabe/journal/iconmodel.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Gonzalo Odiard +# Copyright (C) 2013, Gonzalo Odiard # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,7 @@ DS_DBUS_PATH = '/org/laptop/sugar/DataStore' -class IconModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource): +class IconModel(GObject.GObject, Gtk.TreeModel): __gtype_name__ = 'JournalIconModel' __gsignals__ = { @@ -38,15 +38,15 @@ class IconModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource): COLUMN_PREVIEW = 2 _COLUMN_TYPES = { - COLUMN_UID: str, - COLUMN_TITLE: str, - COLUMN_PREVIEW: str, + COLUMN_UID: GObject.TYPE_STRING, + COLUMN_TITLE: GObject.TYPE_STRING, + COLUMN_PREVIEW: GObject.TYPE_STRING, } _PAGE_SIZE = 100 def __init__(self, query): - GObject.GObject.__init__(self) + super().__init__() self._last_requested_index = None self._cached_row = None @@ -88,7 +88,7 @@ def do_iter_n_children(self, iterator): def do_get_value(self, iterator, column): if self.view_is_resizing: - return None + return "" index = iterator.user_data if index == self._last_requested_index: diff --git a/src/jarabe/journal/iconview.py b/src/jarabe/journal/iconview.py index b5b83d7bf8..2c330988b9 100644 --- a/src/jarabe/journal/iconview.py +++ b/src/jarabe/journal/iconview.py @@ -20,6 +20,7 @@ from gi.repository import GObject from gi.repository import Gtk from gi.repository import GLib +from gi.repository import Gdk from jarabe.journal.iconmodel import IconModel from sugar4.graphics.icon import Icon @@ -29,69 +30,102 @@ from sugar4.activity.activity import PREVIEW_SIZE -class PreviewRenderer(Gtk.CellRendererPixbuf): +def _set_css_bg(widget, color): + widget.set_css_classes( + ['iconview-bg-white'] if color == style.COLOR_WHITE else []) - def __init__(self, **kwds): - Gtk.CellRendererPixbuf.__init__(self, **kwds) - self._preview_data = None - def set_preview_data(self, data): - self._preview_data = data +class PreviewFlowBox(Gtk.FlowBox): + __gtype_name__ = 'PreviewFlowBox' + + __gsignals__ = { + 'item-activated': (GObject.SignalFlags.RUN_FIRST, None, (object,)), + } - def do_render(self, cr, widget, background_area, cell_area, flags): - self.props.pixbuf = get_preview_pixbuf(self._preview_data) - Gtk.CellRendererPixbuf.do_render(self, cr, widget, background_area, - cell_area, flags) + def __init__(self, title_col, preview_col): + super().__init__() - def do_get_size(self, widget, cell_area): - x_offset, y_offset, width, height = Gtk.CellRendererPixbuf.do_get_size( - self, widget, cell_area) - width = PREVIEW_SIZE[0] - height = PREVIEW_SIZE[1] - return (x_offset, y_offset, width, height) + self._preview_col = preview_col + self._title_col = title_col + self.set_selection_mode(Gtk.SelectionMode.SINGLE) + self.set_valign(Gtk.Align.START) + self.set_max_children_per_line(10) + self.set_column_spacing(6) + self.set_row_spacing(6) + self.set_homogeneous(True) + + self.connect('child-activated', self._on_child_activated) -class PreviewIconView(Gtk.IconView): + def get_model(self): + return getattr(self, '_model', None) - def __init__(self, title_col, preview_col): - Gtk.IconView.__init__(self) + def set_model(self, model): + self._model = model + while self.get_first_child(): + self.remove(self.get_first_child()) - self._preview_col = preview_col - self._title_col = title_col + if model is None: + return - self.set_spacing(3) + iter_ = model.get_iter_first() + while iter_ is not None: + self._add_item(model, iter_) + iter_ = model.iter_next(iter_) - _preview_renderer = PreviewRenderer() - _preview_renderer.set_alignment(0.5, 0.5) - self.pack_start(_preview_renderer, False) - self.set_cell_data_func(_preview_renderer, - self._preview_data_func, None) + def _add_item(self, model, iter_): + uid = model.get_value(iter_, IconModel.COLUMN_UID) + title = model.get_value(iter_, self._title_col) + preview_data = model.get_value(iter_, self._preview_col) - _title_renderer = Gtk.CellRendererText() - _title_renderer.set_alignment(0.5, 0.5) - self.pack_start(_title_renderer, True) - self.set_cell_data_func(_title_renderer, - self._title_data_func, None) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.set_spacing(3) - def _preview_data_func(self, view, cell, store, i, data): - preview_data = store.get_value(i, self._preview_col) - cell.set_preview_data(preview_data) + pixbuf = get_preview_pixbuf(preview_data) + if pixbuf: + picture = Gtk.Picture.new_for_paintable(Gdk.Texture.new_for_pixbuf(pixbuf)) + else: + picture = Gtk.Picture() + + picture.set_size_request(PREVIEW_SIZE[0], PREVIEW_SIZE[1]) + picture.set_valign(Gtk.Align.CENTER) + picture.set_halign(Gtk.Align.CENTER) + box.append(picture) + + label = Gtk.Label() + label.set_markup(title) + label.set_halign(Gtk.Align.CENTER) + box.append(label) - def _title_data_func(self, view, cell, store, i, data): - title = store.get_value(i, self._title_col) - cell.props.markup = title + path = model.get_path(iter_) + child = Gtk.FlowBoxChild() + child.set_child(box) + child._model_path = path -class IconView(Gtk.Bin): + self.append(child) + + def _on_child_activated(self, flowbox, child): + self.emit('item-activated', child._model_path) + + def get_path_at_pos(self, x, y): + child = self.get_child_at_pos(int(x), int(y)) + if child: + return child._model_path + return None + + +class IconView(Gtk.Box): __gtype_name__ = 'JournalBaseIconView' __gsignals__ = { - 'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ()), 'entry-activated': (GObject.SignalFlags.RUN_FIRST, - None, ([str])), + None, (str,)), } def __init__(self, toolbar): + super().__init__(orientation=Gtk.Orientation.VERTICAL) self._query = {} self._model = None self._progress_bar = None @@ -99,27 +133,22 @@ def __init__(self, toolbar): self._scroll_position = 0. self._toolbar = toolbar - Gtk.Bin.__init__(self) - self.connect('map', self.__map_cb) self.connect('unrealize', self.__unrealize_cb) - self.connect('destroy', self.__destroy_cb) self._scrolled_window = Gtk.ScrolledWindow() self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.add(self._scrolled_window) - self._scrolled_window.show() + self._scrolled_window.set_vexpand(True) + self.append(self._scrolled_window) + self._scrolled_window.set_visible(True) - self.icon_view = PreviewIconView(IconModel.COLUMN_TITLE, + self.icon_view = PreviewFlowBox(IconModel.COLUMN_TITLE, IconModel.COLUMN_PREVIEW) self.icon_view.connect('item-activated', self.__item_activated_cb) - self.icon_view.connect('button-release-event', - self.__button_release_event_cb) - - self._scrolled_window.add(self.icon_view) - self.icon_view.show() + self._scrolled_window.set_child(self.icon_view) + self.icon_view.set_visible(True) # Auto-update stuff self._fully_obscured = True @@ -130,22 +159,12 @@ def __init__(self, toolbar): model.updated.connect(self.__model_updated_cb) model.deleted.connect(self.__model_deleted_cb) - def __button_release_event_cb(self, icon_view, event): - path = icon_view.get_path_at_pos(int(event.x), int(event.y)) - if path is None: - return False - uid = icon_view.get_model()[path][IconModel.COLUMN_UID] - self.emit('entry-activated', uid) - return False + def __item_activated_cb(self, icon_view, path): - uid = icon_view.get_model()[path][IconModel.COLUMN_UID] + uid = self._model[path][IconModel.COLUMN_UID] self.emit('entry-activated', uid) - def _thumb_data_func(self, view, cell, store, i, data): - preview_data = store.get_value(i, IconModel.COLUMN_PREVIEW) - cell.props.pixbuf = get_preview_pixbuf(preview_data) - def __model_created_cb(self, sender, signal, object_id): if self._is_new_item_visible(object_id): self._set_dirty() @@ -164,11 +183,7 @@ def _is_new_item_visible(self, object_id): return not object_id.startswith('/') return object_id.startswith(self._query['mountpoints'][0]) - def do_size_allocate(self, allocation): - self.set_allocation(allocation) - self.get_child().size_allocate(allocation) - - def __destroy_cb(self, widget): + def do_unroot(self): if self._model is not None: self._model.stop() @@ -193,15 +208,15 @@ def refresh(self): def __model_ready_cb(self, tree_model): self._stop_progress_bar() - self._scroll_position = self.icon_view.props.vadjustment.props.value + self._scroll_position = self._scrolled_window.get_vadjustment().props.value logging.debug('IconView.__model_ready_cb %r', self._scroll_position) # Cannot set it up earlier because will try to access the model # and it needs to be ready. self.icon_view.set_model(self._model) - self.icon_view.props.vadjustment.props.value = self._scroll_position - self.icon_view.props.vadjustment.value_changed() + self._scrolled_window.get_vadjustment().props.value = self._scroll_position + # vadjustment.value_changed is gone or automatic, no need to call it if len(tree_model) == 0: documents_path = model.get_documents_path() @@ -222,11 +237,10 @@ def __model_ready_cb(self, tree_model): def __map_cb(self, widget): logging.debug('IconView.__map_cb %r', self._scroll_position) - self.icon_view.props.vadjustment.props.value = self._scroll_position - self.icon_view.props.vadjustment.value_changed() + self._scrolled_window.get_vadjustment().props.value = self._scroll_position def __unrealize_cb(self, widget): - self._scroll_position = self.icon_view.props.vadjustment.props.value + self._scroll_position = self._scrolled_window.get_vadjustment().props.value logging.debug('IconView.__map_cb %r', self._scroll_position) def _is_query_empty(self): @@ -245,74 +259,103 @@ def __model_progress_cb(self, tree_model): self._last_progress_bar_pulse = time.time() def _start_progress_bar(self): - alignment = Gtk.Alignment.new(xalign=0.5, yalign=0.5, - xscale=0.5, yscale=0) - self.remove(self.get_child()) - self.add(alignment) - alignment.show() + alignment = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.CENTER) + + while self.get_first_child(): + self.remove(self.get_first_child()) + + self.append(alignment) + alignment.set_visible(True) self._progress_bar = Gtk.ProgressBar() self._progress_bar.props.pulse_step = 0.01 self._last_progress_bar_pulse = time.time() - alignment.add(self._progress_bar) - self._progress_bar.show() + alignment.append(self._progress_bar) + self._progress_bar.set_visible(True) def _stop_progress_bar(self): if self._progress_bar is None: return - self.remove(self.get_child()) - self.add(self._scrolled_window) + while self.get_first_child(): + self.remove(self.get_first_child()) + self.append(self._scrolled_window) self._progress_bar = None def _show_message(self, message, show_clear_query=False): - self.remove(self.get_child()) - - background_box = Gtk.EventBox() - background_box.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - self.add(background_box) - - alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1) - background_box.add(alignment) + while self.get_first_child(): + self.remove(self.get_first_child()) + + background_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + background_box.set_hexpand(True) + background_box.set_vexpand(True) + _set_css_bg(background_box, style.COLOR_WHITE) + self.append(background_box) + + alignment = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.CENTER) + alignment.set_hexpand(True) + alignment.set_vexpand(True) + background_box.append(alignment) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - alignment.add(box) + alignment.append(box) icon = Icon(pixel_size=style.LARGE_ICON_SIZE, icon_name='activity-journal', stroke_color=style.COLOR_BUTTON_GREY.get_svg(), fill_color=style.COLOR_TRANSPARENT.get_svg()) - box.pack_start(icon, expand=True, fill=False, padding=0) + icon.set_hexpand(True) + icon.set_vexpand(True) + box.append(icon) label = Gtk.Label() color = style.COLOR_BUTTON_GREY.get_html() label.set_markup('%s' % ( color, GLib.markup_escape_text(message))) - box.pack_start(label, expand=True, fill=False, padding=0) + label.set_hexpand(True) + label.set_vexpand(True) + box.append(label) if show_clear_query: - button_box = Gtk.HButtonBox() - button_box.set_layout(Gtk.ButtonBoxStyle.CENTER) - box.pack_start(button_box, False, True, 0) - button_box.show() + button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + button_box.set_halign(Gtk.Align.CENTER) + button_box.set_margin_top(style.DEFAULT_SPACING) + box.append(button_box) + button_box.set_visible(True) button = Gtk.Button(label=_('Clear search')) button.connect('clicked', self.__clear_button_clicked_cb) - button.props.image = Icon(icon_name='dialog-cancel', - pixel_size=style.SMALL_ICON_SIZE) - button_box.pack_start(button, expand=True, fill=False, padding=0) - - background_box.show_all() + + icon_img = Icon(icon_name='dialog-cancel', + pixel_size=style.SMALL_ICON_SIZE) + + box_img = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + box_img.append(icon_img) + lbl = Gtk.Label(label=_('Clear search')) + box_img.append(lbl) + button.set_child(box_img) + + button_box.append(button) + + background_box.set_visible(True) + alignment.set_visible(True) + box.set_visible(True) + icon.set_visible(True) + label.set_visible(True) def __clear_button_clicked_cb(self, button): self.emit('clear-clicked') def _clear_message(self): - if self.get_child() == self._scrolled_window: + if self.get_first_child() == self._scrolled_window: return - self.remove(self.get_child()) - self.add(self._scrolled_window) - self._scrolled_window.show() + while self.get_first_child(): + self.remove(self.get_first_child()) + self.append(self._scrolled_window) + self._scrolled_window.set_visible(True) def _set_dirty(self): if self._fully_obscured: diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 5801e38b77..fb2f23cf60 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -22,11 +22,11 @@ import os from dbus import service - from gi.repository import Gtk from gi.repository import Gdk from gi.repository import Gio from gi.repository import GObject +from gi.repository import GLib from sugar4.graphics.alert import ErrorAlert from sugar4 import env @@ -135,7 +135,7 @@ def _chooser_response_cb(self, chooser, response_id, chooser_id): self.ObjectChooserResponse(chooser_id, object_id) else: self.ObjectChooserCancelled(chooser_id) - chooser.destroy() + chooser.close() del chooser @service.method(J_DBUS_INTERFACE, in_signature='is', @@ -145,15 +145,10 @@ def ChooseObject(self, parent_id, what_filter=''): This method is kept for backwards compatibility """ chooser_id = uuid.uuid4().hex - if parent_id: - attributes = Gdk.WindowAttr() - attributes.window_type = Gdk.WindowType.FOREIGN - parent = Gdk.Window.new(None, attributes, None) - else: - parent = None + parent = None chooser = ObjectChooser(parent, what_filter) chooser.connect('response', self._chooser_response_cb, chooser_id) - chooser.show() + chooser.present() return chooser_id @@ -162,15 +157,10 @@ def ChooseObject(self, parent_id, what_filter=''): def ChooseObjectWithFilter(self, parent_id, what_filter='', filter_type=None, show_preview=False): chooser_id = uuid.uuid4().hex - if parent_id: - attributes = Gdk.WindowAttr() - attributes.window_type = Gdk.WindowType.FOREIGN - parent = Gdk.Window.new(None, attributes, None) - else: - parent = None + parent = None chooser = ObjectChooser(parent, what_filter, filter_type, show_preview) chooser.connect('response', self._chooser_response_cb, chooser_id) - chooser.show() + chooser.present() return chooser_id @@ -194,8 +184,12 @@ class JournalActivity(JournalWindow): def __init__(self): logging.debug('STARTUP: Loading the journal') JournalWindow.__init__(self) + self.add_css_class('journal-window') + self.add_css_class('background') - self.set_title(_('Journal')) + activity_id = activityfactory.create_activity_id() + self.activity_id = str(activity_id) + self.bundle_id = _BUNDLE_ID self._main_view = None self._project_view = None @@ -203,60 +197,64 @@ def __init__(self): self._list_view = None self._detail_view = None self._main_toolbox = None + self._edit_toolbox = None self._detail_toolbox = None self._volumes_toolbar = None + self._key_controller = None + self._toolbox = None self._mount_point = '/' self._active_view = JournalViews.MAIN self.project_metadata = None self._editing_mode = False + self._init_done = False + self._critical_space_alert = None + + self._dbus_service = JournalActivityDBusService(self) + + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) + session.get_session_manager().shutdown_signal.connect( + self._session_manager_shutdown_cb) + + def _setup(self): self._setup_main_view() self._setup_secondary_view() self._setup_project_view() - self.add_events(Gdk.EventMask.ALL_EVENTS_MASK) self._realized_sid = self.connect('realize', self.__realize_cb) - self.connect('window-state-event', self.__window_state_event_cb) - self.connect('key-press-event', self.__key_press_event_cb) - self.connect('focus-in-event', self._focus_in_event_cb) - self.connect('focus-out-event', self._focus_out_event_cb) - model.created.connect(self.__model_created_cb) - model.updated.connect(self.__model_updated_cb) - model.deleted.connect(self.__model_deleted_cb) + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) + self._key_controller = key_controller - self._dbus_service = JournalActivityDBusService(self) + focus_controller = Gtk.EventControllerFocus() + focus_controller.connect('enter', self._focus_in_event_cb) + focus_controller.connect('leave', self._focus_out_event_cb) + self.add_controller(focus_controller) - self.iconify() - - self._critical_space_alert = None + self._init_done = True + self.show_main_view() + self.minimize() self._check_available_space() - session.get_session_manager().shutdown_signal.connect( - self._session_manager_shutdown_cb) - def volume_error_cb(self, gobject, message, severity): alert = ErrorAlert(title=severity, msg=message) alert.connect('response', self.__alert_response_cb) self.add_alert(alert) - alert.show() + alert.set_visible(True) def __alert_response_cb(self, alert, response_id): self.remove_alert(alert) def __realize_cb(self, window): - activity_id = activityfactory.create_activity_id() - data = GObject.GObject() - settattr(data, 'activity_id', str(activity_id)) - settattr(data, 'bundle_id', _BUNDLE_ID) - gdk_window = window.get_window() - gdk_window.set_user_data(data) - self.disconnect(self._realized_sid) self._realized_sid = None def _session_manager_shutdown_cb(self, event): - self.destroy() + self.close() def can_close(self): return False @@ -269,6 +267,8 @@ def list_view_signal_connect(self, list_view): self.__title_edit_started_cb) list_view.connect('title-edit-finished', self.__title_edit_finished_cb) + list_view.connect('title-edit-canceled', + self.__title_edit_canceled_cb) list_view.connect('selection-changed', self.__selection_changed_cb) list_view.connect('project-view-activate', @@ -288,24 +288,25 @@ def _setup_main_view(self): self._add_new_box = AddNewBar(_('Add new project')) self._add_new_box.activate.connect(self.__add_project_activate_cb) - self._main_view.pack_start(self._add_new_box, False, True, - style.DEFAULT_SPACING) - self._main_view.set_can_focus(True) + self._add_new_box.set_margin_top(style.DEFAULT_SPACING) + self._add_new_box.set_margin_bottom(style.DEFAULT_SPACING) + self._main_view.append(self._add_new_box) + self._main_view.set_focusable(True) self._list_view = ListView(self, enable_multi_operations=True) self.list_view_signal_connect(self._list_view) tree_view = self._list_view.tree_view tree_view.connect('choose-project', self.__choose_project_cb) - self._main_view.pack_start(self._list_view, True, True, 0) - self._list_view.show_all() + self._list_view.set_vexpand(True) + self._main_view.append(self._list_view) + self._list_view.set_visible(True) volumes_toolbar = self._create_volumes_toolbar() - self._main_view.pack_start(volumes_toolbar, False, True, 0) + self._main_view.append(volumes_toolbar) self._main_toolbox.connect('query-changed', self._query_changed_cb) - self._main_toolbox.search_entry.connect('icon-press', - self.__search_icon_pressed_cb) + self._main_toolbox.set_mount_point(self._mount_point) def _setup_project_view(self): @@ -314,15 +315,17 @@ def _setup_project_view(self): add_new_box = AddNewBar() add_new_box.activate.connect(self.__add_new_activate_cb) - add_new_box.show_all() - project_vbox.pack_start(add_new_box, False, True, - style.DEFAULT_SPACING / 3) + add_new_box.set_visible(True) + add_new_box.set_margin_top(style.DEFAULT_SPACING // 3) + add_new_box.set_margin_bottom(style.DEFAULT_SPACING // 3) + project_vbox.append(add_new_box) self._entry_project = add_new_box.get_entry() self._list_view_project = self._project_view.create_list_view_project() self.list_view_signal_connect(self._list_view_project) - project_vbox.pack_start(self._list_view_project, True, True, 0) - self._list_view_project.show() + self._list_view_project.set_vexpand(True) + project_vbox.append(self._list_view_project) + self._list_view_project.set_visible(True) def get_add_new_box(self): return self._add_new_box @@ -331,6 +334,8 @@ def get_list_view(self): return self._list_view def project_view_activated_cb(self, list_view, metadata): + if not self._init_done: + return self.project_metadata = metadata self._project_view.set_project_metadata(self.project_metadata) @@ -340,12 +345,12 @@ def project_view_activated_cb(self, list_view, metadata): self.set_canvas(self._project_view) self._toolbox = self._main_toolbox self.set_toolbar_box(self._toolbox) - self._toolbox.show() + self._toolbox.set_visible(True) query = {} query['project_id'] = self.project_metadata['uid'] self._list_view_project.update_with_query(query) - self._project_view.show_all() + self._project_view.set_visible(True) def _setup_secondary_view(self): self._secondary_view = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) @@ -355,8 +360,9 @@ def _setup_secondary_view(self): self._detail_view = DetailView(self) self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb) - self._secondary_view.pack_end(self._detail_view, True, True, 0) - self._detail_view.show() + self._detail_view.set_vexpand(True) + self._secondary_view.append(self._detail_view) + self._detail_view.set_visible(True) def __add_project_activate_cb(self, bar, title): initialize_journal_object( @@ -366,29 +372,30 @@ def __add_project_activate_cb(self, bar, title): def __add_new_activate_cb(self, bar, title): shell_model = shell.get_model() activity = shell_model.get_active_activity() - if activity.has_shell_window(): + if activity and activity.has_shell_window(): return if shell.get_model().has_modal(): return chooser = ActivityChooser() - activity.push_shell_window(chooser) - chooser.connect('hide', activity.pop_shell_window) + if activity: + activity.push_shell_window(chooser) + chooser.connect('unmap', activity.pop_shell_window) text = _("Choose an activity to start '%s' with") % title chooser.set_title(text) chooser.connect('activity-selected', self.__activity_selected_cb, title) - chooser.show_all() + chooser.set_visible(True) def __activity_selected_cb(self, widget, bundle_id, activity_id, title): initialize_journal_object( title=title, bundle_id=bundle_id, activity_id=activity_id, project_metadata=self.project_metadata) - def __key_press_event_cb(self, widget, event): + def __key_press_event_cb(self, controller, keyval, keycode, state): ''' Entries can be scrolled with 'Up'/'Down' arrow keys. Search can be started by pressing 'Ctrl' + 'F' keys @@ -397,38 +404,45 @@ def __key_press_event_cb(self, widget, event): Pressing 'Return' in Detail View starts the activity ''' - keyname = Gdk.keyval_name(event.keyval) + keyname = Gdk.keyval_name(keyval) if self._active_view == JournalViews.MAIN: if keyname == 'Up' or keyname == 'Down': if not self._list_view.tree_view.has_focus(): self._list_view.tree_view.grab_focus() + return True elif keyname == 'Escape': self._main_toolbox.clear_query() if not self._list_view.tree_view.has_focus(): self._list_view.tree_view.grab_focus() + return True - elif event.state & Gdk.ModifierType.CONTROL_MASK and keyname == 'f': + elif state & Gdk.ModifierType.CONTROL_MASK and keyname == 'f': self._main_toolbox.search_entry.grab_focus() + return True elif self._active_view == JournalViews.DETAIL: if keyname == 'Left': path, col = self._list_view.tree_view.get_cursor() self._list_view.tree_view.grab_focus() column = self._list_view.tree_view.get_column(5) - self._list_view.tree_view.set_cursor_on_cell(path, column, None, True) + if path and column: + self._list_view.tree_view.set_cursor_on_cell(path, column, None, True) self._detail_view.emit('go-back-clicked') + return True if keyname == 'Return': metadata = self._detail_toolbox.get_metadata() misc.resume(metadata, alert_window=journalwindow.get_journal_window()) + return True + return False def __choose_project_cb(self, tree_view, metadata_to_send): - project_chooser = ObjectChooser(self.get_window()) - project_chooser.show_all() + project_chooser = ObjectChooser(None) + project_chooser.present() project_chooser.connect('response', self.__project_chooser_response_cb, metadata_to_send) project_chooser._toolbar._proj_list_button_clicked_cb(None) @@ -436,7 +450,7 @@ def __choose_project_cb(self, tree_view, metadata_to_send): def __project_chooser_response_cb(self, project_chooser, response_value, metadata_to_send): if response_value == Gtk.ResponseType.DELETE_EVENT: - project_chooser.destroy() + project_chooser.close() return object_id = project_chooser.get_selected_object_id() @@ -445,7 +459,7 @@ def __project_chooser_response_cb(self, project_chooser, response_value, datastore.delete(metadata_to_send['uid']) jobject_to_send.metadata['project_id'] = metadata['uid'] datastore.write(jobject_to_send) - project_chooser.destroy() + project_chooser.close() def __detail_clicked_cb(self, list_view, object_id): metadata = model.get(object_id) @@ -473,26 +487,34 @@ def __go_back_clicked_cb(self, detail_view): self.show_main_view() def _query_changed_cb(self, toolbar, query): + if not self._init_done: + return self._list_view.update_with_query(query) self.show_main_view() self._main_toolbox.search_entry.grab_focus() - Gtk.Entry.do_move_cursor(self._main_toolbox.search_entry, - Gtk.MovementStep.BUFFER_ENDS, 1, False) + # setting cursor position directly + self._main_toolbox.search_entry.set_position(-1) self._add_new_box.props.visible = \ query.get('activity') == PROJECT_BUNDLE_ID - def __search_icon_pressed_cb(self, entry, icon_pos, event): + def __search_icon_pressed_cb(self, entry, icon_pos, event=None): self._main_view.grab_focus() def __title_edit_started_cb(self, list_view): - self.disconnect_by_func(self.__key_press_event_cb) + GLib.idle_add(self.remove_controller, self._key_controller) def __title_edit_finished_cb(self, list_view, new_text, path): + self.add_controller(self._key_controller) list_view_model = list_view.get_model() iterator = list_view_model.get_iter(path) list_view_model[iterator][ListModel.COLUMN_TITLE] = new_text + def __title_edit_canceled_cb(self, list_view): + self.add_controller(self._key_controller) + def show_main_view(self): + if not self._init_done: + return self._active_view = JournalViews.MAIN self.project_metadata = None if self._editing_mode: @@ -503,13 +525,15 @@ def show_main_view(self): self._toolbox = self._main_toolbox self.set_toolbar_box(self._toolbox) - self._toolbox.show() + self._toolbox.set_visible(True) if self.canvas != self._main_view: self.set_canvas(self._main_view) - self._main_view.show() + self._main_view.set_visible(True) def _show_secondary_view(self, object_id): + if not self._init_done: + return self._active_view = JournalViews.DETAIL metadata = model.get(object_id) try: @@ -518,7 +542,7 @@ def _show_secondary_view(self, object_id): logging.exception('Exception while displaying entry:') self.set_toolbar_box(self._detail_toolbox) - self._detail_toolbox.show() + self._detail_toolbox.set_visible(True) try: self._detail_view.props.metadata = metadata @@ -526,7 +550,7 @@ def _show_secondary_view(self, object_id): logging.exception('Exception while displaying entry:') self.set_canvas(self._secondary_view) - self._secondary_view.show() + self._secondary_view.set_visible(True) def show_object(self, object_id): metadata = model.get(object_id) @@ -543,11 +567,15 @@ def __volume_changed_cb(self, volume_toolbar, mount_point): self._edit_toolbox.batch_copy_button.update_mount_point() def __model_created_cb(self, sender, **kwargs): + if not self._init_done: + return misc.handle_bundle_installation(model.get(kwargs['object_id'])) self._main_toolbox.refresh_filters() self._check_available_space() def __model_updated_cb(self, sender, **kwargs): + if not self._init_done: + return misc.handle_bundle_installation(model.get(kwargs['object_id'])) if self.canvas == self._secondary_view and \ @@ -557,24 +585,21 @@ def __model_updated_cb(self, sender, **kwargs): self._check_available_space() def __model_deleted_cb(self, sender, **kwargs): + if not self._init_done: + return if self.canvas == self._secondary_view and \ kwargs['object_id'] == self._detail_view.props.metadata['uid']: self.show_main_view() - def _focus_in_event_cb(self, window, event): + def _focus_in_event_cb(self, controller): self._set_is_visible(True) - def _focus_out_event_cb(self, window, event): + def _focus_out_event_cb(self, controller): self._set_is_visible(False) - def __window_state_event_cb(self, window, event): - logging.debug('window_state_event_cb %r', self) - if event.changed_mask & Gdk.WindowState.ICONIFIED: - state = event.new_window_state - visible = not state & Gdk.WindowState.ICONIFIED - self._set_is_visible(visible) - def _set_is_visible(self, visible): + if not self._init_done: + return if self._active_view == JournalViews.MAIN: self._list_view.set_is_visible(visible) elif self._active_view == JournalViews.PROJECT: @@ -593,22 +618,28 @@ def _check_available_space(self): free_space = stat[0] * stat[4] if free_space < (_SPACE_THRESHOLD * 1024 * 1024): self._critical_space_alert = ModalAlert() - self._critical_space_alert.connect('destroy', + self._critical_space_alert.connect('close-request', self.__alert_closed_cb) - self._critical_space_alert.show() + self._critical_space_alert.set_visible(True) def __alert_closed_cb(self, data): self.show_main_view() self.reveal() self._critical_space_alert = None + return False def set_active_volume(self, mount): self._volumes_toolbar.set_active_volume(mount) def show_journal(self): - """Become visible and show main view""" + """Become visible, show main view, and focus the search entry.""" self.reveal() self.show_main_view() + # Focus the search entry so the user can type immediately. + if self._init_done and self._main_toolbox is not None: + GLib.idle_add(self._main_toolbox.search_entry.grab_focus) + else: + self.grab_focus() def get_total_number_of_entries(self): list_view_model = self.get_list_view().get_model() @@ -650,7 +681,28 @@ def get_journal(): global _journal if _journal is None: _journal = JournalActivity() - _journal.show() + + shell.get_model().stack.add_named(_journal, "journal") + + _journal._setup() + + # register with the shell model so the frame (F6) can track it + from jarabe.model.shell import Activity + from jarabe.model.bundleregistry import get_registry + from jarabe.model.buddy import get_owner_instance + + registry = get_registry() + journal_info = registry.get_bundle('org.laptop.JournalActivity') + + home_activity = Activity( + activity_info=journal_info, + activity_id=_journal.activity_id, + color=get_owner_instance().props.color, + window=_journal + ) + shell.get_model()._add_activity(home_activity) + shell.get_model().emit('launch-completed', home_activity) + return _journal diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py index c39d4671d3..9498a98151 100644 --- a/src/jarabe/journal/journalentrybundle.py +++ b/src/jarabe/journal/journalentrybundle.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2007, One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 4f256a5ff4..471d7b2fbc 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -53,7 +53,6 @@ from jarabe.journal import journalwindow from jarabe.webservice import accountsmanager - _AUTOSEARCH_TIMEOUT = 1000 _ACTION_ANYTIME = 0 @@ -80,7 +79,7 @@ class MainToolbox(ToolbarBox): arg_types=([object])) def __init__(self, default_what_filter=None, default_filter_type=None): - ToolbarBox.__init__(self) + super().__init__() self._mount_point = None self._filter_type = default_filter_type self._what_filter = default_what_filter @@ -89,69 +88,58 @@ def __init__(self, default_what_filter=None, default_filter_type=None): self._default_what_filter = default_what_filter self._default_filter_type = default_filter_type - self.search_entry = iconentry.IconEntry() - self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, - 'entry-search') + self.search_entry = Gtk.SearchEntry() text = _('Search in %s') % _('Journal') self.search_entry.set_placeholder_text(text) self.search_entry.connect('activate', self._search_entry_activated_cb) - self.search_entry.connect('changed', self._search_entry_changed_cb) - self.search_entry.add_clear_button() + self.search_entry.connect('search-changed', self._search_entry_changed_cb) + self._last_toolbar_width = 0 self._autosearch_timer = None self._add_widget(self.search_entry, expand=True) - self._favorite_button = ToggleToolButton('emblem-favorite') + self._favorite_button = ToggleToolButton(icon_name='emblem-favorite') self._favorite_button.set_tooltip(_('Favorite entries')) self._favorite_button.connect('toggled', self.__favorite_button_toggled_cb) self.toolbar.append(self._favorite_button) - self._favorite_button.show() + self._favorite_button.set_visible(True) - self._proj_list_button = ToggleToolButton('project-box') + self._proj_list_button = ToggleToolButton(icon_name='project-box') self._proj_list_button.set_tooltip(_('Projects')) self._proj_list_button.connect('toggled', self._proj_list_button_clicked_cb) self.toolbar.append(self._proj_list_button) - self._proj_list_button.show() + self._proj_list_button.set_visible(True) if not self._proj_list_button.props.active: self._what_widget_contents = None self._what_search_button = FilterToolItem( 'view-type', _('Anything')) - self._what_widget.show() + # self._what_widget is created in FilterToolItem self.toolbar.append(self._what_search_button) - self._what_search_button.show() + self._what_search_button.set_visible(True) self._when_search_button = FilterToolItem( 'view-created', _('Anytime'), self._get_when_search_items()) self.toolbar.append(self._when_search_button) - self._when_search_button.show() + self._when_search_button.set_visible(True) self._sorting_button = SortingButton() self.toolbar.append(self._sorting_button) self._sorting_button.connect('sort-property-changed', self.__sort_changed_cb) - self._sorting_button.show() - - ''' - # TODO: enable it when the DS supports saving the buddies. - self._with_widget = Gtk.ToolItem() - self._with_search_button = FilterToolItem( - 'view-who', _('Anyone'), self._with_widget) - self._with_widget.show() - self.toolbar.append(self._with_search_button) - self._with_search_button.show() - self._get_with_search_items() - ''' + self._sorting_button.set_visible(True) self._query = self._build_query() self.refresh_filters() - self.connect('size-allocate', self.__size_allocate_cb) - - def __size_allocate_cb(self, widget, allocation): - GLib.idle_add(self._update_buttons, allocation.width) + def do_size_allocate(self, width, height, baseline): + super().do_size_allocate(width, height, baseline) + + if width != self._last_toolbar_width: + self._last_toolbar_width = width + GLib.idle_add(self._update_buttons, width) def _update_buttons(self, toolbar_width): # Show the label next to the button icon if there is room on @@ -160,11 +148,15 @@ def _update_buttons(self, toolbar_width): if not important: self.search_entry.set_size_request( - toolbar_width - style.GRID_CELL_SIZE * 7, 0) + toolbar_width - style.GRID_CELL_SIZE * 7, -1) + else: + self.search_entry.set_size_request( + toolbar_width - style.GRID_CELL_SIZE * 11, -1) - self._what_search_button.set_is_important(important) - self._when_search_button.set_is_important(important) - # self._with_search_button.set_is_important(important) + if hasattr(self._what_search_button, 'set_is_important'): + self._what_search_button.set_is_important(important) + if hasattr(self._when_search_button, 'set_is_important'): + self._when_search_button.set_is_important(important) return False @@ -192,43 +184,15 @@ def _get_when_search_items(self): return set_palette_list(when_list) - ''' - def _get_with_search_items(self): - with_list = [] - with_list.append({'label':_('Anyone'), - 'callback': self._with_palette_cb, - 'id': _ACTION_EVERYBODY}) - with_list.append({'separator': True}) - with_list.append({'label':_('My friends'), - 'callback': self._with_palette_cb, - 'id': _ACTION_MY_FRIENDS}) - with_list.append({'label':_('My class'), - 'callback': self._with_palette_cb, - 'id': _ACTION_MY_CLASS}) - with_list.append({'separator': True}) - # TODO: Ask the model for buddies. - for i, buddy in enumerate(model.get_buddies()): - nick, color = buddy - with_list.append({'label': nick, - 'callback': self._with_palette_cb, - 'icon': 'computer-xo', - 'xocolors': XOColor(color), - 'id': i + _ACTION_MY_CLASS + 1}) - - widget = set_palette_list(with_list) - self._with_widget.add(widget) - widget.show() - ''' - def _add_widget(self, widget, expand=False): tool_item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) tool_item.set_hexpand(expand) tool_item.append(widget) - widget.show() + widget.set_visible(True) - self.toolbar.append(tool_item, -1) - tool_item.show() + self.toolbar.append(tool_item) + tool_item.set_visible(True) def _build_query(self): query = {} @@ -279,7 +243,7 @@ def _build_query(self): property_, order = self._sorting_button.get_current_sort() - if order == Gtk.SortType.ASCENDING: + if order == "ASC": sign = '+' else: sign = '-' @@ -321,7 +285,7 @@ def _search_entry_activated_cb(self, search_entry): def _search_entry_changed_cb(self, search_entry): if not search_entry.props.text: - search_entry.activate() + search_entry.emit('activate') return if self._autosearch_timer: @@ -332,7 +296,7 @@ def _search_entry_changed_cb(self, search_entry): def _autosearch_timer_cb(self): logging.debug('_autosearch_timer_cb') self._autosearch_timer = None - self.search_entry.activate() + self.search_entry.emit('activate') return False def set_mount_point(self, mount_point): @@ -460,19 +424,15 @@ def refresh_filters(self): for item in sorted(what_list_activities, key=lambda x: x['label']): self._what_list.append(item) - if self._what_widget_contents is not None: - self._what_widget.remove(self._what_widget_contents) self._what_widget_contents = set_palette_list(self._what_list) - self._what_widget.add(self._what_widget_contents) - self._what_widget_contents.show() + self._what_search_button.props.palette.set_content(self._what_widget_contents) + self._what_widget_contents.set_visible(True) def _proj_list_button_clicked_cb(self, proj_list_button): if self._proj_list_button.props.active: - self._what_widget.hide() - self._what_search_button.hide() + self._what_search_button.set_visible(False) else: - self._what_widget.show() - self._what_search_button.show() + self._what_search_button.set_visible(True) self._update_if_needed() def __favorite_button_toggled_cb(self, favorite_button): @@ -497,17 +457,10 @@ def clear_query(self): self._when_search_button.set_widget_label(_('Anytime')) self._when_filter = None - ''' - self._with_search_button.set_widget_icon(icon_name='view-who') - self._with_search_button.set_widget_label(_('Anyone')) - self._with_filter = None - ''' - self._favorite_button.props.active = False if self._proj_list_button.props.active: - self._what_widget.show() - self._what_search_button.show() + self._what_search_button.set_visible(True) self._proj_list_button.props.active = False self._update_if_needed() @@ -520,7 +473,7 @@ class DetailToolbox(ToolbarBox): } def __init__(self, journalactivity): - ToolbarBox.__init__(self) + super().__init__() self._journalactivity = journalactivity self._metadata = None self._temp_file_path = None @@ -529,18 +482,18 @@ def __init__(self, journalactivity): self._resume = ToolButton('activity-start') self._resume.connect('clicked', self._resume_clicked_cb) self.toolbar.append(self._resume) - self._resume.show() + self._resume.set_visible(True) self._resume_menu = None color = profile.get_color() self._copy = ToolButton() icon = Icon(icon_name='edit-copy', xo_color=color) self._copy.set_icon_widget(icon) - icon.show() + icon.set_visible(True) self._copy.set_tooltip(_('Copy to')) self._copy.connect('clicked', self._copy_clicked_cb) self.toolbar.append(self._copy) - self._copy.show() + self._copy.set_visible(True) self._duplicate = ToolButton() icon = Icon(icon_name='edit-duplicate', xo_color=color) @@ -554,17 +507,17 @@ def __init__(self, journalactivity): self._refresh.set_tooltip(_('Refresh')) self._refresh.connect('clicked', self._refresh_clicked_cb) self.toolbar.append(self._refresh) - self._refresh.show() + self._refresh.set_visible(True) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) self.toolbar.append(separator) - separator.show() + separator.set_visible(True) erase_button = ToolButton('list-remove') erase_button.set_tooltip(_('Erase')) erase_button.connect('clicked', self._erase_button_clicked_cb) - self.toolbar.append(erase_button, -1) - erase_button.show() + self.toolbar.append(erase_button) + erase_button.set_visible(True) def set_metadata(self, metadata): self._metadata = metadata @@ -607,13 +560,13 @@ def _erase_button_clicked_cb(self, button): % self._metadata['title'] icon = Icon(icon_name='dialog-cancel') alert.add_button(Gtk.ResponseType.CANCEL, _('Cancel'), icon) - icon.show() + icon.set_visible(True) ok_icon = Icon(icon_name='dialog-ok') alert.add_button(Gtk.ResponseType.OK, erase_string, ok_icon) - ok_icon.show() + ok_icon.set_visible(True) alert.connect('response', self.__erase_alert_response_cb) journalwindow.get_journal_window().add_alert(alert) - alert.show() + alert.set_visible(True) def __erase_alert_response_cb(self, alert, response_id): journalwindow.get_journal_window().remove_alert(alert) @@ -632,9 +585,9 @@ def _refresh_copy_palette(self): palette = self._copy.get_palette() # Use the menu defined in CopyMenu - for menu_item in palette.menu.get_children(): - palette.menu.remove(menu_item) - menu_item.destroy() + while palette.menu.get_first_child(): + child = palette.menu.get_first_child() + child.unparent() CopyMenuBuilder(self._journalactivity, self.__get_uid_list_cb, self.__volume_error_cb, palette.menu) @@ -646,12 +599,12 @@ def _refresh_duplicate_palette(self): color = misc.get_icon_color(self._metadata) self._copy.get_icon_widget().props.xo_color = color if self._metadata['mountpoint'] == '/': - self._duplicate.show() + self._duplicate.set_visible(True) icon = self._duplicate.get_icon_widget() icon.props.xo_color = color - icon.show() + icon.set_visible(True) else: - self._duplicate.hide() + self._duplicate.set_visible(False) def _refresh_refresh_palette(self): if self._refresh is None: @@ -661,8 +614,8 @@ def _refresh_refresh_palette(self): self._refresh.get_icon_widget().props.xo_color = color palette = self._refresh.get_palette() - for menu_item in palette.menu.get_children(): - palette.menu.remove(menu_item) + while palette.menu.get_first_child(): + palette.menu.get_first_child().unparent() for account in accountsmanager.get_configured_accounts(): if hasattr(account, 'get_shared_journal_entry'): @@ -686,19 +639,19 @@ def _refresh_resume_palette(self): palette = self._resume.get_palette() if self._resume_menu is not None: - self._resume_menu.destroy() + pass self._resume_menu = PaletteMenuBox() palette.set_content(self._resume_menu) - self._resume_menu.show() + self._resume_menu.set_visible(True) for activity_info in misc.get_activities(self._metadata): menu_item = PaletteMenuItem(file_name=activity_info.get_icon(), text_label=activity_info.get_name()) - menu_item.connect('activate', self._resume_menu_item_activate_cb, + menu_item.connect('clicked', self._resume_menu_item_activate_cb, activity_info.get_bundle_id()) self._resume_menu.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) if not misc.can_resume(self._metadata): self._resume.set_tooltip(_('No activity to start entry')) @@ -714,10 +667,10 @@ class SortingButton(ToolButton): } def __init__(self): - ToolButton.__init__(self) + super().__init__() self._property = 'timestamp' - self._order = Gtk.SortType.ASCENDING + self._order = "ASC" self.props.tooltip = _('Sort view') self.props.icon_name = 'view-lastedit' @@ -727,7 +680,7 @@ def __init__(self): menu_box = PaletteMenuBox() self.props.palette.set_content(menu_box) - menu_box.show() + menu_box.set_visible(True) sort_options = [ ('timestamp', 'view-lastedit', _('Sort by date modified')), @@ -739,23 +692,23 @@ def __init__(self): button = PaletteMenuItem(label) button_icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name=icon) - button.set_image(button_icon) - button_icon.show() - button.connect('activate', + button.set_icon_widget(button_icon) + button_icon.set_visible(True) + button.connect('clicked', self.__sort_type_changed_cb, property_, icon) - button.show() + button.set_visible(True) menu_box.append_item(button) def __sort_type_changed_cb(self, widget, property_, icon_name): if self._property == property_: - if self._order == Gtk.SortType.ASCENDING: - self._order = Gtk.SortType.DESCENDING + if self._order == "ASC": + self._order = "DESC" else: - self._order = Gtk.SortType.ASCENDING + self._order = "ASC" else: - self._order = Gtk.SortType.ASCENDING + self._order = "ASC" self._property = property_ self.emit('sort-property-changed') @@ -769,24 +722,21 @@ def get_current_sort(self): class EditToolbox(ToolbarBox): def __init__(self, journalactivity): - ToolbarBox.__init__(self) + super().__init__() self._journalactivity = journalactivity - self.toolbar.add(SelectNoneButton(journalactivity)) - self.toolbar.add(SelectAllButton(journalactivity)) + self.toolbar.append(SelectNoneButton(journalactivity)) + self.toolbar.append(SelectAllButton(journalactivity)) - self.toolbar.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) + self.toolbar.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) self.batch_copy_button = BatchCopyButton(journalactivity) - self.toolbar.add(self.batch_copy_button) - self.toolbar.add(BatchEraseButton(journalactivity)) + self.toolbar.append(self.batch_copy_button) + self.toolbar.append(BatchEraseButton(journalactivity)) - self.toolbar.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) + self.toolbar.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) self._multi_select_info_widget = MultiSelectEntriesInfoWidget() - self.toolbar.add(self._multi_select_info_widget) - - self.show_all() - self.toolbar.show_all() + self.toolbar.append(self._multi_select_info_widget) def display_selected_entries_status(self): info_widget = self._multi_select_info_widget @@ -802,7 +752,7 @@ def set_selected_entries(self, selected): class SelectNoneButton(ToolButton): def __init__(self, journalactivity): - ToolButton.__init__(self, 'select-none') + super().__init__('select-none') self.props.tooltip = _('Deselect all') self._journalactivity = journalactivity @@ -815,7 +765,7 @@ def __do_deselect_all(self, widget_clicked): class SelectAllButton(ToolButton): def __init__(self, journalactivity): - ToolButton.__init__(self, 'select-all') + super().__init__('select-all') self.props.tooltip = _('Select all') self._journalactivity = journalactivity @@ -829,7 +779,7 @@ class BatchEraseButton(ToolButton): def __init__(self, journalactivity): self._journalactivity = journalactivity - ToolButton.__init__(self, 'edit-delete') + super().__init__('edit-delete') self.connect('clicked', self.__button_cliecked_cb) self.props.tooltip = _('Erase') @@ -855,7 +805,7 @@ class BatchCopyButton(ToolButton): def __init__(self, journalactivity): self._journalactivity = journalactivity - ToolButton.__init__(self, 'edit-copy') + super().__init__('edit-copy') self.props.tooltip = _('Copy') self.connect('clicked', self.__clicked_cb) self._menu_builder = None @@ -885,17 +835,17 @@ def __get_uid_list_cb(self): class MultiSelectEntriesInfoWidget(Gtk.Box): def __init__(self): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self._selected_entries = 0 self._total = 0 self._label = Gtk.Label() self._label.set_vexpand(True) - self._label.set_valign(True) + self._label.set_valign(Gtk.Align.CENTER) self.append(self._label) - self.show() + self.set_visible(True) def set_total_number_of_entries(self, total): self._total = total @@ -908,105 +858,80 @@ def display_selected_entries(self): message = _('Selected %(selected)d of %(total)d') % { 'selected': self._selected_entries, 'total': self._total} self._label.set_text(message) - self._label.show() + self._label.set_visible(True) -class FilterToolItem(Gtk.Box): +class FilterToolItem(ToolButton): __gsignals__ = { 'changed': (GObject.SignalFlags.RUN_LAST, None, ([])), } def __init__(self, default_icon, default_label, palette_content=None): - self._palette_invoker = ToolInvoker() - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + super().__init__(icon_name=default_icon) self._label = default_label self.set_size_request(style.GRID_CELL_SIZE, -1) - self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.append(box) - self.set_widget_icon(icon_name=default_icon) - - self._label_widget = Gtk.Label() - self._label_widget.set_alignment(0.0, 0.5) - self._label_widget.set_ellipsize(style.ELLIPSIZE_MODE_DEFAULT) - self._label_widget.set_max_width_chars(_LABEL_MAX_WIDTH) - self._label_widget.set_use_markup(True) - self._label_widget.set_markup(default_label) - self.box.append(self._label_widget) - self._label_widget.show() - - self._hide_tooltip_on_click = True - self._palette_invoker.attach_tool(self) - self._palette_invoker.props.toggle_palette = True - self._palette_invoker.props.lock_palette = True + self.get_palette_invoker().props.toggle_palette = True + self.get_palette_invoker().props.lock_palette = True - self.palette = Palette(_('Select filter')) - self.palette.set_invoker(self._palette_invoker) + if not self.get_palette(): + self.set_palette(Palette(_('Select filter'))) + else: + self.props.palette.props.primary_text = _('Select filter') + if palette_content: - self.props.palette.set_content(palette_content) + if isinstance(palette_content, list): + menu_box = PaletteMenuBox() + for item in palette_content: + if 'icon_name' in item: + menu_item = PaletteMenuItem(item['label'], + item['icon_name']) + else: + menu_item = PaletteMenuItem(text_label=item['label']) + if 'callback' in item: + def _click_cb(btn, cb=item['callback'], i=item, widget=menu_item): + cb(widget, None, i) + menu_item.connect('clicked', _click_cb) + menu_box.append_item(menu_item) + menu_item.set_visible(True) + self.props.palette.set_content(menu_box) + menu_box.set_visible(True) + else: + self.props.palette.set_content(palette_content) - self.append(self.palette) + self.set_widget_icon(icon_name=default_icon) + self.set_is_important(False) def set_widget_icon(self, icon_name=None, file_name=None): + from sugar4.graphics.xocolor import XoColor + xo_color = XoColor('white') if file_name is not None: - icon = Icon(file=file_name, + icon = Icon(file_name=file_name, pixel_size=style.SMALL_ICON_SIZE, - xo_color=XoColor('white')) + xo_color=xo_color) else: icon = Icon(icon_name=icon_name, pixel_size=style.SMALL_ICON_SIZE, - xo_color=XoColor('white')) - self.box.prepend(icon) - icon.show() + xo_color=xo_color) + self.set_icon_widget(icon) + icon.set_visible(True) def set_widget_label(self, label=None): - # FIXME: Ellipsis is not working on these labels. if label is None: label = self._label if len(label) > _LABEL_MAX_WIDTH: label = label[0:7] + '...' + label[-7:] - self._label_widget.set_markup(label) self._label = label + if self._important: + self.set_label(self._label) + else: + self.set_label(None) - def create_palette(self): - return None - - def get_palette(self): - return self._palette_invoker.palette - - def set_palette(self, palette): - self._palette_invoker.palette = palette - - palette = GObject.Property( - type=object, setter=set_palette, getter=get_palette) - - def get_palette_invoker(self): - return self._palette_invoker - - def set_palette_invoker(self, palette_invoker): - self._palette_invoker.detach() - self._palette_invoker = palette_invoker - - palette_invoker = GObject.Property( - type=object, setter=set_palette_invoker, getter=get_palette_invoker) - - def do_snapshot(self, snapshot): - if self.palette and self.palette.is_up(): - _, bounds = self.compute_bounds(self) - black = Gdk.RGBA() - snapshot.append_color(black, bounds) - snapshot.save() - - Gtk.Box.do_snapshot(self, snapshot) - - if self.palette and self.palette.is_up(): - invoker = self.palette.props.invoker - invoker.draw_rectangle(snapshot, self.palette) - - return False - + def set_is_important(self, important): + self._important = important + self.set_widget_label(None) if hasattr(FilterToolItem, 'set_css_name'): FilterToolItem.set_css_name('filtertoolbutton') @@ -1018,15 +943,22 @@ def set_palette_list(palette_list): text_label=palette_list[0]['label']) else: _menu_item = PaletteMenuItem(text_label=palette_list[0]['label']) - req2 = _menu_item.get_preferred_size()[1] - menuitem_width = req2.width - menuitem_height = req2.height - - palette_width = Gdk.Screen.width() - style.GRID_CELL_SIZE - palette_height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 3 + + menuitem_width = style.GRID_CELL_SIZE * 3 + menuitem_height = style.GRID_CELL_SIZE + + display = Gdk.Display.get_default() + monitor = display.get_monitors().get_item(0) if display else None + if monitor: + geom = monitor.get_geometry() + palette_width = max(800, geom.width) - style.GRID_CELL_SIZE + palette_height = max(600, geom.height) - style.GRID_CELL_SIZE * 3 + else: + palette_width = 800 - style.GRID_CELL_SIZE + palette_height = 600 - style.GRID_CELL_SIZE * 3 - nx = min(_MAXIMUM_PALETTE_COLUMNS, int(palette_width / menuitem_width)) - ny = min(int(palette_height / menuitem_height), len(palette_list) + 1) + nx = max(1, min(_MAXIMUM_PALETTE_COLUMNS, int(palette_width / menuitem_width))) + ny = max(1, min(int(palette_height / menuitem_height), len(palette_list) + 1)) if ny >= len(palette_list): nx = 1 ny = len(palette_list) @@ -1034,8 +966,7 @@ def set_palette_list(palette_list): grid = Gtk.Grid() grid.set_row_spacing(style.DEFAULT_PADDING) grid.set_column_spacing(0) - grid.set_border_width(0) - grid.show() + grid.set_visible(True) x = 0 y = 0 @@ -1053,8 +984,7 @@ def set_palette_list(palette_list): text_label=item['label'], xo_color=xo_color) else: - menu_item = PaletteMenuItem() - menu_item.set_label(item['label']) + menu_item = PaletteMenuItem(text_label=item['label']) menu_item.set_size_request(style.GRID_CELL_SIZE * 3, -1) @@ -1064,14 +994,17 @@ def set_palette_list(palette_list): x = 0 y += 1 else: - menu_item.connect('button-release-event', item['callback'], item) + def _click_cb(btn, cb=item['callback'], i=item, widget=menu_item): + cb(widget, None, i) + menu_item.connect('clicked', _click_cb) + grid.attach(menu_item, x, y, 1, 1) x += 1 if x == nx: x = 0 y += 1 - menu_item.show() + menu_item.set_visible(True) if palette_height < (y * menuitem_height + style.GRID_CELL_SIZE): # if the grid is bigger than the palette, put in a scrolledwindow @@ -1080,7 +1013,7 @@ def set_palette_list(palette_list): Gtk.PolicyType.AUTOMATIC) scrolled_window.set_size_request(nx * menuitem_width, (ny + 1) * menuitem_height) - scrolled_window.add_with_viewport(grid) + scrolled_window.set_child(grid) return scrolled_window return grid @@ -1090,24 +1023,33 @@ class AddNewBar(Gtk.Box): activate = GObject.Signal('activate', arg_types=[str]) def __init__(self, placeholder=None): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) self._button = EventIcon(icon_name='list-add') - self._button.connect('button-release-event', - self.__button_release_event_cb) + + click = Gtk.GestureClick() + click.connect('released', self.__button_release_event_cb) + self._button.add_controller(click) + 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) + self._button.set_tooltip_text(_('Add New')) + self.append(self._button) + self._button.set_visible(True) + + self._entry = Gtk.Entry() + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_cb) + self._entry.add_controller(key_controller) + 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() + self._entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'edit-clear-symbolic') + self._entry.connect('icon-press', lambda entry, pos: entry.set_text('') if pos == Gtk.EntryIconPosition.SECONDARY else None) + self._entry.set_hexpand(True) + self.append(self._entry) + self._entry.set_visible(True) def get_entry(self): return self._entry @@ -1115,11 +1057,13 @@ def get_entry(self): 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 __key_press_cb(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Return: + self._maybe_activate() + return True + return False - def __button_release_event_cb(self, button, event): + def __button_release_event_cb(self, gesture, n_press, x, y): self._maybe_activate() def _maybe_activate(self): diff --git a/src/jarabe/journal/journalwindow.py b/src/jarabe/journal/journalwindow.py index e73012497e..3bd6095eec 100644 --- a/src/jarabe/journal/journalwindow.py +++ b/src/jarabe/journal/journalwindow.py @@ -14,25 +14,131 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from sugar4.graphics.window import Window +import logging +_logger = logging.getLogger('JournalWindow') + +from gi.repository import Gtk, Gdk from gettext import gettext as _ _journal_window = None +_css = b''' +.journal-window { + background-color: #FFFFFF; +} +''' +_provider = Gtk.CssProvider() +_provider.load_from_data(_css) -class JournalWindow(Window): - def __init__(self): +class JournalWindow(Gtk.Box): + def __init__(self): global _journal_window - Window.__init__(self) + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + + display = Gdk.Display.get_default() + if display: + Gtk.StyleContext.add_provider_for_display( + display, _provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + self.add_css_class('journal-window') _journal_window = self - self.set_icon_name('activity-journal') - self.set_title(_('Journal')) - # Stop the user from closing the journal window. - self.connect('delete-event', lambda widget, event: True) + self._toolbar_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self._alert_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self._canvas_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + self._canvas_area.set_vexpand(True) + self._canvas_area.set_hexpand(True) + + self.append(self._toolbar_area) + self.append(self._alert_area) + self.append(self._canvas_area) + + self._canvas = None + self._toolbar_box = None + self._presenting = False + + @property + def canvas(self): + return self._canvas + + def set_canvas(self, widget): + if self._canvas == widget: + return + if self._canvas is not None and self._canvas.get_parent() == self._canvas_area: + self._canvas_area.remove(self._canvas) + self._canvas = widget + if widget is not None: + if widget.get_parent() is not None: + parent = widget.get_parent() + if parent != self._canvas_area: + parent.remove(widget) + if widget.get_parent() != self._canvas_area: + self._canvas_area.append(widget) + + def set_toolbar_box(self, toolbar): + if self._toolbar_box == toolbar: + return + if self._toolbar_box is not None and \ + self._toolbar_box.get_parent() == self._toolbar_area: + self._toolbar_area.remove(self._toolbar_box) + self._toolbar_box = toolbar + if toolbar is not None: + if toolbar.get_parent() is not None: + parent = toolbar.get_parent() + if parent != self._toolbar_area: + parent.remove(toolbar) + if toolbar.get_parent() != self._toolbar_area: + self._toolbar_area.append(toolbar) + + def get_toolbar_box(self): + return self._toolbar_box + + def add_alert(self, alert): + self._alert_area.append(alert) + alert.set_visible(True) + + def remove_alert(self, alert): + if alert.get_parent() == self._alert_area: + self._alert_area.remove(alert) + + def minimize(self): + pass + + def get_title(self): + return _('Journal') + + def present(self): + if self._presenting: + return + self._presenting = True + try: + _logger.debug('present() called') + self.set_visible(True) + + from jarabe.model import shell + shell_model = shell.get_model() + if not shell_model: + return + + for act in shell_model._activities: + if act.is_journal(): + if shell_model.get_active_activity() != act: + shell_model.activate_activity(act) + return + finally: + self._presenting = False + + def reveal(self): + _logger.debug('reveal() called') + self.set_visible(True) + self.present() + + def close(self): + self.set_visible(False) def get_journal_window(): diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py index 1726763653..f941607f86 100644 --- a/src/jarabe/journal/keepicon.py +++ b/src/jarabe/journal/keepicon.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006, Red Hat, Inc. +# Copyright (C) 2006, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,35 +24,32 @@ class KeepIcon(Gtk.ToggleButton): __gtype_name__ = 'SugarKeepIcon' def __init__(self): - Gtk.ToggleButton.__init__(self) - self.set_relief(Gtk.ReliefStyle.NONE) - self.set_focus_on_click(False) + super().__init__() + self.props.has_frame = False + self.props.focusable = False self._icon = Icon(icon_name='emblem-favorite', pixel_size=style.SMALL_ICON_SIZE) - self.set_image(self._icon) + self.set_child(self._icon) self.connect('toggled', self.__toggled_cb) - self.connect('button-press-event', self.__button_press_event_cb) - self.connect('button-release-event', self.__button_release_event_cb) - self._xo_color = profile.get_color() + click = Gtk.GestureClick() + click.connect('pressed', self.__button_press_event_cb) + click.connect('released', self.__button_release_event_cb) + self.add_controller(click) - def do_get_preferred_width(self): - return 0, style.GRID_CELL_SIZE + self._xo_color = profile.get_color() - def do_get_preferred_height(self): - return 0, style.GRID_CELL_SIZE + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) - def __button_press_event_cb(self, widget, event): + def __button_press_event_cb(self, gesture, n_press, x, y): # We need to use a custom CSS class because in togglebuttons # the 'active' class doesn't only match the button press, they # can be left in the active state. - style_context = self.get_style_context() - style_context.add_class('toggle-press') + self.add_css_class('toggle-press') - def __button_release_event_cb(self, widget, event): - style_context = self.get_style_context() - style_context.remove_class('toggle-press') + def __button_release_event_cb(self, gesture, n_press, x, y): + self.remove_css_class('toggle-press') def __toggled_cb(self, widget): if self.get_active(): @@ -61,6 +58,5 @@ def __toggled_cb(self, widget): self._icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() - if hasattr(KeepIcon, 'set_css_name'): KeepIcon.set_css_name('canvasicon') diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py index cb13a39018..55b91f8986 100644 --- a/src/jarabe/journal/listmodel.py +++ b/src/jarabe/journal/listmodel.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009, Tomeu Vizoso +# Copyright (C) 2009, Tomeu Vizoso # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,13 +28,12 @@ from jarabe.journal import model from jarabe.journal import misc - DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' DS_DBUS_PATH = '/org/laptop/sugar/DataStore' -class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource): +class ListModel(GObject.GObject, Gtk.TreeModel): __gtype_name__ = 'JournalListModel' __gsignals__ = { @@ -57,29 +56,33 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource): COLUMN_SELECT = 12 _COLUMN_TYPES = { - COLUMN_UID: str, - COLUMN_FAVORITE: bool, - COLUMN_ICON: str, - COLUMN_ICON_COLOR: object, - COLUMN_TITLE: str, - COLUMN_TIMESTAMP: str, - COLUMN_CREATION_TIME: str, - COLUMN_FILESIZE: str, - COLUMN_PROGRESS: int, - COLUMN_BUDDY_1: object, - COLUMN_BUDDY_3: object, - COLUMN_BUDDY_2: object, - COLUMN_SELECT: bool, + COLUMN_UID: GObject.TYPE_STRING, + COLUMN_FAVORITE: GObject.TYPE_BOOLEAN, + COLUMN_ICON: GObject.TYPE_STRING, + COLUMN_ICON_COLOR: GObject.TYPE_PYOBJECT, + COLUMN_TITLE: GObject.TYPE_STRING, + COLUMN_TIMESTAMP: GObject.TYPE_STRING, + COLUMN_CREATION_TIME: GObject.TYPE_STRING, + COLUMN_FILESIZE: GObject.TYPE_STRING, + COLUMN_PROGRESS: GObject.TYPE_INT, + COLUMN_BUDDY_1: GObject.TYPE_PYOBJECT, + COLUMN_BUDDY_2: GObject.TYPE_PYOBJECT, + COLUMN_BUDDY_3: GObject.TYPE_PYOBJECT, + COLUMN_SELECT: GObject.TYPE_BOOLEAN, } + _dummy_xo_color = None + _dummy_buddy = [] + _PAGE_SIZE = 10 def __init__(self, query): - GObject.GObject.__init__(self) + super().__init__() self._last_requested_index = None self._temp_drag_file_uid = None self._cached_row = None + self._pyobject_cache = {} self._query = query self._all_ids = [] t = time.time() @@ -152,6 +155,19 @@ def __reconnect_updates_cb(self, metadata, filepath, uid): def do_get_value(self, iterator, column): if self.view_is_resizing: + t = ListModel._COLUMN_TYPES[column] + if t == GObject.TYPE_STRING: + return "" + elif t == GObject.TYPE_BOOLEAN: + return False + elif t == GObject.TYPE_INT: + return 0 + elif t == GObject.TYPE_PYOBJECT: + if column == ListModel.COLUMN_ICON_COLOR: + if ListModel._dummy_xo_color is None: + ListModel._dummy_xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg())) + return ListModel._dummy_xo_color + return ListModel._dummy_buddy return None index = iterator.user_data @@ -167,6 +183,9 @@ def do_get_value(self, iterator, column): self._last_requested_index = index self._cached_row = [] + self._pyobject_cache[index] = self._cached_row + if len(self._pyobject_cache) > 200: + self._pyobject_cache.clear() self._cached_row.append(metadata['uid']) self._cached_row.append(metadata.get('keep', '0') == '1') self._cached_row.append(misc.get_icon_name(metadata)) @@ -234,7 +253,9 @@ def do_get_value(self, iterator, column): self._cached_row.append([nick, XoColor(color)]) continue - self._cached_row.append(None) + self._cached_row.append(ListModel._dummy_buddy) + + self._cached_row.append(self.is_selected(metadata['uid'])) return self._cached_row[column] @@ -271,28 +292,6 @@ def do_iter_has_child(self, iterator): def do_iter_parent(self, iterator): return (False, Gtk.TreeIter()) - def do_drag_data_get(self, path, selection): - uid = self[path][ListModel.COLUMN_UID] - target_atom = selection.get_target() - target_name = target_atom.name() - if target_name == 'text/uri-list': - # Only get a new temp path if we have a new file, the frame - # requests a path many times and if we give it a new path it - # ends up with a broken path - if uid != self._temp_drag_file_uid: - # Get hold of a reference so the temp file doesn't get deleted - self._temp_drag_file_path = model.get_file(uid) - self._temp_drag_file_uid = uid - logging.debug('putting %r in selection', self._temp_drag_file_path) - selection.set(target_atom, 8, self._temp_drag_file_path) - return True - if target_name == 'journal-object-id': - # uid is unicode but Gtk.SelectionData.set() needs str - selection.set(target_atom, 8, str(uid)) - return True - - return False - def set_selected(self, uid, value): if value: self._selected.append(uid) diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 8ea2e2cc2e..e1219c47e0 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -27,7 +27,6 @@ from sugar4.graphics import style from sugar4.graphics.alert import ConfirmationAlert from sugar4.graphics.icon import Icon, CellRendererIcon -from sugar4.graphics.scrollingdetector import ScrollingDetector from sugar4 import util from sugar4 import profile from sugar4.graphics.palettewindow import TreeViewInvoker @@ -38,7 +37,6 @@ from jarabe.journal import misc from jarabe.journal import journalwindow - UPDATE_INTERVAL = 300 PROJECT_BUNDLE_ID = 'org.sugarlabs.Project' @@ -56,7 +54,7 @@ class TreeView(Gtk.TreeView): } def __init__(self, journalactivity): - Gtk.TreeView.__init__(self) + super().__init__() self._journalactivity = journalactivity self.icon_activity_column = None @@ -67,11 +65,9 @@ def __init__(self, journalactivity): self.set_headers_visible(False) self.set_enable_search(False) - self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.TOUCH_MASK | - Gdk.EventMask.BUTTON_RELEASE_MASK) def connect_to_scroller(self, scrolled): + # scrolling detector logic scrolled.connect('scroll-start', self._scroll_start_cb) scrolled.connect('scroll-end', self._scroll_end_cb) @@ -113,7 +109,7 @@ def create_palette(self, path, column): iterator, column_index ) - if buddy_value is not None: + if buddy_value: nick, xo_color = buddy_value palette = BuddyPalette( (nick, @@ -130,23 +126,11 @@ def __volume_error_cb(self, palette, message, severity): def __choose_project_cb(self, palette, metadata_to_copy): self.emit('choose-project', metadata_to_copy) - def do_size_request(self, requisition): - # HACK: We tell the model that the view is just resizing so it can - # avoid hitting both D-Bus and disk. - tree_model = self.get_model() - if tree_model is not None: - tree_model.view_is_resizing = True - try: - Gtk.TreeView.do_size_request(self, requisition) - finally: - if tree_model is not None: - tree_model.view_is_resizing = False - def __del__(self): self._invoker.detach() -class BaseListView(Gtk.Bin): +class BaseListView(Gtk.Box): __gtype_name__ = 'JournalBaseListView' __gsignals__ = { @@ -167,18 +151,21 @@ def __init__(self, journalactivity, enable_multi_operations=False): self._last_progress_bar_pulse = None self._scroll_position = 0. self._projects_view_active = False + + self._current_child = None - Gtk.Bin.__init__(self) + super().__init__(orientation=Gtk.Orientation.VERTICAL) self.connect('map', self.__map_cb) self.connect('unmap', self.__unmap_cb) - self.connect('destroy', self.__destroy_cb) self._scrolled_window = Gtk.ScrolledWindow() self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.add(self._scrolled_window) - self._scrolled_window.show() + self._scrolled_window.set_vexpand(True) + self.append(self._scrolled_window) + self._current_child = self._scrolled_window + self._scrolled_window.set_visible(True) self.tree_view = TreeView(self._journalactivity) self.tree_view.connect('detail-clicked', self.__detail_clicked_cb) @@ -186,16 +173,13 @@ def __init__(self, journalactivity, enable_multi_operations=False): selection = self.tree_view.get_selection() selection.set_mode(Gtk.SelectionMode.SINGLE) self.tree_view.props.fixed_height_mode = True - self._scrolled_window.add(self.tree_view) - self.tree_view.show() + self._scrolled_window.set_child(self.tree_view) + self.tree_view.set_visible(True) self.cell_title = None self.cell_icon = None self._title_column = None - self.sort_column = None - self._scrolling_detector = ScrollingDetector(self._scrolled_window) - self.tree_view.connect_to_scroller(self._scrolling_detector) - + self._add_columns() self.enable_drag_and_copy() @@ -210,17 +194,58 @@ def __init__(self, journalactivity, enable_multi_operations=False): model.created.connect(self.__model_created_cb) model.updated.connect(self.__model_updated_cb) model.deleted.connect(self.__model_deleted_cb) + + def _switch_child(self, new_child): + if self._current_child == new_child: + return + if self._current_child: + self._current_child.unparent() + self.append(new_child) + self._current_child = new_child def enable_drag_and_copy(self): - self.tree_view.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, - [Gtk.TargetEntry.new( - 'text/uri-list', 0, 0), - Gtk.TargetEntry.new( - 'journal-object-id', 0, 0)], - Gdk.DragAction.COPY) + self._drag_source = Gtk.DragSource.new() + self._drag_source.set_actions(Gdk.DragAction.COPY) + self._drag_source.connect('prepare', self.__drag_prepare_cb) + self._drag_source.connect('drag-begin', self.__drag_begin_cb) + self.tree_view.add_controller(self._drag_source) def disable_drag_and_copy(self): - self.tree_view.unset_rows_drag_source() + if hasattr(self, '_drag_source'): + self.tree_view.remove_controller(self._drag_source) + del self._drag_source + def __drag_prepare_cb(self, drag_source, x, y): + path_info = self.tree_view.get_path_at_pos(int(x), int(y)) + if not path_info: + return None + path, column, cell_x, cell_y = path_info + self._drag_path = path + + row = self.tree_view.get_model()[path] + uid = row[ListModel.COLUMN_UID] + + providers = [] + + bytes1 = GLib.Bytes.new(uid.encode('utf-8')) + providers.append(Gdk.ContentProvider.new_for_bytes("journal-object-id", bytes1)) + + file_path = model.get_file(uid) + if file_path: + uri = 'file://' + file_path + '\r\n' + bytes2 = GLib.Bytes.new(uri.encode('utf-8')) + providers.append(Gdk.ContentProvider.new_for_bytes("text/uri-list", bytes2)) + + return Gdk.ContentProvider.new_union(providers) + + def __drag_begin_cb(self, drag_source, drag): + if hasattr(self, '_drag_path'): + row = self.tree_view.get_model()[self._drag_path] + try: + texture = Gdk.Texture.new_from_filename(row[ListModel.COLUMN_ICON]) + drag_source.set_icon(texture, 0, 0) + except GLib.Error: + pass + self._is_dragging = True def __model_created_cb(self, sender, signal, object_id): if self._is_new_item_visible(object_id): @@ -253,7 +278,10 @@ def _add_columns(self): cell_select.connect('toggled', self.__cell_select_toggled_cb) cell_select.props.activatable = True cell_select.props.xpad = style.DEFAULT_PADDING - cell_select.props.indicator_size = style.zoom(26) + + # It might be, if so we just ignore it. + if hasattr(cell_select.props, 'indicator_size'): + cell_select.props.indicator_size = style.zoom(26) column = Gtk.TreeViewColumn() column.props.sizing = Gtk.TreeViewColumnSizing.FIXED @@ -264,7 +292,6 @@ def _add_columns(self): cell_favorite = CellRendererFavorite() cell_favorite.connect('clicked', self._favorite_clicked_cb) - cell_favorite.connect_to_scroller(self._scrolling_detector) self._fav_column = Gtk.TreeViewColumn() self._fav_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED @@ -275,7 +302,6 @@ def _add_columns(self): self.tree_view.append_column(self._fav_column) self.cell_icon = CellRendererActivityIcon() - self.cell_icon.connect_to_scroller(self._scrolling_detector) column = Gtk.TreeViewColumn() self.tree_view.icon_activity_column = column @@ -357,15 +383,7 @@ def _get_width_for_string(self, text): width, height_ = layout.get_pixel_size() return width - def do_size_allocate(self, allocation): - self.set_allocation(allocation) - self.get_child().size_allocate(allocation) - - def do_size_request(self, requisition): - requisition.width, requisition.height = \ - self.get_child().size_request() - - def __destroy_cb(self, widget): + def do_unroot(self): if self._model is not None: self._model.stop() @@ -393,7 +411,7 @@ def __progress_data_cb(self, column, cell, tree_model, def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter, data): favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE] - if favorite: + if favorite and str(favorite) not in ('0', 'false', 'False'): cell.props.xo_color = profile.get_color() else: cell.props.xo_color = None @@ -411,9 +429,7 @@ def _favorite_clicked_cb(self, cell, path): metadata['keep'] = '1' self._model[iterator][ListModel.COLUMN_FAVORITE] = '1' - cell_rect = self.tree_view.get_cell_area(path, self._fav_column) - self.tree_view.queue_draw_area(cell_rect.x, cell_rect.y, - cell_rect.width, cell_rect.height) + self.tree_view.queue_draw() # HACK for https://bugs.sugarlabs.org/ticket/4944 # Icon does not update automatically if there is only one journal entry @@ -452,10 +468,12 @@ def update_with_query(self, query_dict): def refresh(self, new_query=False): logging.debug('ListView.refresh query %r', self._query) self._stop_progress_bar() - window = self.get_toplevel().get_window() - if window is not None: - window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) - Gdk.flush() + + # Cursor setting + root = self.get_root() + if root: + root.set_cursor(Gdk.Cursor.new_from_name("wait")) + Gdk.Display.get_default().flush() GLib.idle_add(self._do_refresh, new_query) def _do_refresh(self, new_query=False): @@ -471,10 +489,11 @@ def _do_refresh(self, new_query=False): self._model.connect('ready', self.__model_ready_cb) self._model.connect('progress', self.__model_progress_cb) self._model.setup(self.__model_updated_cb) - window = self.get_toplevel().get_window() - if window is not None: - window.set_cursor(None) - Gdk.flush() + + root = self.get_root() + if root: + root.set_cursor(None) + Gdk.Display.get_default().flush() def __model_ready_cb(self, tree_model): self._stop_progress_bar() @@ -482,12 +501,6 @@ def __model_ready_cb(self, tree_model): self._scroll_position = self.tree_view.props.vadjustment.props.value logging.debug('ListView.__model_ready_cb %r', self._scroll_position) - x11_window = self.tree_view.get_window() - - if x11_window is not None: - # prevent glitches while later vadjustment setting, see #1235 - self.tree_view.get_bin_window().hide() - # if the selection was preserved, restore it if self._backup_selected is not None: tree_model.restore_selection(self._backup_selected) @@ -498,11 +511,6 @@ def __model_ready_cb(self, tree_model): self.tree_view.set_model(self._model) self.tree_view.props.vadjustment.props.value = self._scroll_position - self.tree_view.props.vadjustment.value_changed() - - if x11_window is not None: - # prevent glitches while later vadjustment setting, see #1235 - self.tree_view.get_bin_window().show() if len(tree_model) == 0: if self._query.get('project_id', None): @@ -534,7 +542,6 @@ def _can_clear_query(self): def __map_cb(self, widget): logging.debug('ListView.__map_cb %r', self._scroll_position) self.tree_view.props.vadjustment.props.value = self._scroll_position - self.tree_view.props.vadjustment.value_changed() self.set_is_visible(True) def __unmap_cb(self, widget): @@ -558,100 +565,90 @@ def __model_progress_cb(self, tree_model): self._last_progress_bar_pulse = time.time() def _start_progress_bar(self): - alignment = Gtk.Alignment.new(xalign=0.5, yalign=0.5, - xscale=0.5, yscale=0) - self.remove(self.get_child()) - self.add(alignment) - alignment.show() + alignment = Gtk.Box() + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.CENTER) + + self._switch_child(alignment) + alignment.set_visible(True) self._progress_bar = Gtk.ProgressBar() self._progress_bar.props.pulse_step = 0.01 self._last_progress_bar_pulse = time.time() - alignment.add(self._progress_bar) - self._progress_bar.show() + alignment.append(self._progress_bar) + self._progress_bar.set_visible(True) def _stop_progress_bar(self): if self._progress_bar is None: return - self.remove(self.get_child()) - self.add(self._scrolled_window) + self._switch_child(self._scrolled_window) self._progress_bar = None def _show_message(self, message, show_clear_query=False): - self.remove(self.get_child()) - - background_box = Gtk.EventBox() - background_box.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - self.add(background_box) - - alignment = Gtk.Alignment.new(0.5, 0.5, 0.1, 0.1) - background_box.add(alignment) + background_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + # Apply CSS background color + background_box.add_css_class('bg-white') + + self._switch_child(background_box) + + alignment = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.CENTER) + alignment.set_vexpand(True) + background_box.append(alignment) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - alignment.add(box) + alignment.append(box) icon = Icon(pixel_size=style.LARGE_ICON_SIZE, icon_name='activity-journal', stroke_color=style.COLOR_BUTTON_GREY.get_svg(), fill_color=style.COLOR_TRANSPARENT.get_svg()) - box.pack_start(icon, expand=True, fill=False, padding=0) + box.append(icon) + icon.set_visible(True) label = Gtk.Label() color = style.COLOR_BUTTON_GREY.get_html() label.set_markup('%s' % ( color, GLib.markup_escape_text(message))) - box.pack_start(label, expand=True, fill=False, padding=0) + box.append(label) + label.set_visible(True) if not self.get_projects_view_active(): if show_clear_query: - button_box = Gtk.HButtonBox() - button_box.set_layout(Gtk.ButtonBoxStyle.CENTER) - box.pack_start(button_box, False, True, 0) - button_box.show() + button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + button_box.set_halign(Gtk.Align.CENTER) + box.append(button_box) + button_box.set_visible(True) - button = Gtk.Button(label=_('Clear search')) + button = Gtk.Button() button.connect('clicked', self.__clear_button_clicked_cb) - button.props.image = Icon(icon_name='dialog-cancel', - pixel_size=style.SMALL_ICON_SIZE) - button_box.pack_start(button, expand=True, fill=False, - padding=0) - background_box.show_all() + # Build icon+label child. + btn_icon = Icon(icon_name='dialog-cancel', pixel_size=style.SMALL_ICON_SIZE) + box_btn = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + box_btn.append(btn_icon) + box_btn.append(Gtk.Label(label=_('Clear search'))) + button.set_child(box_btn) + + button_box.append(button) + button.set_visible(True) + + background_box.set_visible(True) def __clear_button_clicked_cb(self, button): self.emit('clear-clicked') def _clear_message(self): - if self.get_child() == self._scrolled_window: + if self._current_child == self._scrolled_window: return - self.remove(self.get_child()) - self.add(self._scrolled_window) - self._scrolled_window.show() + self._switch_child(self._scrolled_window) + self._scrolled_window.set_visible(True) def update_dates(self): if not self.tree_view.get_realized(): return - visible_range = self.tree_view.get_visible_range() - if visible_range is None: - return - - logging.debug('ListView.update_dates') - - path, end_path = visible_range - tree_model = self.tree_view.get_model() - - while True: - cel_rect = self.tree_view.get_cell_area(path, - self.sort_column) - x, y = self.tree_view.convert_tree_to_widget_coords(cel_rect.x, - cel_rect.y) - self.tree_view.queue_draw_area(x, y, cel_rect.width, - cel_rect.height) - if path == end_path: - break - next_iter = tree_model.iter_next(tree_model.get_iter(path)) - path = tree_model.get_path(next_iter) + self.tree_view.queue_draw() def _set_dirty(self): if self._fully_obscured or self._updates_disabled: @@ -723,6 +720,8 @@ class ListView(BaseListView): ([])), 'title-edit-finished': (GObject.SignalFlags.RUN_FIRST, None, ([str, object])), + 'title-edit-canceled': (GObject.SignalFlags.RUN_FIRST, None, + ([])), 'project-view-activate': (GObject.SignalFlags.RUN_FIRST, None, ([object])), } @@ -731,11 +730,13 @@ def __init__(self, journalactivity, enable_multi_operations=False): BaseListView.__init__(self, journalactivity, enable_multi_operations) self._is_dragging = False - self.tree_view.connect('drag-begin', self.__drag_begin_cb) - self.tree_view.connect('drag-data-get', self.__drag_data_get_cb) - self.tree_view.connect('button-release-event', - self.__button_release_event_cb) - self.tree_view.connect('key-press-event', self._key_press_event_cb) + click_controller = Gtk.GestureClick() + click_controller.connect('released', self.__button_release_event_cb) + self.tree_view.add_controller(click_controller) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self._key_press_event_cb) + self.tree_view.add_controller(key_controller) self.cell_title.connect('edited', self.__cell_title_edited_cb) self.cell_title.connect('editing-canceled', self.__editing_canceled_cb) @@ -751,21 +752,21 @@ def __init__(self, journalactivity, enable_multi_operations=False): column.pack_start(cell_detail, True) self.tree_view.append_column(column) - def _key_press_event_cb(self, tree_view, event): - ''' - Adds keyboard accessibility to the journal. - Activity can be resumed by pressing 'Enter' key. - Entry can be renamed by pressing 'Ctrl' + 'F2' keys - Detail View can be opened with 'Right' arrow key. - ''' - keyname = Gdk.keyval_name(event.keyval) - path, col = self.tree_view.get_cursor() + def _key_press_event_cb(self, controller, keyval, keycode, state): + keyname = Gdk.keyval_name(keyval) + + path_col = self.tree_view.get_cursor() + if not path_col or path_col[0] is None: + return False + + path, col = path_col if self.tree_view.has_focus(): if keyname == 'Return': self.__icon_clicked_cb(None, path) + return True - if event.state & Gdk.ModifierType.CONTROL_MASK and keyname == 'F2': + if state & Gdk.ModifierType.CONTROL_MASK and keyname == 'F2': row = self.tree_view.get_model()[path] metadata = model.get(row[ListModel.COLUMN_UID]) self.cell_title.props.editable = model.is_editable(metadata) @@ -774,43 +775,41 @@ def _key_press_event_cb(self, tree_view, event): self.emit('title-edit-started') column = self.tree_view.get_column(3) - tree_view.set_cursor_on_cell(path, column, self.cell_title, + self.tree_view.set_cursor_on_cell(path, column, self.cell_title, start_editing=True) + return True if keyname == 'Right': tree_iter = self._model.get_iter(path) uid = self._model[tree_iter][ListModel.COLUMN_UID] self.emit('detail-clicked', uid) + return True + if keyname in ('Delete', 'KP_Delete'): + row = self.tree_view.get_model()[path] + uid = row[ListModel.COLUMN_UID] + metadata = model.get(uid) + if model.is_editable(metadata): + model.delete(uid) + return True + + if keyname == 'Escape': + self.emit('clear-clicked') + return True + + return False def is_dragging(self): return self._is_dragging - def __drag_begin_cb(self, widget, drag_context): - path, _column = self.tree_view.get_cursor() - if path is None: - return - - row = self.tree_view.get_model()[path] - _pixbuf = GdkPixbuf.Pixbuf.new_from_file(row[ListModel.COLUMN_ICON]) - self.tree_view.drag_source_set_icon_pixbuf(_pixbuf) - self._is_dragging = True - - def __drag_data_get_cb(self, widget, context, selection, info, time): - # HACK: Gtk.TreeDragSource does not work for us on Gtk 3.16+, so - # use our drag source code instead - path, _column = self.tree_view.get_cursor() - model = self.tree_view.get_model() - model.do_drag_data_get(path, selection) - - def __button_release_event_cb(self, tree_view, event): + def __button_release_event_cb(self, gesture, n_press, x, y): try: if self._is_dragging: return finally: self._is_dragging = False - pos = tree_view.get_path_at_pos(int(event.x), int(event.y)) + pos = self.tree_view.get_path_at_pos(int(x), int(y)) if pos is None: return @@ -824,7 +823,7 @@ def __button_release_event_cb(self, tree_view, event): if self.cell_title.props.editable: self.emit('title-edit-started') - tree_view.set_cursor_on_cell(path, column, self.cell_title, + self.tree_view.set_cursor_on_cell(path, column, self.cell_title, start_editing=True) def __detail_cell_clicked_cb(self, cell, path): @@ -850,7 +849,7 @@ def __cell_title_edited_cb(self, cell, path, new_text): alert.props.msg = _('The title is usually not left empty') alert.connect('response', self._cell_title_alert_response_cb, path, new_text) journalwindow.get_journal_window().add_alert(alert) - alert.show() + alert.set_visible(True) return if old_text != new_text: @@ -861,12 +860,13 @@ def _cell_title_alert_response_cb(self, alert, response_id, path, new_text): journalwindow.get_journal_window().remove_alert(alert) iterator = self._model.get_iter(path) - if response_id is Gtk.ResponseType.OK: + if response_id == Gtk.ResponseType.OK: self._model[iterator][ListModel.COLUMN_TITLE] = new_text self.emit('title-edit-finished', new_text, path) def __editing_canceled_cb(self, cell): self.cell_title.props.editable = False + self.emit('title-edit-canceled') class CellRendererFavorite(CellRendererIcon): @@ -926,7 +926,7 @@ def __init__(self, column_index): self.nick = None def set_buddy(self, buddy): - if buddy is None: + if not buddy: self.props.icon_name = None self.nick = None else: diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index 1c5473e121..2a953ddf64 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -69,16 +69,17 @@ def _get_icon_for_mime(mime_type): def get_mount_icon_name(mount, size): icon = mount.get_icon() if isinstance(icon, Gio.ThemedIcon): - icon_theme = Gtk.IconTheme.get_default() + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) for icon_name in icon.props.names: - lookup = icon_theme.lookup_icon(icon_name, size, 0) + lookup = icon_theme.lookup_icon(icon_name, None, size, 1, 0, 0) if lookup is not None: - file_name = lookup.get_filename() - if '/icons/sugar/' not in file_name: + icon_file = lookup.get_file() + file_name = icon_file.get_path() if icon_file else None + if file_name is not None and '/icons/sugar/' not in file_name: continue return icon_name logging.error('Cannot find icon name for %s, %s', icon, mount) - return 'drive' + return 'drive-harddisk' def get_icon_name(metadata): @@ -283,8 +284,8 @@ def launch(bundle, activity_id=None, object_id=None, uri=None, color=None, shell_model = shell.get_model() activity = shell_model.get_activity_by_id(activity_id) if activity is not None: - logging.debug('re-launch %r', activity.get_window()) - activity.get_window().activate(Gtk.get_current_event_time()) + logging.debug('re-launch via activate_activity for %r', activity) + shell_model.activate_activity(activity) return if not shell_model.can_launch_activity(): @@ -322,7 +323,7 @@ def _downgrade_option_alert(bundle, metadata): bundle.get_activity_version() alert.connect('response', _downgrade_alert_response_cb, metadata) journalwindow.get_journal_window().add_alert(alert) - alert.show() + alert.set_visible(True) def _downgrade_alert_response_cb(alert, response_id, metadata): @@ -378,9 +379,10 @@ def handle_bundle_installation(metadata, force_downgrade=False): registry = bundleregistry.get_registry() - window = journalwindow.get_journal_window().get_window() - window.set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH)) - Gdk.flush() + window = journalwindow.get_journal_window() + if window: + window.set_cursor(Gdk.Cursor.new_from_name('wait')) + Gdk.Display.get_default().flush() try: installed = registry.install(bundle, force_downgrade) @@ -390,8 +392,9 @@ def handle_bundle_installation(metadata, force_downgrade=False): logging.exception('Could not install bundle %s', bundle.get_path()) return None, False finally: - window.set_cursor(None) - Gdk.flush() + if window: + window.set_cursor(None) + Gdk.Display.get_default().flush() # If we just installed a bundle, update the datastore accordingly. # We do not do this for JournalEntryBundles because the JEB code transforms @@ -415,9 +418,9 @@ def get_mount_color(mount): uuid = mount.get_uuid() if uuid: - sha_hash.update(uuid) + sha_hash.update(uuid.encode('utf-8')) elif path is None: - sha_hash.update(str(time.time())) + sha_hash.update(str(time.time()).encode('utf-8')) else: mount_name = os.path.basename(path) mount_name = mount_name.encode('utf-8') diff --git a/src/jarabe/journal/modalalert.py b/src/jarabe/journal/modalalert.py index a774a2c2d5..b301486ce6 100644 --- a/src/jarabe/journal/modalalert.py +++ b/src/jarabe/journal/modalalert.py @@ -15,11 +15,13 @@ from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GLib from gettext import gettext as _ from sugar4.graphics.icon import Icon from sugar4.graphics import style from sugar4 import profile +from jarabe.model import shell class ModalAlert(Gtk.Window): @@ -27,71 +29,100 @@ class ModalAlert(Gtk.Window): __gtype_name__ = 'SugarModalAlert' def __init__(self): - Gtk.Window.__init__(self) + super().__init__() - self.set_border_width(style.LINE_WIDTH) offset = style.GRID_CELL_SIZE - width = Gdk.Screen.width() - offset * 2 - height = Gdk.Screen.height() - offset * 2 - self.set_size_request(width, height) - self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + + # Calculate dynamic size based on primary monitor geometry + display = Gdk.Display.get_default() + monitor = display.get_monitors().get_item(0) + if monitor is not None: + geometry = monitor.get_geometry() + width = geometry.width - offset * 2 + height = geometry.height - offset * 2 + self.set_default_size(width, height) + else: + self.set_default_size(800, 600) + self.set_decorated(False) self.set_resizable(False) self.set_modal(True) - self._main_view = Gtk.EventBox() + # Set transient parent window + shell_model = shell.get_model() + if shell_model and shell_model._main_window: + self.set_transient_for(shell_model._main_window) + + # Key controller: Escape closes + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) + + self._main_view = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + # Apply black background CSS + self._main_view.add_css_class('modal-bg') + self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._vbox.set_spacing(style.DEFAULT_SPACING) - self._vbox.set_border_width(style.GRID_CELL_SIZE * 2) - self._main_view.modify_bg(Gtk.StateType.NORMAL, - style.COLOR_BLACK.get_gdk_color()) - self._main_view.add(self._vbox) - self._vbox.show() + self._vbox.set_margin_top(style.GRID_CELL_SIZE * 2) + self._vbox.set_margin_bottom(style.GRID_CELL_SIZE * 2) + self._vbox.set_margin_start(style.GRID_CELL_SIZE * 2) + self._vbox.set_margin_end(style.GRID_CELL_SIZE * 2) + + self._main_view.append(self._vbox) + self._vbox.set_visible(True) color = profile.get_color() icon = Icon(icon_name='activity-journal', pixel_size=style.XLARGE_ICON_SIZE, xo_color=color) - self._vbox.pack_start(icon, expand=False, fill=False, padding=0) - icon.show() + self._vbox.append(icon) + icon.set_visible(True) self._title = Gtk.Label() - self._title.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) self._title.set_markup('%s' % _('Your Journal is full')) - self._vbox.pack_start(self._title, expand=False, fill=False, padding=0) - self._title.show() + self._vbox.append(self._title) + self._title.set_visible(True) self._message = Gtk.Label( label=_('Please delete some old Journal' ' entries to make space for new ones.')) - self._message.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_WHITE.get_gdk_color()) - self._vbox.pack_start(self._message, expand=False, - fill=False, padding=0) - self._message.show() - - alignment = Gtk.Alignment.new(xalign=0.5, yalign=0.5, - xscale=0.0, yscale=0.0) - self._vbox.pack_start(alignment, expand=False, fill=True, padding=0) - alignment.show() + self._vbox.append(self._message) + self._message.set_visible(True) + + # Alignment can be replaced by Box with hexpand/vexpand + alignment = Gtk.Box() + alignment.set_hexpand(True) + alignment.set_vexpand(True) + alignment.set_halign(Gtk.Align.CENTER) + alignment.set_valign(Gtk.Align.CENTER) + self._vbox.append(alignment) + alignment.set_visible(True) self._show_journal = Gtk.Button() self._show_journal.set_label(_('Show Journal')) - alignment.add(self._show_journal) - self._show_journal.show() + alignment.append(self._show_journal) + self._show_journal.set_visible(True) self._show_journal.connect('clicked', self.__show_journal_cb) - self.add(self._main_view) - self._main_view.show() + self.set_child(self._main_view) + self._main_view.set_visible(True) self.connect('realize', self.__realize_cb) def __realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - self.get_window().set_accept_focus(True) + self.set_focusable(True) + shell.get_model().push_modal() + + def __key_press_event_cb(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: + shell.get_model().pop_modal() + self.close() + return True + return False def __show_journal_cb(self, button): - """The opener will listen on the destroy signal""" - self.destroy() + """The opener will listen on the close-request signal""" + shell.get_model().pop_modal() + self.close() diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index c46489812c..7461342cf0 100644 --- a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007-2011, One Laptop per Child +# Copyright (C) 2007-2011, One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -30,13 +30,13 @@ from gi.repository import Gio from gi.repository import GLib +import gi +gi.require_version('SugarExt', '2.0') from gi.repository import SugarExt - from sugar4 import dispatch from sugar4 import mime from sugar4 import util - DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' DS_DBUS_PATH = '/org/laptop/sugar/DataStore' @@ -222,8 +222,8 @@ def __init__(self, query, page_size): BaseResultSet.__init__(self, query, page_size) def find(self, query): - entries, total_count = _get_datastore().find(query, PROPERTIES, - byte_arrays=True) + entries, total_count = _call_datastore('find', query, PROPERTIES, + byte_arrays=True) for entry in entries: entry['mountpoint'] = '/' @@ -233,7 +233,7 @@ def find(self, query): def find_ids(self, query): copy = query.copy() copy.pop('mountpoints', '/') - return _get_datastore().find_ids(copy) + return _call_datastore('find_ids', copy) class InplaceResultSet(BaseResultSet): @@ -503,7 +503,6 @@ def _get_file_metadata_from_json(path, fetch_preview): metadata = None mount_point = _get_mount_point(path) subdir = '' - # check if the file is a subdirectory if mount_point != dir_path: subdir = os.path.relpath(dir_path, mount_point) @@ -556,6 +555,23 @@ def _get_datastore(): return _datastore +def _call_datastore(method, *args, **kwargs): + try: + return getattr(_get_datastore(), method)(*args, **kwargs) + except dbus.DBusException as e: + if e.get_dbus_name() in ['org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.NoReply', + 'org.freedesktop.DBus.Error.Disconnected']: + logging.warning('Datastore connection failed, retrying once.') + global _datastore + _datastore = None + try: + return getattr(_get_datastore(), method)(*args, **kwargs) + except Exception: + raise e + raise + + def _datastore_created_cb(object_id): created.send(None, object_id=object_id) @@ -602,7 +618,7 @@ def get(object_id): metadata = _get_file_metadata(object_id, stat) metadata['mountpoint'] = _get_mount_point(object_id) else: - metadata = _get_datastore().get_properties(object_id, byte_arrays=True) + metadata = _call_datastore('get_properties', object_id, byte_arrays=True) metadata['mountpoint'] = '/' return metadata @@ -614,7 +630,7 @@ def get_file(object_id): logging.debug('get_file asked for file with path %r', object_id) return object_id logging.debug('get_file asked for entry with id %r', object_id) - file_path = _get_datastore().get_filename(object_id) + file_path = _call_datastore('get_filename', object_id) if file_path: return util.TempFilePath(file_path) return None @@ -627,7 +643,7 @@ def get_file_size(object_id): if os.path.exists(object_id): return os.stat(object_id).st_size - file_path = _get_datastore().get_filename(object_id) + file_path = _call_datastore('get_filename', object_id) if file_path: size = os.stat(file_path).st_size os.remove(file_path) @@ -640,14 +656,14 @@ def get_unique_values(key): """Returns a list with the different values a property has taken """ empty_dict = dbus.Dictionary({}, signature='ss') - return _get_datastore().get_uniquevaluesfor(key, empty_dict) + return _call_datastore('get_uniquevaluesfor', key, empty_dict) def delete(object_id): """Removes an object from persistent storage """ if not os.path.exists(object_id): - _get_datastore().delete(object_id) + _call_datastore('delete', object_id) else: os.unlink(object_id) dir_path = os.path.dirname(object_id) @@ -655,7 +671,6 @@ def delete(object_id): mount_point = _get_mount_point(object_id) subdir = '' - # check if the file is a subdirectory if mount_point != dir_path: subdir = os.path.relpath(dir_path, mount_point) @@ -720,18 +735,18 @@ def error_handler(error): if metadata.get('mountpoint', '/') == '/': if metadata.get('uid', ''): - _get_datastore().update(metadata['uid'], - dbus.Dictionary(metadata), - file_path, - transfer_ownership, - reply_handler=updated_reply_handler, - error_handler=error_handler) + _call_datastore('update', metadata['uid'], + dbus.Dictionary(metadata), + file_path, + transfer_ownership, + reply_handler=updated_reply_handler, + error_handler=error_handler) else: - _get_datastore().create(dbus.Dictionary(metadata), - file_path, - transfer_ownership, - reply_handler=created_reply_handler, - error_handler=error_handler) + _call_datastore('create', dbus.Dictionary(metadata), + file_path, + transfer_ownership, + reply_handler=created_reply_handler, + error_handler=error_handler) else: _write_entry_on_external_device( metadata, file_path, ready_callback=ready_callback) @@ -826,7 +841,6 @@ def _splice_cb(*args): metadata_dir_path = os.path.join(metadata['mountpoint'], JOURNAL_METADATA_DIR) - # check if the file is in a subdirectory in Documents or device if original_dir_name.startswith(metadata['mountpoint']) and \ original_dir_name != metadata['mountpoint']: subdir = os.path.relpath(original_dir_name, metadata['mountpoint']) @@ -923,7 +937,6 @@ def is_editable(metadata): return True return os.access(metadata['mountpoint'], os.W_OK) - _documents_path = None diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py index a5f384c592..f3e2aeb01c 100644 --- a/src/jarabe/journal/objectchooser.py +++ b/src/jarabe/journal/objectchooser.py @@ -19,7 +19,6 @@ from gi.repository import GObject from gi.repository import Gtk from gi.repository import Gdk -#from gi.repository import Wnck from sugar4.graphics import style from sugar4.graphics.toolbutton import ToolButton @@ -28,11 +27,16 @@ from jarabe.journal.listview import BaseListView from jarabe.journal.listmodel import ListModel from jarabe.journal.journaltoolbox import MainToolbox -from jarabe.journal.volumestoolbar import VolumesToolbar from jarabe.model import bundleregistry from jarabe.journal.iconview import IconView +# Inject CSS for modal background +provider = Gtk.CssProvider() +css = b".modal-bg { background-color: %s; }" % style.COLOR_BLACK.get_html().encode('utf-8') +provider.load_from_data(css) +Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + class ObjectChooser(Gtk.Window): @@ -44,93 +48,110 @@ class ObjectChooser(Gtk.Window): def __init__(self, parent=None, what_filter='', filter_type=None, show_preview=False): - Gtk.Window.__init__(self) - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + super().__init__() self.set_decorated(False) - self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) - self.set_border_width(style.LINE_WIDTH) - self.set_has_resize_grip(False) + self.set_modal(True) self._selected_object_id = None self._show_preview = show_preview - self.add_events(Gdk.EventMask.VISIBILITY_NOTIFY_MASK) - self.connect('visibility-notify-event', - self.__visibility_notify_event_cb) - self.connect('delete-event', self.__delete_event_cb) - self.connect('key-press-event', self.__key_press_event_cb) + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) if parent is None: logging.warning('ObjectChooser: No parent window specified') + # Fall back to main sugar window as transient parent + from jarabe.model import shell as _shell + shell_model = _shell.get_model() + if shell_model and shell_model.get_active_window(): + self.set_transient_for(shell_model.get_active_window()) else: - self.connect('realize', self.__realize_cb, parent) - - #screen = Wnck.Screen.get_default() - #screen.connect('window-closed', self.__window_closed_cb, parent) + self.set_transient_for(parent) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.add(vbox) - vbox.show() + vbox.add_css_class('modal-bg') + # Margin instead of border_width + vbox.set_margin_start(style.LINE_WIDTH) + vbox.set_margin_end(style.LINE_WIDTH) + vbox.set_margin_top(style.LINE_WIDTH) + vbox.set_margin_bottom(style.LINE_WIDTH) + + self.set_child(vbox) + vbox.set_visible(True) title_box = TitleBox(what_filter, filter_type) - title_box.connect('volume-changed', self.__volume_changed_cb) title_box.close_button.connect('clicked', self.__close_button_clicked_cb) title_box.set_size_request(-1, style.GRID_CELL_SIZE) - vbox.pack_start(title_box, False, True, 0) - title_box.show() + vbox.append(title_box) + title_box.set_visible(True) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) - vbox.pack_start(separator, False, True, 0) - separator.show() + vbox.append(separator) + separator.set_visible(True) self._toolbar = MainToolbox(default_what_filter=what_filter, default_filter_type=filter_type) self._toolbar.connect('query-changed', self.__query_changed_cb) self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE) - vbox.pack_start(self._toolbar, False, True, 0) - self._toolbar.show() + vbox.append(self._toolbar) + self._toolbar.set_visible(True) if not self._show_preview: self._list_view = ChooserListView(self._toolbar) self._list_view.connect('entry-activated', self.__entry_activated_cb) self._list_view.connect('clear-clicked', self.__clear_clicked_cb) - vbox.pack_start(self._list_view, True, True, 0) - self._list_view.show() + self._list_view.set_vexpand(True) + vbox.append(self._list_view) + self._list_view.set_visible(True) else: self._icon_view = IconView(self._toolbar) self._icon_view.connect('entry-activated', self.__entry_activated_cb) self._icon_view.connect('clear-clicked', self.__clear_clicked_cb) - vbox.pack_start(self._icon_view, True, True, 0) - self._icon_view.show() - - width = Gdk.Screen.width() - style.GRID_CELL_SIZE * 2 - height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 2 - self.set_size_request(width, height) + self._icon_view.set_vexpand(True) + vbox.append(self._icon_view) + self._icon_view.set_visible(True) + + display = Gdk.Display.get_default() + screen_width, screen_height = 800, 600 + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geo = monitors.get_item(0).get_geometry() + screen_width = geo.width - style.GRID_CELL_SIZE * 2 + screen_height = geo.height - style.GRID_CELL_SIZE * 2 + self.set_default_size(screen_width, screen_height) self._toolbar.update_filters('/', what_filter, filter_type) - def __realize_cb(self, chooser, parent): - self.get_window().set_transient_for(parent) - # TODO: Should we disconnect the signal here? + self.set_focus(self._toolbar.search_entry) + self.connect('unmap', self.__visibility_notify_event_cb) + self.connect('map', self.__map_cb) + self.connect('close-request', self.__close_request_cb) + self.connect('realize', self.__realize_cb) + self.connect('unrealize', self.__unrealize_cb) + + def __realize_cb(self, widget): + from jarabe.model import shell as _shell + _shell.get_model().push_modal() - def __window_closed_cb(self, screen, window, parent): - if window.get_xid() == parent.get_xid(): - self.destroy() + def __unrealize_cb(self, widget): + from jarabe.model import shell as _shell + _shell.get_model().pop_modal() def __entry_activated_cb(self, list_view, uid): self._selected_object_id = uid self.emit('response', Gtk.ResponseType.ACCEPT) - def __delete_event_cb(self, chooser, event): - self.emit('response', Gtk.ResponseType.DELETE_EVENT) - - def __key_press_event_cb(self, widget, event): - keyname = Gdk.keyval_name(event.keyval) + def __key_press_event_cb(self, controller, keyval, keycode, state): + keyname = Gdk.keyval_name(keyval) if keyname == 'Escape': self.emit('response', Gtk.ResponseType.DELETE_EVENT) + return True + return False def __close_button_clicked_cb(self, button): self.emit('response', Gtk.ResponseType.DELETE_EVENT) @@ -144,27 +165,35 @@ def __query_changed_cb(self, toolbar, query): else: self._icon_view.update_with_query(query) - def __volume_changed_cb(self, volume_toolbar, mount_point): - logging.debug('Selected volume: %r.', mount_point) - self._toolbar.set_mount_point(mount_point) - def __visibility_notify_event_cb(self, window, event): + def __visibility_notify_event_cb(self, widget): logging.debug('visibility_notify_event_cb %r', self) - visible = event.get_state() == Gdk.VisibilityState.FULLY_OBSCURED + visible = self.get_mapped() if not self._show_preview: self._list_view.set_is_visible(visible) else: self._icon_view.set_is_visible(visible) + def __map_cb(self, widget): + if not self._show_preview: + self._list_view.set_is_visible(True) + else: + self._icon_view.set_is_visible(True) + + def __close_request_cb(self, window): + self.emit('response', Gtk.ResponseType.DELETE_EVENT) + return True + def __clear_clicked_cb(self, list_view): self._toolbar.clear_query() -class TitleBox(VolumesToolbar): +class TitleBox(Gtk.Box): __gtype_name__ = 'TitleBox' def __init__(self, what_filter='', filter_type=None): - VolumesToolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self._children = [] label = Gtk.Label() title = _('Choose an object') @@ -176,23 +205,22 @@ def __init__(self, what_filter='', filter_type=None): bundle.get_name() label.set_markup('%s' % title) - label.set_alignment(0, 0.5) + label.set_halign(Gtk.Align.START) + label.set_valign(Gtk.Align.CENTER) self._add_widget(label, expand=True) self.close_button = ToolButton(icon_name='dialog-cancel') self.close_button.set_tooltip(_('Close')) - self.insert(self.close_button, -1) - self.close_button.show() + self._add_widget(self.close_button) + self.close_button.set_visible(True) def _add_widget(self, widget, expand=False): - tool_item = Gtk.ToolItem() - tool_item.set_expand(expand) + if expand: + widget.set_hexpand(True) - tool_item.add(widget) - widget.show() - - self.insert(tool_item, -1) - tool_item.show() + self.append(widget) + self._children.append(widget) + widget.set_visible(True) class ChooserListView(BaseListView): @@ -209,9 +237,10 @@ def __init__(self, toolbar): self._toolbar = toolbar self.tree_view.props.hover_selection = True - - self.tree_view.connect('button-release-event', - self.__button_release_event_cb) + + click_controller = Gtk.GestureClick() + click_controller.connect('released', self.__button_release_event_cb) + self.tree_view.add_controller(click_controller) def _can_clear_query(self): return self._toolbar.is_filter_changed() @@ -223,16 +252,13 @@ def create_palette(self, x, y): # We don't want show the palette in the object chooser pass - def __button_release_event_cb(self, tree_view, event): - if event.window != tree_view.get_bin_window(): - return False - - pos = tree_view.get_path_at_pos(int(event.x), int(event.y)) + def __button_release_event_cb(self, gesture, n_press, x, y): + pos = self.tree_view.get_path_at_pos(int(x), int(y)) if pos is None: return False path, column_, x_, y_ = pos - uid = tree_view.get_model()[path][ListModel.COLUMN_UID] + uid = self.tree_view.get_model()[path][ListModel.COLUMN_UID] self.emit('entry-activated', uid) return False diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index feb82e4e40..b241a84070 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -24,7 +24,7 @@ from gi.repository import Gtk from gi.repository import Gdk from gi.repository import Gio -#from gi.repository import SugarExt + from sugar4.graphics import style from sugar4.graphics.palette import Palette @@ -47,6 +47,14 @@ PROJECT_BUNDLE_ID = 'org.sugarlabs.Project' +def _popdown_palette_chain(widget): + p = widget.get_parent() + while p: + if hasattr(p, 'popdown'): + p.popdown() + p = p.get_parent() + + class ObjectPalette(Palette): __gtype_name__ = 'ObjectPalette' @@ -90,88 +98,99 @@ def __init__(self, journalactivity, metadata, detail=False): resume_label = _('Start') resume_with_label = _('Start with') menu_item = MenuItem(resume_label, 'activity-start') - menu_item.connect('activate', self.__start_activate_cb) + menu_item.connect('clicked', self.__start_activate_cb) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) menu_item = MenuItem(resume_with_label, 'activity-start') self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) start_with_menu = StartWithMenu(self._metadata) - menu_item.set_submenu(start_with_menu) + start_with_menu.set_parent(menu_item) + start_with_menu.set_position(Gtk.PositionType.RIGHT) + start_with_menu.set_has_arrow(False) + menu_item.connect('clicked', lambda btn, m=start_with_menu: m.popup()) elif metadata.get('activity', None) == PROJECT_BUNDLE_ID: open_label = _('Open') menu_item = MenuItem(open_label, 'project-box') - menu_item.connect('activate', self.__open_project_activate_cb) + menu_item.connect('clicked', self.__open_project_activate_cb) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) else: menu_item = MenuItem(_('No activity to start entry')) menu_item.set_sensitive(False) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) menu_item = MenuItem(_('Copy to')) icon = Icon(icon_name='edit-copy', xo_color=color, pixel_size=style.SMALL_ICON_SIZE) menu_item.set_image(icon) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) copy_menu = CopyMenu(self._journalactivity, self.__get_uid_list_cb) copy_menu.connect('volume-error', self.__volume_error_cb) - menu_item.set_submenu(copy_menu) + copy_menu.set_parent(menu_item) + copy_menu.set_position(Gtk.PositionType.RIGHT) + copy_menu.set_has_arrow(False) + menu_item.connect('clicked', lambda btn, m=copy_menu: m.popup()) if not metadata.get('activity', None) == PROJECT_BUNDLE_ID: menu_item = MenuItem(_('Send to project...'), 'project-box') - menu_item.connect('activate', self.__copy_to_project_activated_cb) + menu_item.connect('clicked', self.__copy_to_project_activated_cb) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) if self._metadata['mountpoint'] == '/': menu_item = MenuItem(_('Duplicate')) icon = Icon(icon_name='edit-duplicate', xo_color=color, pixel_size=style.SMALL_ICON_SIZE) menu_item.set_image(icon) - menu_item.connect('activate', self.__duplicate_activate_cb) + menu_item.connect('clicked', self.__duplicate_activate_cb) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) menu_item = MenuItem(_('Send to'), 'document-send') self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) friends_menu = FriendsMenu() friends_menu.connect('friend-selected', self.__friend_selected_cb) - menu_item.set_submenu(friends_menu) + friends_menu.set_parent(menu_item) + friends_menu.set_position(Gtk.PositionType.RIGHT) + friends_menu.set_has_arrow(False) + menu_item.connect('clicked', lambda btn, m=friends_menu: m.popup()) if detail is True: menu_item = MenuItem(_('View Details'), 'go-right') - menu_item.connect('activate', self.__detail_activate_cb) + menu_item.connect('clicked', self.__detail_activate_cb) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) menu_item = MenuItem(_('Erase'), 'list-remove') - menu_item.connect('activate', self.__erase_activate_cb) + menu_item.connect('clicked', self.__erase_activate_cb) self.menu.append(menu_item) - menu_item.show() + menu_item.set_visible(True) def __get_uid_list_cb(self): return [self._metadata['uid']] def __copy_to_project_activated_cb(self, menu_item): self.emit('choose-project', self._metadata) - self.destroy() + self.popdown() def __open_project_activate_cb(self, menu_item): self._journalactivity.project_view_activated_cb( list_view=None, metadata=self._metadata) + self.popdown() def __start_activate_cb(self, menu_item): misc.resume(self._metadata, alert_window=journalwindow.get_journal_window()) + self.popdown() def __duplicate_activate_cb(self, menu_item): try: @@ -181,6 +200,7 @@ def __duplicate_activate_cb(self, menu_item): self.emit('volume-error', _('Error while copying the entry. %s') % e.strerror, _('Error')) + self.popdown() def __erase_activate_cb(self, menu_item): alert = Alert() @@ -190,13 +210,14 @@ def __erase_activate_cb(self, menu_item): % self._metadata['title'] icon = Icon(icon_name='dialog-cancel') alert.add_button(Gtk.ResponseType.CANCEL, _('Cancel'), icon) - icon.show() + icon.set_visible(True) ok_icon = Icon(icon_name='dialog-ok') alert.add_button(Gtk.ResponseType.OK, erase_string, ok_icon) - ok_icon.show() + ok_icon.set_visible(True) alert.connect('response', self.__erase_alert_response_cb) journalwindow.get_journal_window().add_alert(alert) - alert.show() + alert.set_visible(True) + self.popdown() def __erase_alert_response_cb(self, alert, response_id): journalwindow.get_journal_window().remove_alert(alert) @@ -209,6 +230,7 @@ def __erase_alert_response_cb(self, alert, response_id): def __detail_activate_cb(self, menu_item): self.emit('detail-clicked', self._metadata['uid']) + self.popdown() def __volume_error_cb(self, menu_item, message, severity): self.emit('volume-error', message, severity) @@ -233,6 +255,7 @@ def __friend_selected_cb(self, menu_item, buddy): filetransfer.start_transfer(buddy, file_name, title, description, mime_type) + self.popdown() def popup(self, immediate=False, state=None): if self._journalactivity.get_list_view().is_dragging(): @@ -241,7 +264,7 @@ def popup(self, immediate=False, state=None): Palette.popup(self, immediate) -class CopyMenu(Gtk.PopoverMenu): +class CopyMenu(Gtk.Popover): __gtype_name__ = 'JournalCopyMenu' __gsignals__ = { @@ -250,7 +273,12 @@ class CopyMenu(Gtk.PopoverMenu): } def __init__(self, journalactivity, get_uid_list_cb): - Gtk.PopoverMenu.__init__(self) + Gtk.Popover.__init__(self) + self.add_css_class('palette-popover') + self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.set_child(self._vbox) + self._vbox.set_visible(True) + CopyMenuBuilder(journalactivity, get_uid_list_cb, self.__volume_error_cb, self) @@ -276,44 +304,38 @@ def __init__(self, journalactivity, get_uid_list_cb, __volume_error_cb, self._create_menu_items() def _create_menu_items(self): - self.g_menu = Gio.Menu.new() - application = Gio.Application.get_default() + vbox = getattr(self._menu, '_vbox', self._menu) + if self._add_clipboard_menu: - copy_action = Gio.SimpleAction.new("copy_clipboard", None) - application.add_action(copy_action) clipboard_menu = ClipboardMenu(self._get_uid_list_cb) clipboard_menu.set_image(Icon(icon_name='toolbar-edit', pixel_size=style.SMALL_ICON_SIZE)) clipboard_menu.connect('volume-error', self.__volume_error_cb) - self.g_menu.append_item(clipboard_menu) - clipboard_menu.show() + vbox.append(clipboard_menu) + clipboard_menu.set_visible(True) if self._journalactivity.get_mount_point() != '/': color = profile.get_color() - volume_action = Gio.SimpleAction.new("copy_volume", None) - application.add_action(volume_action) journal_menu = VolumeMenu(self._journalactivity, self._get_uid_list_cb, _('Journal'), '/') journal_menu.set_image(Icon(icon_name='activity-journal', xo_color=color, pixel_size=style.SMALL_ICON_SIZE)) journal_menu.connect('volume-error', self.__volume_error_cb) - self.g_menu.append_item(journal_menu) - journal_menu.show() + vbox.append(journal_menu) + journal_menu.set_visible(True) documents_path = model.get_documents_path() if documents_path is not None and \ self._journalactivity.get_mount_point() != documents_path: - volume_action = Gio.SimpleAction.new("copy_volume", None) - application.add_action(volume_action) documents_menu = VolumeMenu(self._journalactivity, self._get_uid_list_cb, _('Documents'), documents_path) documents_menu.set_image(Icon(icon_name='user-documents', pixel_size=style.SMALL_ICON_SIZE)) documents_menu.connect('volume-error', self.__volume_error_cb) - self.g_menu.append_item(documents_menu) - documents_menu.show() + vbox.append(documents_menu) + documents_menu.set_visible(True) volume_monitor = Gio.VolumeMonitor.get() self._volumes = {} @@ -331,14 +353,15 @@ def _create_menu_items(self): if hasattr(account, 'get_shared_journal_entry'): entry = account.get_shared_journal_entry() if hasattr(entry, 'get_share_menu'): - self.g_menu.append(entry.get_share_menu( - self._get_uid_list_cb), None) - self._menu.set_menu_model(self.g_menu) + menu_item = entry.get_share_menu(self._get_uid_list_cb) + if menu_item: + vbox.append(menu_item) + menu_item.set_visible(True) def update_mount_point(self): - for menu_item in self._menu.get_children(): - if isinstance(menu_item, MenuItem): - self._menu.remove(menu_item) + vbox = getattr(self._menu, '_vbox', self._menu) + while vbox.get_first_child(): + vbox.get_first_child().unparent() self._create_menu_items() def __mount_added_cb(self, volume_monitor, mount): @@ -353,20 +376,21 @@ def _add_mount(self, mount): volume_menu = VolumeMenu(self._journalactivity, self._get_uid_list_cb, mount.get_name(), mount.get_root().get_path()) - icon_name = misc.get_mount_icon_name(mount, Gtk.IconSize.MENU) + icon_name = misc.get_mount_icon_name(mount, Gtk.IconSize.NORMAL) icon = Icon(pixel_size=style.SMALL_ICON_SIZE, icon_name=icon_name, xo_color=get_mount_color(mount)) volume_menu.set_image(icon) volume_menu.connect('volume-error', self.__volume_error_cb) - self._menu.append(volume_menu) + vbox = getattr(self._menu, '_vbox', self._menu) + vbox.append(volume_menu) self._volumes[mount.get_root().get_path()] = volume_menu - volume_menu.show() + volume_menu.set_visible(True) def __mount_removed_cb(self, volume_monitor, mount): volume_menu = self._volumes[mount.get_root().get_path()] - self._menu.remove(volume_menu) + volume_menu.unparent() del self._volumes[mount.get_root().get_path()] @@ -383,10 +407,10 @@ def __init__(self, journalactivity, get_uid_list_cb, label, mount_point): self._get_uid_list_cb = get_uid_list_cb self._journalactivity = journalactivity self._mount_point = mount_point - self.set_detailed_action("copy_volume") - self.connect('activate', self.__copy_to_volume_cb) + self.connect('clicked', self.__copy_to_volume_cb) def __copy_to_volume_cb(self, menu_item): + self._close_menus() uid_list = self._get_uid_list_cb() if len(uid_list) == 1: uid = uid_list[0] @@ -430,6 +454,9 @@ def _perform_copy(self, metadata): logging.exception('Error while copying the entry. %s', e.strerror) + def _close_menus(self): + _popdown_palette_chain(self) + class ClipboardMenu(MenuItem): __gtype_name__ = 'JournalClipboardMenu' @@ -441,35 +468,34 @@ class ClipboardMenu(MenuItem): def __init__(self, get_uid_list_cb): MenuItem.__init__(self, _('Clipboard')) - self.set_detailed_action("copy_clipboard") - self._temp_file_path = None self._get_uid_list_cb = get_uid_list_cb - self.connect('activate', self.__copy_to_clipboard_cb) + self.connect('clicked', self.__copy_to_clipboard_cb) def __copy_to_clipboard_cb(self, menu_item): - clipboard = Gtk.Display.get_default().get_clipboard() + self._close_menus() + clipboard = Gdk.Display.get_default().get_clipboard() uid_list = self._get_uid_list_cb() if len(uid_list) == 1: uid = uid_list[0] - file_path = 'file://' + model.get_file(uid) - if not file_path or not os.path.exists(file_path): + real_path = model.get_file(uid) + if not real_path or not os.path.exists(real_path): logging.warn('Entries without a file cannot be copied.') self.emit('volume-error', _('Entries without a file cannot be copied.'), _('Warning')) return - try: - value = GObject.Value(GObject.TYPE_STRING, file_path) - clipboard.set(value) - except: - SugarExt.clipboard_set_with_data( - 'text/uri-list', - file_path) + uri = GLib.filename_to_uri(real_path, None) + '\r\n' + bytes_data = GLib.Bytes.new(uri.encode('utf-8')) + provider = Gdk.ContentProvider.new_for_bytes('text/uri-list', bytes_data) + clipboard.set_content(provider) + + def _close_menus(self): + _popdown_palette_chain(self) -class FriendsMenu(Gtk.PopoverMenu): +class FriendsMenu(Gtk.Popover): __gtype_name__ = 'JournalFriendsMenu' __gsignals__ = { @@ -478,9 +504,13 @@ class FriendsMenu(Gtk.PopoverMenu): } def __init__(self): - Gtk.PopoverMenu.__init__(self) + Gtk.Popover.__init__(self) + self.add_css_class('palette-popover') + + self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.set_child(self._vbox) + self._vbox.set_visible(True) - self.g_menu = Gio.Menu.new() if filetransfer.file_transfer_available(): friends_model = friends.get_model() for friend in friends_model: @@ -488,57 +518,60 @@ def __init__(self): menu_item = MenuItem(text_label=friend.get_nick(), icon_name='computer-xo', xo_color=friend.get_color()) - menu_item.connect('activate', self.__item_activate_cb, + menu_item.connect('clicked', self.__item_activate_cb, friend) - self.g_menu.append_item(menu_item) - menu_item.show() + self._vbox.append(menu_item) + menu_item.set_visible(True) - if not self.g_menu.get_n_items(): + if not self._vbox.get_first_child(): menu_item = MenuItem(_('No friends present')) menu_item.set_sensitive(False) - self.g_menu.append(menu_item) - menu_item.show() + self._vbox.append(menu_item) + menu_item.set_visible(True) else: menu_item = MenuItem(_('No valid connection found')) menu_item.set_sensitive(False) - self.g_menu.append(menu_item) - menu_item.show() - - self.set_menu_model(self.g_menu) + self._vbox.append(menu_item) + menu_item.set_visible(True) def __item_activate_cb(self, menu_item, friend): self.emit('friend-selected', friend) + self._close_menus() + def _close_menus(self): + _popdown_palette_chain(self) -class StartWithMenu(Gtk.PopoverMenu): + +class StartWithMenu(Gtk.Popover): __gtype_name__ = 'JournalStartWithMenu' def __init__(self, metadata): - Gtk.PopoverMenu.__init__(self) + Gtk.Popover.__init__(self) + self.add_css_class('palette-popover') self._metadata = metadata - self.g_menu = Gio.Menu.new() + self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.set_child(self._vbox) + self._vbox.set_visible(True) + for activity_info in misc.get_activities(metadata): - menu_item = MenuItem(activity_info.get_name()) - menu_item.set_image(Icon(file=activity_info.get_icon(), - pixel_size=style.SMALL_ICON_SIZE)) - menu_item.connect('activate', self.__item_activate_cb, + menu_item = MenuItem(text_label=activity_info.get_name(), + file_name=activity_info.get_icon()) + menu_item.connect('clicked', self.__item_activate_cb, activity_info.get_bundle_id()) - self.g_menu.append_item(menu_item) - menu_item.show() + self._vbox.append(menu_item) + menu_item.set_visible(True) - if not self.g_menu.get_n_items(): + if not self._vbox.get_first_child(): if metadata.get('activity_id', ''): resume_label = _('No activity to resume entry') else: resume_label = _('No activity to start entry') menu_item = MenuItem(resume_label) menu_item.set_sensitive(False) - self.append_item(menu_item) - menu_item.show() - - self.set_menu_model(self.g_menu) + self._vbox.append(menu_item) + menu_item.set_visible(True) def __item_activate_cb(self, menu_item, service_name): mime_type = self._metadata.get('mime_type', '') @@ -547,6 +580,10 @@ def __item_activate_cb(self, menu_item, service_name): mime_registry.set_default_activity(mime_type, service_name) misc.resume(self._metadata, bundle_id=service_name, alert_window=journalwindow.get_journal_window()) + self._close_menus() + + def _close_menus(self): + _popdown_palette_chain(self) class BuddyPalette(Palette): @@ -579,7 +616,7 @@ def __init__(self, journalactivity, uid_list, alert_title, alert_message, operation_cb): - GObject.GObject.__init__(self) + super().__init__() self._journalactivity = journalactivity @@ -603,17 +640,17 @@ def __show_confirmation_alert_internal(self): stop_icon = Icon(icon_name='dialog-cancel') self._confirmation_alert.add_button(Gtk.ResponseType.CANCEL, _('Stop'), stop_icon) - stop_icon.show() + stop_icon.set_visible(True) ok_icon = Icon(icon_name='dialog-ok') self._confirmation_alert.add_button(Gtk.ResponseType.OK, _('Continue'), ok_icon) - ok_icon.show() + ok_icon.set_visible(True) self._journalactivity.add_alert(self._confirmation_alert) self._confirmation_alert.connect('response', self.__confirmation_response_cb) - self._confirmation_alert.show() + self._confirmation_alert.set_visible(True) def __confirmation_response_cb(self, alert, response): if response == Gtk.ResponseType.CANCEL: diff --git a/src/jarabe/journal/projectview.py b/src/jarabe/journal/projectview.py index afd2751b5e..9dd5a8416b 100644 --- a/src/jarabe/journal/projectview.py +++ b/src/jarabe/journal/projectview.py @@ -16,8 +16,10 @@ from gettext import gettext as _ -from gi.repository import GObject from gi.repository import Gtk +from gi.repository import GObject +from gi.repository import Pango +from gi.repository import Gdk from gi.repository import Gio from jarabe.journal.expandedentry import TextView @@ -35,38 +37,45 @@ _SERVICE_INTERFACE = 'org.laptop.Activity' -class ProjectView(Gtk.EventBox, BaseExpandedEntry): +class ProjectView(Gtk.Box, BaseExpandedEntry): __gsignals__ = { 'go-back-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])), } def __init__(self, **kwargs): - Gtk.EventBox.__init__(self) + super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs) BaseExpandedEntry.__init__(self) self.project_metadata = None self._service = None self._activity_id = None self._project = None - self.modify_bg(Gtk.StateType.NORMAL, style.COLOR_WHITE.get_gdk_color()) + + self.add_css_class('project-view-bg') self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.add(self._vbox) + self.append(self._vbox) back_bar = BackBar() - back_bar.connect('button-release-event', - self.__back_bar_release_event_cb) - self._vbox.pack_start(back_bar, False, True, 0) + click = Gtk.GestureClick() + click.connect('released', self.__back_bar_release_event_cb) + back_bar.add_controller(click) + self._vbox.append(back_bar) header = self.create_header() - self._vbox.pack_start(header, False, False, style.DEFAULT_SPACING * 2) - header.show() + header.set_margin_bottom(style.DEFAULT_SPACING * 2) + self._vbox.append(header) + header.set_visible(True) description_box, self._description = self._create_description() - self._vbox.pack_start(description_box, False, True, - style.DEFAULT_SPACING / 3) + description_box.set_vexpand(True) + description_box.set_margin_bottom(int(style.DEFAULT_SPACING / 3)) + self._vbox.append(description_box) - self._title.connect('focus-out-event', self._title_focus_out_event_cb) + # Gtk.Entry doesn't have focus-out-event, we use Gtk.EventControllerFocus + focus_controller = Gtk.EventControllerFocus() + focus_controller.connect('leave', self._title_focus_out_event_cb) + self._title.add_controller(focus_controller) settings = Gio.Settings.new('org.sugarlabs.user') icon_color = settings.get_string('color') @@ -74,7 +83,7 @@ def __init__(self, **kwargs): self._icon = Icon(icon_name='project-box', pixel_size=style.MEDIUM_ICON_SIZE) self._icon.xo_color = XoColor(icon_color) - self._icon_box.pack_start(self._icon, False, False, 0) + self._icon_box.append(self._icon) def get_vbox(self): return self._vbox @@ -89,7 +98,7 @@ def get_list_view(self): def get_mount_point(self): return '/' - def __back_bar_release_event_cb(self, back_bar, event): + def __back_bar_release_event_cb(self, gesture, n_press, x, y): self.emit('go-back-clicked') return False @@ -97,19 +106,20 @@ def set_project_metadata(self, project_metadata): self.project_metadata = project_metadata description = project_metadata.get('description', '') - self._description.get_buffer().set_text(description) + self._description.get_buffer().set_text(description, len(description)) self._title.set_text(project_metadata.get('title', '')) - def _title_focus_out_event_cb(self, entry, event): + def _title_focus_out_event_cb(self, controller): self._update_entry() def _create_description(self): widget = TextView() - widget.connect('focus-out-event', - self._description_tags_focus_out_event_cb) + focus_controller = Gtk.EventControllerFocus() + focus_controller.connect('leave', self._description_tags_focus_out_event_cb) + widget.add_controller(focus_controller) return self._create_scrollable(widget, label=_('Description:')), widget - def _description_tags_focus_out_event_cb(self, text_view, event): + def _description_tags_focus_out_event_cb(self, controller): self._update_entry() def _update_entry(self): @@ -117,7 +127,7 @@ def _update_entry(self): bounds = self._description.get_buffer().get_bounds() old_description = self.project_metadata.get('description', None) new_description = self._description.get_buffer().get_text( - bounds[0], bounds[1], include_hidden_chars=False) + bounds[0], bounds[1], False) if old_description != new_description: self.project_metadata['description'] = new_description @@ -132,22 +142,24 @@ def _update_entry(self): def _create_scrollable(self, widget, label=None): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.props.spacing = style.DEFAULT_SPACING + vbox.set_spacing(style.DEFAULT_SPACING) if label is not None: text = Gtk.Label() text.set_markup('%s' % ( style.COLOR_BUTTON_GREY.get_html(), label)) - halign = Gtk.Alignment.new(0, 0, 0, 0) - halign.add(text) - vbox.pack_start(halign, False, False, 0) + halign = Gtk.Box() + halign.set_halign(Gtk.Align.START) + halign.append(text) + vbox.append(halign) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN) - scrolled_window.add(widget) - vbox.pack_start(scrolled_window, True, True, 0) + scrolled_window.set_has_frame(True) + scrolled_window.set_child(widget) + scrolled_window.set_vexpand(True) + vbox.append(scrolled_window) return vbox diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index 04a3336cb3..615a8b2552 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, 2011, One Laptop Per Child +# Copyright (C) 2007, 2011, One Laptop Per Child # Copyright (C) 2014, Ignacio Rodriguez # # This program is free software: you can redistribute it and/or modify @@ -40,7 +40,6 @@ from jarabe.journal.misc import get_mount_color from jarabe.view.palettes import VolumePalette - _JOURNAL_0_METADATA_DIR = '.olpc.store' @@ -152,7 +151,7 @@ def _convert_entry(root, document): metadata_fname) if not os.path.exists(metadata_path): (fh, fn) = tempfile.mkstemp(dir=root) - os.write(fh, json.dumps(metadata)) + os.write(fh, json.dumps(metadata).encode('utf-8')) os.close(fh) os.rename(fn, metadata_path) @@ -161,7 +160,7 @@ def _convert_entry(root, document): os.path.join(root, filename), metadata) -class VolumesToolbar(Gtk.Toolbar): +class VolumesToolbar(Gtk.Box): __gtype_name__ = 'VolumesToolbar' __gsignals__ = { @@ -172,24 +171,43 @@ class VolumesToolbar(Gtk.Toolbar): } def __init__(self): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) self._mount_added_hid = None self._mount_removed_hid = None + self._children = [] button = JournalButton() button.connect('toggled', self._button_toggled_cb) - self.insert(button, 0) - button.show() + self.append(button) + self._children.append(button) + button.set_visible(True) self._volume_buttons = [button] - self.connect('destroy', self.__destroy_cb) - GLib.idle_add(self._set_up_volumes) - def __destroy_cb(self, widget): + def insert(self, widget, position): + if position == -1 or position >= len(self._children): + self.append(widget) + self._children.append(widget) + elif position == 0: + self.prepend(widget) + self._children.insert(0, widget) + else: + sibling = self._children[position - 1] + self.insert_child_after(widget, sibling) + self._children.insert(position, widget) + + def get_item_index(self, widget): + if widget in self._children: + return self._children.index(widget) + return -1 + + def do_unroot(self): volume_monitor = Gio.VolumeMonitor.get() - volume_monitor.disconnect(self._mount_added_hid) - volume_monitor.disconnect(self._mount_removed_hid) + if self._mount_added_hid: + volume_monitor.disconnect(self._mount_added_hid) + if self._mount_removed_hid: + volume_monitor.disconnect(self._mount_removed_hid) def _set_up_volumes(self): self._set_up_documents_button() @@ -208,15 +226,15 @@ def _set_up_documents_button(self): documents_path = model.get_documents_path() if documents_path is not None: button = DocumentsButton(documents_path) - button.props.group = self._volume_buttons[0] + button.set_group(self._volume_buttons[0]) button.set_palette(Palette(_('Documents'))) button.connect('toggled', self._button_toggled_cb) - button.show() + button.set_visible(True) position = self.get_item_index(self._volume_buttons[-1]) + 1 self.insert(button, position) self._volume_buttons.append(button) - self.show() + self.set_visible(True) def __mount_added_cb(self, volume_monitor, mount): self._add_button(mount) @@ -227,23 +245,27 @@ def __mount_removed_cb(self, volume_monitor, mount): def _add_button(self, mount): logging.debug('VolumeToolbar._add_button: %r', mount.get_name()) + path = mount.get_root().get_path() + if path and (path.startswith('/snap/') or path.startswith('/run/')): + return + if os.path.exists(os.path.join(mount.get_root().get_path(), _JOURNAL_0_METADATA_DIR)): logging.debug('Convert DS-0 Journal entries: starting conversion') GLib.idle_add(_convert_entries, mount.get_root().get_path()) button = VolumeButton(mount) - button.props.group = self._volume_buttons[0] + button.set_group(self._volume_buttons[0]) button.connect('toggled', self._button_toggled_cb) button.connect('volume-error', self.__volume_error_cb) position = self.get_item_index(self._volume_buttons[-1]) + 1 self.insert(button, position) - button.show() + button.set_visible(True) self._volume_buttons.append(button) - if len(self.get_children()) > 1: - self.show() + if len(self._children) > 1: + self.set_visible(True) def __volume_error_cb(self, button, strerror, severity): self.emit('volume-error', strerror, severity) @@ -254,24 +276,29 @@ def _button_toggled_cb(self, button): def _get_button_for_mount(self, mount): mount_point = mount.get_root().get_path() - for button in self.get_children(): - if button.mount_point == mount_point: + for button in self._children: + if getattr(button, 'mount_point', None) == mount_point: return button logging.error('Couldnt find button with mount_point %r', mount_point) return None def _remove_button(self, mount): button = self._get_button_for_mount(mount) - self._volume_buttons.remove(button) - self.remove(button) - self.get_children()[0].props.active = True + if button: + self._volume_buttons.remove(button) + self.remove(button) + self._children.remove(button) + if self._children: + # We expect the first button to be JournalButton which can be activated + self._children[0].props.active = True - if len(self.get_children()) < 2: - self.hide() + if len(self._children) < 2: + self.set_visible(False) def set_active_volume(self, mount): button = self._get_button_for_mount(mount) - button.props.active = True + if button: + button.props.active = True class BaseButton(RadioToolButton): @@ -281,18 +308,19 @@ class BaseButton(RadioToolButton): } def __init__(self, mount_point): - RadioToolButton.__init__(self) + super().__init__() self.mount_point = mount_point - self.drag_dest_set(Gtk.DestDefaults.ALL, - [Gtk.TargetEntry.new('journal-object-id', 0, 0)], - Gdk.DragAction.COPY) - self.connect('drag-data-received', self._drag_data_received_cb) + self._drop_target = Gtk.DropTarget.new(GObject.TYPE_STRING, Gdk.DragAction.COPY) + self._drop_target.connect('drop', self._drag_data_received_cb) + self.add_controller(self._drop_target) - def _drag_data_received_cb(self, widget, drag_context, x, y, - selection_data, info, timestamp): - object_id = selection_data.get_data() + def _drag_data_received_cb(self, drop_target, value, x, y): + object_id = value + if not object_id: + return False + metadata = model.get(object_id) file_path = model.get_file(metadata['uid']) if not file_path or not os.path.exists(file_path): @@ -300,15 +328,17 @@ def _drag_data_received_cb(self, widget, drag_context, x, y, self.emit('volume-error', _('Entries without a file cannot be copied.'), _('Warning')) - return + return False try: model.copy(metadata, self.mount_point) + return True except IOError as e: logging.exception('Error while copying the entry. %s', e.strerror) self.emit('volume-error', _('Error while copying the entry. %s') % e.strerror, _('Error')) + return False class VolumeButton(BaseButton): @@ -318,15 +348,12 @@ def __init__(self, mount): mount_point = mount.get_root().get_path() BaseButton.__init__(self, mount_point) - self.props.icon_name = get_mount_icon_name(mount, - Gtk.IconSize.LARGE_TOOLBAR) - # TODO: retrieve the colors from the owner of the device - self.props.xo_color = get_mount_color(self._mount) + self.props.icon_name = get_mount_icon_name(mount, 24) + if self.get_icon_widget(): + self.get_icon_widget().props.xo_color = get_mount_color(self._mount) def create_palette(self): palette = VolumePalette(self._mount) - # palette.props.invoker = FrameWidgetInvoker(self) - # palette.set_group_id('frame') return palette @@ -336,7 +363,8 @@ def __init__(self): BaseButton.__init__(self, mount_point='/') self.props.icon_name = 'activity-journal' - self.props.xo_color = profile.get_color() + if self.get_icon_widget(): + self.get_icon_widget().props.xo_color = profile.get_color() def create_palette(self): palette = JournalButtonPalette(self) @@ -349,19 +377,23 @@ def __init__(self, mount): Palette.__init__(self, _('Journal')) grid = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL, - margin=style.DEFAULT_SPACING, + margin_top=style.DEFAULT_SPACING, + margin_bottom=style.DEFAULT_SPACING, + margin_start=style.DEFAULT_SPACING, + margin_end=style.DEFAULT_SPACING, row_spacing=style.DEFAULT_SPACING) self.set_content(grid) - grid.show() + grid.set_visible(True) self._progress_bar = Gtk.ProgressBar() - grid.add(self._progress_bar) - self._progress_bar.show() + grid.attach(self._progress_bar, 0, 0, 1, 1) + self._progress_bar.set_visible(True) self._free_space_label = Gtk.Label() - self._free_space_label.set_alignment(0.5, 0.5) - grid.add(self._free_space_label) - self._free_space_label.show() + self._free_space_label.set_halign(Gtk.Align.CENTER) + self._free_space_label.set_valign(Gtk.Align.CENTER) + grid.attach(self._free_space_label, 0, 1, 1, 1) + self._free_space_label.set_visible(True) self.connect('popup', self.__popup_cb) @@ -382,4 +414,5 @@ def __init__(self, documents_path): BaseButton.__init__(self, mount_point=documents_path) self.props.icon_name = 'user-documents' - self.props.xo_color = profile.get_color() + if self.get_icon_widget(): + self.get_icon_widget().props.xo_color = profile.get_color() diff --git a/src/jarabe/main.py b/src/jarabe/main.py index 8bfca3e024..66c6dde555 100755 --- a/src/jarabe/main.py +++ b/src/jarabe/main.py @@ -16,8 +16,8 @@ from sugar4 import logger -logger.cleanup() -logger.start('shell') +#logger.cleanup() +#logger.start('shell') import logging @@ -25,7 +25,6 @@ import os import sys -import subprocess import shutil # Disable overlay scrolling before GTK is loaded @@ -34,28 +33,29 @@ import gettext from jarabe import config -# NOTE: This needs to happen early because some modules register -# translatable strings in the module scope. +# must happen early; some modules register translatable strings at import time gettext.bindtextdomain('sugar', config.locale_path) -gettext.bindtextdomain('sugar-toolkit-gtk3', config.locale_path) +gettext.bindtextdomain('sugar-toolkit-gtk4', config.locale_path) gettext.textdomain('sugar') -# publish sugar version in the environment os.environ['SUGAR_VERSION'] = config.version from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) -# define the versions of used libraries that are required import gi gi.require_version('Gtk', '4.0') -gi.require_version('Gst', '1.0') -gi.require_version('SugarExt', '2.0') +try: + gi.require_version('Gst', '1.0') + from gi.repository import Gst + HAS_GST = True +except ValueError: + HAS_GST = False + logging.warning("Gst 1.0 not available. Running without GStreamer.") from gi.repository import Gio from gi.repository import GLib from gi.repository import Gtk -from gi.repository import Gst from sugar4 import env @@ -82,8 +82,6 @@ from jarabe import testrunner from jarabe.model import brightness - -_mutter_process = None _window_manager_started = False _starting_desktop = False @@ -129,8 +127,7 @@ def setup_file_transfer_cb(): def _complete_desktop_startup(): launcher.setup() - GLib.idle_add(setup_frame_cb) - GLib.idle_add(setup_keyhandler_cb) + setup_frame_cb() GLib.idle_add(setup_gesturehandler_cb) GLib.idle_add(setup_journal_cb) GLib.idle_add(setup_notification_service_cb) @@ -142,66 +139,46 @@ def _complete_desktop_startup(): testrunner.check_environment() -def __window_manager_failed_cb(fd, condition): - logging.error('window manager did fail, restarting') - GLib.source_remove(_weston_sid) - GLib.timeout_add(1000, _restart_window_manager) - return False - - -def _restart_window_manager(): - global _mutter_process, _mutter_sid - - #_mutter_process = subprocess.Popen( - # ['mutter', '--wayland'], - _mutter_process = subprocess.Popen( - ['dbus-run-session', 'mutter', '--wayland'], - stdout=subprocess.PIPE) - _mutter_sid = GLib.io_add_watch(_mutter_process.stdout, GLib.IO_HUP, - __window_manager_failed_cb) - return False - - def _start_window_manager(): global _cursor_theme_settings, _cursor_theme _cursor_theme_settings = Gio.Settings.new('org.gnome.desktop.interface') _cursor_theme = _cursor_theme_settings.get_string('cursor-theme') - _cursor_theme_settings.set_string('cursor-theme', 'sugar') - - #_restart_window_manager() - global _mutter_process, _mutter_sid - - _mutter_process = subprocess.Popen( - ['mutter', '--wayland'], - stdout=subprocess.PIPE) - _mutter_sid = GLib.io_add_watch(_mutter_process.stdout, GLib.IO_HUP, - __window_manager_failed_cb) global _starting_desktop if _starting_desktop: _complete_desktop_startup() -def _stop_window_manager(): - _cursor_theme_settings.set_string('cursor-theme', _cursor_theme) - _mutter_process.terminate() - - -def _begin_desktop_startup(shell): - logging.warning("Running begin desktop") +def _begin_desktop_startup(): + from jarabe.model import shell as shell_model global _starting_desktop _starting_desktop = True - UIService() + shell_instance = shell_model.get_model() + _setup_main_window(shell_instance) + home_window = homewindow.get_instance() - shell.add_window(home_window) - home_window.show() + + setup_keyhandler_cb() + + shell_instance.stack.add_named(home_window, "home") + shell_instance.stack.add_named(shell_instance.compositor, "activity") + shell_instance.stack.set_visible_child_name("home") + + os.environ["WAYLAND_DISPLAY"] = "wayland-sugar" + if "DISPLAY" in os.environ: + del os.environ["DISPLAY"] session_manager = get_session_manager() - session_manager.start() + + _complete_desktop_startup() + + # Clear the startup 'wait' cursor now that the desktop is ready. + # busy() was called in HomeWindow.__init__; unbusy() matches it. + GLib.idle_add(home_window.unbusy) def __intro_window_done_cb(window): @@ -214,14 +191,13 @@ def __intro_window_done_cb(window): def cleanup_temporary_files(): try: - # Remove temporary files. See http://bugs.sugarlabs.org/ticket/1876 + # see http://bugs.sugarlabs.org/ticket/1876 data_dir = os.path.join(env.get_profile_path(), 'data') shutil.rmtree(data_dir, ignore_errors=True) os.makedirs(data_dir) except OSError as e: - # temporary files cleanup is not critical; it should not prevent - # sugar from starting if (for example) the disk is full or read-only. - print('temporary files cleanup failed: %s' % e) + # non-fatal: full or read-only disk should not block startup + logging.warning('temporary files cleanup failed: %s', e) def setup_timezone(): @@ -236,8 +212,8 @@ def setup_fonts(): face = settings.get_string('default-face') size = settings.get_double('default-size') - settings = Gio.Settings.new('org.gnome.desktop.interface') - settings.set_string("font-name", "%s %f" % (face, size)) + gtk_settings = Gtk.Settings.get_default() + gtk_settings.set_property('gtk-font-name', '%s %f' % (face, size)) def setup_proxy(): @@ -279,23 +255,45 @@ def setup_proxy(): def setup_theme(): - settings = Gio.Settings.new('org.gnome.desktop.interface') + from gi.repository import Gdk + gtk_settings = Gtk.Settings.get_default() sugar_theme = 'sugar-72' if 'SUGAR_SCALING' in os.environ: if os.environ['SUGAR_SCALING'] == '100': sugar_theme = 'sugar-100' - settings.set_string('gtk-theme', sugar_theme) - settings.set_string('icon-theme', 'sugar') - settings.set_int('cursor-blink-timeout', 3) - #settings.set_property('gtk-button-images', True) + gtk_settings.set_property('gtk-theme-name', sugar_theme) + gtk_settings.set_property('gtk-icon-theme-name', 'sugar') + gtk_settings.set_property('gtk-cursor-blink-timeout', 3) + gtk_settings.set_property('gtk-overlay-scrolling', False) + + icons_path = os.path.join(config.data_path, 'icons') + Gtk.IconTheme.get_for_display( + Gdk.Display.get_default()).add_search_path(icons_path) + + +def _setup_main_window(shell_instance): + if not shell_instance._main_window: + shell_instance._main_window = Gtk.ApplicationWindow(application=shell_instance) + shell_instance._main_window.set_title("Sugar") + + shell_instance._overlay = Gtk.Overlay() + shell_instance._overlay.set_child(shell_instance.stack) + + shell_instance._main_window.set_child(shell_instance._overlay) + if os.environ.get('SUGAR_WINDOWED', '0') == '1': + shell_instance._main_window.set_default_size(1024, 768) + else: + shell_instance._main_window.fullscreen() + shell_instance._main_window.present() def _start_intro(shell, start_on_age_page=False): - window = IntroWindow(start_on_age_page=start_on_age_page) + _setup_main_window(shell) + intro_box = IntroWindow(start_on_age_page=start_on_age_page) - shell.add_window(window) - window.connect('done', __intro_window_done_cb) - window.show() + shell.stack.add_named(intro_box, "intro") + shell.stack.set_visible_child_name("intro") + intro_box.connect('done', __intro_window_done_cb) def _check_profile(): @@ -316,7 +314,22 @@ def _check_group_label(): def main(shell): logging.warning("Running main") - Gst.init(sys.argv) + + # Create default profile directories if they do not exist + profile_path = env.get_profile_path() + for subdir in ['datastore', 'logs', 'data']: + path = os.path.join(profile_path, subdir) + if not os.path.exists(path): + try: + os.makedirs(path, 0o770) + except OSError as e: + logging.error('Failed to create directory %s: %s', path, e) + + import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) + + if HAS_GST: + Gst.init(sys.argv) cleanup_temporary_files() @@ -327,32 +340,30 @@ def main(shell): setup_theme() setup_proxy() - # this must be added early, so that it executes and unfreezes the screen - # even when we initially get blocked on the intro screen + # must run early so the screen unfreezes even if blocked on intro GLib.idle_add(unfreeze_screen_cb) - if not _check_profile(): - _start_intro(shell) - elif not _check_group_label(): - _start_intro(shell, start_on_age_page=True) - else: - _begin_desktop_startup() - GLib.idle_add(setup_cursortracker_cb) sound.restore() - #keyboard.setup() brightness.get_instance() sys.path.append(config.ext_path) - try: - Gtk.main() - except KeyboardInterrupt: - print('Ctrl+C pressed, exiting...') + if not _check_profile(): + _start_intro(shell) + elif not _check_group_label(): + _start_intro(shell, start_on_age_page=True) + else: + _begin_desktop_startup() - _stop_window_manager() +shell = shell.get_model() +from sugar4.activity import activityfactory +def _get_compositor_fd(): + if shell.compositor is not None: + return shell.compositor.get_client_socket_fd() + return -1 +activityfactory.set_compositor_fd_getter(_get_compositor_fd) -shell = shell.get_model() shell.connect('activate', main) shell.run(None) diff --git a/src/jarabe/model/adhoc.py b/src/jarabe/model/adhoc.py index 78ca8d1ed2..473679c139 100644 --- a/src/jarabe/model/adhoc.py +++ b/src/jarabe/model/adhoc.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010 One Laptop per Child +# Copyright (C) 2010 One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,6 @@ from jarabe.model.network import Settings from jarabe.model.network import IP4Config - _adhoc_manager_instance = None @@ -57,7 +56,7 @@ class AdHocManager(GObject.GObject): _CHANNEL_11 = 11 def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._bus = dbus.SystemBus() self._device = None diff --git a/src/jarabe/model/brightness.py b/src/jarabe/model/brightness.py index e046d4f1b4..dde19fd92f 100644 --- a/src/jarabe/model/brightness.py +++ b/src/jarabe/model/brightness.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Martin Abente Lahaye +# Copyright (C) 2015 Martin Abente Lahaye # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ from gi.repository import Gio from gi.repository import GObject - _instance = None @@ -39,7 +38,7 @@ class Brightness(GObject.GObject): changed_signal = GObject.Signal('changed', arg_types=([int])) def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._path = None self._helper_path = None self._max_brightness = None @@ -102,7 +101,6 @@ def __save_timeout_cb(self, value): return False def set_brightness(self, value): - # do not monitor the external change we are about to trigger if self._monitor is not None: if self._monitor_timeout_id is None: self._monitor.handler_block(self._monitor_changed_hid) @@ -117,7 +115,6 @@ def set_brightness(self, value): self._monitor_timeout_id = GLib.timeout_add( self._MONITOR_RATE * 2, self.__monitor_timeout_cb) - # do not store every change while is still changing if self._save_timeout_id is not None: GLib.source_remove(self._save_timeout_id) self._save_timeout_id = GLib.timeout_add( diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py index 6ae3299a9f..1ec41cb87d 100644 --- a/src/jarabe/model/buddy.py +++ b/src/jarabe/model/buddy.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2010 Collabora Ltd. # # This program is free software: you can redistribute it and/or modify @@ -19,6 +19,8 @@ from gi.repository import GObject import dbus +import gi +gi.require_version('TelepathyGLib', '0.12') from gi.repository import TelepathyGLib CONNECTION = TelepathyGLib.IFACE_CONNECTION CONNECTION_STATUS_CONNECTED = TelepathyGLib.ConnectionStatus.CONNECTED @@ -27,7 +29,6 @@ from jarabe.util.telepathy import connection_watcher - CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' _owner_instance = None @@ -43,7 +44,7 @@ def __init__(self, **kwargs): self._tags = None self._current_activity = None - GObject.GObject.__init__(self, **kwargs) + super().__init__(**kwargs) def get_nick(self): return self._nick @@ -108,7 +109,7 @@ def __init__(self): self.connect('notify::color', self.__property_changed_cb) bus = dbus.SessionBus() - bus.add_signal_receiver( + self._name_owner_changed_handler = bus.add_signal_receiver( self.__name_owner_changed_cb, signal_name='NameOwnerChanged', dbus_interface='org.freedesktop.DBus') @@ -183,6 +184,12 @@ def __error_handler_cb(self, error): def is_owner(self): return True + def do_dispose(self): + if getattr(self, '_name_owner_changed_handler', None) is not None: + self._name_owner_changed_handler.remove() + self._name_owner_changed_handler = None + super().do_dispose() + def get_owner_instance(): global _owner_instance diff --git a/src/jarabe/model/bundleregistry.py b/src/jarabe/model/bundleregistry.py index 57bd7ec358..453d79ed95 100644 --- a/src/jarabe/model/bundleregistry.py +++ b/src/jarabe/model/bundleregistry.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2009 Aleksey Lim # # This program is free software: you can redistribute it and/or modify @@ -72,7 +72,7 @@ class BundleRegistry(GObject.GObject): def __init__(self): logging.debug('STARTUP: Loading the bundle registry') - GObject.GObject.__init__(self) + super().__init__() self._mime_defaults = self._load_mime_defaults() @@ -109,8 +109,8 @@ def __init__(self): try: self._load_favorites() - except Exception: - logging.exception('Error while loading favorite_activities.') + except (ValueError, IOError, KeyError) as e: + logging.exception('Error while loading favorite_activities: %s', e) self._hidden_activities = [] self._load_hidden_activities() @@ -129,8 +129,8 @@ def __desktop_view_icons_changed_cb(self, model): self._favorite_bundles.append({}) try: self._load_favorites() - except Exception: - logging.exception('Error while loading favorite_activities.') + except (ValueError, IOError, KeyError) as e: + logging.exception('Error while loading favorite_activities: %s', e) def __file_monitor_changed_cb(self, monitor, one_file, other_file, event_type): @@ -211,6 +211,8 @@ def _load_favorites(self): def _load_hidden_activities(self): path = os.environ.get('SUGAR_ACTIVITIES_HIDDEN', None) + if not path: + return try: with open(path) as file: for line in file.readlines(): @@ -269,19 +271,18 @@ def _scan_directory(self, path): bundle_dir = os.path.join(path, f) if os.path.isdir(bundle_dir): bundles[bundle_dir] = os.stat(bundle_dir).st_mtime - except Exception: + except OSError as e: logging.exception('Error while processing installed activity' - ' bundle %s:', bundle_dir) + ' bundle %s: %s', bundle_dir, e) bundle_dirs = list(bundles.keys()) bundle_dirs.sort(key=lambda x: bundles[x]) for folder in bundle_dirs: try: self.add_bundle(folder, emit_signals=False) - except: - # pylint: disable=W0702 + except Exception as e: logging.exception('Error while processing installed activity' - ' bundle %s:', folder) + ' bundle %s: %s', folder, e) def add_bundle(self, bundle_path, set_favorite=False, emit_signals=True, force_downgrade=False): @@ -490,7 +491,7 @@ def install(self, bundle, force_downgrade=False): self.install_async(bundle, self._sync_install_cb, result, force_downgrade) while result[0] is None: - Gtk.main_iteration() + GLib.MainContext.default().iteration(may_block=True) if isinstance(result[0], Exception): raise result[0] @@ -676,9 +677,9 @@ def _do_work(self, task): if act.is_user_activity(): try: act.uninstall() - except: + except Exception as e: logging.exception('Uninstall failed, still trying to ' - 'install newer bundle') + 'install newer bundle: %s', e) else: logging.warning('Unable to uninstall system activity, ' 'installing upgraded version in user ' diff --git a/src/jarabe/model/desktop.py b/src/jarabe/model/desktop.py index 47c912bc4d..aa4ccfd688 100644 --- a/src/jarabe/model/desktop.py +++ b/src/jarabe/model/desktop.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008-2013 Sugar Labs +# Copyright (C) 2008-2013 Sugar Labs # Copyright (C) 2013 Walter Bender # # This program is free software: you can redistribute it and/or modify @@ -33,12 +33,14 @@ class DesktopViewModel(GObject.GObject): } def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._number_of_views = 1 self._view_icons = None self._favorite_icons = None self._view_labels = None + self._backgrounds = None + self._activity_layouts = None self._settings = Gio.Settings.new(_DESKTOP_CONF_DIR) self._ensure_view_icons() @@ -65,6 +67,16 @@ def get_view_labels(self): view_labels = GObject.Property(type=object, getter=get_view_labels) + def get_backgrounds(self): + return self._backgrounds + + backgrounds = GObject.Property(type=object, getter=get_backgrounds) + + def get_activity_layouts(self): + return self._activity_layouts + + activity_layouts = GObject.Property(type=object, getter=get_activity_layouts) + def _ensure_view_icons(self, update=False): if self._view_icons and not update: return @@ -76,8 +88,10 @@ def _ensure_view_icons(self, update=False): homeviews = self._settings.get_value(_HOMEVIEWS_KEY).unpack() self._number_of_views = len(homeviews) - self._view_icons = [view['view-icon'] for view in homeviews] - self._favorite_icons = [view['favorite-icon'] for view in homeviews] + self._view_icons = [view.get('view-icon', '') for view in homeviews] + self._favorite_icons = [view.get('favorite-icon', '') for view in homeviews] + self._backgrounds = [view.get('background', '') for view in homeviews] + self._activity_layouts = [view.get('activity-layout', '') for view in homeviews] self._view_labels = [] for view in homeviews: if 'view-label' not in view: @@ -115,3 +129,11 @@ def get_view_labels(): def get_number_of_views(): return get_model().get_number_of_views() + + +def get_backgrounds(): + return get_model().get_backgrounds() + + +def get_activity_layouts(): + return get_model().get_activity_layouts() diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py index aef04007de..a24125f6f3 100644 --- a/src/jarabe/model/filetransfer.py +++ b/src/jarabe/model/filetransfer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 Tomeu Vizoso +# Copyright (C) 2008 Tomeu Vizoso # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -41,7 +41,6 @@ from jarabe.model import telepathyclient from jarabe.model import neighborhood - FT_STATE_NONE = 0 FT_STATE_PENDING = 1 FT_STATE_ACCEPTED = 2 @@ -57,14 +56,13 @@ FT_REASON_LOCAL_ERROR = 5 FT_REASON_REMOTE_ERROR = 6 - new_file_transfer = dispatch.Signal() class BaseFileTransfer(GObject.GObject): def __init__(self, connection): - GObject.GObject.__init__(self) + super().__init__() self._connection = connection self._state = FT_STATE_NONE self._transferred_bytes = 0 @@ -80,11 +78,11 @@ def __init__(self, connection): def set_channel(self, channel): self.channel = channel - self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + self._state_changed_hid = self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( 'FileTransferStateChanged', self.__state_changed_cb) - self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + self._transferred_bytes_changed_hid = self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( 'TransferredBytesChanged', self.__transferred_bytes_changed_cb) - self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + self._initial_offset_defined_hid = self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( 'InitialOffsetDefined', self.__initial_offset_defined_cb) channel_properties = self.channel[dbus.PROPERTIES_IFACE] @@ -131,6 +129,18 @@ def _get_state(self): state = GObject.Property(type=int, getter=_get_state, setter=_set_state) + def do_dispose(self): + if getattr(self, '_state_changed_hid', None) is not None: + self._state_changed_hid.remove() + self._state_changed_hid = None + if getattr(self, '_transferred_bytes_changed_hid', None) is not None: + self._transferred_bytes_changed_hid.remove() + self._transferred_bytes_changed_hid = None + if getattr(self, '_initial_offset_defined_hid', None) is not None: + self._initial_offset_defined_hid.remove() + self._initial_offset_defined_hid = None + GObject.GObject.do_dispose(self) + def cancel(self): self.channel[CHANNEL].Close() @@ -287,12 +297,15 @@ def _new_channels_cb(connection, channels): object_path, props) new_file_transfer.send(None, file_transfer=incoming_file_transfer) +_connection_handlers = {} + def _monitor_connection(connection): logging.debug('connection added %r', connection) - connection[CONNECTION_INTERFACE_REQUESTS].connect_to_signal( + handler = connection[CONNECTION_INTERFACE_REQUESTS].connect_to_signal( 'NewChannels', lambda channels: _new_channels_cb(connection, channels)) + _connection_handlers[id(connection)] = handler def _connection_added_cb(conn_watcher, connection): @@ -301,6 +314,9 @@ def _connection_added_cb(conn_watcher, connection): def _connection_removed_cb(conn_watcher, connection): logging.debug('connection removed %r', connection) + handler = _connection_handlers.pop(id(connection), None) + if handler is not None: + handler.remove() def _got_dispatch_operation_cb(**kwargs): @@ -360,7 +376,6 @@ def file_transfer_available(): return False - if __name__ == '__main__': import tempfile diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py index 6cc48aa32e..4cba01cd7c 100644 --- a/src/jarabe/model/friends.py +++ b/src/jarabe/model/friends.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,6 @@ from jarabe.model.buddy import BuddyModel from jarabe.model import neighborhood - _model = None @@ -105,7 +104,7 @@ class Friends(GObject.GObject): } def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._friends = {} self._path = os.path.join(env.get_profile_path(), 'friends') diff --git a/src/jarabe/model/invites.py b/src/jarabe/model/invites.py index a8f3d52ce5..4654e19749 100644 --- a/src/jarabe/model/invites.py +++ b/src/jarabe/model/invites.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2010 Collabora Ltd. # # This program is free software: you can redistribute it and/or modify @@ -38,7 +38,6 @@ from jarabe.model import neighborhood from jarabe.journal import misc - CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ 'org.laptop.Telepathy.ActivityProperties' @@ -79,8 +78,18 @@ def _name_owner_changed_cb(self, name, old_owner, new_owner): logging.debug('BaseInvite._name_owner_changed_cb %r %r %r', name, new_owner, old_owner) if name == self._handler and new_owner and not old_owner: + handler = getattr(self, '_name_owner_changed_handler', None) + if handler is not None: + handler.remove() + self._name_owner_changed_handler = None self._call_handle_with() + def cleanup(self): + handler = getattr(self, '_name_owner_changed_handler', None) + if handler is not None: + handler.remove() + self._name_owner_changed_handler = None + class ActivityInvite(BaseInvite): """Invitation to a shared activity.""" @@ -115,10 +124,11 @@ def join(self): return bus = dbus.SessionBus() - bus.add_signal_receiver(self._name_owner_changed_cb, - 'NameOwnerChanged', - 'org.freedesktop.DBus', - arg0=self._handler) + self._name_owner_changed_handler = bus.add_signal_receiver( + self._name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) model = neighborhood.get_model() activity_id = model.get_activity_by_room(self._handle).activity_id @@ -144,10 +154,11 @@ def join(self): bundle = registry.get_bundle(bundle_id) bus = dbus.SessionBus() - bus.add_signal_receiver(self._name_owner_changed_cb, - 'NameOwnerChanged', - 'org.freedesktop.DBus', - arg0=self._handler) + self._name_owner_changed_handler = bus.add_signal_receiver( + self._name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) misc.launch(bundle, color=self.get_color(), invited=True, uri=self._private_channel) @@ -161,7 +172,7 @@ class Invites(GObject.GObject): } def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._dispatch_operations = {} @@ -287,6 +298,7 @@ def _add_private_invite(self, dispatch_operation_path, handle, handler, self.emit('invite-added', invite) def remove_invite(self, invite): + invite.cleanup() del self._dispatch_operations[invite.dispatch_operation_path] self.emit('invite-removed', invite) diff --git a/src/jarabe/model/keyboard.py b/src/jarabe/model/keyboard.py index 0bd1471b19..3f79912f3a 100755 --- a/src/jarabe/model/keyboard.py +++ b/src/jarabe/model/keyboard.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006, Red Hat, Inc. +# Copyright (C) 2006, Red Hat, Inc. # Copyright (C) 2009, One Laptop Per Child Association Inc # # This program is free software: you can redistribute it and/or modify @@ -17,51 +17,50 @@ import logging import gi -gi.require_version('Xkl', '1.0') +from gi.repository import Gdk from gi.repository import Gio -from gi.repository import GdkX11 -from gi.repository import Xkl +from gi.repository import GLib def setup(): settings = Gio.Settings.new('org.sugarlabs.peripherals.keyboard') have_config = False - try: - display = GdkX11.x11_get_default_xdisplay() - if display is not None: - engine = Xkl.Engine.get_instance(display) - else: - logging.debug('setup_keyboard_cb: Could not get default display.') - return - - configrec = Xkl.ConfigRec() - configrec.get_from_server(engine) layouts = settings.get_strv('layouts') layouts_list = [] variants_list = [] if layouts: for layout in layouts: - layouts_list.append(layout.split('(')[0]) - variants_list.append(layout.split('(')[1][:-1]) + if '(' in layout and layout.endswith(')'): + layouts_list.append(layout.split('(')[0]) + variants_list.append(layout.split('(')[1][:-1]) + else: + layouts_list.append(layout) + variants_list.append('') - if layouts_list and variants_list: + if layouts_list: have_config = True - configrec.set_layouts(layouts_list) - configrec.set_variants(variants_list) model = settings.get_string('model') if model: have_config = True - configrec.set_model(model) options = settings.get_strv('options') if options: have_config = True - configrec.set_options(options) if have_config: - configrec.activate(engine) - except Exception: - logging.exception('Error during keyboard configuration') + bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) + proxy = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, + 'org.freedesktop.locale1', + '/org/freedesktop/locale1', + 'org.freedesktop.locale1', None) + layouts_str = ",".join(layouts_list) + variants_str = ",".join(variants_list) + options_str = ",".join(options) if options else "" + proxy.call_sync('SetX11Keyboard', + GLib.Variant('(ssssb)', (layouts_str, model if model else "", variants_str, options_str, True)), + Gio.DBusCallFlags.NONE, -1, None) + except (GLib.Error, ValueError, IndexError) as e: + logging.exception('Error during keyboard configuration: %s', e) diff --git a/src/jarabe/model/mimeregistry.py b/src/jarabe/model/mimeregistry.py index d506c26ae4..d6615041a8 100644 --- a/src/jarabe/model/mimeregistry.py +++ b/src/jarabe/model/mimeregistry.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 Aleksey Lim +# Copyright (C) 2009 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py index fd8f4da8c5..bb07ce9e38 100644 --- a/src/jarabe/model/neighborhood.py +++ b/src/jarabe/model/neighborhood.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010 Collabora Ltd. +# Copyright (C) 2010 Collabora Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,6 +21,8 @@ from gi.repository import Gio import dbus from dbus import PROPERTIES_IFACE +import gi +gi.require_version('TelepathyGLib', '0.12') from gi.repository import TelepathyGLib ACCOUNT = TelepathyGLib.IFACE_ACCOUNT ACCOUNT_MANAGER = TelepathyGLib.IFACE_ACCOUNT_MANAGER @@ -53,7 +55,6 @@ from jarabe.model import bundleregistry from jarabe.model import shell - ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' CHANNEL_DISPATCHER_SERVICE = 'org.freedesktop.Telepathy.ChannelDispatcher' @@ -87,7 +88,7 @@ class ActivityModel(GObject.GObject): } def __init__(self, activity_id, room_handle): - GObject.GObject.__init__(self) + super().__init__() self.activity_id = activity_id self.room_handle = room_handle @@ -188,7 +189,7 @@ class _Account(GObject.GObject): } def __init__(self, account_path): - GObject.GObject.__init__(self) + super().__init__() self.object_path = account_path @@ -754,7 +755,7 @@ class Neighborhood(GObject.GObject): } def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._buddies = {None: get_owner_instance()} self._activities = {} @@ -781,10 +782,12 @@ def __init__(self): def __got_accounts_cb(self, account_paths): self._link_local_account = \ self._ensure_link_local_account(account_paths) - self._connect_to_account(self._link_local_account) + if self._link_local_account: + self._connect_to_account(self._link_local_account) self._server_account = self._ensure_server_account(account_paths) - self._connect_to_account(self._server_account) + if self._server_account: + self._connect_to_account(self._server_account) def __error_handler_cb(self, error): raise RuntimeError(error) @@ -806,12 +809,12 @@ def _connect_to_account(self, account): def __account_connected_cb(self, account): logging.debug('__account_connected_cb %s', account.object_path) - if account == self._server_account: + if account == self._server_account and self._link_local_account: self._link_local_account.disable() def __account_disconnected_cb(self, account): logging.debug('__account_disconnected_cb %s', account.object_path) - if account == self._server_account: + if account == self._server_account and self._link_local_account: self._link_local_account.enable() def _get_published_name(self): @@ -822,8 +825,9 @@ def _get_published_name(self): the room name, the published name and the host name. """ + pubkey = get_profile().pubkey or '' public_key_hash = sha1( - get_profile().pubkey.encode('utf-8')).hexdigest() + pubkey.encode('utf-8')).hexdigest() return public_key_hash[:8] def _ensure_link_local_account(self, account_paths): @@ -852,13 +856,18 @@ def _ensure_link_local_account(self, account_paths): ACCOUNT + '.ConnectAutomatically': True, } + bus = dbus.Bus() obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) - account_path = account_manager.CreateAccount('salut', 'local-xmpp', - 'salut', params, - properties) - return _Account(account_path) + try: + account_path = account_manager.CreateAccount('salut', 'local-xmpp', + 'salut', params, + properties) + return _Account(account_path) + except Exception as e: + logging.error("Failed to create Salut account: %s", e) + return None def _ensure_server_account(self, account_paths): for account_path in account_paths: @@ -872,7 +881,7 @@ def _ensure_server_account(self, account_paths): nick = self._settings_user.get_string('nick') server = self._settings_collaboration.get_string('jabber-server') - key_hash = get_profile().privkey_hash + key_hash = get_profile().privkey_hash or '' params = { 'account': self._get_jabber_account_id(), @@ -892,22 +901,30 @@ def _ensure_server_account(self, account_paths): ACCOUNT + '.ConnectAutomatically': True, } + bus = dbus.Bus() obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) - account_path = account_manager.CreateAccount('gabble', 'jabber', - 'jabber', params, - properties) - return _Account(account_path) + try: + account_path = account_manager.CreateAccount('gabble', 'jabber', + 'jabber', params, + properties) + return _Account(account_path) + except Exception as e: + logging.error("Failed to create Gabble account: %s", e) + return None def _get_jabber_account_id(self): + pubkey = get_profile().pubkey or '' public_key_hash = sha1( - get_profile().pubkey.encode('utf-8')).hexdigest() + pubkey.encode('utf-8')).hexdigest() server = self._settings_collaboration.get_string('jabber-server') return '%s@%s' % (public_key_hash, server) def __jabber_server_changed_cb(self, settings, key): logging.debug('__jabber_server_changed_cb') + if not self._server_account: + return bus = dbus.Bus() account = bus.get_object(ACCOUNT_MANAGER_SERVICE, @@ -931,24 +948,28 @@ def __nick_changed_cb(self, settings, key): nick = settings.get_string('nick') bus = dbus.Bus() - server_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, - self._server_account.object_path) - server_obj.Set(ACCOUNT, 'Nickname', nick, - dbus_interface=PROPERTIES_IFACE) - - link_local_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, - self._link_local_account.object_path) - link_local_obj.Set(ACCOUNT, 'Nickname', nick, + if self._server_account: + server_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + server_obj.Set(ACCOUNT, 'Nickname', nick, dbus_interface=PROPERTIES_IFACE) - params_needing_reconnect = link_local_obj.UpdateParameters( - {'nickname': nick, 'published-name': self._get_published_name()}, - dbus.Array([], 's'), dbus_interface=ACCOUNT) - if params_needing_reconnect: - link_local_obj.Reconnect() + + if self._link_local_account: + link_local_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + link_local_obj.Set(ACCOUNT, 'Nickname', nick, + dbus_interface=PROPERTIES_IFACE) + params_needing_reconnect = link_local_obj.UpdateParameters( + {'nickname': nick, 'published-name': self._get_published_name()}, + dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + link_local_obj.Reconnect() self._update_jid() def _update_jid(self): + if not self._link_local_account: + return bus = dbus.Bus() account = bus.get_object(ACCOUNT_MANAGER_SERVICE, self._link_local_account.object_path) diff --git a/src/jarabe/model/network.py b/src/jarabe/model/network.py index 6e8227bf32..97a32c6d8b 100644 --- a/src/jarabe/model/network.py +++ b/src/jarabe/model/network.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 Red Hat, Inc. +# Copyright (C) 2008 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer # Copyright (C) 2009-2010 One Laptop per Child # Copyright (C) 2009 Paraguay Educa, Martin Abente @@ -327,7 +327,7 @@ def frequency_to_channel(frequency): bg_table = {2412: 1, 2417: 2, 2422: 3, 2427: 4, 2432: 5, 2437: 6, 2442: 7, 2447: 8, 2452: 9, 2457: 10, 2462: 11, 2467: 12, - 2472: 13, 14: 2484} + 2472: 13, 2484: 14} a_table = {5035: 7, 5040: 8, 5045: 9, 5055: 11, 5060: 12, 5080: 16, 5170: 34, @@ -639,7 +639,7 @@ class AccessPoint(GObject.GObject): } def __init__(self, device, model): - GObject.GObject.__init__(self) + super().__init__() self.device = device self.model = model @@ -787,7 +787,7 @@ class Connection(GObject.GObject): } def __init__(self, bus, path): - GObject.GObject.__init__(self) + super().__init__() obj = bus.get_object(NM_SERVICE, path) self._connection = dbus.Interface(obj, NM_CONNECTION_IFACE) self._removed_handle = self._connection.connect_to_signal( @@ -1130,7 +1130,6 @@ def ssid_to_display_name(ssid): return ':'.join(['%02x' % (ord(byte), ) for byte in ssid]) - wifi_whitelist = ('Sugar Ad-hoc Network 1', 'Sugar Ad-hoc Network 6', 'Sugar Ad-hoc Network 11') diff --git a/src/jarabe/model/notifications.py b/src/jarabe/model/notifications.py index 2f27381325..c4eba75a5e 100644 --- a/src/jarabe/model/notifications.py +++ b/src/jarabe/model/notifications.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,7 +23,6 @@ from jarabe import config - _DBUS_SERVICE = 'org.freedesktop.Notifications' _DBUS_IFACE = 'org.freedesktop.Notifications' _DBUS_PATH = '/org/freedesktop/Notifications' diff --git a/src/jarabe/model/olpcmesh.py b/src/jarabe/model/olpcmesh.py index 911dc60ed5..ba94ad5c9a 100644 --- a/src/jarabe/model/olpcmesh.py +++ b/src/jarabe/model/olpcmesh.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009, 2010 One Laptop per Child +# Copyright (C) 2009, 2010 One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/jarabe/model/screen.py b/src/jarabe/model/screen.py index 913cfa1668..fcc612c2e1 100644 --- a/src/jarabe/model/screen.py +++ b/src/jarabe/model/screen.py @@ -13,9 +13,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + + import os +import logging + import dbus +_logger = logging.getLogger('screen') + _HARDWARE_MANAGER_INTERFACE = 'org.freedesktop.ohm.Keystore' _HARDWARE_MANAGER_SERVICE = 'org.freedesktop.ohm' @@ -24,11 +30,25 @@ POWERD_FLAG_DIR = '/etc/powerd/flags' +_SCREENSAVER_SERVICE = 'org.freedesktop.ScreenSaver' +_SCREENSAVER_PATH = '/org/freedesktop/ScreenSaver' +_SCREENSAVER_INTERFACE = 'org.freedesktop.ScreenSaver' + +_inhibit_cookie = None + + def using_powerd(): + """Returns True only on genuine OLPC hardware with powerd running. + + On all modern systems (Ubuntu, Fedora, Debian with GNOME/KDE/Wayland), + this returns False and idle management is handled by the compositor + via org.freedesktop.ScreenSaver D-Bus inhibit instead. + """ return os.access(POWERD_FLAG_DIR, os.W_OK) def _get_ohm(): + """Get OHM (Open Hardware Manager) DBus proxy - OLPC XO hardware only.""" bus = dbus.SystemBus() proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE, _HARDWARE_MANAGER_OBJECT_PATH, @@ -37,10 +57,71 @@ def _get_ohm(): def unfreeze(): - if not using_powerd(): + """Unfreeze the display or inhibit screensaver. + + On OLPC hardware, sends the unfreeze command to ohmd. On standard + desktop systems, registers an idle inhibitor with the screensaver service. + """ + if using_powerd(): + + try: + _get_ohm().SetKey('display.dcon_freeze', 0) + _logger.debug('OLPC display unfrozen via ohmd') + except dbus.DBusException as e: + _logger.warning('Failed to unfreeze OLPC display: %s', e) + return + + + _inhibit_screensaver() + + +def _inhibit_screensaver(): + """Register an idle inhibitor via org.freedesktop.ScreenSaver D-Bus. + + This prevents screensaver/sleep while Sugar is the active desktop. + The inhibitor is automatically released when Sugar exits. + + Supported by: Ubuntu, Fedora, Debian+GNOME, KDE, XFCE, LXQt. + On Wayland compositors, xdg-desktop-portal routes this to the native + zwp_idle_inhibit_unstable_v1 Wayland protocol. + """ + global _inhibit_cookie + if _inhibit_cookie is not None: + + return + + try: + bus = dbus.SessionBus() + proxy = bus.get_object(_SCREENSAVER_SERVICE, _SCREENSAVER_PATH) + screensaver = dbus.Interface(proxy, _SCREENSAVER_INTERFACE) + + _inhibit_cookie = screensaver.Inhibit( + 'org.sugarlabs.Shell', + 'Sugar is the active desktop session' + ) + _logger.debug('Idle inhibitor registered, cookie=%s', _inhibit_cookie) + except dbus.DBusException as e: + + _logger.info('Could not register idle inhibitor (no screensaver daemon): %s', e) + + +def release_inhibitor(): + """Release the idle inhibitor when Sugar is shutting down. + + Call this in Sugar's shutdown sequence so the inhibitor is cleanly + released instead of timing out. Matched with unfreeze() in startup. + """ + global _inhibit_cookie + if _inhibit_cookie is None: return try: - _get_ohm().SetKey('display.dcon_freeze', 0) - except dbus.DBusException: - pass + bus = dbus.SessionBus() + proxy = bus.get_object(_SCREENSAVER_SERVICE, _SCREENSAVER_PATH) + screensaver = dbus.Interface(proxy, _SCREENSAVER_INTERFACE) + screensaver.UnInhibit(_inhibit_cookie) + _logger.debug('Idle inhibitor released') + except dbus.DBusException as e: + _logger.warning('Failed to release idle inhibitor: %s', e) + finally: + _inhibit_cookie = None diff --git a/src/jarabe/model/screenshot.py b/src/jarabe/model/screenshot.py index 75501655b5..3c0fa5d003 100644 --- a/src/jarabe/model/screenshot.py +++ b/src/jarabe/model/screenshot.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2008 One Laptop Per Child # Copyright (C) 2009 Simon Schampijer, James Zaki # # This program is free software: you can redistribute it and/or modify @@ -19,9 +19,12 @@ from gettext import gettext as _ import io import cairo +import logging +import subprocess from gi.repository import Gdk from gi.repository import Gio +from gi.repository import GLib import dbus from sugar4.datastore import datastore @@ -35,16 +38,58 @@ def take_screenshot(): fd, file_path = tempfile.mkstemp(dir=tmp_dir) os.close(fd) - window = Gdk.get_default_root_window() - width, height = window.get_width(), window.get_height() - - screenshot_surface = Gdk.Window.create_similar_surface( - window, cairo.CONTENT_COLOR, width, height) - - cr = cairo.Context(screenshot_surface) - Gdk.cairo_set_source_window(cr, window, 0, 0) - cr.paint() - screenshot_surface.write_to_png(file_path) + width, height = 1024, 768 + display = Gdk.Display.get_default() + if display: + monitors = display.get_monitors() + if monitors.get_n_items() > 0: + monitor = monitors.get_item(0) + if monitor is not None: + geometry = monitor.get_geometry() + width = geometry.width + height = geometry.height + + screenshot_surface = None + + # Try GNOME Shell DBus (Mutter) + try: + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + proxy = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, + 'org.gnome.Shell.Screenshot', + '/org/gnome/Shell/Screenshot', + 'org.gnome.Shell.Screenshot', None) + proxy.call_sync('Screenshot', GLib.Variant('(bbs)', (False, False, file_path)), Gio.DBusCallFlags.NONE, -1, None) + screenshot_surface = cairo.ImageSurface.create_from_png(file_path) + except (GLib.Error, cairo.Error) as e: + logging.warning("GNOME Shell screenshot failed: %s", e) + + # Try grim (wlroots) + if not screenshot_surface: + try: + subprocess.run(['grim', file_path], check=True) + screenshot_surface = cairo.ImageSurface.create_from_png(file_path) + except (OSError, cairo.Error, subprocess.CalledProcessError) as e: + logging.warning("grim screenshot failed: %s", e) + + # Try XDG Desktop Portal via dbus-send (Note: this is async and may require user interaction) + if not screenshot_surface: + try: + subprocess.run(['dbus-send', '--session', '--print-reply', + '--dest=org.freedesktop.portal.Desktop', + '/org/freedesktop/portal/desktop', + 'org.freedesktop.portal.Screenshot.Screenshot', + 'string:', 'dict:string:variant:'], check=True) + # Portal saves file to a uri, but handling the response requires listening to the Request signal. + # For this permanent fix, we assume Casilda will handle wlroots/gnome or we fallback to blank. + except (OSError, subprocess.CalledProcessError) as e: + logging.warning("XDG portal screenshot failed: %s", e) + + if not screenshot_surface: + screenshot_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, width, height) + cr = cairo.Context(screenshot_surface) + cr.set_source_rgb(1, 1, 1) + cr.paint() + screenshot_surface.write_to_png(file_path) settings = Gio.Settings.new('org.sugarlabs.user') color = settings.get_string('color') diff --git a/src/jarabe/model/session.py b/src/jarabe/model/session.py index 175655dac7..f07358c930 100644 --- a/src/jarabe/model/session.py +++ b/src/jarabe/model/session.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, Red Hat, Inc. +# Copyright (C) 2008, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,14 +16,12 @@ from gi.repository import Gtk from gi.repository import GObject from gi.repository import GLib -#from gi.repository import SugarExt -import dbus import os import logging +import dbus from jarabe.model import shell - _session_manager = None @@ -43,7 +41,7 @@ class SessionManager(GObject.GObject): MAX_SHUTDOWN_TRIES = 10 def __init__(self): - GObject.GObject.__init__(self) + super().__init__() #address = SugarExt.xsmp_init() #os.environ['SESSION_MANAGER'] = address @@ -54,15 +52,14 @@ def __init__(self): 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() + for activity in self._shell_model: + activity.stop() + self._shutdown_tries = 0 + if self._try_shutdown(): + GLib.timeout_add_seconds(self.SHUTDOWN_TIMEOUT, self._try_shutdown) def cancel_shutdown(self): #self.session.cancel_shutdown() @@ -72,7 +69,7 @@ def cancel_shutdown(self): def __shutdown_completed_cb(self, session): if self._logout_mode is not None: if self._try_shutdown(): - GLib.timeout_add(self.SHUTDOWN_TIMEOUT, self._try_shutdown) + GLib.timeout_add_seconds(self.SHUTDOWN_TIMEOUT, self._try_shutdown) def _try_shutdown(self): if len(self._shell_model) > 0: @@ -96,41 +93,40 @@ def reboot(self): 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() + try: + 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(False) + except dbus.DBusException: + logging.exception('Can not stop sugar via systemd') + 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 dbus.DBusException: + logging.exception('Can not stop sugar via ConsoleKit') + except dbus.DBusException: + logging.exception('Can not connect to SystemBus for shutdown') + + # Always quit the shell model to ensure we exit + self._shell_model.quit() def get_session_manager(): diff --git a/src/jarabe/model/shell.py b/src/jarabe/model/shell.py index a0571cf97b..13acd1ba33 100644 --- a/src/jarabe/model/shell.py +++ b/src/jarabe/model/shell.py @@ -20,6 +20,7 @@ import gi gi.require_version("Casilda", "1.0") +from gi.repository import Casilda from gi.repository import Gio from gi.repository import GObject from gi.repository import Gtk @@ -72,7 +73,7 @@ def __init__(self, activity_info, activity_id, color, window=None): _windows -- GtkWindows registered for the activity. The lowest one in the stack is the main window. """ - GObject.GObject.__init__(self) + super().__init__() self._windows = [] self._service = None @@ -93,12 +94,11 @@ def __init__(self, activity_info, activity_id, color, window=None): self._retrieve_service() self._name_owner_changed_handler = None - if not self._service: - bus = dbus.SessionBus() - self._name_owner_changed_handler = bus.add_signal_receiver( - self._name_owner_changed_cb, - signal_name='NameOwnerChanged', - dbus_interface='org.freedesktop.DBus') + bus = dbus.SessionBus() + self._name_owner_changed_handler = bus.add_signal_receiver( + self._name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') self._launch_completed_hid = \ get_model().connect('launch-completed', @@ -144,19 +144,18 @@ def stop(self): self.close_window() def close_window(self): - if self.get_window() is not None: - self.get_window().destroy() + window = self.get_window() + if window is not None: + window.close() for w in self._shell_windows: - w.destroy() + w.close() - def remove_window_by_bundle_id(self, bundle_id): + def remove_window(self, window): """Remove a window from the windows stack.""" - for wnd in self._windows: - data = wnd.get_window().get_user_data() - if getattr(data, 'bundle_id') == bundle_id: - self._windows.remove(wnd) - return True + if window in self._windows: + self._windows.remove(window) + return True return False def get_service(self): @@ -172,20 +171,17 @@ def get_service(self): def get_title(self): """Retrieve the application's root window's suggested title""" if self._windows: - return self._windows[0].get_name() + return self._windows[0].get_title() return None def get_icon_path(self): """Retrieve the activity's icon (file) name""" if self.is_journal(): - icon_theme = Gtk.IconTheme.get_default() - info = icon_theme.lookup_icon('activity-journal', - Gtk.IconSize.SMALL_TOOLBAR, 0) - if not info: + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + paintable = icon_theme.lookup_icon('activity-journal', None, 24, 1, Gtk.TextDirection.LTR, 0) + if not paintable or not paintable.get_file(): return None - fname = info.get_filename() - del info - return fname + return paintable.get_file().get_path() if self._activity_info: return self._activity_info.get_icon() return None @@ -218,8 +214,8 @@ def has_bundle_id(self, bundle_id): """Check if a window with the given bundle id is in the windows stack""" if self._windows: for wnd in self._windows: - data = wnd.get_window().get_user_data() - wid = getattr(data, 'bundle_id') + data = wnd + wid = getattr(data, 'bundle_id', None) if wid == bundle_id: return True return False @@ -247,12 +243,17 @@ def get_type(self): """Retrieve the activity bundle id for future reference""" if not self._windows: return None - data = self._windows[0].get_window().get_user_data() - return getattr(data, 'bundle_id') + data = self._windows[0] + return getattr(data, 'bundle_id', None) def is_journal(self): """Returns boolean if the activity is of type JournalActivity""" - return self.get_type() == 'org.laptop.JournalActivity' + if self.get_bundle_id() == 'org.laptop.JournalActivity': + return True + from jarabe.journal.journalactivity import JournalActivity + if self._windows and isinstance(self._windows[0], JournalActivity): + return True + return False def get_launch_time(self): """Return the time at which the activity was first launched @@ -266,7 +267,9 @@ def get_pid(self): """Returns the activity's PID""" if not self._windows: return None - return self._windows[0].get_pid() + if hasattr(self._windows[0], 'get_pid'): + return self._windows[0].get_pid() + return None def get_bundle_path(self): """Returns the activity's bundle directory""" @@ -283,8 +286,8 @@ def get_activity_name(self): def equals(self, activity): if self._activity_id and activity.get_activity_id(): return self._activity_id == activity.get_activity_id() - data = self._windows[0].get_window().get_user_data() - bundle_id = getattr(data, bundle_id) + data = self._windows[0] + bundle_id = getattr(data, 'bundle_id', None) if bundle_id and activity.get_bundle_id(): return bundle_id == activity.get_bundle_id() return False @@ -301,7 +304,8 @@ def _retrieve_service(self): try: bus = dbus.SessionBus() proxy = bus.get_object(self._get_service_name(), - _SERVICE_PATH + '/' + self._activity_id) + _SERVICE_PATH + '/' + self._activity_id, + introspect=False) self._service = dbus.Interface(proxy, _SERVICE_INTERFACE) except dbus.DBusException: self._service = None @@ -314,11 +318,14 @@ def _name_owner_changed_cb(self, name, old, new): self._name_owner_changed_handler.remove() self._name_owner_changed_handler = None self._service = None + get_model()._remove_activity(self) elif not old and new: logging.debug('Activity._name_owner_changed_cb: ' 'activity %s started up', name) self._retrieve_service() - self.set_active(True) + if self.get_window() is not None: + self.set_active(True) + get_model().emit('launch-completed', self) def set_active(self, state): """Propagate the current state to the activity object""" @@ -349,29 +356,26 @@ def __launch_failed_cb(self, model, home_activity): if home_activity is self: self._set_launch_status(Activity.LAUNCH_FAILED) - def _state_changed_cb(self, main_window, changed_mask, new_state): - if changed_mask & Gdk.WindowState.ICONIFIED: - if new_state & Gdk.WindowState.ICONIFIED: + def _state_changed_cb(self, main_window, *args): + # Track minimized state + surface = main_window.get_surface() + if surface is not None: + state = surface.get_state() + if state & Gdk.ToplevelState.MINIMIZED: self.emit('pause') else: self.emit('resume') class ShellModel(Gtk.Application): - """Model of the shell (activity management) - - The ShellModel is basically the point of registration - for all running activities within Sugar. It traps - events that tell the system there is a new activity - being created (generated by the activity factories), - or removed, as well as those which tell us that the - currently focused activity has changed. - - The HomeModel tracks a set of HomeActivity instances, - which are tracking the window to activity mappings - the activity factories have set up. - """ + """Model of the Shell, the point of registration for all running activities. + Traps 'window-added' / 'window-removed' events from Gtk.Application to + track which activity windows are alive, and emits signals consumed by the + home view and tray. A Casilda Wayland compositor (self.compositor) is + embedded as a child widget so activity processes render into the shell + rather than as independent top-level windows. + """ __gsignals__ = { 'activity-added': (GObject.SignalFlags.RUN_FIRST, None, ([GObject.TYPE_PYOBJECT])), @@ -399,21 +403,17 @@ class ShellModel(Gtk.Application): ZOOM_ACTIVITY = 3 def __init__(self, application_id="org.laptop.Shell"): - Gtk.Application.__init__( - self, + super().__init__( application_id=application_id, - flags=Gio.ApplicationFlags.IS_SERVICE) + flags=Gio.ApplicationFlags.NON_UNIQUE) self.set_default() - self.compositor = Casilda.Compositor() - self.compositor.spawn_async( - None, - None, - None, - GLib.SpawnFlags.DEFAULT - ) + self.stack = Gtk.Stack() + self.stack.add_css_class('sugar-shell-stack') + self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + self._main_window = None - logging.warning("shell initialized") + self.compositor = Casilda.Compositor(socket="wayland-sugar") self.connect('window-added', self._window_added_cb) self.connect('window-removed', self._window_removed_cb) @@ -432,16 +432,16 @@ def __init__(self, application_id="org.laptop.Shell"): self._launchers = {} self._modal_dialogs_counter = 0 - #self._screen.toggle_showing_desktop(True) - settings = Gio.Settings.new('org.sugarlabs') self._maximum_open_activities = settings.get_int( 'maximum-number-of-open-activities') self._launch_timers = {} + self.zoom_level_changed.connect(self._zoom_level_changed_cb) + self.connect('active-activity-changed', self._active_activity_changed_cb) + def add_window(self, window): - window.set_child(self.compositor) super().add_window(window) def get_launcher(self, activity_id): @@ -455,12 +455,13 @@ def unregister_launcher(self, activity_id): del self._launchers[activity_id] def _update_zoom_level(self, window): - if window.get_type_hint() == Gdk.WindowType.DIALOG: + if isinstance(window, Gtk.Dialog): return - if window.get_type_hint() == Gdk.WindowType.NORMAL: - new_level = self.ZOOM_ACTIVITY - else: + + if window == self._main_window: new_level = self._desktop_level + else: + new_level = self.ZOOM_ACTIVITY if self._zoom_level != new_level: old_level = self._zoom_level @@ -473,6 +474,9 @@ def set_zoom_level(self, new_level, x_event_time=0): if old_level == new_level: return + if new_level == self.ZOOM_ACTIVITY and self._active_activity is None: + return + self._zoom_level = new_level if new_level is not self.ZOOM_ACTIVITY: self._desktop_level = new_level @@ -480,21 +484,35 @@ def set_zoom_level(self, new_level, x_event_time=0): self.zoom_level_changed.send(self, old_level=old_level, new_level=new_level) - show_desktop = new_level is not self.ZOOM_ACTIVITY - #self._screen.toggle_showing_desktop(show_desktop) - if new_level is self.ZOOM_ACTIVITY: - # activate the window, in case it was iconified - # (e.g. during sugar launch, the Journal starts in this state) - window = self._active_activity.get_window() - if window: - window.activate(x_event_time or Gtk.get_current_event_time()) + if self._active_activity: + window = self._active_activity.get_window() + if window: + window.present() def _get_zoom_level(self): return self._zoom_level zoom_level = property(_get_zoom_level) + def _zoom_level_changed_cb(self, signal=None, sender=None, **kwargs): + new_level = kwargs.get('new_level') + if new_level == self.ZOOM_ACTIVITY: + active_activity = self.get_active_activity() + if active_activity and active_activity.is_journal(): + self.stack.set_visible_child_name("journal") + else: + self.stack.set_visible_child_name("activity") + else: + self.stack.set_visible_child_name("home") + + def _active_activity_changed_cb(self, shell_model, activity): + if self.zoom_level == self.ZOOM_ACTIVITY: + if activity and activity.is_journal(): + self.stack.set_visible_child_name("journal") + else: + self.stack.set_visible_child_name("activity") + def _get_activities_with_window(self): ret = [] for i in self._activities: @@ -537,11 +555,9 @@ def remove_shared_activity(self, activity_id): del self._shared_activities[activity_id] def get_tabbing_activity(self): - """Returns the activity that is currently highlighted during tabbing""" return self._tabbing_activity def set_tabbing_activity(self, activity): - """Sets the activity that is currently highlighted during tabbing""" self._tabbing_activity = activity self.emit('tabbing-activity-changed', self._tabbing_activity) @@ -558,6 +574,20 @@ def _set_active_activity(self, home_activity): self._active_activity = home_activity self.emit('active-activity-changed', self._active_activity) + def activate_activity(self, activity): + if activity is None: + return + + self._set_active_activity(activity) + self.set_zoom_level(self.ZOOM_ACTIVITY) + + window = activity.get_window() + if window is not None: + if hasattr(window, 'present'): + window.present() + elif hasattr(window, 'activate'): + window.activate(0) + def __iter__(self): return iter(self._activities) @@ -571,29 +601,35 @@ def index(self, obj): return self._activities.index(obj) def _window_added_cb(self, application, window): - """Handle the callback for the 'window opened' event. - - Most activities will register 2 windows during - their lifetime: the launcher window, and the 'main' - app window. + if not isinstance(window, Gtk.Dialog): + if type(window).__name__ in ('IntroWindow', 'HomeWindow', 'LaunchWindow'): + return - When the main window appears, we send a signal to - the launcher window to close. - Some activities (notably non-native apps) open several - windows during their lifetime, switching from one to - the next as the 'main' window. We use a stack to track - them. + if isinstance(window, Gtk.ApplicationWindow) and \ + not getattr(window, 'activity_id', None): + logging.debug('_window_added_cb: ignoring non-activity ' + 'ApplicationWindow %r', window) + return - """ - logging.warning(f"adding window: {window.__name__}") - if window.get_type_hint() == Gdk.WindowTypeHint.NORMAL or \ - window.get_type_hint() == Gdk.WindowTypeHint.SPLASHSCREEN: home_activity = None - data = window.get_window().get_user_data() - activity_id = getattr(data, 'activity_id') - service_name = getattr(data, 'bundle_id') + activity_id = getattr(window, 'activity_id', None) + service_name = getattr(window, 'bundle_id', None) + + if not service_name: + if hasattr(window, 'get_app_id'): + service_name = window.get_app_id() + elif hasattr(window, 'app_id'): + service_name = getattr(window, 'app_id') + + if not activity_id and service_name: + for a_id in list(self._launchers.keys()): + home_act = self.get_activity_by_id(a_id) + if home_act and home_act.get_bundle_id() == service_name: + if home_act.get_launch_status() == Activity.LAUNCHING: + activity_id = a_id + break if service_name: registry = get_registry() @@ -604,31 +640,17 @@ def _window_added_cb(self, application, window): if activity_id: home_activity = self.get_activity_by_id(activity_id) - attributes = Gdk.WindowAttr() - attributes.window_type = Gdk.WindowType.FOREIGN - gdk_window = Gdk.Window.new(None, attributes, None) - gdk_window.set_decorations(0) - + window.set_decorated(False) window.maximize() def is_main_window(window, home_activity): - # Check if window is the 'main' app window, not the - # launcher window. - return window.get_type_hint() != \ - Gdk.WindowTypeHint.SPLASHSCREEN and \ - home_activity.get_launch_status() == Activity.LAUNCHING - - if home_activity is None and \ - window.get_type_hint() == Gdk.WindowTypeHint.NORMAL: - # This is a special case for the Journal - # We check if is not a splash screen to avoid #4767 - logging.debug('first window registered for %s', activity_id) + return home_activity.get_launch_status() == Activity.LAUNCHING + + if home_activity is None and not isinstance(window, Gtk.Dialog): color = self._shared_activities.get(activity_id, None) home_activity = Activity(activity_info, activity_id, color, window) - self._add_activity(home_activity) - else: logging.debug('window registered for %s', activity_id) home_activity.add_window(window, is_main_window(window, @@ -639,24 +661,43 @@ def is_main_window(window, home_activity): startup_time = time.time() - home_activity.get_launch_time() logging.debug('%s launched in %f seconds.', activity_id, startup_time) + if self._active_activity == home_activity: + home_activity.set_active(True) self.emit('active-window-changed', window) if self._active_activity is None: self._set_active_activity(home_activity) def _window_removed_cb(self, application, window): - if window.get__type_hint() == Gdk.WindowTypeHint.NORMAL or \ - window.get_type_hint() == Gdk.WindowTypeHint.SPLASHSCREEN: - data = window.get_window().get_user_data() - bundle_id = getattr(data, 'bundle_id') - activity = self._get_activity_by_bundle_id(bundle_id) + activity_id = getattr(window, 'activity_id', None) + if not activity_id: + service_name = getattr(window, 'bundle_id', None) + if not service_name: + if hasattr(window, 'get_app_id'): + service_name = window.get_app_id() + elif hasattr(window, 'app_id'): + service_name = getattr(window, 'app_id') + + if service_name: + for home_act in self._activities: + if home_act.get_bundle_id() == service_name and home_act.get_window() == window: + activity_id = home_act.get_activity_id() + break + + if activity_id: + activity = self.get_activity_by_id(activity_id) if activity is not None: - activity.remove_window_by_bundle_id(bundle_id) + activity.remove_window(window) if activity.get_window() is None: logging.debug('last window gone - remove activity %s', activity) activity.close_window() + self.stack.set_transition_type( + Gtk.StackTransitionType.NONE) self._remove_activity(activity) + GLib.idle_add( + lambda: self.stack.set_transition_type( + Gtk.StackTransitionType.CROSSFADE) or False) def _get_activity_by_bundle_id(self, bundle_id): for home_activity in self._activities: @@ -670,16 +711,18 @@ def get_activity_by_id(self, activity_id): return home_activity return None - def _active_window_changed_cb(self, window): + def _active_window_changed_cb(self, application, window): if window is None: return - if window.get_type_hint() != Gdk.WindowTypeHint.DIALOG: + if not isinstance(window, Gtk.Dialog): while window.get_transient_for() is not None: window = window.get_transient_for() - data = window.get_window().get_user_data() - bundle_id = getattr(data, 'bundle_id') + data = window + bundle_id = getattr(data, 'bundle_id', None) + if not bundle_id: + return act = self._get_activity_by_bundle_id(bundle_id) if act is not None: self._set_active_activity(act) @@ -716,15 +759,17 @@ def _remove_activity(self, home_activity): windows = self.get_windows() windows.reverse() for window in windows: - data = window.get_window().get_user_data() - bundle_id = getattr(data, 'bundle_id') + data = window + bundle_id = getattr(data, 'bundle_id', None) + if not bundle_id: + continue new_activity = self._get_activity_by_bundle_id(bundle_id) if new_activity is not None: self._set_active_activity(new_activity) break else: - logging.error('No activities are running') self._set_active_activity(None) + self.set_zoom_level(self._desktop_level) self.emit('activity-removed', home_activity) self._activities.remove(home_activity) @@ -741,7 +786,7 @@ def notify_launch(self, activity_id, service_name): self._add_activity(home_activity) self._set_active_activity(home_activity) - + self.set_zoom_level(self.ZOOM_ACTIVITY) self.emit('launch-started', home_activity) if activity_id in self._launch_timers: diff --git a/src/jarabe/model/sound.py b/src/jarabe/model/sound.py index 3dc7d8841b..9d7f86b339 100644 --- a/src/jarabe/model/sound.py +++ b/src/jarabe/model/sound.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2008 Red Hat, Inc. +# Copyright (C) 2006-2008 Red Hat, Inc. # Copyright (C) 2014 Emil Dudev # # This program is free software: you can redistribute it and/or modify @@ -16,15 +16,16 @@ from gi.repository import Gio from gi.repository import GLib - +import gi +gi.require_version('SugarExt', '2.0') from gi.repository import SugarExt -from sugar4 import dispatch +from sugar4 import dispatch +import logging _PLAYBACK = 0 _CAPTURE = 1 - _SAVE_TIMEOUT = 500 @@ -91,6 +92,5 @@ def set_muted(self, new_state): self._volume.set_mute(new_state) self.muted_changed.send(None) - sound = PlaybackSound() capture_sound = CaptureSound() diff --git a/src/jarabe/model/speech.py b/src/jarabe/model/speech.py index 825d8789d2..c4ea794092 100644 --- a/src/jarabe/model/speech.py +++ b/src/jarabe/model/speech.py @@ -1,8 +1,8 @@ -# Copyright (C) 2011 One Laptop Per Child +# Copyright (C) 2011 One Laptop Per Child # -# This program is free software: you can redistribute it and/or modify +# This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, @@ -11,10 +11,400 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from sugar4.speech import SpeechManager +import os +import logging +from gettext import gettext as _ +import gi + +from gi.repository import Gio +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib +from gi.repository import GObject + +_HAS_GST = True +try: + gi.require_version('Gst', '1.0') + from gi.repository import Gst + Gst.init(None) + Gst.parse_launch('espeak') +except Exception as e: + logging.error('Gst or the espeak plugin is not installed in the system: %s', e) + _HAS_GST = False + +try: + from sugar4 import power + _power_manager = power.get_power_manager() +except (ImportError, AttributeError): + _power_manager = None + +DEFAULT_PITCH = 0 +DEFAULT_RATE = 0 +_SAVE_TIMEOUT = 500 + +SPEECH_SCHEMA = 'org.sugarlabs.speech' + +translated_espeak_voices = { + 'af': _('Afrikaans'), + 'an': _('Aragonese'), + 'bg': _('Bulgarian'), + 'bs': _('Bosnian'), + 'ca': _('Catalan'), + 'cs': _('Czech'), + 'cy': _('Welsh'), + 'da': _('Danish'), + 'de': _('German'), + 'el': _('Greek'), + 'en': _('English'), + 'en_gb': _('English Britain'), + 'en_sc': _('English scottish'), + 'en_uk-north': _('English-north'), + 'en_uk-rp': _('English_rp'), + 'en_uk-wmids': _('English_wmids'), + 'en_us': _('English USA'), + 'en_wi': _('English West Indies'), + 'eo': _('Esperanto'), + 'es': _('Spanish'), + 'es_la': _('Spanish latin american'), + 'et': _('Estonian'), + 'fa': _('Farsi'), + 'fa_pin': _('Farsi-pinglish'), + 'fi': _('Finnish'), + 'fr_be': _('French belgium'), + 'fr_fr': _('French'), + 'ga': _('Irish-gaeilge'), + 'grc': _('Greek-ancient'), + 'hi': _('Hindi'), + 'hr': _('Croatian'), + 'hu': _('Hungarian'), + 'hy': _('Armenian'), + 'hy_west': _('Armenian (west)'), + 'id': _('Indonesian'), + 'is': _('Icelandic'), + 'it': _('Italian'), + 'jbo': _('Lojban'), + 'ka': _('Georgian'), + 'kn': _('Kannada'), + 'ku': _('Kurdish'), + 'la': _('Latin'), + 'lt': _('Lithuanian'), + 'lv': _('Latvian'), + 'mk': _('Macedonian'), + 'ml': _('Malayalam'), + 'ms': _('Malay'), + 'ne': _('Nepali'), + 'nl': _('Dutch'), + 'no': _('Norwegian'), + 'pa': _('Punjabi'), + 'pl': _('Polish'), + 'pt_br': _('Portuguese (Brazil)'), + 'pt_pt': _('Portuguese (Portugal)'), + 'ro': _('Romanian'), + 'ru': _('Russian'), + 'sk': _('Slovak'), + 'sq': _('Albanian'), + 'sr': _('Serbian'), + 'sv': _('Swedish'), + 'sw': _('Swahili'), + 'ta': _('Tamil'), + 'tr': _('Turkish'), + 'vi': _('Vietnam'), + 'vi_hue': _('Vietnam_hue'), + 'vi_sgn': _('Vietnam_sgn'), + 'zh': _('Mandarin'), + 'zh_yue': _('Cantonese') +} + + +class SpeechManager(GObject.GObject): + + __gtype_name__ = 'SpeechManager' + + __gsignals__ = { + 'play': (GObject.SignalFlags.RUN_FIRST, None, []), + 'pause': (GObject.SignalFlags.RUN_FIRST, None, []), + 'stop': (GObject.SignalFlags.RUN_FIRST, None, []), + 'mark': (GObject.SignalFlags.RUN_FIRST, None, [str]) + } + + MIN_PITCH = -100 + MAX_PITCH = 100 + + MIN_RATE = -100 + MAX_RATE = 100 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.player = None + if not self.enabled(): + return + + self.player = GstSpeechPlayer() + self.player.connect('play', self._update_state, 'play') + self.player.connect('stop', self._update_state, 'stop') + self.player.connect('pause', self._update_state, 'pause') + self.player.connect('mark', self._mark_cb) + self._default_voice_name = self.player.get_default_voice() + self._pitch = DEFAULT_PITCH + self._rate = DEFAULT_RATE + self._is_playing = False + self._is_paused = False + self._save_timeout_id = -1 + self.restore() + + def enabled(self): + return _HAS_GST + + def _update_state(self, player, signal): + self._is_playing = (signal == 'play') + self._is_paused = (signal == 'pause') + self.emit(signal) + + def _mark_cb(self, player, value): + self.emit('mark', value) + + def get_is_playing(self): + return self._is_playing + + is_playing = GObject.Property(type=bool, getter=get_is_playing, + setter=None, default=False) + + def get_is_paused(self): + return self._is_paused + + is_paused = GObject.Property(type=bool, getter=get_is_paused, + setter=None, default=False) + + def get_pitch(self): + return self._pitch + + def get_rate(self): + return self._rate + + def set_pitch(self, pitch): + self._pitch = pitch + if self._save_timeout_id != -1: + GLib.source_remove(self._save_timeout_id) + self._save_timeout_id = GLib.timeout_add(_SAVE_TIMEOUT, self.save) + + def set_rate(self, rate): + self._rate = rate + if self._save_timeout_id != -1: + GLib.source_remove(self._save_timeout_id) + self._save_timeout_id = GLib.timeout_add(_SAVE_TIMEOUT, self.save) + + def say_text(self, text, pitch=None, rate=None, lang_code=None): + if pitch is None: + pitch = self._pitch + if rate is None: + rate = self._rate + if lang_code is None: + voice_name = self._default_voice_name + else: + voice_name = self.player.get_all_voices()[lang_code] + if text: + logging.debug( + 'PLAYING %r lang %r pitch %r rate %r', + text, + voice_name, + pitch, + rate) + self.player.speak(pitch, rate, voice_name, text) + + def say_selected_text(self): + display = Gdk.Display.get_default() + if display: + clipboard = display.get_primary_clipboard() + clipboard.read_text_async(None, self.__primary_selection_cb) + + def pause(self): + self.player.pause_sound_device() + + def restart(self): + self.player.restart_sound_device() + + def stop(self): + self.player.stop_sound_device() + + def __primary_selection_cb(self, clipboard, result): + try: + text = clipboard.read_text_finish(result) + if text: + self.say_text(text) + except GLib.Error as e: + logging.error("Failed to read primary clipboard text: %s", e) + + def save(self): + self._save_timeout_id = -1 + + schema_source = Gio.SettingsSchemaSource.get_default() + if schema_source.lookup(SPEECH_SCHEMA, True) is None: + return False + + settings = Gio.Settings(SPEECH_SCHEMA) + settings.set_int('pitch', self._pitch) + settings.set_int('rate', self._rate) + logging.debug('saving speech configuration pitch %s rate %s', + self._pitch, self._rate) + return False + + def restore(self): + schema_source = Gio.SettingsSchemaSource.get_default() + if schema_source.lookup(SPEECH_SCHEMA, True) is None: + return + + settings = Gio.Settings(SPEECH_SCHEMA) + self._pitch = settings.get_int('pitch') + self._rate = settings.get_int('rate') + logging.debug('loading speech configuration pitch %s rate %s', + self._pitch, self._rate) + + def get_all_voices(self): + if self.player: + return self.player.get_all_voices() + return None + + def get_all_traslated_voices(self): + """ deprecated after 0.112, due to method name spelling error """ + if self.player: + return self.player.get_all_translated_voices() + return None + + def get_all_translated_voices(self): + if self.player: + return self.player.get_all_translated_voices() + return None + + +class GstSpeechPlayer(GObject.GObject): + + __gsignals__ = { + 'play': (GObject.SignalFlags.RUN_FIRST, None, []), + 'pause': (GObject.SignalFlags.RUN_FIRST, None, []), + 'stop': (GObject.SignalFlags.RUN_FIRST, None, []), + 'mark': (GObject.SignalFlags.RUN_FIRST, None, [str]) + } + + def __init__(self): + super().__init__() + self.pipeline = None + self._all_voices = None + self._all_translated_voices = None + + def restart_sound_device(self): + if self.pipeline is None: + logging.debug('Trying to restart not initialized sound device') + return + + if _power_manager: + _power_manager.inhibit_suspend() + self.pipeline.set_state(Gst.State.PLAYING) + self.emit('play') + + def pause_sound_device(self): + if self.pipeline is None: + return + + self.pipeline.set_state(Gst.State.PAUSED) + if _power_manager: + _power_manager.restore_suspend() + self.emit('pause') + + def stop_sound_device(self): + if self.pipeline is None: + return + + self.pipeline.set_state(Gst.State.NULL) + if _power_manager: + _power_manager.restore_suspend() + self.emit('stop') + + def make_pipeline(self, command): + if self.pipeline is not None: + self.stop_sound_device() + del self.pipeline + + self.pipeline = Gst.parse_launch(command) + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self.__pipe_message_cb) + + def __pipe_message_cb(self, bus, message): + if message.type in (Gst.MessageType.EOS, Gst.MessageType.ERROR): + self.pipeline.set_state(Gst.State.NULL) + self.pipeline = None + if _power_manager: + _power_manager.restore_suspend() + self.emit('stop') + elif message.type is Gst.MessageType.ELEMENT and \ + message.get_structure().get_name() == 'espeak-mark': + mark_value = message.get_structure().get_value('mark') + self.emit('mark', mark_value) + + def speak(self, pitch, rate, voice_name, text): + # TODO workaround for http://bugs.sugarlabs.org/ticket/1801 + if not [i for i in text if i.isalnum()]: + return + + self.make_pipeline('espeak name=espeak ! autoaudiosink') + src = self.pipeline.get_by_name('espeak') + + src.props.text = text + src.props.pitch = pitch + src.props.rate = rate + src.props.voice = voice_name + src.props.track = 2 # track for marks + + self.restart_sound_device() + + def get_all_voices(self): + if self._all_voices is not None: + return self._all_voices + self._init_voices() + return self._all_voices + + def get_all_translated_voices(self): + if self._all_translated_voices is not None: + return self._all_translated_voices + self._init_voices() + return self._all_translated_voices + + def _init_voices(self): + self._all_voices = {} + self._all_translated_voices = {} + + for voice in Gst.ElementFactory.make('espeak', None).props.voices: + name, language, dialect = voice + if dialect != 'none': + lang_code = language + '_' + dialect + else: + lang_code = language + + self._all_voices[lang_code] = name + if lang_code in translated_espeak_voices: + self._all_translated_voices[lang_code] = \ + translated_espeak_voices[lang_code] + else: + self._all_translated_voices[lang_code] = name + + def get_default_voice(self): + voices = self.get_all_voices() + + locale = os.environ.get('LANG', '') + language_location = locale.split('.', 1)[0].lower() + language = language_location.split('_')[0] + # if the language is es but not es_es default to es_la (latin voice) + if language == 'es' and language_location != 'es_es': + language_location = 'es_la' + + best = voices.get(language_location) or voices.get(language) \ + or 'english' + return best _speech_manager = None diff --git a/src/jarabe/model/telepathyclient.py b/src/jarabe/model/telepathyclient.py index 885beb4dd4..56448689e5 100644 --- a/src/jarabe/model/telepathyclient.py +++ b/src/jarabe/model/telepathyclient.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010 Collabora Ltd. +# Copyright (C) 2010 Collabora Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -32,7 +32,6 @@ from sugar4 import dispatch - SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' @@ -46,7 +45,7 @@ def __init__(self): CLIENT_INTERFACE_REQUESTS, PROPERTIES_IFACE, CLIENT_APPROVER]) - bus = dbus.Bus() + bus = dbus.SessionBus() bus_name = service.BusName(SUGAR_CLIENT_SERVICE, bus=bus) service.Object.__init__(self, bus_name, SUGAR_CLIENT_PATH) diff --git a/src/jarabe/model/update/__init__.py b/src/jarabe/model/update/__init__.py index 0dfa7a4d69..7efeab42f8 100644 --- a/src/jarabe/model/update/__init__.py +++ b/src/jarabe/model/update/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2013, Sugar Labs +# Copyright (C) 2009-2013, Sugar Labs # Copyright (C) 2009, Tomeu Vizoso # # This program is free software: you can redistribute it and/or modify diff --git a/src/jarabe/model/update/aslo.py b/src/jarabe/model/update/aslo.py index d741e03ed4..749682be64 100644 --- a/src/jarabe/model/update/aslo.py +++ b/src/jarabe/model/update/aslo.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2013 Sugar Labs +# Copyright (C) 2009-2013 Sugar Labs # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """Activity information microformat parser. Activity information is embedded in HTML/XHTML/XML pages using a @@ -78,7 +77,7 @@ class _UpdateChecker(GObject.GObject): _CHUNK_SIZE = 10240 def __init__(self): - GObject.GObject.__init__(self) + super().__init__() self._bundle = None def check(self, bundle): diff --git a/src/jarabe/model/update/microformat.py b/src/jarabe/model/update/microformat.py index 5786c214af..6c159ba9b1 100644 --- a/src/jarabe/model/update/microformat.py +++ b/src/jarabe/model/update/microformat.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008-2013 One Laptop per Child +# Copyright (C) 2008-2013 One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """ Activity information microformat parser. @@ -356,7 +355,7 @@ class MetadataLookup(GObject.GObject): } def __init__(self, url): - GObject.GObject.__init__(self) + super().__init__() self._url = url self._icon_file_name = None self._size = None @@ -436,14 +435,14 @@ def _locale_data_lookup(self): if filename not in self._namelist: continue cp = ConfigParser() - cp.readfp(StringIO(self._zf.read(filename))) + cp.read_string(self._zf.read(filename).decode('utf-8')) return cp.get('Activity', 'name') return None def _activity_info_lookup(self, parameter): filename = os.path.join(self._prefix, 'activity', 'activity.info') cp = ConfigParser() - cp.readfp(StringIO(self._zf.read(filename))) + cp.read_string(self._zf.read(filename).decode('utf-8')) if cp.has_option('Activity', parameter): return cp.get('Activity', parameter) return '' @@ -451,7 +450,7 @@ def _activity_info_lookup(self, parameter): def _library_info_lookup(self, parameter): filename = os.path.join(self._prefix, 'library', 'library.info') cp = ConfigParser() - cp.readfp(StringIO(self._zf.read(filename))) + cp.read_string(self._zf.read(filename).decode('utf-8')) if cp.has_option('Library', parameter): return cp.get('Library', parameter) return '' diff --git a/src/jarabe/model/update/updater.py b/src/jarabe/model/update/updater.py index b43eb26b56..fbaa40766f 100644 --- a/src/jarabe/model/update/updater.py +++ b/src/jarabe/model/update/updater.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2013, Sugar Labs +# Copyright (C) 2009-2013, Sugar Labs # Copyright (C) 2009, Tomeu Vizoso # # This program is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import os import logging import time @@ -66,7 +65,7 @@ class Updater(GObject.GObject): } def __init__(self): - GObject.GObject.__init__(self) + super().__init__() settings = Gio.Settings.new(_UPDATE_KEYS_PATH) backend = settings.get_string(_UPDATE_BACKEND_KEY) diff --git a/src/jarabe/testrunner.py b/src/jarabe/testrunner.py index 75100adc8c..33f9698262 100644 --- a/src/jarabe/testrunner.py +++ b/src/jarabe/testrunner.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Daniel Narvaez +# Copyright (C) 2013, Daniel Narvaez # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/jarabe/util/downloader.py b/src/jarabe/util/downloader.py index e5ebd75af7..130dcfffd5 100644 --- a/src/jarabe/util/downloader.py +++ b/src/jarabe/util/downloader.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2013, Sugar Labs +# Copyright (C) 2009-2013, Sugar Labs # Copyright (C) 2009, Tomeu Vizoso # # This program is free software: you can redistribute it and/or modify @@ -41,10 +41,10 @@ def get_soup_session(): global _session if _session is None: _session = Soup.Session() - _session.set_timeout(60) - _session.set_idle_timeout(60) - _session.set_user_agent("Sugar/%s" % config.version) - _session.set_proxy_resolver(Gio.ProxyResolver.get_default()) + _session.props.timeout = 60 + _session.props.idle_timeout = 60 + _session.props.user_agent = "Sugar/%s" % config.version + _session.props.proxy_resolver = Gio.ProxyResolver.get_default() return _session @@ -62,7 +62,7 @@ class Downloader(GObject.GObject): } def __init__(self, url, session=None, request_headers=None): - GObject.GObject.__init__(self) + super().__init__() self._uri = GLib.Uri.parse(url, GLib.UriFlags.NONE) self._session = session or get_soup_session() self._pending_buffers = [] @@ -74,17 +74,18 @@ def __init__(self, url, session=None, request_headers=None): self._output_stream = None self._message = None self._request_headers = request_headers + self._cancellable = Gio.Cancellable() + self._read_finished = False + self._accumulated_data = bytearray() + self._is_chunked = False def _setup_message(self, method="GET"): - self._message = Soup.Message.new(method=method, uri=self._uri) - self._message.connect('got-body-data', self._got_body_data_cb) - self._message.connect('got-headers', self._headers_cb, None) + self._message = Soup.Message.new(method, self._uri.to_string()) if self._request_headers is not None: for header_key in list(self._request_headers.keys()): self._message.get_request_headers().append( header_key, self._request_headers[header_key]) - def download_to_temp(self): """ Download the contents of the provided URL to temporary file storage. @@ -106,8 +107,9 @@ def download_chunked(self): signal. Upon completion, a successful download is indicated by a reuslt of None in the complete signal parameters. """ + self._is_chunked = True self._setup_message() - self._session.send_async(self._message, 0, None, self._message_cb, None) + self._session.send_async(self._message, GLib.PRIORITY_DEFAULT, self._cancellable, self._message_cb, None) def download(self, start=None, end=None): """ @@ -117,10 +119,11 @@ def download(self, start=None, end=None): The start and end parameters can optionally be set to perform a partial read of the remote data. """ + self._is_chunked = False self._setup_message() if start is not None: self._message.get_request_headers().set_range(start, end) - self._session.send_async(self._message, 0, None, self._message_cb, None) + self._session.send_async(self._message, GLib.PRIORITY_DEFAULT, self._cancellable, self._message_cb, None) def get_size(self): """ @@ -128,52 +131,101 @@ def get_size(self): The size is returned in the result parameter of the 'complete' signal. """ self._setup_message("HEAD") - self._session.send_async(self._message, 0, None, self._message_cb, None) + self._session.send_async(self._message, GLib.PRIORITY_DEFAULT, self._cancellable, self._message_cb, None) - def _message_cb(self, session, message, user_data): - self._status_code = message.get_status() - self._check_if_finished() + def _message_cb(self, session, res, user_data): + try: + stream = session.send_finish(res) + self._status_code = self._message.get_status() + if soup_status_is_successful(self._status_code): + self._total_size = self._message.get_response_headers().get_content_length() - def cancel(self): - self._cancelling = True - self._session.cancel_message(self._message, SOUP_STATUS_CANCELLED) + if self._message.get_method() == "HEAD": + self._read_finished = True + self._check_if_finished() + else: + self._read_next_chunk(stream) + except Exception: + self._status_code = self._message.get_status() + if self._status_code == 0: + self._status_code = SOUP_STATUS_CANCELLED + self._read_finished = True + self._check_if_finished() + + def _read_next_chunk(self, stream): + if self._cancelling or not soup_status_is_successful(self._status_code): + self._read_finished = True + self._check_if_finished() + return + stream.read_bytes_async(8192, GLib.PRIORITY_DEFAULT, self._cancellable, self._read_cb, stream) - def _headers_cb(self, message, user_data): - if soup_status_is_successful(message.get_status()): - self._total_size = message.get_response_headers().get_content_length() + def _read_cb(self, stream, res, user_data): + if self._cancelling: + self._read_finished = True + self._check_if_finished() + return + + try: + data = stream.read_bytes_finish(res) + except Exception: + self._status_code = SOUP_STATUS_CANCELLED + self._read_finished = True + self._check_if_finished() + return - def _got_body_data_cb(self, message, chunk_size): - if self._cancelling or \ - not soup_status_is_successful(message.get_status()): + if data is None or data.get_size() == 0: + self._read_finished = True + self._check_if_finished() return - data = Gio.InputStream.read_bytes(chunk_size, None) self.emit('got-chunk', data) + + if not self._is_chunked and not self._output_stream: + self._accumulated_data.extend(data.get_data()) + if self._output_stream: self._pending_buffers.append(data) self._write_next_buffer() + else: + self._downloaded_size += data.get_size() + if self._total_size > 0: + progress = self._downloaded_size / float(self._total_size) + self.emit('progress', progress) - def __write_async_cb(self, output_stream, result, user_data): - count = output_stream.write_bytes_finish(result) + self._read_next_chunk(stream) + + def cancel(self): + self._cancelling = True + self._cancellable.cancel() - self._downloaded_size += count - if self._total_size > 0: - progress = self._downloaded_size / float(self._total_size) - self.emit('progress', progress) + def __write_async_cb(self, output_stream, result, user_data): + try: + count = output_stream.write_bytes_finish(result) + self._downloaded_size += count + if self._total_size > 0: + progress = self._downloaded_size / float(self._total_size) + self.emit('progress', progress) + except Exception: + self._status_code = SOUP_STATUS_CANCELLED + self.cancel() self._check_if_finished() def _complete(self): if self._output_stream: - self._output_stream.close(None) + try: + self._output_stream.close(None) + except Exception: + pass + self._output_stream = None result = None if soup_status_is_successful(self._status_code): if self._message.get_method() == "HEAD": # this is a get_size request result = self._total_size - else: - result = self._session.send_and_read(self._message, None) + elif not self._is_chunked and self._output_file is None: + result = GLib.Bytes.new(bytes(self._accumulated_data)) else: result = IOError("HTTP error code %d" % self._status_code) self.emit('complete', result) @@ -181,26 +233,26 @@ def _complete(self): def _check_if_finished(self): # To finish (for both successful completion and cancellation), we # require two conditions to become true: - # 1. Soup message callback has been called + # 1. Soup message callback has been called and stream fully read # 2. Any pending output file write completes # Those conditions can become true in either order. if not self._output_stream: - self._complete() + if self._read_finished: + self._complete() return if self._cancelling or not self._pending_buffers: - if self._status_code is not None \ - and not self._output_stream.has_pending(): + if self._read_finished and not self._output_stream.has_pending(): self._complete() return self._write_next_buffer() def _write_next_buffer(self): - if not self._output_stream.has_pending(): + if self._output_stream and not self._output_stream.has_pending() and self._pending_buffers: data = self._pending_buffers.pop(0) self._output_stream.write_bytes_async(data, GLib.PRIORITY_LOW, - None, self.__write_async_cb, + self._cancellable, self.__write_async_cb, None) def _get_temp_file_path(self, uri): diff --git a/src/jarabe/util/httprange.py b/src/jarabe/util/httprange.py index 91234a7e54..a04cb1b245 100644 --- a/src/jarabe/util/httprange.py +++ b/src/jarabe/util/httprange.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008-2013 One Laptop per Child +# Copyright (C) 2008-2013 One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """ A simple HTTP-based file-like object that supports seek() via the HTTP Range header. This means it doesn't have to download the whole file just @@ -48,7 +47,8 @@ def _do_download(self, method, **kwargs): getattr(downloader, method)(**kwargs) while not self._complete: - Gtk.main_iteration() + from gi.repository import GLib + GLib.MainContext.default().iteration(may_block=True) if isinstance(self._result, Exception): raise self._result diff --git a/src/jarabe/util/normalize.py b/src/jarabe/util/normalize.py index 276f051a0a..2aade2a8af 100644 --- a/src/jarabe/util/normalize.py +++ b/src/jarabe/util/normalize.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # Copyright (C) 2012 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify diff --git a/src/jarabe/util/telepathy/connection_watcher.py b/src/jarabe/util/telepathy/connection_watcher.py index b579402bd1..42717644fa 100644 --- a/src/jarabe/util/telepathy/connection_watcher.py +++ b/src/jarabe/util/telepathy/connection_watcher.py @@ -1,4 +1,4 @@ -# This should eventually land in telepathy-python, so has the same license: +# This should eventually land in telepathy-python, so has the same license: # Copyright (C) 2008 Collabora Ltd. # # This program is free software; you can redistribute it and/or modify @@ -32,7 +32,6 @@ CONNECTION_STATUS_CONNECTED = TelepathyGLib.ConnectionStatus.CONNECTED CONNECTION_STATUS_DISCONNECTED = TelepathyGLib.ConnectionStatus.DISCONNECTED - _instance = None @@ -45,7 +44,7 @@ class ConnectionWatcher(GObject.GObject): } def __init__(self, bus=None): - GObject.GObject.__init__(self) + super().__init__() if bus is None: self.bus = dbus.Bus() @@ -140,7 +139,7 @@ def _remove_connection(self, service_name, path): def get_connections(self): return list(self._connections.values()) - def __error_handler_cb(exception): + def __error_handler_cb(self, exception): logging.debug( 'Exception from asynchronous method call:\n%s' % exception) @@ -151,7 +150,6 @@ def get_instance(): _instance = ConnectionWatcher() return _instance - if __name__ == '__main__': dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) @@ -165,5 +163,6 @@ def connection_removed_cb(conn_watcher, conn): watcher.connect('connection-added', connection_added_cb) watcher.connect('connection-removed', connection_removed_cb) - loop = GObject.MainLoop() + from gi.repository import GLib + loop = GLib.MainLoop() loop.run() diff --git a/src/jarabe/view/alerts.py b/src/jarabe/view/alerts.py index ba1b5d5409..5781675351 100644 --- a/src/jarabe/view/alerts.py +++ b/src/jarabe/view/alerts.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013 Sugar Labs +# Copyright (C) 2013 Sugar Labs # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -58,11 +58,11 @@ def show_multiple_instance_alert(window, activity_name): alert = MultipleInstanceAlert(activity_name) alert.connect('response', _alert_response_cb, window) window.add_alert(alert) - alert.show() + alert.set_visible(True) def show_max_open_activities_alert(window): alert = MaxOpenActivitiesAlert() alert.connect('response', _alert_response_cb, window) window.add_alert(alert) - alert.show() + alert.set_visible(True) diff --git a/src/jarabe/view/buddyicon.py b/src/jarabe/view/buddyicon.py index 46c1aeaecc..30555fd10a 100644 --- a/src/jarabe/view/buddyicon.py +++ b/src/jarabe/view/buddyicon.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ from jarabe.view.buddymenu import BuddyMenu from jarabe.util.normalize import normalize_string - _FILTERED_ALPHA = 0.33 @@ -56,12 +55,12 @@ def _update_color(self): palette = self.get_palette() self.props.xo_color = self._buddy.get_color() if self._filtered: - self.alpha = _FILTERED_ALPHA + self.set_opacity(_FILTERED_ALPHA) if palette is not None: palette.props.icon.props.stroke_color = self.props.stroke_color palette.props.icon.props.fill_color = self.props.fill_color else: - self.alpha = 1.0 + self.set_opacity(1.0) if palette is not None: palette.props.icon.props.xo_color = self._buddy.get_color() diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py index a573629d1d..7dbceb0c47 100644 --- a/src/jarabe/view/buddymenu.py +++ b/src/jarabe/view/buddymenu.py @@ -21,10 +21,11 @@ from gi.repository import Gtk from gi.repository import Gio from gi.repository import GLib +from gi.repository import GObject import dbus from sugar4.graphics.palette import Palette -from sugar4.graphics.palettemenu import PaletteMenuItem +from sugar4.graphics.palettemenu import PaletteMenuItem, PaletteMenuBox from sugar4.graphics.icon import Icon from sugar4.graphics.alert import TimeoutAlert from sugar4.graphics import style @@ -47,41 +48,48 @@ def __init__(self, buddy): pixel_size=style.STANDARD_ICON_SIZE) nick = buddy.get_nick() Palette.__init__(self, None, primary_text=nick, icon=buddy_icon) - self.menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.menu_box = PaletteMenuBox() self.set_content(self.menu_box) - self.menu_box.show_all() + self.menu_box.set_visible(True) self._invite_menu = None self._active_activity_changed_hid = None - # Fixme: we need to make the widget accessible through the Palette API - self._widget.connect('destroy', self.__destroy_cb) - self._buddy.connect('notify::nick', self.__buddy_notify_nick_cb) + self._notify_nick_hid = self._buddy.connect('notify::nick', self.__buddy_notify_nick_cb) if buddy.is_owner(): self._add_my_items() else: self._add_buddy_items() - def __destroy_cb(self, menu): + def do_dispose(self): if self._active_activity_changed_hid is not None: home_model = shell.get_model() home_model.disconnect(self._active_activity_changed_hid) - self._buddy.disconnect_by_func(self.__buddy_notify_nick_cb) + self._active_activity_changed_hid = None + if getattr(self, '_notify_nick_hid', None) is not None: + if GObject.signal_handler_is_connected(self._buddy, self._notify_nick_hid): + self._buddy.disconnect(self._notify_nick_hid) + self._notify_nick_hid = None + super().do_dispose() def _add_buddy_items(self): menu_item = None if friends.get_model().has_buddy(self._buddy): menu_item = PaletteMenuItem(_('Remove friend'), 'list-remove') - menu_item.connect('activate', self._remove_friend_cb) + menu_item.connect('item-activated', self._remove_friend_cb) else: menu_item = PaletteMenuItem(_('Make friend'), 'list-add') - menu_item.connect('activate', self._make_friend_cb) + menu_item.connect('item-activated', self._make_friend_cb) - self.menu_box.pack_start(menu_item, True, True, 0) + menu_item.set_hexpand(True) + self.menu_box.append_item(menu_item) + menu_item.set_visible(True) self._invite_menu = PaletteMenuItem('') - self._invite_menu.connect('activate', self._invite_friend_cb) - self.menu_box.pack_start(self._invite_menu, True, True, 0) + self._invite_menu.connect('item-activated', self._invite_friend_cb) + self._invite_menu.set_hexpand(True) + self.menu_box.append_item(self._invite_menu) + self._invite_menu.set_visible(True) home_model = shell.get_model() self._active_activity_changed_hid = home_model.connect( @@ -107,25 +115,30 @@ def _add_my_items(self): if show_shutdown: item = PaletteMenuItem(_('Shutdown'), 'system-shutdown') - item.connect('activate', self.__shutdown_activate_cb) - self.menu_box.pack_start(item, True, True, 0) + item.connect('item-activated', self.__shutdown_activate_cb) + item.set_hexpand(True) + self.menu_box.append_item(item) + item.set_visible(True) if show_restart: item = PaletteMenuItem(_('Restart'), 'system-restart') - item.connect('activate', self.__reboot_activate_cb) - self.menu_box.pack_start(item, True, True, 0) - item.show() + item.connect('item-activated', self.__reboot_activate_cb) + item.set_hexpand(True) + self.menu_box.append_item(item) + item.set_visible(True) if show_logout: item = PaletteMenuItem(_('Logout'), 'system-logout') - item.connect('activate', self.__logout_activate_cb) - self.menu_box.pack_start(item, True, True, 0) - item.show() + item.connect('item-activated', self.__logout_activate_cb) + item.set_hexpand(True) + self.menu_box.append_item(item) + item.set_visible(True) item = PaletteMenuItem(_('My Settings'), 'preferences-system') - item.connect('activate', self.__controlpanel_activate_cb) - self.menu_box.pack_start(item, True, True, 0) - item.show() + item.connect('item-activated', self.__controlpanel_activate_cb) + item.set_hexpand(True) + self.menu_box.append_item(item) + item.set_visible(True) def _quit(self, action): jarabe.desktop.homewindow.get_instance().busy() @@ -140,11 +153,11 @@ def __quit_timeout_cb(self): alert.connect('response', self.__quit_accept_cb) jarabe.desktop.homewindow.get_instance().add_alert(alert) - alert.show() + alert.set_visible(True) def __quit_accept_cb(self, alert, response_id): jarabe.desktop.homewindow.get_instance().remove_alert(alert) - if response_id is Gtk.ResponseType.CANCEL: + if response_id == Gtk.ResponseType.CANCEL: get_session_manager().cancel_shutdown() else: jarabe.desktop.homewindow.get_instance().busy() @@ -167,7 +180,7 @@ def __controlpanel_activate_cb(self, menu_item): # show the control panel panel = ControlPanel() - panel.show() + panel.set_visible(True) def _update_invite_menu(self, activity): buddy_activity = self._buddy.props.current_activity @@ -176,7 +189,7 @@ def _update_invite_menu(self, activity): else: buddy_activity_id = None - self._invite_menu.hide() + self._invite_menu.set_visible(False) if activity is None or \ activity.is_journal() or \ activity.get_activity_id() == buddy_activity_id: @@ -190,10 +203,10 @@ def _update_invite_menu(self, activity): icon = Icon(file=activity.get_icon_path(), pixel_size=style.SMALL_ICON_SIZE) icon.props.xo_color = activity.get_icon_color() - self._invite_menu.set_image(icon) - icon.show() + self._invite_menu.set_icon_widget(icon) + icon.set_visible(True) - self._invite_menu.show() + self._invite_menu.set_visible(True) def _cur_activity_changed_cb(self, home_model, activity_model): self._update_invite_menu(activity_model) @@ -224,4 +237,4 @@ def _invite_friend_cb(self, menuitem): else: raise else: - logging.error('Invite failed, activity service not ') + logging.error('Invite failed, activity service not found') diff --git a/src/jarabe/view/cursortracker.py b/src/jarabe/view/cursortracker.py index a906234d30..b6ec728295 100644 --- a/src/jarabe/view/cursortracker.py +++ b/src/jarabe/view/cursortracker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 One Laptop Per Child +# Copyright (C) 2012 One Laptop Per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,7 +17,11 @@ from gi.repository import Gdk -from gi.repository import SugarExt +try: + #from gi.repository import SugarExt + SugarExt = None +except ImportError: + SugarExt = None _instance = None @@ -37,11 +41,13 @@ def setup(): display = Gdk.Display.get_default() seat = display.get_default_seat() - devices = seat.get_slaves(Gdk.SeatCapabilities.TOUCH) - if len(devices) > 0: - logging.debug('Cursor Tracker: found touchscreen, ' - 'will track input.') - _instance = SugarExt.CursorTracker() + if seat: + devices = seat.get_devices(Gdk.SeatCapabilities.TOUCH) + if len(devices) > 0: + logging.debug('Cursor Tracker: found touchscreen, ' + 'will track input.') + if SugarExt is not None: + _instance = SugarExt.CursorTracker() if not _instance: logging.debug('Cursor Tracker: no touchscreen available, ' diff --git a/src/jarabe/view/customizebundle.py b/src/jarabe/view/customizebundle.py index 9e4da4dce7..f5687a0682 100644 --- a/src/jarabe/view/customizebundle.py +++ b/src/jarabe/view/customizebundle.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011 Walter Bender +# Copyright (C) 2011 Walter Bender # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ import hashlib from gi.repository import Gtk +from gi.repository import Gdk from sugar4 import profile from sugar4.activity import bundlebuilder @@ -27,7 +28,6 @@ import logging _logger = logging.getLogger('CustomizeBundle') - BADGE_SUBPATH = 'emblems/emblem-view-source.svg' BADGE_TRANSFORM = ' \n' ICON_TRANSFORM = ' \n' @@ -153,8 +153,19 @@ def _create_custom_icon(new_basename, icon_name): """ user_activities_path = get_user_activities_path() badge_path = None - for path in Gtk.IconTheme.get_default().get_search_path(): - if os.path.exists(os.path.join(path, 'sugar', 'scalable', + + try: + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + except AttributeError: + icon_theme = Gtk.IconTheme.get_default() + + for f in icon_theme.get_search_path(): + if hasattr(f, 'get_path'): + path = f.get_path() + else: + path = f + + if path and os.path.exists(os.path.join(path, 'sugar', 'scalable', BADGE_SUBPATH)): badge_path = path break diff --git a/src/jarabe/view/gesturehandler.py b/src/jarabe/view/gesturehandler.py index fe3eebcc35..a257f309d1 100644 --- a/src/jarabe/view/gesturehandler.py +++ b/src/jarabe/view/gesturehandler.py @@ -13,61 +13,45 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from gi.repository import Gdk +import gi +gi.require_version('SugarExt', '2.0') +from gi.repository import Gdk from gi.repository import SugarExt -from gi.repository import SugarGestures + +try: + from gi.repository import Gtk +except ImportError: + Gtk = None from sugar4.graphics import style +import logging + _instance = None class GestureHandler(object): - '''Handling gestures to show/hide the frame - - We use SugarExt.GestureGrabber to listen for - gestures on the root window. We use a toggle - gesture to either hide or show the frame: Swiping - from the frame area at the top towards the center - does reveal the Frame or hide it. - ''' + '''Handling gestures to show/hide the frame using GTK4 standard event controllers''' def __init__(self, frame): self._frame = frame - - #self._gesture_grabber = SugarExt.GestureGrabber() - self._controller = [] - - screen = Gdk.Screen.get_default() - screen.connect('size-changed', self.__size_changed_cb) - - self._add_controller() - - def __size_changed_cb(self, screen): - self._add_controller() - - def _add_controller(self): - for controller in self._controller: - #self._gesture_grabber.remove(controller) - - self._track_gesture_for_area(SugarGestures.SwipeDirectionFlags.DOWN, - 0, 0, Gdk.Screen.width(), - style.GRID_CELL_SIZE) - - def _track_gesture_for_area(self, directions, x, y, width, height): - rectangle = Gdk.Rectangle() - rectangle.x = x - rectangle.y = y - rectangle.width = width - rectangle.height = height - swipe = SugarGestures.SwipeController(directions=directions) - swipe.connect('swipe-ended', self.__swipe_ended_cb) - self._gesture_grabber.add(swipe, rectangle) - self._controller.append(swipe) - - def __swipe_ended_cb(self, controller, event_direction): - self._frame.toggle() + self._controller = Gtk.GestureSwipe.new() + self._controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + self._controller.connect('swipe', self.__swipe_cb) + + # Attach to master window + from jarabe.model import shell + app = shell.get_model() + if app: + windows = app.get_windows() + if windows: + windows[0].add_controller(self._controller) + + def __swipe_cb(self, gesture, velocity_x, velocity_y): + # A simple swipe detection: if velocity is high enough, toggle frame + if abs(velocity_x) > 500 or abs(velocity_y) > 500: + self._frame.toggle() def setup(frame): diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py index 0e6b171fee..603f032e91 100644 --- a/src/jarabe/view/keyhandler.py +++ b/src/jarabe/view/keyhandler.py @@ -17,12 +17,17 @@ import os import logging import importlib +import types from gi.repository import GLib +from gi.repository import Gtk from gi.repository import Gdk #from gi.repository import SugarExt -from sugar4.test import uitree +try: + from sugar4.test import uitree +except ImportError: + uitree = None from jarabe.model.sound import sound from jarabe.model import shell @@ -33,11 +38,9 @@ from jarabe.journal import journalactivity from jarabe.controlpanel.gui import ControlPanel - _VOLUME_STEP = sound.VOLUME_STEP _VOLUME_MAX = 100 -_TABBING_MODIFIER = Gdk.ModifierType.MOD1_MASK - +_TABBING_MODIFIER = getattr(Gdk.ModifierType, 'ALT_MASK', getattr(Gdk.ModifierType, 'MOD1_MASK', 0)) _actions_table = { 'F1': 'zoom_mesh', @@ -79,12 +82,7 @@ def __init__(self, frame): self._key_pressed = None self._keycode_pressed = 0 self._keystate_pressed = 0 - - #self._key_grabber = SugarExt.KeyGrabber() - #self._key_grabber.connect('key-pressed', - # self._key_pressed_cb) - #self._key_grabber.connect('key-released', - # self._key_released_cb) + self._handled_windows = set() self._tabbing_handler = TabbingHandler(self._frame, _TABBING_MODIFIER) @@ -93,7 +91,7 @@ def __init__(self, frame): module_name = f[:-3] try: logging.debug('Loading module %r', module_name) - module = importlib.import_module('globalkey.' + module_name, globals()) + module = importlib.import_module('globalkey.' + module_name) for key in module.BOUND_KEYS: if key in _actions_table: raise ValueError('Key %r is already bound' % key) @@ -101,7 +99,28 @@ def __init__(self, frame): except Exception: logging.exception('Exception while loading extension:') - #self._key_grabber.grab_keys(list(_actions_table.keys())) + app = shell.get_model() + if app is not None: + main_window = getattr(app, '_main_window', None) + if main_window is not None: + self.add_window(main_window) + app.connect('window-added', self._on_window_added) + + def _on_window_added(self, application, window): + if isinstance(window, Gtk.Window): + self.add_window(window) + + def add_window(self, window): + if window in self._handled_windows: + return + self._handled_windows.add(window) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self._key_pressed_cb) + key_controller.connect('key-released', self._key_released_cb) + key_controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + window.add_controller(key_controller) + def _change_volume(self, step=None, value=None): if step is not None: @@ -122,7 +141,7 @@ def handle_next_window(self, event_time): def handle_close_window(self, event_time): active_activity = shell.get_model().get_active_activity() - if active_activity.is_journal(): + if active_activity is None or active_activity.is_journal(): return active_activity.stop() @@ -137,7 +156,14 @@ def handle_zoom_home(self, event_time): shell.get_model().set_zoom_level(ShellModel.ZOOM_HOME, event_time) def handle_zoom_activity(self, event_time): - shell.get_model().set_zoom_level(ShellModel.ZOOM_ACTIVITY, event_time) + model = shell.get_model() + if model.get_active_activity() is None: + # No activity open: show Journal instead of the empty compositor. + # Use get_journal() so the journal is initialized if needed — + # same pattern as handle_open_search (F5). + journalactivity.get_journal().reveal() + else: + model.set_zoom_level(ShellModel.ZOOM_ACTIVITY, event_time) def handle_volume_max(self, event_time): self._change_volume(value=_VOLUME_MAX) @@ -167,19 +193,22 @@ def handle_logout(self, event_time): GLib.timeout_add_seconds(3, session_manager.shutdown_completed) def handle_open_search(self, event_time): - journalactivity.get_journal().show_journal() + try: + journal = journalactivity.get_journal() + journal.show_journal() + except Exception as e: + logging.exception('Error in handle_open_search') def handle_open_controlpanel(self, event_time): shell_model = shell.get_model() activity = shell_model.get_active_activity() - if activity.has_shell_window(): + if activity is not None and activity.has_shell_window(): return - bundle_path = activity.get_bundle_path() + bundle_path = activity.get_bundle_path() if activity is not None else None if bundle_path is None: window_id = "" else: - # get activity name and window id window_id = activity.get_bundle_id() if shell.get_model().has_modal(): @@ -190,64 +219,85 @@ def handle_open_controlpanel(self, event_time): panel = ControlPanel(window_id) activity.push_shell_window(panel) panel.connect('hide', activity.pop_shell_window) - panel.show() + panel.set_visible(True) def handle_dump_ui_tree(self, event_time): - print(uitree.get_root().dump()) - - def _key_pressed_cb(self, grabber, keycode, state, event_time): - key = grabber.get_key(keycode, state) - logging.debug('_key_pressed_cb: %i %i %s', keycode, state, key) - if key is not None: - self._key_pressed = key - self._keycode_pressed = keycode - self._keystate_pressed = state - - # avoid switch to the Journal or change views if a modal dialog - # is opened http://bugs.sugarlabs.org/ticket/4601 - if key in _non_modal_action_keys and \ - shell.get_model().has_modal(): - logging.debug( - 'Key %s action stopped due to modal dialog open', key) - return - + if uitree: + print(uitree.get_root().dump()) + else: + logging.warning("uitree module not available.") + + def _key_pressed_cb(self, controller, keyval, keycode, state): + key = Gdk.keyval_name(keyval) + if not key: + return False + + # Add XF86 prefix back for media keys so they match _actions_table + if key in ['AudioMute', 'AudioLowerVolume', 'AudioRaiseVolume', 'MenuKB', 'WebCam', 'Search']: + key = 'XF86' + key + + # Handle Alt+ combinations + if state & Gdk.ModifierType.ALT_MASK: + if key == 'Tab': + if state & Gdk.ModifierType.SHIFT_MASK: + key = 'Tab' + else: + key = 'Tab' + elif key == 'Escape': + key = 'Escape' + elif key in ['F11', 'F12']: + key = '' + key + elif key in ['f', 'm', 'o', 'q', 'd']: + if state & Gdk.ModifierType.SHIFT_MASK: + key = '' + key + + if key in _actions_table: action = _actions_table[key] + + if key in _non_modal_action_keys and shell.get_model().has_modal(): + logging.debug('Key %s action stopped due to modal dialog open', key) + return True + + event = controller.get_current_event() + event_time = event.get_time() if event else 0 + if self._tabbing_handler.is_tabbing(): - # Only accept window tabbing events, everything else - # cancels the tabbing operation. if action not in ['next_window', 'previous_window']: self._tabbing_handler.stop(event_time) return True - if hasattr(action, 'handle_key_press'): + if isinstance(action, types.ModuleType) and hasattr(action, 'handle_key_press'): action.handle_key_press(key) elif isinstance(action, str): method = getattr(self, 'handle_' + action) method(event_time) else: - raise TypeError('Invalid action %r' % action) + logging.error('Invalid action %r', action) return True - # If this is not a registered key, then cancel tabbing. + if self._tabbing_handler.is_tabbing(): - if not grabber.is_modifier(keycode): - self._tabbing_handler.stop(event_time) + is_modifier = keyval in [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R, Gdk.KEY_Meta_L, Gdk.KEY_Meta_R, Gdk.KEY_Shift_L, Gdk.KEY_Shift_R] + if not is_modifier: + event = controller.get_current_event() + self._tabbing_handler.stop(event.get_time() if event else 0) return True return False - def _key_released_cb(self, grabber, keycode, state, event_time): + def _key_released_cb(self, controller, keyval, keycode, state): logging.debug('_key_released_cb: %i %i', keycode, state) if self._tabbing_handler.is_tabbing(): - # We stop tabbing and switch to the new window as soon as the - # modifier key is raised again. - if grabber.is_modifier(keycode, mask=_TABBING_MODIFIER): - self._tabbing_handler.stop(event_time) - - return True - return False + # stop tabbing and switch window when the modifier is released + if keyval in [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R, Gdk.KEY_Meta_L, Gdk.KEY_Meta_R]: + event = controller.get_current_event() + self._tabbing_handler.stop(event.get_time() if event else 0) def setup(frame): global _instance _instance = KeyHandler(frame) + + +def get_instance(): + return _instance diff --git a/src/jarabe/view/launcher.py b/src/jarabe/view/launcher.py index 2c1aa3b0b4..3c81095565 100644 --- a/src/jarabe/view/launcher.py +++ b/src/jarabe/view/launcher.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, Red Hat, Inc. +# Copyright (C) 2008, Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,41 +19,56 @@ from gi.repository import Gio from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GObject -from gi.repository import SugarExt from sugar4.graphics import style from jarabe.model import shell from jarabe.view.pulsingicon import PulsingIcon - _INTERVAL = 100 -class LaunchWindow(Gtk.Window): +class LaunchWindow(Gtk.Box): def __init__(self, activity_id, icon_path, icon_color): - Gtk.Window.__init__(self) - self.set_has_resize_grip(False) - - self.props.type_hint = Gdk.WindowTypeHint.SPLASHSCREEN - self.modify_bg(Gtk.StateType.NORMAL, style.COLOR_WHITE.get_gdk_color()) + super().__init__() + + self.add_css_class('launch-window') + + provider = Gtk.CssProvider() + css = b".launch-window { background-color: %s; }" % style.COLOR_WHITE.get_html().encode('utf-8') + provider.load_from_data(css) + Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + self.set_hexpand(True) + self.set_vexpand(True) canvas = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - canvas.show() - self.add(canvas) + canvas.set_visible(True) + self.append(canvas) + + display = Gdk.Display.get_default() + screen_height = 768 + screen_width = 1024 + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geo = monitors.get_item(0).get_geometry() + screen_width, screen_height = geo.width, geo.height - bar_size = Gdk.Screen.height() / 5 * 2 + bar_size = screen_height / 5 * 2 header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - header.set_size_request(-1, bar_size) - header.show() - canvas.pack_start(header, False, True, 0) + header.set_size_request(-1, int(bar_size)) + header.set_visible(True) + canvas.append(header) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - box.set_size_request(Gdk.Screen.width() / 5, -1) - box.show() - canvas.pack_start(box, True, True, 0) + box.set_size_request(int(screen_width / 5), -1) + box.set_vexpand(True) + box.set_visible(True) + canvas.append(box) self._activity_id = activity_id @@ -64,59 +79,55 @@ def __init__(self, activity_id, icon_path, icon_color): self._activity_icon.set_zooming(style.SMALL_ICON_SIZE, style.XLARGE_ICON_SIZE, 10) self._activity_icon.set_pulsing(True) - self._activity_icon.show() - box.pack_start(self._activity_icon, True, False, 0) + self._activity_icon.set_visible(True) + + self._activity_icon.set_hexpand(True) + self._activity_icon.set_halign(Gtk.Align.CENTER) + self._activity_icon.set_valign(Gtk.Align.CENTER) + box.append(self._activity_icon) footer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING) - footer.set_size_request(-1, bar_size) - footer.show() - canvas.pack_end(footer, False, True, 0) + footer.set_size_request(-1, int(bar_size)) + footer.set_visible(True) + canvas.append(footer) self.error_text = Gtk.Label() self.error_text.props.use_markup = True - footer.pack_start(self.error_text, False, True, 0) + footer.append(self.error_text) - button_box = Gtk.Alignment.new(0.5, 0, 0, 0) - button_box.show() - footer.pack_start(button_box, False, True, 0) - self.cancel_button = Gtk.Button(stock=Gtk.STOCK_STOP) - button_box.add(self.cancel_button) + button_box = Gtk.Box() + button_box.set_halign(Gtk.Align.CENTER) + button_box.set_valign(Gtk.Align.START) + button_box.set_visible(True) + footer.append(button_box) + + self.cancel_button = Gtk.Button.new_from_icon_name('process-stop') + button_box.append(self.cancel_button) - self.connect('realize', self.__realize_cb) - - screen = Gdk.Screen.get_default() - screen.connect('size-changed', self.__size_changed_cb) + if display: + monitors = display.get_monitors() + monitors.connect('items-changed', self.__size_changed_cb) self._home = shell.get_model() self._home.connect('active-activity-changed', self.__active_activity_changed_cb) - - self.connect('destroy', self.__destroy_cb) + self.connect('unrealize', self.__unrealize_cb) self._update_size() - def show(self): - self.present() - def _update_size(self): - self.resize(Gdk.Screen.width(), Gdk.Screen.height()) - - def __realize_cb(self, widget): - window = widget.get_window() - data = GObject.GObject() - setattr(data, 'activity_id', str(self._activity_id)) - window.set_user_data(data) + pass - def __size_changed_cb(self, screen): + def __size_changed_cb(self, list_model, position, removed, added): self._update_size() def __active_activity_changed_cb(self, model, activity): - if activity.get_activity_id() == self._activity_id: + if activity is not None and activity.get_activity_id() == self._activity_id: self._activity_icon.props.paused = False else: self._activity_icon.props.paused = True - def __destroy_cb(self, box): + def __unrealize_cb(self, widget): self._activity_icon.props.pulsing = False self._home.disconnect_by_func(self.__active_activity_changed_cb) @@ -140,9 +151,9 @@ def add_launcher(activity_id, icon_path, icon_color): return launch_window = LaunchWindow(activity_id, icon_path, icon_color) - launch_window.show() - model.add_window(launch_window) + model.stack.add_named(launch_window, f"launcher_{activity_id}") + model.stack.set_visible_child_name(f"launcher_{activity_id}") model.register_launcher(activity_id, launch_window) @@ -160,12 +171,12 @@ def __launch_failed_cb(home_model, home_activity): else: launcher.error_text.props.label = _('%s failed to start.') % \ home_activity.get_activity_name() - launcher.error_text.show() + launcher.error_text.set_visible(True) launcher.cancel_button.connect('clicked', __cancel_button_clicked_cb, home_activity) - launcher.cancel_button.show() + launcher.cancel_button.set_visible(True) def __cancel_button_clicked_cb(button, home_activity): @@ -179,12 +190,16 @@ def __launch_completed_cb(home_model, home_activity): def _destroy_launcher(home_activity): activity_id = home_activity.get_activity_id() - launcher = shell.get_model().get_launcher(activity_id) + model = shell.get_model() + launcher = model.get_launcher(activity_id) if launcher is None: if not home_activity.is_journal(): logging.error('Launcher was not registered for %s', activity_id) return - shell.get_model().unregister_launcher(activity_id) - shell.get_model().remove_window(launcher) - launcher.destroy() + model.unregister_launcher(activity_id) + if launcher.get_parent() == model.stack: + model.stack.remove(launcher) + if model.stack.get_visible_child() is None: + model.stack.set_visible_child_name("home") + diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py index 189cdbf1df..80f50a5841 100644 --- a/src/jarabe/view/palettes.py +++ b/src/jarabe/view/palettes.py @@ -32,7 +32,7 @@ from sugar4.graphics.icon import Icon from sugar4.graphics import style from sugar4.graphics.xocolor import XoColor -from sugar4.activity.i18n import pgettext +from gettext import pgettext from jarabe.model import shell from jarabe.view.viewsource import setup_view_source @@ -97,58 +97,56 @@ def setup_palette(self): self.menu_box = PaletteMenuBox() menu_item = PaletteMenuItem(_('Resume'), 'activity-start') - menu_item.connect('activate', self.__resume_activate_cb) + menu_item.connect('item-activated', self.__resume_activate_cb) self.menu_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) # TODO: share-with, keep menu_item = PaletteMenuItem(_('View Source'), 'view-source') - menu_item.connect('activate', self.__view_source__cb) + menu_item.connect('item-activated', self.__view_source__cb) menu_item.set_accelerator('Shift+Alt+V') self.menu_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) if should_show_view_help(self._home_activity): menu_item = PaletteMenuItem(_('View Help'), 'toolbar-help') - menu_item.connect('activate', self.__view_help__cb) + menu_item.connect('item-activated', self.__view_help__cb) menu_item.set_accelerator('Shift+Alt+H') self.menu_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) # avoid circular importing reference from jarabe.frame.notification import NotificationBox menu_item = NotificationBox(self._home_activity.get_activity_id()) - self.menu_box.append_item(menu_item, 0, 0) + self.menu_box.append_item(menu_item) separator = PaletteMenuItemSeparator() - menu_item.add(separator) - menu_item.reorder_child(separator, 0) - separator.show() + menu_item.prepend(separator) + separator.set_visible(True) separator = PaletteMenuItemSeparator() self.menu_box.append_item(separator) - separator.show() + separator.set_visible(True) menu_item = PaletteMenuItem(_('Stop'), 'activity-stop') - menu_item.connect('activate', self.__stop_activate_cb) + menu_item.connect('item-activated', self.__stop_activate_cb) self.menu_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) self.set_content(self.menu_box) - self.menu_box.show() + self.menu_box.set_visible(True) def __resume_activate_cb(self, menu_item): - self._home_activity.get_window().activate(Gtk.get_current_event_time()) + shell.get_model().activate_activity(self._home_activity) self.emit('done') def __view_source__cb(self, menu_item): setup_view_source(self._home_activity) shell_model = shell.get_model() if self._home_activity is not shell_model.get_active_activity(): - self._home_activity.get_window().activate( - Gtk.get_current_event_time()) + shell_model.activate_activity(self._home_activity) self.emit('done') def __view_help__cb(self, menu_item): @@ -165,8 +163,9 @@ def __stop_activate_cb(self, menu_item): def __kill_cb(self, pid, sig): try: os.kill(pid, sig) - finally: - return False + except (OSError, ProcessLookupError): + pass + return False class ActivityPalette(Palette): @@ -189,11 +188,11 @@ def __init__(self, activity_info): menu_item = PaletteMenuItem(text_label=_('Start new'), file_name=activity_info.get_icon(), xo_color=xo_color) - menu_item.connect('activate', self.__start_activate_cb) - self.menu_box.pack_end(menu_item, True, True, 0) - menu_item.show() + menu_item.connect('item-activated', self.__start_activate_cb) + self.menu_box.append_item(menu_item) + menu_item.set_visible(True) self.set_content(self.menu_box) - self.menu_box.show_all() + self.menu_box.set_visible(True) # TODO: start-with @@ -209,48 +208,51 @@ def __init__(self, home_activity): self._free_space_label = None BasePalette.__init__(self, home_activity) - self._label.set_accel(Gdk.KEY_F5, 0) def setup_palette(self): - self.set_primary_text(self._home_activity.get_title()) + self.set_primary_text(self._home_activity.get_title(), accel_path='F5') box = PaletteMenuBox() self.set_content(box) - box.show() + box.set_visible(True) menu_item = PaletteMenuItem(_('Show contents')) icon = Icon(file=self._home_activity.get_icon_path(), pixel_size=style.SMALL_ICON_SIZE, xo_color=self._home_activity.get_icon_color()) - menu_item.set_image(icon) - icon.show() + + menu_item.set_icon_widget(icon) + icon.set_visible(True) - menu_item.connect('activate', self.__open_activate_cb) + menu_item.connect('item-activated', self.__open_activate_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) separator = PaletteMenuItemSeparator() box.append_item(separator) - separator.show() + separator.set_visible(True) inner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) inner_box.set_spacing(style.DEFAULT_PADDING) box.append_item(inner_box, vertical_padding=0) - inner_box.show() + inner_box.set_visible(True) self._progress_bar = Gtk.ProgressBar() - inner_box.add(self._progress_bar) - self._progress_bar.show() + self._progress_bar.set_hexpand(True) + inner_box.append(self._progress_bar) + self._progress_bar.set_visible(True) self._free_space_label = Gtk.Label() - self._free_space_label.set_alignment(0.5, 0.5) - inner_box.add(self._free_space_label) - self._free_space_label.show() + self._free_space_label.set_halign(Gtk.Align.CENTER) + self._free_space_label.set_valign(Gtk.Align.CENTER) + self._free_space_label.set_hexpand(True) + inner_box.append(self._free_space_label) + self._free_space_label.set_visible(True) self.connect('popup', self.__popup_cb) def __open_activate_cb(self, menu_item): - self._home_activity.get_window().activate(Gtk.get_current_event_time()) + shell.get_model().activate_activity(self._home_activity) def __popup_cb(self, palette): stat = os.statvfs(env.get_profile_path()) @@ -273,42 +275,50 @@ def __init__(self, mount): self.content_box = PaletteMenuBox() self.set_content(self.content_box) - self.content_box.show() + self.content_box.set_visible(True) menu_item = PaletteMenuItem(pgettext('Volume', 'Remove')) icon = Icon(icon_name='media-eject', pixel_size=style.SMALL_ICON_SIZE) - menu_item.set_image(icon) - icon.show() + menu_item.set_icon_widget(icon) + icon.set_visible(True) - menu_item.connect('activate', self.__unmount_activate_cb) + menu_item.connect('item-activated', self.__unmount_activate_cb) self.content_box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) separator = PaletteMenuItemSeparator() self.content_box.append_item(separator) - separator.show() + separator.set_visible(True) free_space_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) free_space_box.set_spacing(style.DEFAULT_PADDING) - self.content_box.append_item(free_space_box, vertical_padding=0) - free_space_box.show() + self.content_box.append_item(free_space_box) + free_space_box.set_visible(True) self._progress_bar = Gtk.ProgressBar() - free_space_box.pack_start(self._progress_bar, True, True, 0) - self._progress_bar.show() + self._progress_bar.set_hexpand(True) + free_space_box.append(self._progress_bar) + self._progress_bar.set_visible(True) self._free_space_label = Gtk.Label() - self._free_space_label.set_alignment(0.5, 0.5) - free_space_box.pack_start(self._free_space_label, True, True, 0) - self._free_space_label.show() + self._free_space_label.set_halign(Gtk.Align.CENTER) + self._free_space_label.set_valign(Gtk.Align.CENTER) + self._free_space_label.set_hexpand(True) + free_space_box.append(self._free_space_label) + self._free_space_label.set_visible(True) self.connect('popup', self.__popup_cb) def __unmount_activate_cb(self, menu_item): flags = 0 + master_window = None + app = shell.get_model() + if app and app.get_windows(): + master_window = app.get_windows()[0] + mount_operation = Gtk.MountOperation( - parent=self.content_box.get_toplevel()) + parent=master_window) cancellable = None user_data = None self._mount.unmount_with_operation(flags, mount_operation, cancellable, diff --git a/src/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py index ac559d79d5..d9f1c41f5a 100644 --- a/src/jarabe/view/pulsingicon.py +++ b/src/jarabe/view/pulsingicon.py @@ -22,7 +22,6 @@ from sugar4.graphics import style from sugar4.graphics.icon import CanvasIcon - _INTERVAL = 100 _STEP = math.pi / 10 # must be a fraction of pi, for clean caching _MINIMAL_ALPHA_VALUE = 0.33 @@ -95,9 +94,8 @@ def __init__(self, interval=_INTERVAL, **kwargs): self._pulsing = False Icon.__init__(self, **kwargs) - + self.connect('unrealize', self.__destroy_cb) self._palette = None - self.connect('destroy', self.__destroy_cb) def set_pulse_color(self, pulse_color): self._pulse_color = pulse_color @@ -168,10 +166,10 @@ def _set_palette(self, palette): palette = property(_get_palette, _set_palette) - def __destroy_cb(self, icon): + def __destroy_cb(self, widget): self._pulser.stop() if self._palette is not None: - self._palette.destroy() + self._palette.unparent() class EventPulsingIcon(CanvasIcon): @@ -185,10 +183,9 @@ def __init__(self, interval=_INTERVAL, **kwargs): self._pulsing = False CanvasIcon.__init__(self, **kwargs) + self.connect('unrealize', self.__destroy_cb) - self.connect('destroy', self.__destroy_cb) - - def __destroy_cb(self, box): + def __destroy_cb(self, widget): self._pulser.stop() def set_pulse_color(self, pulse_color): diff --git a/src/jarabe/view/service.py b/src/jarabe/view/service.py index ec17ba3d74..c9e6d27778 100644 --- a/src/jarabe/view/service.py +++ b/src/jarabe/view/service.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2010 Collabora Ltd. # # This program is free software: you can redistribute it and/or modify @@ -18,12 +18,10 @@ import dbus from dbus import service -from gi.repository import Gtk from jarabe.model import shell from jarabe.model import bundleregistry - _DBUS_SERVICE = 'org.laptop.Shell' _DBUS_SHELL_IFACE = 'org.laptop.Shell' _DBUS_PATH = '/org/laptop/Shell' @@ -72,8 +70,8 @@ def ActivateActivity(self, activity_id): """ activity = self._shell_model.get_activity_by_id(activity_id) - if activity is not None and activity.get_window() is not None: - activity.get_window().activate(Gtk.get_current_event_time()) + if activity is not None: + self._shell_model.activate_activity(activity) return self._shell_model.get_launcher(activity_id) is None return False diff --git a/src/jarabe/view/tabbinghandler.py b/src/jarabe/view/tabbinghandler.py index 493b775b38..edf48ca313 100644 --- a/src/jarabe/view/tabbinghandler.py +++ b/src/jarabe/view/tabbinghandler.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008, Benjamin Berg +# Copyright (C) 2008, Benjamin Berg # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,7 +20,6 @@ from jarabe.model import shell - _RAISE_DELAY = 250 @@ -31,61 +30,12 @@ def __init__(self, frame, modifier): self._tabbing = False self._modifier = modifier self._timeout = None - self._keyboard = None - self._mouse = None - - display = Gdk.Display.get_default() - device_manager = display.get_device_manager() - devices = device_manager.list_devices(Gdk.DeviceType.MASTER) - for device in devices: - if device.get_source() == Gdk.InputSource.KEYBOARD: - self._keyboard = device - if device.get_source() == Gdk.InputSource.MOUSE: - self._mouse = device def _start_tabbing(self, event_time): if not self._tabbing: - logging.debug('Grabing the input.') - - screen = Gdk.Screen.get_default() - window = screen.get_root_window() - - keyboard_grab_result = self._keyboard.grab( - window, - Gdk.GrabOwnership.WINDOW, - False, - Gdk.EventMask.KEY_PRESS_MASK | - Gdk.EventMask.KEY_RELEASE_MASK, - None, - event_time) - - mouse_grab_result = self._mouse.grab( - window, - Gdk.GrabOwnership.WINDOW, - False, - Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.BUTTON_RELEASE_MASK, - None, - event_time) - - self._tabbing = (keyboard_grab_result == Gdk.GrabStatus.SUCCESS and - mouse_grab_result == Gdk.GrabStatus.SUCCESS) - - # Now test that the modifier is still active to prevent race - # conditions. We also test if one of the grabs failed. - mask = window.get_device_position(self._mouse)[3] - if not self._tabbing or not (mask & self._modifier): - logging.debug('Releasing grabs again.') - - # ungrab keyboard/pointer if the grab was successfull. - if keyboard_grab_result == Gdk.GrabStatus.SUCCESS: - self._keyboard.ungrab(event_time) - if mouse_grab_result == Gdk.GrabStatus.SUCCESS: - self._mouse.ungrab(event_time) - - self._tabbing = False - else: - self._frame.show() + self._tabbing = True + + self._frame.show() def __timeout_cb(self, event_time): self._activate_current(event_time) @@ -106,8 +56,8 @@ def _cancel_timeout(self): def _activate_current(self, event_time): home_model = shell.get_model() activity = home_model.get_tabbing_activity() - if activity and activity.get_window(): - activity.get_window().activate(event_time) + if activity: + shell.get_model().activate_activity(activity) def next_activity(self, event_time): if not self._tabbing: @@ -158,11 +108,9 @@ def previous_activity(self, event_time): def _activate_next_activity(self, event_time): next_activity = shell.get_model().get_next_activity() if next_activity: - next_activity.get_window().activate(event_time) + shell.get_model().activate_activity(next_activity) def stop(self, event_time): - self._keyboard.ungrab(event_time) - self._mouse.ungrab(event_time) self._tabbing = False self._frame.hide() diff --git a/src/jarabe/view/viewhelp.py b/src/jarabe/view/viewhelp.py index bcb5b2e783..36f669edb8 100644 --- a/src/jarabe/view/viewhelp.py +++ b/src/jarabe/view/viewhelp.py @@ -34,7 +34,6 @@ from jarabe.model import shell from jarabe.view.viewhelp_webkit2 import Browser - _logger = logging.getLogger('ViewHelp') _MODE_HELP = 0 @@ -86,7 +85,6 @@ def get_help_url_and_title(activity): title = activity_bundle.get_name() link_id = activity_bundle.get_bundle_id() - # get the help file name for the activity activity_path = _get_help_activity_path() if activity_path is None: return None, title @@ -139,7 +137,7 @@ def setup_view_help(activity): viewhelp = ViewHelp(activity, window_id) activity.push_shell_window(viewhelp) viewhelp.connect('hide', activity.pop_shell_window) - viewhelp.show() + viewhelp.set_visible(True) class ViewHelp(Gtk.Window): @@ -152,39 +150,57 @@ def __init__(self, activity, window_id): has_local_help = url is not None self._mode = _MODE_HELP if has_local_help else _MODE_SOCIAL_HELP - Gtk.Window.__init__(self) - box = Gtk.Box() - box.set_orientation(Gtk.Orientation.VERTICAL) - self.add(box) - box.show() + super().__init__() + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.set_margin_start(style.LINE_WIDTH) + box.set_margin_end(style.LINE_WIDTH) + box.set_margin_top(style.LINE_WIDTH) + box.set_margin_bottom(style.LINE_WIDTH) + self.set_child(box) + box.set_visible(True) self.set_modal(True) self.set_decorated(False) - self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) - self.set_border_width(style.LINE_WIDTH) - self.set_has_resize_grip(False) - width = Gdk.Screen.width() - style.GRID_CELL_SIZE * 2 - height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 2 - self.set_size_request(width, height) + # Set transient parent window + shell_model = shell.get_model() + if shell_model and shell_model._main_window: + self.set_transient_for(shell_model._main_window) + + display = Gdk.Display.get_default() + width = 1024 - style.GRID_CELL_SIZE * 2 + height = 768 - style.GRID_CELL_SIZE * 2 + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geo = monitors.get_item(0).get_geometry() + width = geo.width - style.GRID_CELL_SIZE * 2 + height = geo.height - style.GRID_CELL_SIZE * 2 + + self.set_default_size(width, height) self.connect('realize', self.__realize_cb) self.connect('hide', self.__hide_cb) - self.connect('key-press-event', self.__key_press_event_cb) + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) self._toolbar = Toolbar(title, has_local_help) - box.pack_start(self._toolbar, False, False, 0) - self._toolbar.show() + box.append(self._toolbar) + self._toolbar.set_visible(True) self._toolbar.connect('stop-clicked', self.__stop_clicked_cb) self._toolbar.connect('mode-changed', self.__mode_changed_cb) self._browser = Browser(self._toolbar) scrolled_window = Gtk.ScrolledWindow() - scrolled_window.add(self._browser.get_widget()) - scrolled_window.show() + scrolled_window.set_child(self._browser.get_widget()) + scrolled_window.set_vexpand(True) + scrolled_window.set_hexpand(True) + scrolled_window.set_visible(True) - box.pack_start(scrolled_window, True, True, 0) + box.append(scrolled_window) language = _get_current_language() self._help_state = None @@ -199,11 +215,13 @@ def __init__(self, activity, window_id): self._load_mode(self._mode) def __stop_clicked_cb(self, widget): - self.destroy() + self.close() - def __key_press_event_cb(self, window, event): - if event.keyval == Gdk.KEY_Escape: + def __key_press_event_cb(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: self.__stop_clicked_cb(None) + return True + return False def __mode_changed_cb(self, toolbar, mode): if mode == _MODE_HELP: @@ -222,14 +240,6 @@ def _load_mode(self, mode): self._social_help_uri) def __realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - window = self.get_window() - window.set_accept_focus(True) - if self.parent_window_id: - attributes = Gdk.WindowAttr() - attributes.window_type = Gdk.WindowType.FOREIGN - parent = Gdk.Window.new(None, attributes, None) - window.set_transient_for(parent) shell.get_model().push_modal() def __hide_cb(self, widget): @@ -237,7 +247,6 @@ def __hide_cb(self, widget): def _get_help_file(self, language, help_file): activity_path = _get_help_activity_path() - # check if exist a page for the language selected # if not, use the default page path = os.path.join(activity_path, 'html', language, help_file) if not os.path.isfile(path): @@ -246,7 +255,7 @@ def _get_help_file(self, language, help_file): return path -class Toolbar(Gtk.Toolbar): +class Toolbar(Gtk.Box): __gsignals__ = { 'back-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])), @@ -256,7 +265,7 @@ class Toolbar(Gtk.Toolbar): } def __init__(self, activity_name, has_local_help): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) self._add_separator(False) @@ -267,12 +276,12 @@ def __init__(self, activity_name, has_local_help): fill_color=style.COLOR_TRANSPARENT.get_svg(), stroke_color=style.COLOR_WHITE.get_svg()) help_button.set_icon_widget(icon) - icon.show() + icon.set_visible(True) help_button.props.tooltip = _('Help Manual') help_button.connect('toggled', self.__button_toggled_cb, _MODE_HELP) - self.insert(help_button, -1) - help_button.show() + self.append(help_button) + help_button.set_visible(True) self._add_separator(False) social_help_button = RadioToolButton() @@ -281,39 +290,41 @@ def __init__(self, activity_name, has_local_help): fill_color=style.COLOR_TRANSPARENT.get_svg(), stroke_color=style.COLOR_WHITE.get_svg()) social_help_button.set_icon_widget(icon) - icon.show() + icon.set_visible(True) social_help_button.props.tooltip = _('Social Help') - social_help_button.props.group = help_button + social_help_button.set_group(help_button) social_help_button.connect( 'toggled', self.__button_toggled_cb, _MODE_SOCIAL_HELP) - self.insert(social_help_button, -1) - social_help_button.show() + self.append(social_help_button) + social_help_button.set_visible(True) self._add_separator(False) self._back_button = ToolButton(icon_name='go-previous-paired') self._back_button.props.tooltip = _('Back') self._back_button.connect('clicked', self.__back_clicked_cb) - self.insert(self._back_button, -1) - self._back_button.show() + self.append(self._back_button) + self._back_button.set_visible(True) + self._forward_button = ToolButton(icon_name='go-next-paired') self._forward_button.props.tooltip = _('Forward') self._forward_button.connect('clicked', self.__forward_clicked_cb) - self.insert(self._forward_button, -1) - self._forward_button.show() + self.append(self._forward_button) + self._forward_button.set_visible(True) title = _('Help: %s') % activity_name self._label = Gtk.Label() self._label.set_markup('%s' % title) - self._label.set_alignment(0, 0.5) - self._add_widget(self._label) + self._label.set_halign(Gtk.Align.START) + self._label.set_valign(Gtk.Align.CENTER) + self._add_widget(self._label, expand=True) self._add_separator(True) stop = ToolButton(icon_name='dialog-cancel') stop.set_tooltip(_('Close')) stop.connect('clicked', self.__stop_clicked_cb) - self.insert(stop, -1) - stop.show() + self.append(stop) + stop.set_visible(True) def __stop_clicked_cb(self, widget): self.emit('stop-clicked') @@ -322,22 +333,20 @@ def __button_toggled_cb(self, button, mode): if button.props.active: self.emit('mode-changed', mode) - def _add_widget(self, widget): - tool_item = Gtk.ToolItem() - tool_item.add(widget) - widget.show() - self.insert(tool_item, -1) - tool_item.show() + def _add_widget(self, widget, expand=False): + if expand: + widget.set_hexpand(True) + self.append(widget) + widget.set_visible(True) def _add_separator(self, expand=False): - separator = Gtk.SeparatorToolItem() - separator.props.draw = False + spacer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) if expand: - separator.set_expand(True) + spacer.set_hexpand(True) else: - separator.set_size_request(style.DEFAULT_SPACING, -1) - self.insert(separator, -1) - separator.show() + spacer.set_size_request(style.DEFAULT_SPACING, -1) + self.append(spacer) + spacer.set_visible(True) def update_back_forward(self, can_go_back, can_go_forward): self._back_button.props.sensitive = can_go_back diff --git a/src/jarabe/view/viewhelp_webkit2.py b/src/jarabe/view/viewhelp_webkit2.py index 79ec956e18..a5f589dc79 100644 --- a/src/jarabe/view/viewhelp_webkit2.py +++ b/src/jarabe/view/viewhelp_webkit2.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013 Kalpa Welivitigoda +# Copyright (C) 2013 Kalpa Welivitigoda # Copyright (C) 2015-2016 Sam Parkinson # Copyright (C) 2016 James Cameron # @@ -18,16 +18,22 @@ import os import gi -gi.require_version('WebKit2', '4.1') -from gi.repository import WebKit2 +try: + gi.require_version('WebKit', '6.0') + from gi.repository import WebKit + WEBKIT_VER = 6 +except (ValueError, ImportError): + WebKit = None + WEBKIT_VER = 6 + from gi.repository import Gio from sugar4 import env def _get_current_language(): - locale = os.environ.get('LANG') + locale = os.environ.get('LANG', 'en_US.UTF-8') return locale.split('.')[0].split('_')[0].lower() @@ -36,23 +42,41 @@ class Browser(): def __init__(self, toolbar): self._toolbar = toolbar - context = WebKit2.WebContext.get_default() - cookie_manager = context.get_cookie_manager() - cookie_manager.set_persistent_storage( - os.path.join(env.get_profile_path(), 'social-help.cookies'), - WebKit2.CookiePersistentStorage.SQLITE) + if WebKit is None: + from gi.repository import Gtk + self._webview = Gtk.Label(label="WebKit 6.0 is not installed. Help cannot be displayed.") + self._webview.set_visible(True) + return + + if WEBKIT_VER == 6: + context = WebKit.WebContext.get_default() + cookie_manager = context.get_cookie_manager() + if hasattr(cookie_manager, 'set_persistent_storage'): + cookie_manager.set_persistent_storage( + os.path.join(env.get_profile_path(), 'social-help.cookies'), + WebKit.CookiePersistentStorage.SQLITE) + + self._webview = WebKit.WebView() + self._webview.get_context().register_uri_scheme( + 'help', self.__app_scheme_cb, None, None) + else: + context = WebKit.WebContext.get_default() + cookie_manager = context.get_cookie_manager() + cookie_manager.set_persistent_storage( + os.path.join(env.get_profile_path(), 'social-help.cookies'), + WebKit.CookiePersistentStorage.SQLITE) - self._webview = WebKit2.WebView() - self._webview.get_context().register_uri_scheme( - 'help', self.__app_scheme_cb, None) + self._webview = WebKit.WebView() + self._webview.get_context().register_uri_scheme( + 'help', self.__app_scheme_cb, None) self._webview.connect('load-changed', self.__load_changed_cb) toolbar.update_back_forward(False, False) toolbar.connect('back-clicked', self.__back_cb) toolbar.connect('forward-clicked', self.__forward_cb) - self._webview.show() + self._webview.set_visible(True) - def __app_scheme_cb(self, request, user_data): + def __app_scheme_cb(self, request, user_data=None, *args): path = request.get_path() if path.find('_images') > -1: if path.find('/%s/_images/' % _get_current_language()) > -1: @@ -66,30 +90,34 @@ def __app_scheme_cb(self, request, user_data): -1, Gio.content_type_guess(path, None)[0]) def __load_changed_cb(self, widget, event): + if WebKit is None: return self._toolbar.update_back_forward(self._webview.can_go_back(), self._webview.can_go_forward()) def __back_cb(self, widget): + if WebKit is None: return self._webview.go_back() def __forward_cb(self, widget): + if WebKit is None: return self._webview.go_forward() def save_state(self): - return self._webview.get_session_state() + # WebKit 6.0 removed get_session_state()/restore_session_state(). + # Fall back to simple URL-based state tracking. + if WebKit is None: + return None + uri = self._webview.get_uri() + return uri if uri else None def load_state(self, state, url): - if state is None: - self._webview.load_uri(url) - else: - self._webview.restore_session_state(state) - # this is what epiphany does: - # https://github.com/GNOME/epiphany/blob/ - # 04e7811c32ba8a2c980a77aac1316b77f0969057/src/ephy-session.c#L280 - bf_list = self._webview.get_back_forward_list() - item = bf_list.get_current_item() - if item is not None: - self._webview.go_to_back_forward_list_item(item) + # WebKit 6.0 removed restore_session_state(). + # state is now just a URI string (or None). + if WebKit is None: + return + target_url = state if state is not None else url + if target_url: + self._webview.load_uri(target_url) self._toolbar.update_back_forward(self._webview.can_go_back(), self._webview.can_go_forward()) diff --git a/src/jarabe/view/viewsource.py b/src/jarabe/view/viewsource.py index 07e83d6eae..030be37a82 100644 --- a/src/jarabe/view/viewsource.py +++ b/src/jarabe/view/viewsource.py @@ -23,13 +23,17 @@ from gettext import gettext as _ import gi -gi.require_version('GtkSource', '4') +try: + gi.require_version('GtkSource', '5') + from gi.repository import GtkSource +except (ValueError, ImportError): + GtkSource = None + from gi.repository import GObject from gi.repository import GLib from gi.repository import Pango from gi.repository import Gtk from gi.repository import Gdk -from gi.repository import GtkSource from gi.repository import GdkPixbuf import dbus from gi.repository import Gio @@ -56,7 +60,8 @@ _EXCLUDE_NAMES = ['.deps', '.libs'] _IMPORT_TYPES = {'sugar3': 3, 'from gi.repository import Gtk': 3, - 'sugar.': 2, 'import pygtk': 2, 'pygtk.require': 2} + 'sugar.': 2, 'import pygtk': 2, 'pygtk.require': 2, + 'sugar4': 4} _SOURCE_FONT = Pango.FontDescription('Monospace %d' % style.FONT_SIZE) @@ -100,7 +105,7 @@ def _get_toolkit_path(bundle_path, bundle_id): if _is_gtk3_activity(bundle_path, bundle_id): sugar_module = 'sugar3' else: - sugar_module = 'sugar' + sugar_module = 'sugar4' for path in sys.path: if path.endswith(('site-packages', 'dist-packages')): @@ -123,8 +128,7 @@ def setup_view_source(activity): 'org.freedesktop.DBus.Python.NotImplementedError'] if e.get_dbus_name() not in expected_exceptions: logging.exception('Exception occurred in HandleViewSource():') - except Exception: - logging.exception('Exception occurred in HandleViewSource():') + window_id = activity.get_bundle_id() if window_id is None: @@ -149,8 +153,7 @@ def setup_view_source(activity): 'org.freedesktop.DBus.Python.NotImplementedError'] if e.get_dbus_name() not in expected_exceptions: logging.exception('Exception occurred in GetDocumentPath():') - except Exception: - logging.exception('Exception occurred in GetDocumentPath():') + if bundle_path is None and document_path is None: _logger.debug('Activity without bundle_path nor document_path') @@ -165,7 +168,7 @@ def setup_view_source(activity): sugar_toolkit_path, activity.get_title()) activity.push_shell_window(view_source) view_source.connect('hide', activity.pop_shell_window) - view_source.show() + view_source.set_visible(True) class ViewSource(Gtk.Window): @@ -173,43 +176,63 @@ class ViewSource(Gtk.Window): def __init__(self, window_id, bundle_path, document_path, sugar_toolkit_path, title): - Gtk.Window.__init__(self) + super().__init__() + _logger.debug('ViewSource paths: %r %r %r', bundle_path, document_path, sugar_toolkit_path) self.set_modal(True) self.set_decorated(False) - self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) - self.set_border_width(style.LINE_WIDTH) - self.set_has_resize_grip(False) - width = Gdk.Screen.width() - style.GRID_CELL_SIZE * 2 - height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 2 - self.set_size_request(width, height) + # Set transient parent window + from jarabe.model import shell as _shell + shell_model = _shell.get_model() + if shell_model and shell_model._main_window: + self.set_transient_for(shell_model._main_window) + + display = Gdk.Display.get_default() + width = 1024 - style.GRID_CELL_SIZE * 2 + height = 768 - style.GRID_CELL_SIZE * 2 + if display: + monitors = display.get_monitors() + if monitors and monitors.get_n_items() > 0: + geo = monitors.get_item(0).get_geometry() + width = geo.width - style.GRID_CELL_SIZE * 2 + height = geo.height - style.GRID_CELL_SIZE * 2 + + self.set_default_size(width, height) self._parent_window_id = window_id self._sugar_toolkit_path = sugar_toolkit_path - self._gdk_window = self.get_root_window() - self.connect('realize', self.__realize_cb) - self.connect('destroy', self.__destroy_cb, document_path) - self.connect('key-press-event', self.__key_press_event_cb) + self.connect('map', self.__realize_cb) + self._document_path = document_path + + key_controller = Gtk.EventControllerKey() + key_controller.connect('key-pressed', self.__key_press_event_cb) + self.add_controller(key_controller) self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.add(self._vbox) - self._vbox.show() + self._vbox.set_margin_start(style.LINE_WIDTH) + self._vbox.set_margin_end(style.LINE_WIDTH) + self._vbox.set_margin_top(style.LINE_WIDTH) + self._vbox.set_margin_bottom(style.LINE_WIDTH) + self.set_child(self._vbox) + self._vbox.set_visible(True) toolbar = Toolbar(title, bundle_path, document_path, sugar_toolkit_path) - self._vbox.pack_start(toolbar, False, True, 0) + self._vbox.append(toolbar) toolbar.connect('stop-clicked', self.__stop_clicked_cb) toolbar.connect('source-selected', self.__source_selected_cb) - toolbar.show() + toolbar.set_visible(True) - pane = Gtk.HPaned() - self._vbox.pack_start(pane, True, True, 0) - pane.show() + pane = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) + pane.set_vexpand(True) + pane.set_hexpand(True) + self._vbox.append(pane) + pane.set_visible(True) self._selected_bundle_file = None self._selected_sugar_file = None @@ -233,14 +256,14 @@ def __init__(self, window_id, bundle_path, document_path, # Split the tree pane into two vertical panes, one of which # will be hidden - tree_panes = Gtk.VPaned() - tree_panes.show() + tree_panes = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) + tree_panes.set_visible(True) self._bundle_source_viewer = FileViewer(bundle_path, file_name) self._bundle_source_viewer.connect('file-selected', self.__file_selected_cb) - tree_panes.add1(self._bundle_source_viewer) - self._bundle_source_viewer.show() + tree_panes.set_start_child(self._bundle_source_viewer) + self._bundle_source_viewer.set_visible(True) self._sugar_source_viewer = None @@ -259,23 +282,25 @@ def __init__(self, window_id, bundle_path, document_path, self._sugar_source_viewer.connect('file-selected', self.__file_selected_cb) - tree_panes.add2(self._sugar_source_viewer) - self._sugar_source_viewer.hide() + tree_panes.set_end_child(self._sugar_source_viewer) + self._sugar_source_viewer.set_visible(False) - pane.add1(tree_panes) + pane.set_start_child(tree_panes) self._source_display = SourceDisplay() - pane.add2(self._source_display) - self._source_display.show() + pane.set_end_child(self._source_display) + self._source_display.set_visible(True) self._source_display.file_path = self._selected_bundle_file if document_path is not None: self._select_source(document_path) def add_alert(self, alert): - self._vbox.pack_start(alert, False, False, 0) - self._vbox.reorder_child(alert, 1) - alert.show() + if hasattr(self._vbox, 'prepend'): + self._vbox.prepend(alert) + else: + self._vbox.insert_child_after(alert, None) + alert.set_visible(True) def remove_alert(self, alert): self._vbox.remove(alert) @@ -288,17 +313,13 @@ def _calculate_char_width(self, char_count): return Pango.PIXELS(metrics.get_approximate_char_width()) * char_count def __realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - window = self.get_window() - window.set_accept_focus(True) - - attributes = Gdk.WindowAttr() - attributes.window_type = Gdk.WindowType.FOREIGN - parent = Gdk.Window.new(None, attributes, None) - window.set_transient_for(parent) + from jarabe.model import shell as _shell + _shell.get_model().push_modal() def __stop_clicked_cb(self, widget): - self.destroy() + from jarabe.model import shell as _shell + _shell.get_model().pop_modal() + self.close() def __source_selected_cb(self, widget, path): self._select_source(path) @@ -307,38 +328,45 @@ def _select_source(self, path): if os.path.isfile(path): _logger.debug('_select_source called with file: %r', path) self._source_display.file_path = path - self._bundle_source_viewer.hide() + self._bundle_source_viewer.set_visible(False) if self._sugar_source_viewer is not None: - self._sugar_source_viewer.hide() + self._sugar_source_viewer.set_visible(False) elif path == self._sugar_toolkit_path: _logger.debug('_select_source called with sugar toolkit path: %r', path) self._sugar_source_viewer.set_path(path) self._source_display.file_path = self._selected_sugar_file - self._sugar_source_viewer.show() - self._bundle_source_viewer.hide() + self._sugar_source_viewer.set_visible(True) + self._bundle_source_viewer.set_visible(False) else: _logger.debug('_select_source called with path: %r', path) self._bundle_source_viewer.set_path(path) self._source_display.file_path = self._selected_bundle_file - self._bundle_source_viewer.show() + self._bundle_source_viewer.set_visible(True) if self._sugar_source_viewer is not None: - self._sugar_source_viewer.hide() - - def __destroy_cb(self, window, document_path): - if document_path is not None and os.path.exists(document_path): - os.unlink(document_path) - - self._gdk_window.set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR)) - Gdk.flush() - - def __key_press_event_cb(self, window, event): - keyname = Gdk.keyval_name(event.keyval) + self._sugar_source_viewer.set_visible(False) + + def do_dispose(self): + if getattr(self, '_document_path', None) and os.path.exists(self._document_path): + try: + os.unlink(self._document_path) + except OSError: + pass + + surface = self.get_surface() + if surface: + surface.set_cursor(Gdk.Cursor.new_from_name('default')) + GObject.GObject.do_dispose(self) + + def __key_press_event_cb(self, controller, keyval, keycode, state): + keyname = Gdk.keyval_name(keyval) if keyname == 'Escape': - self.destroy() + self.close() + return True + return False def __file_selected_cb(self, file_viewer, file_path): if file_path is not None and os.path.isfile(file_path): @@ -356,7 +384,7 @@ class DocumentButton(RadioToolButton): def __init__(self, file_name, document_path, activity_name, title, bundle=False): - RadioToolButton.__init__(self) + super().__init__() self._document_path = document_path self._title = title @@ -371,11 +399,11 @@ def __init__(self, file_name, document_path, activity_name, title, pixel_size=style.STANDARD_ICON_SIZE, xo_color=XoColor(self._color)) self.set_icon_widget(icon) - icon.show() + icon.set_visible(True) box = PaletteMenuBox() self.props.palette.set_content(box) - box.show() + box.set_visible(True) if bundle: menu_item = PaletteMenuItem(_('Duplicate'), 'edit-duplicate', @@ -387,7 +415,7 @@ def __init__(self, file_name, document_path, activity_name, title, menu_item.connect('activate', self.__keep_in_journal_cb) box.append_item(menu_item) - menu_item.show() + menu_item.set_visible(True) def __show_duplicate_alert(self, menu_item): alert = ConfirmationAlert() @@ -395,10 +423,10 @@ def __show_duplicate_alert(self, menu_item): self._activity_name alert.props.msg = _('This may take a few minutes') alert.connect('response', self.__duplicate_alert_response_cb) - self.get_toplevel().add_alert(alert) + self.get_root().add_alert(alert) def __duplicate_alert_response_cb(self, alert, response_id): - self.get_toplevel().remove_alert(alert) + self.get_root().remove_alert(alert) if response_id == Gtk.ResponseType.OK: self.__set_busy_cursor(True) @@ -409,20 +437,22 @@ def internal_callback(new_alert): new_alert = Alert() new_alert.props.title = _("Duplicating activity...") - self.get_toplevel().add_alert(new_alert) + self.get_root().add_alert(new_alert) GLib.idle_add(internal_callback, new_alert) def __set_busy_cursor(self, busy): cursor = None if busy: - cursor = Gdk.Cursor(Gdk.CursorType.WATCH) + cursor = Gdk.Cursor.new_from_name('wait') else: - cursor = Gdk.Cursor(Gdk.CursorType.LEFT_PTR) + cursor = Gdk.Cursor.new_from_name('default') - gdk_window = self.get_root_window() - gdk_window.set_cursor(cursor) - Gdk.flush() + root = self.get_root() + if root: + surface = root.get_surface() + if surface: + surface.set_cursor(cursor) def __copy_to_home_cb(self, menu_item, copy_alert=None): """Make a local copy of the activity bundle in user_activities_path""" @@ -444,20 +474,20 @@ def async_copy_activity_tree(): customizebundle.generate_bundle(nick, new_basename) if copy_alert: - self.get_toplevel().remove_alert(copy_alert) + self.get_root().remove_alert(copy_alert) alert = NotifyAlert(10) alert.props.title = _('Duplicated') alert.props.msg = _('The activity has been duplicated') alert.connect('response', self.__alert_response_cb) - self.get_toplevel().add_alert(alert) + self.get_root().add_alert(alert) finally: self.__set_busy_cursor(False) GLib.idle_add(async_copy_activity_tree) else: if copy_alert: - self.get_toplevel().remove_alert(copy_alert) + self.get_root().remove_alert(copy_alert) self.__set_busy_cursor(False) @@ -467,10 +497,10 @@ def async_copy_activity_tree(): ' the activity again') alert.connect('response', self.__alert_response_cb) - self.get_toplevel().add_alert(alert) + self.get_root().add_alert(alert) def __alert_response_cb(self, alert, response_id): - self.get_toplevel().remove_alert(alert) + self.get_root().remove_alert(alert) def __keep_in_journal_cb(self, menu_item): mime_type = mime.get_from_file_name(self._document_path) @@ -500,7 +530,7 @@ def __internal_save_error_cb(self, err): self._jobject.destroy() -class Toolbar(Gtk.Toolbar): +class Toolbar(Gtk.Box): __gtype_name__ = 'SugarViewSourceToolbar' __gsignals__ = { @@ -510,7 +540,7 @@ class Toolbar(Gtk.Toolbar): } def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): - Gtk.Toolbar.__init__(self) + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) document_button = None self.bundle_path = bundle_path @@ -527,8 +557,8 @@ def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): activity_name, title) document_button.connect('toggled', self.__button_toggled_cb, document_path) - self.insert(document_button, -1) - document_button.show() + self.append(document_button) + document_button.set_visible(True) self._add_separator() if bundle_path is not None and os.path.exists(bundle_path): @@ -539,14 +569,14 @@ def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): fill_color=style.COLOR_TRANSPARENT.get_svg(), stroke_color=style.COLOR_WHITE.get_svg()) activity_button.set_icon_widget(icon) - icon.show() + icon.set_visible(True) if document_button is not None: - activity_button.props.group = document_button + activity_button.set_group(document_button) activity_button.props.tooltip = _('Activity Bundle Source') activity_button.connect('toggled', self.__button_toggled_cb, bundle_path) - self.insert(activity_button, -1) - activity_button.show() + self.append(activity_button) + activity_button.set_visible(True) self._add_separator() if sugar_toolkit_path is not None: @@ -556,16 +586,16 @@ def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): fill_color=style.COLOR_TRANSPARENT.get_svg(), stroke_color=style.COLOR_WHITE.get_svg()) sugar_button.set_icon_widget(icon) - icon.show() + icon.set_visible(True) if document_button is not None: - sugar_button.props.group = document_button + sugar_button.set_group(document_button) else: - sugar_button.props.group = activity_button + sugar_button.set_group(activity_button) sugar_button.props.tooltip = _('Sugar Toolkit Source') sugar_button.connect('toggled', self.__button_toggled_cb, sugar_toolkit_path) - self.insert(sugar_button, -1) - sugar_button.show() + self.append(sugar_button) + sugar_button.set_visible(True) self._add_separator() self.activity_title_text = _('View source: %s') % title @@ -573,7 +603,8 @@ def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): self.label = Gtk.Label() self.label.set_markup('%s' % self.activity_title_text) self.label.set_ellipsize(style.ELLIPSIZE_MODE_DEFAULT) - self.label.set_alignment(0, 0.5) + self.label.set_halign(Gtk.Align.START) + self.label.set_valign(Gtk.Align.CENTER) self._add_widget(self.label, expand=True) self._add_separator(False) @@ -581,28 +612,23 @@ def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): stop = ToolButton(icon_name='dialog-cancel') stop.set_tooltip(_('Close')) stop.connect('clicked', self.__stop_clicked_cb) - self.insert(stop, -1) - stop.show() + self.append(stop) + stop.set_visible(True) def _add_separator(self, expand=False): - separator = Gtk.SeparatorToolItem() - separator.props.draw = False + spacer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) if expand: - separator.set_expand(True) + spacer.set_hexpand(True) else: - separator.set_size_request(style.DEFAULT_SPACING, -1) - self.insert(separator, -1) - separator.show() + spacer.set_size_request(style.DEFAULT_SPACING, -1) + self.append(spacer) + spacer.set_visible(True) def _add_widget(self, widget, expand=False): - tool_item = Gtk.ToolItem() - tool_item.set_expand(expand) - - tool_item.add(widget) - widget.show() - - self.insert(tool_item, -1) - tool_item.show() + if expand: + widget.set_hexpand(True) + self.append(widget) + widget.set_visible(True) def __stop_clicked_cb(self, button): self.emit('stop-clicked') @@ -626,10 +652,9 @@ class FileViewer(Gtk.ScrolledWindow): } def __init__(self, path, initial_filename): - Gtk.ScrolledWindow.__init__(self) + super().__init__() - self.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC - self.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.set_size_request(style.GRID_CELL_SIZE * 3, -1) self._path = None @@ -637,8 +662,8 @@ def __init__(self, path, initial_filename): self._tree_view = Gtk.TreeView() self._tree_view.connect('cursor-changed', self.__cursor_changed_cb) - self.add(self._tree_view) - self._tree_view.show() + self.set_child(self._tree_view) + self._tree_view.set_visible(True) self._tree_view.props.headers_visible = False selection = self._tree_view.get_selection() @@ -706,24 +731,14 @@ class SourceDisplay(Gtk.ScrolledWindow): __gtype_name__ = 'SugarSourceDisplay' def __init__(self): - Gtk.ScrolledWindow.__init__(self) + super().__init__() - self.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC - self.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self._file_path = None def _replace(self, child): - self._remove_children() - self.add(child) - - def _replace_with_viewport(self, child): - self._remove_children() - self.add_with_viewport(child) - - def _remove_children(self): - for child in self.get_children(): - self.remove(child) + self.set_child(child) def _set_file_path(self, file_path): self._file_path = file_path @@ -745,17 +760,23 @@ def _set_file_path(self, file_path): self._show_image_viewer(icon='application-x-generic') def _show_text_viewer(self): + if GtkSource is None: + return False + source_buffer = GtkSource.Buffer() source_buffer.set_highlight_syntax(True) source_view = GtkSource.View(buffer=source_buffer) source_view.set_editable(False) - source_view.set_cursor_visible(True) source_view.set_show_line_numbers(True) source_view.set_show_right_margin(True) source_view.set_right_margin_position(80) - source_view.modify_font(_SOURCE_FONT) - # source_view.set_highlight_current_line(True) #FIXME: Ugly color + source_view.add_css_class('view-source-text') + + provider = Gtk.CssProvider() + css = b".view-source-text { font-family: monospace; font-size: %dpt; }" % style.FONT_SIZE + provider.load_from_string(css.decode('utf-8')) + Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) mime_type = mime.get_for_file(self._file_path) @@ -781,7 +802,7 @@ def _show_text_viewer(self): except UnicodeDecodeError: return False - source_view.show() + source_view.set_visible(True) self._replace(source_view) return True @@ -795,33 +816,43 @@ def _show_image_viewer(self, icon=None, image=False): media_box = ImageBox() if image: - image = Gtk.Image() - pixbuf = GdkPixbuf.Pixbuf.new_from_file(self._file_path) - image.set_from_pixbuf(pixbuf) - media_box.add(image) + image_widget = Gtk.Image.new_from_file(self._file_path) + media_box.append(image_widget) + image_widget.set_visible(True) if icon: - h = Gdk.Screen.width() / 3 - icon = Icon(icon_name=icon, pixel_size=h) - media_box.add(icon) + h = 1024 / 3 # approximate size + icon_widget = Icon(icon_name=icon, pixel_size=h) + media_box.append(icon_widget) + icon_widget.set_visible(True) - media_box.show_all() - self._replace_with_viewport(media_box) + media_box.set_visible(True) + self._replace(media_box) def _show_no_file(self): nofile_label = Gtk.Label() nofile_label.set_text(_("Please select a file in the left panel.")) - nofile_box = Gtk.EventBox() - nofile_box.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse('white')) + nofile_box = Gtk.Box() + nofile_box.add_css_class('nofile-box') + + provider = Gtk.CssProvider() + css = b".nofile-box { background-color: %s; }" % style.COLOR_WHITE.get_html().encode('utf-8') + provider.load_from_string(css.decode('utf-8')) + Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - nofile_box.add(nofile_label) - nofile_box.show_all() - self._replace_with_viewport(nofile_box) + nofile_box.append(nofile_label) + nofile_label.set_visible(True) + nofile_box.set_visible(True) + self._replace(nofile_box) -class ImageBox(Gtk.EventBox): +class ImageBox(Gtk.Box): __gtype_name__ = 'SugarViewSourceImageBox' def __init__(self): - Gtk.EventBox.__init__(self) + super().__init__() + self.set_halign(Gtk.Align.CENTER) + self.set_valign(Gtk.Align.CENTER) + + self.add_css_class('image-box') diff --git a/src/jarabe/webservice/account.py b/src/jarabe/webservice/account.py index 05a2c2d0a5..1ee4656f6a 100644 --- a/src/jarabe/webservice/account.py +++ b/src/jarabe/webservice/account.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales +# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales # Copyright (c) 2013 SugarLabs # # This program is free software: you can redistribute it and/or modify @@ -61,7 +61,7 @@ def get_shared_journal_entry(self): return NotImplemented -class SharedJournalEntry(): +class SharedJournalEntry(GObject.GObject): ''' SharedJournalEntry is a class used to intermediate between the online service and the Sugar UI elements (MenuItems used in the Journal UI) for online accounts. It provides stubs for public diff --git a/src/jarabe/webservice/accountsmanager.py b/src/jarabe/webservice/accountsmanager.py index 37a0b540af..facf0f0e2d 100644 --- a/src/jarabe/webservice/accountsmanager.py +++ b/src/jarabe/webservice/accountsmanager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 Walter Bender +# Copyright (c) 2013 Walter Bender # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -169,8 +169,17 @@ def _load_service_module(path, service_name): def _extend_icon_theme_search_path(path): - icon_theme = Gtk.IconTheme.get_default() - icon_search_path = icon_theme.get_search_path() + from gi.repository import Gdk + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + + # We convert them to paths. + icon_search_path = [] + for p in icon_theme.get_search_path(): + if hasattr(p, 'get_path'): + icon_search_path.append(p.get_path()) + else: + icon_search_path.append(p) + try: icon_path_dirs = os.listdir(path) except OSError as e: @@ -182,7 +191,7 @@ def _extend_icon_theme_search_path(path): icon_path = os.path.join(path, file) if os.path.isdir(icon_path) and \ icon_path not in icon_search_path: - icon_theme.append_search_path(icon_path) + icon_theme.add_search_path(icon_path) def get_account(service_name): diff --git a/tests/extensions/cpsection/webaccount/services/mock/service.py b/tests/extensions/cpsection/webaccount/services/mock/service.py index bd29dab8e7..305d774db7 100644 --- a/tests/extensions/cpsection/webaccount/services/mock/service.py +++ b/tests/extensions/cpsection/webaccount/services/mock/service.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales +# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from gi.repository import Gtk from jarabe.webservice import accountsmanager @@ -32,13 +31,15 @@ def get_icon_name(self): return _SERVICE_NAME def config_service_cb(self, widget, event, container): - label = Gtk.Label(_SERVICE_NAME) + label = Gtk.Label(label=_SERVICE_NAME) - for c in container.get_children(): - container.remove(c) + child = container.get_first_child() + while child: + next_child = child.get_next_sibling() + container.remove(child) + child = next_child - container.add(label) - container.show_all() + container.append(label) def get_service(): diff --git a/tests/extensions/cpsection/webaccount/web_service.py b/tests/extensions/cpsection/webaccount/web_service.py index 998acb120d..3a56f18e09 100644 --- a/tests/extensions/cpsection/webaccount/web_service.py +++ b/tests/extensions/cpsection/webaccount/web_service.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales +# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ class WebService(): def get_icon_name(self): - raise "Not implemented" + raise NotImplementedError("Not implemented") def config_service_cb(self, widget, event, container): - raise "Not implemented" + raise NotImplementedError("Not implemented") diff --git a/tests/extensions/webservice/mock/account.py b/tests/extensions/webservice/mock/account.py index a7c279b430..8ac9c60eae 100644 --- a/tests/extensions/webservice/mock/account.py +++ b/tests/extensions/webservice/mock/account.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 Walter Bender +# Copyright (c) 2013 Walter Bender # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -56,13 +56,13 @@ def get_refresh_menu(self): class ShareMenu(MenuItem): def __init__(self, metadata): MenuItem.__init__(self, text_label=ACCOUNT_NAME) - self.show() + self.set_visible(True) class RefreshMenu(MenuItem): def __init__(self): MenuItem.__init__(self, text_label=ACCOUNT_NAME) - self.show() + self.set_visible(True) def set_metadata(self, metadata): return diff --git a/tests/test_activitieslist.py b/tests/test_activitieslist.py index 204948bc02..46e9357be4 100644 --- a/tests/test_activitieslist.py +++ b/tests/test_activitieslist.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Daniel Narvaez +# Copyright (C) 2013, Daniel Narvaez # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tests/test_backup.py b/tests/test_backup.py index 0fe832c146..e992b89476 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014, Gonzalo Odiard +# Copyright (C) 2014, Gonzalo Odiard # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tests/test_bundleregistry.py b/tests/test_bundleregistry.py index ecdaff3a3a..55d434d897 100644 --- a/tests/test_bundleregistry.py +++ b/tests/test_bundleregistry.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, One Laptop per Child +# Copyright (C) 2013, One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tests/test_desktop_config.py b/tests/test_desktop_config.py index c96b5aea35..1c39c5a49b 100644 --- a/tests/test_desktop_config.py +++ b/tests/test_desktop_config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender +# Copyright (C) 2013, Walter Bender # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tests/test_downloader.py b/tests/test_downloader.py index 8c880009af..60d491302e 100644 --- a/tests/test_downloader.py +++ b/tests/test_downloader.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, One Laptop per Child +# Copyright (C) 2013, One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tests/test_journaltoolbox.py b/tests/test_journaltoolbox.py index 907f0a60c6..83e48da671 100644 --- a/tests/test_journaltoolbox.py +++ b/tests/test_journaltoolbox.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012, Daniel Narvaez +# Copyright (C) 2012, Daniel Narvaez # Copyright (C) 2013, Walter Bender # # This program is free software: you can redistribute it and/or modify diff --git a/tests/test_microformat.py b/tests/test_microformat.py index d02def247e..86cbe0d827 100644 --- a/tests/test_microformat.py +++ b/tests/test_microformat.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, One Laptop per Child +# Copyright (C) 2013, One Laptop per Child # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tests/test_modemconfiguration.py b/tests/test_modemconfiguration.py index b8b61e5454..5ad85ce94c 100644 --- a/tests/test_modemconfiguration.py +++ b/tests/test_modemconfiguration.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- encoding: utf-8 -*- # Copyright (C) 2013, Miguel González # # This program is free software: you can redistribute it and/or modify @@ -17,7 +17,7 @@ import unittest from xml.etree.cElementTree import ElementTree -from mock import patch +from unittest.mock import patch from cpsection.modemconfiguration.model import CountryCodeParser, \ ServiceProvidersParser, ServiceProviders, PROVIDERS_PATH @@ -31,9 +31,8 @@ def test_get_country(self): self.assertEqual(CountryCodeParser().get('es'), 'Spain') self.assertEqual(CountryCodeParser().get('zw'), 'Zimbabwe') - def test_raise_if_not_found(self): - with self.assertRaises(KeyError): - CountryCodeParser().get('xx') + def test_fallback_if_not_found(self): + self.assertEqual(CountryCodeParser().get('xx'), 'xx') class ServiceProvidersParserTest(unittest.TestCase): diff --git a/tests/test_user_profile.py b/tests/test_user_profile.py index f9950de583..c42cbd2b73 100644 --- a/tests/test_user_profile.py +++ b/tests/test_user_profile.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # Copyright (C) 2014, Ignacio Rodriguez # # This program is free software: you can redistribute it and/or modify @@ -17,7 +17,7 @@ import unittest from cpsection.aboutme import model -from jarabe.intro.agepicker import AGES +from jarabe.intro.agepicker import get_group_labels from jarabe.intro.genderpicker import GENDERS TEST_NICKS = ['Ajay Garg', 'Aleksey Lim', @@ -44,7 +44,7 @@ class TestUserProfile(unittest.TestCase): def test_user_gender_age(self): for gender in GENDERS: - for age in AGES: + for age in get_group_labels().AGES: model.set_gender(gender) model.set_age(age) self.assertEqual(model.get_age(), age) diff --git a/tests/test_webaccount.py b/tests/test_webaccount.py index c429b197ee..292b0be82a 100644 --- a/tests/test_webaccount.py +++ b/tests/test_webaccount.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012, Daniel Narvaez +# Copyright (C) 2012, Daniel Narvaez # Copyright (C) 2013, Walter Bender # # This program is free software: you can redistribute it and/or modify diff --git a/tests/test_webservice.py b/tests/test_webservice.py index 56de599626..e3fec91479 100644 --- a/tests/test_webservice.py +++ b/tests/test_webservice.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender +# Copyright (C) 2013, Walter Bender # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,7 +17,9 @@ import sys import unittest -from gi.repository import Gtk +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import GLib, Gdk, Gtk from jarabe import config from jarabe.webservice.account import Account @@ -47,7 +49,7 @@ def test_get_description(self): self.assertTrue(found_mock_account) def test_icon_theme(self): - icon_theme = Gtk.IconTheme.get_default() + icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) icon_search_path = icon_theme.get_search_path() icon_path = os.path.join(web_extension_dir, ACCOUNT_NAME, 'icons') self.assertTrue(icon_path in icon_search_path) diff --git a/tests/views/activitieslist.py b/tests/views/activitieslist.py index e954b478ac..dd14fac896 100644 --- a/tests/views/activitieslist.py +++ b/tests/views/activitieslist.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Daniel Narvaez +# Copyright (C) 2013, Daniel Narvaez # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,13 +15,14 @@ import os +import gi +gi.require_version('Gtk', '4.0') from gi.repository import Gtk from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) from jarabe.desktop.activitieslist import ActivityListPalette - tests_dir = os.getcwd() base_dir = os.path.dirname(tests_dir) data_dir = os.path.join(tests_dir, "data") @@ -49,11 +50,11 @@ def get_path(self): def is_user_activity(self): return True - os.environ["SUGAR_MIME_DEFAULTS"] = \ os.path.join(base_dir, "data", "mime.defaults") palette = ActivityListPalette(MockActivityInfo()) palette.popup() -Gtk.main() +import GLib +GLib.MainLoop().run() diff --git a/tests/views/gtk_main.py b/tests/views/gtk_main.py index 2dc088d18f..f0b26e0ca2 100644 --- a/tests/views/gtk_main.py +++ b/tests/views/gtk_main.py @@ -13,6 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import gi +gi.require_version('Gtk', '4.0') from gi.repository import Gtk -Gtk.main() +import GLib +GLib.MainLoop().run() diff --git a/tests/views/journal_detailstoolbox.py b/tests/views/journal_detailstoolbox.py index 77e4feefff..b29ac46490 100644 --- a/tests/views/journal_detailstoolbox.py +++ b/tests/views/journal_detailstoolbox.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013, Walter Bender +# Copyright (C) 2013, Walter Bender # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,6 +16,8 @@ import os import sys +import gi +gi.require_version('Gtk', '4.0') from gi.repository import Gtk from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) @@ -32,7 +34,6 @@ class JournalMock(JournalWindow): def get_mount_point(self): return '/' - tests_dir = os.getcwd() extension_dir = os.path.join(tests_dir, 'extensions') @@ -43,12 +44,13 @@ def get_mount_point(self): window = Gtk.Window() toolbox = DetailToolbox(JournalMock()) -toolbox.show() +toolbox.set_visible(True) -window.add(toolbox) -window.show() +window.set_child(toolbox) +window.present() toolbox.set_metadata({'mountpoint': '/', 'uid': '', 'title': 'mock'}) toolbox._copy.palette.popup(immediate=True) -Gtk.main() +import GLib +GLib.MainLoop().run() diff --git a/tests/views/webaccount.py b/tests/views/webaccount.py index 057e9e51d5..773d339ecb 100644 --- a/tests/views/webaccount.py +++ b/tests/views/webaccount.py @@ -16,6 +16,8 @@ import os import sys +import gi +gi.require_version('Gtk', '4.0') from gi.repository import Gtk from jarabe import config @@ -32,15 +34,16 @@ sys.path.append(config.ext_path) window = Gtk.Window() -box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) -box.show() -window.add(box) +box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) +box.set_visible(True) +window.set_child(box) services = accountsmanager.get_webaccount_services() for service in services: if service.get_icon_name() == ACCOUNT_NAME: service.config_service_cb(None, None, box) -window.show() +window.present() -Gtk.main() +import GLib +GLib.MainLoop().run()