1# Copyright 2008-2021 Jaap Karssenberg <jaap.karssenberg@gmail.com>
2
3'''This module contains the main text editor widget.
4It includes all classes needed to display and edit a single page as well
5as related dialogs like the dialogs to insert images, links etc.
6
7The main widget accessed by the rest of the application is the
8L{PageView} class. This wraps a L{TextView} widget which actually
9shows the page. The L{TextBuffer} class is the data model used by the
10L{TextView}.
11
12@todo: for documentation group functions in more logical order
13'''
14
15
16
17import logging
18
19from gi.repository import GObject
20from gi.repository import GLib
21from gi.repository import Gtk
22from gi.repository import Gdk
23from gi.repository import GdkPixbuf
24from gi.repository import Pango
25
26import re
27import string
28import weakref
29import functools
30import zim.datetimetz as datetime
31
32import zim.formats
33import zim.errors
34
35from zim.fs import File, Dir, normalize_file_uris, FilePath, adapt_from_newfs
36from zim.errors import Error
37from zim.config import String, Float, Integer, Boolean, Choice, ConfigManager
38from zim.notebook import Path, interwiki_link, HRef, PageNotFoundError
39from zim.notebook.operations import NotebookState, ongoing_operation
40from zim.parsing import link_type, Re
41from zim.formats import heading_to_anchor, get_format, increase_list_iter, \
42	ParseTree, ElementTreeModule, OldParseTreeBuilder, \
43	BULLET, CHECKED_BOX, UNCHECKED_BOX, XCHECKED_BOX, TRANSMIGRATED_BOX, MIGRATED_BOX, LINE, OBJECT, \
44	HEADING, LISTITEM, BLOCK_LEVEL
45from zim.formats.wiki import url_re, match_url
46from zim.actions import get_gtk_actiongroup, action, toggle_action, get_actions, \
47	ActionClassMethod, ToggleActionClassMethod, initialize_actiongroup
48from zim.gui.widgets import \
49	Dialog, FileDialog, QuestionDialog, ErrorDialog, \
50	IconButton, MenuButton, BrowserTreeView, InputEntry, \
51	ScrolledWindow, \
52	rotate_pixbuf, populate_popup_add_separator, strip_boolean_result, \
53	widget_set_css
54from zim.gui.applications import OpenWithMenu, open_url, open_file, edit_config_file
55from zim.gui.clipboard import Clipboard, SelectionClipboard, \
56	textbuffer_register_serialize_formats
57from zim.gui.insertedobjects import \
58	InsertedObjectWidget, UnknownInsertedObject, UnknownInsertedImageObject, \
59	POSITION_BEGIN, POSITION_END
60from zim.signals import callback
61from zim.formats import get_dumper
62from zim.formats.wiki import Dumper as WikiDumper
63from zim.plugins import PluginManager
64
65from .editbar import EditBar
66
67
68logger = logging.getLogger('zim.gui.pageview')
69
70
71MAX_PAGES_UNDO_STACK = 10 #: Keep this many pages in a queue to keep ref and thus undostack alive
72
73
74class LineSeparator(InsertedObjectWidget):
75	'''Class to create a separation line.'''
76
77	def __init__(self):
78		InsertedObjectWidget.__init__(self)
79		widget = Gtk.Box()
80		widget.get_style_context().add_class(Gtk.STYLE_CLASS_BACKGROUND)
81		widget.set_size_request(-1, 3)
82		self.add(widget)
83
84
85def is_line(line):
86	'''Function used for line autoformatting.'''
87	length = len(line)
88	return (line == '-' * length) and (length >= 3)
89
90
91STOCK_CHECKED_BOX = 'zim-checked-box'
92STOCK_UNCHECKED_BOX = 'zim-unchecked-box'
93STOCK_XCHECKED_BOX = 'zim-xchecked-box'
94STOCK_MIGRATED_BOX = 'zim-migrated-box'
95STOCK_TRANSMIGRATED_BOX = 'zim-transmigrated-box'
96
97bullet_types = {
98	CHECKED_BOX: STOCK_CHECKED_BOX,
99	UNCHECKED_BOX: STOCK_UNCHECKED_BOX,
100	XCHECKED_BOX: STOCK_XCHECKED_BOX,
101	MIGRATED_BOX: STOCK_MIGRATED_BOX,
102	TRANSMIGRATED_BOX: STOCK_TRANSMIGRATED_BOX,
103}
104
105# reverse dict
106bullets = {}
107for bullet in bullet_types:
108	bullets[bullet_types[bullet]] = bullet
109
110autoformat_bullets = {
111	'*': BULLET,
112	'[]': UNCHECKED_BOX,
113	'[ ]': UNCHECKED_BOX,
114	'[*]': CHECKED_BOX,
115	'[x]': XCHECKED_BOX,
116	'[>]': MIGRATED_BOX,
117	'[<]': TRANSMIGRATED_BOX,
118	'()': UNCHECKED_BOX,
119	'( )': UNCHECKED_BOX,
120	'(*)': CHECKED_BOX,
121	'(x)': XCHECKED_BOX,
122	'(>)': MIGRATED_BOX,
123	'(<)': TRANSMIGRATED_BOX,
124}
125
126BULLETS = (BULLET, UNCHECKED_BOX, CHECKED_BOX, XCHECKED_BOX, MIGRATED_BOX, TRANSMIGRATED_BOX)
127CHECKBOXES = (UNCHECKED_BOX, CHECKED_BOX, XCHECKED_BOX, MIGRATED_BOX, TRANSMIGRATED_BOX)
128
129NUMBER_BULLET = '#.' # Special case for autonumbering
130is_numbered_bullet_re = re.compile('^(\d+|\w|#)\.$')
131	#: This regular expression is used to test whether a bullet belongs to a numbered list or not
132
133# Check the (undocumented) list of constants in Gtk.keysyms to see all names
134KEYVALS_HOME = list(map(Gdk.keyval_from_name, ('Home', 'KP_Home')))
135KEYVALS_ENTER = list(map(Gdk.keyval_from_name, ('Return', 'KP_Enter', 'ISO_Enter')))
136KEYVALS_BACKSPACE = list(map(Gdk.keyval_from_name, ('BackSpace',)))
137KEYVALS_TAB = list(map(Gdk.keyval_from_name, ('Tab', 'KP_Tab')))
138KEYVALS_LEFT_TAB = list(map(Gdk.keyval_from_name, ('ISO_Left_Tab',)))
139
140# ~ CHARS_END_OF_WORD = (' ', ')', '>', '.', '!', '?')
141CHARS_END_OF_WORD = ('\t', ' ', ')', '>', ';')
142KEYVALS_END_OF_WORD = list(map(
143	Gdk.unicode_to_keyval, list(map(ord, CHARS_END_OF_WORD)))) + KEYVALS_TAB
144
145KEYVALS_ASTERISK = (
146	Gdk.unicode_to_keyval(ord('*')), Gdk.keyval_from_name('KP_Multiply'))
147KEYVALS_SLASH = (
148	Gdk.unicode_to_keyval(ord('/')), Gdk.keyval_from_name('KP_Divide'))
149KEYVALS_GT = (Gdk.unicode_to_keyval(ord('>')),)
150KEYVALS_SPACE = (Gdk.unicode_to_keyval(ord(' ')),)
151
152KEYVAL_ESC = Gdk.keyval_from_name('Escape')
153KEYVAL_POUND = Gdk.unicode_to_keyval(ord('#'))
154
155# States that influence keybindings - we use this to explicitly
156# exclude other states. E.g. MOD2_MASK seems to be set when either
157# numlock or fn keys are active, resulting in keybindings failing
158KEYSTATES = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.META_MASK | Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK
159
160MENU_ACTIONS = (
161	# name, stock id, label
162	('insert_new_file_menu', None, _('New _Attachment')), # T: Menu title
163)
164
165COPY_FORMATS = zim.formats.list_formats(zim.formats.TEXT_FORMAT)
166
167ui_preferences = (
168	# key, type, category, label, default
169	('show_edit_bar', 'bool', 'Interface',
170		_('Show edit bar along bottom of editor'), True),
171		# T: option in preferences dialog
172	('follow_on_enter', 'bool', 'Interface',
173		_('Use the <Enter> key to follow links\n(If disabled you can still use <Alt><Enter>)'), True),
174		# T: option in preferences dialog
175	('read_only_cursor', 'bool', 'Interface',
176		_('Show the cursor also for pages that can not be edited'), False),
177		# T: option in preferences dialog
178	('autolink_camelcase', 'bool', 'Editing',
179		_('Automatically turn "CamelCase" words into links'), True),
180		# T: option in preferences dialog
181	('autolink_files', 'bool', 'Editing',
182		_('Automatically turn file paths into links'), True),
183		# T: option in preferences dialog
184	('autoselect', 'bool', 'Editing',
185		_('Automatically select the current word when you apply formatting'), True),
186		# T: option in preferences dialog
187	('unindent_on_backspace', 'bool', 'Editing',
188		_('Unindent on <BackSpace>\n(If disabled you can still use <Shift><Tab>)'), True),
189		# T: option in preferences dialog
190	('cycle_checkbox_type', 'bool', 'Editing',
191		_('Repeated clicking a checkbox cycles through the checkbox states'), True),
192		# T: option in preferences dialog
193	('recursive_indentlist', 'bool', 'Editing',
194		_('(Un-)indenting a list item also changes any sub-items'), True),
195		# T: option in preferences dialog
196	('recursive_checklist', 'bool', 'Editing',
197		_('Checking a checkbox also changes any sub-items'), False),
198		# T: option in preferences dialog
199	('auto_reformat', 'bool', 'Editing',
200		_('Reformat wiki markup on the fly'), False),
201		# T: option in preferences dialog
202	('copy_format', 'choice', 'Editing',
203		_('Default format for copying text to the clipboard'), 'Text', COPY_FORMATS),
204		# T: option in preferences dialog
205	('file_templates_folder', 'dir', 'Editing',
206		_('Folder with templates for attachment files'), '~/Templates'),
207		# T: option in preferences dialog
208)
209
210_is_zim_tag = lambda tag: hasattr(tag, 'zim_type')
211_is_indent_tag = lambda tag: _is_zim_tag(tag) and tag.zim_type == 'indent'
212_is_not_indent_tag = lambda tag: _is_zim_tag(tag) and tag.zim_type != 'indent'
213_is_heading_tag = lambda tag: hasattr(tag, 'zim_tag') and tag.zim_tag == 'h'
214_is_pre_tag = lambda tag: hasattr(tag, 'zim_tag') and tag.zim_tag == 'pre'
215_is_pre_or_code_tag = lambda tag: hasattr(tag, 'zim_tag') and tag.zim_tag in ('pre', 'code')
216_is_line_based_tag = lambda tag: _is_indent_tag(tag) or _is_heading_tag(tag) or _is_pre_tag(tag)
217_is_not_line_based_tag = lambda tag: not _is_line_based_tag(tag)
218_is_style_tag = lambda tag: _is_zim_tag(tag) and tag.zim_type == 'style'
219_is_not_style_tag = lambda tag: not (_is_zim_tag(tag) and tag.zim_type == 'style')
220_is_link_tag = lambda tag: _is_zim_tag(tag) and tag.zim_type == 'link'
221_is_not_link_tag = lambda tag: not (_is_zim_tag(tag) and tag.zim_type == 'link')
222_is_tag_tag = lambda tag: _is_zim_tag(tag) and tag.zim_type == 'tag'
223_is_not_tag_tag = lambda tag: not (_is_zim_tag(tag) and tag.zim_type == 'tag')
224_is_inline_nesting_tag = lambda tag: _is_zim_tag(tag) and tag.zim_tag in TextBuffer._nesting_style_tags or tag.zim_type == 'link'
225_is_non_nesting_tag = lambda tag: hasattr(tag, 'zim_tag') and tag.zim_tag in ('pre', 'code', 'tag')
226_is_link_tag_without_href = lambda tag: _is_link_tag(tag) and not tag.zim_attrib['href']
227
228PIXBUF_CHR = '\uFFFC'
229
230# Minimal distance from mark to window border after scroll_to_mark()
231SCROLL_TO_MARK_MARGIN = 0.2
232
233# Regexes used for autoformatting
234heading_re = Re(r'^(={2,7})\s*(.*?)(\s=+)?$')
235
236link_to_page_re = Re(r'''(
237	  [\w\.\-\(\)]*(?: :[\w\.\-\(\)]{2,} )+ (?: : | \#\w[\w_-]+)?
238	| \+\w[\w\.\-\(\)]+(?: :[\w\.\-\(\)]{2,} )* (?: : | \#\w[\w_-]+)?
239)$''', re.X | re.U)
240	# e.g. namespace:page or +subpage, but not word without ':' or '+'
241	#      optionally followed by anchor id
242	#      links with only anchor id or page (without ':' or '+') and achor id are matched by 'link_to_anchor_re'
243
244interwiki_re = Re(r'\w[\w\+\-\.]+\?\w\S+$', re.U) # name?page, where page can be any url style
245
246file_re = Re(r'''(
247	  ~/[^/\s]
248	| ~[^/\s]*/
249	| \.\.?/
250	| /[^/\s]
251)\S*$''', re.X | re.U) # ~xxx/ or ~name/xxx or ../xxx  or ./xxx  or /xxx
252
253markup_re = [
254	# All ending in "$" to match last sequence on end-of-word
255	# the group captures the content to keep
256	('style-strong', re.compile(r'\*\*(.*)\*\*$')),
257	('style-emphasis', re.compile(r'\/\/(.*)\/\/$')),
258	('style-mark', re.compile(r'__(.*)__$')),
259	('style-code', re.compile(r'\'\'(.*)\'\'$')),
260	('style-strike', re.compile(r'~~(.*)~~$')),
261	('style-sup', re.compile(r'(?<=\w)\^\{(\S*)}$')),
262	('style-sup', re.compile(r'(?<=\w)\^(\S*)$')),
263	('style-sub', re.compile(r'(?<=\w)_\{(\S*)}$')),
264]
265
266link_to_anchor_re = Re(r'^([\w\.\-\(\)]*#\w[\w_-]+)$', re.U) # before the "#" can be a page name, needs to match logic in 'link_to_page_re'
267
268anchor_re = Re(r'^(##\w[\w_-]+)$', re.U)
269
270tag_re = Re(r'^(@\w+)$', re.U)
271
272twoletter_re = re.compile(r'[^\W\d]{2}', re.U) # match letters but not numbers - not non-alphanumeric and not number
273
274
275def camelcase(word):
276	# To be CamelCase, a word needs to start uppercase, followed
277	# by at least one lower case, followed by at least one uppercase.
278	# As a result:
279	# - CamelCase needs at least 3 characters
280	# - first char needs to be upper case
281	# - remainder of the text needs to be mixed case
282	if len(word) < 3 \
283	or not str.isalpha(word) \
284	or not str.isupper(word[0]) \
285	or str.islower(word[1:]) \
286	or str.isupper(word[1:]):
287		return False
288
289	# Now do detailed check and check indeed lower case followed by
290	# upper case and exclude e.g. "AAbb"
291	# Also check that check that string does not contain letters that
292	# are neither upper or lower case (e.g. some Arabic letters)
293	upper = list(map(str.isupper, word))
294	lower = list(map(str.islower, word))
295	if not all(upper[i] or lower[i] for i in range(len(word))):
296		return False
297
298	count = 0
299	for i in range(1, len(word)):
300		if not upper[i - 1] and upper[i]:
301			return True
302	else:
303		return False
304
305
306def increase_list_bullet(bullet):
307	'''Like L{increase_list_iter()}, but handles bullet string directly
308	@param bullet: a numbered list bullet, e.g. C{"1."}
309	@returns: the next bullet, e.g. C{"2."} or C{None}
310	'''
311	next = increase_list_iter(bullet.rstrip('.'))
312	if next:
313		return next + '.'
314	else:
315		return None
316
317
318class AsciiString(String):
319
320	# pango doesn't like unicode attributes
321
322	def check(self, value):
323		value = String.check(self, value)
324		if isinstance(value, str):
325			return str(value)
326		else:
327			return value
328
329
330
331class ConfigDefinitionConstant(String):
332
333	def __init__(self, default, group, prefix):
334		self.group = group
335		self.prefix = prefix
336		String.__init__(self, default=default)
337
338	def check(self, value):
339		value = String.check(self, value)
340		if isinstance(value, str):
341			value = value.upper()
342			for prefix in (self.prefix, self.prefix.split('_', 1)[1]):
343				# e.g. PANGO_WEIGHT_BOLD --> BOLD but also WEIGHT_BOLD --> BOLD
344				if value.startswith(prefix):
345					value = value[len(prefix):]
346				value = value.lstrip('_')
347
348			if hasattr(self.group, value):
349				return getattr(self.group, value)
350			else:
351				raise ValueError('No such constant: %s_%s' % (self.prefix, value))
352		else:
353			return value
354
355	def tostring(self, value):
356		if hasattr(value, 'value_name'):
357			return value.value_name
358		else:
359			return str(value)
360
361
362
363class UserActionContext(object):
364	'''Context manager to wrap actions in proper user-action signals
365
366	This class used for the L{TextBuffer.user_action} attribute
367
368	This allows syntax like::
369
370		with buffer.user_action:
371			buffer.insert(...)
372
373	instead off::
374
375		buffer.begin_user_action()
376		buffer.insert(...)
377		buffer.end_user_action()
378
379	By wrapping actions in this "user-action" block the
380	L{UndoStackManager} will see it as a single action and make it
381	undo-able in a single step.
382	'''
383
384	def __init__(self, buffer):
385		self.buffer = buffer
386
387	def __enter__(self):
388		self.buffer.begin_user_action()
389
390	def __exit__(self, *a):
391		self.buffer.end_user_action()
392
393
394GRAVITY_RIGHT = 'right'
395GRAVITY_LEFT = 'left'
396
397class SaveCursorContext(object):
398	'''Context manager used by L{TextBuffer.tmp_cursor()}
399
400	This allows syntax like::
401
402		with buffer.tmp_cursor(iter):
403			# some manipulation using iter as cursor position
404
405		# old cursor position restored
406
407	Basically it keeps a mark for the old cursor and restores it
408	after exiting the context.
409	'''
410
411	def __init__(self, buffer, iter=None, gravity=GRAVITY_LEFT):
412		self.buffer = buffer
413		self.iter = iter
414		self.mark = None
415		self.gravity = gravity
416
417	def __enter__(self):
418		buffer = self.buffer
419		cursor = buffer.get_iter_at_mark(buffer.get_insert())
420		self.mark = buffer.create_mark(None, cursor, left_gravity=(self.gravity == GRAVITY_LEFT))
421		if self.iter:
422			buffer.place_cursor(self.iter)
423
424	def __exit__(self, *a):
425		buffer = self.buffer
426		iter = buffer.get_iter_at_mark(self.mark)
427		buffer.place_cursor(iter)
428		buffer.delete_mark(self.mark)
429
430
431def image_file_get_dimensions(file_path):
432	"""
433	Replacement for GdkPixbuf.Pixbuf.get_file_info
434	@return (width, height) in pixels
435		or None if file does not exist or failed to load
436	"""
437
438	# Let GTK try reading the file
439	_, width, height = GdkPixbuf.Pixbuf.get_file_info(file_path)
440	if width > 0 and height > 0:
441		return (width, height)
442
443	# Fallback to Pillow
444	try:
445		from PIL import Image # load Pillow only if necessary
446		with Image.open(file_path) as img_pil:
447			return (img_pil.width, img_pil.height)
448	except:
449		return None
450
451
452def image_file_load_pixels(file, width_override=-1, height_override=-1):
453	"""
454	Replacement for GdkPixbuf.Pixbuf.new_from_file_at_size(file.path, w, h)
455	When file does not exist or fails to load, this throws exceptions.
456	"""
457
458	if not file.exists():
459		# if the file does not exist, no need to make the effort of trying to read it
460		raise FileNotFoundError(file.path)
461
462	b_size_override = width_override > 0 or height_override > 0
463
464	# Let GTK try reading the file
465	try:
466		if b_size_override:
467			pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(file.path, width_override, height_override)
468		else:
469			pixbuf = GdkPixbuf.Pixbuf.new_from_file(file.path)
470
471		pixbuf = rotate_pixbuf(pixbuf)
472
473	except:
474		logger.debug('GTK failed to read image, using Pillow fallback: %s', file.path)
475
476		from PIL import Image # load Pillow only if necessary
477
478		with Image.open(file.path) as img_pil:
479
480			# resize if a specific size was requested
481			if b_size_override:
482				if height_override <= 0:
483					height_override = int(img_pil.height * width_override / img_pil.width)
484				if width_override <= 0:
485					width_override = int(img_pil.width * height_override / img_pil.height)
486
487				logger.debug('PIL resizing %s %s', width_override, height_override)
488				img_pil = img_pil.resize((width_override, height_override))
489
490			# check if there is an alpha channel
491			if img_pil.mode == 'RGB':
492				has_alpha = False
493			elif img_pil.mode == 'RGBA':
494				has_alpha = True
495			else:
496				raise ValueError('Pixel format {fmt} can not be converted to Pixbuf for image {p}'.format(
497					fmt = img_pil.mode, p = file.path,
498				))
499
500			# convert to GTK pixbuf
501			data_gtk = GLib.Bytes.new_take(img_pil.tobytes())
502
503			pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
504				data = data_gtk,
505				colorspace = GdkPixbuf.Colorspace.RGB,
506				has_alpha = has_alpha,
507				# GTK docs: "Currently only RGB images with 8 bits per sample are supported"
508				# https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-Image-Data-in-Memory.html#gdk-pixbuf-new-from-bytes
509				bits_per_sample = 8,
510				width = img_pil.width,
511				height = img_pil.height,
512				rowstride = img_pil.width * (4 if has_alpha else 3),
513			)
514
515	return pixbuf
516
517
518class TextBuffer(Gtk.TextBuffer):
519	'''Data model for the editor widget
520
521	This sub-class of C{Gtk.TextBuffer} manages the contents of
522	the L{TextView} widget. It has an internal data model that allows
523	to manipulate the formatted text by cursor positions. It manages
524	images, links, bullet lists etc. The methods L{set_parsetree()}
525	and L{get_parsetree()} can exchange the formatted text as a
526	L{ParseTree} object which can be parsed by the L{zim.formats}
527	modules.
528
529	Styles
530	======
531
532	Formatting styles like bold, italic etc. as well as functional
533	text objects like links and tags are represented by C{Gtk.TextTags}.
534	For static styles these TextTags have the same name as the style.
535	For links and tag anonymous TextTags are used. Be aware though that
536	not all TextTags in the model are managed by us, e.g. gtkspell
537	uses it's own tags. TextTags that are managed by us have an
538	additional attribute C{zim_type} which gives the format type
539	for this tag. All TextTags without this attribute are not ours.
540	All TextTags that have a C{zim_type} attribute also have an
541	C{zim_attrib} attribute, which can be either C{None} or contain
542	some properties, like the C{href} property for a link. See the
543	parsetree documentation for what properties to expect.
544
545	The buffer keeps an internal state for what tags should be applied
546	to new text and applies these automatically when text is inserted.
547	E.g. when you place the cursor at the end of a bold area and
548	start typing the new text will be bold as well. However when you
549	move to the beginning of the area it will not be bold.
550
551	One limitation is that the current code supposes only one format
552	style can be applied to a part of text at the same time. This
553	means you can not overlap e.g. bold and italic styles. But it
554	makes the code simpler because we only deal with one style at a
555	time.
556
557	Images
558	======
559
560	Embedded images and icons are handled by C{GdkPixbuf.Pixbuf} object.
561	Again the ones that are handled by us have the extry C{zim_type} and
562	C{zim_attrib} attributes.
563
564	Lists
565	=====
566
567	As far as this class is concerned bullet and checkbox lists are just
568	a number of lines that start with a bullet (checkboxes are rendered
569	with small images or icons, but are also considered bullets).
570	There is some logic to keep list formatting nicely but it only
571	applies to one line at a time. For functionality affecting a list
572	as a whole see the L{TextBufferList} class.
573
574	@todo: The buffer needs a reference to the notebook and page objects
575	for the text that is being shown to make sure that e.g. serializing
576	links works correctly. Check if we can get rid of page and notebook
577	here and just put provide them as arguments when needed.
578
579	@cvar tag_styles: This dict defines the formatting styles supported
580	by the editor. The style properties are overruled by the values
581	from the X{style.conf} config file.
582
583	@ivar notebook: The L{Notebook} object
584	@ivar page: The L{Page} object
585	@ivar user_action: A L{UserActionContext} context manager
586	@ivar finder: A L{TextFinder} for this buffer
587
588	@signal: C{begin-insert-tree (interactive)}:
589	Emitted at the begin of a complex insert, c{interactive} is boolean flag
590	@signal: C{end-insert-tree ()}:
591	Emitted at the end of a complex insert
592	@signal: C{textstyle-changed (style)}:
593	Emitted when textstyle at the cursor changes, gets the list of text styles or None.
594	@signal: C{link-clicked ()}:
595	Emitted when a link is clicked; for example within a table cell
596	@signal: C{undo-save-cursor (iter)}:
597	emitted in some specific case where the undo stack should
598	lock the current cursor position
599	@signal: C{insert-objectanchor (achor)}: emitted when an object
600	is inserted, should trigger L{TextView} to attach a widget
601
602	@todo: document tag styles that are supported
603	'''
604
605	# We rely on the priority of gtk TextTags to sort links before styles,
606	# and styles before indenting. Since styles are initialized on init,
607	# while indenting tags are created when needed, indenting tags always
608	# have the higher priority. By explicitly lowering the priority of new
609	# link tags to zero we keep those tags on the lower endof the scale.
610
611
612	# define signals we want to use - (closure type, return type and arg types)
613	__gsignals__ = {
614		'insert-text': 'override',
615		'begin-insert-tree': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
616		'end-insert-tree': (GObject.SignalFlags.RUN_LAST, None, ()),
617		'textstyle-changed': (GObject.SignalFlags.RUN_LAST, None, (object,)),
618		'undo-save-cursor': (GObject.SignalFlags.RUN_LAST, None, (object,)),
619		'insert-objectanchor': (GObject.SignalFlags.RUN_LAST, None, (object,)),
620		'link-clicked': (GObject.SignalFlags.RUN_LAST, None, (object,)),
621	}
622
623	# style attributes
624	pixels_indent = 30 #: pixels indent for a single indent level
625	bullet_icon_size = Gtk.IconSize.MENU #: constant for icon size of checkboxes etc.
626
627	#: text styles supported by the editor
628	tag_styles = {
629		'h1': {'weight': Pango.Weight.BOLD, 'scale': 1.15**4},
630		'h2': {'weight': Pango.Weight.BOLD, 'scale': 1.15**3},
631		'h3': {'weight': Pango.Weight.BOLD, 'scale': 1.15**2},
632		'h4': {'weight': Pango.Weight.ULTRABOLD, 'scale': 1.15},
633		'h5': {'weight': Pango.Weight.BOLD, 'scale': 1.15, 'style': Pango.Style.ITALIC},
634		'h6': {'weight': Pango.Weight.BOLD, 'scale': 1.15},
635		'emphasis': {'style': Pango.Style.ITALIC},
636		'strong': {'weight': Pango.Weight.BOLD},
637		'mark': {'background': 'yellow'},
638		'strike': {'strikethrough': True, 'foreground': 'grey'},
639		'code': {'family': 'monospace'},
640		'pre': {'family': 'monospace', 'wrap-mode': Gtk.WrapMode.NONE},
641		'sub': {'rise': -3500, 'scale': 0.7},
642		'sup': {'rise': 7500, 'scale': 0.7},
643		'link': {'foreground': 'blue'},
644		'tag': {'foreground': '#ce5c00'},
645		'indent': {},
646		'bullet-list': {},
647		'numbered-list': {},
648		'unchecked-checkbox': {},
649		'checked-checkbox': {},
650		'xchecked-checkbox': {},
651		'migrated-checkbox': {},
652		'transmigrated-checkbox': {},
653		'find-highlight': {'background': 'magenta', 'foreground': 'white'},
654		'find-match': {'background': '#38d878', 'foreground': 'white'}
655	}
656
657	#: tags that can be mapped to named TextTags
658	_static_style_tags = (
659		# The order determines order of nesting, and order of formatting
660		# Indent-tags will be inserted before headings
661		# Link-tags and tag-tags will be inserted before "pre" and "code"
662		# search for "set_priority()" and "get_priority()" to see impact
663		'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
664		'emphasis', 'strong', 'mark', 'strike', 'sub', 'sup',
665		'pre', 'code',
666	)
667	_static_tag_before_links = 'sup' # link will be inserted with this prio +1
668	_static_tag_after_tags = 'pre' # link will be inserted with this prio
669
670	#: tags that can nest in any order
671	_nesting_style_tags = (
672		'emphasis', 'strong', 'mark', 'strike', 'sub', 'sup',
673	)
674
675	tag_attributes = {
676		'weight': ConfigDefinitionConstant(None, Pango.Weight, 'PANGO_WEIGHT'),
677		'scale': Float(None),
678		'style': ConfigDefinitionConstant(None, Pango.Style, 'PANGO_STYLE'),
679		'background': AsciiString(None),
680		'paragraph-background': AsciiString(None),
681		'foreground': AsciiString(None),
682		'strikethrough': Boolean(None),
683		'font': AsciiString(None),
684		'family': AsciiString(None),
685		'wrap-mode': ConfigDefinitionConstant(None, Gtk.WrapMode, 'GTK_WRAP'),
686		'indent': Integer(None),
687		'underline': ConfigDefinitionConstant(None, Pango.Underline, 'PANGO_UNDERLINE'),
688		'linespacing': Integer(None),
689		'wrapped-lines-linespacing': Integer(None),
690		'rise': Integer(None),
691	} #: Valid properties for a style in tag_styles
692
693	def __init__(self, notebook, page, parsetree=None):
694		'''Constructor
695
696		@param notebook: a L{Notebook} object
697		@param page: a L{Page} object
698		@param parsetree: optional L{ParseTree} object, if given this will
699		initialize the buffer content *before* initializing the undostack
700		'''
701		GObject.GObject.__init__(self)
702		self.notebook = notebook
703		self.page = page
704		self._insert_tree_in_progress = False
705		self._deleted_editmode_mark = None
706		self._deleted_line_end = False
707		self._check_renumber = []
708		self._renumbering = False
709		self.user_action = UserActionContext(self)
710		self.finder = TextFinder(self)
711		self.showing_template = False
712
713		for name in self._static_style_tags:
714			tag = self.create_tag('style-' + name, **self.tag_styles[name])
715			tag.zim_type = 'style'
716			if name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
717				# This is needed to get proper output in get_parse_tree
718				tag.zim_tag = 'h'
719				tag.zim_attrib = {'level': int(name[1])}
720			else:
721				tag.zim_tag = name
722				tag.zim_attrib = None
723
724		self._editmode_tags = []
725
726		textbuffer_register_serialize_formats(self, notebook, page)
727
728		self.connect('delete-range', self.__class__.do_pre_delete_range)
729		self.connect_after('delete-range', self.__class__.do_post_delete_range)
730
731		if parsetree is not None:
732			# Do this *before* initializing the undostack
733			self.set_parsetree(parsetree)
734			self.set_modified(False)
735
736		self.undostack = UndoStackManager(self)
737
738	#~ def do_begin_user_action(self):
739		#~ print('>>>> USER ACTION')
740		#~ pass
741
742	@property
743	def hascontent(self):
744		if self.showing_template:
745			return False
746		else:
747			start, end = self.get_bounds()
748			return not start.equal(end)
749
750	def do_end_user_action(self):
751		#print('<<<< USER ACTION')
752		if self._deleted_editmode_mark is not None:
753			self.delete_mark(self._deleted_editmode_mark)
754			self._deleted_editmode_mark = None
755
756		if True: # not self._renumbering:
757			lines = list(self._check_renumber)
758				# copy to avoid infinite loop when updating bullet triggers new delete
759			self._renumbering = True
760			for line in lines:
761				self.renumber_list(line)
762				# This flag means we deleted a line, and now we need
763				# to check if the numbering is still valid.
764				# It is delayed till here because this logic only applies
765				# to interactive actions.
766			self._renumbering = False
767			self._check_renumber = []
768
769	def clear(self):
770		self.delete(*self.get_bounds())
771		if self._deleted_editmode_mark is not None:
772			self.delete_mark(self._deleted_editmode_mark)
773			self._deleted_editmode_mark = None
774		self._editmode_tags = []
775
776	def get_insert_iter(self):
777		'''Get a C{Gtk.TextIter} for the current cursor position'''
778		return self.get_iter_at_mark(self.get_insert())
779
780	def tmp_cursor(self, iter=None, gravity=GRAVITY_LEFT):
781		'''Get a L{SaveCursorContext} object
782
783		@param iter: a C{Gtk.TextIter} for the new (temporary) cursor
784		position
785		@param gravity: give mark left or right "gravity" compared to new
786		inserted text, default is "left" which means new text goes after the
787		cursor position
788		'''
789		return SaveCursorContext(self, iter, gravity)
790
791	def set_parsetree(self, tree, showing_template=False):
792		'''Load a new L{ParseTree} in the buffer
793
794		This method replaces any content in the buffer with the new
795		parser tree.
796
797		@param tree: a L{ParseTree} object
798		@param showing_template: if C{True} the C{tree} represents a template
799		and not actual page content (yet)
800		'''
801		with self.user_action:
802			self.clear()
803			self.insert_parsetree_at_cursor(tree)
804
805		self.showing_template = showing_template # Set after modifying!
806
807	def insert_parsetree(self, iter, tree, interactive=False):
808		'''Insert a L{ParseTree} in the buffer
809
810		This method inserts a parsetree at a specific place in the
811		buffer.
812
813		@param iter: a C{Gtk.TextIter} for the insert position
814		@param tree: a L{ParseTree} object
815		@param interactive: Boolean which determines how current state
816		in the buffer is handled. If not interactive we break any
817		existing tags and insert the tree, otherwise we insert using the
818		formatting tags that that are present at iter.
819
820		For example when a parsetree is inserted because the user pastes
821		content from the clipboard C{interactive} should be C{True}.
822		'''
823		with self.tmp_cursor(iter):
824			self.insert_parsetree_at_cursor(tree, interactive)
825
826	def append_parsetree(self, tree, interactive=False):
827		'''Append a L{ParseTree} to the buffer
828
829		Like L{insert_parsetree()} but inserts at the end of the current buffer.
830		'''
831		self.insert_parsetree(self.get_end_iter(), tree, interactive)
832
833	def insert_parsetree_at_cursor(self, tree, interactive=False):
834		'''Insert a L{ParseTree} in the buffer
835
836		Like L{insert_parsetree()} but inserts at the current cursor
837		position.
838
839		@param tree: a L{ParseTree} object
840		@param interactive: Boolean which determines how current state
841		in the buffer is handled.
842		'''
843		#print('INSERT AT CURSOR', tree.tostring())
844		tree.resolve_images(self.notebook, self.page)
845
846		# Check tree
847		root = tree._etree.getroot() # HACK - switch to new interface !
848		assert root.tag == 'zim-tree'
849		raw = root.attrib.get('raw')
850		if isinstance(raw, str):
851			raw = (raw != 'False')
852
853		# Check if we are at a bullet or checkbox line
854		iter = self.get_iter_at_mark(self.get_insert())
855		if not raw and iter.starts_line() \
856		and not tree.get_ends_with_newline():
857			bullet = self._get_bullet_at_iter(iter)
858			if bullet:
859				self._iter_forward_past_bullet(iter, bullet)
860				self.place_cursor(iter)
861
862		# Prepare
863		startoffset = iter.get_offset()
864		if not interactive:
865			self._editmode_tags = []
866		tree.decode_urls()
867
868		if self._deleted_editmode_mark is not None:
869			self.delete_mark(self._deleted_editmode_mark)
870			self._deleted_editmode_mark = None
871
872		# Actual insert
873		modified = self.get_modified()
874		try:
875			self.emit('begin-insert-tree', interactive)
876			if root.text:
877				self.insert_at_cursor(root.text)
878			self._insert_element_children(root, raw=raw)
879
880			# Fix partial tree inserts
881			startiter = self.get_iter_at_offset(startoffset)
882			if not startiter.starts_line():
883				self._do_lines_merged(startiter)
884
885			enditer = self.get_iter_at_mark(self.get_insert())
886			if not enditer.ends_line():
887				self._do_lines_merged(enditer)
888
889			# Fix text direction of indent tags
890			for line in range(startiter.get_line(), enditer.get_line() + 1):
891				iter = self.get_iter_at_line(line)
892				tags = list(filter(_is_indent_tag, iter.get_tags()))
893				if tags:
894					dir = self._find_base_dir(line)
895					if dir == 'RTL':
896						bullet = self.get_bullet(line)
897						level = self.get_indent(line)
898						self._set_indent(line, level, bullet, dir=dir)
899					# else pass, LTR is the default
900		except:
901			# Try to recover buffer state before raising
902			self.update_editmode()
903			startiter = self.get_iter_at_offset(startoffset)
904			enditer = self.get_iter_at_mark(self.get_insert())
905			self.delete(startiter, enditer)
906			self.set_modified(modified)
907			self.emit('end-insert-tree')
908			raise
909		else:
910			# Signal the tree that was inserted
911			self.update_editmode()
912			startiter = self.get_iter_at_offset(startoffset)
913			enditer = self.get_iter_at_mark(self.get_insert())
914			self.emit('end-insert-tree')
915
916	def do_begin_insert_tree(self, interactive):
917		self._insert_tree_in_progress = True
918
919	def do_end_insert_tree(self):
920		self._insert_tree_in_progress = False
921		self.emit('textstyle-changed', self.get_textstyles())
922
923	# emitting textstyle-changed is skipped while loading the tree
924
925	def _insert_element_children(self, node, list_level=-1, list_type=None, list_start='0', raw=False, textstyles=[]):
926		# FIXME should load list_level from cursor position
927		#~ list_level = get_indent --- with bullets at indent 0 this is not bullet proof...
928		list_iter = list_start
929
930		def set_indent(level, bullet=None):
931			# Need special set_indent() function here because the normal
932			# function affects the whole line. THis has unwanted side
933			# effects when we e.g. paste a multi-line tree in the
934			# middle of a indented line.
935			# In contrast to the normal set_indent we treat level=None
936			# and level=0 as different cases.
937			self._editmode_tags = list(filter(_is_not_indent_tag, self._editmode_tags))
938			if level is None:
939				return  # Nothing more to do
940
941			iter = self.get_insert_iter()
942			if not iter.starts_line():
943				# Check existing indent - may have bullet type while we have not
944				tags = list(filter(_is_indent_tag, self.iter_get_zim_tags(iter)))
945				if len(tags) > 1:
946					logger.warn('BUG: overlapping indent tags')
947				if tags and int(tags[0].zim_attrib['indent']) == level:
948					self._editmode_tags.append(tags[0])
949					return  # Re-use tag
950
951			tag = self._get_indent_tag(level, bullet)
952				# We don't set the LTR / RTL direction here
953				# instead we update all indent tags after the full
954				# insert is done.
955			self._editmode_tags.append(tag)
956
957		def force_line_start():
958			# Inserts a newline if we are not at the beginning of a line
959			# makes pasting a tree halfway in a line more sane
960			if not raw:
961				iter = self.get_iter_at_mark(self.get_insert())
962				if not iter.starts_line():
963					self.insert_at_cursor('\n')
964
965		for element in iter(node):
966			if element.tag in ('p', 'div'):
967				# No force line start here on purpose
968				if 'indent' in element.attrib:
969					set_indent(int(element.attrib['indent']))
970				else:
971					set_indent(None)
972
973				if element.text:
974					self.insert_at_cursor(element.text)
975
976				self._insert_element_children(element, list_level=list_level, raw=raw, textstyles=textstyles)  # recurs
977
978				set_indent(None)
979			elif element.tag in ('ul', 'ol'):
980				start = element.attrib.get('start')
981				if 'indent' in element.attrib:
982					level = int(element.attrib['indent'])
983				else:
984					level = list_level + 1
985				self._insert_element_children(element, list_level=level, list_type=element.tag, list_start=start, raw=raw,
986											  textstyles=textstyles)  # recurs
987				set_indent(None)
988			elif element.tag == 'li':
989				force_line_start()
990
991				if 'indent' in element.attrib:
992					list_level = int(element.attrib['indent'])
993				elif list_level < 0:
994					list_level = 0 # We skipped the <ul> - raw tree ?
995
996				if list_type == 'ol':
997					bullet = list_iter + '.'
998					list_iter = increase_list_iter(list_iter)
999				elif 'bullet' in element.attrib and element.attrib['bullet'] != '*':
1000					bullet = element.attrib['bullet']
1001				else:
1002					bullet = BULLET # default to '*'
1003
1004				set_indent(list_level, bullet)
1005				self._insert_bullet_at_cursor(bullet, raw=raw)
1006
1007				if element.text:
1008					self.insert_at_cursor(element.text)
1009
1010				self._insert_element_children(element, list_level=list_level, raw=raw, textstyles=textstyles)  # recurs
1011				set_indent(None)
1012
1013				if not raw:
1014					self.insert_at_cursor('\n')
1015
1016			elif element.tag == 'link':
1017				self.set_textstyles(textstyles)  # reset Needed for interactive insert tree after paste
1018				tag = self._create_link_tag('', **element.attrib)
1019				self._editmode_tags = list(filter(_is_not_link_tag, self._editmode_tags)) + [tag]
1020				linkstartpos = self.get_insert_iter().get_offset()
1021				if element.text:
1022					self.insert_at_cursor(element.text)
1023				self._insert_element_children(element, list_level=list_level, raw=raw,
1024											  textstyles=textstyles)  # recurs
1025				linkstart = self.get_iter_at_offset(linkstartpos)
1026				text = linkstart.get_text(self.get_insert_iter())
1027				if element.attrib['href'] and text != element.attrib['href']:
1028					# same logic in _create_link_tag, but need to check text after all child elements inserted
1029					tag.zim_attrib['href'] = element.attrib['href']
1030				else:
1031					tag.zim_attrib['href'] = None
1032				self._editmode_tags.pop()
1033			elif element.tag == 'tag':
1034				self.set_textstyles(textstyles)  # reset Needed for interactive insert tree after paste
1035				self.insert_tag_at_cursor(element.text, **element.attrib)
1036			elif element.tag == 'anchor':
1037				self.set_textstyles(textstyles)
1038				self.insert_anchor_at_cursor(element.attrib['name'])
1039			elif element.tag == 'img':
1040				file = element.attrib['_src_file']
1041				self.insert_image_at_cursor(file, **element.attrib)
1042			elif element.tag == 'pre':
1043				if 'indent' in element.attrib:
1044					set_indent(int(element.attrib['indent']))
1045				self.set_textstyles([element.tag])
1046				if element.text:
1047					self.insert_at_cursor(element.text)
1048				self.set_textstyles(None)
1049				set_indent(None)
1050			elif element.tag == 'table':
1051				if 'indent' in element.attrib:
1052					set_indent(int(element.attrib['indent']))
1053				self.insert_table_element_at_cursor(element)
1054				set_indent(None)
1055			elif element.tag == 'line':
1056				anchor = LineSeparatorAnchor()
1057				self.insert_objectanchor_at_cursor(anchor)
1058
1059			elif element.tag == 'object':
1060				if 'indent' in element.attrib:
1061					set_indent(int(element.attrib['indent']))
1062				self.insert_object_at_cursor(element.attrib, element.text)
1063				set_indent(None)
1064			else:
1065				# Text styles
1066				flushed = False
1067				if element.tag == 'h':
1068					force_line_start()
1069					tag = 'h' + str(element.attrib['level'])
1070					self.set_textstyles([tag])
1071					if element.text:
1072						self.insert_at_cursor(element.text)
1073					flushed = True
1074					self._insert_element_children(element, list_level=list_level, raw=raw,
1075												  textstyles=[tag])  # recurs
1076				elif element.tag in self._static_style_tags:
1077					self.set_textstyles(textstyles + [element.tag])
1078					if element.text:
1079						self.insert_at_cursor(element.text)
1080					flushed = True
1081					self._insert_element_children(element, list_level=list_level, raw=raw,
1082												  textstyles=textstyles + [element.tag])  # recurs
1083				elif element.tag == '_ignore_':
1084					# raw tree from undo can contain these
1085					self._insert_element_children(element, list_level=list_level, raw=raw, textstyles=textstyles)  # recurs
1086				else:
1087					logger.debug("Unknown tag : %s, %s, %s", element.tag,
1088								 element.attrib, element.text)
1089					assert False, 'Unknown tag: %s' % element.tag
1090
1091				if element.text and not flushed:
1092					self.insert_at_cursor(element.text)
1093
1094				self.set_textstyles(textstyles)
1095
1096			if element.tail:
1097				self.insert_at_cursor(element.tail)
1098
1099	#region Links
1100
1101	def insert_link(self, iter, text, href, **attrib):
1102		'''Insert a link into the buffer
1103
1104		@param iter: a C{Gtk.TextIter} for the insert position
1105		@param text: the text for the link as string
1106		@param href: the target (URL, pagename) of the link as string
1107		@param attrib: any other link attributes
1108		'''
1109		with self.tmp_cursor(iter):
1110			self.insert_link_at_cursor(text, href, **attrib)
1111
1112	def insert_link_at_cursor(self, text, href=None, **attrib):
1113		'''Insert a link into the buffer
1114
1115		Like insert_link() but inserts at the current cursor position
1116
1117		@param text: the text for the link as string
1118		@param href: the target (URL, pagename) of the link as string
1119		@param attrib: any other link attributes
1120		'''
1121		if self._deleted_editmode_mark is not None:
1122			self.delete_mark(self._deleted_editmode_mark)
1123			self._deleted_editmode_mark = None
1124
1125		tag = self._create_link_tag(text, href, **attrib)
1126		self._editmode_tags = list(filter(_is_not_link_tag, self._editmode_tags)) + [tag]
1127		self.insert_at_cursor(text)
1128		self._editmode_tags = self._editmode_tags[:-1]
1129
1130	def _create_link_tag(self, text, href, **attrib):
1131		'''Creates an anonymouse TextTag for a link'''
1132		# These are created after __init__, so higher priority for Formatting
1133		# properties than any of the _static_style_tags
1134		if isinstance(href, File):
1135			href = href.uri
1136		assert isinstance(href, str) or href is None
1137
1138		tag = self.create_tag(None, **self.tag_styles['link'])
1139		tag.zim_type = 'link'
1140		tag.zim_tag = 'link'
1141		tag.zim_attrib = attrib
1142		if href == text or not href or href.isspace():
1143			tag.zim_attrib['href'] = None
1144		else:
1145			tag.zim_attrib['href'] = href
1146
1147		prio_tag = self.get_tag_table().lookup('style-' + self._static_tag_before_links)
1148		tag.set_priority(prio_tag.get_priority()+1)
1149
1150		return tag
1151
1152	def get_link_tag(self, iter):
1153		'''Get the C{Gtk.TextTag} for a link at a specific position, if any
1154
1155		@param iter: a C{Gtk.TextIter}
1156		@returns: a C{Gtk.TextTag} if there is a link at C{iter},
1157		C{None} otherwise
1158		'''
1159		# Explicitly left gravity, otherwise position behind the link
1160		# would also be considered part of the link. Position before the
1161		# link is included here.
1162		for tag in sorted(iter.get_tags(), key=lambda i: i.get_priority()):
1163			if hasattr(tag, 'zim_type') and tag.zim_type == 'link':
1164				return tag
1165		else:
1166			return None
1167
1168	def get_link_text(self, iter):
1169		tag = self.get_link_tag(iter)
1170		return self.get_tag_text(iter, tag) if tag else None
1171
1172	def get_link_data(self, iter, raw=False):
1173		'''Get the link attributes for a link at a specific position, if any
1174
1175		@param iter: a C{Gtk.TextIter}
1176		@returns: a dict with link properties if there is a link
1177		at C{iter}, C{None} otherwise
1178		'''
1179		tag = self.get_link_tag(iter)
1180
1181		if tag:
1182			link = tag.zim_attrib.copy()
1183			if link['href'] is None:
1184				if raw:
1185					link['href'] = ''
1186				else:
1187					# Copy text content as href
1188					start, end = self.get_tag_bounds(iter, tag)
1189					link['href'] = start.get_text(end)
1190			return link
1191		else:
1192			return None
1193
1194	#endregion
1195
1196	#region TextTags
1197
1198	def get_tag(self, iter, type):
1199		'''Get the C{Gtk.TextTag} for a zim type at a specific position, if any
1200
1201		@param iter: a C{Gtk.TextIter}
1202		@param type: the zim type to look for ('style', 'link', 'tag', 'indent', 'anchor')
1203		@returns: a C{Gtk.TextTag} if there is a tag at C{iter},
1204		C{None} otherwise
1205		'''
1206		for tag in iter.get_tags():
1207			if hasattr(tag, 'zim_type') and tag.zim_type == type:
1208				return tag
1209		else:
1210			return None
1211
1212	def get_tag_bounds(self, iter, tag):
1213		start = iter.copy()
1214		if not start.begins_tag(tag):
1215			start.backward_to_tag_toggle(tag)
1216		end = iter.copy()
1217		if not end.ends_tag(tag):
1218			end.forward_to_tag_toggle(tag)
1219		return start, end
1220
1221	def get_tag_text(self, iter, tag):
1222		start, end = self.get_tag_bounds(iter, tag)
1223		return start.get_text(end)
1224
1225	#endregion
1226
1227	#region Tags
1228
1229	def insert_tag(self, iter, text, **attrib):
1230		'''Insert a tag into the buffer
1231
1232		Insert a tag in the buffer (not a TextTag, but a tag
1233		like "@foo")
1234
1235		@param iter: a C{Gtk.TextIter} object
1236		@param text: The text for the tag
1237		@param attrib: any other tag attributes
1238		'''
1239		with self.tmp_cursor(iter):
1240			self.insert_tag_at_cursor(text, **attrib)
1241
1242	def insert_tag_at_cursor(self, text, **attrib):
1243		'''Insert a tag into the buffer
1244
1245		Like C{insert_tag()} but inserts at the current cursor position
1246
1247		@param text: The text for the tag
1248		@param attrib: any other tag attributes
1249		'''
1250		if self._deleted_editmode_mark is not None:
1251			self.delete_mark(self._deleted_editmode_mark)
1252			self._deleted_editmode_mark = None
1253
1254		tag = self._create_tag_tag(text, **attrib)
1255		self._editmode_tags = \
1256			[t for t in self._editmode_tags if not _is_non_nesting_tag(t)] + [tag]
1257		self.insert_at_cursor(text)
1258		self._editmode_tags = self._editmode_tags[:-1]
1259
1260	def _create_tag_tag(self, text, **attrib):
1261		'''Creates an anonymous TextTag for a tag'''
1262		# These are created after __init__, so higher priority for Formatting
1263		# properties than any of the _static_style_tags
1264		tag = self.create_tag(None, **self.tag_styles['tag'])
1265		tag.zim_type = 'tag'
1266		tag.zim_tag = 'tag'
1267		tag.zim_attrib = attrib
1268		tag.zim_attrib['name'] = None
1269
1270		prio_tag = self.get_tag_table().lookup('style-' + self._static_tag_after_tags)
1271		tag.set_priority(prio_tag.get_priority())
1272
1273		return tag
1274
1275	def get_tag_tag(self, iter):
1276		'''Get the C{Gtk.TextTag} for a tag at a specific position, if any
1277
1278		@param iter: a C{Gtk.TextIter}
1279		@returns: a C{Gtk.TextTag} if there is a tag at C{iter},
1280		C{None} otherwise
1281		'''
1282		# Explicitly left gravity, otherwise position behind the tag
1283		# would also be considered part of the tag. Position before the
1284		# tag is included here.
1285		for tag in iter.get_tags():
1286			if hasattr(tag, 'zim_type') and tag.zim_type == 'tag':
1287				return tag
1288		else:
1289			return None
1290
1291	def get_tag_data(self, iter):
1292		'''Get the attributes for a tag at a specific position, if any
1293
1294		@param iter: a C{Gtk.TextIter}
1295		@returns: a dict with tag properties if there is a link
1296		at C{iter}, C{None} otherwise
1297		'''
1298		tag = self.get_tag_tag(iter)
1299
1300		if tag:
1301			attrib = tag.zim_attrib.copy()
1302			# Copy text content as name
1303			start = iter.copy()
1304			if not start.begins_tag(tag):
1305				start.backward_to_tag_toggle(tag)
1306			end = iter.copy()
1307			if not end.ends_tag(tag):
1308				end.forward_to_tag_toggle(tag)
1309			attrib['name'] = start.get_text(end).lstrip('@').strip()
1310			return attrib
1311		else:
1312			return None
1313
1314	#endregion
1315
1316	#region Anchors
1317
1318	def insert_anchor(self, iter, name, **attrib):
1319		'''Insert a "link anchor" with id C{name} at C{iter}'''
1320		widget = Gtk.HBox() # Need *some* widget here...
1321		pixbuf = widget.render_icon('zim-pilcrow', self.bullet_icon_size)
1322		pixbuf.zim_type = 'anchor'
1323		pixbuf.zim_attrib = attrib
1324		pixbuf.zim_attrib['name'] = name
1325		self.insert_pixbuf(iter, pixbuf)
1326
1327	def insert_anchor_at_cursor(self, name):
1328		'''Insert a "link anchor" with id C{name}'''
1329		iter = self.get_iter_at_mark(self.get_insert())
1330		self.insert_anchor(iter, name)
1331
1332	def get_anchor_data(self, iter):
1333		pixbuf = iter.get_pixbuf()
1334		if pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'anchor':
1335			return pixbuf.zim_attrib.copy()
1336		else:
1337			return None
1338
1339	def get_anchor_or_object_id(self, iter):
1340		# anchor or image
1341		pixbuf = iter.get_pixbuf()
1342		if pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'anchor':
1343			return pixbuf.zim_attrib.get('name', None)
1344		elif pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'image':
1345			return pixbuf.zim_attrib.get('id', None)
1346
1347		# object?
1348		anchor = iter.get_child_anchor()
1349		if anchor and isinstance(anchor, PluginInsertedObjectAnchor):
1350			object_type = anchor.objecttype
1351			object_model = anchor.objectmodel
1352			attrib, _ = object_type.data_from_model(object_model)
1353			return attrib.get('id', None)
1354
1355	def iter_anchors_for_range(self, start, end):
1356		iter = start.copy()
1357		match = iter.forward_search(PIXBUF_CHR, 0, limit=end)
1358		while match:
1359			iter, mend = match
1360			name = self.get_anchor_or_object_id(iter)
1361			if name:
1362				yield (iter.copy(), name)
1363			match = mend.forward_search(PIXBUF_CHR, 0, limit=end)
1364
1365	def get_anchor_for_location(self, iter):
1366		'''Returns an anchor name that refers to C{iter} or the same line
1367		Uses C{iter} to return id of explicit anchor on the same line closest
1368		to C{iter}. If no explicit anchor is found and C{iter} is within a heading
1369		line, the implicit anchor for the heading is returned.
1370		@param iter: the location to refer to
1371		@returns: an anchor name if any anchor object or heading is found, else C{None}
1372		'''
1373		return self.get_anchor_or_object_id(iter) \
1374			or self._get_close_anchor_or_object_id(iter) \
1375				or self._get_implict_anchor_if_heading(iter)
1376
1377	def _get_close_anchor_or_object_id(self, iter):
1378		line_start = iter.copy() if iter.starts_line() else self.get_iter_at_line(iter.get_line())
1379		line_end = line_start.copy()
1380		line_end.forward_line()
1381		line_offset = iter.get_line_offset()
1382		anchors = [
1383			(abs(myiter.get_line_offset() - line_offset), name)
1384				for myiter, name in self.iter_anchors_for_range(line_start, line_end)
1385		]
1386		if anchors:
1387			anchors.sort()
1388			return anchors[0][1]
1389		else:
1390			return None
1391
1392	def _get_implict_anchor_if_heading(self, iter):
1393		text = self._get_heading_text(iter)
1394		return heading_to_anchor(text) if text else None
1395
1396	def _get_heading_text(self, iter):
1397		line_start = iter.copy() if iter.starts_line() else self.get_iter_at_line(iter.get_line())
1398		is_heading = any(filter(_is_heading_tag, line_start.get_tags()))
1399		if not is_heading:
1400			return None
1401
1402		line_end = line_start.copy()
1403		line_end.forward_line()
1404		return line_start.get_text(line_end)
1405
1406	#endregion
1407
1408	#region Images
1409
1410	def insert_image(self, iter, file, src, **attrib):
1411		'''Insert an image in the buffer
1412
1413		@param iter: a C{Gtk.TextIter} for the insert position
1414		@param file: a L{File} object or a file path or URI
1415		@param src: the file path the show to the user
1416
1417		If the image is e.g. specified in the page source as a relative
1418		link, C{file} should give the absolute path the link resolves
1419		to, while C{src} gives the relative path.
1420
1421		@param attrib: any other image properties
1422		'''
1423		#~ If there is a property 'alt' in attrib we try to set a tooltip.
1424		#~ '''
1425		if isinstance(file, str):
1426			file = File(file)
1427		try:
1428			pixbuf = image_file_load_pixels(file, int(attrib.get('width', -1)), int(attrib.get('height', -1)))
1429		except:
1430			#~ logger.exception('Could not load image: %s', file)
1431			logger.warn('No such image: %s', file)
1432			widget = Gtk.HBox() # Need *some* widget here...
1433			pixbuf = widget.render_icon(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
1434			pixbuf = pixbuf.copy() # need unique instance to set zim_attrib
1435
1436		pixbuf.zim_type = 'image'
1437		pixbuf.zim_attrib = attrib
1438		pixbuf.zim_attrib['src'] = src
1439		pixbuf.zim_attrib['_src_file'] = file
1440		self.insert_pixbuf(iter, pixbuf)
1441
1442	def insert_image_at_cursor(self, file, src, **attrib):
1443		'''Insert an image in the buffer
1444
1445		Like L{insert_image()} but inserts at the current cursor
1446		position
1447
1448		@param file: a L{File} object or a file path or URI
1449		@param src: the file path the show to the user
1450		@param attrib: any other image properties
1451		'''
1452		iter = self.get_iter_at_mark(self.get_insert())
1453		self.insert_image(iter, file, src, **attrib)
1454
1455	def get_image_data(self, iter):
1456		'''Get the attributes for an image at a specific position, if any
1457
1458		@param iter: a C{Gtk.TextIter} object
1459		@returns: a dict with image properties or C{None}
1460		'''
1461		pixbuf = iter.get_pixbuf()
1462		if pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'image':
1463			return pixbuf.zim_attrib.copy()
1464		else:
1465			return None
1466
1467	#endregion
1468
1469	#region Objects
1470
1471	def insert_object_at_cursor(self, attrib, data):
1472		'''Inserts a custom object in the page
1473		@param attrib: dict with object attributes
1474		@param data: string data of object
1475		'''
1476		try:
1477			objecttype = PluginManager.insertedobjects[attrib['type']]
1478		except KeyError:
1479			if attrib['type'].startswith('image+'):
1480				# Fallback for backward compatibility of image generators < zim 0.70
1481				objecttype = UnknownInsertedImageObject()
1482			else:
1483				objecttype = UnknownInsertedObject()
1484
1485		model = objecttype.model_from_data(self.notebook, self.page, attrib, data)
1486		self.insert_object_model_at_cursor(objecttype, model)
1487
1488	def insert_object_model_at_cursor(self, objecttype, model):
1489		from zim.plugins.tableeditor import TableViewObjectType # XXX
1490
1491		model.connect('changed', lambda o: self.set_modified(True))
1492
1493		if isinstance(objecttype, TableViewObjectType):
1494			anchor = TableAnchor(objecttype, model)
1495		else:
1496			anchor = PluginInsertedObjectAnchor(objecttype, model)
1497
1498		self.insert_objectanchor_at_cursor(anchor)
1499
1500	def insert_table_element_at_cursor(self, element):
1501		try:
1502			obj = PluginManager.insertedobjects['table']
1503		except KeyError:
1504			# HACK - if table plugin is not loaded - show table as plain text
1505			tree = ParseTree(element)
1506			lines = get_dumper('wiki').dump(tree)
1507			self.insert_object_at_cursor({'type': 'table'}, ''.join(lines))
1508		else:
1509			model = obj.model_from_element(element.attrib, element)
1510			model.connect('changed', lambda o: self.set_modified(True))
1511
1512			anchor = TableAnchor(obj, model)
1513			self.insert_objectanchor_at_cursor(anchor)
1514
1515	def insert_objectanchor_at_cursor(self, anchor):
1516		iter = self.get_insert_iter()
1517		self.insert_objectanchor(iter, anchor)
1518
1519	def insert_objectanchor(self, iter, anchor):
1520		self.insert_child_anchor(iter, anchor)
1521		self.emit('insert-objectanchor', anchor)
1522
1523	def get_objectanchor_at_cursor(self):
1524		iter = self.get_insert_iter()
1525		return self.get_object_achor(iter)
1526
1527	def get_objectanchor(self, iter):
1528		anchor = iter.get_child_anchor()
1529		if anchor and isinstance(anchor, InsertedObjectAnchor):
1530			return anchor
1531		else:
1532			return None
1533
1534	def list_objectanchors(self):
1535		start, end = self.get_bounds()
1536		match = start.forward_search(PIXBUF_CHR, 0)
1537		while match:
1538			start, end = match
1539			anchor = start.get_child_anchor()
1540			if anchor and isinstance(anchor, InsertedObjectAnchor):
1541				yield anchor
1542			match = end.forward_search(PIXBUF_CHR, 0)
1543
1544	#endregion
1545
1546	#region Bullets
1547
1548	def set_bullet(self, line, bullet, indent=None):
1549		'''Sets the bullet type for a line
1550
1551		Replaces any bullet that may already be present on the line.
1552		Set bullet C{None} to remove any bullet at this line.
1553
1554		@param line: the line number
1555		@param bullet: the bullet type, one of::
1556			BULLET
1557			UNCHECKED_BOX
1558			CHECKED_BOX
1559			XCHECKED_BOX
1560			MIGRATED_BOX
1561			TRANSMIGRATED_BOX
1562			NUMBER_BULLET
1563			None
1564		or a numbered bullet, like C{"1."}
1565		@param indent: optional indent to set after inserting the bullet,
1566		but before renumbering
1567		'''
1568		if bullet == NUMBER_BULLET:
1569			indent = self.get_indent(line)
1570			_, prev = self._search_bullet(line, indent, -1)
1571			if prev and is_numbered_bullet_re.match(prev):
1572				bullet = increase_list_bullet(prev)
1573			else:
1574				bullet = '1.'
1575
1576		with self.user_action:
1577			self._replace_bullet(line, bullet)
1578			if indent is not None:
1579				self.set_indent(line, indent)
1580			if bullet and is_numbered_bullet_re.match(bullet):
1581				self.renumber_list(line)
1582
1583	def _replace_bullet(self, line, bullet):
1584		indent = self.get_indent(line)
1585		with self.tmp_cursor(gravity=GRAVITY_RIGHT):
1586			iter = self.get_iter_at_line(line)
1587			bound = iter.copy()
1588			self.iter_forward_past_bullet(bound)
1589			self.delete(iter, bound)
1590			# Will trigger do_delete_range, which will update indent tag
1591
1592			if not bullet is None:
1593				iter = self.get_iter_at_line(line)
1594				self.place_cursor(iter) # update editmode
1595
1596				insert = self.get_insert_iter()
1597				assert insert.starts_line(), 'BUG: bullet not at line start'
1598
1599				# Turning into list item removes heading
1600				if not insert.ends_line():
1601					end = insert.copy()
1602					end.forward_to_line_end()
1603					self.smart_remove_tags(_is_heading_tag, insert, end)
1604
1605				# TODO: convert 'pre' to 'code' ?
1606
1607				self._insert_bullet_at_cursor(bullet)
1608
1609			#~ self.update_indent_tag(line, bullet)
1610			self._set_indent(line, indent, bullet)
1611
1612	def _insert_bullet_at_cursor(self, bullet, raw=False):
1613		'''Insert a bullet plus a space at the cursor position.
1614		If 'raw' is True the space will be omitted and the check that
1615		cursor position must be at the start of a line will not be
1616		enforced.
1617
1618		External interface should use set_bullet(line, bullet)
1619		instead of calling this method directly.
1620		'''
1621		assert bullet in BULLETS or is_numbered_bullet_re.match(bullet), 'Bullet: >>%s<<' % bullet
1622		if self._deleted_editmode_mark is not None:
1623			self.delete_mark(self._deleted_editmode_mark)
1624			self._deleted_editmode_mark = None
1625
1626		orig_editmode_tags = self._editmode_tags
1627		if not raw:
1628			insert = self.get_insert_iter()
1629			assert insert.starts_line(), 'BUG: bullet not at line start'
1630
1631			# Temporary clear non indent tags during insert
1632			self._editmode_tags = list(filter(_is_indent_tag, self._editmode_tags))
1633
1634			if not self._editmode_tags:
1635				# Without indent get_parsetree will not recognize
1636				# the icon as a bullet item. This will mess up
1637				# undo stack. If 'raw' we assume indent tag is set
1638				# already.
1639				dir = self._find_base_dir(insert.get_line())
1640				tag = self._get_indent_tag(0, bullet, dir=dir)
1641				self._editmode_tags.append(tag)
1642
1643		with self.user_action:
1644			if bullet == BULLET:
1645				if raw:
1646					self.insert_at_cursor('\u2022')
1647				else:
1648					self.insert_at_cursor('\u2022 ')
1649			elif bullet in bullet_types:
1650				# Insert icon
1651				stock = bullet_types[bullet]
1652				widget = Gtk.HBox() # Need *some* widget here...
1653				pixbuf = widget.render_icon(stock, self.bullet_icon_size)
1654				if pixbuf is None:
1655					logger.warn('Could not find icon: %s', stock)
1656					pixbuf = widget.render_icon(Gtk.STOCK_MISSING_IMAGE, self.bullet_icon_size)
1657				pixbuf.zim_type = 'icon'
1658				pixbuf.zim_attrib = {'stock': stock}
1659				self.insert_pixbuf(self.get_insert_iter(), pixbuf)
1660
1661				if not raw:
1662					self.insert_at_cursor(' ')
1663			else:
1664				# Numbered
1665				if raw:
1666					self.insert_at_cursor(bullet)
1667				else:
1668					self.insert_at_cursor(bullet + ' ')
1669
1670		self._editmode_tags = orig_editmode_tags
1671
1672	def renumber_list(self, line):
1673		'''Renumber list from this line downward
1674
1675		This method is called when the user just typed a new bullet or
1676		when we suspect the user deleted some line(s) that are part
1677		of a numbered list. Typically there is no need to call this
1678		method directly, but it is exposed for testing.
1679
1680		It implements the following rules:
1681
1682		- If there is a numered list item above on the same level, number down
1683		  from there
1684		- Else if the line itself has a numbered bullet (and thus is top of a
1685		  numbered list) number down
1686		- Stop renumbering at the end of the list, or when a non-numeric bullet
1687		  is encountered on the same list level
1688
1689		@param line: line number to start updating
1690		'''
1691		indent = self.get_indent(line)
1692		bullet = self.get_bullet(line)
1693		if bullet is None or not is_numbered_bullet_re.match(bullet):
1694			return
1695
1696		_, prev = self._search_bullet(line, indent, -1)
1697		if prev and is_numbered_bullet_re.match(prev):
1698			newbullet = increase_list_bullet(prev)
1699		else:
1700			newbullet = bullet
1701
1702		self._renumber_list(line, indent, newbullet)
1703
1704	def renumber_list_after_indent(self, line, old_indent):
1705		'''Like L{renumber_list()}, but more complex rules because indent
1706		change has different heuristics.
1707
1708		It implements the following rules:
1709
1710		- If the bullet type is a checkbox, never change it (else information is
1711		  lost on the checkbox state)
1712		- Check for bullet style of the item above on the same level, else
1713		  the item below on the same level
1714		- If the bullet became part of a numbered list, renumber that list
1715		  either from the item above, or copying starting number from below
1716		- If the bullet became part of a bullet or checkbox list, change it to
1717		  match the list
1718		- If there are no other bullets on the same level and the bullet was
1719		  a numbered bullet, switch bullet style (number vs letter) and reset
1720		  the count
1721		- Else keep the bullet as it was
1722
1723		Also, if the bullet was a numbered bullet, also renumber the
1724		list level where it came from.
1725		'''
1726		indent = self.get_indent(line)
1727		bullet = self.get_bullet(line)
1728		if bullet is None or bullet in CHECKBOXES:
1729			return
1730
1731		_, prev = self._search_bullet(line, indent, -1)
1732		if prev:
1733			newbullet = increase_list_bullet(prev) or prev
1734		else:
1735			_, newbullet = self._search_bullet(line, indent, +1)
1736			if not newbullet:
1737				if not is_numbered_bullet_re.match(bullet):
1738					return
1739				elif bullet.rstrip('.') in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz':
1740					newbullet = '1.' # switch e.g. "a." -> "1."
1741				else:
1742					newbullet = 'a.' # switch "1." -> "a."
1743
1744		if is_numbered_bullet_re.match(newbullet):
1745			self._renumber_list(line, indent, newbullet)
1746		else:
1747			if newbullet in CHECKBOXES:
1748				newbullet = UNCHECKED_BOX
1749			self._replace_bullet(line, newbullet)
1750
1751		if is_numbered_bullet_re.match(bullet):
1752			# Also update old list level
1753			newline, newbullet = self._search_bullet(line+1, old_indent, -1)
1754			if newbullet and is_numbered_bullet_re.match(newbullet):
1755				self._renumber_list(newline, old_indent, newbullet)
1756			else:
1757				# If no item above on old level, was top or middle on old level,
1758				# so reset count
1759				newline, newbullet = self._search_bullet(line, old_indent, +1)
1760				if newbullet and is_numbered_bullet_re.match(newbullet):
1761					if newbullet.rstrip('.') in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz':
1762						self._renumber_list(newline, old_indent, 'a.')
1763					else:
1764						self._renumber_list(newline, old_indent, '1.')
1765
1766	def _search_bullet(self, line, indent, step):
1767		# Return bullet for previous/next bullet item at same level
1768		while True:
1769			line += step
1770			try:
1771				mybullet = self.get_bullet(line)
1772				myindent = self.get_indent(line)
1773			except ValueError:
1774				return None, None
1775
1776			if not mybullet or myindent < indent:
1777				return None, None
1778			elif myindent == indent:
1779				return line, mybullet
1780			# else mybullet and myindent > indent
1781
1782	def _renumber_list(self, line, indent, newbullet):
1783		# Actually renumber for a given line downward
1784		assert is_numbered_bullet_re.match(newbullet)
1785
1786		while True:
1787			try:
1788				mybullet = self.get_bullet(line)
1789				myindent = self.get_indent(line)
1790			except ValueError:
1791				break
1792
1793			if not mybullet or myindent < indent:
1794				break
1795			elif myindent == indent:
1796				if not is_numbered_bullet_re.match(mybullet):
1797					break # Do not replace other bullet types
1798				elif mybullet != newbullet:
1799					self._replace_bullet(line, newbullet)
1800				newbullet = increase_list_bullet(newbullet)
1801			else:
1802				pass # mybullet and myindent > indent
1803
1804			line += 1
1805
1806	#endregion
1807
1808	#region Text Styles
1809
1810	def set_textstyles(self, names):
1811		'''Sets the current text format style.
1812
1813		@param names: the name of the format style
1814
1815		This style will be applied to text inserted at the cursor.
1816		Use C{set_textstyles(None)} to reset to normal text.
1817		'''
1818		if self._deleted_editmode_mark is not None:
1819			self.delete_mark(self._deleted_editmode_mark)
1820			self._deleted_editmode_mark = None
1821
1822		self._editmode_tags = list(filter(_is_not_style_tag, self._editmode_tags))  # remove all text styles first
1823
1824		if names:
1825			for name in names:
1826				tag = self.get_tag_table().lookup('style-' + name)
1827				if _is_heading_tag(tag):
1828					self._editmode_tags = \
1829						list(filter(_is_not_indent_tag, self._editmode_tags))
1830				self._editmode_tags.append(tag)
1831
1832		if not self._insert_tree_in_progress:
1833			self.emit('textstyle-changed', names)
1834
1835	def get_textstyles(self):
1836		'''Get the name of the formatting style that will be applied
1837		to newly inserted text
1838
1839		This style may change as soon as the cursor position changes,
1840		so only relevant for current cursor position.
1841		'''
1842		tags = list(filter(_is_style_tag, self._editmode_tags))
1843		if tags:
1844			# X not anymore assert len(tags) == 1, 'BUG: can not have multiple text styles'
1845			return [tag.get_property('name')[6:] for tag in tags]  # len('style-') == 6
1846		else:
1847			return []
1848
1849	#endregion
1850
1851	def update_editmode(self):
1852		'''Updates the text style and indenting applied to newly indented
1853		text based on the current cursor position
1854
1855		This method is triggered automatically when the cursor is moved,
1856		but there are some cases where you may need to call it manually
1857		to force a consistent state.
1858		'''
1859		bounds = self.get_selection_bounds()
1860		if bounds:
1861			# For selection we set editmode based on left hand side and looking forward
1862			# so counting tags that apply to start of selection
1863			tags = list(filter(_is_zim_tag, bounds[0].get_tags()))
1864		else:
1865			# Otherwise base editmode on cursor position (looking backward)
1866			iter = self.get_insert_iter()
1867			tags = self.iter_get_zim_tags(iter)
1868
1869		tags = list(tags)
1870		if not tags == self._editmode_tags:
1871			#print('> %r' % [(t.zim_type, t.get_property('name')) for t in tags])
1872			self._editmode_tags = tags
1873			self.emit('textstyle-changed', [tag.get_property('name')[6:] for tag in tags if tag.zim_type == 'style'])
1874
1875	def iter_get_zim_tags(self, iter):
1876		'''Replacement for C{Gtk.TextIter.get_tags()} which returns
1877		zim specific tags
1878
1879		In contrast to C{Gtk.TextIter.get_tags()} this method assumes
1880		"left gravity" for TextTags. This means that it returns TextTags
1881		ending to the left of the iter position but not TextTags starting
1882		to the right.
1883
1884		For TextTags that should be applied per line (like 'indent', 'h',
1885		'pre') some additional logic is used to keep them consistent.
1886		So at the start of the line, we do copy TextTags starting to
1887		the right and not inadvertently copy formatting from the
1888		previous line which ends on the left.
1889
1890		This method is for example used by L{update_editmode()} to
1891		determine which TextTags should be applied to newly inserted
1892		text at at a specific location.
1893
1894		@param iter: a C{Gtk.TextIter}
1895		@returns: a list of C{Gtk.TextTag}s (sorted by priority)
1896		'''
1897		# Current logic works without additional indent set in
1898		# do_end_of_line due to the fact that the "\n" also caries
1899		# formatting. So putting a new \n at the end of e.g. an indented
1900		# line will result in two indent formatted \n characters.
1901		# The start of the new line is in between and has continuous
1902		# indent formatting.
1903		start_tags = list(filter(_is_zim_tag, iter.get_toggled_tags(True)))
1904		tags = list(filter(_is_zim_tag, iter.get_tags()))
1905		for tag in start_tags:
1906			if tag in tags:
1907				tags.remove(tag)
1908		end_tags = list(filter(_is_zim_tag, iter.get_toggled_tags(False)))
1909		# So now we have 3 separate sets with tags ending here,
1910		# starting here and being continuous here. Result will be
1911		# continuous tags and ending tags but logic for line based
1912		# tags can mix in tags starting here and filter out
1913		# tags ending here.
1914
1915		if iter.starts_line():
1916			tags += list(filter(_is_line_based_tag, start_tags))
1917			tags += list(filter(_is_not_line_based_tag, end_tags))
1918		elif iter.ends_line():
1919			# Force only use tags from the left in order to prevent tag
1920			# from next line "spilling over" (should not happen, since
1921			# \n after end of line is still formatted with same line
1922			# based tag as rest of line, but handled anyway to be
1923			# robust to edge cases)
1924			tags += end_tags
1925		else:
1926			# Take any tag from left or right, with left taking precendence
1927			#
1928			# HACK: We assume line based tags are mutually exclusive
1929			# if this assumption breaks down need to check by tag type
1930			tags += end_tags
1931			if not list(filter(_is_line_based_tag, tags)):
1932				tags += list(filter(_is_line_based_tag, start_tags))
1933
1934		tags.sort(key=lambda tag: tag.get_priority())
1935		return tags
1936
1937	def toggle_textstyle(self, name):
1938		'''Toggle the current textstyle
1939
1940		If there is a selection toggle the text style of the selection,
1941		otherwise toggle the text style for newly inserted text.
1942
1943		This method is mainly to change the behavior for
1944		interactive editing. E.g. it is called indirectly when the
1945		user clicks one of the formatting buttons in the EditBar.
1946
1947		For selections we remove the format if the whole range has the
1948		format already. If some part of the range does not have the
1949		format we apply the format to the whole tange. This makes the
1950		behavior of the format buttons consistent if a single tag
1951		applies to any range.
1952
1953		@param name: the format style name
1954		'''
1955		if not self.get_has_selection():
1956			styles = self.get_textstyles()
1957			if 'pre' in styles and name != 'pre':
1958				pass # do not allow styles within verbatim block
1959			elif name in styles:
1960				styles.remove(name)
1961				self.set_textstyles(styles)
1962			else:
1963				self.set_textstyles(styles + [name])
1964		else:
1965			with self.user_action:
1966				start, end = self.get_selection_bounds()
1967				if name == 'code' and start.starts_line() \
1968					and end.get_line() != start.get_line():
1969						name = 'pre'
1970						tag = self.get_tag_table().lookup('style-' + name)
1971						if not self.whole_range_has_tag(tag, start, end):
1972							start, end = self._fix_pre_selection(start, end)
1973
1974				tag = self.get_tag_table().lookup('style-' + name)
1975				had_tag = self.whole_range_has_tag(tag, start, end)
1976				pre_tag = self.get_tag_table().lookup('style-pre')
1977
1978				if tag.zim_tag == "h":
1979					self.smart_remove_tags(_is_heading_tag, start, end)
1980					for line in range(start.get_line(), end.get_line()+1):
1981						self._remove_indent(line)
1982				elif tag.zim_tag in ('pre', 'code'):
1983					self.smart_remove_tags(_is_non_nesting_tag, start, end)
1984					if tag.zim_tag == 'pre':
1985						self.smart_remove_tags(_is_link_tag, start, end)
1986						self.smart_remove_tags(_is_style_tag, start, end)
1987				elif self.range_has_tag(pre_tag, start, end):
1988					return # do not allow formatting withing verbatim block
1989
1990				if had_tag:
1991					self.remove_tag(tag, start, end)
1992				else:
1993					self.apply_tag(tag, start, end)
1994				self.set_modified(True)
1995
1996			self.update_editmode()
1997
1998	def _fix_pre_selection(self, start, end):
1999		# This method converts indent back into TAB before a region is
2000		# formatted as "pre"
2001		start_mark = self.create_mark(None, start, True)
2002		end_mark = self.create_mark(None, end, True)
2003
2004		lines = range(*sorted([start.get_line(), end.get_line()+1]))
2005		min_indent = min(self.get_indent(line) for line in lines)
2006
2007		for line in lines:
2008			indent = self.get_indent(line)
2009			if indent > min_indent:
2010				self.set_indent(line, min_indent)
2011				n_tabs = indent - min_indent
2012				iter = self.get_iter_at_line(line)
2013				self.insert(iter, "\t"*n_tabs)
2014
2015		start = self.get_iter_at_mark(start_mark)
2016		end = self.get_iter_at_mark(end_mark)
2017		self.delete_mark(start_mark)
2018		self.delete_mark(end_mark)
2019		return start, end
2020
2021	def whole_range_has_tag(self, tag, start, end):
2022		'''Check if a certain TextTag is applied to the whole range or
2023		not
2024
2025		@param tag: a C{Gtk.TextTag}
2026		@param start: a C{Gtk.TextIter}
2027		@param end: a C{Gtk.TextIter}
2028		'''
2029		if tag in start.get_tags() \
2030				and tag in self.iter_get_zim_tags(end):
2031			iter = start.copy()
2032			if iter.forward_to_tag_toggle(tag):
2033				return iter.compare(end) >= 0
2034			else:
2035				return True
2036		else:
2037			return False
2038
2039	def range_has_tag(self, tag, start, end):
2040		'''Check if a certain TextTag appears anywhere in a range
2041
2042		@param tag: a C{Gtk.TextTag}
2043		@param start: a C{Gtk.TextIter}
2044		@param end: a C{Gtk.TextIter}
2045		'''
2046		# test right gravity for start iter, but left gravity for end iter
2047		if tag in start.get_tags() \
2048				or tag in self.iter_get_zim_tags(end):
2049			return True
2050		else:
2051			iter = start.copy()
2052			if iter.forward_to_tag_toggle(tag):
2053				return iter.compare(end) < 0
2054			else:
2055				return False
2056
2057	def range_has_tags(self, func, start, end):
2058		'''Like L{range_has_tag()} but uses a function to check for
2059		multiple tags. The function gets called for each TextTag in the
2060		range and the method returns as soon as the function returns
2061		C{True} for any tag. There are a number of lambda functions
2062		defined in the module to test categories of TextTags.
2063
2064		@param func: a function that is called as: C{func(tag)} for each
2065		TextTag in the range
2066		@param start: a C{Gtk.TextIter}
2067		@param end: a C{Gtk.TextIter}
2068		'''
2069		# test right gravity for start iter, but left gravity for end iter
2070		if any(filter(func, start.get_tags())) \
2071				or any(filter(func, self.iter_get_zim_tags(end))):
2072			return True
2073		else:
2074			iter = start.copy()
2075			iter.forward_to_tag_toggle(None)
2076			while iter.compare(end) == -1:
2077				if any(filter(func, iter.get_tags())):
2078					return True
2079
2080				if not iter.forward_to_tag_toggle(None):
2081					return False
2082
2083			return False
2084
2085	def remove_textstyle_tags(self, start, end):
2086		'''Removes all format style TexTags from a range
2087
2088		@param start: a C{Gtk.TextIter}
2089		@param end: a C{Gtk.TextIter}
2090		'''
2091		# Also remove links until we support links nested in tags
2092		self.smart_remove_tags(_is_style_tag, start, end)
2093		self.smart_remove_tags(_is_link_tag, start, end)
2094		self.smart_remove_tags(_is_tag_tag, start, end)
2095		self.update_editmode()
2096
2097	def smart_remove_tags(self, func, start, end):
2098		'''This method removes tags over a range based on a function
2099
2100		So L{range_has_tags()} for a details on such a test function.
2101
2102		Please use this method instead of C{remove_tag()} when you
2103		are not sure if specific tags are present in the first place.
2104		Calling C{remove_tag()} will emit signals which make the
2105		L{UndoStackManager} assume the tag was there. If this was not
2106		the case the undo stack gets messed up.
2107		'''
2108		with self.user_action:
2109			iter = start.copy()
2110			while iter.compare(end) == -1:
2111				for tag in filter(func, iter.get_tags()):
2112					bound = iter.copy()
2113					bound.forward_to_tag_toggle(tag)
2114					if not bound.compare(end) == -1:
2115						bound = end.copy()
2116					self.remove_tag(tag, iter, bound)
2117					self.set_modified(True)
2118
2119				if not iter.forward_to_tag_toggle(None):
2120					break
2121
2122	def get_indent_at_cursor(self):
2123		'''Get the indent level at the cursor
2124
2125		@returns: a number for the indenting level
2126		'''
2127		iter = self.get_iter_at_mark(self.get_insert())
2128		return self.get_indent(iter.get_line())
2129
2130	def get_indent(self, line):
2131		'''Get the indent level for a specific line
2132
2133		@param line: the line number
2134		@returns: a number for the indenting level
2135		'''
2136		iter = self.get_iter_at_line(line)
2137		tags = list(filter(_is_indent_tag, iter.get_tags()))
2138		if tags:
2139			if len(tags) > 1:
2140				logger.warn('BUG: overlapping indent tags')
2141			return int(tags[0].zim_attrib['indent'])
2142		else:
2143			return 0
2144
2145	def _get_indent_tag(self, level, bullet=None, dir='LTR'):
2146		if dir is None:
2147			dir = 'LTR'  # Assume western default direction - FIXME need system default
2148		name = 'indent-%s-%i' % (dir, level)
2149		if bullet:
2150			name += '-' + bullet
2151		tag = self.get_tag_table().lookup(name)
2152		if tag is None:
2153			if bullet:
2154				if bullet == BULLET:
2155					stylename = 'bullet-list'
2156				elif bullet == CHECKED_BOX:
2157					stylename = 'checked-checkbox'
2158				elif bullet == UNCHECKED_BOX:
2159					stylename = 'unchecked-checkbox'
2160				elif bullet == XCHECKED_BOX:
2161					stylename = 'xchecked-checkbox'
2162				elif bullet == MIGRATED_BOX:
2163					stylename = 'migrated-checkbox'
2164				elif bullet == TRANSMIGRATED_BOX:
2165					stylename = 'transmigrated-checkbox'
2166				elif is_numbered_bullet_re.match(bullet):
2167					stylename = 'numbered-list'
2168				else:
2169					raise AssertionError('BUG: Unknown bullet type')
2170				margin = 12 + self.pixels_indent * level # offset from left side for all lines
2171				indent = -12 # offset for first line (bullet)
2172				if dir == 'LTR':
2173					tag = self.create_tag(name,
2174						left_margin=margin, indent=indent,
2175						**self.tag_styles[stylename])
2176				else: # RTL
2177					tag = self.create_tag(name,
2178						right_margin=margin, indent=indent,
2179						**self.tag_styles[stylename])
2180			else:
2181				margin = 12 + self.pixels_indent * level
2182				# Note: I would think the + 12 is not needed here, but
2183				# the effect in the view is different than expected,
2184				# putting text all the way to the left against the
2185				# window border
2186				if dir == 'LTR':
2187					tag = self.create_tag(name,
2188						left_margin=margin,
2189						**self.tag_styles['indent'])
2190				else: # RTL
2191					tag = self.create_tag(name,
2192						right_margin=margin,
2193						**self.tag_styles['indent'])
2194
2195			tag.zim_type = 'indent'
2196			tag.zim_tag = 'indent'
2197			tag.zim_attrib = {'indent': level, '_bullet': (bullet is not None)}
2198
2199			# Set the prioriy below any _static_style_tags
2200			tag.set_priority(0)
2201
2202		return tag
2203
2204	def _find_base_dir(self, line):
2205		# Look for basedir of current line, else previous line
2206		# till start of paragraph
2207		# FIXME: anyway to actually find out what the TextView will render ??
2208		while line >= 0:
2209			start, end = self.get_line_bounds(line)
2210			text = start.get_slice(start)
2211			if not text or text.isspace():
2212				break
2213
2214			dir = Pango.find_base_dir(text, len(text))
2215			if dir == Pango.DIRECTION_LTR:
2216				return 'LTR'
2217			elif dir == Pango.DIRECTION_RTL:
2218				return 'RTL'
2219			else:
2220				line -= 1
2221		else:
2222			return 'LTR' # default
2223
2224	def set_indent(self, line, level, interactive=False):
2225		'''Set the indenting for a specific line.
2226
2227		May also trigger renumbering for numbered lists.
2228
2229		@param line: the line number
2230		@param level: the indenting level as a number, C{0} for no
2231		indenting, C{1} for the equivalent of 1 tab, etc.
2232		@param interactive: hint if indenting is result of user
2233		interaction, or automatic action
2234
2235		If interactive, the line will be forced to end with a newline.
2236		Reason is that if the last line of the buffer is empty and
2237		does not end with a newline, the indenting will not be visible,
2238		giving the impression that it failed.
2239
2240		@returns: C{True} for success (e.g. indenting a heading is not
2241		allowed, if you try it will fail and return C{False} here)
2242		'''
2243		level = level or 0
2244
2245		if interactive:
2246			# Without content effect of indenting is not visible
2247			# end-of-line gives content to empty line, but last line
2248			# may not have end-of-line.
2249			start, end = self.get_line_bounds(line)
2250			bufferend = self.get_end_iter()
2251			if start.equal(end) or end.equal(bufferend):
2252				with self.tmp_cursor():
2253					self.insert(end, '\n')
2254					start, end = self.get_line_bounds(line)
2255
2256		bullet = self.get_bullet(line)
2257		ok = self._set_indent(line, level, bullet)
2258
2259		if ok:
2260			self.set_modified(True)
2261		return ok
2262
2263	def update_indent_tag(self, line, bullet):
2264		'''Update the indent TextTag for a given line
2265
2266		The TextTags used for indenting differ between normal indented
2267		paragraphs and indented items in a bullet list. The reason for
2268		this is that the line wrap behavior of list items should be
2269		slightly different to align wrapped text with the bullet.
2270
2271		This method does not change the indent level for a specific line,
2272		but it makes sure the correct TextTag is applied. Typically
2273		called e.g. after inserting or deleting a bullet.
2274
2275		@param line: the line number
2276		@param bullet: the bullet type for this line, or C{None}
2277		'''
2278		level = self.get_indent(line)
2279		self._set_indent(line, level, bullet)
2280
2281	def _set_indent(self, line, level, bullet, dir=None):
2282		# Common code between set_indent() and update_indent_tag()
2283		self._remove_indent(line)
2284
2285		start, end = self.get_line_bounds(line)
2286		if list(filter(_is_heading_tag, start.get_tags())):
2287			return level == 0 # False if you try to indent a header
2288
2289		if level > 0 or bullet is not None:
2290			# For bullets there is a 0-level tag, otherwise 0 means None
2291			if dir is None:
2292				dir = self._find_base_dir(line)
2293			tag = self._get_indent_tag(level, bullet, dir=dir)
2294			self.apply_tag(tag, start, end)
2295
2296		self.update_editmode() # also updates indent tag
2297		return True
2298
2299	def _remove_indent(self, line):
2300		start, end = self.get_line_bounds(line)
2301		for tag in filter(_is_indent_tag, start.get_tags()):
2302			self.remove_tag(tag, start, end)
2303
2304	def indent(self, line, interactive=False):
2305		'''Increase the indent for a given line
2306
2307		Can be used as function for L{foreach_line_in_selection()}.
2308
2309		@param line: the line number
2310		@param interactive: hint if indenting is result of user
2311		interaction, or automatic action
2312
2313		@returns: C{True} if successful
2314		'''
2315		level = self.get_indent(line)
2316		return self.set_indent(line, level + 1, interactive)
2317
2318	def unindent(self, line, interactive=False):
2319		'''Decrease the indent level for a given line
2320
2321		Can be used as function for L{foreach_line_in_selection()}.
2322
2323		@param line: the line number
2324		@param interactive: hint if indenting is result of user
2325		interaction, or automatic action
2326
2327		@returns: C{True} if successful
2328		'''
2329		level = self.get_indent(line)
2330		return self.set_indent(line, level - 1, interactive)
2331
2332	def foreach_line_in_selection(self, func, *args, **kwarg):
2333		'''Convenience function to call a function for each line that
2334		is currently selected
2335
2336		@param func: function which will be called as::
2337
2338			func(line, *args, **kwargs)
2339
2340		where C{line} is the line number
2341		@param args: additional argument for C{func}
2342		@param kwarg: additional keyword argument for C{func}
2343
2344		@returns: C{False} if there is no selection, C{True} otherwise
2345		'''
2346		bounds = self.get_selection_bounds()
2347		if bounds:
2348			start, end = bounds
2349			if end.starts_line():
2350				# exclude last line if selection ends at newline
2351				# because line is not visually part of selection
2352				end.backward_char()
2353			for line in range(start.get_line(), end.get_line() + 1):
2354				func(line, *args, **kwarg)
2355			return True
2356		else:
2357			return False
2358
2359	def do_mark_set(self, iter, mark):
2360		Gtk.TextBuffer.do_mark_set(self, iter, mark)
2361		if mark.get_name() in ('insert', 'selection_bound'):
2362			self.update_editmode()
2363
2364	def do_insert_text(self, iter, string, length):
2365		'''Signal handler for insert-text signal'''
2366		#print("INSERT %r %d" % (string, length))
2367
2368		if self._deleted_editmode_mark is not None:
2369			# Use mark if we are the same postion, clear it anyway
2370			markiter = self.get_iter_at_mark(self._deleted_editmode_mark)
2371			if iter.equal(markiter):
2372				self._editmode_tags = self._deleted_editmode_mark.editmode_tags
2373			self.delete_mark(self._deleted_editmode_mark)
2374			self._deleted_editmode_mark = None
2375
2376		def end_or_protect_tags(string, length):
2377			tags = list(filter(_is_tag_tag, self._editmode_tags))
2378			if tags:
2379				if iter.ends_tag(tags[0]):
2380					# End tags if end-of-word char is typed at end of a tag
2381					# without this you can not insert text behind a tag e.g. at the end of a line
2382					self._editmode_tags = list(filter(_is_not_tag_tag, self._editmode_tags))
2383				else:
2384					# Forbid breaking a tag
2385					return '', 0
2386				# TODO this should go into the TextView, not here
2387				# Now it goes OK only because we only check single char inserts, but would break
2388				# for multi char inserts from the view - fixing that here breaks insert parsetree
2389			return string, length
2390
2391		# Check if we are at a bullet or checkbox line
2392		# if so insert behind the bullet when you type at start of line
2393		# FIXME FIXME FIXME - break undo - instead disallow this home position ?
2394		if not self._insert_tree_in_progress and iter.starts_line() \
2395		and not string.endswith('\n'):
2396			bullet = self._get_bullet_at_iter(iter)
2397			if bullet:
2398				self._iter_forward_past_bullet(iter, bullet)
2399				self.place_cursor(iter)
2400
2401		# Check current formatting
2402		if string == '\n': # CHARS_END_OF_LINE
2403			# Break tags that are not allowed to span over multiple lines
2404			self._editmode_tags = [tag for tag in self._editmode_tags if _is_pre_tag(tag) or _is_not_style_tag(tag)]
2405			self._editmode_tags = list(filter(_is_not_link_tag, self._editmode_tags))
2406			self.emit('textstyle-changed', None)
2407			# TODO make this more robust for multiline inserts
2408
2409			string, length = end_or_protect_tags(string, length)
2410
2411		elif not self._insert_tree_in_progress and string in CHARS_END_OF_WORD:
2412			# Break links if end-of-word char is typed at end of a link
2413			# without this you can not insert text behind a link e.g. at the end of a line
2414			links = list(filter(_is_link_tag, self._editmode_tags))
2415			if links and iter.ends_tag(links[0]):
2416				self._editmode_tags = list(filter(_is_not_link_tag, self._editmode_tags))
2417				# TODO this should go into the TextView, not here
2418				# Now it goes OK only because we only check single char inserts, but would break
2419				# for multi char inserts from the view - fixing that here breaks insert parsetree
2420
2421			string, length = end_or_protect_tags(string, length)
2422
2423		# Call parent for the actual insert
2424		Gtk.TextBuffer.do_insert_text(self, iter, string, length)
2425
2426		# And finally apply current text style
2427		# Note: looks like parent call modified the position of the TextIter object
2428		# since it is still valid and now matched the end of the inserted string
2429		length = len(string)
2430			# default function argument gives byte length :S
2431		start = iter.copy()
2432		start.backward_chars(length)
2433		self.remove_all_tags(start, iter)
2434		for tag in self._editmode_tags:
2435			self.apply_tag(tag, start, iter)
2436
2437	def insert_child_anchor(self, iter, anchor):
2438		# Make sure we always apply the correct tags when inserting an object
2439		if iter.equal(self.get_iter_at_mark(self.get_insert())):
2440			Gtk.TextBuffer.insert_child_anchor(self, iter, anchor)
2441		else:
2442			with self.tmp_cursor(iter):
2443				Gtk.TextBuffer.insert_child_anchor(self, iter, anchor)
2444
2445	def do_insert_child_anchor(self, iter, anchor):
2446		# Like do_insert_pixbuf()
2447		Gtk.TextBuffer.do_insert_child_anchor(self, iter, anchor)
2448
2449		start = iter.copy()
2450		start.backward_char()
2451		self.remove_all_tags(start, iter)
2452		for tag in filter(_is_indent_tag, self._editmode_tags):
2453			self.apply_tag(tag, start, iter)
2454
2455	def insert_pixbuf(self, iter, pixbuf):
2456		# Make sure we always apply the correct tags when inserting a pixbuf
2457		if iter.equal(self.get_iter_at_mark(self.get_insert())):
2458			Gtk.TextBuffer.insert_pixbuf(self, iter, pixbuf)
2459		else:
2460			with self.tmp_cursor(iter):
2461				Gtk.TextBuffer.insert_pixbuf(self, iter, pixbuf)
2462
2463	def do_insert_pixbuf(self, iter, pixbuf):
2464		# Like do_insert_text() but for pixbuf
2465		# however only apply indenting tags, ignore other
2466		Gtk.TextBuffer.do_insert_pixbuf(self, iter, pixbuf)
2467
2468		start = iter.copy()
2469		start.backward_char()
2470		self.remove_all_tags(start, iter)
2471		if hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'anchor':
2472			for tag in self._editmode_tags:
2473				self.apply_tag(tag, start, iter)
2474		else:
2475			for tag in filter(_is_indent_tag, self._editmode_tags):
2476				self.apply_tag(tag, start, iter)
2477
2478	def do_pre_delete_range(self, start, end):
2479		# (Interactive) deleting a formatted word with <del>, or <backspace>
2480		# should drop the formatting, however selecting a formatted word and
2481		# than typing to replace it, should keep formatting
2482		# Therefore we set a mark to remember the formatting and clear it
2483		# at the end of a user action, or with the next insert at a different
2484		# location
2485		if self._deleted_editmode_mark:
2486			self.delete_mark(self._deleted_editmode_mark)
2487		self._deleted_editmode_mark = self.create_mark(None, end, left_gravity=True)
2488		self._deleted_editmode_mark.editmode_tags = self.iter_get_zim_tags(end)
2489
2490		# Also need to know whether range spanned multiple lines or not
2491		self._deleted_line_end = start.get_line() != end.get_line()
2492
2493	def do_post_delete_range(self, start, end):
2494		# Post handler to hook _do_lines_merged and do some logic
2495		# when deleting bullets
2496		# Note that 'start' and 'end' refer to the same postion here ...
2497
2498		was_list = (
2499			not start.ends_line()
2500			and any(t for t in start.get_tags() if _is_indent_tag(t) and t.zim_attrib.get('_bullet'))
2501		)
2502
2503		# Do merging of tags regardless of whether we deleted a line end or not
2504		# worst case some clean up of run-aways tags is done
2505		if (
2506			(
2507				not start.starts_line()
2508				and list(filter(_is_line_based_tag, start.get_toggled_tags(True)))
2509			) or (
2510				not start.ends_line()
2511				and list(filter(_is_line_based_tag, start.get_toggled_tags(False)))
2512			)
2513		):
2514			self._do_lines_merged(start)
2515
2516		# For cleaning up bullets do check more, else we can delete sequences
2517		# that look like a bullet but aren't - see issue #1328
2518		bullet = self._get_bullet_at_iter(start) # Does not check start of line !
2519		if self._deleted_line_end and bullet is not None:
2520			if start.starts_line():
2521				self._check_renumber.append(start.get_line())
2522			elif was_list:
2523				# Clean up the redundant bullet
2524				offset = start.get_offset()
2525				bound = start.copy()
2526				self._iter_forward_past_bullet(bound, bullet)
2527				self.delete(start, bound)
2528				new = self.get_iter_at_offset(offset)
2529
2530				# NOTE: these assignments should not be needed, but without them
2531				# there is a crash here on some systems - see issue #766
2532				start.assign(new)
2533				end.assign(new)
2534			else:
2535				pass
2536		elif start.starts_line():
2537			indent_tags = list(filter(_is_indent_tag, start.get_tags()))
2538			if indent_tags and indent_tags[0].zim_attrib['_bullet']:
2539				# had a bullet, but no longer (implies we are start of
2540				# line - case where we are not start of line is
2541				# handled by _do_lines_merged by extending the indent tag)
2542				self.update_indent_tag(start.get_line(), None)
2543
2544		self.update_editmode()
2545
2546	def _do_lines_merged(self, iter):
2547		# Enforce tags like 'h', 'pre' and 'indent' to be consistent over the line
2548		# Merge links that have same href target
2549		if iter.starts_line() or iter.ends_line():
2550			return # TODO Why is this ???
2551
2552		end = iter.copy()
2553		end.forward_to_line_end()
2554
2555		self.smart_remove_tags(_is_line_based_tag, iter, end)
2556
2557		for tag in self.iter_get_zim_tags(iter):
2558			if _is_line_based_tag(tag):
2559				if tag.zim_tag == 'pre':
2560					self.smart_remove_tags(_is_zim_tag, iter, end)
2561				self.apply_tag(tag, iter, end)
2562			elif _is_link_tag(tag):
2563				for rh_tag in filter(_is_link_tag, iter.get_tags()):
2564					if rh_tag is not tag and rh_tag.zim_attrib['href'] == tag.zim_attrib['href']:
2565						bound = iter.copy()
2566						bound.forward_to_tag_toggle(rh_tag)
2567						self.remove_tag(rh_tag, iter, bound)
2568						self.apply_tag(tag, iter, bound)
2569
2570		self.update_editmode()
2571
2572	def get_bullet(self, line):
2573		'''Get the bullet type on a specific line, if any
2574
2575		@param line: the line number
2576		@returns: the bullet type, if any, or C{None}.
2577		The bullet type can be any of::
2578				BULLET
2579				UNCHECKED_BOX
2580				CHECKED_BOX
2581				XCHECKED_BOX
2582				MIGRATED_BOX
2583				TRANSMIGRATED_BOX
2584		or a numbered list bullet (test with L{is_numbered_bullet_re})
2585		'''
2586		iter = self.get_iter_at_line(line)
2587		return self._get_bullet_at_iter(iter)
2588
2589	def get_bullet_at_iter(self, iter):
2590		'''Return the bullet type in a specific location
2591
2592		Like L{get_bullet()}
2593
2594		@param iter: a C{Gtk.TextIter}
2595		@returns: a bullet type, or C{None}
2596		'''
2597		if not iter.starts_line():
2598			return None
2599		else:
2600			return self._get_bullet_at_iter(iter)
2601
2602	def _get_bullet_at_iter(self, iter):
2603		pixbuf = iter.get_pixbuf()
2604		if pixbuf:
2605			if getattr(pixbuf, 'zim_type', None) == 'icon':
2606
2607				return bullets.get(pixbuf.zim_attrib['stock'])
2608			else:
2609				return None
2610		else:
2611			bound = iter.copy()
2612			if not self.iter_forward_word_end(bound):
2613				return None # empty line or whitespace at start of line
2614
2615			text = iter.get_slice(bound)
2616			if text.startswith('\u2022'):
2617				return BULLET
2618			elif is_numbered_bullet_re.match(text):
2619				return text
2620			else:
2621				return None
2622
2623	def iter_forward_past_bullet(self, iter):
2624		'''Move an TextIter past a bullet
2625
2626		This method is useful because we typically want to insert new
2627		text on a line with a bullet after the bullet. This method can
2628		help to find that position.
2629
2630		@param iter: a C{Gtk.TextIter}. The position of this iter will
2631		be modified by this method.
2632		'''
2633		bullet = self.get_bullet_at_iter(iter)
2634		if bullet:
2635			self._iter_forward_past_bullet(iter, bullet)
2636			return True
2637		else:
2638			return False
2639
2640	def _iter_forward_past_bullet(self, iter, bullet, raw=False):
2641		if bullet in BULLETS:
2642			# Each of these just means one char
2643			iter.forward_char()
2644		else:
2645			assert is_numbered_bullet_re.match(bullet)
2646			self.iter_forward_word_end(iter)
2647
2648		if not raw:
2649			# Skip whitespace as well
2650			bound = iter.copy()
2651			bound.forward_char()
2652			while iter.get_text(bound) == ' ':
2653				if iter.forward_char():
2654					bound.forward_char()
2655				else:
2656					break
2657
2658	def get_parsetree(self, bounds=None, raw=False):
2659		'''Get a L{ParseTree} representing the buffer contents
2660
2661		@param bounds: a 2-tuple with two C{Gtk.TextIter} specifying a
2662		range in the buffer (e.g. current selection). If C{None} the
2663		whole buffer is returned.
2664
2665		@param raw: if C{True} you get a tree that is B{not} nicely
2666		cleaned up. This raw tree should result in the exact same
2667		contents in the buffer when reloaded. However such a 'raw'
2668		tree may cause problems when passed to one of the format
2669		modules. So it is intended only for internal use between the
2670		buffer and e.g. the L{UndoStackManager}.
2671
2672		Raw parsetrees have an attribute to flag them as a raw tree, so
2673		on insert we can make sure they are inserted in the same way.
2674
2675		When C{raw} is C{False} reloading the same tree may have subtle
2676		differences.
2677
2678		@returns: a L{ParseTree} object
2679		'''
2680		if self.showing_template and not raw:
2681			return None
2682
2683		if bounds is None:
2684			start, end = self.get_bounds()
2685			attrib = {}
2686		else:
2687			start, end = bounds
2688			attrib = {'partial': True}
2689
2690		if raw:
2691			builder = ElementTreeModule.TreeBuilder()
2692			attrib['raw'] = True
2693			builder.start('zim-tree', attrib)
2694		else:
2695			builder = OldParseTreeBuilder()
2696			builder.start('zim-tree', attrib)
2697
2698		open_tags = []
2699		def set_tags(iter, tags):
2700			# This function changes the parse tree based on the TextTags in
2701			# effect for the next section of text.
2702			# It does so be keeping the stack of open tags and compare it
2703			# with the new set of tags in order to decide which of the
2704			# tags can be closed and which new ones need to be opened.
2705
2706			tags.sort(key=lambda tag: tag.get_priority())
2707			if any(_is_tag_tag(t) for t in tags):
2708				# Although not highest prio, no other tag can nest below a tag-tag
2709				while not _is_tag_tag(tags[-1]):
2710					tags.pop()
2711
2712			if any(_is_inline_nesting_tag(t) for t in tags):
2713				tags = self._sort_nesting_style_tags(iter, end, tags, [t[0] for t in open_tags])
2714
2715			# For tags that can only appear once, if somehow an overlap
2716			# occured, choose the one with the highest prio
2717			for i in range(len(tags)-2, -1, -1):
2718				if tags[i].zim_type in ('link', 'tag', 'indent') \
2719					and tags[i+1].zim_type == tags[i].zim_type:
2720						tags.pop(i)
2721				elif tags[i+1].zim_tag == 'h' \
2722					and tags[i].zim_tag in ('h', 'indent'):
2723						tags.pop(i)
2724				elif tags[i+1].zim_tag == 'pre' \
2725					and tags[i].zim_type == 'style':
2726						tags.pop(i)
2727
2728			i = 0
2729			while i < len(tags) and i < len(open_tags) \
2730			and tags[i] == open_tags[i][0]:
2731				i += 1
2732
2733			# so i is the breakpoint where new stack is different
2734			while len(open_tags) > i:
2735				builder.end(open_tags[-1][1])
2736				open_tags.pop()
2737
2738			# Convert some tags on the fly
2739			if tags:
2740				continue_attrib = {}
2741				for tag in tags[i:]:
2742					t, attrib = tag.zim_tag, tag.zim_attrib
2743					if t == 'indent':
2744						attrib = attrib.copy() # break ref with tree
2745						del attrib['_bullet']
2746						bullet = self._get_bullet_at_iter(iter)
2747						if bullet:
2748							t = 'li'
2749							attrib['bullet'] = bullet
2750							self._iter_forward_past_bullet(iter, bullet, raw=raw)
2751						elif not raw and not iter.starts_line():
2752							# Indent not visible if it does not start at begin of line
2753							t = '_ignore_'
2754						elif len([t for t in tags[i:] if t.zim_tag == 'pre']):
2755							# Indent of 'pre' blocks handled in subsequent iteration
2756							continue_attrib.update(attrib)
2757							continue
2758						else:
2759							t = 'div'
2760					elif t == 'pre' and not raw and not iter.starts_line():
2761						# Without indenting 'pre' looks the same as 'code'
2762						# Prevent turning into a separate paragraph here
2763						t = 'code'
2764					elif t == 'pre':
2765						if attrib:
2766							attrib.update(continue_attrib)
2767						else:
2768							attrib = continue_attrib
2769						continue_attrib = {}
2770					elif t == 'link':
2771						attrib = self.get_link_data(iter, raw=raw)
2772					elif t == 'tag':
2773						attrib = self.get_tag_data(iter)
2774						if not attrib['name']:
2775							t = '_ignore_'
2776					builder.start(t, attrib or {})
2777					open_tags.append((tag, t))
2778					if t == 'li':
2779						break
2780						# HACK - ignore any other tags because we moved
2781						# the cursor - needs also a break_tags before
2782						# which is special cased below
2783						# TODO: cleaner solution for this issue -
2784						# maybe easier when tags for list and indent
2785						# are separated ?
2786
2787		def break_tags(type):
2788			# Forces breaking the stack of open tags on the level of 'tag'
2789			# The next set_tags() will re-open any tags that are still open
2790			i = 0
2791			for i in range(len(open_tags)):
2792				if open_tags[i][1] == type:
2793					break
2794
2795			# so i is the breakpoint
2796			while len(open_tags) > i:
2797				builder.end(open_tags[-1][1])
2798				open_tags.pop()
2799
2800		# And now the actual loop going through the buffer
2801		iter = start.copy()
2802		set_tags(iter, list(filter(_is_zim_tag, iter.get_tags())))
2803		while iter.compare(end) == -1:
2804			pixbuf = iter.get_pixbuf()
2805			anchor = iter.get_child_anchor()
2806			if pixbuf:
2807				if pixbuf.zim_type == 'icon':
2808					# Reset all tags - and let set_tags parse the bullet
2809					if open_tags:
2810						break_tags(open_tags[0][1])
2811					set_tags(iter, list(filter(_is_indent_tag, iter.get_tags())))
2812				elif pixbuf.zim_type == 'anchor':
2813					pass # allow as object nested in e.g. header tag
2814				else:
2815					# reset all tags except indenting
2816					set_tags(iter, list(filter(_is_indent_tag, iter.get_tags())))
2817
2818				pixbuf = iter.get_pixbuf() # iter may have moved
2819				if pixbuf is None:
2820					continue
2821
2822				if pixbuf.zim_type == 'icon':
2823					logger.warn('BUG: Checkbox outside of indent ?')
2824				elif pixbuf.zim_type == 'image':
2825					attrib = pixbuf.zim_attrib.copy()
2826					builder.start('img', attrib or {})
2827					builder.end('img')
2828				elif pixbuf.zim_type == 'anchor':
2829					attrib = pixbuf.zim_attrib.copy()
2830					builder.start('anchor', attrib)
2831					builder.data(attrib['name']) # HACK for OldParseTreeBuilder cleanup
2832					builder.end('anchor')
2833				else:
2834					assert False, 'BUG: unknown pixbuf type'
2835
2836				iter.forward_char()
2837
2838			# embedded widget
2839			elif anchor:
2840				set_tags(iter, list(filter(_is_indent_tag, iter.get_tags())))
2841				anchor = iter.get_child_anchor() # iter may have moved
2842				if isinstance(anchor, InsertedObjectAnchor):
2843					anchor.dump(builder)
2844					iter.forward_char()
2845				else:
2846					continue
2847			else:
2848				# Set tags
2849				copy = iter.copy()
2850
2851				bullet = self.get_bullet_at_iter(iter) # implies check for start of line
2852				if bullet:
2853					break_tags('indent')
2854					# This is part of the HACK for bullets in
2855					# set_tags()
2856
2857				set_tags(iter, list(filter(_is_zim_tag, iter.get_tags())))
2858				if not iter.equal(copy): # iter moved
2859					continue
2860
2861				# Find biggest slice without tags being toggled
2862				bound = iter.copy()
2863				toggled = []
2864				while not toggled:
2865					if not bound.is_end() and bound.forward_to_tag_toggle(None):
2866						# For some reason the not is_end check is needed
2867						# to prevent an odd corner case infinite loop
2868						toggled = list(filter(_is_zim_tag,
2869							bound.get_toggled_tags(False)
2870							+ bound.get_toggled_tags(True)))
2871					else:
2872						bound = end.copy() # just to be sure..
2873						break
2874
2875				# But limit slice to first pixbuf or any embeddded widget
2876
2877				text = iter.get_slice(bound)
2878				if text.startswith(PIXBUF_CHR):
2879					text = text[1:] # special case - we see this char, but get_pixbuf already returned None, so skip it
2880
2881				if PIXBUF_CHR in text:
2882					i = text.index(PIXBUF_CHR)
2883					bound = iter.copy()
2884					bound.forward_chars(i)
2885					text = text[:i]
2886
2887				# And limit to end
2888				if bound.compare(end) == 1:
2889					bound = end.copy()
2890					text = iter.get_slice(end)
2891
2892				break_at = None
2893				MULTI_LINE_BLOCK = [t for t in BLOCK_LEVEL if t != HEADING]
2894				if bound.get_line() != iter.get_line():
2895					if any(t[1] == LISTITEM for t in open_tags):
2896						# And limit bullets to a single line
2897						break_at = LISTITEM
2898					elif not raw and any(t[1] not in MULTI_LINE_BLOCK for t in open_tags):
2899						# Prevent formatting tags to run multiple lines
2900						for t in open_tags:
2901							if t[1] not in MULTI_LINE_BLOCK:
2902								break_at = t[1]
2903								break
2904
2905				if break_at:
2906					orig = bound
2907					bound = iter.copy()
2908					bound.forward_line()
2909					assert bound.compare(orig) < 1
2910					text = iter.get_slice(bound).rstrip('\n')
2911					builder.data(text)
2912					break_tags(break_at)
2913					builder.data('\n') # add to tail
2914				else:
2915					# Else just insert text we got
2916					builder.data(text)
2917
2918				iter = bound
2919
2920		# close any open tags
2921		set_tags(end, [])
2922
2923		builder.end('zim-tree')
2924		tree = ParseTree(builder.close())
2925		tree.encode_urls()
2926
2927		if not raw and tree.hascontent:
2928			# Reparsing the parsetree in order to find raw wiki codes
2929			# and get rid of oddities in our generated parsetree.
2930			#print(">>> Parsetree original:\n", tree.tostring())
2931			from zim.formats import get_format
2932			format = get_format("wiki") # FIXME should the format used here depend on the store ?
2933			dumper = format.Dumper()
2934			parser = format.Parser()
2935			text = dumper.dump(tree)
2936			#print(">>> Wiki text:\n", text)
2937			tree = parser.parse(text, partial=tree.ispartial)
2938			#print(">>> Parsetree recreated:\n", tree.tostring())
2939
2940		return tree
2941
2942	def _sort_nesting_style_tags(self, iter, end, tags, open_tags):
2943		new_block, new_nesting, new_leaf = self._split_nesting_style_tags(tags)
2944		open_block, open_nesting, open_leaf = self._split_nesting_style_tags(open_tags)
2945		sorted_new_nesting = []
2946
2947		# First prioritize open tags - these are sorted already
2948		if new_block == open_block:
2949			for tag in open_nesting:
2950				if tag in new_nesting:
2951					i = new_nesting.index(tag)
2952					sorted_new_nesting.append(new_nesting.pop(i))
2953				else:
2954					break
2955
2956		# Then sort by length untill closing all tags that open at the same time
2957		def tag_close_pos(tag):
2958			my_iter = iter.copy()
2959			my_iter.forward_to_tag_toggle(tag)
2960			if my_iter.compare(end) > 0:
2961				return end.get_offset()
2962			else:
2963				return my_iter.get_offset()
2964
2965		new_nesting.sort(key=tag_close_pos, reverse=True)
2966		sorted_new_nesting += new_nesting
2967
2968		return new_block + sorted_new_nesting + new_leaf
2969
2970	def _split_nesting_style_tags(self, tags):
2971		block, nesting = [], []
2972		while tags and not _is_inline_nesting_tag(tags[0]):
2973			block.append(tags.pop(0))
2974		while tags and _is_inline_nesting_tag(tags[0]):
2975			nesting.append(tags.pop(0))
2976		return block, nesting, tags
2977
2978	def select_line(self, line=None):
2979		'''Selects a line
2980		@param line: line number; if C{None} current line will be selected
2981		@returns: C{True} when successful
2982		'''
2983		# Differs from get_line_bounds because we exclude the trailing
2984		# line break while get_line_bounds selects these
2985		if line is None:
2986			iter = self.get_iter_at_mark(self.get_insert())
2987			line = iter.get_line()
2988		return self.select_lines(line, line)
2989
2990	def select_lines(self, first, last):
2991		'''Select multiple lines
2992		@param first: line number first line
2993		@param last: line number last line
2994		@returns: C{True} when successful
2995		'''
2996		start = self.get_iter_at_line(first)
2997		end = self.get_iter_at_line(last)
2998		if end.ends_line():
2999			if end.equal(start):
3000				return False
3001			else:
3002				pass
3003		else:
3004			end.forward_to_line_end()
3005		self.select_range(start, end)
3006		return True
3007
3008	def select_word(self):
3009		'''Selects the current word, if any
3010
3011		@returns: C{True} when succcessful
3012		'''
3013		insert = self.get_iter_at_mark(self.get_insert())
3014		if not insert.inside_word():
3015			return False
3016
3017		bound = insert.copy()
3018		if not insert.starts_word():
3019			insert.backward_word_start()
3020		if not bound.ends_word():
3021			bound.forward_word_end()
3022
3023		self.select_range(insert, bound)
3024		return True
3025
3026	def strip_selection(self):
3027		'''Shrinks the selection to exclude any whitespace on start and end.
3028		If only white space was selected this function will not change the selection.
3029		@returns: C{True} when this function changed the selection.
3030		'''
3031		bounds = self.get_selection_bounds()
3032		if not bounds:
3033			return False
3034
3035		text = bounds[0].get_text(bounds[1])
3036		if not text or text.isspace():
3037			return False
3038
3039		start, end = bounds[0].copy(), bounds[1].copy()
3040		iter = start.copy()
3041		iter.forward_char()
3042		text = start.get_text(iter)
3043		while text and text.isspace():
3044			start.forward_char()
3045			iter.forward_char()
3046			text = start.get_text(iter)
3047
3048		iter = end.copy()
3049		iter.backward_char()
3050		text = iter.get_text(end)
3051		while text and text.isspace():
3052			end.backward_char()
3053			iter.backward_char()
3054			text = iter.get_text(end)
3055
3056		if (start.equal(bounds[0]) and end.equal(bounds[1])):
3057			return False
3058		else:
3059			self.select_range(start, end)
3060			return True
3061
3062	def select_link(self):
3063		'''Selects the current link, if any
3064		@returns: link attributes when succcessful, C{None} otherwise
3065		'''
3066		insert = self.get_iter_at_mark(self.get_insert())
3067		tag = self.get_link_tag(insert)
3068		if tag is None:
3069			return None
3070		start, end = self.get_tag_bounds(insert, tag)
3071		self.select_range(start, end)
3072		return self.get_link_data(start)
3073
3074	def get_has_link_selection(self):
3075		'''Check whether a link is selected or not
3076		@returns: link attributes when succcessful, C{None} otherwise
3077		'''
3078		bounds = self.get_selection_bounds()
3079		if not bounds:
3080			return None
3081
3082		insert = self.get_iter_at_mark(self.get_insert())
3083		tag = self.get_link_tag(insert)
3084		if tag is None:
3085			return None
3086		start, end = self.get_tag_bounds(insert, tag)
3087		if start.equal(bounds[0]) and end.equal(bounds[1]):
3088			return self.get_link_data(start)
3089		else:
3090			return None
3091
3092	def remove_link(self, start, end):
3093		'''Removes any links between in a range
3094
3095		@param start: a C{Gtk.TextIter}
3096		@param end: a C{Gtk.TextIter}
3097		'''
3098		self.smart_remove_tags(_is_link_tag, start, end)
3099		self.update_editmode()
3100
3101	def find_implicit_anchor(self, name):
3102		"""Search the current page for a heading who's derived (implicit) anchor name is
3103		matching the provided parameter.
3104		@param name: the name of the anchor
3105		@returns: a C{Gtk.TextIter} pointing to the start of the heading or C{None}.
3106		"""
3107		iter = self.get_start_iter()
3108		while True:
3109			tags = list(filter(_is_heading_tag, iter.get_tags()))
3110			if tags:
3111				tag = tags[0]
3112				end = iter.copy()
3113				end.forward_to_tag_toggle(tag)
3114				text = iter.get_text(end)
3115				if heading_to_anchor(text) == name:
3116					return iter
3117			if not iter.forward_line():
3118				break
3119		return None
3120
3121	def find_anchor(self, name):
3122		"""Searches the current page for an anchor with the requested name.
3123
3124		Explicit anchors are being searched with precedence over implicit
3125		anchors derived from heading elements.
3126
3127		@param name: the name of the anchor to look for
3128		@returns: a C{Gtk.TextIter} pointing to the start of the heading or C{None}.
3129		"""
3130		# look for explicit anchors tags including image or object tags
3131		start, end = self.get_bounds()
3132		for iter, myname in self.iter_anchors_for_range(start, end):
3133			if myname == name:
3134				return iter
3135
3136		# look for an implicit heading anchor
3137		return self.find_implicit_anchor(name)
3138
3139	def toggle_checkbox(self, line, checkbox_type=None, recursive=False):
3140		'''Toggles the state of the checkbox at a specific line, if any
3141
3142		@param line: the line number
3143		@param checkbox_type: the checkbox type that we want to toggle:
3144		one of C{CHECKED_BOX}, C{XCHECKED_BOX}, C{MIGRATED_BOX},
3145		C{TRANSMIGRATED_BOX}.
3146		If C{checkbox_type} is given, it toggles between this type and
3147		unchecked. Otherwise it rotates through unchecked, checked
3148		and xchecked.
3149		As a special case when the C{checkbox_type} ir C{UNCHECKED_BOX}
3150		the box is always unchecked.
3151		@param recursive: When C{True} any child items in the list will
3152		also be upadted accordingly (see L{TextBufferList.set_bullet()}
3153
3154		@returns: C{True} for success, C{False} if no checkbox was found.
3155		'''
3156		# For mouse click no checkbox type is given, so we cycle
3157		# For <F12> and <Shift><F12> checkbox_type is given so we toggle
3158		# between the two
3159		bullet = self.get_bullet(line)
3160		if bullet in CHECKBOXES:
3161			if checkbox_type:
3162				if bullet == checkbox_type:
3163					newbullet = UNCHECKED_BOX
3164				else:
3165					newbullet = checkbox_type
3166			else:
3167				i = list(CHECKBOXES).index(bullet) # use list() to be python 2.5 compatible
3168				next = (i + 1) % len(CHECKBOXES)
3169				newbullet = CHECKBOXES[next]
3170		else:
3171			return False
3172
3173		if recursive:
3174			row, clist = TextBufferList.new_from_line(self, line)
3175			clist.set_bullet(row, newbullet)
3176		else:
3177			self.set_bullet(line, newbullet)
3178
3179		return True
3180
3181	def toggle_checkbox_for_cursor_or_selection(self, checkbox_type=None, recursive=False):
3182		'''Like L{toggle_checkbox()} but applies to current line or
3183		current selection. Intended for interactive use.
3184
3185		@param checkbox_type: the checkbox type that we want to toggle
3186		@param recursive: When C{True} any child items in the list will
3187		also be upadted accordingly (see L{TextBufferList.set_bullet()}
3188		'''
3189		if self.get_has_selection():
3190			self.foreach_line_in_selection(self.toggle_checkbox, checkbox_type, recursive)
3191		else:
3192			line = self.get_insert_iter().get_line()
3193			return self.toggle_checkbox(line, checkbox_type, recursive)
3194
3195	def iter_backward_word_start(self, iter):
3196		'''Like C{Gtk.TextIter.backward_word_start()} but less intelligent.
3197		This method does not take into account the language or
3198		punctuation and just skips to either the last whitespace or
3199		the beginning of line.
3200
3201		@param iter: a C{Gtk.TextIter}, the position of this iter will
3202		be modified
3203		@returns: C{True} when successful
3204		'''
3205		if iter.starts_line():
3206			return False
3207
3208		orig = iter.copy()
3209		while True:
3210			if iter.starts_line():
3211				break
3212			else:
3213				bound = iter.copy()
3214				bound.backward_char()
3215				char = bound.get_slice(iter)
3216				if char == PIXBUF_CHR or char.isspace():
3217					break # whitespace or pixbuf before start iter
3218				else:
3219					iter.backward_char()
3220
3221		return iter.compare(orig) != 0
3222
3223	def iter_forward_word_end(self, iter):
3224		'''Like C{Gtk.TextIter.forward_word_end()} but less intelligent.
3225		This method does not take into account the language or
3226		punctuation and just skips to either the next whitespace or the
3227		end of the line.
3228
3229		@param iter: a C{Gtk.TextIter}, the position of this iter will
3230		be modified
3231		@returns: C{True} when successful
3232		'''
3233		if iter.ends_line():
3234			return False
3235
3236		orig = iter.copy()
3237		while True:
3238			if iter.ends_line():
3239				break
3240			else:
3241				bound = iter.copy()
3242				bound.forward_char()
3243				char = bound.get_slice(iter)
3244				if char == PIXBUF_CHR or char.isspace():
3245					break # whitespace or pixbuf after iter
3246				else:
3247					iter.forward_char()
3248
3249		return iter.compare(orig) != 0
3250
3251	def get_iter_at_line(self, line):
3252		'''Like C{Gtk.TextBuffer.get_iter_at_line()} but with additional
3253		safety check
3254		@param line: an integer line number counting from 0
3255		@returns: a Gtk.TextIter
3256		@raises ValueError: when line is not within the buffer
3257		'''
3258		# Gtk TextBuffer returns iter of last line for lines past the
3259		# end of the buffer
3260		if line < 0:
3261			raise ValueError('Negative line number: %i' % line)
3262		else:
3263			iter = Gtk.TextBuffer.get_iter_at_line(self, line)
3264			if iter.get_line() != line:
3265				raise ValueError('Line number beyond the end of the buffer: %i' % line)
3266			return iter
3267
3268	def get_line_bounds(self, line):
3269		'''Get the TextIters at start and end of line
3270
3271		@param line: the line number
3272		@returns: a 2-tuple of C{Gtk.TextIter} for start and end of the
3273		line
3274		'''
3275		start = self.get_iter_at_line(line)
3276		end = start.copy()
3277		end.forward_line()
3278		return start, end
3279
3280	def get_line_is_empty(self, line):
3281		'''Check for empty lines
3282
3283		@param line: the line number
3284		@returns: C{True} if the line only contains whitespace
3285		'''
3286		start, end = self.get_line_bounds(line)
3287		return start.equal(end) or start.get_slice(end).isspace()
3288
3289	def get_has_selection(self):
3290		'''Check if there is a selection
3291
3292		Method available in C{Gtk.TextBuffer} for gtk version >= 2.10
3293		reproduced here for backward compatibility.
3294
3295		@returns: C{True} when there is a selection
3296		'''
3297		return bool(self.get_selection_bounds())
3298
3299	def iter_in_selection(self, iter):
3300		'''Check if a specific TextIter is within the selection
3301
3302		@param iter: a C{Gtk.TextIter}
3303		@returns: C{True} if there is a selection and C{iter} is within
3304		the range of the selection
3305		'''
3306		bounds = self.get_selection_bounds()
3307		return bounds \
3308			and bounds[0].compare(iter) <= 0 \
3309			and bounds[1].compare(iter) >= 0
3310		# not using iter.in_range to be inclusive of bounds
3311
3312	def unset_selection(self):
3313		'''Remove any selection in the buffer'''
3314		iter = self.get_iter_at_mark(self.get_insert())
3315		self.select_range(iter, iter)
3316
3317	def copy_clipboard(self, clipboard, format='plain'):
3318		'''Copy current selection to a clipboard
3319
3320		@param clipboard: a L{Clipboard} object
3321		@param format: a format name
3322		'''
3323		bounds = self.get_selection_bounds()
3324		if bounds:
3325			tree = self.get_parsetree(bounds)
3326			#~ print(">>>> SET", tree.tostring())
3327			clipboard.set_parsetree(self.notebook, self.page, tree, format)
3328
3329	def cut_clipboard(self, clipboard, default_editable):
3330		'''Cut current selection to a clipboard
3331
3332		First copies the selection to the clipboard and then deletes
3333		the selection in the buffer.
3334
3335		@param clipboard: a L{Clipboard} object
3336		@param default_editable: default state of the L{TextView}
3337		'''
3338		if self.get_has_selection():
3339			self.copy_clipboard(clipboard)
3340			self.delete_selection(True, default_editable)
3341
3342	def paste_clipboard(self, clipboard, iter, default_editable, text_format=None):
3343		'''Paste data from a clipboard into the buffer
3344
3345		@param clipboard: a L{Clipboard} object
3346		@param iter: a C{Gtk.TextIter} for the insert location
3347		@param default_editable: default state of the L{TextView}
3348		'''
3349		if not default_editable:
3350			return
3351
3352		if iter is None:
3353			iter = self.get_iter_at_mark(self.get_insert())
3354			tags = list(filter(_is_pre_or_code_tag, self._editmode_tags))
3355			if tags:
3356				text_format = 'verbatim-' + tags[0].zim_tag
3357		elif self.get_has_selection():
3358			# unset selection if explicit iter is given
3359			bound = self.get_selection_bound()
3360			insert = self.get_insert()
3361			self.move_mark(bound, self.get_iter_at_mark(insert))
3362
3363		mark = self.get_mark('zim-paste-position')
3364		if mark:
3365			self.move_mark(mark, iter)
3366		else:
3367			self.create_mark('zim-paste-position', iter, left_gravity=False)
3368
3369		#~ clipboard.debug_dump_contents()
3370		if text_format is None:
3371			tags = list(filter(_is_pre_or_code_tag, self.iter_get_zim_tags(iter)))
3372			if tags:
3373				text_format = 'verbatim-' + tags[0].zim_tag
3374			else:
3375				text_format = 'wiki' # TODO: should depend on page format
3376		parsetree = clipboard.get_parsetree(self.notebook, self.page, text_format)
3377		if not parsetree:
3378			return
3379
3380		#~ print('!! PASTE', parsetree.tostring())
3381		with self.user_action:
3382			if self.get_has_selection():
3383				start, end = self.get_selection_bounds()
3384				self.delete(start, end)
3385
3386			mark = self.get_mark('zim-paste-position')
3387			if not mark:
3388				return # prevent crash - see lp:807830
3389
3390			iter = self.get_iter_at_mark(mark)
3391			self.delete_mark(mark)
3392
3393			self.place_cursor(iter)
3394			self.insert_parsetree_at_cursor(parsetree, interactive=True)
3395
3396
3397class TextBufferList(list):
3398	'''This class represents a bullet or checkbox list in a L{TextBuffer}.
3399	It is used to perform recursive actions on the list.
3400
3401	While the L{TextBuffer} just treats list items as lines that start
3402	with a bullet, the TextBufferList maps to a number of lines that
3403	together form a list. It uses "row ids" to refer to specific
3404	items within this range.
3405
3406	TextBufferList objects will become invalid after any modification
3407	to the buffer that changes the line count within the list. Using
3408	them after such modification will result in errors.
3409	'''
3410
3411	# This class is a list of tuples, each tuple is a pair of
3412	# (linenumber, indentlevel, bullettype)
3413
3414	LINE_COL = 0
3415	INDENT_COL = 1
3416	BULLET_COL = 2
3417
3418	@classmethod
3419	def new_from_line(self, textbuffer, line):
3420		'''Constructor for a new TextBufferList mapping the list at a
3421		specific line in the buffer
3422
3423		@param textbuffer: a L{TextBuffer} object
3424		@param line: a line number
3425
3426		This line should be part of a list, the TextBufferList object
3427		that is returned maps the full list, so it possibly extends
3428		above and below C{line}.
3429
3430		@returns: a 2-tuple of a row id and a the new TextBufferList
3431		object, or C{(None, None)} if C{line} is not part of a list.
3432		The row id points to C{line} in the list.
3433		'''
3434		if textbuffer.get_bullet(line) is None:
3435			return None, None
3436
3437		# find start of list
3438		start = line
3439		for myline in range(start, -1, -1):
3440			if textbuffer.get_bullet(myline) is None:
3441				break # TODO skip lines with whitespace
3442			else:
3443				start = myline
3444
3445		# find end of list
3446		end = line
3447		lastline = textbuffer.get_end_iter().get_line()
3448		for myline in range(end, lastline + 1, 1):
3449			if textbuffer.get_bullet(myline) is None:
3450				break # TODO skip lines with whitespace
3451			else:
3452				end = myline
3453
3454		list = TextBufferList(textbuffer, start, end)
3455		row = list.get_row_at_line(line)
3456		#~ print('!! LIST %i..%i ROW %i' % (start, end, row))
3457		#~ print('>>', list)
3458		return row, list
3459
3460	def __init__(self, textbuffer, firstline, lastline):
3461		'''Constructor
3462
3463		@param textbuffer: a L{TextBuffer} object
3464		@param firstline: the line number for the first line of the list
3465		@param lastline: the line number for the last line of the list
3466		'''
3467		self.buffer = textbuffer
3468		self.firstline = firstline
3469		self.lastline = lastline
3470		for line in range(firstline, lastline + 1):
3471			bullet = self.buffer.get_bullet(line)
3472			indent = self.buffer.get_indent(line)
3473			if bullet:
3474				self.append((line, indent, bullet))
3475
3476	def get_row_at_line(self, line):
3477		'''Get the row in the list for a specific line
3478
3479		@param line: the line number for a line in the L{TextBuffer}
3480		@returns: the row id for a row in the list or C{None} when
3481		the line was outside of the list
3482		'''
3483		for i in range(len(self)):
3484			if self[i][self.LINE_COL] == line:
3485				return i
3486		else:
3487			return None
3488
3489	def can_indent(self, row):
3490		'''Check whether a specific item in the list can be indented
3491
3492		List items can only be indented if they are on top of the list
3493		or when there is some node above them to serve as new parent node.
3494		This avoids indenting two levels below the parent.
3495
3496		So e.g. in the case of::
3497
3498		  * item a
3499		  * item b
3500
3501		then "item b" can indent and become a child of "item a".
3502		However after indenting once::
3503
3504		  * item a
3505		      * item b
3506
3507		now "item b" can not be indented further because it is already
3508		one level below "item a".
3509
3510		@param row: the row id
3511		@returns: C{True} when indenting is possible
3512		'''
3513		if row == 0:
3514			return True
3515		else:
3516			parents = self._parents(row)
3517			if row - 1 in parents:
3518				return False # we are first child
3519			else:
3520				return True
3521
3522	def can_unindent(self, row):
3523		'''Check if a specific item in the list has indenting which
3524		can be reduced
3525
3526		@param row: the row id
3527		@returns: C{True} when the item has indenting
3528		'''
3529		return self[row][self.INDENT_COL] > 0
3530
3531	def indent(self, row):
3532		'''Indent a list item and all it's children
3533
3534		For example, when indenting "item b" in this list::
3535
3536		  * item a
3537		  * item b
3538		      * item C
3539
3540		it will result in::
3541
3542		  * item a
3543		      * item b
3544		          * item C
3545
3546		@param row: the row id
3547		@returns: C{True} if successfulll
3548		'''
3549		if not self.can_indent(row):
3550			return False
3551		with self.buffer.user_action:
3552			self._indent(row, 1)
3553		return True
3554
3555	def unindent(self, row):
3556		'''Un-indent a list item and it's children
3557
3558		@param row: the row id
3559		@returns: C{True} if successfulll
3560		'''
3561		if not self.can_unindent(row):
3562			return False
3563		with self.buffer.user_action:
3564			self._indent(row, -1)
3565		return True
3566
3567	def _indent(self, row, step):
3568		line, level, bullet = self[row]
3569		self._indent_row(row, step)
3570
3571		if row == 0:
3572			# Indent the whole list
3573			for i in range(1, len(self)):
3574				if self[i][self.INDENT_COL] >= level:
3575					# double check implicit assumption that first item is at lowest level
3576					self._indent_row(i, step)
3577				else:
3578					break
3579		else:
3580			# Indent children
3581			for i in range(row + 1, len(self)):
3582				if self[i][self.INDENT_COL] > level:
3583					self._indent_row(i, step)
3584				else:
3585					break
3586
3587			# Renumber - *after* children have been updated as well
3588			# Do not restrict to number bullets - we might be moving
3589			# a normal bullet into a numbered sub list
3590			# TODO - pull logic of renumber_list_after_indent here and use just renumber_list
3591			self.buffer.renumber_list_after_indent(line, level)
3592
3593	def _indent_row(self, row, step):
3594		#~ print("(UN)INDENT", row, step)
3595		line, level, bullet = self[row]
3596		newlevel = level + step
3597		if self.buffer.set_indent(line, newlevel):
3598			self.buffer.update_editmode() # also updates indent tag
3599			self[row] = (line, newlevel, bullet)
3600
3601	def set_bullet(self, row, bullet):
3602		'''Set the bullet type for a specific item and update parents
3603		and children accordingly
3604
3605		Used to (un-)check the checkboxes and synchronize child
3606		nodes and parent nodes. When a box is checked, any open child
3607		nodes are checked. Also when this is the last checkbox on the
3608		given level to be checked, the parent box can be checked as
3609		well. When a box is un-checked, also the parent checkbox is
3610		un-checked. Both updating of children and parents is recursive.
3611
3612		@param row: the row id
3613		@param bullet: the bullet type, which can be one of::
3614			BULLET
3615			CHECKED_BOX
3616			UNCHECKED_BOX
3617			XCHECKED_BOX
3618			MIGRATED_BOX
3619			TRANSMIGRATED_BOX
3620		'''
3621		assert bullet in BULLETS
3622		with self.buffer.user_action:
3623			self._change_bullet_type(row, bullet)
3624			if bullet == BULLET:
3625				pass
3626			elif bullet == UNCHECKED_BOX:
3627				self._checkbox_unchecked(row)
3628			else: # CHECKED_BOX, XCHECKED_BOX, MIGRATED_BOX, TRANSMIGRATED_BOX
3629				self._checkbox_checked(row, bullet)
3630
3631	def _checkbox_unchecked(self, row):
3632		# When a row is unchecked, it's children are untouched but
3633		# all parents will be unchecked as well
3634		for parent in self._parents(row):
3635			if self[parent][self.BULLET_COL] not in CHECKBOXES:
3636				continue # ignore non-checkbox bullet
3637
3638			self._change_bullet_type(parent, UNCHECKED_BOX)
3639
3640	def _checkbox_checked(self, row, state):
3641		# If a row is checked, all un-checked children are updated as
3642		# well. For parent nodes we first check consistency of all
3643		# children before we check them.
3644
3645		# First synchronize down
3646		level = self[row][self.INDENT_COL]
3647		for i in range(row + 1, len(self)):
3648			if self[i][self.INDENT_COL] > level:
3649				if self[i][self.BULLET_COL] == UNCHECKED_BOX:
3650					self._change_bullet_type(i, state)
3651				else:
3652					# ignore non-checkbox bullet
3653					# ignore xchecked items etc.
3654					pass
3655			else:
3656				break
3657
3658		# Then go up, checking direct children for each parent
3659		# if children are inconsistent, do not change the parent
3660		# and break off updating parents. Do overwrite parents that
3661		# are already checked with a different type.
3662		for parent in self._parents(row):
3663			if self[parent][self.BULLET_COL] not in CHECKBOXES:
3664				continue # ignore non-checkbox bullet
3665
3666			consistent = True
3667			level = self[parent][self.INDENT_COL]
3668			for i in range(parent + 1, len(self)):
3669				if self[i][self.INDENT_COL] <= level:
3670					break
3671				elif self[i][self.INDENT_COL] == level + 1 \
3672				and self[i][self.BULLET_COL] in CHECKBOXES \
3673				and self[i][self.BULLET_COL] != state:
3674					consistent = False
3675					break
3676
3677			if consistent:
3678				self._change_bullet_type(parent, state)
3679			else:
3680				break
3681
3682	def _change_bullet_type(self, row, bullet):
3683		line, indent, _ = self[row]
3684		self.buffer.set_bullet(line, bullet)
3685		self[row] = (line, indent, bullet)
3686
3687	def _parents(self, row):
3688		# Collect row ids of parent nodes
3689		parents = []
3690		level = self[row][self.INDENT_COL]
3691		for i in range(row, -1, -1):
3692			if self[i][self.INDENT_COL] < level:
3693				parents.append(i)
3694				level = self[i][self.INDENT_COL]
3695		return parents
3696
3697
3698FIND_CASE_SENSITIVE = 1 #: Constant to find case sensitive
3699FIND_WHOLE_WORD = 2 #: Constant to find whole words only
3700FIND_REGEX = 4 #: Constant to find based on regexes
3701
3702class TextFinder(object):
3703	'''This class handles finding text in the L{TextBuffer}
3704
3705	Typically you should get an instance of this class from the
3706	L{TextBuffer.finder} attribute.
3707	'''
3708
3709	def __init__(self, textbuffer):
3710		'''constructor
3711
3712		@param textbuffer: a L{TextBuffer} object
3713		'''
3714		self.buffer = textbuffer
3715		self._signals = ()
3716		self.regex = None
3717		self.string = None
3718		self.flags = 0
3719		self.highlight = False
3720
3721		self.highlight_tag = self.buffer.create_tag(
3722			None, **self.buffer.tag_styles['find-highlight'])
3723		self.match_tag = self.buffer.create_tag(
3724			None, **self.buffer.tag_styles['find-match'])
3725
3726	def get_state(self):
3727		'''Get the query and any options. Used to copy the current state
3728		of find, can be restored later using L{set_state()}.
3729
3730		@returns: a 3-tuple of the search string, the option flags, and
3731		the highlight state
3732		'''
3733		return self.string, self.flags, self.highlight
3734
3735	def set_state(self, string, flags, highlight):
3736		'''Set the query and any options. Can be used to restore the
3737		state of a find action without triggering a find immediatly.
3738
3739		@param string: the text (or regex) to find
3740		@param flags: a combination of C{FIND_CASE_SENSITIVE},
3741		C{FIND_WHOLE_WORD} & C{FIND_REGEX}
3742		@param highlight: highlight state C{True} or C{False}
3743		'''
3744		if not string is None:
3745			self._parse_query(string, flags)
3746			self.set_highlight(highlight)
3747
3748	def find(self, string, flags=0):
3749		'''Find and select the next occurrence of a given string
3750
3751		@param string: the text (or regex) to find
3752		@param flags: options, a combination of:
3753			- C{FIND_CASE_SENSITIVE}: check case of matches
3754			- C{FIND_WHOLE_WORD}: only match whole words
3755			- C{FIND_REGEX}: input is a regular expression
3756		@returns: C{True} if a match was found
3757		'''
3758		self._parse_query(string, flags)
3759		#~ print('!! FIND "%s" (%s, %s)' % (self.regex.pattern, string, flags))
3760
3761		if self.highlight:
3762			self._update_highlight()
3763
3764		iter = self.buffer.get_insert_iter()
3765		return self._find_next(iter)
3766
3767	def _parse_query(self, string, flags):
3768		assert isinstance(string, str)
3769		self.string = string
3770		self.flags = flags
3771
3772		if not flags & FIND_REGEX:
3773			string = re.escape(string)
3774
3775		if flags & FIND_WHOLE_WORD:
3776			string = '\\b' + string + '\\b'
3777
3778		if flags & FIND_CASE_SENSITIVE:
3779			self.regex = re.compile(string, re.U)
3780		else:
3781			self.regex = re.compile(string, re.U | re.I)
3782
3783	def find_next(self):
3784		'''Skip to the next match and select it
3785
3786		@returns: C{True} if a match was found
3787		'''
3788		iter = self.buffer.get_insert_iter()
3789		iter.forward_char() # Skip current position
3790		return self._find_next(iter)
3791
3792	def _find_next(self, iter):
3793		# Common functionality between find() and find_next()
3794		# Looking for a match starting at iter
3795		if self.regex is None:
3796			self.unset_match()
3797			return False
3798
3799		line = iter.get_line()
3800		lastline = self.buffer.get_end_iter().get_line()
3801		for start, end, _ in self._check_range(line, lastline, 1):
3802			if start.compare(iter) == -1:
3803				continue
3804			else:
3805				self.set_match(start, end)
3806				return True
3807
3808		for start, end, _ in self._check_range(0, line, 1):
3809			self.set_match(start, end)
3810			return True
3811
3812		self.unset_match()
3813		return False
3814
3815	def find_previous(self):
3816		'''Go back to the previous match and select it
3817
3818		@returns: C{True} if a match was found
3819		'''
3820		if self.regex is None:
3821			self.unset_match()
3822			return False
3823
3824		iter = self.buffer.get_insert_iter()
3825		line = iter.get_line()
3826		lastline = self.buffer.get_end_iter().get_line()
3827		for start, end, _ in self._check_range(line, 0, -1):
3828			if start.compare(iter) != -1:
3829				continue
3830			else:
3831				self.set_match(start, end)
3832				return True
3833		for start, end, _ in self._check_range(lastline, line, -1):
3834			self.set_match(start, end)
3835			return True
3836
3837		self.unset_match()
3838		return False
3839
3840	def set_match(self, start, end):
3841		self._remove_tag()
3842
3843		self.buffer.apply_tag(self.match_tag, start, end)
3844		self.buffer.select_range(start, end)
3845
3846		self._signals = tuple(
3847			self.buffer.connect(s, self._remove_tag)
3848				for s in ('mark-set', 'changed'))
3849
3850	def unset_match(self):
3851		self._remove_tag()
3852		self.buffer.unset_selection()
3853
3854	def _remove_tag(self, *a):
3855		if len(a) > 2 and isinstance(a[2], Gtk.TextMark) \
3856		and a[2] is not self.buffer.get_insert():
3857			# mark-set signal, but not for cursor
3858			return
3859
3860		for id in self._signals:
3861			self.buffer.disconnect(id)
3862		self._signals = ()
3863		self.buffer.remove_tag(self.match_tag, *self.buffer.get_bounds())
3864
3865	def select_match(self):
3866		# Select last match
3867		bounds = self.match_bounds
3868		if not None in bounds:
3869			self.buffer.select_range(*bounds)
3870
3871	def set_highlight(self, highlight):
3872		'''Toggle highlighting of matches in the L{TextBuffer}
3873
3874		@param highlight: C{True} to enable highlighting, C{False} to
3875		disable
3876		'''
3877		self.highlight = highlight
3878		self._update_highlight()
3879		# TODO we could connect to buffer signals to update highlighting
3880		# when the buffer is modified.
3881
3882	def _update_highlight(self):
3883		# Clear highlighting
3884		tag = self.highlight_tag
3885		start, end = self.buffer.get_bounds()
3886		self.buffer.remove_tag(tag, start, end)
3887
3888		# Set highlighting
3889		if self.highlight:
3890			lastline = end.get_line()
3891			for start, end, _ in self._check_range(0, lastline, 1):
3892				self.buffer.apply_tag(tag, start, end)
3893
3894	def _check_range(self, firstline, lastline, step):
3895		# Generator for matches in a line. Arguments are start and
3896		# end line numbers and a step size (1 or -1). If the step is
3897		# negative results are yielded in reversed order. Yields pair
3898		# of TextIter's for begin and end of the match as well as the
3899		# match object.
3900		assert self.regex
3901		for line in range(firstline, lastline + step, step):
3902			start = self.buffer.get_iter_at_line(line)
3903			if start.ends_line():
3904				continue
3905
3906			end = start.copy()
3907			end.forward_to_line_end()
3908			text = start.get_slice(end)
3909			matches = self.regex.finditer(text)
3910			if step == -1:
3911				matches = list(matches)
3912				matches.reverse()
3913			for match in matches:
3914				startiter = self.buffer.get_iter_at_line_offset(
3915					line, match.start())
3916				enditer = self.buffer.get_iter_at_line_offset(
3917					line, match.end())
3918				yield startiter, enditer, match
3919
3920	def replace(self, string):
3921		'''Replace current match
3922
3923		@param string: the replacement string
3924
3925		In case of a regex find and replace the string will be expanded
3926		with terms from the regex.
3927
3928		@returns: C{True} is successful
3929		'''
3930		iter = self.buffer.get_insert_iter()
3931		if not self._find_next(iter):
3932			return False
3933
3934		iter = self.buffer.get_insert_iter()
3935		line = iter.get_line()
3936		for start, end, match in self._check_range(line, line, 1):
3937			if start.equal(iter):
3938				if self.flags & FIND_REGEX:
3939					string = match.expand(string)
3940
3941				offset = start.get_offset()
3942
3943				with self.buffer.user_action:
3944					self.buffer.select_range(start, end) # ensure editmode logic is used
3945					self.buffer.delete(start, end)
3946					self.buffer.insert_at_cursor(string)
3947
3948				start = self.buffer.get_iter_at_offset(offset)
3949				end = self.buffer.get_iter_at_offset(offset + len(string))
3950				self.buffer.select_range(start, end)
3951
3952				return True
3953		else:
3954			return False
3955
3956		self._update_highlight()
3957
3958	def replace_all(self, string):
3959		'''Replace all matched
3960
3961		Like L{replace()} but replaces all matches in the buffer
3962
3963		@param string: the replacement string
3964		@returns: C{True} is successful
3965		'''
3966		# Avoid looping when replace value matches query
3967
3968		matches = []
3969		orig = string
3970		lastline = self.buffer.get_end_iter().get_line()
3971		for start, end, match in self._check_range(0, lastline, 1):
3972			if self.flags & FIND_REGEX:
3973				string = match.expand(orig)
3974			matches.append((start.get_offset(), end.get_offset(), string))
3975
3976		matches.reverse() # work our way back top keep offsets valid
3977
3978		with self.buffer.user_action:
3979			for startoff, endoff, string in matches:
3980				start = self.buffer.get_iter_at_offset(startoff)
3981				end = self.buffer.get_iter_at_offset(endoff)
3982				if start.get_child_anchor() is not None:
3983					self._replace_in_widget(start, self.regex, string, True)
3984				else:
3985					self.buffer.delete(start, end)
3986					start = self.buffer.get_iter_at_offset(startoff)
3987					self.buffer.insert(start, string)
3988
3989		self._update_highlight()
3990
3991
3992CURSOR_TEXT = Gdk.Cursor.new_from_name(Gdk.Display.get_default(), 'text')
3993CURSOR_LINK = Gdk.Cursor.new_from_name(Gdk.Display.get_default(), 'pointer')
3994CURSOR_WIDGET = Gdk.Cursor.new_from_name(Gdk.Display.get_default(), 'default')
3995
3996
3997class TextView(Gtk.TextView):
3998	'''Widget to display a L{TextBuffer} with page content. Implements
3999	zim specific behavior like additional key bindings, on-mouse-over
4000	signals for links, and the custom popup menu.
4001
4002	@ivar preferences: dict with preferences
4003
4004	@signal: C{link-clicked (link)}: Emitted when the user clicks a link
4005	@signal: C{link-enter (link)}: Emitted when the mouse pointer enters a link
4006	@signal: C{link-leave (link)}: Emitted when the mouse pointer leaves a link
4007	@signal: C{end-of-word (start, end, word, char, editmode)}:
4008	Emitted when the user typed a character like space that ends a word
4009
4010	  - C{start}: a C{Gtk.TextIter} for the start of the word
4011	  - C{end}: a C{Gtk.TextIter} for the end of the word
4012	  - C{word}: the word as string
4013	  - C{char}: the character that caused the signal (a space, tab, etc.)
4014	  - C{editmode}: a list of constants for the formatting being in effect,
4015	    e.g. C{VERBATIM}
4016
4017	Plugins that want to add auto-formatting logic can connect to this
4018	signal. If the handler matches the word it should stop the signal
4019	with C{stop_emission()} to prevent other hooks from formatting the
4020	same word.
4021
4022	@signal: C{end-of-line (end)}: Emitted when the user typed a newline
4023	'''
4024
4025	# define signals we want to use - (closure type, return type and arg types)
4026	__gsignals__ = {
4027		# New signals
4028		'link-clicked': (GObject.SignalFlags.RUN_LAST, None, (object,)),
4029		'link-enter': (GObject.SignalFlags.RUN_LAST, None, (object,)),
4030		'link-leave': (GObject.SignalFlags.RUN_LAST, None, (object,)),
4031		'end-of-word': (GObject.SignalFlags.RUN_LAST, None, (object, object, object, object, object)),
4032		'end-of-line': (GObject.SignalFlags.RUN_LAST, None, (object,)),
4033	}
4034
4035	def __init__(self, preferences):
4036		'''Constructor
4037
4038		@param preferences: dict with preferences
4039
4040		@todo: make sure code sets proper defaults for preferences
4041		& document preferences used
4042		'''
4043		GObject.GObject.__init__(self)
4044		self.set_buffer(TextBuffer(None, None))
4045		self.set_name('zim-pageview')
4046		self.set_size_request(24, 24)
4047		self._cursor = CURSOR_TEXT
4048		self._cursor_link = None
4049		self._object_widgets = weakref.WeakSet()
4050		self.set_left_margin(10)
4051		self.set_right_margin(5)
4052		self.set_wrap_mode(Gtk.WrapMode.WORD)
4053		self.preferences = preferences
4054
4055		self._object_wrap_width = -1
4056		self.connect_after('size-allocate', self.__class__.on_size_allocate)
4057		self.connect_after('motion-notify-event', self.__class__.on_motion_notify_event)
4058
4059		# Tooltips for images
4060		self.props.has_tooltip = True
4061		self.connect("query-tooltip", self.on_query_tooltip)
4062
4063	def set_buffer(self, buffer):
4064		# Clear old widgets
4065		for child in self.get_children():
4066			if isinstance(child, InsertedObjectWidget):
4067				self._object_widgets.remove(child)
4068				self.remove(child)
4069
4070		# Set new buffer
4071		Gtk.TextView.set_buffer(self, buffer)
4072
4073		# Connect new widgets
4074		for anchor in buffer.list_objectanchors():
4075			self.on_insert_object(buffer, anchor)
4076
4077		buffer.connect('insert-objectanchor', self.on_insert_object)
4078
4079	def on_insert_object(self, buffer, anchor):
4080		# Connect widget for this view to object
4081		widget = anchor.create_widget()
4082		assert isinstance(widget, InsertedObjectWidget)
4083
4084		def on_release_cursor(widget, position, anchor):
4085			myiter = buffer.get_iter_at_child_anchor(anchor)
4086			if position == POSITION_END:
4087				myiter.forward_char()
4088			buffer.place_cursor(myiter)
4089			self.grab_focus()
4090
4091		widget.connect('release-cursor', on_release_cursor, anchor)
4092
4093		def widget_connect(signal):
4094			widget.connect(signal, lambda o, *a: self.emit(signal, *a))
4095
4096		for signal in ('link-clicked', 'link-enter', 'link-leave'):
4097			widget_connect(signal)
4098
4099		widget.set_textview_wrap_width(self._object_wrap_width)
4100			# TODO - compute indenting
4101
4102		self.add_child_at_anchor(widget, anchor)
4103		self._object_widgets.add(widget)
4104		widget.show_all()
4105
4106	def on_size_allocate(self, *a):
4107		# Update size request for widgets
4108		wrap_width = self._get_object_wrap_width()
4109		if wrap_width != self._object_wrap_width:
4110			for widget in self._object_widgets:
4111				widget.set_textview_wrap_width(wrap_width)
4112					# TODO - compute indenting
4113			self._object_wrap_width = wrap_width
4114
4115	def _get_object_wrap_width(self):
4116		text_window = self.get_window(Gtk.TextWindowType.TEXT)
4117		if text_window:
4118			width = text_window.get_geometry()[2]
4119			hmargin = self.get_left_margin() + self.get_right_margin() + 5
4120				# the +5 is arbitrary, but without it we show a scrollbar anyway ..
4121			return width - hmargin
4122		else:
4123			return -1
4124
4125	def do_copy_clipboard(self, format=None):
4126		# Overriden to force usage of our Textbuffer.copy_clipboard
4127		# over Gtk.TextBuffer.copy_clipboard
4128		format = format or self.preferences['copy_format']
4129		format = zim.formats.canonical_name(format)
4130		self.get_buffer().copy_clipboard(Clipboard, format)
4131
4132	def do_cut_clipboard(self):
4133		# Overriden to force usage of our Textbuffer.cut_clipboard
4134		# over Gtk.TextBuffer.cut_clipboard
4135		self.get_buffer().cut_clipboard(Clipboard, self.get_editable())
4136		self.scroll_mark_onscreen(self.get_buffer().get_insert())
4137
4138	def do_paste_clipboard(self, format=None):
4139		# Overriden to force usage of our Textbuffer.paste_clipboard
4140		# over Gtk.TextBuffer.paste_clipboard
4141		self.get_buffer().paste_clipboard(Clipboard, None, self.get_editable(), text_format=format)
4142		self.scroll_mark_onscreen(self.get_buffer().get_insert())
4143
4144	#~ def do_drag_motion(self, context, *a):
4145		#~ # Method that echos drag data types - only enable for debugging
4146		#~ print context.targets
4147
4148	def on_motion_notify_event(self, event):
4149		# Update the cursor type when the mouse moves
4150		x, y = event.get_coords()
4151		x, y = int(x), int(y) # avoid some strange DeprecationWarning
4152		coords = self.window_to_buffer_coords(Gtk.TextWindowType.WIDGET, x, y)
4153		self.update_cursor(coords)
4154
4155	def do_visibility_notify_event(self, event):
4156		# Update the cursor type when the window visibility changed
4157		self.update_cursor()
4158		return False # continue emit
4159
4160	def do_move_cursor(self, step_size, count, extend_selection):
4161		# Overloaded signal handler for cursor movements which will
4162		# move cursor into any object that accept a cursor focus
4163
4164		if step_size in (Gtk.MovementStep.LOGICAL_POSITIONS, Gtk.MovementStep.VISUAL_POSITIONS) \
4165		and count in (1, -1) and not extend_selection:
4166			# logic below only supports 1 char forward or 1 char backward movements
4167
4168			buffer = self.get_buffer()
4169			iter = buffer.get_iter_at_mark(buffer.get_insert())
4170			if count == -1:
4171				iter.backward_char()
4172				position = POSITION_END # enter end of object
4173			else:
4174				position = POSITION_BEGIN
4175
4176			anchor = iter.get_child_anchor()
4177			if iter.get_child_anchor():
4178				widgets = anchor.get_widgets()
4179				assert len(widgets) == 1, 'TODO: support multiple views of same buffer'
4180				widget = widgets[0]
4181				if widget.has_cursor():
4182					widget.grab_cursor(position)
4183					return None
4184
4185		return Gtk.TextView.do_move_cursor(self, step_size, count, extend_selection)
4186
4187	def do_button_press_event(self, event):
4188		# Handle middle click for pasting and right click for context menu
4189		# Needed to override these because implementation details of
4190		# gtktextview.c do not use proper signals for these actions.
4191		#
4192		# Note that clicking links is in button-release to avoid
4193		# conflict with making selections
4194		buffer = self.get_buffer()
4195
4196		if event.type == Gdk.EventType.BUTTON_PRESS:
4197			iter, coords = self._get_pointer_location()
4198			if event.button == 2 and iter and not buffer.get_has_selection():
4199				buffer.paste_clipboard(SelectionClipboard, iter, self.get_editable())
4200				return False
4201			elif Gdk.Event.triggers_context_menu(event):
4202				self._set_popup_menu_mark(iter) # allow iter to be None
4203
4204		return Gtk.TextView.do_button_press_event(self, event)
4205
4206	def do_button_release_event(self, event):
4207		# Handle clicking a link or checkbox
4208		cont = Gtk.TextView.do_button_release_event(self, event)
4209		if not self.get_buffer().get_has_selection():
4210			if self.get_editable():
4211				if event.button == 1:
4212					if self.preferences['cycle_checkbox_type']:
4213						# Cycle through all states - more useful for
4214						# single click input devices
4215						self.click_link() or self.click_checkbox() or self.click_anchor()
4216					else:
4217						self.click_link() or self.click_checkbox(CHECKED_BOX) or self.click_anchor()
4218			elif event.button == 1:
4219				# no changing checkboxes for read-only content
4220				self.click_link()
4221
4222		return cont # continue emit ?
4223
4224	def do_popup_menu(self):
4225		# Handler that gets called when user activates the popup-menu
4226		# by a keybinding (Shift-F10 or "menu" key).
4227		# Due to implementation details in gtktextview.c this method is
4228		# not called when a popup is triggered by a mouse click.
4229		buffer = self.get_buffer()
4230		iter = buffer.get_iter_at_mark(buffer.get_insert())
4231		self._set_popup_menu_mark(iter)
4232		return Gtk.TextView.do_popup_menu(self)
4233
4234	def get_popup(self):
4235		'''Get the popup menu - intended for testing'''
4236		buffer = self.get_buffer()
4237		iter = buffer.get_iter_at_mark(buffer.get_insert())
4238		self._set_popup_menu_mark(iter)
4239		menu = Gtk.Menu()
4240		self.emit('populate-popup', menu)
4241		return menu
4242
4243	def _set_popup_menu_mark(self, iter):
4244		# If iter is None, remove the mark
4245		buffer = self.get_buffer()
4246		mark = buffer.get_mark('zim-popup-menu')
4247		if iter:
4248			if mark:
4249				buffer.move_mark(mark, iter)
4250			else:
4251				mark = buffer.create_mark('zim-popup-menu', iter, True)
4252		elif mark:
4253			buffer.delete_mark(mark)
4254		else:
4255			pass
4256
4257	def _get_popup_menu_mark(self):
4258		buffer = self.get_buffer()
4259		mark = buffer.get_mark('zim-popup-menu')
4260		return buffer.get_iter_at_mark(mark) if mark else None
4261
4262	def do_key_press_event(self, event):
4263		keyval = strip_boolean_result(event.get_keyval())
4264		#print 'KEY %s (%r)' % (Gdk.keyval_name(keyval), keyval)
4265		event_state = event.get_state()
4266		#print 'STATE %s' % event_state
4267
4268		run_post, handled = self._do_key_press_event(keyval, event_state)
4269		if not handled:
4270			handled = Gtk.TextView.do_key_press_event(self, event)
4271
4272		if run_post and handled:
4273			self._post_key_press_event(keyval)
4274
4275		return handled
4276
4277	def test_key_press_event(self, keyval, event_state=0):
4278		run_post, handled = self._do_key_press_event(keyval, event_state)
4279
4280		if not handled:
4281			if keyval in KEYVALS_BACKSPACE:
4282				self.emit('backspace')
4283			else:
4284				if keyval in KEYVALS_ENTER:
4285					char = '\n'
4286				elif keyval in KEYVALS_TAB:
4287					char = '\t'
4288				else:
4289					char = chr(Gdk.keyval_to_unicode(keyval))
4290
4291				self.emit('insert-at-cursor', char)
4292			handled = True
4293
4294		if run_post and handled:
4295			self._post_key_press_event(keyval)
4296
4297		return handled
4298
4299	def _do_key_press_event(self, keyval, event_state):
4300		buffer = self.get_buffer()
4301		if not self.get_editable():
4302			# Dispatch read-only mode
4303			return False, self._do_key_press_event_readonly(keyval, event_state)
4304		elif buffer.get_has_selection():
4305			# Dispatch selection mode
4306			return False, self._do_key_press_event_selection(keyval, event_state)
4307		else:
4308			return True, self._do_key_press_event_default(keyval, event_state)
4309
4310	def _do_key_press_event_default(self, keyval, event_state):
4311		buffer = self.get_buffer()
4312		if (keyval in KEYVALS_HOME
4313		and not event_state & Gdk.ModifierType.CONTROL_MASK):
4314			# Smart Home key - can be combined with shift state
4315			insert = buffer.get_iter_at_mark(buffer.get_insert())
4316			home, ourhome = self.get_visual_home_positions(insert)
4317			if insert.equal(ourhome):
4318				iter = home
4319			else:
4320				iter = ourhome
4321			if event_state & Gdk.ModifierType.SHIFT_MASK:
4322				buffer.move_mark_by_name('insert', iter)
4323			else:
4324				buffer.place_cursor(iter)
4325			return True
4326		elif keyval in KEYVALS_TAB and not (event_state & KEYSTATES):
4327			# Tab at start of line indents
4328			iter = buffer.get_insert_iter()
4329			home, ourhome = self.get_visual_home_positions(iter)
4330			if home.starts_line() and iter.compare(ourhome) < 1 \
4331			and not list(filter(_is_pre_tag, iter.get_tags())):
4332				row, mylist = TextBufferList.new_from_line(buffer, iter.get_line())
4333				if mylist and self.preferences['recursive_indentlist']:
4334					mylist.indent(row)
4335				else:
4336					buffer.indent(iter.get_line(), interactive=True)
4337				return True
4338		elif (keyval in KEYVALS_LEFT_TAB
4339			and not (event_state & KEYSTATES & ~Gdk.ModifierType.SHIFT_MASK)
4340		) or (keyval in KEYVALS_BACKSPACE
4341			and self.preferences['unindent_on_backspace']
4342			and not (event_state & KEYSTATES)
4343		):
4344			# Backspace or Ctrl-Tab unindents line
4345			# note that Shift-Tab give Left_Tab + Shift mask, so allow shift
4346			default = True if keyval in KEYVALS_LEFT_TAB else False
4347				# Prevent <Shift><Tab> to insert a Tab if unindent fails
4348			iter = buffer.get_iter_at_mark(buffer.get_insert())
4349			home, ourhome = self.get_visual_home_positions(iter)
4350			if home.starts_line() and iter.compare(ourhome) < 1 \
4351			and not list(filter(_is_pre_tag, iter.get_tags())):
4352				bullet = buffer.get_bullet_at_iter(home)
4353				indent = buffer.get_indent(home.get_line())
4354				if keyval in KEYVALS_BACKSPACE \
4355				and bullet and indent == 0 and not iter.equal(home):
4356					# Delete bullet at start of line (if iter not before bullet)
4357					buffer.delete(home, ourhome)
4358					return True
4359				elif indent == 0 or indent is None:
4360					# Nothing to unindent
4361					return default
4362				elif bullet:
4363					# Unindent list maybe recursive
4364					row, mylist = TextBufferList.new_from_line(buffer, iter.get_line())
4365					if mylist and self.preferences['recursive_indentlist']:
4366						return bool(mylist.unindent(row)) or default
4367					else:
4368						return bool(buffer.unindent(iter.get_line(), interactive=True)) or default
4369				else:
4370					# Unindent normal text
4371					return bool(buffer.unindent(iter.get_line(), interactive=True)) or default
4372
4373		elif keyval in KEYVALS_ENTER:
4374			# Enter can trigger links
4375			iter = buffer.get_iter_at_mark(buffer.get_insert())
4376			tag = buffer.get_link_tag(iter)
4377			if tag and not iter.begins_tag(tag):
4378				# get_link_tag() is left gravitating, we additionally
4379				# exclude the position in front of the link.
4380				# As a result you can not "Enter" a 1 character link,
4381				# this is by design.
4382				if (self.preferences['follow_on_enter']
4383				or event_state & Gdk.ModifierType.MOD1_MASK): # MOD1 == Alt
4384					self.click_link_at_iter(iter)
4385				# else do not insert newline, just ignore
4386				return True
4387
4388	def _post_key_press_event(self, keyval):
4389		# Trigger end-of-line and/or end-of-word signals if char was
4390		# really inserted by parent class.
4391		#
4392		# We do it this way because in some cases e.g. a space is not
4393		# inserted but is used to select an option in an input mode e.g.
4394		# to select between various Chinese characters. See lp:460438
4395
4396		if not (keyval in KEYVALS_END_OF_WORD or keyval in KEYVALS_ENTER):
4397			return
4398
4399		buffer = self.get_buffer()
4400		insert = buffer.get_iter_at_mark(buffer.get_insert())
4401		mark = buffer.create_mark(None, insert, left_gravity=False)
4402		iter = insert.copy()
4403		iter.backward_char()
4404
4405		if keyval in KEYVALS_ENTER:
4406			char = '\n'
4407		elif keyval in KEYVALS_TAB:
4408			char = '\t'
4409		else:
4410			char = chr(Gdk.keyval_to_unicode(keyval))
4411
4412		if iter.get_text(insert) != char:
4413			return
4414
4415		with buffer.user_action:
4416			buffer.emit('undo-save-cursor', insert)
4417			start = iter.copy()
4418			if buffer.iter_backward_word_start(start):
4419				word = start.get_text(iter)
4420				editmode = [t.zim_tag for t in buffer.iter_get_zim_tags(iter)]
4421				self.emit('end-of-word', start, iter, word, char, editmode)
4422
4423			if keyval in KEYVALS_ENTER:
4424				# iter may be invalid by now because of end-of-word
4425				iter = buffer.get_iter_at_mark(mark)
4426				iter.backward_char()
4427				self.emit('end-of-line', iter)
4428
4429		buffer.place_cursor(buffer.get_iter_at_mark(mark))
4430		self.scroll_mark_onscreen(mark)
4431		buffer.delete_mark(mark)
4432
4433	def _do_key_press_event_readonly(self, keyval, event_state):
4434		# Key bindings in read-only mode:
4435		#   Space scrolls one page
4436		#   Shift-Space scrolls one page up
4437		if keyval in KEYVALS_SPACE:
4438			if event_state & Gdk.ModifierType.SHIFT_MASK:
4439				i = -1
4440			else:
4441				i = 1
4442			self.emit('move-cursor', Gtk.MovementStep.PAGES, i, False)
4443			return True
4444		else:
4445			return False
4446
4447	def _do_key_press_event_selection(self, keyval, event_state):
4448		# Key bindings when there is an active selections:
4449		#   Tab indents whole selection
4450		#   Shift-Tab and optionally Backspace unindent whole selection
4451		#   * Turns whole selection in bullet list, or toggle back
4452		#   > Quotes whole selection with '>'
4453		handled = True
4454		buffer = self.get_buffer()
4455
4456		def delete_char(line):
4457			# Deletes the character at the iterator position
4458			iter = buffer.get_iter_at_line(line)
4459			next = iter.copy()
4460			if next.forward_char():
4461				buffer.delete(iter, next)
4462
4463		def decrement_indent(start, end):
4464			# Check if inside verbatim block AND entire selection without tag toggle
4465			if selection_in_pre_block(start, end):
4466				# Handle indent in pre differently
4467				missing_tabs = []
4468				check_tab = lambda l: (buffer.get_iter_at_line(l).get_char() == '\t') or missing_tabs.append(1)
4469				buffer.foreach_line_in_selection(check_tab)
4470				if len(missing_tabs) == 0:
4471					return buffer.foreach_line_in_selection(delete_char)
4472				else:
4473					return False
4474			elif multi_line_indent(start, end):
4475				level = []
4476				buffer.foreach_line_in_selection(
4477					lambda l: level.append(buffer.get_indent(l)))
4478				if level and min(level) > 0:
4479					# All lines have some indent
4480					return buffer.foreach_line_in_selection(buffer.unindent)
4481				else:
4482					return False
4483			else:
4484				return False
4485
4486		def selection_in_pre_block(start, end):
4487			# Checks if there are any tag changes within the selection
4488			if list(filter(_is_pre_tag, start.get_tags())):
4489				toggle = start.copy()
4490				toggle.forward_to_tag_toggle(None)
4491				return toggle.compare(end) < 0
4492			else:
4493				return False
4494
4495		def multi_line_indent(start, end):
4496			# Check if:
4497			# a) one line selected from start till end or
4498			# b) multiple lines selected and selection starts at line start
4499			home, ourhome = self.get_visual_home_positions(start)
4500			if not (home.starts_line() and start.compare(ourhome) < 1):
4501				return False
4502			else:
4503				return end.ends_line() \
4504				or end.get_line() > start.get_line()
4505
4506		start, end = buffer.get_selection_bounds()
4507		with buffer.user_action:
4508			if keyval in KEYVALS_TAB:
4509				if selection_in_pre_block(start, end):
4510					# Handle indent in pre differently
4511					prepend_tab = lambda l: buffer.insert(buffer.get_iter_at_line(l), '\t')
4512					buffer.foreach_line_in_selection(prepend_tab)
4513				elif multi_line_indent(start, end):
4514					buffer.foreach_line_in_selection(buffer.indent)
4515				else:
4516					handled = False
4517			elif keyval in KEYVALS_LEFT_TAB:
4518				decrement_indent(start, end)
4519					# do not set handled = False when decrement failed -
4520					# LEFT_TAB should not do anything else
4521			elif keyval in KEYVALS_BACKSPACE \
4522			and self.preferences['unindent_on_backspace']:
4523				handled = decrement_indent(start, end)
4524			elif keyval in KEYVALS_ASTERISK + (KEYVAL_POUND,):
4525				def toggle_bullet(line, newbullet):
4526					bullet = buffer.get_bullet(line)
4527					if not bullet and not buffer.get_line_is_empty(line):
4528						buffer.set_bullet(line, newbullet)
4529					elif bullet == newbullet: # FIXME broken for numbered list
4530						buffer.set_bullet(line, None)
4531				if keyval == KEYVAL_POUND:
4532					buffer.foreach_line_in_selection(toggle_bullet, NUMBER_BULLET)
4533				else:
4534					buffer.foreach_line_in_selection(toggle_bullet, BULLET)
4535			elif keyval in KEYVALS_GT \
4536			and multi_line_indent(start, end):
4537				def email_quote(line):
4538					iter = buffer.get_iter_at_line(line)
4539					bound = iter.copy()
4540					bound.forward_char()
4541					if iter.get_text(bound) == '>':
4542						buffer.insert(iter, '>')
4543					else:
4544						buffer.insert(iter, '> ')
4545				buffer.foreach_line_in_selection(email_quote)
4546			else:
4547				handled = False
4548
4549		return handled
4550
4551	def _get_pointer_location(self):
4552		'''Get an iter and coordinates for the mouse pointer
4553
4554		@returns: a 2-tuple of a C{Gtk.TextIter} and a C{(x, y)}
4555		tupple with coordinates for the mouse pointer.
4556		'''
4557		x, y = self.get_pointer()
4558		x, y = self.window_to_buffer_coords(Gtk.TextWindowType.WIDGET, x, y)
4559		iter = strip_boolean_result(self.get_iter_at_location(x, y))
4560		return iter, (x, y)
4561
4562	def _get_pixbuf_at_pointer(self, iter, coords):
4563		'''Returns the pixbuf that is under the mouse or C{None}. The
4564		parameters should be the TextIter and the (x, y) coordinates
4565		from L{_get_pointer_location()}. This method handles the special
4566		case where the pointer it on an iter next to the image but the
4567		mouse is visible above the image.
4568		'''
4569		pixbuf = iter.get_pixbuf()
4570		if not pixbuf:
4571			# right side of pixbuf will map to next iter
4572			iter = iter.copy()
4573			iter.backward_char()
4574			pixbuf = iter.get_pixbuf()
4575
4576		if pixbuf and hasattr(pixbuf, 'zim_type'):
4577			# If we have a pixbuf double check the cursor is really
4578			# over the image and not actually on the next cursor position
4579			area = self.get_iter_location(iter)
4580			if (coords[0] >= area.x and coords[0] <= area.x + area.width
4581				and coords[1] >= area.y and coords[1] <= area.y + area.height):
4582				return pixbuf
4583			else:
4584				return None
4585		else:
4586			return None
4587
4588	def update_cursor(self, coords=None):
4589		'''Update the mouse cursor type
4590
4591		E.g. set a "hand" cursor when hovering over a link.
4592
4593		@param coords: a tuple with C{(x, y)} position in buffer coords.
4594		Only give this argument if coords are known from an event,
4595		otherwise the current cursor position is used.
4596
4597		@emits: link-enter
4598		@emits: link-leave
4599		'''
4600		if coords is None:
4601			iter, coords = self._get_pointer_location()
4602		else:
4603			iter = strip_boolean_result(self.get_iter_at_location(*coords))
4604
4605		if iter is None:
4606			self._set_cursor(CURSOR_TEXT)
4607		else:
4608			pixbuf = self._get_pixbuf_at_pointer(iter, coords)
4609			if pixbuf:
4610				if pixbuf.zim_type == 'icon' and pixbuf.zim_attrib['stock'] in bullets:
4611					self._set_cursor(CURSOR_WIDGET)
4612				elif pixbuf.zim_type == 'anchor':
4613					self._set_cursor(CURSOR_WIDGET)
4614				elif 'href' in pixbuf.zim_attrib:
4615					self._set_cursor(CURSOR_LINK, link={'href': pixbuf.zim_attrib['href']})
4616				else:
4617					self._set_cursor(CURSOR_TEXT)
4618			else:
4619				link = self.get_buffer().get_link_data(iter)
4620				if link:
4621					self._set_cursor(CURSOR_LINK, link=link)
4622				else:
4623					self._set_cursor(CURSOR_TEXT)
4624
4625	def _set_cursor(self, cursor, link=None):
4626		if cursor != self._cursor:
4627			window = self.get_window(Gtk.TextWindowType.TEXT)
4628			window.set_cursor(cursor)
4629
4630		# Check if we need to emit any events for hovering
4631		if self._cursor == CURSOR_LINK: # was over link before
4632			if cursor == CURSOR_LINK: # still over link
4633				if link != self._cursor_link:
4634					# but other link
4635					self.emit('link-leave', self._cursor_link)
4636					self.emit('link-enter', link)
4637			else:
4638				self.emit('link-leave', self._cursor_link)
4639		elif cursor == CURSOR_LINK: # was not over link, but is now
4640			self.emit('link-enter', link)
4641
4642		self._cursor = cursor
4643		self._cursor_link = link
4644
4645	def click_link(self):
4646		'''Activate the link under the mouse pointer, if any
4647
4648		@emits: link-clicked
4649		@returns: C{True} when there was indeed a link
4650		'''
4651		iter, coords = self._get_pointer_location()
4652		if iter is None:
4653			return False
4654
4655		pixbuf = self._get_pixbuf_at_pointer(iter, coords)
4656		if pixbuf and pixbuf.zim_attrib.get('href'):
4657			self.emit('link-clicked', {'href': pixbuf.zim_attrib['href']})
4658			return True
4659		elif iter:
4660			return self.click_link_at_iter(iter)
4661
4662	def click_link_at_iter(self, iter):
4663		'''Activate the link at C{iter}, if any
4664
4665		Like L{click_link()} but activates a link at a specific text
4666		iter location
4667
4668		@emits: link-clicked
4669		@param iter: a C{Gtk.TextIter}
4670		@returns: C{True} when there was indeed a link
4671		'''
4672		link = self.get_buffer().get_link_data(iter)
4673		if link:
4674			self.emit('link-clicked', link)
4675			return True
4676		else:
4677			return False
4678
4679	def click_checkbox(self, checkbox_type=None):
4680		'''Toggle the checkbox under the mouse pointer, if any
4681
4682		@param checkbox_type: the checkbox type to toggle between, see
4683		L{TextBuffer.toggle_checkbox()} for details.
4684		@returns: C{True} for success, C{False} if no checkbox was found.
4685		'''
4686		iter, coords = self._get_pointer_location()
4687		if iter and iter.get_line_offset() < 2:
4688			# Only position 0 or 1 can map to a checkbox
4689			buffer = self.get_buffer()
4690			recurs = self.preferences['recursive_checklist']
4691			return buffer.toggle_checkbox(iter.get_line(), checkbox_type, recurs)
4692		else:
4693			return False
4694
4695	def click_anchor(self):
4696		'''Show popover for anchor under the cursor'''
4697		iter, coords = self._get_pointer_location()
4698		if not iter:
4699			return False
4700
4701		pixbuf = self._get_pixbuf_at_pointer(iter, coords)
4702		if not (pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'anchor'):
4703			return False
4704
4705		# Show popover with achor name and option to copy link
4706		popover = Gtk.Popover()
4707		popover.set_relative_to(self)
4708		rect = Gdk.Rectangle()
4709		rect.x, rect.y = self.get_pointer()
4710		rect.width, rect.height = 1, 1
4711		popover.set_pointing_to(rect)
4712
4713		name =  pixbuf.zim_attrib['name']
4714		def _copy_link_to_anchor(o):
4715			buffer = self.get_buffer()
4716			notebook, page = buffer.notebook, buffer.page
4717			Clipboard.set_pagelink(notebook, page, name)
4718			SelectionClipboard.set_pagelink(notebook, page, name)
4719			popover.popdown()
4720
4721		hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, 12)
4722		hbox.set_border_width(3)
4723		label = Gtk.Label()
4724		label.set_markup('#%s' %name)
4725		hbox.add(label)
4726		button = Gtk.Button.new_from_icon_name('edit-copy-symbolic', Gtk.IconSize.BUTTON)
4727		button.set_tooltip_text(_("Copy link to clipboard")) # T: tooltip for button in anchor popover
4728		button.connect('clicked', _copy_link_to_anchor)
4729		hbox.add(button)
4730		popover.add(hbox)
4731		popover.show_all()
4732		popover.popup()
4733
4734		return True
4735
4736	def get_visual_home_positions(self, iter):
4737		'''Get the TextIters for the visuale start of the line
4738
4739		@param iter: a C{Gtk.TextIter}
4740		@returns: a 2-tuple with two C{Gtk.TextIter}
4741
4742		The first iter is the start of the visual line - which can be
4743		the start of the line as the buffer sees it (which is also called
4744		the paragraph start in the view) or the iter at the place where
4745		the line is wrapped. The second iter is the start of the line
4746		after skipping any bullets and whitespace. For a wrapped line
4747		the second iter will be the same as the first.
4748		'''
4749		home = iter.copy()
4750		if not self.starts_display_line(home):
4751			self.backward_display_line_start(home)
4752
4753		if home.starts_line():
4754			ourhome = home.copy()
4755			self.get_buffer().iter_forward_past_bullet(ourhome)
4756			bound = ourhome.copy()
4757			bound.forward_char()
4758			while ourhome.get_text(bound) in (' ', '\t'):
4759				if ourhome.forward_char():
4760					bound.forward_char()
4761				else:
4762					break
4763			return home, ourhome
4764		else:
4765			# only start visual line, not start of real line
4766			return home, home.copy()
4767
4768	def do_end_of_word(self, start, end, word, char, editmode):
4769		# Default handler with built-in auto-formatting options
4770		buffer = self.get_buffer()
4771		handled = False
4772		#print('WORD >>%s<< CHAR >>%s<<' % (word, char))
4773
4774		def apply_anchor(match):
4775			#print("ANCHOR >>%s<<" % word)
4776			if buffer.range_has_tags(_is_non_nesting_tag, start, end):
4777				return False
4778			name = match[2:]
4779			buffer.delete(start, end)
4780			buffer.insert_anchor(start, name)
4781			return True
4782
4783		def apply_tag(match):
4784			#print("TAG >>%s<<" % word)
4785			start = end.copy()
4786			if not start.backward_chars(len(match)):
4787				return False
4788			elif buffer.range_has_tags(_is_non_nesting_tag, start, end):
4789				return False
4790			else:
4791				tag = buffer._create_tag_tag(match)
4792				buffer.apply_tag(tag, start, end)
4793				return True
4794
4795		def apply_link(match, offset_end=0):
4796			#print("LINK >>%s<<" % word)
4797			myend = end.copy()
4798			myend.backward_chars(offset_end)
4799			start = myend.copy()
4800			if not start.backward_chars(len(match)):
4801				return False
4802			elif buffer.range_has_tags(_is_non_nesting_tag, start, myend) \
4803				or buffer.range_has_tags(_is_link_tag, start, myend):
4804					return False # No link inside a link
4805			else:
4806				tag = buffer._create_link_tag(match, match)
4807				buffer.apply_tag(tag, start, myend)
4808				return True
4809
4810		def allow_bullet(iter, is_replacement_numbered_bullet):
4811			if iter.starts_line():
4812				return True
4813			elif iter.get_line_offset() < 10:
4814				home = buffer.get_iter_at_line(iter.get_line())
4815				return buffer.iter_forward_past_bullet(home) \
4816				and start.equal(iter) \
4817				and not is_replacement_numbered_bullet # don't replace existing bullets with numbered bullets
4818			else:
4819				return False
4820		word_is_numbered_bullet = is_numbered_bullet_re.match(word)
4821		if (char == ' ' or char == '\t') \
4822		and allow_bullet(start, word_is_numbered_bullet) \
4823		and (word in autoformat_bullets or word_is_numbered_bullet):
4824			if buffer.range_has_tags(_is_heading_tag, start, end):
4825				handled = False # No bullets in headings
4826			else:
4827				# format bullet and checkboxes
4828				line = start.get_line()
4829				end.forward_char() # also overwrite the space triggering the action
4830				buffer.delete(start, end)
4831				bullet = autoformat_bullets.get(word) or word
4832				buffer.set_bullet(line, bullet) # takes care of replacing bullets as well
4833				handled = True
4834		elif tag_re.match(word):
4835			handled = apply_tag(tag_re[0])
4836		elif anchor_re.match(word):
4837			handled = apply_anchor(anchor_re[0])
4838		elif url_re.search(word):
4839			if char == ')':
4840				handled = False # to early to call
4841			else:
4842				m = url_re.search(word)
4843				url = match_url(m.group(0))
4844				tail = word[m.start()+len(url):]
4845				handled = apply_link(url, offset_end=len(tail))
4846		elif link_to_anchor_re.match(word):
4847			handled = apply_link(link_to_anchor_re[0])
4848		elif link_to_page_re.match(word):
4849			# Do not link "10:20h", "10:20PM" etc. so check two letters before first ":"
4850			w = word.strip(':').split(':')
4851			if w and twoletter_re.search(w[0]):
4852				handled = apply_link(link_to_page_re[0])
4853			else:
4854				handled = False
4855		elif interwiki_re.match(word):
4856			handled = apply_link(interwiki_re[0])
4857		elif self.preferences['autolink_files'] and file_re.match(word):
4858			handled = apply_link(file_re[0])
4859		elif self.preferences['autolink_camelcase'] and camelcase(word):
4860			handled = apply_link(word)
4861		elif self.preferences['auto_reformat']:
4862			linestart = buffer.get_iter_at_line(end.get_line())
4863			partial_line = linestart.get_slice(end)
4864			for style, style_re in markup_re:
4865				m = style_re.search(partial_line)
4866				if m:
4867					matchstart = linestart.copy()
4868					matchstart.forward_chars(m.start())
4869					matchend = linestart.copy()
4870					matchend.forward_chars(m.end())
4871					if buffer.range_has_tags(_is_non_nesting_tag, matchstart, matchend) \
4872						or buffer.range_has_tags(_is_link_tag_without_href, matchstart, matchend):
4873							break
4874					else:
4875						with buffer.tmp_cursor(matchstart):
4876							buffer.delete(matchstart, matchend)
4877							buffer.insert_with_tags_by_name(matchstart, m.group(1), style)
4878							handled = True
4879							break
4880
4881		if handled:
4882			self.stop_emission('end-of-word')
4883
4884	def do_end_of_line(self, end):
4885		# Default handler, takes care of cutting of formatting on the
4886		# line end, set indenting and bullet items on the new line etc.
4887
4888		if end.starts_line():
4889			return # empty line
4890
4891		buffer = self.get_buffer()
4892		start = buffer.get_iter_at_line(end.get_line())
4893		if any(_is_pre_or_code_tag(t) for t in start.get_tags()):
4894			logger.debug('pre-formatted code')
4895			return # pre-formatted
4896
4897		line = start.get_text(end)
4898		#~ print('LINE >>%s<<' % line)
4899
4900		if heading_re.match(line):
4901			level = len(heading_re[1]) - 1
4902			heading = heading_re[2]
4903			mark = buffer.create_mark(None, end)
4904			buffer.delete(start, end)
4905			buffer.insert_with_tags_by_name(
4906				buffer.get_iter_at_mark(mark), heading, 'style-h' + str(level))
4907			buffer.delete_mark(mark)
4908		elif is_line(line):
4909			with buffer.user_action:
4910				offset = start.get_offset()
4911				buffer.delete(start, end)
4912				iter = buffer.get_iter_at_offset(offset)
4913				buffer.insert_objectanchor(iter, LineSeparatorAnchor())
4914		elif buffer.get_bullet_at_iter(start) is not None:
4915			# we are part of bullet list
4916			# FIXME should logic be handled by TextBufferList ?
4917			ourhome = start.copy()
4918			buffer.iter_forward_past_bullet(ourhome)
4919			newlinestart = end.copy()
4920			newlinestart.forward_line()
4921			if ourhome.equal(end) and newlinestart.ends_line():
4922				# line with bullet but no text - break list if no text on next line
4923				line, newline = start.get_line(), newlinestart.get_line()
4924				buffer.delete(start, end)
4925				buffer.set_indent(line, None)
4926				buffer.set_indent(newline, None)
4927			else:
4928				# determine indent
4929				start_sublist = False
4930				newline = newlinestart.get_line()
4931				indent = buffer.get_indent(start.get_line())
4932				nextlinestart = newlinestart.copy()
4933				if nextlinestart.forward_line() \
4934				and buffer.get_bullet_at_iter(nextlinestart):
4935					nextindent = buffer.get_indent(nextlinestart.get_line())
4936					if nextindent >= indent:
4937						# we are at the head of a sublist
4938						indent = nextindent
4939						start_sublist = True
4940
4941				# add bullet on new line
4942				bulletiter = nextlinestart if start_sublist else start # Either look back or look forward
4943				bullet = buffer.get_bullet_at_iter(bulletiter)
4944				if bullet in (CHECKED_BOX, XCHECKED_BOX, MIGRATED_BOX, TRANSMIGRATED_BOX):
4945					bullet = UNCHECKED_BOX
4946				elif is_numbered_bullet_re.match(bullet):
4947					if not start_sublist:
4948						bullet = increase_list_bullet(bullet)
4949					# else copy number
4950				else:
4951					pass # Keep same bullet
4952
4953				buffer.set_bullet(newline, bullet, indent=indent)
4954					# Set indent in one-go because setting before fails for
4955					# end of buffer while setting after messes up renumbering
4956					# of lists
4957
4958			buffer.update_editmode() # also updates indent tag
4959
4960	def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip):
4961		# Handle tooltip query event
4962		x,y = self.window_to_buffer_coords(Gtk.TextWindowType.WIDGET, x, y)
4963		iter = strip_boolean_result(self.get_iter_at_location(x, y))
4964		if iter is not None:
4965			pixbuf = self._get_pixbuf_at_pointer(iter, (x, y))
4966			if pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'image':
4967				data = pixbuf.zim_attrib.copy()
4968				text = data['src'] + '\n\n'
4969				if 'href' in data:
4970					text += '<b>%s:</b> %s\n' % (_('Link'), data['href']) # T: tooltip label for image with href
4971				if 'id' in data:
4972					text += '<b>%s:</b> %s\n' % (_('Id'), data['id']) # T: tooltip label for image with anchor id
4973				tooltip.set_markup(text.strip())
4974				return True
4975			elif pixbuf and hasattr(pixbuf, 'zim_type') and pixbuf.zim_type == 'anchor':
4976				text = '#' + pixbuf.zim_attrib['name']
4977				tooltip.set_markup(text)
4978				return True
4979
4980		return False
4981
4982
4983class UndoActionGroup(list):
4984	'''Group of actions that should un-done or re-done in a single step
4985
4986	Inherits from C{list}, so can be treates as a list of actions.
4987	See L{UndoStackManager} for more details on undo actions.
4988
4989	@ivar can_merge: C{True} when this group can be merged with another
4990	group
4991	@ivar cursor: the position to restore the cursor afre un-/re-doning
4992	'''
4993
4994	__slots__ = ('can_merge', 'cursor')
4995
4996	def __init__(self):
4997		self.can_merge = False
4998		self.cursor = None
4999
5000	def reversed(self):
5001		'''Returns a new UndoActionGroup with the reverse actions of
5002		this group.
5003		'''
5004		group = UndoActionGroup()
5005		group.cursor = self.cursor
5006		for action in self:
5007			# constants are defined such that negating them reverses the action
5008			action = (-action[0],) + action[1:]
5009			group.insert(0, action)
5010		return group
5011
5012
5013class UndoStackManager:
5014	'''Undo stack implementation for L{TextBuffer}. It records any
5015	changes to the buffer and allows undoing and redoing edits.
5016
5017	The stack undostack will be folded when you undo a few steps and
5018	then start editing again. This means that even the 'undo' action
5019	is recorded in the undo stakc and can always be undone itself;
5020	so no data is discarded.
5021
5022	Say you start with a certain buffer state "A", then make two edits
5023	("B" and "C") and then undo the last one, so you get back in state
5024	"B"::
5025
5026	  State A --> State B --> State C
5027	                      <--
5028	                      undo
5029
5030	when you now make a new edit ("D"), state "C" is not discarded, instead
5031	it is "folded" as follows::
5032
5033	  State A --> State B --> State C --> State B --> State D
5034
5035	so you can still go back to state "C" using Undo.
5036
5037	Undo actions
5038	============
5039
5040	Each action is recorded as a 4-tuple of:
5041	  - C{action_type}: one of C{ACTION_INSERT}, C{ACTION_DELETE},
5042	    C{ACTION_APPLY_TAG}, C{ACTION_REMOVE_TAG}
5043	  - C{start_iter}: a C{Gtk.TextIter}
5044	  - C{end_iter}: a C{Gtk.TextIter}
5045	  - C{data}: either a (raw) L{ParseTree} or a C{Gtk.TextTag}
5046
5047	These actions are low level operations, so they are
5048
5049	Actions are collected as L{UndoActionGroup}s. When the user selects
5050	Undo or Redo we actually undo or redo a whole UndoActionGroup as a
5051	single step. E.g. inserting a link will consist of inserting the
5052	text and than applying the TextTag with the link data. These are
5053	technically two separate modifications of the TextBuffer, however
5054	when selecting Undo both are undone at once because they are
5055	combined in a single group.
5056
5057	Typically when recording modifications the action groups are
5058	delimited by the begin-user-action and end-user-action signals of
5059	the L{TextBuffer}. (This is why we use the L{TextBuffer.user_action}
5060	attribute context manager in the TextBuffer code.)
5061
5062	Also we try to group single-character inserts and deletes into words.
5063	This makes the stack more compact and makes the undo action more
5064	meaningful.
5065	'''
5066
5067	# Each interactive action (e.g. every single key stroke) is wrapped
5068	# in a set of begin-user-action and end-user-action signals. We use
5069	# these signals to group actions. This implies that any sequence on
5070	# non-interactive actions will also end up in a single group. An
5071	# interactively created group consisting of a single character insert
5072	# or single character delete is a candidate for merging.
5073
5074	MAX_UNDO = 100 #: Constant for the max number of undo steps to be remembered
5075
5076	# Constants for action types - negating an action gives it opposite.
5077	ACTION_INSERT = 1 #: action type for inserting text
5078	ACTION_DELETE = -1 #: action type for deleting text
5079	ACTION_APPLY_TAG = 2 #: action type for applying a C{Gtk.TextTag}
5080	ACTION_REMOVE_TAG = -2 #: action type for removing a C{Gtk.TextTag}
5081
5082	def __init__(self, textbuffer):
5083		'''Constructor
5084
5085		@param textbuffer: a C{Gtk.TextBuffer}
5086		'''
5087		self.buffer = textbuffer
5088		self.stack = [] # stack of actions & action groups
5089		self.group = UndoActionGroup() # current group of actions
5090		self.interactive = False # interactive edit or not
5091		self.insert_pending = False # whether we need to call flush insert or not
5092		self.undo_count = 0 # number of undo steps that were done
5093		self.block_count = 0 # number of times block() was called
5094		self._insert_tree_start = None
5095
5096		self.recording_handlers = [] # handlers to be blocked when not recording
5097		for signal, handler in (
5098			('undo-save-cursor', self.do_save_cursor),
5099			('insert-text', self.do_insert_text),
5100			('insert-pixbuf', self.do_insert_pixbuf),
5101			('insert-child-anchor', self.do_insert_pixbuf),
5102			('delete-range', self.do_delete_range),
5103			('begin-user-action', self.do_begin_user_action),
5104			('end-user-action', self.do_end_user_action),
5105		):
5106			self.recording_handlers.append(
5107				self.buffer.connect(signal, handler))
5108
5109		for signal, handler in (
5110			('end-user-action', self.do_end_user_action),
5111		):
5112			self.recording_handlers.append(
5113				self.buffer.connect_after(signal, handler))
5114
5115		for signal, action in (
5116			('apply-tag', self.ACTION_APPLY_TAG),
5117			('remove-tag', self.ACTION_REMOVE_TAG),
5118		):
5119			self.recording_handlers.append(
5120				self.buffer.connect(signal, self.do_change_tag, action))
5121
5122		for signal, handler in (
5123			('begin-insert-tree', self.do_begin_insert_tree),
5124			('end-insert-tree', self.do_end_insert_tree),
5125		):
5126			self.buffer.connect_after(signal, handler)
5127
5128		#~ self.buffer.connect_object('edit-textstyle-changed',
5129			#~ self.__class__._flush_if_typing, self)
5130		#~ self.buffer.connect_object('set-mark',
5131			#~ self.__class__._flush_if_typing, self)
5132
5133	def block(self):
5134		'''Stop listening to events from the L{TextBuffer} until
5135		the next call to L{unblock()}. Any change in between will not
5136		be undo-able (and mess up the undo stack) unless it is recorded
5137		explicitly.
5138
5139		The number of calls C{block()} and C{unblock()} is counted, so
5140		they can be called recursively.
5141		'''
5142		if self.block_count == 0:
5143			for id in self.recording_handlers:
5144				self.buffer.handler_block(id)
5145		self.block_count += 1
5146
5147	def unblock(self):
5148		'''Start listening to events from the L{TextBuffer} again'''
5149		if self.block_count > 1:
5150			self.block_count -= 1
5151		else:
5152			for id in self.recording_handlers:
5153				self.buffer.handler_unblock(id)
5154			self.block_count = 0
5155
5156	def do_save_cursor(self, buffer, iter):
5157		# Store the cursor position
5158		self.group.cursor = iter.get_offset()
5159
5160	def do_begin_user_action(self, buffer):
5161		# Start a group of actions that will be undone as a single action
5162		if self.undo_count > 0:
5163			self.flush_redo_stack()
5164
5165		if self.group:
5166			self.stack.append(self.group)
5167			self.group = UndoActionGroup()
5168			while len(self.stack) > self.MAX_UNDO:
5169				self.stack.pop(0)
5170
5171		self.interactive = True
5172
5173	def do_end_user_action(self, buffer):
5174		# End a group of actions that will be undone as a single action
5175		if self.group:
5176			self.stack.append(self.group)
5177			self.group = UndoActionGroup()
5178			while len(self.stack) > self.MAX_UNDO:
5179				self.stack.pop(0)
5180
5181		self.interactive = False
5182
5183	def do_begin_insert_tree(self, buffer, interactive):
5184		if self.block_count == 0:
5185			if self.undo_count > 0:
5186				self.flush_redo_stack()
5187			elif self.insert_pending:
5188				self.flush_insert()
5189			# Do not start new group here - insert tree can be part of bigger change
5190
5191			self._insert_tree_start = buffer.get_insert_iter().get_offset()
5192		self.block()
5193
5194	def do_end_insert_tree(self, buffer):
5195		self.unblock()
5196		if self.block_count == 0:
5197			start = self._insert_tree_start
5198			start_iter = buffer.get_iter_at_offset(start)
5199			end_iter = buffer.get_insert_iter()
5200			end = end_iter.get_offset()
5201			tree = self.buffer.get_parsetree((start_iter, end_iter), raw=True)
5202			self.group.append((self.ACTION_INSERT, start, end, tree))
5203
5204	def do_insert_text(self, buffer, iter, text, length):
5205		# Handle insert text event
5206		# Do not use length argument, it gives length in bytes, not characters
5207		length = len(text)
5208		if self.undo_count > 0:
5209			self.flush_redo_stack()
5210
5211		start = iter.get_offset()
5212		end = start + length
5213		#~ print('INSERT at %i: "%s" (%i)' % (start, text, length))
5214
5215		if length == 1 and not text.isspace() \
5216		and self.interactive and not self.group:
5217			# we can merge
5218			if self.stack and self.stack[-1].can_merge:
5219				previous = self.stack[-1][-1]
5220				if previous[0] == self.ACTION_INSERT \
5221				and previous[2] == start \
5222				and previous[3] is None:
5223					# so can previous group - let's merge
5224					self.group = self.stack.pop()
5225					self.group[-1] = (self.ACTION_INSERT, previous[1], end, None)
5226					return
5227			# we didn't merge - set flag for next
5228			self.group.can_merge = True
5229
5230		self.group.append((self.ACTION_INSERT, start, end, None))
5231		self.insert_pending = True
5232
5233	def do_insert_pixbuf(self, buffer, iter, pixbuf):
5234		# Handle insert pixbuf event
5235		if self.undo_count > 0:
5236			self.flush_redo_stack()
5237		elif self.insert_pending:
5238			self.flush_insert()
5239
5240		start = iter.get_offset()
5241		end = start + 1
5242		#~ print('INSERT PIXBUF at %i' % start)
5243		self.group.append((self.ACTION_INSERT, start, end, None))
5244		self.group.can_merge = False
5245		self.insert_pending = True
5246
5247	def flush_insert(self):
5248		'''Flush all pending actions and store them on the stack
5249
5250		The reason for this method is that because of the possibility of
5251		merging actions we do not immediatly request the parse tree for
5252		each single character insert. Instead we first group inserts
5253		based on cursor positions and then request the parse tree for
5254		the group at once. This method proceses all such delayed
5255		requests.
5256		'''
5257		def _flush_group(group):
5258			for i in reversed(list(range(len(group)))):
5259				action, start, end, tree = group[i]
5260				if action == self.ACTION_INSERT and tree is None:
5261					bounds = (self.buffer.get_iter_at_offset(start),
5262								self.buffer.get_iter_at_offset(end))
5263					tree = self.buffer.get_parsetree(bounds, raw=True)
5264					#~ print('FLUSH %i to %i\n\t%s' % (start, end, tree.tostring()))
5265					group[i] = (self.ACTION_INSERT, start, end, tree)
5266				else:
5267					return False
5268			return True
5269
5270		if _flush_group(self.group):
5271			for i in reversed(list(range(len(self.stack)))):
5272				if not _flush_group(self.stack[i]):
5273					break
5274
5275		self.insert_pending = False
5276
5277	def do_delete_range(self, buffer, start, end):
5278		# Handle deleting text
5279		if self.undo_count > 0:
5280			self.flush_redo_stack()
5281		elif self.insert_pending:
5282			self.flush_insert()
5283
5284		bounds = (start, end)
5285		tree = self.buffer.get_parsetree(bounds, raw=True)
5286		start, end = start.get_offset(), end.get_offset()
5287		#~ print('DELETE RANGE from %i to %i\n\t%s' % (start, end, tree.tostring()))
5288		self.group.append((self.ACTION_DELETE, start, end, tree))
5289		self.group.can_merge = False
5290
5291	def do_change_tag(self, buffer, tag, start, end, action):
5292		assert action in (self.ACTION_APPLY_TAG, self.ACTION_REMOVE_TAG)
5293		if not hasattr(tag, 'zim_type'):
5294			return
5295
5296		start, end = start.get_offset(), end.get_offset()
5297		if self.group \
5298		and self.group[-1][0] == self.ACTION_INSERT \
5299		and self.group[-1][1] <= start \
5300		and self.group[-1][2] >= end \
5301		and self.group[-1][3] is None:
5302			pass # for text that is not yet flushed tags will be in the tree
5303		else:
5304			if self.undo_count > 0:
5305				self.flush_redo_stack()
5306			elif self.insert_pending:
5307				self.flush_insert()
5308
5309			#~ print('TAG CHANGED', start, end, tag)
5310			self.group.append((action, start, end, tag))
5311			self.group.can_merge = False
5312
5313	def undo(self):
5314		'''Undo one user action'''
5315		if self.group:
5316			self.stack.append(self.group)
5317			self.group = UndoActionGroup()
5318		if self.insert_pending:
5319			self.flush_insert()
5320
5321		#~ import pprint
5322		#~ pprint.pprint( self.stack )
5323
5324		l = len(self.stack)
5325		if self.undo_count == l:
5326			return False
5327		else:
5328			self.undo_count += 1
5329			i = l - self.undo_count
5330			self._replay(self.stack[i].reversed())
5331			return True
5332
5333	def flush_redo_stack(self):
5334		'''Fold the "redo" part of the stack, called before new actions
5335		are appended after some step was undone.
5336		'''
5337		i = len(self.stack) - self.undo_count
5338		fold = UndoActionGroup()
5339		for group in reversed(self.stack[i:]):
5340			fold.extend(group.reversed())
5341		self.stack.append(fold)
5342		self.undo_count = 0
5343
5344	def redo(self):
5345		'''Redo one user action'''
5346		if self.undo_count == 0:
5347			return False
5348		else:
5349			assert not self.group, 'BUG: undo count should have been zero'
5350			i = len(self.stack) - self.undo_count
5351			self._replay(self.stack[i])
5352			self.undo_count -= 1
5353			return True
5354
5355	def _replay(self, actiongroup):
5356		self.block()
5357
5358		#~ print('='*80)
5359		for action, start, end, data in actiongroup:
5360			iter = self.buffer.get_iter_at_offset(start)
5361			bound = self.buffer.get_iter_at_offset(end)
5362
5363			if action == self.ACTION_INSERT:
5364				#~ print('INSERTING', data.tostring())
5365				self.buffer.place_cursor(iter)
5366				self.buffer.insert_parsetree_at_cursor(data)
5367			elif action == self.ACTION_DELETE:
5368				#~ print('DELETING', data.tostring())
5369				self.buffer.place_cursor(iter)
5370				tree = self.buffer.get_parsetree((iter, bound), raw=True)
5371				#~ print('REAL', tree.tostring())
5372				with self.buffer.user_action:
5373					self.buffer.delete(iter, bound)
5374					self.buffer._check_renumber = []
5375						# Flush renumber check - HACK to avoid messing up the stack
5376				if tree.tostring() != data.tostring():
5377					logger.warn('Mismatch in undo stack\n%s\n%s\n', tree.tostring(), data.tostring())
5378			elif action == self.ACTION_APPLY_TAG:
5379				#~ print('APPLYING', data)
5380				self.buffer.apply_tag(data, iter, bound)
5381				self.buffer.place_cursor(bound)
5382			elif action == self.ACTION_REMOVE_TAG:
5383				#~ print('REMOVING', data)
5384				self.buffer.remove_tag(data, iter, bound)
5385				self.buffer.place_cursor(bound)
5386			else:
5387				assert False, 'BUG: unknown action type'
5388
5389		if not actiongroup.cursor is None:
5390			iter = self.buffer.get_iter_at_offset(actiongroup.cursor)
5391			self.buffer.place_cursor(iter)
5392
5393		self.unblock()
5394
5395
5396class SavePageHandler(object):
5397	'''Object for handling page saving.
5398
5399	This class implements auto-saving on a timer and tries writing in
5400	a background thread to ot block the user interface.
5401	'''
5402
5403	def __init__(self, pageview, notebook, get_page_cb, timeout=15, use_thread=True):
5404		self.pageview = pageview
5405		self.notebook = notebook
5406		self.get_page_cb = get_page_cb
5407		self.timeout = timeout
5408		self.use_thread = use_thread
5409		self._autosave_timer = None
5410		self._error_event = None
5411
5412	def wait_for_store_page_async(self):
5413		# FIXME: duplicate of notebook method
5414		self.notebook.wait_for_store_page_async()
5415
5416	def queue_autosave(self, timeout=15):
5417		'''Queue a single autosave action after a given timeout.
5418		Will not do anything once an autosave is already queued.
5419		Autosave will keep running until page is no longer modified and
5420		then stop.
5421		@param timeout: timeout in seconds
5422		'''
5423		if not self._autosave_timer:
5424			self._autosave_timer = GObject.timeout_add(
5425				self.timeout * 1000, # s -> ms
5426				self.do_try_save_page
5427			)
5428
5429	def cancel_autosave(self):
5430		'''Cancel a pending autosave'''
5431		if self._autosave_timer:
5432			GObject.source_remove(self._autosave_timer)
5433			self._autosave_timer = None
5434
5435	def _assert_can_save_page(self, page):
5436		if self.pageview.readonly:
5437			raise AssertionError('BUG: can not save page when UI is read-only')
5438		elif page.readonly:
5439			raise AssertionError('BUG: can not save read-only page')
5440
5441	def save_page_now(self, dialog_timeout=False):
5442		'''Save the page in the foregound
5443
5444		Can result in a L{SavePageErrorDialog} when there is an error
5445		while saving a page. If that dialog is cancelled by the user,
5446		the page may not be saved after all.
5447
5448		@param dialog_timeout: passed on to L{SavePageErrorDialog}
5449		'''
5450		self.cancel_autosave()
5451
5452		self._error_event = None
5453
5454		with NotebookState(self.notebook):
5455			page = self.get_page_cb()
5456			if page:
5457				try:
5458					self._assert_can_save_page(page)
5459					logger.debug('Saving page: %s', page)
5460					buffer = page.get_textbuffer()
5461					if buffer:
5462						buffer.showing_template = False # allow save_page to save template content
5463					#~ assert False, "TEST"
5464					self.notebook.store_page(page)
5465
5466				except Exception as error:
5467					logger.exception('Failed to save page: %s', page.name)
5468					SavePageErrorDialog(self.pageview, error, page, dialog_timeout).run()
5469
5470	def try_save_page(self):
5471		'''Try to save the page
5472
5473		  * Will not do anything if page is not modified or when an
5474		    autosave is already in progress.
5475		  * If last autosave resulted in an error, will run in the
5476		    foreground, else it tries to write the page in a background
5477		    thread
5478		'''
5479		self.cancel_autosave()
5480		self.do_try_save_page()
5481
5482	def do_try_save_page(self, *a):
5483		page = self.get_page_cb()
5484		if not (page and page.modified):
5485			self._autosave_timer = None
5486			return False # stop timer
5487
5488		if ongoing_operation(self.notebook):
5489			logger.debug('Operation in progress, skipping auto-save') # Could be auto-save
5490			return True # Check back later if on timer
5491
5492
5493		if not self.use_thread:
5494			self.save_page_now(dialog_timeout=True)
5495		elif self._error_event and self._error_event.is_set():
5496			# Error in previous auto-save, save in foreground to allow error dialog
5497			logger.debug('Last auto-save resulted in error, re-try in foreground')
5498			self.save_page_now(dialog_timeout=True)
5499		else:
5500			# Save in background async
5501			# Retrieve tree here and pass on to thread to prevent
5502			# changing the buffer while extracting it
5503			parsetree = page.get_parsetree()
5504			op = self.notebook.store_page_async(page, parsetree)
5505			self._error_event = op.error_event
5506
5507		if page.modified:
5508			return True # if True, timer will keep going
5509		else:
5510			self._autosave_timer = None
5511			return False # stop timer
5512
5513
5514class SavePageErrorDialog(ErrorDialog):
5515	'''Error dialog used when we hit an error while trying to save a page.
5516	Allow to save a copy or to discard changes. Includes a timer which
5517	delays the action buttons becoming sensitive. Reason for this timer is
5518	that the dialog may popup from auto-save while the user is typing, and
5519	we want to prevent an accidental action.
5520	'''
5521
5522	def __init__(self, pageview, error, page, timeout=False):
5523		msg = _('Could not save page: %s') % page.name
5524			# T: Heading of error dialog
5525		desc = str(error).strip() \
5526				+ '\n\n' \
5527				+ _('''\
5528To continue you can save a copy of this page or discard
5529any changes. If you save a copy changes will be also
5530discarded, but you can restore the copy later.''')
5531			# T: text in error dialog when saving page failed
5532		ErrorDialog.__init__(self, pageview, (msg, desc), buttons=Gtk.ButtonsType.NONE)
5533
5534		self.timeout = timeout
5535
5536		self.pageview = pageview
5537		self.page = page
5538		self.error = error
5539
5540		self.timer_label = Gtk.Label()
5541		self.timer_label.set_alignment(0.9, 0.5)
5542		self.timer_label.set_sensitive(False)
5543		self.timer_label.show()
5544		self.vbox.add(self.timer_label)
5545
5546		cancel_button = Gtk.Button.new_with_mnemonic(_('_Cancel')) # T: Button label
5547		self.add_action_widget(cancel_button, Gtk.ResponseType.CANCEL)
5548
5549		self._done = False
5550
5551		discard_button = Gtk.Button.new_with_mnemonic(_('_Discard Changes'))
5552			# T: Button in error dialog
5553		discard_button.connect('clicked', lambda o: self.discard())
5554		self.add_action_widget(discard_button, Gtk.ResponseType.OK)
5555
5556		save_button = Gtk.Button.new_with_mnemonic(_('_Save Copy'))
5557			# T: Button in error dialog
5558		save_button.connect('clicked', lambda o: self.save_copy())
5559		self.add_action_widget(save_button, Gtk.ResponseType.OK)
5560
5561		for button in (cancel_button, discard_button, save_button):
5562			button.set_sensitive(False)
5563			button.show()
5564
5565	def discard(self):
5566		self.page.reload_textbuffer()
5567		self._done = True
5568
5569	def save_copy(self):
5570		from zim.gui.uiactions import SaveCopyDialog
5571		if SaveCopyDialog(self, self.pageview.notebook, self.page).run():
5572			self.discard()
5573
5574	def do_response_ok(self):
5575		return self._done
5576
5577	def run(self):
5578		if self.timeout:
5579			self.timer = 5
5580			self.timer_label.set_text('%i sec.' % self.timer)
5581			def timer(self):
5582				self.timer -= 1
5583				if self.timer > 0:
5584					self.timer_label.set_text('%i sec.' % self.timer)
5585					return True # keep timer going
5586				else:
5587					for button in self.action_area.get_children():
5588						button.set_sensitive(True)
5589					self.timer_label.set_text('')
5590					return False # remove timer
5591
5592			# older gobject version doesn't know about seconds
5593			id = GObject.timeout_add(1000, timer, self)
5594			ErrorDialog.run(self)
5595			GObject.source_remove(id)
5596		else:
5597			for button in self.action_area.get_children():
5598				button.set_sensitive(True)
5599			ErrorDialog.run(self)
5600
5601
5602from zim.plugins import ExtensionBase, extendable
5603from zim.config import ConfigDict
5604from zim.gui.actionextension import ActionExtensionBase
5605from zim.gui.widgets import LEFT_PANE, RIGHT_PANE, BOTTOM_PANE, PANE_POSITIONS
5606
5607
5608class PageViewExtensionBase(ActionExtensionBase):
5609	'''Base class for extensions that want to interact with the "page view",
5610	which is the primary editor view of the application.
5611
5612	This extension class will collect actions defined with the C{@action},
5613	C{@toggle_action} or C{@radio_action} decorators and add them to the window.
5614
5615	This extension class also supports showing side panes that are visible as
5616	part of the "decoration" of the editor view.
5617
5618	@ivar pageview: the L{PageView} object
5619	@ivar navigation: a L{NavigationModel} model
5620	@ivar uistate: a L{ConfigDict} to store the extensions ui state or
5621
5622	The "uistate" is the per notebook state of the interface, it is
5623	intended for stuff like the last folder opened by the user or the
5624	size of a dialog after resizing. It is stored in the X{state.conf}
5625	file in the notebook cache folder. It differs from the preferences,
5626	which are stored globally and dictate the behavior of the application.
5627	(To access the preference use C{plugin.preferences}.)
5628	'''
5629
5630	def __init__(self, plugin, pageview):
5631		ExtensionBase.__init__(self, plugin, pageview)
5632		self.pageview = pageview
5633		self._window = self.pageview.get_toplevel()
5634		assert hasattr(self._window, 'add_tab'), 'expect mainwindow, got %s' % self._window
5635
5636		self.navigation = self._window.navigation
5637		self.uistate = pageview.notebook.state[self.plugin.config_key]
5638
5639		self._sidepane_widgets = {}
5640		self._add_actions(self._window.uimanager)
5641
5642		actiongroup = self.pageview.get_action_group('pageview')
5643		for name, action in get_actions(self):
5644			gaction = action.get_gaction()
5645			actiongroup.add_action(gaction)
5646
5647	def add_sidepane_widget(self, widget, preferences_key):
5648		key = widget.__class__.__name__
5649		position = self.plugin.preferences[preferences_key]
5650		self._window.add_tab(key, widget, position)
5651
5652		def on_preferences_changed(preferences):
5653			position = self.plugin.preferences[preferences_key]
5654			self._window.remove(widget)
5655			self._window.add_tab(key, widget, position)
5656
5657		sid = self.connectto(self.plugin.preferences, 'changed', on_preferences_changed)
5658		self._sidepane_widgets[widget] = sid
5659		widget.show_all()
5660
5661	def remove_sidepane_widget(self, widget):
5662		try:
5663			self._window.remove(widget)
5664		except ValueError:
5665			pass
5666
5667		try:
5668			sid = self._sidepane_widgets.pop(widget)
5669			self.plugin.preferences.disconnect(sid)
5670		except KeyError:
5671			pass
5672
5673	def teardown(self):
5674		for widget in list(self._sidepane_widgets):
5675			self.remove_sidepane_widget(widget)
5676			widget.disconnect_all()
5677
5678		actiongroup = self.pageview.get_action_group('pageview')
5679		for name, action in get_actions(self):
5680			actiongroup.remove_action(action.name)
5681
5682
5683class PageViewExtension(PageViewExtensionBase):
5684	'''Base class for extensions of the L{PageView},
5685	see L{PageViewExtensionBase} for API documentation.
5686	'''
5687	pass
5688
5689
5690class InsertedObjectPageviewManager(object):
5691	'''"Glue" object to manage "insert object" actions for the L{PageView}
5692	Creates an action object for each object type and inserts UI elements
5693	for the action in the pageview.
5694	'''
5695
5696	_class_actions = set()
5697
5698	def __init__(self, pageview):
5699		self.pageview = pageview
5700		self._actions = set()
5701		self.on_changed(None)
5702		PluginManager.insertedobjects.connect('changed', self.on_changed)
5703
5704	@staticmethod
5705	def _action_name(key):
5706		return 'insert_' + re.sub('\W', '_', key)
5707
5708	def on_changed(self, o):
5709		insertedobjects = PluginManager.insertedobjects
5710		keys = set(insertedobjects.keys())
5711
5712		actiongroup = self.pageview.get_action_group('pageview')
5713		for key in self._actions - keys:
5714			action = getattr(self, self._action_name(key))
5715			actiongroup.remove_action(action.name)
5716			self._actions.remove(key)
5717
5718		self._update_class_actions() # Modifies class
5719
5720		for key in keys - self._actions:
5721			action = getattr(self, self._action_name(key))
5722			gaction = action.get_gaction()
5723			actiongroup.add_action(gaction)
5724			self._actions.add(key)
5725
5726		assert self._actions == keys
5727
5728	@classmethod
5729	def _update_class_actions(cls):
5730		# Triggered by instance, could be run multiple times for same change
5731		# but redundant runs should do nothing because of no change compared
5732		# to "_class_actions"
5733		insertedobjects = PluginManager.insertedobjects
5734		keys = set(insertedobjects.keys())
5735		for key in cls._class_actions - keys:
5736			name = cls._action_name(key)
5737			if hasattr(cls, name):
5738				delattr(cls, name)
5739			cls._class_actions.remove(key)
5740
5741		for key in keys - cls._class_actions:
5742			name = cls._action_name(key)
5743			obj = insertedobjects[key]
5744			func = functools.partial(cls._action_handler, key)
5745			action = ActionClassMethod(
5746				name, func, obj.label,
5747				verb_icon=obj.verb_icon,
5748				menuhints='insert',
5749			)
5750			setattr(cls, name, action)
5751			cls._class_actions.add(key)
5752
5753		assert cls._class_actions == keys
5754
5755	def _action_handler(key, self): # reverse arg spec due to partial
5756		try:
5757			otype = PluginManager.insertedobjects[key]
5758			notebook, page = self.pageview.notebook, self.pageview.page
5759			try:
5760				model = otype.new_model_interactive(self.pageview, notebook, page)
5761			except ValueError:
5762				return # dialog cancelled
5763			self.pageview.insert_object_model(otype, model)
5764		except:
5765			zim.errors.exception_handler(
5766				'Exception during action: insert_%s' % key)
5767
5768
5769def _install_format_actions(klass):
5770	for name, label, accelerator in (
5771		('apply_format_h1', _('Heading _1'), '<Primary>1'), # T: Menu item
5772		('apply_format_h2', _('Heading _2'), '<Primary>2'), # T: Menu item
5773		('apply_format_h3', _('Heading _3'), '<Primary>3'), # T: Menu item
5774		('apply_format_h4', _('Heading _4'), '<Primary>4'), # T: Menu item
5775		('apply_format_h5', _('Heading _5'), '<Primary>5'), # T: Menu item
5776		('apply_format_strong', _('_Strong'), '<Primary>B'), # T: Menu item
5777		('apply_format_emphasis', _('_Emphasis'), '<Primary>I'), # T: Menu item
5778		('apply_format_mark', _('_Mark'), '<Primary>U'), # T: Menu item
5779		('apply_format_strike', _('_Strike'), '<Primary>K'), # T: Menu item
5780		('apply_format_sub', _('_Subscript'), '<Primary><Shift>b'), # T: Menu item
5781		('apply_format_sup', _('_Superscript'), '<Primary><Shift>p'), # T: Menu item
5782		('apply_format_code', _('_Verbatim'), '<Primary>T'), # T: Menu item
5783	):
5784		func = functools.partial(klass.do_toggle_format_action, action=name)
5785		setattr(klass, name,
5786			ActionClassMethod(name, func, label, accelerator=accelerator, menuhints='edit')
5787		)
5788
5789	klass._format_toggle_actions = []
5790	for name, label, icon in (
5791		('toggle_format_strong', _('_Strong'), 'format-text-bold-symbolic'), # T: menu item for formatting
5792		('toggle_format_emphasis', _('_Emphasis'), 'format-text-italic-symbolic'), # T: menu item for formatting
5793		('toggle_format_mark', _('_Mark'), 'format-text-underline-symbolic'), # T: menu item for formatting
5794		('toggle_format_strike', _('_Strike'), 'format-text-strikethrough-symbolic'), # T: menu item for formatting
5795		('toggle_format_code', _('_Verbatim'), 'format-text-code-symbolic'), # T: menu item for formatting
5796		('toggle_format_sup', _('Su_perscript'), 'format-text-superscript-symbolic'), # T: menu item for formatting
5797		('toggle_format_sub', _('Su_bscript'), 'format-text-subscript-symbolic'), # T: menu item for formatting
5798	):
5799		func = functools.partial(klass.do_toggle_format_action_alt, action=name)
5800		setattr(klass, name,
5801			ToggleActionClassMethod(name, func, label, icon=icon, menuhints='edit')
5802		)
5803		klass._format_toggle_actions.append(name)
5804
5805	return klass
5806
5807
5808from zim.signals import GSignalEmitterMixin
5809
5810@_install_format_actions
5811@extendable(PageViewExtension, register_after_init=False)
5812class PageView(GSignalEmitterMixin, Gtk.VBox):
5813	'''Widget to display a single page, consists of a L{TextView} and
5814	a L{FindBar}. Also adds menu items and in general integrates
5815	the TextView with the rest of the application.
5816
5817	@ivar text_style: a L{ConfigSectionsDict} with style properties. Although this
5818	is a class attribute loading the data from the config file is
5819	delayed till the first object is constructed
5820
5821	@ivar page: L{Page} object for the current page displayed in the widget
5822	@ivar readonly: C{True} when the widget is read-only, see
5823	L{set_readonly()} for details
5824	@ivar view: the L{TextView} child object
5825	@ivar find_bar: the L{FindBar} child widget
5826	@ivar preferences: a L{ConfigDict} with preferences
5827
5828	@signal: C{modified-changed ()}: emitted when the page is edited
5829	@signal: C{textstyle-changed (style)}:
5830	Emitted when textstyle at the cursor changes, gets the list of text styles or None.
5831	@signal: C{activate-link (link, hints)}: emitted when a link is opened,
5832	stops emission after the first handler returns C{True}
5833
5834	@todo: document preferences supported by PageView
5835	@todo: document extra keybindings implemented in this widget
5836	@todo: document style properties supported by this widget
5837	'''
5838
5839	# define signals we want to use - (closure type, return type and arg types)
5840	__gsignals__ = {
5841		'modified-changed': (GObject.SignalFlags.RUN_LAST, None, ()),
5842		'textstyle-changed': (GObject.SignalFlags.RUN_LAST, None, (object,)),
5843		'page-changed': (GObject.SignalFlags.RUN_LAST, None, (object,)),
5844		'link-caret-enter': (GObject.SignalFlags.RUN_LAST, None, (object,)),
5845		'link-caret-leave': (GObject.SignalFlags.RUN_LAST, None, (object,)),
5846		'readonly-changed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
5847	}
5848
5849	__signals__ = {
5850		'activate-link': (GObject.SignalFlags.RUN_LAST, bool, (object, object))
5851	}
5852
5853	def __init__(self, notebook, navigation):
5854		'''Constructor
5855		@param notebook: the L{Notebook} object
5856		@param navigation: L{NavigationModel} object
5857		'''
5858		GObject.GObject.__init__(self)
5859		GSignalEmitterMixin.__init__(self)
5860
5861		self._buffer_signals = ()
5862		self.notebook = notebook
5863		self.page = None
5864		self.navigation = navigation
5865		self.readonly = True
5866		self._readonly_set = False
5867		self._readonly_set_error = False
5868		self.ui_is_initialized = False
5869		self._caret_link = None
5870		self._undo_history_queue = [] # we never lookup in this list, only keep refs - notebook does the caching
5871
5872		self.preferences = ConfigManager.preferences['PageView']
5873		self.preferences.define(
5874			show_edit_bar=Boolean(True),
5875			follow_on_enter=Boolean(True),
5876			read_only_cursor=Boolean(False),
5877			autolink_camelcase=Boolean(True),
5878			autolink_files=Boolean(True),
5879			autoselect=Boolean(True),
5880			unindent_on_backspace=Boolean(True),
5881			cycle_checkbox_type=Boolean(True),
5882			recursive_indentlist=Boolean(True),
5883			recursive_checklist=Boolean(False),
5884			auto_reformat=Boolean(False),
5885			copy_format=Choice('Text', COPY_FORMATS),
5886			file_templates_folder=String('~/Templates'),
5887		)
5888
5889		self.textview = TextView(preferences=self.preferences)
5890		self.swindow = ScrolledWindow(self.textview)
5891		self._hack_hbox = Gtk.HBox()
5892		self._hack_hbox.add(self.swindow)
5893		self._hack_label = Gtk.Label() # any widget would do I guess
5894		self._hack_hbox.pack_end(self._hack_label, False, True, 1)
5895
5896		self.overlay = Gtk.Overlay()
5897		self.overlay.add(self._hack_hbox)
5898		self._overlay_label = Gtk.Label()
5899		self._overlay_label.set_halign(Gtk.Align.START)
5900		self._overlay_label.set_margin_start(12)
5901		self._overlay_label.set_valign(Gtk.Align.END)
5902		self._overlay_label.set_margin_bottom(5)
5903		widget_set_css(self._overlay_label, 'overlay-label',
5904			'background: rgba(0, 0, 0, 0.8); '
5905			'padding: 3px 5px; border-radius: 3px; '
5906			'color: #fff; '
5907		) # Tried to make it look like tooltip - based on Adwaita css
5908		self._overlay_label.set_no_show_all(True)
5909		self.overlay.add_overlay(self._overlay_label)
5910		self.overlay.set_overlay_pass_through(self._overlay_label, True)
5911		self.add(self.overlay)
5912
5913		self.textview.connect_object('link-clicked', PageView.activate_link, self)
5914		self.textview.connect_object('populate-popup', PageView.do_populate_popup, self)
5915		self.textview.connect('link-enter', self.on_link_enter)
5916		self.textview.connect('link-leave', self.on_link_leave)
5917		self.connect('link-caret-enter', self.on_link_enter)
5918		self.connect('link-caret-leave', self.on_link_leave)
5919
5920		## Create search box
5921		self.find_bar = FindBar(textview=self.textview)
5922		self.pack_end(self.find_bar, False, True, 0)
5923		self.find_bar.hide()
5924
5925		## setup GUI actions
5926		group = get_gtk_actiongroup(self)
5927		group.add_actions(MENU_ACTIONS, self)
5928
5929		# setup hooks for new file submenu
5930		action = self.actiongroup.get_action('insert_new_file_menu')
5931		action.zim_readonly = False
5932		action.connect('activate', self._update_new_file_submenu)
5933
5934		# ...
5935		self.edit_bar = EditBar(self)
5936		self._edit_bar_visible = True
5937		self.pack_start(self.edit_bar, False, True, 0)
5938		#self.reorder_child(self.edit_bar, 0)
5939
5940		self.edit_bar.show_all()
5941		self.edit_bar.set_no_show_all(True)
5942
5943		def _show_edit_bar_on_hide_find(*a):
5944			if self._edit_bar_visible and not self.readonly:
5945				self.edit_bar.show()
5946
5947		self.find_bar.connect('show', lambda o: self.edit_bar.hide())
5948		self.find_bar.connect_after('hide', _show_edit_bar_on_hide_find)
5949
5950		# ...
5951		self.preferences.connect('changed', self.on_preferences_changed)
5952		self.on_preferences_changed()
5953
5954		self.text_style = ConfigManager.get_config_dict('style.conf')
5955		self.text_style.connect('changed', lambda o: self.on_text_style_changed())
5956		self.on_text_style_changed()
5957
5958		def assert_not_modified(page, *a):
5959			if page == self.page \
5960			and self.textview.get_buffer().get_modified():
5961				raise AssertionError('BUG: page changed while buffer changed as well')
5962				# not using assert here because it could be optimized away
5963
5964		for s in ('store-page', 'delete-page', 'move-page'):
5965			self.notebook.connect(s, assert_not_modified)
5966
5967		# Setup saving
5968		if_preferences = ConfigManager.preferences['GtkInterface']
5969		if_preferences.setdefault('autosave_timeout', 15)
5970		if_preferences.setdefault('autosave_use_thread', True)
5971		logger.debug('Autosave interval: %r - use threads: %r',
5972			if_preferences['autosave_timeout'],
5973			if_preferences['autosave_use_thread']
5974		)
5975		self._save_page_handler = SavePageHandler(
5976			self, notebook,
5977			lambda: self.page,
5978			timeout=if_preferences['autosave_timeout'],
5979			use_thread=if_preferences['autosave_use_thread']
5980		)
5981
5982		def on_focus_out_event(*a):
5983			self._save_page_handler.try_save_page()
5984			return False # don't block the event
5985		self.textview.connect('focus-out-event', on_focus_out_event)
5986
5987		PluginManager.insertedobjects.connect(
5988			'changed',
5989			self.on_insertedobjecttypemap_changed
5990		)
5991
5992		initialize_actiongroup(self, 'pageview')
5993		self._insertedobject_manager = InsertedObjectPageviewManager(self)
5994		self.__zim_extension_objects__.append(self._insertedobject_manager) # HACK to make actions discoverable
5995
5996	def grab_focus(self):
5997		self.textview.grab_focus()
5998
5999	def on_preferences_changed(self, *a):
6000		self.textview.set_cursor_visible(
6001			self.preferences['read_only_cursor'] or not self.readonly)
6002		self._set_edit_bar_visible(self.preferences['show_edit_bar'])
6003
6004	def on_text_style_changed(self, *a):
6005		'''(Re-)intializes properties for TextView, TextBuffer and
6006		TextTags based on the properties in the style config.
6007		'''
6008
6009		# TODO: reload buffer on style changed to make change visible
6010		#       now it is only visible on next page load
6011
6012		self.text_style['TextView'].define(
6013			bullet_icon_size=ConfigDefinitionConstant(
6014				'GTK_ICON_SIZE_MENU',
6015				Gtk.IconSize,
6016				'GTK_ICON_SIZE'
6017			)
6018		)
6019
6020		self.text_style['TextView'].setdefault('indent', TextBuffer.pixels_indent)
6021		self.text_style['TextView'].setdefault('tabs', None, int)
6022			# Don't set a default for 'tabs' as not to break pages that
6023			# were created before this setting was introduced.
6024		self.text_style['TextView'].setdefault('linespacing', 3)
6025		self.text_style['TextView'].setdefault('wrapped-lines-linespacing', 0)
6026		self.text_style['TextView'].setdefault('font', None, str)
6027		self.text_style['TextView'].setdefault('justify', None, str)
6028		#~ print self.text_style['TextView']
6029
6030		# Set properties for TextVIew
6031		if self.text_style['TextView']['tabs']:
6032			tabarray = Pango.TabArray(1, True) # Initial size, position in pixels
6033			tabarray.set_tab(0, Pango.TabAlign.LEFT, self.text_style['TextView']['tabs'])
6034				# We just set the size for one tab, apparently this gets
6035				# copied automaticlly when a new tab is created by the textbuffer
6036			self.textview.set_tabs(tabarray)
6037
6038		if self.text_style['TextView']['linespacing']:
6039			self.textview.set_pixels_below_lines(self.text_style['TextView']['linespacing'])
6040
6041		if self.text_style['TextView']['wrapped-lines-linespacing']:
6042			self.textview.set_pixels_inside_wrap(self.text_style['TextView']['wrapped-lines-linespacing'])
6043
6044		if self.text_style['TextView']['font']:
6045			font = Pango.FontDescription(self.text_style['TextView']['font'])
6046			self.textview.modify_font(font)
6047		else:
6048			self.textview.modify_font(None)
6049
6050		if self.text_style['TextView']['justify']:
6051			try:
6052				const = self.text_style['TextView']['justify']
6053				assert hasattr(gtk, const), 'No such constant: Gtk.%s' % const
6054				self.textview.set_justification(getattr(gtk, const))
6055			except:
6056				logger.exception('Exception while setting justification:')
6057
6058		# Set properties for TextBuffer
6059		TextBuffer.pixels_indent = self.text_style['TextView']['indent']
6060		TextBuffer.bullet_icon_size = self.text_style['TextView']['bullet_icon_size']
6061
6062		# Load TextTags
6063		testbuffer = Gtk.TextBuffer()
6064		for key in [k for k in list(self.text_style.keys()) if k.startswith('Tag ')]:
6065			section = self.text_style[key]
6066			defs = [(k, TextBuffer.tag_attributes[k])
6067				for k in section._input if k in TextBuffer.tag_attributes]
6068			section.define(defs)
6069			tag = key[4:]
6070
6071			try:
6072				if not tag in TextBuffer.tag_styles:
6073					raise AssertionError('No such tag: %s' % tag)
6074
6075				attrib = dict(i for i in list(section.items()) if i[1] is not None)
6076				if 'linespacing' in attrib:
6077					attrib['pixels-below-lines'] = attrib.pop('linespacing')
6078
6079				#~ print('TAG', tag, attrib)
6080				testtag = testbuffer.create_tag('style-' + tag, **attrib)
6081				if not testtag:
6082					raise AssertionError('Could not create tag: %s' % tag)
6083			except:
6084				logger.exception('Exception while parsing tag: %s:', tag)
6085			else:
6086				TextBuffer.tag_styles[tag].update(attrib)
6087
6088	def _connect_focus_event(self):
6089		# Connect to parent window here in a HACK to ensure
6090		# we do not hijack keybindings like ^C and ^V while we are not
6091		# focus (e.g. paste in find bar) Put it here to ensure
6092		# mainwindow is initialized.
6093		def set_actiongroup_sensitive(window, widget):
6094			#~ print('!! FOCUS SET:', widget)
6095			sensitive = widget is self.textview
6096
6097			# Enable keybindings and buttons for find functionality if find bar is in focus
6098			force_sensitive = ()
6099			if widget and widget.get_parent() is self.find_bar:
6100				force_sensitive = ("show_find", "find_next", "find_previous",
6101					"show_find_alt1", "find_next_alt1", "find_previous_alt1")
6102
6103			self._set_menuitems_sensitive(sensitive, force_sensitive)
6104
6105		window = self.get_toplevel()
6106		if window and window != self:
6107			window.connect('set-focus', set_actiongroup_sensitive)
6108
6109	def on_link_enter(self, view, link):
6110		href = normalize_file_uris(link['href'])
6111		if link_type(href) == 'page':
6112			href = HRef.new_from_wiki_link(href)
6113			path = self.notebook.pages.resolve_link(self.page, href)
6114			name = path.name + '#' + href.anchor if href.anchor else path.name
6115			self._overlay_label.set_text('Go to "%s"' % name)# T: tooltip text for links to pages
6116		else:
6117			self._overlay_label.set_text('Open "%s"' % href) # T: tooltip text for links to files/URLs etc.
6118
6119		self._overlay_label.show()
6120
6121	def on_link_leave(self, view, link):
6122		self._overlay_label.hide()
6123
6124	def set_edit_bar_visible(self, visible):
6125		self.preferences['show_edit_bar'] = visible
6126			# Bit of a hack, but prevents preferences to overwrite setting from Toolbar plugin
6127			# triggers _set_edit_bar_visible() via changed signal on preferences
6128
6129	def _set_edit_bar_visible(self, visible):
6130		self._edit_bar_visible = visible
6131		if not visible:
6132			self.edit_bar.hide()
6133		elif self.find_bar.get_property('visible') or self.readonly:
6134			self.edit_bar.hide()
6135		else:
6136			self.edit_bar.show()
6137
6138	def set_page(self, page, cursor=None):
6139		'''Set the current page to be displayed in the pageview
6140
6141		When the page does not yet exist a template is loaded for a
6142		new page which is obtained from
6143		L{Notebook.get_template()<zim.notebook.Notebook.get_template>}.
6144
6145		Exceptions while loading the page are handled gracefully with
6146		an error dialog and will result in the widget to be read-only
6147		and insensitive until the next page is loaded.
6148
6149		@param page: a L{Page} object
6150		@keyword cursor: optional cursor position (integer)
6151
6152		When the cursor is set to C{-1} the cursor will be placed at
6153		the end of the buffer.
6154
6155		If cursor is C{None} the cursor is set at the start of the page
6156		for existing pages or to the end of the template when the page
6157		does not yet exist.
6158		'''
6159		if self.page is None:
6160			# first run - bootstrap HACK
6161			self._connect_focus_event()
6162
6163		# Teardown connection with current page buffer
6164		prev_buffer = self.textview.get_buffer()
6165		finderstate = prev_buffer.finder.get_state()
6166		for id in self._buffer_signals:
6167			prev_buffer.disconnect(id)
6168		self._buffer_signals = ()
6169
6170		# now create the new buffer
6171		self._readonly_set_error = False
6172		try:
6173			self.page = page
6174			buffer = page.get_textbuffer(self._create_textbuffer)
6175			self._buffer_signals = (
6176				buffer.connect('end-insert-tree', self._hack_on_inserted_tree),
6177			)
6178			# TODO: also connect after insert widget ?
6179
6180			self.textview.set_buffer(buffer)
6181			self._hack_on_inserted_tree()
6182
6183			if cursor is None:
6184				cursor = -1 if buffer.showing_template else 0
6185
6186		except Exception as error:
6187			# Maybe corrupted parse tree - prevent page to be edited or saved back
6188			self._readonly_set_error = True
6189			self._update_readonly()
6190			self.set_sensitive(False)
6191			ErrorDialog(self, error).run()
6192		else:
6193
6194			# Finish hooking up the new page
6195			self.set_cursor_pos(cursor)
6196
6197			self._buffer_signals += (
6198				buffer.connect('textstyle-changed', lambda o, *a: self.emit('textstyle-changed', *a)),
6199				buffer.connect('modified-changed', lambda o: self.on_modified_changed(o)),
6200				buffer.connect_after('mark-set', self.do_mark_set),
6201			)
6202
6203			buffer.finder.set_state(*finderstate) # maintain state
6204
6205			self.set_sensitive(True)
6206			self._update_readonly()
6207
6208			self.emit('page-changed', self.page)
6209
6210	def _create_textbuffer(self, parsetree=None):
6211		# Callback for page.get_textbuffer
6212		buffer = TextBuffer(self.notebook, self.page, parsetree=parsetree)
6213
6214		readonly = self._readonly_set or self.notebook.readonly or self.page.readonly
6215			# Do not use "self.readonly" here, may not yet be intialized
6216		if parsetree is None and not readonly:
6217			# HACK: using None value instead of "hascontent" to distinguish
6218			# between a page without source and an existing empty page
6219			parsetree = self.notebook.get_template(self.page)
6220			buffer.set_parsetree(parsetree, showing_template=True)
6221			buffer.set_modified(False)
6222			# By setting this instead of providing to the TextBuffer constructor
6223			# this template can be undone
6224
6225		return buffer
6226
6227	def on_modified_changed(self, buffer):
6228		if buffer.get_modified():
6229			if self.readonly:
6230				logger.warn('Buffer edited while textview read-only - potential bug')
6231			else:
6232				if not (self._undo_history_queue and self._undo_history_queue[-1] is self.page):
6233					if self.page in self._undo_history_queue:
6234						self._undo_history_queue.remove(self.page)
6235					elif len(self._undo_history_queue) > MAX_PAGES_UNDO_STACK:
6236						self._undo_history_queue.pop(0)
6237					self._undo_history_queue.append(self.page)
6238
6239				buffer.showing_template = False
6240				self.emit('modified-changed')
6241				self._save_page_handler.queue_autosave()
6242
6243	def save_changes(self, write_if_not_modified=False):
6244		'''Save contents of the widget back to the page object and
6245		synchronize it with the notebook.
6246
6247		@param write_if_not_modified: If C{True} page will be written
6248		even if it is not changed. (This allows e.g. to force saving template
6249		content to disk without editing.)
6250		'''
6251		if write_if_not_modified or self.page.modified:
6252			self._save_page_handler.save_page_now()
6253		self._save_page_handler.wait_for_store_page_async()
6254
6255	def _hack_on_inserted_tree(self, *a):
6256		if self.textview._object_widgets:
6257			# Force resize of the scroll window, forcing a redraw to fix
6258			# glitch in allocation of embedded obejcts, see isse #642
6259			# Will add another timeout to rendering the page, increasing the
6260			# priority breaks the hack though. Which shows the glitch is
6261			# probably also happening in a drawing or resizing idle event
6262			#
6263			# Additional hook is needed for scrolling because re-rendering the
6264			# objects changes the textview size and thus looses the scrolled
6265			# position. Here idle didn't work so used a time-out with the
6266			# potential risk that in some cases the timeout is to fast or to slow.
6267
6268			self._hack_label.show_all()
6269			def scroll():
6270				self.scroll_cursor_on_screen()
6271				return False
6272
6273			def hide_hack():
6274				self._hack_label.hide()
6275				GLib.timeout_add(100, scroll)
6276				return False
6277
6278			GLib.idle_add(hide_hack)
6279		else:
6280			self._hack_label.hide()
6281
6282	def on_insertedobjecttypemap_changed(self, *a):
6283		self.save_changes()
6284		self.page.reload_textbuffer() # HACK - should not need to reload whole page just to load objects
6285
6286	def set_readonly(self, readonly):
6287		'''Set the widget read-only or not
6288
6289		Sets the read-only state but also update menu items etc. to
6290		reflect the new state.
6291
6292		@param readonly: C{True} or C{False} to set the read-only state
6293
6294		Effective read-only state seen in the C{self.readonly} attribute
6295		is in fact C{True} (so read-only) when either the widget itself
6296		OR the current page is read-only. So setting read-only to
6297		C{False} here may not immediately change C{self.readonly} if
6298		a read-only page is loaded.
6299		'''
6300		self._readonly_set = readonly
6301		self._update_readonly()
6302		self.emit('readonly-changed', readonly)
6303
6304	def _update_readonly(self):
6305		self.readonly = self._readonly_set \
6306			or self._readonly_set_error \
6307			or self.page is None \
6308			or self.notebook.readonly \
6309			or self.page.readonly
6310		self.textview.set_editable(not self.readonly)
6311		self.textview.set_cursor_visible(
6312			self.preferences['read_only_cursor'] or not self.readonly)
6313		self._set_menuitems_sensitive(True) # XXX not sure why this is here
6314
6315		if not self._edit_bar_visible:
6316			pass
6317		elif self.find_bar.get_property('visible') or self.readonly:
6318			self.edit_bar.hide()
6319		else:
6320			self.edit_bar.show()
6321
6322	def _set_menuitems_sensitive(self, sensitive, force_sensitive=()):
6323		'''Batch update global menu sensitivity while respecting
6324		sensitivities set due to cursor position, readonly state etc.
6325		'''
6326
6327		if sensitive:
6328			# partly overrule logic in window.toggle_editable()
6329			for action in self.actiongroup.list_actions():
6330				action.set_sensitive(
6331					action.zim_readonly or not self.readonly)
6332
6333			# update state for menu items for checkboxes and links
6334			buffer = self.textview.get_buffer()
6335			iter = buffer.get_insert_iter()
6336			mark = buffer.get_insert()
6337			self.do_mark_set(buffer, iter, mark)
6338		else:
6339			for action in self.actiongroup.list_actions():
6340				if action.get_name() not in force_sensitive:
6341					action.set_sensitive(False)
6342				else:
6343					action.set_sensitive(True)
6344
6345	def set_cursor_pos(self, pos):
6346		'''Set the cursor position in the buffer and scroll the TextView
6347		to show it
6348
6349		@param pos: the cursor position as an integer offset from the
6350		start of the buffer
6351
6352		As a special case when the cursor position is C{-1} the cursor
6353		is set at the end of the buffer.
6354		'''
6355		buffer = self.textview.get_buffer()
6356		if pos < 0:
6357			start, end = buffer.get_bounds()
6358			iter = end
6359		else:
6360			iter = buffer.get_iter_at_offset(pos)
6361
6362		buffer.place_cursor(iter)
6363		self.scroll_cursor_on_screen()
6364
6365	def get_cursor_pos(self):
6366		'''Get the cursor position in the buffer
6367
6368		@returns: the cursor position as an integer offset from the
6369		start of the buffer
6370		'''
6371		buffer = self.textview.get_buffer()
6372		iter = buffer.get_iter_at_mark(buffer.get_insert())
6373		return iter.get_offset()
6374
6375	def scroll_cursor_on_screen(self):
6376		buffer = self.textview.get_buffer()
6377		self.textview.scroll_to_mark(buffer.get_insert(), SCROLL_TO_MARK_MARGIN, False, 0, 0)
6378
6379	def set_scroll_pos(self, pos):
6380		pass # FIXME set scroll position
6381
6382	def get_scroll_pos(self):
6383		pass # FIXME get scroll position
6384
6385	def get_selection(self, format=None):
6386		'''Convenience method to get the text of the current selection.
6387
6388		@param format: format to use for the formatting of the returned
6389		text (e.g. 'wiki' or 'html'). If the format is C{None} only the
6390		text will be returned without any formatting.
6391
6392		@returns: text selection or C{None}
6393		'''
6394		buffer = self.textview.get_buffer()
6395		bounds = buffer.get_selection_bounds()
6396		if bounds:
6397			if format:
6398				tree = buffer.get_parsetree(bounds)
6399				dumper = get_format(format).Dumper()
6400				lines = dumper.dump(tree)
6401				return ''.join(lines)
6402			else:
6403				return bounds[0].get_text(bounds[1])
6404		else:
6405			return None
6406
6407	def get_word(self, format=None):
6408		'''Convenience method to get the word that is under the cursor
6409
6410		@param format: format to use for the formatting of the returned
6411		text (e.g. 'wiki' or 'html'). If the format is C{None} only the
6412		text will be returned without any formatting.
6413
6414		@returns: current word or C{None}
6415		'''
6416		buffer = self.textview.get_buffer()
6417		buffer.select_word()
6418		return self.get_selection(format)
6419
6420	def replace_selection(self, text, autoselect=None):
6421		assert autoselect in (None, 'word')
6422		buffer = self.textview.get_buffer()
6423		if not buffer.get_has_selection():
6424			if autoselect == 'word':
6425				buffer.select_word()
6426			else:
6427				raise AssertionError
6428
6429		bounds = buffer.get_selection_bounds()
6430		if bounds:
6431			start, end = bounds
6432			with buffer.user_action:
6433				buffer.delete(start, end)
6434				buffer.insert_at_cursor(''.join(text))
6435		else:
6436			buffer.insert_at_cursor(''.join(text))
6437
6438	def do_mark_set(self, buffer, iter, mark):
6439		'''
6440		@emits link-caret-enter
6441		@emits link-caret-leave
6442		'''
6443
6444		# Update menu items relative to cursor position
6445		if self.readonly or mark.get_name() != 'insert':
6446			return
6447
6448		# Set sensitivity of various menu options
6449		line = iter.get_line()
6450		bullet = buffer.get_bullet(line)
6451		if bullet and bullet in CHECKBOXES:
6452			self.actiongroup.get_action('uncheck_checkbox').set_sensitive(True)
6453			self.actiongroup.get_action('toggle_checkbox').set_sensitive(True)
6454			self.actiongroup.get_action('xtoggle_checkbox').set_sensitive(True)
6455			self.actiongroup.get_action('migrate_checkbox').set_sensitive(True)
6456			self.actiongroup.get_action('transmigrate_checkbox').set_sensitive(True)
6457		else:
6458			self.actiongroup.get_action('uncheck_checkbox').set_sensitive(False)
6459			self.actiongroup.get_action('toggle_checkbox').set_sensitive(False)
6460			self.actiongroup.get_action('xtoggle_checkbox').set_sensitive(False)
6461			self.actiongroup.get_action('migrate_checkbox').set_sensitive(False)
6462			self.actiongroup.get_action('transmigrate_checkbox').set_sensitive(False)
6463
6464		if buffer.get_link_tag(iter):
6465			self.actiongroup.get_action('remove_link').set_sensitive(True)
6466			self.actiongroup.get_action('edit_object').set_sensitive(True)
6467		elif buffer.get_image_data(iter):
6468			self.actiongroup.get_action('remove_link').set_sensitive(False)
6469			self.actiongroup.get_action('edit_object').set_sensitive(True)
6470		else:
6471			self.actiongroup.get_action('edit_object').set_sensitive(False)
6472			self.actiongroup.get_action('remove_link').set_sensitive(False)
6473
6474		# Emit signal if passing through a link
6475		link = buffer.get_link_data(iter)
6476		if link:
6477			if not self._caret_link:  # we enter link for the first time
6478				self.emit("link-caret-enter", link)
6479			elif self._caret_link != link:  # we changed the link
6480				self.emit("link-caret-leave", self._caret_link)
6481				self.emit("link-caret-enter", link)
6482		elif self._caret_link:  # we left the link
6483			self.emit("link-caret-leave", self._caret_link)
6484		self._caret_link = link
6485
6486	def do_textstyle_changed(self, styles):
6487		if not styles:  # styles can be None or a list
6488			styles = []
6489
6490		for name in self._format_toggle_actions:
6491			getattr(self, name).set_active(name[14:] in styles) # len("toggle_format_") = 14
6492
6493	def activate_link(self, link, new_window=False):
6494		if not isinstance(link, str):
6495			link = link['href']
6496
6497		href = normalize_file_uris(link)
6498			# can translate file:// -> smb:// so do before link_type()
6499			# FIXME implement function in notebook to resolve any link
6500			#       type and take care of this stuff ?
6501		logger.debug('Activate link: %s', link)
6502
6503		if link_type(link) == 'interwiki':
6504			target = interwiki_link(link)
6505			if target is not None:
6506				link = target
6507			else:
6508				name = link.split('?')[0]
6509				error = Error(_('No such wiki defined: %s') % name)
6510					# T: error when unknown interwiki link is clicked
6511				return ErrorDialog(self, error).run()
6512
6513		hints = {'new_window': new_window}
6514		self.emit_return_first('activate-link', link, hints)
6515
6516	def do_activate_link(self, link, hints):
6517		try:
6518			self._do_activate_link(link, hints)
6519		except:
6520			zim.errors.exception_handler(
6521				'Exception during activate-link(%r)' % ((link, hints),))
6522
6523	def _do_activate_link(self, link, hints):
6524		type = link_type(link)
6525
6526		if type == 'page':
6527			href = HRef.new_from_wiki_link(link)
6528			path = self.notebook.pages.resolve_link(self.page, href)
6529			self.navigation.open_page(path, anchor=href.anchor, new_window=hints.get('new_window', False))
6530		elif type == 'file':
6531			path = self.notebook.resolve_file(link, self.page)
6532			open_file(self, path)
6533		elif type == 'notebook':
6534			from zim.main import ZIM_APPLICATION
6535
6536			if link.startswith('zim+'):
6537				uri, pagename = link[4:], None
6538				if '?' in uri:
6539					uri, pagename = uri.split('?', 1)
6540
6541				ZIM_APPLICATION.run('--gui', uri, pagename)
6542
6543			else:
6544				ZIM_APPLICATION.run('--gui', FilePath(link).uri)
6545
6546		else:
6547			if type == 'mailto' and not link.startswith('mailto:'):
6548				link = 'mailto:' + link  # Enforce proper URI form
6549			open_url(self, link)
6550
6551		return True # handled
6552
6553	def navigate_to_anchor(self, name, select_line=False):
6554		"""Navigate to an anchor on the current page.
6555		@param name: The name of the anchor to navigate to
6556		@param select_line: Select the whole line after
6557		"""
6558		logger.debug("navigating to anchor '%s'", name)
6559		textview = self.textview
6560		buffer = textview.get_buffer()
6561		iter = buffer.find_anchor(name)
6562		if not iter:
6563			ErrorDialog(self, _('Id "%s" not found on the current page') % name).run() # T: error when anchor location in page not found
6564			return
6565		buffer.place_cursor(iter)
6566		if select_line:
6567			buffer.select_line()
6568		textview.scroll_to_mark(buffer.get_insert(), SCROLL_TO_MARK_MARGIN, False, 0, 0)
6569
6570	def do_populate_popup(self, menu):
6571		buffer = self.textview.get_buffer()
6572		if not buffer.get_has_selection():
6573			iter = self.textview._get_popup_menu_mark()
6574			if iter is None:
6575				self._default_do_populate_popup(menu)
6576			else:
6577				if iter.get_line_offset() == 1:
6578					iter.backward_char() # if clicked on right half of image, iter is after the image
6579				bullet = buffer.get_bullet_at_iter(iter)
6580				if bullet and bullet in CHECKBOXES:
6581					self._checkbox_do_populate_popup(menu, buffer, iter)
6582				else:
6583					self._default_do_populate_popup(menu)
6584		else:
6585			self._default_do_populate_popup(menu)
6586		menu.show_all()
6587
6588	def _default_do_populate_popup(self, menu):
6589		# Add custom tool
6590		# FIXME need way to (deep)copy widgets in the menu
6591		#~ toolmenu = uimanager.get_widget('/text_popup')
6592		#~ tools = [tool for tool in toolmenu.get_children()
6593					#~ if not isinstance(tool, Gtk.SeparatorMenuItem)]
6594		#~ print('>>> TOOLS', tools)
6595		#~ if tools:
6596			#~ menu.prepend(Gtk.SeparatorMenuItem())
6597			#~ for tool in tools:
6598				#~ tool.reparent(menu)
6599
6600		buffer = self.textview.get_buffer()
6601
6602		### Copy As option ###
6603		default = self.preferences['copy_format'].lower()
6604		copy_as_menu = Gtk.Menu()
6605		for label in COPY_FORMATS:
6606			if label.lower() == default:
6607				continue # Covered by default Copy action
6608
6609			format = zim.formats.canonical_name(label)
6610			item = Gtk.MenuItem.new_with_mnemonic(label)
6611			if buffer.get_has_selection():
6612				item.connect('activate',
6613					lambda o, f: self.textview.do_copy_clipboard(format=f),
6614					format)
6615			else:
6616				item.set_sensitive(False)
6617			copy_as_menu.append(item)
6618
6619		item = Gtk.MenuItem.new_with_mnemonic(_('Copy _As...')) # T: menu item for context menu of editor
6620		item.set_submenu(copy_as_menu)
6621		menu.insert(item, 2) # position after Copy in the standard menu - may not be robust...
6622			# FIXME get code from test to seek stock item
6623
6624		### Paste As
6625		item = Gtk.MenuItem.new_with_mnemonic(_('Paste As _Verbatim')) # T: menu item for context menu of editor
6626		item.set_sensitive(Clipboard.clipboard.wait_is_text_available())
6627		item.connect('activate', lambda o: self.textview.do_paste_clipboard(format='verbatim'))
6628		item.show_all()
6629		menu.insert(item, 4) # position after Paste in the standard menu - may not be robust...
6630			# FIXME get code from test to seek stock item
6631
6632		### Move text to new page ###
6633		item = Gtk.MenuItem.new_with_mnemonic(_('Move Selected Text...'))
6634			# T: Context menu item for pageview to move selected text to new/other page
6635		menu.insert(item, 7) # position after Copy in the standard menu - may not be robust...
6636			# FIXME get code from test to seek stock item
6637
6638		if buffer.get_has_selection():
6639			item.connect('activate',
6640				lambda o: MoveTextDialog(self, self.notebook, self.page, buffer, self.navigation).run())
6641		else:
6642			item.set_sensitive(False)
6643		###
6644
6645		iter = self.textview._get_popup_menu_mark()
6646			# This iter can be either cursor position or pointer
6647			# position, depending on how the menu was called
6648		if iter is None:
6649			return
6650
6651		def _copy_link_to_anchor(o, anchor, text):
6652			path = self.page
6653			Clipboard.set_pagelink(self.notebook, path, anchor, text)
6654			SelectionClipboard.set_pagelink(self.notebook, path, anchor, text)
6655
6656		# copy link to anchor or heading
6657		item = Gtk.MenuItem.new_with_mnemonic(_('Copy _link to this location')) # T: menu item to copy link to achor location in page
6658		anchor = buffer.get_anchor_for_location(iter)
6659		if anchor:
6660			heading_text = buffer._get_heading_text(iter) # can be None if not a heading
6661			item.connect('activate', _copy_link_to_anchor, anchor, heading_text)
6662		else:
6663			item.set_sensitive(False)
6664		menu.insert(item, 3)
6665
6666		# link
6667		link = buffer.get_link_data(iter)
6668		if link:
6669			type = link_type(link['href'])
6670			if type == 'file':
6671				file = link['href']
6672			else:
6673				file = None
6674		else:
6675			image = buffer.get_image_data(iter)
6676			if image is None:
6677				# Maybe we clicked right side of an image
6678				iter.backward_char()
6679				image = buffer.get_image_data(iter)
6680
6681			if image:
6682				type = 'image'
6683				file = image['src']
6684			else:
6685				return # No link or image
6686
6687		if file:
6688			file = self.notebook.resolve_file(file, self.page)
6689
6690		menu.prepend(Gtk.SeparatorMenuItem())
6691
6692		# remove link
6693		if link:
6694			item = Gtk.MenuItem.new_with_mnemonic(_('_Remove Link'))
6695			item.connect('activate', lambda o: self.remove_link(iter=iter))
6696			item.set_sensitive(not self.readonly)
6697			menu.prepend(item)
6698
6699		# edit
6700		if type == 'image':
6701			item = Gtk.MenuItem.new_with_mnemonic(_('_Edit Properties')) # T: menu item in context menu for image
6702		else:
6703			item = Gtk.MenuItem.new_with_mnemonic(_('_Edit Link')) # T: menu item in context menu
6704		item.connect('activate', lambda o: self.edit_object(iter=iter))
6705		item.set_sensitive(not self.readonly)
6706		menu.prepend(item)
6707
6708		# copy
6709		def set_pagelink(o, path, anchor):
6710			Clipboard.set_pagelink(self.notebook, path, anchor)
6711			SelectionClipboard.set_pagelink(self.notebook, path, anchor)
6712
6713		def set_interwikilink(o, data):
6714			href, url = data
6715			Clipboard.set_interwikilink(href, url)
6716			SelectionClipboard.set_interwikilink(href, url)
6717
6718		def set_uri(o, uri):
6719			Clipboard.set_uri(uri)
6720			SelectionClipboard.set_uri(uri)
6721
6722		if type == 'page':
6723			item = Gtk.MenuItem.new_with_mnemonic(_('Copy _Link')) # T: context menu item
6724			href = HRef.new_from_wiki_link(link['href'])
6725			path = self.notebook.pages.resolve_link(self.page, href)
6726			item.connect('activate', set_pagelink, path, href.anchor)
6727		elif type == 'interwiki':
6728			item = Gtk.MenuItem.new_with_mnemonic(_('Copy _Link')) # T: context menu item
6729			url = interwiki_link(link['href'])
6730			item.connect('activate', set_interwikilink, (link['href'], url))
6731		elif type == 'mailto':
6732			item = Gtk.MenuItem.new_with_mnemonic(_('Copy Email Address')) # T: context menu item
6733			item.connect('activate', set_uri, file or link['href'])
6734		else:
6735			item = Gtk.MenuItem.new_with_mnemonic(_('Copy _Link')) # T: context menu item
6736			item.connect('activate', set_uri, file or link['href'])
6737		menu.prepend(item)
6738
6739		menu.prepend(Gtk.SeparatorMenuItem())
6740
6741		# open with & open folder
6742		if type in ('file', 'image') and file:
6743			item = Gtk.MenuItem.new_with_mnemonic(_('Open Folder'))
6744				# T: menu item to open containing folder of files
6745			menu.prepend(item)
6746			dir = file.dir
6747			if dir.exists():
6748				item.connect('activate', lambda o: open_file(self, dir))
6749			else:
6750				item.set_sensitive(False)
6751
6752			item = Gtk.MenuItem.new_with_mnemonic(_('Open With...'))
6753				# T: menu item for sub menu with applications
6754			menu.prepend(item)
6755			if file.exists():
6756				submenu = OpenWithMenu(self, file)
6757				item.set_submenu(submenu)
6758			else:
6759				item.set_sensitive(False)
6760		elif type not in ('page', 'notebook', 'interwiki', 'file', 'image'): # urls etc.
6761			# FIXME: for interwiki inspect final link and base
6762			# open with menu based on that url type
6763			item = Gtk.MenuItem.new_with_mnemonic(_('Open With...'))
6764			menu.prepend(item)
6765			submenu = OpenWithMenu(self, link['href'])
6766			if submenu.get_children():
6767				item.set_submenu(submenu)
6768			else:
6769				item.set_sensitive(False)
6770
6771		# open in new window
6772		if type == 'page':
6773			item = Gtk.MenuItem.new_with_mnemonic(_('Open in New _Window'))
6774				# T: menu item to open a link
6775			item.connect(
6776				'activate', lambda o: self.activate_link(link, new_window=True))
6777			menu.prepend(item)
6778
6779		# open
6780		if type == 'image':
6781			link = {'href': file.uri}
6782
6783		item = Gtk.MenuItem.new_with_mnemonic(_('_Open'))
6784			# T: menu item to open a link or file
6785		if file and not file.exists():
6786			item.set_sensitive(False)
6787		else:
6788			item.connect_object(
6789				'activate', PageView.activate_link, self, link)
6790		menu.prepend(item)
6791
6792	def _checkbox_do_populate_popup(self, menu, buffer, iter):
6793		line = iter.get_line()
6794
6795		menu.prepend(Gtk.SeparatorMenuItem())
6796
6797		for bullet, label in (
6798			(TRANSMIGRATED_BOX, _('Check Checkbox \'<\'')), # T: popup menu menuitem
6799			(MIGRATED_BOX, _('Check Checkbox \'>\'')), # T: popup menu menuitem
6800			(XCHECKED_BOX, _('Check Checkbox \'X\'')), # T: popup menu menuitem
6801			(CHECKED_BOX, _('Check Checkbox \'V\'')), # T: popup menu menuitem
6802			(UNCHECKED_BOX, _('Un-check Checkbox')), # T: popup menu menuitem
6803		):
6804			item = Gtk.ImageMenuItem(bullet_types[bullet])
6805			item.set_label(label)
6806			item.connect('activate', callback(buffer.set_bullet, line, bullet))
6807			menu.prepend(item)
6808
6809		menu.show_all()
6810
6811	@action(_('_Save'), '<Primary>S', menuhints='edit') # T: Menu item
6812	def save_page(self):
6813		'''Menu action to save the current page.
6814
6815		Can result in a L{SavePageErrorDialog} when there is an error
6816		while saving a page. If that dialog is cancelled by the user,
6817		the page may not be saved after all.
6818		'''
6819		self.save_changes(write_if_not_modified=True)
6820
6821	@action(_('_Reload'), '<Primary>R') # T: Menu item
6822	def reload_page(self):
6823		'''Menu action to reload the current page. Will first try
6824		to save any unsaved changes, then reload the page from disk.
6825		'''
6826		cursor = self.get_cursor_pos()
6827		self.save_changes()
6828		self.page.reload_textbuffer()
6829		self.set_cursor_pos(cursor)
6830
6831	@action(_('_Undo'), '<Primary>Z', menuhints='edit') # T: Menu item
6832	def undo(self):
6833		'''Menu action to undo a single step'''
6834		buffer = self.textview.get_buffer()
6835		buffer.undostack.undo()
6836		self.scroll_cursor_on_screen()
6837
6838	@action(_('_Redo'), '<Primary><shift>Z', alt_accelerator='<Primary>Y', menuhints='edit') # T: Menu item
6839	def redo(self):
6840		'''Menu action to redo a single step'''
6841		buffer = self.textview.get_buffer()
6842		buffer.undostack.redo()
6843		self.scroll_cursor_on_screen()
6844
6845	@action(_('Cu_t'), '<Primary>X', menuhints='edit') # T: Menu item
6846	def cut(self):
6847		'''Menu action for cut to clipboard'''
6848		self.textview.emit('cut-clipboard')
6849
6850	@action(_('_Copy'), '<Primary>C', menuhints='edit') # T: Menu item
6851	def copy(self):
6852		'''Menu action for copy to clipboard'''
6853		self.textview.emit('copy-clipboard')
6854
6855	@action(_('_Paste'), '<Primary>V', menuhints='edit') # T: Menu item
6856	def paste(self):
6857		'''Menu action for paste from clipboard'''
6858		self.textview.emit('paste-clipboard')
6859
6860	@action(_('_Delete'), menuhints='edit') # T: Menu item
6861	def delete(self):
6862		'''Menu action for delete'''
6863		self.textview.emit('delete-from-cursor', Gtk.DeleteType.CHARS, 1)
6864
6865	@action(_('Un-check Checkbox'), verb_icon=STOCK_UNCHECKED_BOX, menuhints='edit') # T: Menu item
6866	def uncheck_checkbox(self):
6867		buffer = self.textview.get_buffer()
6868		recurs = self.preferences['recursive_checklist']
6869		buffer.toggle_checkbox_for_cursor_or_selection(UNCHECKED_BOX, recurs)
6870
6871	@action(_('Toggle Checkbox \'V\''), 'F12', verb_icon=STOCK_CHECKED_BOX, menuhints='edit') # T: Menu item
6872	def toggle_checkbox(self):
6873		'''Menu action to toggle checkbox at the cursor or in current
6874		selected text
6875		'''
6876		buffer = self.textview.get_buffer()
6877		recurs = self.preferences['recursive_checklist']
6878		buffer.toggle_checkbox_for_cursor_or_selection(CHECKED_BOX, recurs)
6879
6880	@action(_('Toggle Checkbox \'X\''), '<shift>F12', verb_icon=STOCK_XCHECKED_BOX, menuhints='edit') # T: Menu item
6881	def xtoggle_checkbox(self):
6882		'''Menu action to toggle checkbox at the cursor or in current
6883		selected text
6884		'''
6885		buffer = self.textview.get_buffer()
6886		recurs = self.preferences['recursive_checklist']
6887		buffer.toggle_checkbox_for_cursor_or_selection(XCHECKED_BOX, recurs)
6888
6889	@action(_('Toggle Checkbox \'>\''), verb_icon=STOCK_MIGRATED_BOX, menuhints='edit') # T: Menu item
6890	def migrate_checkbox(self):
6891		'''Menu action to toggle checkbox at the cursor or in current
6892		selected text
6893		'''
6894		buffer = self.textview.get_buffer()
6895		recurs = self.preferences['recursive_checklist']
6896		buffer.toggle_checkbox_for_cursor_or_selection(MIGRATED_BOX, recurs)
6897
6898	@action(_('Toggle Checkbox \'<\''), verb_icon=STOCK_TRANSMIGRATED_BOX, menuhints='edit') # T: Menu item
6899	def transmigrate_checkbox(self):
6900		'''Menu action to toggle checkbox at the cursor or in current
6901		selected text
6902		'''
6903		buffer = self.textview.get_buffer()
6904		recurs = self.preferences['recursive_checklist']
6905		buffer.toggle_checkbox_for_cursor_or_selection(TRANSMIGRATED_BOX, recurs)
6906
6907	@action(_('_Edit Link or Object...'), '<Primary>E', menuhints='edit') # T: Menu item
6908	def edit_object(self, iter=None):
6909		'''Menu action to trigger proper edit dialog for the current
6910		object at the cursor
6911
6912		Can show e.g. L{InsertLinkDialog} for a link, C{EditImageDialog}
6913		for the a image, or a plugin dialog for e.g. an equation.
6914
6915		@param iter: C{TextIter} for an alternative cursor position
6916		'''
6917		buffer = self.textview.get_buffer()
6918		if iter:
6919			buffer.place_cursor(iter)
6920
6921		iter = buffer.get_iter_at_mark(buffer.get_insert())
6922		if buffer.get_link_tag(iter):
6923			return InsertLinkDialog(self, self).run()
6924
6925		image = buffer.get_image_data(iter)
6926		anchor = buffer.get_objectanchor(iter)
6927		if not (image or (anchor and isinstance(anchor, PluginInsertedObjectAnchor))):
6928			iter.backward_char() # maybe we clicked right side of an image
6929			image = buffer.get_image_data(iter)
6930			anchor = buffer.get_objectanchor(iter)
6931
6932		if image:
6933			EditImageDialog(self, buffer, self.notebook, self.page).run()
6934		elif anchor and isinstance(anchor, PluginInsertedObjectAnchor):
6935			widget = anchor.get_widgets()[0]
6936			try:
6937				widget.edit_object()
6938			except NotImplementedError:
6939				return False
6940			else:
6941				return True
6942		else:
6943			return False
6944
6945	@action(_('_Remove Link'), menuhints='edit') # T: Menu item
6946	def remove_link(self, iter=None):
6947		'''Menu action to remove link object at the current cursor position
6948
6949		@param iter: C{TextIter} for an alternative cursor position
6950		'''
6951		buffer = self.textview.get_buffer()
6952
6953		if not buffer.get_has_selection() \
6954		or (iter and not buffer.iter_in_selection(iter)):
6955			if iter:
6956				buffer.place_cursor(iter)
6957			buffer.select_link()
6958
6959		bounds = buffer.get_selection_bounds()
6960		if bounds:
6961			buffer.remove_link(*bounds)
6962
6963	@action(_('Copy Line'), accelerator='<Primary><Shift>C', menuhints='edit') # T: menu item to copy current line to clipboard
6964	def copy_current_line(self):
6965		'''Menu action to copy the current line to the clipboard'''
6966		buffer = self.textview.get_buffer()
6967		mark = buffer.create_mark(None, buffer.get_insert_iter())
6968		buffer.select_line()
6969
6970		if buffer.get_has_selection():
6971			bounds = buffer.get_selection_bounds()
6972			tree = buffer.get_parsetree(bounds)
6973			Clipboard.set_parsetree(self.notebook, self.page, tree)
6974			buffer.unset_selection()
6975			buffer.place_cursor(buffer.get_iter_at_mark(mark))
6976
6977		buffer.delete_mark(mark)
6978
6979	@action(_('Date and Time...'), accelerator='<Primary>D', menuhints='insert') # T: Menu item
6980	def insert_date(self):
6981		'''Menu action to insert a date, shows the L{InsertDateDialog}'''
6982		InsertDateDialog(self, self.textview.get_buffer(), self.notebook, self.page).run()
6983
6984	def insert_object(self, attrib, data):
6985		buffer = self.textview.get_buffer()
6986		with buffer.user_action:
6987			buffer.insert_object_at_cursor(attrib, data)
6988
6989	def insert_object_model(self, otype, model):
6990		buffer = self.textview.get_buffer()
6991		with buffer.user_action:
6992			buffer.insert_object_model_at_cursor(otype, model)
6993
6994	@action(_('Horizontal _Line'), menuhints='insert') # T: Menu item for Insert menu
6995	def insert_line(self):
6996		'''Menu action to insert a line at the cursor position'''
6997		buffer = self.textview.get_buffer()
6998		with buffer.user_action:
6999			buffer.insert_objectanchor_at_cursor(LineSeparatorAnchor())
7000			# Add newline after line separator widget.
7001			buffer.insert_at_cursor('\n')
7002
7003	@action(_('_Image...'), menuhints='insert') # T: Menu item
7004	def show_insert_image(self, file=None):
7005		'''Menu action to insert an image, shows the L{InsertImageDialog}
7006		@param file: optional file to suggest in the dialog
7007		'''
7008		InsertImageDialog(self, self.textview.get_buffer(), self.notebook, self.page, file).run()
7009
7010	@action(_('_Attachment...'), verb_icon='zim-attachment', menuhints='insert') # T: Menu item
7011	def attach_file(self, file=None):
7012		'''Menu action to show the L{AttachFileDialog}
7013		@param file: optional file to suggest in the dialog
7014		'''
7015		AttachFileDialog(self, self.textview.get_buffer(), self.notebook, self.page, file).run()
7016
7017	def insert_image(self, file):
7018		'''Insert a image
7019		@param file: the image file to insert. If C{file} does not exist or
7020		isn't an image, a "broken image" icon will be shown
7021		'''
7022		file = adapt_from_newfs(file)
7023		assert isinstance(file, File)
7024		src = self.notebook.relative_filepath(file, self.page) or file.uri
7025		self.textview.get_buffer().insert_image_at_cursor(file, src)
7026
7027	@action(_('Bulle_t List'), menuhints='insert') # T: Menu item
7028	def insert_bullet_list(self):
7029		'''Menu action insert a bullet item at the cursor'''
7030		self._start_bullet(BULLET)
7031
7032	@action(_('_Numbered List'), menuhints='insert') # T: Menu item
7033	def insert_numbered_list(self):
7034		'''Menu action insert a numbered list item at the cursor'''
7035		self._start_bullet(NUMBER_BULLET)
7036
7037	@action(_('Checkbo_x List'), menuhints='insert') # T: Menu item
7038	def insert_checkbox_list(self):
7039		'''Menu action insert an open checkbox at the cursor'''
7040		self._start_bullet(UNCHECKED_BOX)
7041
7042	def _start_bullet(self, bullet_type):
7043		buffer = self.textview.get_buffer()
7044		line = buffer.get_insert_iter().get_line()
7045
7046		with buffer.user_action:
7047			iter = buffer.get_iter_at_line(line)
7048			buffer.insert(iter, '\n')
7049			buffer.set_bullet(line, bullet_type)
7050			iter = buffer.get_iter_at_line(line)
7051			iter.forward_to_line_end()
7052			buffer.place_cursor(iter)
7053
7054	@action(_('Bulle_t List'), menuhints='edit') # T: Menu item,
7055	def apply_format_bullet_list(self):
7056		'''Menu action to format selection as bullet list'''
7057		self._apply_bullet(BULLET)
7058
7059	@action(_('_Numbered List'), menuhints='edit') # T: Menu item,
7060	def apply_format_numbered_list(self):
7061		'''Menu action to format selection as numbered list'''
7062		self._apply_bullet(NUMBER_BULLET)
7063
7064	@action(_('Checkbo_x List'), menuhints='edit') # T: Menu item,
7065	def apply_format_checkbox_list(self):
7066		'''Menu action to format selection as checkbox list'''
7067		self._apply_bullet(UNCHECKED_BOX)
7068
7069	@action(_('_Remove List'), menuhints='edit') # T: Menu item,
7070	def clear_list_format(self):
7071		'''Menu action to remove list formatting'''
7072		self._apply_bullet(None)
7073
7074	def _apply_bullet(self, bullet_type):
7075		buffer = self.textview.get_buffer()
7076		bounds = buffer.get_selection_bounds()
7077		if bounds:
7078			# set for selected lines & restore selection
7079			start_mark = buffer.create_mark(None, bounds[0], left_gravity=True)
7080			end_mark = buffer.create_mark(None, bounds[1], left_gravity=False)
7081			try:
7082				buffer.foreach_line_in_selection(buffer.set_bullet, bullet_type)
7083			except:
7084				raise
7085			else:
7086				start = buffer.get_iter_at_mark(start_mark)
7087				end = buffer.get_iter_at_mark(end_mark)
7088				buffer.select_range(start, end)
7089			finally:
7090				buffer.delete_mark(start_mark)
7091				buffer.delete_mark(end_mark)
7092		else:
7093			# set for current line
7094			line = buffer.get_insert_iter().get_line()
7095			buffer.set_bullet(line, bullet_type)
7096
7097	@action(_('Text From _File...'), menuhints='insert') # T: Menu item
7098	def insert_text_from_file(self):
7099		'''Menu action to show a L{InsertTextFromFileDialog}'''
7100		InsertTextFromFileDialog(self, self.textview.get_buffer(), self.notebook, self.page).run()
7101
7102	def insert_links(self, links):
7103		'''Non-interactive method to insert one or more links
7104
7105		Inserts the links separated by newlines. Intended e.g. for
7106		drag-and-drop or copy-paste actions of e.g. files from a
7107		file browser.
7108
7109		@param links: list of links, either as string, L{Path} objects,
7110		or L{File} objects
7111		'''
7112		links = list(map(adapt_from_newfs, links))
7113		for i in range(len(links)):
7114			if isinstance(links[i], Path):
7115				links[i] = links[i].name
7116				continue
7117			elif isinstance(links[i], File):
7118				file = links[i]
7119			else:
7120				type = link_type(links[i])
7121				if type == 'file':
7122					file = File(links[i])
7123				else:
7124					continue # not a file
7125			links[i] = self.notebook.relative_filepath(file, self.page) or file.uri
7126
7127		if len(links) == 1:
7128			sep = ' '
7129		else:
7130			sep = '\n'
7131
7132		buffer = self.textview.get_buffer()
7133		with buffer.user_action:
7134			if buffer.get_has_selection():
7135				start, end = buffer.get_selection_bounds()
7136				buffer.delete(start, end)
7137			for link in links:
7138				buffer.insert_link_at_cursor(link, link)
7139				buffer.insert_at_cursor(sep)
7140
7141	@action(_('_Link...'), '<Primary>L', verb_icon='zim-link', menuhints='insert') # T: Menu item
7142	def insert_link(self):
7143		'''Menu item to show the L{InsertLinkDialog}'''
7144		InsertLinkDialog(self, self).run()
7145
7146	def _update_new_file_submenu(self, action):
7147		dir = self.preferences['file_templates_folder']
7148		if isinstance(dir, str):
7149			dir = Dir(dir)
7150
7151		items = []
7152		if dir.exists():
7153			def handler(menuitem, file):
7154				self.insert_new_file(file)
7155
7156			for name in dir.list(): # FIXME could use list objects here
7157				file = dir.file(name)
7158				if file.exists(): # it is a file
7159					name = file.basename
7160					if '.' in name:
7161						name, x = name.rsplit('.', 1)
7162					name = name.replace('_', ' ')
7163					item = Gtk.MenuItem.new_with_mnemonic(name)
7164						# TODO mimetype icon would be nice to have
7165					item.connect('activate', handler, file)
7166					item.zim_new_file_action = True
7167					items.append(item)
7168
7169		if not items:
7170			item = Gtk.MenuItem.new_with_mnemonic(_('No templates installed'))
7171				# T: message when no file templates are found in ~/Templates
7172			item.set_sensitive(False)
7173			item.zim_new_file_action = True
7174			items.append(item)
7175
7176
7177		for widget in action.get_proxies():
7178			if hasattr(widget, 'get_submenu'):
7179				menu = widget.get_submenu()
7180				if not menu:
7181					continue
7182
7183				# clear old items
7184				for item in menu.get_children():
7185					if hasattr(item, 'zim_new_file_action'):
7186						menu.remove(item)
7187
7188				# add new ones
7189				populate_popup_add_separator(menu, prepend=True)
7190				for item in reversed(items):
7191					menu.prepend(item)
7192
7193				# and finish up
7194				menu.show_all()
7195
7196	def insert_new_file(self, template, basename=None):
7197		dir = self.notebook.get_attachments_dir(self.page)
7198
7199		if not basename:
7200			basename = NewFileDialog(self, template.basename).run()
7201			if basename is None:
7202				return # cancelled
7203
7204		file = dir.new_file(basename)
7205		template.copyto(file)
7206
7207		# Same logic as in AttachFileDialog
7208		# TODO - incorporate in the insert_links function ?
7209		if file.isimage():
7210			ok = self.insert_image(file)
7211			if not ok: # image type not supported?
7212				logger.info('Could not insert image: %s', file)
7213				self.insert_links([file])
7214		else:
7215			self.insert_links([file])
7216
7217		#~ open_file(self, file) # FIXME should this be optional ?
7218
7219	@action(_('File _Templates...')) # T: Menu item in "Insert > New File Attachment" submenu
7220	def open_file_templates_folder(self):
7221		'''Menu action to open the templates folder'''
7222		dir = self.preferences['file_templates_folder']
7223		if isinstance(dir, str):
7224			dir = Dir(dir)
7225
7226		if dir.exists():
7227			open_file(self, dir)
7228		else:
7229			path = dir.user_path or dir.path
7230			question = (
7231				_('Create folder?'),
7232					# T: Heading in a question dialog for creating a folder
7233				_('The folder\n%s\ndoes not yet exist.\nDo you want to create it now?')
7234					% path
7235			)
7236					# T: Text in a question dialog for creating a folder, %s is the folder path
7237			create = QuestionDialog(self, question).run()
7238			if create:
7239				dir.touch()
7240				open_file(self, dir)
7241
7242	@action(_('_Clear Formatting'), accelerator='<Primary>9', menuhints='edit', verb_icon='edit-clear-all-symbolic') # T: Menu item
7243	def clear_formatting(self):
7244		'''Menu item to remove formatting from current (auto-)selection'''
7245		buffer = self.textview.get_buffer()
7246		mark = buffer.create_mark(None, buffer.get_insert_iter())
7247		selected = self.autoselect()
7248
7249		if buffer.get_has_selection():
7250			start, end = buffer.get_selection_bounds()
7251			buffer.remove_textstyle_tags(start, end)
7252			if selected:
7253				# If we keep the selection we can not continue typing
7254				# so remove the selection and restore the cursor.
7255				buffer.unset_selection()
7256				buffer.place_cursor(buffer.get_iter_at_mark(mark))
7257		else:
7258			buffer.set_textstyles(None)
7259
7260		buffer.delete_mark(mark)
7261
7262	@action(_('_Remove Heading'), menuhints='edit') # T: Menu item
7263	def clear_heading_format(self):
7264		'''Menu item to remove heading'''
7265		buffer = self.textview.get_buffer()
7266		mark = buffer.create_mark(None, buffer.get_insert_iter())
7267		selected = self.autoselect(selectline=True)
7268		if buffer.get_has_selection():
7269			start, end = buffer.get_selection_bounds()
7270			buffer.smart_remove_tags(_is_heading_tag, start, end)
7271			if selected:
7272				buffer.unset_selection()
7273				buffer.place_cursor(buffer.get_iter_at_mark(mark))
7274		else:
7275			buffer.set_textstyles(None)
7276
7277		buffer.delete_mark(mark)
7278
7279	def do_toggle_format_action_alt(self, active, action):
7280		self.do_toggle_format_action(action)
7281
7282	def do_toggle_format_action(self, action):
7283		'''Handler that catches all actions to apply and/or toggle formats'''
7284		if isinstance(action, str):
7285			name = action
7286		else:
7287			name = action.get_name()
7288		logger.debug('Action: %s (toggle_format action)', name)
7289		if name.startswith('apply_format_'):
7290			style = name[13:]
7291		elif name.startswith('toggle_format_'):
7292			style = name[14:]
7293		else:
7294			assert False, "BUG: don't known this action"
7295		self.toggle_format(style)
7296
7297	def toggle_format(self, format):
7298		'''Toggle the format for the current (auto-)selection or new
7299		insertions at the current cursor position
7300
7301		When the cursor is at the begin or in the middle of a word and there is
7302		not selection, the word is selected automatically to toggle the format.
7303		For headings and other line based formats auto-selects the whole line.
7304
7305		This is the handler for all the format actions.
7306
7307		@param format: the format style name (e.g. "h1", "strong" etc.)
7308		'''
7309		buffer = self.textview.get_buffer()
7310		selected = False
7311		mark = buffer.create_mark(None, buffer.get_insert_iter())
7312
7313		if format in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
7314			selected = self.autoselect(selectline=True)
7315		else:
7316			# Check formatting is consistent left and right
7317			iter = buffer.get_insert_iter()
7318			format_left = format in [t.zim_tag for t in buffer.iter_get_zim_tags(iter)]
7319			format_right = format in [t.zim_tag for t in filter(_is_zim_tag, iter.get_tags())]
7320			if format_left is format_right:
7321				selected = self.autoselect(selectline=False)
7322
7323		buffer.toggle_textstyle(format)
7324
7325		if selected:
7326			# If we keep the selection we can not continue typing
7327			# so remove the selection and restore the cursor.
7328			buffer.unset_selection()
7329			buffer.place_cursor(buffer.get_iter_at_mark(mark))
7330		buffer.delete_mark(mark)
7331
7332	def autoselect(self, selectline=False):
7333		'''Auto select either a word or a line.
7334
7335		Does not do anything if a selection is present already or when
7336		the preference for auto-select is set to False.
7337
7338		@param selectline: if C{True} auto-select a whole line,
7339		only auto-select a single word otherwise
7340		@returns: C{True} when this function changed the selection.
7341		'''
7342		if not self.preferences['autoselect']:
7343			return False
7344
7345		buffer = self.textview.get_buffer()
7346		if buffer.get_has_selection():
7347			if selectline:
7348				start, end = buffer.get_selection_bounds()
7349				return buffer.select_lines(start.get_line(), end.get_line())
7350			else:
7351				return buffer.strip_selection()
7352		elif selectline:
7353			return buffer.select_line()
7354		else:
7355			return buffer.select_word()
7356
7357	def find(self, string, flags=0):
7358		'''Find some string in the text, scroll there and select it
7359
7360		@param string: the text to find
7361		@param flags: options for find behavior, see L{TextFinder.find()}
7362		'''
7363		self.hide_find() # remove previous highlighting etc.
7364		buffer = self.textview.get_buffer()
7365		buffer.finder.find(string, flags)
7366		self.textview.scroll_to_mark(buffer.get_insert(), SCROLL_TO_MARK_MARGIN, False, 0, 0)
7367
7368	@action(_('_Find...'), '<Primary>F', alt_accelerator='<Primary>F3') # T: Menu item
7369	def show_find(self, string=None, flags=0, highlight=False):
7370		'''Show the L{FindBar} widget
7371
7372		@param string: the text to find
7373		@param flags: options for find behavior, see L{TextFinder.find()}
7374		@param highlight: if C{True} highlight the results
7375		'''
7376		self.find_bar.show()
7377		if string:
7378			self.find_bar.find(string, flags, highlight)
7379			self.textview.grab_focus()
7380		else:
7381			self.find_bar.set_from_buffer()
7382			self.find_bar.grab_focus()
7383
7384	def hide_find(self):
7385		'''Hide the L{FindBar} widget'''
7386		self.find_bar.hide()
7387		self.textview.grab_focus()
7388
7389	@action(_('Find Ne_xt'), accelerator='<Primary>G', alt_accelerator='F3') # T: Menu item
7390	def find_next(self):
7391		'''Menu action to skip to next match'''
7392		self.find_bar.show()
7393		self.find_bar.find_next()
7394
7395	@action(_('Find Pre_vious'), accelerator='<Primary><shift>G', alt_accelerator='<shift>F3') # T: Menu item
7396	def find_previous(self):
7397		'''Menu action to go back to previous match'''
7398		self.find_bar.show()
7399		self.find_bar.find_previous()
7400
7401	@action(_('_Replace...'), '<Primary>H', menuhints='edit') # T: Menu item
7402	def show_find_and_replace(self):
7403		'''Menu action to show the L{FindAndReplaceDialog}'''
7404		dialog = FindAndReplaceDialog.unique(self, self, self.textview)
7405		dialog.set_from_buffer()
7406		dialog.present()
7407
7408	@action(_('Word Count...')) # T: Menu item
7409	def show_word_count(self):
7410		'''Menu action to show the L{WordCountDialog}'''
7411		WordCountDialog(self).run()
7412
7413	@action(_('_Zoom In'), '<Primary>plus', alt_accelerator='<Primary>equal') # T: Menu item
7414	def zoom_in(self):
7415		'''Menu action to increase the font size'''
7416		self._zoom_increase_decrease_font_size(+1)
7417
7418	@action(_('Zoom _Out'), '<Primary>minus') # T: Menu item
7419	def zoom_out(self):
7420		'''Menu action to decrease the font size'''
7421		self._zoom_increase_decrease_font_size(-1)
7422
7423	def _zoom_increase_decrease_font_size(self, plus_or_minus):
7424		style = self.text_style
7425		if self.text_style['TextView']['font']:
7426			font = Pango.FontDescription(self.text_style['TextView']['font'])
7427		else:
7428			logger.debug('Switching to custom font implicitly because of zoom action')
7429			style = self.textview.get_style_context()
7430			font = style.get_property(Gtk.STYLE_PROPERTY_FONT, Gtk.StateFlags.NORMAL)
7431
7432		font_size = font.get_size()
7433		if font_size <= 1 * 1024 and plus_or_minus < 0:
7434			return
7435		else:
7436			font_size_new = font_size + plus_or_minus * 1024
7437			font.set_size(font_size_new)
7438		try:
7439			self.text_style['TextView']['font'] = font.to_string()
7440		except UnicodeDecodeError:
7441			logger.exception('FIXME')
7442		self.textview.modify_font(font)
7443
7444		self.text_style.write()
7445
7446	@action(_('_Normal Size'), '<Primary>0') # T: Menu item to reset zoom
7447	def zoom_reset(self):
7448		'''Menu action to reset the font size'''
7449		if not self.text_style['TextView']['font']:
7450			return
7451
7452		widget = TextView({}) # Get new widget
7453		style = widget.get_style_context()
7454		default_font = style.get_property(Gtk.STYLE_PROPERTY_FONT, Gtk.StateFlags.NORMAL)
7455
7456		font = Pango.FontDescription(self.text_style['TextView']['font'])
7457		font.set_size(default_font.get_size())
7458
7459		if font.equal(default_font):
7460			self.text_style['TextView']['font'] = None
7461			self.textview.modify_font(None)
7462		else:
7463			self.text_style['TextView']['font'] = font.to_string()
7464			self.textview.modify_font(font)
7465
7466		self.text_style.write()
7467
7468
7469class InsertedObjectAnchor(Gtk.TextChildAnchor):
7470
7471	def create_widget(self):
7472		raise NotImplementedError
7473
7474	def dump(self, builder):
7475		raise NotImplementedError
7476
7477
7478class LineSeparatorAnchor(InsertedObjectAnchor):
7479
7480	def create_widget(self):
7481		return LineSeparator()
7482
7483	def dump(self, builder):
7484		builder.start(LINE, {})
7485		builder.data('-'*20) # FIXME: get rid of text here
7486		builder.end(LINE)
7487
7488
7489class TableAnchor(InsertedObjectAnchor):
7490	# HACK - table support is native in formats, but widget is still in plugin
7491	#        so we need to "glue" the table tokens to the plugin widget
7492
7493	def __init__(self, objecttype, objectmodel):
7494		GObject.GObject.__init__(self)
7495		self.objecttype = objecttype
7496		self.objectmodel = objectmodel
7497
7498	def create_widget(self):
7499		return self.objecttype.create_widget(self.objectmodel)
7500
7501	def dump(self, builder):
7502		self.objecttype.dump(builder, self.objectmodel)
7503
7504
7505class PluginInsertedObjectAnchor(InsertedObjectAnchor):
7506
7507	def __init__(self, objecttype, objectmodel):
7508		GObject.GObject.__init__(self)
7509		self.objecttype = objecttype
7510		self.objectmodel = objectmodel
7511
7512	def create_widget(self):
7513		return self.objecttype.create_widget(self.objectmodel)
7514
7515	def dump(self, builder):
7516		attrib, data = self.objecttype.data_from_model(self.objectmodel)
7517		builder.start(OBJECT, dict(attrib)) # dict() because ElementTree doesn't like ConfigDict
7518		if data is not None:
7519			builder.data(data)
7520		builder.end(OBJECT)
7521
7522
7523class InsertDateDialog(Dialog):
7524	'''Dialog to insert a date-time in the page'''
7525
7526	FORMAT_COL = 0 # format string
7527	DATE_COL = 1 # strfime rendering of the format
7528
7529	def __init__(self, parent, buffer, notebook, page):
7530		Dialog.__init__(
7531			self,
7532			parent,
7533			_('Insert Date and Time'), # T: Dialog title
7534			button=_('_Insert'), # T: Button label
7535			use_default_button=True
7536		)
7537		self.buffer = buffer
7538		self.notebook = notebook
7539		self.page = page
7540		self.date = datetime.now()
7541
7542		self.uistate.setdefault('lastusedformat', '')
7543		self.uistate.setdefault('linkdate', False)
7544
7545		## Add Calendar widget
7546		from zim.plugins.journal import Calendar # FIXME put this in zim.gui.widgets
7547
7548		label = Gtk.Label()
7549		label.set_markup('<b>' + _("Date") + '</b>') # T: label in "insert date" dialog
7550		label.set_alignment(0.0, 0.5)
7551		self.vbox.pack_start(label, False, False, 0)
7552
7553		self.calendar = Calendar()
7554		self.calendar.set_display_options(
7555			Gtk.CalendarDisplayOptions.SHOW_HEADING |
7556			Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES |
7557			Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS)
7558		self.calendar.connect('day-selected', lambda c: self.set_date(c.get_date()))
7559		self.vbox.pack_start(self.calendar, False, True, 0)
7560
7561		## Add format list box
7562		label = Gtk.Label()
7563		label.set_markup('<b>' + _("Format") + '</b>') # T: label in "insert date" dialog
7564		label.set_alignment(0.0, 0.5)
7565		self.vbox.pack_start(label, False, False, 0)
7566
7567		model = Gtk.ListStore(str, str) # FORMAT_COL, DATE_COL
7568		self.view = BrowserTreeView(model)
7569		self.vbox.pack_start(ScrolledWindow(self.view), True, True, 0)
7570
7571		cell_renderer = Gtk.CellRendererText()
7572		column = Gtk.TreeViewColumn('_date_', cell_renderer, text=1)
7573		self.view.append_column(column)
7574		self.view.set_headers_visible(False)
7575		self.view.connect('row-activated',
7576			lambda *a: self.response(Gtk.ResponseType.OK))
7577
7578		## Add Link checkbox and Edit button
7579		self.linkbutton = Gtk.CheckButton.new_with_mnemonic(_('_Link to date'))
7580			# T: check box in InsertDate dialog
7581		self.linkbutton.set_active(self.uistate['linkdate'])
7582		self.vbox.pack_start(self.linkbutton, False, True, 0)
7583
7584		button = Gtk.Button.new_with_mnemonic(_('_Edit')) # T: Button label
7585		button.connect('clicked', self.on_edit)
7586		self.action_area.add(button)
7587		self.action_area.reorder_child(button, 1)
7588
7589		## Setup data
7590		self.load_file()
7591		self.set_date(self.date)
7592
7593	def load_file(self):
7594		lastused = None
7595		model = self.view.get_model()
7596		model.clear()
7597		file = ConfigManager.get_config_file('dates.list')
7598		for line in file.readlines():
7599			line = line.strip()
7600			if not line or line.startswith('#'):
7601				continue
7602			try:
7603				format = line
7604				iter = model.append((format, format))
7605				if format == self.uistate['lastusedformat']:
7606					lastused = iter
7607			except:
7608				logger.exception('Could not parse date: %s', line)
7609
7610		if len(model) == 0:
7611			# file not found ?
7612			model.append(("%c", "%c"))
7613
7614		if not lastused is None:
7615			path = model.get_path(lastused)
7616			self.view.get_selection().select_path(path)
7617
7618	def set_date(self, date):
7619		self.date = date
7620
7621		def update_date(model, path, iter):
7622			format = model[iter][self.FORMAT_COL]
7623			try:
7624				string = datetime.strftime(format, date)
7625			except ValueError:
7626				string = 'INVALID: ' + format
7627			model[iter][self.DATE_COL] = string
7628
7629		model = self.view.get_model()
7630		model.foreach(update_date)
7631
7632		link = date.strftime('%Y-%m-%d') # YYYY-MM-DD
7633		self.link = self.notebook.suggest_link(self.page, link)
7634		self.linkbutton.set_sensitive(not self.link is None)
7635
7636	#def run(self):
7637		#self.view.grab_focus()
7638		#Dialog.run(self)
7639
7640	def save_uistate(self):
7641		model, iter = self.view.get_selection().get_selected()
7642		if iter:
7643			format = model[iter][self.FORMAT_COL]
7644			self.uistate['lastusedformat'] = format
7645		self.uistate['linkdate'] = self.linkbutton.get_active()
7646
7647	def on_edit(self, button):
7648		file = ConfigManager.get_config_file('dates.list') # XXX
7649		if edit_config_file(self, file):
7650			self.load_file()
7651
7652	def do_response_ok(self):
7653		model, iter = self.view.get_selection().get_selected()
7654		if iter:
7655			text = model[iter][self.DATE_COL]
7656		else:
7657			text = model[0][self.DATE_COL]
7658
7659		if self.link and self.linkbutton.get_active():
7660			self.buffer.insert_link_at_cursor(text, self.link.name)
7661		else:
7662			self.buffer.insert_at_cursor(text)
7663
7664		return True
7665
7666
7667class InsertImageDialog(FileDialog):
7668	'''Dialog to insert an image in the page'''
7669
7670	def __init__(self, parent, buffer, notebook, path, file=None):
7671		FileDialog.__init__(
7672			self, parent, _('Insert Image'), Gtk.FileChooserAction.OPEN)
7673			# T: Dialog title
7674
7675		self.buffer = buffer
7676		self.notebook = notebook
7677		self.path = path
7678
7679		self.uistate.setdefault('attach_inserted_images', False)
7680		self.uistate.setdefault('last_image_folder', None, check=str)
7681
7682		self.add_shortcut(notebook, path)
7683		self.add_filter_images()
7684
7685		checkbox = Gtk.CheckButton.new_with_mnemonic(_('Attach image first'))
7686			# T: checkbox in the "Insert Image" dialog
7687		checkbox.set_active(self.uistate['attach_inserted_images'])
7688		self.filechooser.set_extra_widget(checkbox)
7689
7690		if file:
7691			self.set_file(file)
7692		else:
7693			self.load_last_folder()
7694
7695	def do_response_ok(self):
7696		file = self.get_file()
7697		if file is None:
7698			return False
7699
7700		if not image_file_get_dimensions(file.path):
7701			ErrorDialog(self, _('File type not supported: %s') % file.get_mimetype()).run()
7702				# T: Error message when trying to insert a not supported file as image
7703			return False
7704
7705		self.save_last_folder()
7706
7707		# Similar code in AttachFileDialog
7708		checkbox = self.filechooser.get_extra_widget()
7709		self.uistate['attach_inserted_images'] = checkbox.get_active()
7710		if self.uistate['attach_inserted_images']:
7711			folder = self.notebook.get_attachments_dir(self.path)
7712			if not file.ischild(folder):
7713				file = attach_file(self, self.notebook, self.path, file)
7714				if file is None:
7715					return False # Cancelled overwrite dialog
7716
7717		src = self.notebook.relative_filepath(file, self.path) or file.uri
7718		self.buffer.insert_image_at_cursor(file, src)
7719		return True
7720
7721
7722class AttachFileDialog(FileDialog):
7723
7724	def __init__(self, parent, buffer, notebook, path, file=None):
7725		assert path, 'Need a page here'
7726		FileDialog.__init__(self, parent, _('Attach File'), multiple=True) # T: Dialog title
7727		self.buffer = buffer
7728		self.notebook = notebook
7729		self.path = path
7730
7731		dir = notebook.get_attachments_dir(path)
7732		if dir is None:
7733			ErrorDialog(self, _('Page "%s" does not have a folder for attachments') % self.path)
7734				# T: Error dialog - %s is the full page name
7735			raise Exception('Page "%s" does not have a folder for attachments' % self.path)
7736
7737		self.add_shortcut(notebook, path)
7738		if file:
7739			self.set_file(file)
7740		else:
7741			self.load_last_folder()
7742
7743	def do_response_ok(self):
7744		files = self.get_files()
7745		if not files:
7746			return False
7747
7748		self.save_last_folder()
7749
7750		inserted = False
7751		last = len(files) - 1
7752		for i, file in enumerate(files):
7753			file = attach_file(self, self.notebook, self.path, file)
7754			if file is not None:
7755				inserted = True
7756				text = self.notebook.relative_filepath(file, path=self.path)
7757				self.buffer.insert_link_at_cursor(text, href=text)
7758				if i != last:
7759					self.buffer.insert_at_cursor(' ')
7760
7761		return inserted # If nothing is inserted, return False and do not close dialog
7762
7763
7764def attach_file(widget, notebook, path, file, force_overwrite=False):
7765	folder = notebook.get_attachments_dir(path)
7766	if folder is None:
7767		raise Error('%s does not have an attachments dir' % path)
7768
7769	dest = folder.file(file.basename)
7770	if dest.exists() and not force_overwrite:
7771		dialog = PromptExistingFileDialog(widget, dest)
7772		dest = dialog.run()
7773		if dest is None:
7774			return None	# dialog was cancelled
7775		elif dest.exists():
7776			dest.remove()
7777
7778	file.copyto(dest)
7779	return dest
7780
7781
7782class PromptExistingFileDialog(Dialog):
7783	'''Dialog that is used e.g. when a file should be attached to zim,
7784	but a file with the same name already exists in the attachment
7785	directory. This Dialog allows to suggest a new name or overwrite
7786	the existing one.
7787
7788	For this dialog C{run()} will return either the original file
7789	(for overwrite), a new file, or None when the dialog was canceled.
7790	'''
7791
7792	def __init__(self, widget, file):
7793		Dialog.__init__(self, widget, _('File Exists'), buttons=None) # T: Dialog title
7794		self.add_help_text( _('''\
7795A file with the name <b>"%s"</b> already exists.
7796You can use another name or overwrite the existing file.''' % file.basename),
7797		) # T: Dialog text in 'new filename' dialog
7798		self.folder = file.parent()
7799		self.old_file = file
7800
7801		suggested_filename = self.folder.new_file(file.basename).basename
7802		self.add_form((
7803				('name', 'string', _('Filename')), # T: Input label
7804			), {
7805				'name': suggested_filename
7806			}
7807		)
7808		self.form.widgets['name'].set_check_func(self._check_valid)
7809
7810		# all buttons are defined in this class, to get the ordering right
7811		# [show folder]      [overwrite] [cancel] [ok]
7812		button = Gtk.Button.new_with_mnemonic(_('_Browse')) # T: Button label
7813		button.connect('clicked', self.do_show_folder)
7814		self.action_area.add(button)
7815		self.action_area.set_child_secondary(button, True)
7816
7817		button = Gtk.Button.new_with_mnemonic(_('Overwrite')) # T: Button label
7818		button.connect('clicked', self.do_response_overwrite)
7819		self.add_action_widget(button, Gtk.ResponseType.NONE)
7820
7821		self.add_button(_('_Cancel'), Gtk.ResponseType.CANCEL) # T: Button label
7822		self.add_button(_('_OK'), Gtk.ResponseType.OK) # T: Button label
7823		self._no_ok_action = False
7824
7825		self.form.widgets['name'].connect('focus-in-event', self._on_focus)
7826
7827	def _on_focus(self, widget, event):
7828		# filename length without suffix
7829		length = len(os.path.splitext(widget.get_text())[0])
7830		widget.select_region(0, length)
7831
7832	def _check_valid(self, filename):
7833		# Only valid when same dir and does not yet exist
7834		file = self.folder.file(filename)
7835		return file.ischild(self.folder) and not file.exists()
7836
7837	def do_show_folder(self, *a):
7838		open_folder(self, self.folder)
7839
7840	def do_response_overwrite(self, *a):
7841		logger.info('Overwriting %s', self.old_file.path)
7842		self.result = self.old_file
7843
7844	def do_response_ok(self):
7845		if not self.form.widgets['name'].get_input_valid():
7846			return False
7847
7848		newfile = self.folder.file(self.form['name'])
7849		logger.info('Selected %s', newfile.path)
7850		assert newfile.ischild(self.folder) # just to be real sure
7851		assert not newfile.exists() # just to be real sure
7852		self.result = newfile
7853		return True
7854
7855
7856class EditImageDialog(Dialog):
7857	'''Dialog to edit properties of an embedded image'''
7858
7859	def __init__(self, parent, buffer, notebook, path):
7860		Dialog.__init__(self, parent, _('Edit Image')) # T: Dialog title
7861		self.buffer = buffer
7862		self.notebook = notebook
7863		self.path = path
7864
7865		iter = buffer.get_iter_at_mark(buffer.get_insert())
7866		image_data = self.buffer.get_image_data(iter)
7867		if image_data is None:
7868			iter.backward_char()
7869			image_data = self.buffer.get_image_data(iter)
7870			assert image_data, 'No image found'
7871		self._image_data = image_data.copy()
7872		self._iter = iter.get_offset()
7873
7874		src = image_data['src']
7875		if '?' in src:
7876			i = src.find('?')
7877			src = src[:i]
7878		href = image_data.get('href', '')
7879		anchor = image_data.get('id', '')
7880		self.add_form([
7881				('file', 'image', _('Location')), # T: Input in 'edit image' dialog
7882				('href', 'link', _('Link to'), path), # T: Input in 'edit image' dialog
7883				('width', 'int', _('Width'), (0, 1)), # T: Input in 'edit image' dialog
7884				('height', 'int', _('Height'), (0, 1)), # T: Input in 'edit image' dialog
7885				('anchor', 'string', _('Id'))
7886			],
7887			{'file': src, 'href': href, 'anchor': anchor}
7888			# range for width and height are set in set_ranges()
7889		)
7890		self.form.widgets['file'].set_use_relative_paths(notebook, path)
7891		self.form.widgets['file'].allow_empty = False
7892		self.form.widgets['file'].show_empty_invalid = True
7893		self.form.widgets['file'].update_input_valid()
7894
7895		reset_button = Gtk.Button.new_with_mnemonic(_('_Reset Size'))
7896			# T: Button in 'edit image' dialog
7897		hbox = Gtk.HBox()
7898		hbox.pack_end(reset_button, False, True, 0)
7899		self.vbox.add(hbox)
7900
7901		reset_button.connect_object('clicked',
7902			self.__class__.reset_dimensions, self)
7903		self.form.widgets['file'].connect_object('changed',
7904			self.__class__.do_file_changed, self)
7905		self.form.widgets['width'].connect_object('value-changed',
7906			self.__class__.do_width_changed, self)
7907		self.form.widgets['height'].connect_object('value-changed',
7908			self.__class__.do_height_changed, self)
7909
7910		# Init ranges based on original
7911		self.reset_dimensions()
7912
7913		# Set current scale if any
7914		if 'width' in image_data:
7915			self.form.widgets['width'].set_value(int(image_data['width']))
7916		elif 'height' in image_data:
7917			self.form.widgets['height'].set_value(int(image_data['height']))
7918
7919	def reset_dimensions(self):
7920		self._image_data.pop('width', None)
7921		self._image_data.pop('height', None)
7922		width = self.form.widgets['width']
7923		height = self.form.widgets['height']
7924		file = self.form['file']
7925		try:
7926			if file is None:
7927				raise AssertionError
7928			w, h = image_file_get_dimensions(file.path)
7929			if w <= 0 or h <= 0:
7930				raise AssertionError
7931		except:
7932			logger.warn('Could not get size for image: %s', file.path)
7933			width.set_sensitive(False)
7934			height.set_sensitive(False)
7935		else:
7936			width.set_sensitive(True)
7937			height.set_sensitive(True)
7938			self._block = True
7939			width.set_range(0, 4 * w)
7940			width.set_value(w)
7941			height.set_range(0, 4 * w)
7942			height.set_value(h)
7943			self._block = False
7944			self._ratio = float(w) / h
7945
7946	def do_file_changed(self):
7947		# Prevent images becoming one pixel wide
7948		file = self.form['file']
7949		if file is None:
7950			return
7951		try:
7952			if self._image_data['width'] == 1:
7953				self.reset_dimensions()
7954		except KeyError:
7955			# width hasn't been set
7956			pass
7957
7958	def do_width_changed(self):
7959		if hasattr(self, '_block') and self._block:
7960			return
7961		self._image_data.pop('height', None)
7962		self._image_data['width'] = int(self.form['width'])
7963		h = int(float(self._image_data['width']) / self._ratio)
7964		self._block = True
7965		self.form['height'] = h
7966		self._block = False
7967
7968	def do_height_changed(self):
7969		if hasattr(self, '_block') and self._block:
7970			return
7971		self._image_data.pop('width', None)
7972		self._image_data['height'] = int(self.form['height'])
7973		w = int(self._ratio * float(self._image_data['height']))
7974		self._block = True
7975		self.form['width'] = w
7976		self._block = False
7977
7978	def do_response_ok(self):
7979		file = self.form['file']
7980		if file is None:
7981			return False
7982
7983		attrib = self._image_data
7984		attrib['src'] = self.notebook.relative_filepath(file, self.path) or file.uri
7985
7986		href = self.form['href']
7987		if href:
7988			type = link_type(href)
7989			if type == 'file':
7990				# Try making the path relative
7991				linkfile = self.form.widgets['href'].get_file()
7992				href = self.notebook.relative_filepath(linkfile, self.path) or linkfile.uri
7993			attrib['href'] = href
7994		else:
7995			attrib.pop('href', None)
7996
7997		id = self.form['anchor']
7998		if id:
7999			attrib['id'] = id
8000		else:
8001			attrib.pop('id', None)
8002
8003		iter = self.buffer.get_iter_at_offset(self._iter)
8004		bound = iter.copy()
8005		bound.forward_char()
8006		with self.buffer.user_action:
8007			self.buffer.delete(iter, bound)
8008			self.buffer.insert_image_at_cursor(file, **attrib)
8009		return True
8010
8011
8012class InsertTextFromFileDialog(FileDialog):
8013	'''Dialog to insert text from an external file into the page'''
8014
8015	def __init__(self, parent, buffer, notebook, page):
8016		FileDialog.__init__(
8017			self, parent, _('Insert Text From File'), Gtk.FileChooserAction.OPEN)
8018			# T: Dialog title
8019		self.load_last_folder()
8020		self.add_shortcut(notebook, page)
8021		self.buffer = buffer
8022
8023	def do_response_ok(self):
8024		file = self.get_file()
8025		if file is None:
8026			return False
8027		parser = get_format('plain').Parser()
8028		tree = parser.parse(file.readlines())
8029		self.buffer.insert_parsetree_at_cursor(tree)
8030		self.save_last_folder()
8031		return True
8032
8033
8034class InsertLinkDialog(Dialog):
8035	'''Dialog to insert a new link in the page or edit properties of
8036	an existing link
8037	'''
8038
8039	def __init__(self, parent, pageview):
8040		self.pageview = pageview
8041		href, text = self._get_link_from_buffer()
8042
8043		if href:
8044			title = _('Edit Link') # T: Dialog title
8045		else:
8046			title = _('Insert Link') # T: Dialog title
8047
8048		Dialog.__init__(self, parent, title, button=_('_Link'))  # T: Dialog button
8049
8050		self.uistate.setdefault('short_links', pageview.notebook.config['Notebook']['short_links'])
8051		self.add_form(
8052			[
8053				('href', 'link', _('Link to'), pageview.page), # T: Input in 'insert link' dialog
8054				('text', 'string', _('Text')), # T: Input in 'insert link' dialog
8055				('short_links', 'bool', _('Prefer short link names for pages')), # T: Input in 'insert link' dialog
8056			], {
8057				'href': href,
8058				'text': text,
8059				'short_links': self.uistate['short_links'],
8060			},
8061			notebook=pageview.notebook
8062		)
8063
8064		# Hook text entry to copy text from link when apropriate
8065		self.form.widgets['href'].connect('changed', self.on_href_changed)
8066		self.form.widgets['text'].connect('changed', self.on_text_changed)
8067		self.form.widgets['short_links'].connect('toggled', self.on_short_link_pref_changed)
8068		self._text_for_link = self._link_to_text(href)
8069		self._copy_text = self._text_for_link == text and not self._selected_text
8070
8071	def _get_link_from_buffer(self):
8072		# Get link and text from the text buffer
8073		href, text = '', ''
8074
8075		buffer = self.pageview.textview.get_buffer()
8076		if buffer.get_has_selection():
8077			buffer.strip_selection()
8078			link = buffer.get_has_link_selection()
8079		else:
8080			link = buffer.select_link()
8081			if not link:
8082				self.pageview.autoselect()
8083
8084		if buffer.get_has_selection():
8085			start, end = buffer.get_selection_bounds()
8086			text = start.get_text(end)
8087			self._selection_bounds = (start.get_offset(), end.get_offset())
8088				# Interaction in the dialog causes buffer to loose selection
8089				# maybe due to clipboard focus !??
8090				# Anyway, need to remember bounds ourselves.
8091			if link:
8092				href = link['href']
8093				self._selected_text = False
8094			else:
8095				href = text
8096				self._selected_text = True
8097		else:
8098			self._selection_bounds = None
8099			self._selected_text = False
8100
8101		return href, text
8102
8103	def on_href_changed(self, o):
8104		# Check if we can also update text
8105		self._text_for_link = self._link_to_text(self.form['href'])
8106		if self._copy_text:
8107			self.form['text'] = self._text_for_link
8108			self._copy_text = True # just to be sure
8109
8110	def on_text_changed(self, o):
8111		# Check if we should stop updating text
8112		self._copy_text = self.form['text'] == self._text_for_link
8113
8114	def on_short_link_pref_changed(self, o):
8115		self.on_href_changed(None)
8116
8117	def _link_to_text(self, link):
8118		if not link:
8119			return ''
8120		if self.form['short_links'] and link_type(link) == 'page':
8121				# Similar to 'short_links' notebook property but using uistate instead
8122				parts = HRef.new_from_wiki_link(link).parts()
8123				if len(parts) > 0:
8124					return parts[-1]
8125		return link
8126
8127	def do_response_ok(self):
8128		self.uistate['short_links'] = self.form['short_links']
8129
8130		href = self.form['href']
8131		if not href:
8132			self.form.widgets['href'].set_input_valid(False)
8133			return False
8134
8135		type = link_type(href)
8136		if type == 'file':
8137			# Try making the path relative
8138			try:
8139				file = self.form.widgets['href'].get_file()
8140				page = self.pageview.page
8141				notebook = self.pageview.notebook
8142				href = notebook.relative_filepath(file, page) or file.uri
8143			except:
8144				pass # E.g. non-local file://host/path URI causing exception
8145
8146		text = self.form['text'] or href
8147
8148		buffer = self.pageview.textview.get_buffer()
8149		with buffer.user_action:
8150			if self._selection_bounds:
8151				start, end = list(map(
8152					buffer.get_iter_at_offset, self._selection_bounds))
8153				buffer.delete(start, end)
8154			buffer.insert_link_at_cursor(text, href)
8155
8156		return True
8157
8158
8159class FindWidget(object):
8160	'''Base class for L{FindBar} and L{FindAndReplaceDialog}'''
8161
8162	def __init__(self, textview):
8163		self.textview = textview
8164
8165		self.find_entry = InputEntry(allow_whitespace=True)
8166		self.find_entry.connect_object(
8167			'changed', self.__class__.on_find_entry_changed, self)
8168		self.find_entry.connect_object(
8169			'activate', self.__class__.on_find_entry_activate, self)
8170
8171		self.next_button = Gtk.Button.new_with_mnemonic(_('_Next'))
8172			# T: button in find bar and find & replace dialog
8173		self.next_button.connect_object(
8174			'clicked', self.__class__.find_next, self)
8175		self.next_button.set_sensitive(False)
8176
8177		self.previous_button = Gtk.Button.new_with_mnemonic(_('_Previous'))
8178			# T: button in find bar and find & replace dialog
8179		self.previous_button.connect_object(
8180			'clicked', self.__class__.find_previous, self)
8181		self.previous_button.set_sensitive(False)
8182
8183		self.case_option_checkbox = Gtk.CheckButton.new_with_mnemonic(_('Match _case'))
8184			# T: checkbox option in find bar and find & replace dialog
8185		self.case_option_checkbox.connect_object(
8186			'toggled', self.__class__.on_find_entry_changed, self)
8187
8188		self.word_option_checkbox = Gtk.CheckButton.new_with_mnemonic(_('Whole _word'))
8189			# T: checkbox option in find bar and find & replace dialog
8190		self.word_option_checkbox.connect_object(
8191			'toggled', self.__class__.on_find_entry_changed, self)
8192
8193		self.regex_option_checkbox = Gtk.CheckButton.new_with_mnemonic(_('_Regular expression'))
8194			# T: checkbox option in find bar and find & replace dialog
8195		self.regex_option_checkbox.connect_object(
8196			'toggled', self.__class__.on_find_entry_changed, self)
8197
8198		self.highlight_checkbox = Gtk.CheckButton.new_with_mnemonic(_('_Highlight'))
8199			# T: checkbox option in find bar and find & replace dialog
8200		self.highlight_checkbox.connect_object(
8201			'toggled', self.__class__.on_highlight_toggled, self)
8202
8203	@property
8204	def _flags(self):
8205		flags = 0
8206		if self.case_option_checkbox.get_active():
8207			flags = flags | FIND_CASE_SENSITIVE
8208		if self.word_option_checkbox.get_active():
8209			flags = flags | FIND_WHOLE_WORD
8210		if self.regex_option_checkbox.get_active():
8211			flags = flags | FIND_REGEX
8212		return flags
8213
8214	def set_from_buffer(self):
8215		'''Copies settings from last find in the buffer. Uses the
8216		selected text for find if there is a selection.
8217		'''
8218		buffer = self.textview.get_buffer()
8219		string, flags, highlight = buffer.finder.get_state()
8220		bounds = buffer.get_selection_bounds()
8221		if bounds:
8222			start, end = bounds
8223			string = start.get_slice(end)
8224			if flags & FIND_REGEX:
8225				string = re.escape(string)
8226		self.find(string, flags, highlight)
8227
8228	def on_find_entry_changed(self):
8229		string = self.find_entry.get_text()
8230		buffer = self.textview.get_buffer()
8231		ok = buffer.finder.find(string, flags=self._flags)
8232
8233		if not string:
8234			self.find_entry.set_input_valid(True)
8235		else:
8236			self.find_entry.set_input_valid(ok)
8237
8238		for button in (self.next_button, self.previous_button):
8239			button.set_sensitive(ok)
8240
8241		if ok:
8242			self.textview.scroll_to_mark(buffer.get_insert(), SCROLL_TO_MARK_MARGIN, False, 0, 0)
8243
8244	def on_find_entry_activate(self):
8245		self.on_find_entry_changed()
8246
8247	def on_highlight_toggled(self):
8248		highlight = self.highlight_checkbox.get_active()
8249		buffer = self.textview.get_buffer()
8250		buffer.finder.set_highlight(highlight)
8251
8252	def find(self, string, flags=0, highlight=False):
8253		if string:
8254			self.find_entry.set_text(string)
8255		self.case_option_checkbox.set_active(flags & FIND_CASE_SENSITIVE)
8256		self.word_option_checkbox.set_active(flags & FIND_WHOLE_WORD)
8257		self.regex_option_checkbox.set_active(flags & FIND_REGEX)
8258		self.highlight_checkbox.set_active(highlight)
8259
8260		# Force update
8261		self.on_find_entry_changed()
8262		self.on_highlight_toggled()
8263
8264	def find_next(self):
8265		buffer = self.textview.get_buffer()
8266		buffer.finder.find_next()
8267		self.textview.scroll_to_mark(buffer.get_insert(), SCROLL_TO_MARK_MARGIN, False, 0, 0)
8268
8269	def find_previous(self):
8270		buffer = self.textview.get_buffer()
8271		buffer.finder.find_previous()
8272		self.textview.scroll_to_mark(buffer.get_insert(), SCROLL_TO_MARK_MARGIN, False, 0, 0)
8273
8274
8275class FindBar(FindWidget, Gtk.ActionBar):
8276	'''Bar to be shown below the TextView for find functions'''
8277
8278	# TODO use smaller buttons ?
8279
8280	def __init__(self, textview):
8281		GObject.GObject.__init__(self)
8282		FindWidget.__init__(self, textview)
8283
8284		self.pack_start(Gtk.Label(_('Find') + ': '))
8285			# T: label for input in find bar on bottom of page
8286		self.pack_start(self.find_entry)
8287		self.pack_start(self.previous_button)
8288		self.pack_start(self.next_button)
8289		self.pack_start(self.case_option_checkbox)
8290		self.pack_start(self.highlight_checkbox)
8291		# TODO allow box to shrink further by putting buttons in menu
8292
8293		close_button = IconButton(Gtk.STOCK_CLOSE, relief=False, size=Gtk.IconSize.MENU)
8294		close_button.connect_object('clicked', self.__class__.hide, self)
8295		self.pack_end(close_button)
8296
8297	def grab_focus(self):
8298		self.find_entry.grab_focus()
8299
8300	def show(self):
8301		self.on_highlight_toggled()
8302		self.set_no_show_all(False)
8303		self.show_all()
8304
8305	def hide(self):
8306		Gtk.ActionBar.hide(self)
8307		self.set_no_show_all(True)
8308		buffer = self.textview.get_buffer()
8309		buffer.finder.set_highlight(False)
8310		self.textview.grab_focus()
8311
8312	def on_find_entry_activate(self):
8313		self.on_find_entry_changed()
8314		self.find_next()
8315
8316	def do_key_press_event(self, event):
8317		keyval = strip_boolean_result(event.get_keyval())
8318		if keyval == KEYVAL_ESC:
8319			self.hide()
8320			return True
8321		else:
8322			return Gtk.HBox.do_key_press_event(self, event)
8323
8324
8325class FindAndReplaceDialog(FindWidget, Dialog):
8326	'''Dialog for find and replace'''
8327
8328	def __init__(self, parent, textview):
8329		Dialog.__init__(self, parent,
8330			_('Find and Replace'), buttons=Gtk.ButtonsType.CLOSE) # T: Dialog title
8331		FindWidget.__init__(self, textview)
8332
8333		hbox = Gtk.HBox(spacing=12)
8334		hbox.set_border_width(12)
8335		self.vbox.add(hbox)
8336
8337		vbox = Gtk.VBox(spacing=5)
8338		hbox.pack_start(vbox, True, True, 0)
8339
8340		label = Gtk.Label(label=_('Find what') + ': ')
8341			# T: input label in find & replace dialog
8342		label.set_alignment(0.0, 0.5)
8343		vbox.add(label)
8344		vbox.add(self.find_entry)
8345		vbox.add(self.case_option_checkbox)
8346		vbox.add(self.word_option_checkbox)
8347		vbox.add(self.regex_option_checkbox)
8348		vbox.add(self.highlight_checkbox)
8349
8350		label = Gtk.Label(label=_('Replace with') + ': ')
8351			# T: input label in find & replace dialog
8352		label.set_alignment(0.0, 0.5)
8353		vbox.add(label)
8354		self.replace_entry = InputEntry(allow_whitespace=True)
8355		vbox.add(self.replace_entry)
8356
8357		self.bbox = Gtk.ButtonBox(orientation=Gtk.Orientation.VERTICAL)
8358		self.bbox.set_layout(Gtk.ButtonBoxStyle.START)
8359		self.bbox.set_spacing(5)
8360		hbox.pack_start(self.bbox, False, False, 0)
8361		self.bbox.add(self.next_button)
8362		self.bbox.add(self.previous_button)
8363
8364		replace_button = Gtk.Button.new_with_mnemonic(_('_Replace'))
8365			# T: Button in search & replace dialog
8366		replace_button.connect_object('clicked', self.__class__.replace, self)
8367		self.bbox.add(replace_button)
8368
8369		all_button = Gtk.Button.new_with_mnemonic(_('Replace _All'))
8370			# T: Button in search & replace dialog
8371		all_button.connect_object('clicked', self.__class__.replace_all, self)
8372		self.bbox.add(all_button)
8373
8374	def set_input(self, **inputs):
8375		# Hide implementation for test cases
8376		for key, value in list(inputs.items()):
8377			if key == 'query':
8378				self.find_entry.set_text(value)
8379			elif key == 'replacement':
8380				self.replace_entry.set_text(value)
8381			else:
8382				raise ValueError
8383
8384	def replace(self):
8385		string = self.replace_entry.get_text()
8386		buffer = self.textview.get_buffer()
8387		buffer.finder.replace(string)
8388		buffer.finder.find_next()
8389
8390	def replace_all(self):
8391		string = self.replace_entry.get_text()
8392		buffer = self.textview.get_buffer()
8393		buffer.finder.replace_all(string)
8394
8395	def do_response(self, id):
8396		Dialog.do_response(self, id)
8397		buffer = self.textview.get_buffer()
8398		buffer.finder.set_highlight(False)
8399
8400
8401class WordCountDialog(Dialog):
8402	'''Dialog showing line, word, and character counts'''
8403
8404	def __init__(self, pageview):
8405		Dialog.__init__(self, pageview,
8406			_('Word Count'), buttons=Gtk.ButtonsType.CLOSE) # T: Dialog title
8407		self.set_resizable(False)
8408
8409		def count(buffer, bounds):
8410			start, end = bounds
8411			lines = end.get_line() - start.get_line() + 1
8412			chars = end.get_offset() - start.get_offset()
8413
8414			strings = start.get_text(end).strip().split()
8415			non_space_chars = sum(len(s) for s in strings)
8416
8417			words = 0
8418			iter = start.copy()
8419			while iter.compare(end) < 0:
8420				if iter.forward_word_end():
8421					words += 1
8422				elif iter.compare(end) == 0:
8423					# When end is end of buffer forward_end_word returns False
8424					words += 1
8425					break
8426				else:
8427					break
8428
8429			return lines, words, chars, non_space_chars
8430
8431		buffer = pageview.textview.get_buffer()
8432		buffercount = count(buffer, buffer.get_bounds())
8433		insert = buffer.get_iter_at_mark(buffer.get_insert())
8434		start = buffer.get_iter_at_line(insert.get_line())
8435		end = start.copy()
8436		end.forward_line()
8437		paracount = count(buffer, (start, end))
8438		if buffer.get_has_selection():
8439			selectioncount = count(buffer, buffer.get_selection_bounds())
8440		else:
8441			selectioncount = (0, 0, 0, 0)
8442
8443		table = Gtk.Table(3, 4)
8444		table.set_row_spacings(5)
8445		table.set_col_spacings(12)
8446		self.vbox.add(table)
8447
8448		plabel = Gtk.Label(label=_('Page')) # T: label in word count dialog
8449		alabel = Gtk.Label(label=_('Paragraph')) # T: label in word count dialog
8450		slabel = Gtk.Label(label=_('Selection')) # T: label in word count dialog
8451		wlabel = Gtk.Label(label='<b>' + _('Words') + '</b>:') # T: label in word count dialog
8452		llabel = Gtk.Label(label='<b>' + _('Lines') + '</b>:') # T: label in word count dialog
8453		clabel = Gtk.Label(label='<b>' + _('Characters') + '</b>:') # T: label in word count dialog
8454		dlabel = Gtk.Label(label='<b>' + _('Characters excluding spaces') + '</b>:') # T: label in word count dialog
8455
8456		for label in (wlabel, llabel, clabel, dlabel):
8457			label.set_use_markup(True)
8458			label.set_alignment(0.0, 0.5)
8459
8460		# Heading
8461		table.attach(plabel, 1, 2, 0, 1)
8462		table.attach(alabel, 2, 3, 0, 1)
8463		table.attach(slabel, 3, 4, 0, 1)
8464
8465		# Lines
8466		table.attach(llabel, 0, 1, 1, 2)
8467		table.attach(Gtk.Label(label=str(buffercount[0])), 1, 2, 1, 2)
8468		table.attach(Gtk.Label(label=str(paracount[0])), 2, 3, 1, 2)
8469		table.attach(Gtk.Label(label=str(selectioncount[0])), 3, 4, 1, 2)
8470
8471		# Words
8472		table.attach(wlabel, 0, 1, 2, 3)
8473		table.attach(Gtk.Label(label=str(buffercount[1])), 1, 2, 2, 3)
8474		table.attach(Gtk.Label(label=str(paracount[1])), 2, 3, 2, 3)
8475		table.attach(Gtk.Label(label=str(selectioncount[1])), 3, 4, 2, 3)
8476
8477		# Characters
8478		table.attach(clabel, 0, 1, 3, 4)
8479		table.attach(Gtk.Label(label=str(buffercount[2])), 1, 2, 3, 4)
8480		table.attach(Gtk.Label(label=str(paracount[2])), 2, 3, 3, 4)
8481		table.attach(Gtk.Label(label=str(selectioncount[2])), 3, 4, 3, 4)
8482
8483		# Characters excluding spaces
8484		table.attach(dlabel, 0, 1, 4, 5)
8485		table.attach(Gtk.Label(label=str(buffercount[3])), 1, 2, 4, 5)
8486		table.attach(Gtk.Label(label=str(paracount[3])), 2, 3, 4, 5)
8487		table.attach(Gtk.Label(label=str(selectioncount[3])), 3, 4, 4, 5)
8488
8489
8490from zim.notebook import update_parsetree_and_copy_images
8491
8492class MoveTextDialog(Dialog):
8493	'''This dialog allows moving a selected text to a new page
8494	The idea is to allow "refactoring" of pages more easily.
8495	'''
8496
8497	def __init__(self, pageview, notebook, page, buffer, navigation):
8498		assert buffer.get_has_selection(), 'No Selection present'
8499		Dialog.__init__(
8500			self,
8501			pageview,
8502			_('Move Text to Other Page'), # T: Dialog title
8503			button=_('_Move')  # T: Button label
8504		)
8505		self.pageview = pageview
8506		self.notebook = notebook
8507		self.page = page
8508		self.buffer = buffer
8509		self.navigation = navigation
8510
8511		self.uistate.setdefault('link', True)
8512		self.uistate.setdefault('open_page', False)
8513		self.add_form([
8514			('page', 'page', _('Move text to'), page), # T: Input in 'move text' dialog
8515			('link', 'bool', _('Leave link to new page')), # T: Input in 'move text' dialog
8516			('open_page', 'bool', _('Open new page')), # T: Input in 'move text' dialog
8517
8518		], self.uistate)
8519
8520	def do_response_ok(self):
8521		newpage = self.form['page']
8522		if not newpage:
8523			return False
8524
8525		try:
8526			newpage = self.notebook.get_page(newpage)
8527		except PageNotFoundError:
8528			return False
8529
8530		# Copy text
8531		bounds = self.buffer.get_selection_bounds()
8532		if not bounds:
8533			ErrorDialog(self, _('No text selected')).run() # T: error message in "move selected text" action
8534			return False
8535
8536		if not newpage.exists():
8537			template = self.notebook.get_template(newpage)
8538			newpage.set_parsetree(template)
8539
8540		parsetree = self.buffer.get_parsetree(bounds)
8541		update_parsetree_and_copy_images(parsetree, self.notebook, self.page, newpage)
8542
8543		newpage.append_parsetree(parsetree)
8544		self.notebook.store_page(newpage)
8545
8546		# Delete text (after copy was successfulll..)
8547		self.buffer.delete(*bounds)
8548
8549		# Insert Link
8550		self.uistate['link'] = self.form['link']
8551		if self.form['link']:
8552			href = self.form.widgets['page'].get_text() # TODO add method to Path "get_link" which gives rel path formatted correctly
8553			self.buffer.insert_link_at_cursor(href, href)
8554
8555		# Show page
8556		self.uistate['open_page'] = self.form['open_page']
8557		if self.form['open_page']:
8558			self.navigation.open_page(newpage)
8559
8560		return True
8561
8562
8563class NewFileDialog(Dialog):
8564
8565	def __init__(self, parent, basename):
8566		Dialog.__init__(self, parent, _('New File')) # T: Dialog title
8567		self.add_form((
8568			('basename', 'string', _('Name')), # T: input for new file name
8569		), {
8570			'basename': basename
8571		})
8572
8573	def show_all(self):
8574		Dialog.show_all(self)
8575
8576		# Select only first part of name
8577		# TODO - make this a widget type in widgets.py
8578		text = self.form.widgets['basename'].get_text()
8579		if '.' in text:
8580			name, ext = text.split('.', 1)
8581			self.form.widgets['basename'].select_region(0, len(name))
8582
8583	def do_response_ok(self):
8584		self.result = self.form['basename']
8585		return True
8586