diff options
author | Gonzalo Odiard <godiard@gmail.com> | 2013-05-23 21:35:53 (GMT) |
---|---|---|
committer | Gonzalo Odiard <godiard@gmail.com> | 2013-05-23 21:35:53 (GMT) |
commit | 1bdaa38656f1ed3da04d0d52620e0988356900cc (patch) | |
tree | 5c34a237653258b20d83b61b009c8790db6d14c8 | |
parent | 29a1a65e4f6279a20769b2e1c4769c2e811adfe6 (diff) |
Changes in support of touch
In support of touch, we have:
* switched from combo boxes to menu items;
* used gtk.fixed to position a gtk.textview in order to invoke the OSK
* added support for screen rotation
* added support for dragging the screen
Signed-by: Walter Bender <walter@sugarlabs.org>
Reviewed-by: Gonzalo Odiard <gonzalo@laptop.org>
-rw-r--r-- | NEWS | 5 | ||||
-rw-r--r-- | labyrinthactivity.py | 744 | ||||
-rw-r--r-- | src/MMapArea.py | 178 | ||||
-rw-r--r-- | src/TextThought.py | 411 | ||||
-rw-r--r-- | src/utils.py | 2 |
5 files changed, 973 insertions, 367 deletions
@@ -133,3 +133,8 @@ v0.0.2 ------ Initial release + +* Add gtk.fixed for positioning textviews (for OSK) +* Reconfigure toolbars to fit in portrait mode +* Refresh display when changing screen dimensions (rotation) +* Replace combo boxes with buttons/palettes (more touch friendly) diff --git a/labyrinthactivity.py b/labyrinthactivity.py index 3340bc6..5879835 100644 --- a/labyrinthactivity.py +++ b/labyrinthactivity.py @@ -23,28 +23,32 @@ from gettext import gettext as _ import xml.dom.minidom as dom import gtk +import gobject import pango import pangocairo import cairo from sugar.activity import activity from sugar.graphics.toolbutton import ToolButton -from sugar.graphics.toolcombobox import ToolComboBox from sugar.graphics.radiotoolbutton import RadioToolButton from sugar.graphics.colorbutton import ColorToolButton from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon from sugar.datastore import datastore +from sugar.graphics import style from port.tarball import Tarball try: - # >= 0.86 toolbars - from sugar.graphics.toolbarbox import ToolbarButton, ToolbarBox - from sugar.activity.widgets import ActivityToolbarButton - from sugar.activity.widgets import StopButton + from sugar.graphics.toolbarbox import ToolbarBox + HASTOOLBARBOX = True except ImportError: - # <= 0.84 toolbars + HASTOOLBARBOX = False pass +if HASTOOLBARBOX: + from sugar.graphics.toolbarbox import ToolbarButton + from sugar.activity.widgets import ActivityToolbarButton + from sugar.activity.widgets import StopButton # labyrinth sources are shipped inside the 'src' subdirectory sys.path.append(os.path.join(activity.get_bundle_path(), 'src')) @@ -56,6 +60,78 @@ import utils EMPTY = -800 +def stop_editing(main_area): + if len(main_area.selected) == 1: + if hasattr(main_area.selected[0], 'textview'): + main_area.selected[0].remove_textview() + + +class MyMenuItem(MenuItem): + + def __init__(self, text_label=None, icon_name=None, text_maxlen=60, + xo_color=None, file_name=None, image=None): + super(MenuItem, self).__init__() + self._accelerator = None + self.props.submenu = None + + label = gtk.AccelLabel(text_label) + label.set_alignment(0.0, 0.5) + label.set_accel_widget(self) + if text_maxlen > 0: + label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + label.set_max_width_chars(text_maxlen) + self.add(label) + label.show() + + if image is not None: + self.set_image(image) + image.show() + + elif icon_name is not None: + icon = Icon(icon_name=icon_name, + icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR) + if xo_color is not None: + icon.props.xo_color = xo_color + self.set_image(icon) + icon.show() + + elif file_name is not None: + icon = Icon(file=file_name, icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR) + if xo_color is not None: + icon.props.xo_color = xo_color + self.set_image(icon) + icon.show() + + +class FontImage(gtk.Image): + + _FONT_ICON = \ +'<?xml version="1.0" encoding="UTF-8" standalone="no"?>\ +<svg\ + version="1.1"\ + width="27.5"\ + height="27.5"\ + viewBox="0 0 27.5 27.5">\ +<text\ + x="5"\ + y="21"\ + style="font-size:25px;fill:#ffffff;stroke:none"><tspan\ + x="5"\ + y="21"\ + style="font-family:%s">F</tspan></text>\ +</svg>' + + def __init__(self, font_name): + super(gtk.Image, self).__init__() + + pl = gtk.gdk.PixbufLoader('svg') + pl.write(self._FONT_ICON % (font_name)) + pl.close() + pixbuf = pl.get_pixbuf() + self.set_from_pixbuf(pixbuf) + self.show() + + class EditToolbar(activity.EditToolbar): def __init__(self, _parent): activity.EditToolbar.__init__(self) @@ -72,10 +148,19 @@ class EditToolbar(activity.EditToolbar): menu_item.show() self.copy.get_palette().menu.append(menu_item) + self.insert(gtk.SeparatorToolItem(), -1) + + self.erase_button = ToolButton('edit-delete') + self.erase_button.set_tooltip(_('Erase selected thought(s)')) + self.erase_button.connect('clicked', self.__delete_cb) + self.insert(self.erase_button, -1) + + self.show_all() self.clipboard = gtk.Clipboard() self.copy.child.set_sensitive(False) self.paste.child.set_sensitive(False) + self.erase_button.set_sensitive(False) def __undo_cb(self, button): self._parent._undo.undo_action(None) @@ -92,6 +177,18 @@ class EditToolbar(activity.EditToolbar): def __paste_cb(self, event): self._parent._main_area.paste_clipboard(self.clipboard) + def __delete_cb(self, widget): + self._stop_moving() + self.stop_dragging() + self._parent._main_area.delete_selected_elements() + + def stop_dragging(self): + if self._parent._main_area.is_dragging(): + self._parent._main_area.drag_menu_cb(self._sw, False) + + def _stop_moving(self): + self._parent._main_area.move_mode = False + class ViewToolbar(gtk.Toolbar): def __init__(self, main_area): @@ -126,22 +223,50 @@ class ViewToolbar(gtk.Toolbar): self.show_all() def __zoom_in_cb(self, button): + stop_editing(self._main_area) self._main_area.scale_fac *= 1.2 + hadj = self._main_area.sw.get_hadjustment() + hadj.set_upper(hadj.get_upper() * 1.2) + vadj = self._main_area.sw.get_vadjustment() + vadj.set_upper(vadj.get_upper() * 1.2) self._main_area.invalidate() def __zoom_out_cb(self, button): + stop_editing(self._main_area) self._main_area.scale_fac /= 1.2 + hadj = self._main_area.sw.get_hadjustment() + hadj.set_upper(hadj.get_upper() / 1.2) + vadj = self._main_area.sw.get_vadjustment() + vadj.set_upper(vadj.get_upper() / 1.2) self._main_area.invalidate() def __zoom_original_cb(self, button): + stop_editing(self._main_area) self._main_area.scale_fac = 1.0 + self._main_area.translation[0] = 0 + self._main_area.translation[1] = 0 + hadj = self._main_area.sw.get_hadjustment() + hadj.set_lower(0) + hadj.set_upper(max(gtk.gdk.screen_width(), gtk.gdk.screen_height())) + vadj = self._main_area.sw.get_vadjustment() + vadj.set_lower(0) + vadj.set_upper(max(gtk.gdk.screen_width(), gtk.gdk.screen_height())) self._main_area.invalidate() def __zoom_tofit_cb(self, button): + stop_editing(self._main_area) bounds = self.__get_thought_bounds() self._main_area.translation[0] = bounds['x'] self._main_area.translation[1] = bounds['y'] self._main_area.scale_fac = bounds['scale'] + hadj = self._main_area.sw.get_hadjustment() + hadj.set_lower(0) + hadj.set_upper(max(gtk.gdk.screen_width(), + gtk.gdk.screen_height()) * bounds['scale']) + vadj = self._main_area.sw.get_vadjustment() + vadj.set_lower(0) + vadj.set_upper(max(gtk.gdk.screen_width(), + gtk.gdk.screen_height()) * bounds['scale']) self._main_area.invalidate() def __get_thought_bounds(self): @@ -164,13 +289,18 @@ class ViewToolbar(gtk.Toolbar): upper = t.ul[1] width = right - left height = lower - upper - geom = self._main_area.window.get_geometry() - overlap = (width - geom[2], height - geom[3]) - # Leave 10% space around the edge - width_scale = float(geom[2]) / (width * 1.1) - height_scale = float(geom[3]) / (height * 1.1) - return {'x': (geom[2] / 2.0) - (width / 2.0 + left), - 'y': (geom[3] / 2.0) - (height / 2.0 + upper), + ''' + screen_width = self._main_area.window.get_geometry()[2] + screen_height = self._main_area.window.get_geometry()[3] + ''' + screen_width = gtk.gdk.screen_width() + screen_height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE + overlap = (width - screen_width, height - screen_height) + width_scale = float(screen_width) / (width * 1.1) + height_scale = float(screen_height) / (height * 1.1) + return {'x': (screen_width / 2.0) - (width / 2.0 + left), + 'y': (screen_height / 2.0) - (height / 2.0 + upper) + \ + style.GRID_CELL_SIZE, 'scale': min(width_scale, height_scale)} @@ -179,16 +309,83 @@ class TextAttributesToolbar(gtk.Toolbar): gtk.Toolbar.__init__(self) self._main_area = main_area + self._font_list = ['ABC123', 'Sans', 'Serif', 'Monospace', 'Symbol'] + self._font_sizes = ['8', '9', '10', '11', '12', '14', '16', '20', + '22', '24', '26', '28', '36', '48', '72'] + self._font_black_list = ['abc123', + 'Bitstream Charter', + 'Cantarell', + 'Century Schoolbook L', + 'Courier 10 Pitch', + 'Cursor', + 'DejaVu LGC Sans Mono', + 'DejaVu Sans', + 'DejaVu Sans Condensed', + 'DejaVu Sans Mono', + 'DejaVu Serif', + 'DejaVu Serif Condensed', + 'Droid Sans', + 'Droid Sans Mono', + 'Droid Serif', + 'FreeMono', + 'FreeSans', + 'FreeSerif', + 'Khmer OS Content', + 'Khmer OS System', + 'Liberation Mono', + 'Liberation Sans', + 'Liberation Serif', + 'LKLUG', + 'Nimbus Mono L', + 'Nimbus Roman No9 L', + 'Nimbus Sans L', + 'PT Sans', + 'PT Sans Narrow', + 'Standard Symbols L', + 'STIX', + 'STIX Math', + 'URW Bookman L', + 'URW Chancery L', + 'URW Gothic L', + 'URW Palladio L', + 'Utopia', + 'VL Gothic', + 'VL PGothic', + 'cmex10', 'cmmi10', 'cmr10', 'cmsy10', + 'esint10', 'eufm10', 'msam10', 'msbm10', + 'rsfs10', 'uming', 'wasy10', + ] + + self.font_button = ToolButton('font-text') + self.font_button.set_tooltip(_('Select font')) + self.font_button.connect('clicked', self.__font_selection_cb) + self.insert(self.font_button, -1) + self._setup_font_palette() + + self.insert(gtk.SeparatorToolItem(), -1) + + self.font_size_up = ToolButton('resize+') + self.font_size_up.set_tooltip(_('Bigger')) + self.font_size_up.connect('clicked', self.__font_sizes_cb, True) + self.insert(self.font_size_up, -1) + + if len(self._main_area.selected) > 0: + font_size = self._main_area.font_size + else: + font_size = utils.default_font_size + self.size_label = gtk.Label(str(font_size)) + self.size_label.show() + toolitem = gtk.ToolItem() + toolitem.add(self.size_label) + toolitem.show() + self.insert(toolitem, -1) - self.fonts_combo_box = ToolComboBox(self.__get_fonts_combo_box()) - self.fonts_combo_box.combo.connect('changed', self.__fonts_cb) - self.insert(self.fonts_combo_box, -1) + self.font_size_down = ToolButton('resize-') + self.font_size_down.set_tooltip(_('Smaller')) + self.font_size_down.connect('clicked', self.__font_sizes_cb, False) + self.insert(self.font_size_down, -1) - self.font_sizes_combo_box = ToolComboBox(self. - __get_font_sizes_combo_box()) - self.font_sizes_combo_box.combo.connect('changed', - self.__font_sizes_cb) - self.insert(self.font_sizes_combo_box, -1) + self.insert(gtk.SeparatorToolItem(), -1) self.bold = ToolButton('bold-text') self.bold.set_tooltip(_('Bold')) @@ -205,39 +402,54 @@ class TextAttributesToolbar(gtk.Toolbar): self.underline.connect('clicked', self.__underline_cb) self.insert(self.underline, -1) - self.foreground_color = ColorToolButton() - self.foreground_color.connect('color-set', self.__foreground_color_cb) - self.insert(self.foreground_color, -1) + foreground_color = ColorToolButton() + foreground_color.set_title(_('Set font color')) + foreground_color.connect('color-set', self.__foreground_color_cb) + self.insert(foreground_color, -1) + + bakground_color = ColorToolButton() + bakground_color.set_title(_('Set background color')) + bakground_color.connect('color-set', self.__background_color_cb) + bakground_color.set_color(gtk.gdk.Color(65535, 65535, 65535)) + self.insert(bakground_color, -1) self.show_all() - def __get_fonts_combo_box(self): - context = self._main_area.pango_context - fonts_combo_box = gtk.combo_box_new_text() - fonts = context.list_families() - index_tnr = -1 - for index, font in enumerate(fonts): - pango_font = pango.FontDescription(font.get_name()) - font_name = pango_font.to_string() - fonts_combo_box.append_text(font_name) - if font_name in ['Times New', 'Times New Roman', 'Sans']: - index_tnr = index - if index_tnr == -1: - fonts_combo_box.set_active(0) - else: - fonts_combo_box.set_active(index_tnr) + def __font_selection_cb(self, widget): + if self._font_palette: + if not self._font_palette.is_up(): + self._font_palette.popup(immediate=True, + state=self._font_palette.SECONDARY) + else: + self._font_palette.popdown(immediate=True) + return - return fonts_combo_box + def _setup_font_palette(self): + context = self._main_area.pango_context + for family in context.list_families(): + name = pango.FontDescription(family.get_name()).to_string() + if name not in self._font_list and \ + name not in self._font_black_list: + self._font_list.append(name) + + self._font_palette = self.font_button.get_palette() + for font in self._font_list: + menu_item = MyMenuItem(image=FontImage(font.replace(' ', '-')), + text_label=font) + menu_item.connect('activate', self.__font_selected_cb, font) + self._font_palette.menu.append(menu_item) + menu_item.show() - def __get_font_sizes_combo_box(self): - font_sizes_combo_box = gtk.combo_box_new_text() - self.__font_sizes = ['8', '9', '10', '11', '12', '14', '16', '20', \ - '22', '24', '26', '28', '36', '48', '72'] - for index, size in enumerate(self.__font_sizes): - font_sizes_combo_box.append_text(size) - if size == '11': - font_sizes_combo_box.set_active(index) - return font_sizes_combo_box + def __font_selected_cb(self, widget, font_name): + if not hasattr(self._main_area, 'font_name'): + return + if len(self._main_area.selected) > 0: + font_size = self._main_area.font_size + else: + font_size = utils.default_font_size + self._main_area.set_font(font_name, font_size) + self._main_area.font_name = font_name + self._main_area.font_size = font_size def __attribute_values(self): attributes = {"bold": True, "italics": True, "underline": True, @@ -291,33 +503,44 @@ class TextAttributesToolbar(gtk.Toolbar): return attributes - def __fonts_cb(self, combo_box): - font_name = combo_box.get_active_text() - font_size = self.font_sizes_combo_box.combo.get_active_text() - start_index = self._main_area.selected[0].index - end_index = self._main_area.selected[0].end_index - #if start_index != end_index: - self._main_area.set_font(font_name, font_size) - self._main_area.font_name = font_name + def __font_sizes_cb(self, button, increase): + if not hasattr(self._main_area, 'font_size'): + return + if len(self._main_area.selected) < 1: + return + font_size = self._main_area.font_size + if font_size in self._font_sizes: + i = self._font_sizes.index(font_size) + if increase: + if i < len(self._font_sizes) - 2: + i += 1 + else: + if i > 0: + i -= 1 + else: + i = self._font_sizes.index(utils.default_font_size) - def __font_sizes_cb(self, combo_box): - font_size = combo_box.get_active_text() - font_name = self.fonts_combo_box.combo.get_active_text() - start_index = self._main_area.selected[0].index - end_index = self._main_area.selected[0].end_index - #if start_index != end_index: - self._main_area.set_font(font_name, font_size) - self._main_area.font_name = font_size + font_size = self._font_sizes[i] + self.size_label.set_text(str(font_size)) + self.font_size_down.set_sensitive(i != 0) + self.font_size_up.set_sensitive(i < len(self._font_sizes) - 2) + self._main_area.set_font(self._main_area.font_name, font_size) def __bold_cb(self, button): + if len(self._main_area.selected) < 1: + return value = self.__attribute_values()["bold"] self._main_area.set_bold(value) def __italics_cb(self, button): + if len(self._main_area.selected) < 1: + return value = self.__attribute_values()["italics"] self._main_area.set_italics(value) def __underline_cb(self, button): + if len(self._main_area.selected) < 1: + return value = self.__attribute_values()["underline"] self._main_area.set_underline(value) @@ -325,82 +548,162 @@ class TextAttributesToolbar(gtk.Toolbar): color = button.get_color() self._main_area.set_foreground_color(color) + def __background_color_cb(self, button): + color = button.get_color() + self._parent._main_area.set_background_color(color) + def change_active_font(self): - current_font = str(self.__attribute_values()["font"]) - for index, size in enumerate(self.__font_sizes): - index_size = current_font.find(size) - if index_size != -1: - current_font_name = current_font[:int(index_size)].rstrip() - current_font_size = current_font[int(index_size):] - index_cfs = index - fonts = self._main_area.pango_context.list_families() - for index, font in enumerate(fonts): - pango_font = pango.FontDescription(font.get_name()) - font_name = pango_font.to_string() - if font_name == current_font_name: - index_cf = index - break - self.fonts_combo_box.combo.set_active(index_cf) - self.font_sizes_combo_box.combo.set_active(index_cfs) + # TODO: update the toolbar + return class ThoughtsToolbar(gtk.Toolbar): + def __init__(self, parent): gtk.Toolbar.__init__(self) - self._parent = parent - self._parent.mods[1] = RadioToolButton(named_icon='text-mode') - self._parent.mods[1].set_tooltip(_('Text mode')) - self._parent.mods[1].set_accelerator(_('<ctrl>t')) - self._parent.mods[1].set_group(None) - self._parent.mods[1].connect('clicked', self._parent.mode_cb, - MMapArea.MODE_TEXT) - self.insert(self._parent.mods[1], -1) - - self._parent.mods[2] = RadioToolButton(named_icon='image-mode') - self._parent.mods[2].set_group(self._parent.mods[1]) - self._parent.mods[2].set_tooltip(_('Image add mode')) - self._parent.mods[2].set_accelerator(_('<ctrl>i')) - self._parent.mods[2].connect('clicked', self._parent.mode_cb, - MMapArea.MODE_IMAGE) - self.insert(self._parent.mods[2], -1) - - self._parent.mods[3] = RadioToolButton(named_icon='draw-mode') - self._parent.mods[3].set_group(self._parent.mods[1]) - self._parent.mods[3].set_tooltip(_('Drawing mode')) - self._parent.mods[3].set_accelerator(_('<ctrl>d')) - self._parent.mods[3].connect('clicked', self._parent.mode_cb, - MMapArea.MODE_DRAW) - self.insert(self._parent.mods[3], -1) - - self._parent.mods[5] = RadioToolButton(named_icon='label-mode') - self._parent.mods[5].set_tooltip(_('Label mode')) - self._parent.mods[5].set_accelerator(_('<ctrl>a')) - self._parent.mods[5].set_group(self._parent.mods[1]) - self._parent.mods[5].connect('clicked', self._parent.mode_cb, - MMapArea.MODE_LABEL) - self.insert(self._parent.mods[5], -1) - - bakground_color = ColorToolButton() - bakground_color.connect('color-set', self.__background_color_cb) - self.insert(bakground_color, -1) + text_mode_btn = RadioToolButton(named_icon='text-mode') + text_mode_btn.set_tooltip(_('Text mode')) + text_mode_btn.set_accelerator(_('<ctrl>t')) + text_mode_btn.set_group(None) + text_mode_btn.connect('clicked', self._parent.mode_cb, + MMapArea.MODE_TEXT) + self._parent.btn_group = text_mode_btn + self.insert(text_mode_btn, -1) + + image_mode_btn = RadioToolButton(named_icon='image-mode') + image_mode_btn.set_group(text_mode_btn) + image_mode_btn.set_tooltip(_('Image add mode')) + image_mode_btn.set_accelerator(_('<ctrl>i')) + image_mode_btn.connect('clicked', self._parent.mode_cb, + MMapArea.MODE_IMAGE) + self.insert(image_mode_btn, -1) + + draw_mode_btn = RadioToolButton(named_icon='draw-mode') + draw_mode_btn.set_group(text_mode_btn) + draw_mode_btn.set_tooltip(_('Drawing mode')) + draw_mode_btn.set_accelerator(_('<ctrl>d')) + draw_mode_btn.connect('clicked', self._parent.mode_cb, + MMapArea.MODE_DRAW) + self.insert(draw_mode_btn, -1) + + label_mode_btn = RadioToolButton(named_icon='label-mode') + label_mode_btn.set_tooltip(_('Label mode')) + label_mode_btn.set_accelerator(_('<ctrl>a')) + label_mode_btn.set_group(text_mode_btn) + label_mode_btn.connect('clicked', self._parent.mode_cb, + MMapArea.MODE_LABEL) + self.insert(label_mode_btn, -1) self.show_all() - def __background_color_cb(self, button): - color = button.get_color() - self._parent._main_area.set_background_color(color) + +class ActionButtons(): + ''' This class manages the action buttons that move among toolsbars ''' + + def __init__(self, parent): + self._main_toolbar = parent.get_toolbar_box().toolbar + self._main_area = parent._main_area + self._erase_button = parent.edit_toolbar.erase_button + self._sw = parent._sw + + if HASTOOLBARBOX: + target_toolbar = self._main_toolbar + else: + target_toolbar = self.parent.edit_toolbar + + self._mods = RadioToolButton(named_icon='select-mode') + self._mods.set_tooltip(_('Select thoughts')) + self._mods.set_group(parent.btn_group) + self._mods.set_accelerator(_('<ctrl>e')) + self._mods.connect('clicked', parent.mode_cb, MMapArea.MODE_NULL) + target_toolbar.insert(self._mods, -1) + + self._link_button = RadioToolButton(named_icon='link') + self._link_button.set_tooltip(_('Link/unlink two selected thoughts')) + self._link_button.set_group(parent.btn_group) + self._link_button.set_accelerator(_('<ctrl>l')) + self._link_button.connect('clicked', self.__link_cb) + target_toolbar.insert(self._link_button, -1) + + self.move_button = RadioToolButton(named_icon='move') + self.move_button.set_tooltip(_('Move selected thoughs')) + self.move_button.set_group(parent.btn_group) + self.move_button.set_accelerator(_('<ctrl>m')) + self.move_button.connect('clicked', self.__move_cb) + target_toolbar.insert(self.move_button, -1) + + self.drag_button = RadioToolButton(named_icon='drag') + self.drag_button.set_tooltip(_('Scroll the screen')) + self.drag_button.set_group(parent.btn_group) + self.drag_button.connect('clicked', self.__drag_cb) + target_toolbar.insert(self.drag_button, -1) + + if HASTOOLBARBOX: + self._separator_2 = gtk.SeparatorToolItem() + self._separator_2.props.draw = False + #self._separator_2.set_size_request(0, -1) + self._separator_2.set_expand(True) + self._separator_2.show() + target_toolbar.insert(self._separator_2, -1) + + self._stop_button = StopButton(parent) + target_toolbar.insert(self._stop_button, -1) + + def stop_dragging(self): + if self._main_area.is_dragging(): + self._main_area.drag_menu_cb(self._sw, False) + + def _stop_moving(self): + self._main_area.move_mode = False + + def __link_cb(self, widget): + self._stop_moving() + self.stop_dragging() + self._main_area.link_menu_cb() + + def __move_cb(self, widget): + self.stop_dragging() + if self._main_area.move_mode: + self._main_area.stop_moving() + else: + self._main_area.start_moving(self.move_button) + self._erase_button.set_sensitive(False) + + def __drag_cb(self, widget): + # If we were moving, stop + self._stop_moving() + if not self._main_area.is_dragging(): + self._main_area.drag_menu_cb(self._sw, True) + else: + self.stop_dragging() + self._erase_button.set_sensitive(False) + + def reconfigure(self): + ''' If screen width has changed, we may need to reconfigure + the toolbars ''' + if not HASTOOLBARBOX: + return + + if hasattr(self, '_separator_2'): + if gtk.gdk.screen_width() / 13 > style.GRID_CELL_SIZE: + if self._separator_2.get_parent() is None: + self._main_toolbar.remove(self._stop_button) + self._main_toolbar.insert(self._separator_2, -1) + self._main_toolbar.insert(self._stop_button, -1) + else: + self._main_toolbar.remove(self._separator_2) class LabyrinthActivity(activity.Activity): def __init__(self, handle): activity.Activity.__init__(self, handle) - try: - # Use new >= 0.86 toolbar design + if HASTOOLBARBOX: self.max_participants = 1 toolbar_box = ToolbarBox() + self.set_toolbar_box(toolbar_box) activity_button = ActivityToolbarButton(self) toolbar_box.toolbar.insert(activity_button, 0) @@ -421,15 +724,16 @@ class LabyrinthActivity(activity.Activity): activity_button.props.page.insert(tool, -1) tool.show() - self.edit_toolbar = ToolbarButton() - self.edit_toolbar.props.page = EditToolbar(self) - self.edit_toolbar.props.icon_name = 'toolbar-edit' - self.edit_toolbar.props.label = _('Edit'), - toolbar_box.toolbar.insert(self.edit_toolbar, -1) + tool = ToolbarButton() + self.edit_toolbar = EditToolbar(self) + tool.props.page = self.edit_toolbar + tool.props.icon_name = 'toolbar-edit' + tool.props.label = _('Edit'), + toolbar_box.toolbar.insert(tool, -1) self._undo = UndoManager.UndoManager(self, - self.edit_toolbar.props.page.undo.child, - self.edit_toolbar.props.page.redo.child) + self.edit_toolbar.undo.child, + self.edit_toolbar.redo.child) self.__build_main_canvas_area() @@ -439,40 +743,25 @@ class LabyrinthActivity(activity.Activity): tool.props.label = _('View'), toolbar_box.toolbar.insert(tool, -1) - self.text_format_toolbar = ToolbarButton() - self.text_format_toolbar.props.page = \ - TextAttributesToolbar(self._main_area) - self.text_format_toolbar.props.icon_name = 'toolbar-text' - self.text_format_toolbar.props.label = _('Text') - toolbar_box.toolbar.insert(self.text_format_toolbar, -1) - self._main_area.set_text_attributes(self.text_format_toolbar) - - separator = gtk.SeparatorToolItem() - toolbar_box.toolbar.insert(separator, -1) + tool = ToolbarButton() + self.text_format_toolbar = TextAttributesToolbar(self._main_area) + tool.props.page = self.text_format_toolbar + tool.props.icon_name = 'toolbar-text' + tool.props.label = _('Text') + toolbar_box.toolbar.insert(tool, -1) + # self._main_area.set_text_attributes(self.text_format_toolbar) - self.mods = [None] * 6 self.thought_toolbar = ToolbarButton() self.thought_toolbar.props.page = ThoughtsToolbar(self) self.thought_toolbar.props.icon_name = 'thought' self.thought_toolbar.props.label = _('Thought Type') toolbar_box.toolbar.insert(self.thought_toolbar, -1) - separator = gtk.SeparatorToolItem() - separator.props.draw = False - separator.set_expand(True) - separator.show() - toolbar_box.toolbar.insert(separator, -1) - - target_toolbar = toolbar_box.toolbar - tool_offset = 6 - - tool = StopButton(self) - toolbar_box.toolbar.insert(tool, -1) + self.action_buttons = ActionButtons(self) toolbar_box.show_all() - self.set_toolbar_box(toolbar_box) - except NameError: + else: # Use old <= 0.84 toolbar design toolbox = activity.ActivityToolbox(self) self.set_toolbox(toolbox) @@ -496,9 +785,6 @@ class LabyrinthActivity(activity.Activity): self.edit_toolbar.insert(separator, 0) self.edit_toolbar.show() - target_toolbar = self.edit_toolbar - tool_offset = 0 - self._undo = UndoManager.UndoManager(self, self.edit_toolbar.undo.child, self.edit_toolbar.redo.child) @@ -508,43 +794,36 @@ class LabyrinthActivity(activity.Activity): view_toolbar = ViewToolbar(self._main_area) toolbox.add_toolbar(_('View'), view_toolbar) - self.mods = [None] * 6 - self.thought_toolbar = ThoughtsToolbar(self) - toolbox.add_toolbar(_('Thought Type'), self.thought_toolbar) - activity_toolbar = toolbox.get_activity_toolbar() activity_toolbar.share.props.visible = False toolbox.set_current_toolbar(1) - self.mods[0] = ToolButton('select-mode') - self.mods[0].set_tooltip(_('Edit mode')) - self.mods[0].set_accelerator(_('<ctrl>e')) - self.mods[0].connect('clicked', self.mode_cb, MMapArea.MODE_NULL) - target_toolbar.insert(self.mods[0], tool_offset) - - #separator = gtk.SeparatorToolItem() - #target_toolbar.insert(separator, tool_offset + 5) - - tool = ToolButton('link') - tool.set_tooltip(_('Link/unlink two selected thoughts')) - tool.set_accelerator(_('<ctrl>l')) - tool.connect('clicked', self.__link_cb) - target_toolbar.insert(tool, tool_offset + 1) + self.show_all() - tool = ToolButton('edit-delete') - tool.set_tooltip(_('Erase selected thought(s)')) - tool.connect('clicked', self.__delete_cb) - target_toolbar.insert(tool, tool_offset + 2) + self.__configure_cb(None) - self.show_all() self._mode = MMapArea.MODE_TEXT self._main_area.set_mode(self._mode) - self.mods[MMapArea.MODE_TEXT].set_active(True) self.set_focus_child(self._main_area) def __build_main_canvas_area(self): - self._undo.block() + self.fixed = gtk.Fixed() + self.fixed.show() + self.set_canvas(self.fixed) + + self._vbox = gtk.VBox() + self._vbox.set_size_request( + gtk.gdk.screen_width(), + gtk.gdk.screen_height() - style.GRID_CELL_SIZE) + self._main_area = MMapArea.MMapArea(self._undo) + + self._undo.block() + + self._main_area.set_size_request( + max(gtk.gdk.screen_width(), gtk.gdk.screen_height()), + max(gtk.gdk.screen_width(), gtk.gdk.screen_height())) + self._main_area.show() self._main_area.connect("set_focus", self.__main_area_focus_cb) self._main_area.connect("button-press-event", self.__main_area_focus_cb) @@ -553,21 +832,63 @@ class LabyrinthActivity(activity.Activity): self.__text_selection_cb) self._main_area.connect("thought_selection_changed", self.__thought_selected_cb) - self.set_canvas(self._main_area) + gtk.gdk.screen_get_default().connect('size-changed', + self.__configure_cb) + + self._sw = gtk.ScrolledWindow() + self._sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self._sw.add_with_viewport(self._main_area) + self._vbox.pack_end(self._sw, True, True) + self._sw.show() + self._main_area.show() + self._vbox.show() + self.fixed.put(self._vbox, 0, 0) + + self.hadj = self._sw.get_hadjustment() + self.hadj.connect("value_changed", self._hadj_adjusted_cb, + self.hadj) + + self.vadj = self._sw.get_vadjustment() + self.vadj.connect("value_changed", self._vadj_adjusted_cb, + self.vadj) + + self._main_area.drag_menu_cb(self._sw, True) + self._main_area.drag_menu_cb(self._sw, False) self._undo.unblock() + def _hadj_adjusted_cb(self, adj, data=None): + self._main_area.hadj = adj.value + stop_editing(self._main_area) + + def _vadj_adjusted_cb(self, adj, data=None): + self._main_area.vadj = adj.value + stop_editing(self._main_area) + + def __configure_cb(self, event): + ''' Screen size has changed ''' + self._vbox.set_size_request( + gtk.gdk.screen_width(), + gtk.gdk.screen_height() - style.GRID_CELL_SIZE) + + self._vbox.show() + + self.action_buttons.reconfigure() + self.show_all() + def __text_selection_cb(self, thought, start, end, text): - """Update state of copy button based on text selection + """Update state of edit buttons based on text selection """ + self.__change_erase_state(True) if start != end: self.__change_copy_state(True) - self.text_format_toolbar.props.page.change_active_font() + self.text_format_toolbar.change_active_font() else: self.__change_copy_state(False) if self._mode == (MMapArea.MODE_TEXT and - len(self._main_area.selected) and - self._main_area.selected[0].editing): + len(self._main_area.selected)): + # With textview, we are always editing + # and self._main_area.selected[0].editing): self.__change_paste_state(True) else: self.__change_paste_state(False) @@ -576,20 +897,18 @@ class LabyrinthActivity(activity.Activity): def __thought_selected_cb(self, arg, background_color, foreground_color): """Disable copy button if whole thought object is selected """ + self.__change_erase_state(True) self.__change_copy_state(False) self.__change_paste_state(False) + def __change_erase_state(self, state): + self.edit_toolbar.erase_button.set_sensitive(state) + def __change_copy_state(self, state): - try: - self.edit_toolbar.props.page.copy.child.set_sensitive(state) - except AttributeError: - self.edit_toolbar.copy.child.set_sensitive(state) + self.edit_toolbar.copy.child.set_sensitive(state) def __change_paste_state(self, state): - try: - self.edit_toolbar.props.page.paste.child.set_sensitive(state) - except AttributeError: - self.edit_toolbar.paste.child.set_sensitive(state) + self.edit_toolbar.paste.child.set_sensitive(state) def __expose(self, widget, event): """Create canvas hint message at start @@ -607,17 +926,25 @@ class LabyrinthActivity(activity.Activity): geom = list(self._main_area.window.get_geometry()) geom[3] = geom[3] - ((self.window.get_geometry()[3] - geom[3]) / 2) + # Make sure initial thought is "above the fold" + if geom[2] < geom[3]: + xf = 2 + yf = 4 + else: + xf = 4 + yf = 2 + layout.set_alignment(pango.ALIGN_CENTER) layout.set_text(_('Click to add\ncentral thought')) width, height = layout.get_pixel_size() - context.move_to(geom[2] / 2 - (width / 2), geom[3] / 2 - (height / 2)) + context.move_to(geom[2] / xf - (width / 2), geom[3] / yf - (height / 2)) context.show_layout(layout) round = 40 - ul = (geom[2] / 2 - (width / 2) - round, - geom[3] / 2 - (height / 2) - round) - lr = (geom[2] / 2 + (width / 2) + round, - geom[3] / 2 + (height / 2) + round) + ul = (geom[2] / xf - (width / 2) - round, + geom[3] / yf - (height / 2) - round) + lr = (geom[2] / xf + (width / 2) + round, + geom[3] / yf + (height / 2) + round) context.move_to(ul[0], ul[1] + round) context.line_to(ul[0], lr[1] - round) context.curve_to(ul[0], lr[1], ul[0], lr[1], ul[0] + round, lr[1]) @@ -639,8 +966,13 @@ class LabyrinthActivity(activity.Activity): return False def mode_cb(self, button, mode): + self.action_buttons.stop_dragging() + if self._mode == MMapArea.MODE_TEXT: + if len(self._main_area.selected) > 0: + self._main_area.selected[0].leave() self._mode = mode self._main_area.set_mode(self._mode) + # self.edit_toolbar.erase_button.set_sensitive(True) def __export_pdf_cb(self, event): maxx, maxy = self._main_area.get_max_area() @@ -707,7 +1039,9 @@ class LabyrinthActivity(activity.Activity): del fileObject def __main_area_focus_cb(self, arg, event, extended=False): - self._main_area.grab_focus() + # Don't steal focus from textview + # self._main_area.grab_focus() + pass def read_file(self, file_path): tar = Tarball(file_path) @@ -730,13 +1064,9 @@ class LabyrinthActivity(activity.Activity): x, y = utils.parse_coords(tmp) self._main_area.translation = [x, y] - self.thought_toolbar.props.page.mods[self._mode].set_active(True) - tar.close() def write_file(self, file_path): - logging.debug('write_file') - tar = Tarball(file_path, 'w') self._main_area.update_save() @@ -761,9 +1091,3 @@ class LabyrinthActivity(activity.Activity): str(self._main_area.translation)) string = doc.toxml() return string.encode("utf-8") - - def __link_cb(self, widget): - self._main_area.link_menu_cb() - - def __delete_cb(self, widget): - self._main_area.delete_selected_elements() diff --git a/src/MMapArea.py b/src/MMapArea.py index 838175a..b007080 100644 --- a/src/MMapArea.py +++ b/src/MMapArea.py @@ -22,6 +22,7 @@ import math import time +import string import gtk import pango import gobject @@ -84,30 +85,32 @@ class MMapArea (gtk.DrawingArea): It is responsible for processing signals and such from the whole area and \ passing these on to the correct child. It also informs things when to draw''' - __gsignals__ = dict (title_changed = (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - (gobject.TYPE_STRING, )), - change_mode = (gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_INT, )), - change_buffer = (gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_OBJECT, )), - text_selection_changed = (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - (gobject.TYPE_INT, gobject.TYPE_INT, gobject.TYPE_STRING)), - thought_selection_changed = (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)), - set_focus = (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - (gobject.TYPE_PYOBJECT, gobject.TYPE_BOOLEAN)), - set_attrs = (gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN, pango.FontDescription)), - link_selected = (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ())) + __gsignals__ = dict ( + title_changed = (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, (gobject.TYPE_STRING, )), + change_mode = (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_INT, )), + change_buffer = (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_OBJECT, )), + text_selection_changed = (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + (gobject.TYPE_INT, gobject.TYPE_INT, + gobject.TYPE_STRING)), + thought_selection_changed = (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT)), + set_focus = (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, gobject.TYPE_BOOLEAN)), + set_attrs = (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN, + gobject.TYPE_BOOLEAN, pango.FontDescription)), + link_selected = (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ())) def __init__(self, undo): super (MMapArea, self).__init__() @@ -146,7 +149,15 @@ class MMapArea (gtk.DrawingArea): self.connect ("scroll_event", self.scroll) self.commit_handler = None self.title_change_handler = None + self.drag_mode = False + self._dragging = False + self.sw = None + self.hadj = 0 + self.vadj = 0 + self.origin_x = None + self.origin_y = None self.moving = False + self.move_mode = False self.move_origin = None self.move_origin_new = None self.focus = None @@ -170,11 +181,17 @@ class MMapArea (gtk.DrawingArea): w.realize() style = w.get_style() self.pango_context.set_font_description(style.font_desc) - self.font_name = style.font_desc.to_string() + + # FIXME: rude hack to remove fontsize from font name + parts = style.font_desc.to_string().split() + try: + float(parts[-1]) + self.font_name = string.join(parts[0:-2]) + except ValueError: + self.font_name = style.font_desc.to_string() + utils.default_font = self.font_name - - self.font_size = utils.default_font - + self.font_size = utils.default_font_size utils.default_colors["text"] = utils.gtk_to_cairo_color(style.text[gtk.STATE_NORMAL]) utils.default_colors["base"] = utils.gtk_to_cairo_color(style.base[gtk.STATE_NORMAL]) # Match the fixed white canvas colour (makes thought focus visible) @@ -189,8 +206,11 @@ class MMapArea (gtk.DrawingArea): utils.selected_colors["fill"] = utils.gtk_to_cairo_color(style.base[gtk.STATE_SELECTED]) def set_text_attributes(self, text_attributes): + return + ''' self.font_combo_box = text_attributes.props.page.fonts_combo_box.combo - self.font_sizes_combo_box = text_attributes.props.page.font_sizes_combo_box.combo + self.font_sizes_combo_box = utils.default_font_size #text_attributes.props.page.font_sizes_combo_box.combo + ''' def transform_coords(self, loc_x, loc_y): if hasattr(self, "transform"): @@ -201,6 +221,13 @@ class MMapArea (gtk.DrawingArea): return self.untransform.transform_point(loc_x, loc_y) def button_down (self, widget, event): + if self.drag_mode: + self.set_cursor(gtk.gdk.HAND2) + self.origin_x = event.x + self.origin_y = event.y + self._dragging = True + return + if event.button == 2 or \ event.button == 1 and self.translate == True: self.set_cursor (gtk.gdk.FLEUR) @@ -213,8 +240,11 @@ class MMapArea (gtk.DrawingArea): obj = self.find_object_at (coords) if obj: - if event.button == 3: - self.moving = not (event.state & gtk.gdk.CONTROL_MASK) + if event.button == 3 or self.move_mode: + if self.move_mode: + self.moving = True + else: + self.moving = not (event.state & gtk.gdk.CONTROL_MASK) if self.moving: self.set_cursor(gtk.gdk.FLEUR) self.move_origin = (coords[0], coords[1]) @@ -252,6 +282,10 @@ class MMapArea (gtk.DrawingArea): self.invalidate ((old_coords[0], old_coords[1], new_coords[0], new_coords[1])) def button_release (self, widget, event): + if self._dragging: + self.set_cursor(gtk.gdk.LEFT_PTR) + self._dragging = False + coords = self.transform_coords (event.get_coords()[0], event.get_coords()[1]) if self.is_bbox_selecting: @@ -272,8 +306,10 @@ class MMapArea (gtk.DrawingArea): self.undo.add_undo (self.move_action) self.move_action = None - self.moving = False - self.move_origin = None + was_moving = False + if self.moving: + was_moving = True + self.stop_moving() obj = self.find_object_at (coords) @@ -295,6 +331,9 @@ class MMapArea (gtk.DrawingArea): return True self.invalidate () + + if was_moving: + self.start_moving(self.move_button) return True def undo_transform_cb (self, action, mode): @@ -354,6 +393,16 @@ class MMapArea (gtk.DrawingArea): return True def motion (self, widget, event): + if self._dragging: + if self.origin_x is None: + self.origin_x = event.get_coords()[0] + self.origin_y = event.get_coords()[1] + dx = self.origin_x - event.get_coords()[0] + dy = self.origin_y - event.get_coords()[1] + self.origin_x = event.get_coords()[0] + self.origin_y = event.get_coords()[1] + self._adjust_sw(dx, dy) + return True coords = self.transform_coords (event.get_coords()[0], event.get_coords()[1]) if event.state & gtk.gdk.BUTTON1_MASK and self.is_bbox_selecting: @@ -482,8 +531,8 @@ class MMapArea (gtk.DrawingArea): self.commit_handler = None if thought: try: - self.commit_handler = self.im_context.connect ("commit", thought.commit_text, self.mode, self.font_combo_box, \ - self.font_sizes_combo_box) + self.commit_handler = self.im_context.connect ("commit", thought.commit_text, self.mode, None, None) + # self.font_combo_box, self.font_sizes_combo_box) self.delete_handler = self.im_context.connect ("delete-surrounding", thought.delete_surroundings, self.mode) self.preedit_changed_handler = self.im_context.connect ("preedit-changed", thought.preedit_changed, self.mode) self.preedit_end_handler = self.im_context.connect ("preedit-end", thought.preedit_end, self.mode) @@ -733,7 +782,8 @@ class MMapArea (gtk.DrawingArea): type = self.mode if type == MODE_TEXT: - thought = TextThought.TextThought (coords, self.pango_context, self.nthoughts, self.save, self.undo, loading, self.background_color, self.foreground_color) + # fixed<-_vbox<-_sw<-_main_area + thought = TextThought.TextThought (coords, self.pango_context, self.nthoughts, self.save, self.undo, loading, self.background_color, self.foreground_color, fixed=self.parent.parent.parent.parent, parent=self) elif type == MODE_LABEL: thought = LabelThought.LabelThought (coords, self.pango_context, self.nthoughts, self.save, self.undo, loading, self.background_color, self.foreground_color) elif type == MODE_IMAGE: @@ -774,6 +824,9 @@ class MMapArea (gtk.DrawingArea): else: action = None + if hasattr(thought, 'textview'): + thought.remove_textview() + if thought.element in self.element.childNodes: self.element.removeChild (thought.element) self.thoughts.remove (thought) @@ -1179,6 +1232,59 @@ class MMapArea (gtk.DrawingArea): return True return False + def drag_menu_cb(self, sw, mode): + if len(self.selected) == 1: + if hasattr(self.selected[0], 'textview'): + self.selected[0].remove_textview() + if mode == True: + self.sw = sw + self.drag_mode = True + else: + self.drag_mode = False + + def is_dragging(self): + return self.drag_mode + + def _adjust_sw(self, dx, dy): + if self.sw is None: + return + if not self.drag_mode: + return + hadj = self.sw.get_hadjustment() + hvalue = hadj.get_value() + dx + try: + if hvalue < hadj.get_lower(): + hvalue = hadj.get_lower() + elif hvalue > hadj.get_upper(): + hvalue = hadj.get_upper() + except AttributeError: + pass + hadj.set_value(hvalue) + self.sw.set_hadjustment(hadj) + vadj = self.sw.get_vadjustment() + vvalue = vadj.get_value() + dy + try: + if vvalue < vadj.get_lower(): + vvalue = vadj.get_lower() + elif vvalue > vadj.get_upper(): + vvalue = vadj.get_upper() + except AttributeError: + pass + vadj.set_value(vvalue) + self.sw.set_vadjustment(vadj) + + def stop_moving(self): + self.moving = False + self.move_mode = False + self.move_origin = None + + def start_moving(self, move_button): + if len(self.selected) == 1: + if hasattr(self.selected[0], 'textview'): + self.selected[0].remove_textview() + self.move_mode = True + self.move_button = move_button + def link_menu_cb (self): if len (self.selected) != 2: return diff --git a/src/TextThought.py b/src/TextThought.py index 1fef255..28d11dc 100644 --- a/src/TextThought.py +++ b/src/TextThought.py @@ -20,9 +20,17 @@ # Boston, MA 02110-1301 USA # +# In order to support an on-screen keyboard, a textview widget is used +# instead of capturing individual keyboard events. The down side to +# this is that the maintenance of text attributes is mangled. The good +# news is that much of the complexity disappears. +# --Walter Bender <walter@sugarlabs.org> 2013 + import gtk +import gobject import pango import utils +import string import os import xml.dom import logging @@ -37,7 +45,9 @@ UNDO_REMOVE_ATTR=66 UNDO_REMOVE_ATTR_SELECTION=67 class TextThought (ResizableThought): - def __init__ (self, coords, pango_context, thought_number, save, undo, loading, background_color, foreground_color, name="thought"): + def __init__ (self, coords, pango_context, thought_number, save, undo, + loading, background_color, foreground_color, name="thought", + fixed=None, parent=None): super (TextThought, self).__init__(coords, save, name, undo, background_color, foreground_color) self.index = 0 @@ -56,6 +66,11 @@ class TextThought (ResizableThought): self.current_attrs = [] self.double_click = False self.orig_text = None + self._parent = parent + self._fixed = fixed + self.textview = None + self._textview_handler = None + self._clipboard = None if prefs.get_direction () == gtk.TEXT_DIR_LTR: self.pango_context.set_base_dir (pango.DIRECTION_LTR) @@ -71,7 +86,7 @@ class TextThought (ResizableThought): self.all_okay = True - def index_from_bindex (self, bindex): + def index_from_bindex (self, bindex): if bindex == 0: return 0 index = 0 @@ -102,7 +117,7 @@ class TextThought (ResizableThought): self.attrlist = pango.AttrList () # TODO: splice instead of own method it = self.attributes.get_iterator() - + while 1: at = it.get_attrs() for x in at: @@ -123,7 +138,7 @@ class TextThought (ResizableThought): self.attrlist.splice(ins_style, self.index, len(ins_text)) else: show_text = self.text - + it = self.attributes.get_iterator() while(1): found = False @@ -199,13 +214,13 @@ class TextThought (ResizableThought): self.emit("update-attrs", bold, italics, underline, pango_font) return show_text - def recalc_text_edges (self): + def recalc_text_edges (self): if (not hasattr(self, "layout")): return del self.layout - + show_text = self.attrs_changed () - + ''' r,g,b = utils.selected_colors["fill"] r *= 65536 g *= 65536 @@ -215,6 +230,7 @@ class TextThought (ResizableThought): else: bgsel = pango.AttrBackground (int(r), int(g), int(b), self.index, self.end_index) self.attrlist.insert (bgsel) + ''' self.layout = pango.Layout (self.pango_context) self.layout.set_text (show_text) @@ -246,21 +262,21 @@ class TextThought (ResizableThought): self.ul = (self.lr[0] - margin[0] - margin[2] - text_w, tmp1) """ - def recalc_edges (self): + def recalc_edges (self): self.lr = (self.ul[0]+self.width, self.ul[1]+self.height) if not self.creating: self.recalc_text_edges() - def commit_text (self, context, string, mode, font_combo_box, font_sizes_combo_box): + def commit_text (self, context, string, mode, font_combo_box, font_sizes_combo_box): font_name = font_combo_box.get_active_text() - font_size = font_sizes_combo_box.get_active_text() + font_size = utils.default_font_size # font_sizes_combo_box.get_active_text() self.set_font(font_name, font_size) self.add_text (string) self.recalc_edges () self.emit ("title_changed", self.text) self.emit ("update_view") - def add_text (self, string): + def add_text (self, string): if self.index > self.end_index: left = self.text[:self.end_index] right = self.text[self.index:] @@ -276,12 +292,12 @@ class TextThought (ResizableThought): bleft = self.bytes[:self.b_f_i (self.index)] bright = self.bytes[self.b_f_i (self.end_index):] change = self.index - self.end_index + len(string) - else: + else: left = self.text[:self.index] right = self.text[self.index:] bleft = self.bytes[:self.b_f_i(self.index)] bright = self.bytes[self.b_f_i(self.index):] - change = len(string) + change = len(string) it = self.attributes.get_iterator() changes= [] @@ -317,7 +333,7 @@ class TextThought (ResizableThought): changes.append(x) if it.next() == False: break - + del self.attributes self.attributes = pango.AttrList() for x in changes: @@ -329,14 +345,14 @@ class TextThought (ResizableThought): self.index += len (string) self.bytes = bleft + str(len(string)) + bright self.bindex = self.b_f_i (self.index) - self.end_index = self.index + self.end_index = self.index - def draw (self, context): + def draw (self, context): self.recalc_edges () ResizableThought.draw(self, context) if self.creating: return - + (textx, texty) = (self.min_x, self.min_y) if self.am_primary: r, g, b = utils.primary_colors["text"] @@ -364,14 +380,21 @@ class TextThought (ResizableThought): context.set_source_rgb (0,0,0) context.stroke () - def process_key_press (self, event, mode): + def process_key_press (self, event, mode): + # Since we are using textviews, we don't use the + # keypress code anymore + if not self.editing: + return False + else: + return True + modifiers = gtk.accelerator_get_default_mod_mask () shift = event.state & modifiers == gtk.gdk.SHIFT_MASK handled = True clear_attrs = True if not self.editing: return False - + if (event.state & modifiers) & gtk.gdk.CONTROL_MASK: if event.keyval == gtk.keysyms.a: self.index = self.bindex = 0 @@ -410,7 +433,7 @@ class TextThought (ResizableThought): else: handled = False - if clear_attrs: + if clear_attrs: del self.current_attrs self.current_attrs = [] @@ -422,7 +445,7 @@ class TextThought (ResizableThought): return handled - def undo_text_action (self, action, mode): + def undo_text_action (self, action, mode): self.undo.block () if action.undo_type == UndoManager.DELETE_LETTER or action.undo_type == UndoManager.DELETE_WORD: real_mode = not mode @@ -442,7 +465,7 @@ class TextThought (ResizableThought): self.add_text (action.text) self.rebuild_byte_table () self.bindex = self.b_f_i (self.index) - + del self.attributes self.attributes = pango.AttrList() map(lambda a : self.attributes.change(a), attrs) @@ -452,7 +475,7 @@ class TextThought (ResizableThought): self.emit ("grab_focus", False) self.undo.unblock () - def delete_char (self): + def delete_char (self): if self.index == self.end_index == len (self.text): return if self.index > self.end_index: @@ -477,7 +500,7 @@ class TextThought (ResizableThought): changes= [] old_attrs = [] accounted = -change - + it = self.attributes.get_iterator() while (1): (start,end) = it.range() @@ -516,11 +539,11 @@ class TextThought (ResizableThought): changes.append(x) if it.next() == False: break - + del self.attributes self.attributes = pango.AttrList() map(lambda a : self.attributes.change(a), changes) - + self.undo.add_undo (UndoManager.UndoAction (self, UndoManager.DELETE_LETTER, self.undo_text_action, self.b_f_i (self.index), local_text, len(local_text), local_bytes, old_attrs, changes)) @@ -528,7 +551,7 @@ class TextThought (ResizableThought): self.bytes = bleft+bright self.end_index = self.index - def backspace_char (self): + def backspace_char (self): if self.index == self.end_index == 0: return if self.index > self.end_index: @@ -594,12 +617,11 @@ class TextThought (ResizableThought): changes.append(x) if it.next() == False: break - - + del self.attributes self.attributes = pango.AttrList() map(lambda a : self.attributes.change(a), changes) - + self.text = left+right self.bytes = bleft+bright self.end_index = self.index @@ -609,7 +631,7 @@ class TextThought (ResizableThought): if self.index < 0: self.index = 0 - def move_index_back (self, mod): + def move_index_back (self, mod): if self.index <= 0: self.index = 0 return @@ -617,7 +639,7 @@ class TextThought (ResizableThought): if not mod: self.end_index = self.index - def move_index_forward (self, mod): + def move_index_forward (self, mod): if self.index >= len(self.text): self.index = len(self.text) return @@ -625,7 +647,7 @@ class TextThought (ResizableThought): if not mod: self.end_index = self.index - def move_index_up (self, mod): + def move_index_up (self, mod): tmp = self.text.decode () lines = tmp.splitlines () if len (lines) == 1: @@ -659,7 +681,7 @@ class TextThought (ResizableThought): if not mod: self.end_index = self.index - def move_index_down (self, mod): + def move_index_down (self, mod): tmp = self.text.decode () lines = tmp.splitlines () if len (lines) == 1: @@ -683,7 +705,7 @@ class TextThought (ResizableThought): if not mod: self.end_index = self.index - def move_index_horizontal(self, mod, home=False): + def move_index_horizontal(self, mod, home=False): lines = self.text.splitlines () loc = 0 line = 0 @@ -698,12 +720,18 @@ class TextThought (ResizableThought): return line += 1 - def process_button_down (self, event, coords): + def process_button_down (self, event, coords): + if not self._parent.move_mode and self.textview is None: + self._create_textview() + if self.textview is not None: + self.textview.grab_focus() + if ResizableThought.process_button_down(self, event, coords): return True - if not self.editing: - return False + # With textview, we are always editing + # if not self.editing: + # return False modifiers = gtk.accelerator_get_default_mod_mask () @@ -722,6 +750,7 @@ class TextThought (ResizableThought): self.index = len(self.text) self.end_index = 0 # and mark all self.double_click = True + elif event.button == 2: x = int ((coords[0] - self.min_x)*pango.SCALE) y = int ((coords[1] - self.min_y)*pango.SCALE) @@ -734,15 +763,141 @@ class TextThought (ResizableThought): if os.name != 'nt': clip = gtk.Clipboard (selection="PRIMARY") self.paste_text (clip) - + del self.current_attrs - self.current_attrs = [] + self.current_attrs = [] self.recalc_edges() self.emit ("update_view") self.selection_changed() - def process_button_release (self, event, transformed): + def _create_textview(self): + # When the button is pressed inside a text thought, + # create a textview (necessary for invoking the + # on-screen keyboard) instead of processing the text + # by grabbing keyboard events. + if self.textview is None: + self.textview = gtk.TextView() + margin = utils.margin_required (utils.STYLE_NORMAL) + x, y, w, h = self.textview_rescale() + self.textview.set_size_request(w, h) + self._fixed.put(self.textview, x, y) + self.textview.set_justification(gtk.JUSTIFY_CENTER) + + font, size = None, None + bold, italic, underline = False, False, False + # Get current attributes and set them here + it = self.attributes.get_iterator() + while (1): + r = it.range() + for x in it.get_attrs(): + if x.type == pango.ATTR_WEIGHT and x.value == pango.WEIGHT_BOLD: + bold = True + elif x.type == pango.ATTR_STYLE and x.value == pango.STYLE_ITALIC: + italic = True + elif x.type == pango.ATTR_UNDERLINE and x.value == pango.UNDERLINE_SINGLE: + underline = True + elif x.type == pango.ATTR_FONT_DESC: + parts = x.desc.to_string ().split() + font = string.join(parts[0:-2]) + size = parts[-1] + + if not it.next(): + break + + if font is None: + font = 'Sans' + if size is None: + size = utils.default_font_size + font_desc = pango.FontDescription(font) + font_desc.set_size( + int(int(size) * pango.SCALE * self._parent.scale_fac)) + if bold: + font_desc.set_weight(pango.WEIGHT_BOLD) + if italic: + font_desc.set_style(pango.STYLE_ITALIC) + self.textview.modify_font(font_desc) + + r, g, b = utils.gtk_to_cairo_color(self.foreground_color) + rgba = gtk.gdk.Color( + int(65535 * r), int(65535 * g), int(65535 * b)) + self.textview.modify_text(gtk.STATE_NORMAL, rgba) + + self.textview.get_buffer().set_text(self.text) + self.textview.show() + if self._textview_handler is None: + self._textview_handler = self.textview.connect( + 'focus-out-event', self._textview_focus_out_cb) + self.copy_handler = self.textview.connect( + 'copy-clipboard', self._textview_copy_cb) + self.cut_handler = self.textview.connect( + 'cut-clipboard', self._textview_cut_cb) + self.paste_handler = self.textview.connect( + 'paste-clipboard', self._textview_paste_cb) + self.select_handler = self.textview.connect( + 'select-all', self._textview_select_cb) + self.textview.grab_focus() + self._fixed.show() + + def textview_rescale(self): + tx = self._parent.translation[0] * self._parent.scale_fac + ty = self._parent.translation[1] * self._parent.scale_fac + margin = utils.margin_required (utils.STYLE_NORMAL) + hadj = int(self._parent.hadj) + vadj = int(self._parent.vadj) + w = int((self.width - margin[0] - margin[2]) \ + * self._parent.scale_fac) + # w = max(w, margin[0] + margin[2]) + h = int((self.height - margin[1] - margin[3]) \ + * self._parent.scale_fac) + # h = max(h, margin[1] + margin[3]) + xo = gtk.gdk.screen_width() \ + * (1. - self._parent.scale_fac) / 2. + yo = gtk.gdk.screen_height() \ + * (1. - self._parent.scale_fac) / 1.25 # FIXME + x = (self.ul[0] + margin[0]) * self._parent.scale_fac + y = (self.ul[1] + margin[1]) * self._parent.scale_fac + return int(x + xo - hadj + tx), int(y + yo - vadj + ty), \ + int(w), int(h) + + def _textview_copy_cb(self, widget=None, event=None): + self.textview.get_buffer().copy_clipboard(self._clipboard) + return True + + def _textview_cut_cb(self, widget=None, event=None): + self.textview.get_buffer().cut_clipboard( + self._clipboard, self.textview.get_editable()) + self._textview_process() + return True + + def _textview_paste_cb(self, widget=None, event=None): + self.textview.get_buffer().paste_clipboard( + self._clipboard, None, self.textview.get_editable()) + self._textview_process() + return True + + def _textview_select_cb(self, widget=None, event=None): + buffer = self.textview.get_buffer() + buffer.select_range(buffer.get_start_iter(), + buffer.get_end_iter()) + return True + + def _textview_focus_out_cb(self, widget=None, event=None): + self._textview_process() + return False + + def _textview_process(self): + self.index = 0 + self.end_index = len(self.text) + self.delete_char() + bounds = self.textview.get_buffer().get_bounds() + self.add_text(self.textview.get_buffer().get_text( + bounds[0], bounds[1], True)) + self.emit ("title_changed", self.text) + self.emit ("update_view") + return False + + def process_button_release (self, event, transformed): if self.orig_size: if self.creating: orig_size = self.width >= MIN_SIZE or self.height >= MIN_SIZE @@ -757,15 +912,22 @@ class TextThought (ResizableThought): self.double_click = False return ResizableThought.process_button_release(self, event, transformed) - def selection_changed (self): - (start, end) = (min(self.index, self.end_index), max(self.index, self.end_index)) + def selection_changed (self): + # Fix me: We are forcing selection to entire buffer + # (start, end) = (min(self.index, self.end_index), max(self.index, self.end_index)) + start, end = 0, len(self.text) self.emit ("text_selection_changed", start, end, self.text[start:end]) - def handle_motion (self, event, transformed): + def handle_motion (self, event, transformed): if ResizableThought.handle_motion(self, event, transformed): self.recalc_edges() return True + if self.textview is not None and self.ul is not None: + x, y, w, h = self.textview_rescale() + self.textview.set_size_request(w, h) + self._fixed.move(self.textview, x, y) + if not self.editing or self.resizing: return False @@ -784,7 +946,7 @@ class TextThought (ResizableThought): return False - def export (self, context, move_x, move_y): + def export (self, context, move_x, move_y): utils.export_thought_outline (context, self.ul, self.lr, self.background_color, self.am_selected, self.am_primary, utils.STYLE_NORMAL, (move_x, move_y)) @@ -867,7 +1029,7 @@ class TextThought (ResizableThought): if not it.next(): break - def rebuild_byte_table (self): + def rebuild_byte_table (self): # Build the Byte table del self.bytes self.bytes = '' @@ -941,34 +1103,18 @@ class TextThought (ResizableThought): self.recalc_edges() def copy_text (self, clip): - if self.end_index > self.index: - clip.set_text (self.text[self.index:self.end_index]) - else: - clip.set_text (self.text[self.end_index:self.index]) + self._clipboard = clip + self._textview_copy_cb() def cut_text (self, clip): - if self.end_index > self.index: - clip.set_text (self.text[self.index:self.end_index]) - else: - clip.set_text (self.text[self.end_index:self.index]) - self.delete_char () - self.recalc_edges () - self.emit ("title_changed", self.text) - self.bindex = self.bindex_from_index (self.index) - self.emit ("update_view") + self._clipboard = clip + self._textview_copy_cb() def paste_text (self, clip): - text = clip.wait_for_text() - if not text: - return - self.add_text (text) - self.rebuild_byte_table () - self.recalc_edges () - self.emit ("title_changed", self.text) - self.bindex = self.bindex_from_index (self.index) - self.emit ("update_view") + self._clipboard = clip + self._textview_paste_cb() - def delete_surroundings(self, imcontext, offset, n_chars, mode): + def delete_surroundings(self, imcontext, offset, n_chars, mode): # TODO: Add in Attr stuff orig = len(self.text) left = self.text[:offset] @@ -979,7 +1125,7 @@ class TextThought (ResizableThought): new = len(self.text) if self.index > len(self.text): self.index = len(self.text) - + change = old - new changes = [] old_attrs = [] @@ -1025,26 +1171,27 @@ class TextThought (ResizableThought): changes.append(x) if it.next() == False: break - + del self.attributes self.attributes = pango.AttrList() map(lambda x : self.attributes.change(x), changes) self.recalc_edges () self.undo.add_undo (UndoManager.UndoAction (self, UndoManager.DELETE_LETTER, self.undo_text_action, - self.b_f_i (offset), local_text, len(local_text), local_bytes, old_attrs, changes)) + self.b_f_i (offset), local_text, len(local_text), + local_bytes, old_attrs, changes)) self.emit ("title_changed", self.text) self.bindex = self.bindex_from_index (self.index) self.emit ("update_view") - def preedit_changed (self, imcontext, mode): + def preedit_changed (self, imcontext, mode): self.preedit = imcontext.get_preedit_string () if self.preedit[0] == '': self.preedit = None self.recalc_edges () self.emit ("update_view") - def retrieve_surroundings (self, imcontext, mode): + def retrieve_surroundings (self, imcontext, mode): imcontext.set_surrounding (self.text, -1, self.bindex) return True @@ -1071,18 +1218,18 @@ class TextThought (ResizableThought): self.recalc_edges() self.emit("update_view") self.undo.unblock() - - def create_attribute(self, attribute, start, end): + + def create_attribute(self, attribute, start, end): if attribute == 'bold': return pango.AttrWeight(pango.WEIGHT_BOLD, start, end) elif attribute == 'italic': return pango.AttrStyle(pango.STYLE_ITALIC, start, end) elif attribute == 'underline': return pango.AttrUnderline(pango.UNDERLINE_SINGLE, start, end) - - def set_attribute(self, active, attribute): - if not self.editing: - return + + def set_attribute(self, active, attribute): + # if not self.editing: + # return if attribute == 'bold': pstyle, ptype, pvalue = (pango.WEIGHT_NORMAL, pango.ATTR_WEIGHT, pango.WEIGHT_BOLD) @@ -1090,14 +1237,18 @@ class TextThought (ResizableThought): pstyle, ptype, pvalue = (pango.STYLE_NORMAL, pango.ATTR_STYLE, pango.STYLE_ITALIC) elif attribute == 'underline': pstyle, ptype, pvalue = (pango.UNDERLINE_NONE, pango.ATTR_UNDERLINE, pango.UNDERLINE_SINGLE) - + + # Always modify whole string + self.index = 0 + self.end_index = len(self.text) + index, end_index = (self.index, self.end_index) init = min(index, end_index) end = max(index, end_index) if not active: attr = pango.AttrStyle(pstyle, init, end) - #if index == end_index: + #if index == end_index: # self.current_attrs.change(attr) #else: self.attributes.change(attr) @@ -1112,15 +1263,15 @@ class TextThought (ResizableThought): tmp.append(x) self.current_attrs = tmp self.recalc_edges() - self.undo.add_undo(UndoManager.UndoAction(self, UNDO_REMOVE_ATTR, \ - self.undo_attr_cb,\ - attr)) + self.undo.add_undo(UndoManager.UndoAction(self, UNDO_REMOVE_ATTR, + self.undo_attr_cb, + attr)) return - + it = self.attributes.get_iterator() old_attrs = self.attributes.copy() changed = [] - + while True: r = it.range() if r[0] <= init and r[1] >= end: @@ -1135,7 +1286,7 @@ class TextThought (ResizableThought): if not it.next(): break - + del self.attributes self.attributes = pango.AttrList() map(lambda x : self.attributes.change(x), changed) @@ -1145,77 +1296,97 @@ class TextThought (ResizableThought): tmp.append(x) self.current_attrs = tmp self.undo.add_undo(UndoManager.UndoAction(self, UNDO_REMOVE_ATTR_SELECTION, - self.undo_attr_cb, - old_attrs, - self.attributes.copy())) + self.undo_attr_cb, + old_attrs, + self.attributes.copy())) else: - if index == end_index: + if index == end_index: attr = self.create_attribute(attribute, index, end_index) self.undo.add_undo(UndoManager.UndoAction(self, UNDO_ADD_ATTR, - self.undo_attr_cb, - attr)) + self.undo_attr_cb, + attr)) self.current_attrs.append(attr) #self.attributes.insert(attr) - else: - attr = self.create_attribute(attribute, init, end) + else: + attr = self.create_attribute(attribute, init, end) old_attrs = self.attributes.copy() self.attributes.change(attr) self.undo.add_undo(UndoManager.UndoAction(self, UNDO_ADD_ATTR_SELECTION, - self.undo_attr_cb, - old_attrs, - self.attributes.copy())) + self.undo_attr_cb, + old_attrs, + self.attributes.copy())) self.recalc_edges() + self.remove_textview() - def set_bold (self, active): + def set_bold (self, active): self.set_attribute(active, 'bold') - - def set_italics (self, active): + + def set_italics (self, active): self.set_attribute(active, 'italic') - def set_underline (self, active): + def set_underline (self, active): self.set_attribute(active, 'underline') - def set_font (self, font_name, font_size): - if not self.editing: - return - + def set_font (self, font_name, font_size): + # With textview, we are always editing + # if not self.editing: + # return + + # Always modify whole string + self.index = 0 + self.end_index = len(self.text) + start = min(self.index, self.end_index) end = max(self.index, self.end_index) - - pango_font = pango.FontDescription(font_name+" "+font_size) - + + + pango_font = pango.FontDescription('%s %s' % (font_name, str(font_size))) + attr = pango.AttrFontDesc (pango_font, start, end) if start == end: self.undo.add_undo(UndoManager.UndoAction(self, UNDO_ADD_ATTR, - self.undo_attr_cb, - attr)) - try: + self.undo_attr_cb, + attr)) + try: self.current_attrs.change(attr) - except AttributeError: + except AttributeError: self.current_attrs.append(attr) else: old_attrs = self.attributes.copy() self.attributes.change(attr) self.undo.add_undo(UndoManager.UndoAction(self, UNDO_ADD_ATTR_SELECTION, - self.undo_attr_cb, - old_attrs, - self.attributes.copy())) + self.undo_attr_cb, + old_attrs, + self.attributes.copy())) self.recalc_edges() + self.remove_textview() - def inside(self, inside): + def inside(self, inside): + # FIXME: with switch to textview, we don't need cursor update if self.editing: + if self.textview is not None: + self.textview.grab_focus() self.emit ("change_mouse_cursor", gtk.gdk.XTERM) else: ResizableThought.inside(self, inside) - def enter(self): + def enter(self): if self.editing: return self.orig_text = self.text self.editing = True - def leave(self): + def remove_textview(self): + if self.textview is not None: + self._textview_process() + self._textview_handler = None + self.textview.hide() + self.textview.destroy() + self.textview = None + + def leave(self): + self.remove_textview() if not self.editing: return ResizableThought.leave(self) diff --git a/src/utils.py b/src/utils.py index 4f19d92..559d960 100644 --- a/src/utils.py +++ b/src/utils.py @@ -72,7 +72,7 @@ selected_colors = { default_font = None -default_font_size = 10 +default_font_size = '10' try: # Sugar specific tweak |