Fix issues with RTL languages and LAT/LONG (#922)

* Fix display of GPS coordinates in Places view for RTL languages

Issue #11335

* Fix place editor lat/long entry for RTL languages

Fixes #11335
This commit is contained in:
Paul Culley 2019-11-04 19:42:09 -06:00 committed by Sam Manzi
parent 2054c467db
commit 1025a38caa
4 changed files with 120 additions and 43 deletions

View File

@ -158,24 +158,30 @@ class EditPlace(EditPrimary):
self.obj.set_code, self.obj.get_code, self.obj.set_code, self.obj.get_code,
self.db.readonly) self.db.readonly)
entry = self.top.get_object("lon_entry")
entry.set_ltr_mode()
self.longitude = MonitoredEntry( self.longitude = MonitoredEntry(
self.top.get_object("lon_entry"), entry,
self.obj.set_longitude, self.obj.get_longitude, self.obj.set_longitude, self.obj.get_longitude,
self.db.readonly) self.db.readonly)
self.longitude.connect("validate", self._validate_coordinate, "lon") self.longitude.connect("validate", self._validate_coordinate, "lon")
#force validation now with initial entry #force validation now with initial entry
self.top.get_object("lon_entry").validate(force=True) entry.validate(force=True)
entry = self.top.get_object("lat_entry")
entry.set_ltr_mode()
self.latitude = MonitoredEntry( self.latitude = MonitoredEntry(
self.top.get_object("lat_entry"), entry,
self.obj.set_latitude, self.obj.get_latitude, self.obj.set_latitude, self.obj.get_latitude,
self.db.readonly) self.db.readonly)
self.latitude.connect("validate", self._validate_coordinate, "lat") self.latitude.connect("validate", self._validate_coordinate, "lat")
#force validation now with initial entry #force validation now with initial entry
self.top.get_object("lat_entry").validate(force=True) entry.validate(force=True)
entry = self.top.get_object("latlon_entry")
entry.set_ltr_mode()
self.latlon = MonitoredEntry( self.latlon = MonitoredEntry(
self.top.get_object("latlon_entry"), entry,
self.set_latlongitude, self.get_latlongitude, self.set_latlongitude, self.get_latlongitude,
self.db.readonly) self.db.readonly)

View File

@ -151,24 +151,30 @@ class EditPlaceRef(EditReference):
self.source.set_code, self.source.get_code, self.source.set_code, self.source.get_code,
self.db.readonly) self.db.readonly)
entry = self.top.get_object("lon_entry")
entry.set_ltr_mode()
self.longitude = MonitoredEntry( self.longitude = MonitoredEntry(
self.top.get_object("lon_entry"), entry,
self.source.set_longitude, self.source.get_longitude, self.source.set_longitude, self.source.get_longitude,
self.db.readonly) self.db.readonly)
self.longitude.connect("validate", self._validate_coordinate, "lon") self.longitude.connect("validate", self._validate_coordinate, "lon")
#force validation now with initial entry #force validation now with initial entry
self.top.get_object("lon_entry").validate(force=True) entry.validate(force=True)
entry = self.top.get_object("lat_entry")
entry.set_ltr_mode()
self.latitude = MonitoredEntry( self.latitude = MonitoredEntry(
self.top.get_object("lat_entry"), entry,
self.source.set_latitude, self.source.get_latitude, self.source.set_latitude, self.source.get_latitude,
self.db.readonly) self.db.readonly)
self.latitude.connect("validate", self._validate_coordinate, "lat") self.latitude.connect("validate", self._validate_coordinate, "lat")
#force validation now with initial entry #force validation now with initial entry
self.top.get_object("lat_entry").validate(force=True) entry.validate(force=True)
entry = self.top.get_object("latlon_entry")
entry.set_ltr_mode()
self.latlon = MonitoredEntry( self.latlon = MonitoredEntry(
self.top.get_object("latlon_entry"), entry,
self.set_latlongitude, self.get_latlongitude, self.set_latlongitude, self.get_latlongitude,
self.db.readonly) self.db.readonly)

View File

@ -143,7 +143,7 @@ class PlaceBaseModel:
value = conv_lat_lon('0', data[3], format='DEG')[1] value = conv_lat_lon('0', data[3], format='DEG')[1]
if not value: if not value:
return _("Error in format") return _("Error in format")
return value return ("\u202d" + value + "\u202e") if glocale.rtl_locale else value
def column_latitude(self, data): def column_latitude(self, data):
if not data[4]: if not data[4]:
@ -151,7 +151,7 @@ class PlaceBaseModel:
value = conv_lat_lon(data[4], '0', format='DEG')[0] value = conv_lat_lon(data[4], '0', format='DEG')[0]
if not value: if not value:
return _("Error in format") return _("Error in format")
return value return ("\u202d" + value + "\u202e") if glocale.rtl_locale else value
def sort_longitude(self, data): def sort_longitude(self, data):
if not data[3]: if not data[3]:

View File

@ -46,10 +46,10 @@ from gi.repository import Gtk
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
from .undoablebuffer import Stack from .undoablebuffer import Stack
from gramps.gen.const import GRAMPS_LOCALE as glocale
# table for skipping illegal control chars # table for skipping illegal control chars
INVISIBLE = dict.fromkeys(list(range(32))) INVISIBLE = dict.fromkeys(list(range(32)) + [0x202d, 0x202e])
class UndoableInsertEntry: class UndoableInsertEntry:
@ -89,6 +89,10 @@ class UndoableEntry(Gtk.Entry, Gtk.Editable):
Additional features: Additional features:
- Undo and Redo on CTRL-Z/CTRL-SHIFT-Z - Undo and Redo on CTRL-Z/CTRL-SHIFT-Z
- ltr_mode (forces the field to always be left to right, useful for GPS
coordinates and similar numbers that might contain RTL characters.
See set_ltr_mode.
""" """
__gtype_name__ = 'UndoableEntry' __gtype_name__ = 'UndoableEntry'
@ -99,12 +103,12 @@ class UndoableEntry(Gtk.Entry, Gtk.Editable):
undo_stack_size = 50 undo_stack_size = 50
def __init__(self): def __init__(self):
Gtk.Entry.__init__(self)
self.undo_stack = Stack(self.undo_stack_size) self.undo_stack = Stack(self.undo_stack_size)
self.redo_stack = [] self.redo_stack = []
self.not_undoable_action = False self.not_undoable_action = False
self.undo_in_progress = False self.undo_in_progress = False
self.ltr_mode = False
Gtk.Entry.__init__(self)
self.connect('delete-text', self._on_delete_text) self.connect('delete-text', self._on_delete_text)
self.connect('key-press-event', self._on_key_press_event) self.connect('key-press-event', self._on_key_press_event)
@ -162,10 +166,16 @@ class UndoableEntry(Gtk.Entry, Gtk.Editable):
return True return True
text = text.translate(INVISIBLE) text = text.translate(INVISIBLE)
if self.ltr_mode:
if position == 0:
position = 1
elif position >= self.get_text_length():
position -= 1
if not self.undo_in_progress: if not self.undo_in_progress:
self.__empty_redo_stack() self.__empty_redo_stack()
while not self.not_undoable_action: while not self.not_undoable_action:
undo_action = self.insertclass(text, length, self.get_position()) undo_action = self.insertclass(text, length, position)
try: try:
prev_insert = self.undo_stack.pop() prev_insert = self.undo_stack.pop()
except IndexError: except IndexError:
@ -186,6 +196,7 @@ class UndoableEntry(Gtk.Entry, Gtk.Editable):
self.get_buffer().insert_text(position, text, len(text)) self.get_buffer().insert_text(position, text, len(text))
return position + len(text) return position + len(text)
def _on_delete_text(self, editable, start, end): def _on_delete_text(self, editable, start, end):
def can_be_merged(prev, cur): def can_be_merged(prev, cur):
""" """
@ -212,32 +223,49 @@ class UndoableEntry(Gtk.Entry, Gtk.Editable):
return False return False
return True return True
if not self.undo_in_progress: if self.ltr_mode: # limit deletes to area between LRO/PDF
self.__empty_redo_stack() if start == 0:
if self.not_undoable_action: start = 1
return elif start > self.get_text_length() - 1:
undo_action = self.deleteclass(editable, start, end) start -= 1
try: if end == 0:
prev_delete = self.undo_stack.pop() end = 1
except IndexError: elif end > self.get_text_length() - 1:
self.undo_stack.append(undo_action) end -= 1
return elif end < 0:
if not isinstance(prev_delete, self.deleteclass): end = self.get_text_length() - 1
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action) while True:
return if not self.undo_in_progress:
if can_be_merged(prev_delete, undo_action): self.__empty_redo_stack()
if prev_delete.start == undo_action.start: # delete key used if self.not_undoable_action:
prev_delete.text += undo_action.text break
prev_delete.end += (undo_action.end - undo_action.start) undo_action = self.deleteclass(self, start, end)
else: # Backspace used try:
prev_delete.text = "%s%s" % (undo_action.text, prev_delete = self.undo_stack.pop()
prev_delete.text) except IndexError:
prev_delete.start = undo_action.start self.undo_stack.append(undo_action)
self.undo_stack.append(prev_delete) break
else: if not isinstance(prev_delete, self.deleteclass):
self.undo_stack.append(prev_delete) self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action) self.undo_stack.append(undo_action)
break
if can_be_merged(prev_delete, undo_action):
if prev_delete.start == undo_action.start: # delete key used
prev_delete.text += undo_action.text
prev_delete.end += (undo_action.end - undo_action.start)
else: # Backspace used
prev_delete.text = "%s%s" % (undo_action.text,
prev_delete.text)
prev_delete.start = undo_action.start
self.undo_stack.append(prev_delete)
else:
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
break
self.get_buffer().delete_text(start, end - start)
self.stop_emission_by_name('delete-text')
return True
def begin_not_undoable_action(self): def begin_not_undoable_action(self):
"""don't record the next actions """don't record the next actions
@ -329,3 +357,40 @@ class UndoableEntry(Gtk.Entry, Gtk.Editable):
def _handle_redo(self, redo_action): def _handle_redo(self, redo_action):
raise NotImplementedError raise NotImplementedError
def set_ltr_mode(self):
""" sets up the Entry to always be in LTR left to right even if some
characters are RTL.
This works by inserting the LRO/PDF Unicode Explicit Directional
Override characters around the entry text. These characters are then
protected agains insert/delete operations.
This call must be made before other text is inserted to the Entry.
Note: we only enable this during rtl_local languages because it has a
minor consequence; if cutting a field from this Entry with this mode
enabled, the LRO/PDF characters may end up in the clipboard. If pasted
back into another UndoableEntry, this is ignored, but if pasted in
another app it may be noticable.
"""
if glocale.rtl_locale:
self.get_buffer().set_text("\u202d\u202e", -1)
self.ltr_mode = True
def do_set_position(self, position):
""" In ltr_mode, this ensures that the cursor cannot be put outside
the LRO/PDF characters on the ends of the buffer. """
if position < 0:
position = self.get_text_length()
if self.ltr_mode:
if position == 0:
position = 1
elif position == self.get_text_length():
position -= 1
Gtk.Editable.select_region(self, position, position)
def get_text(self):
""" Used to remove the LRO/PDF characters when in ltr_mode.
"""
text = Gtk.Entry.get_text(self)
return text[1:-1] if self.ltr_mode else text