place cursor in newly opened files at the end, fixes LP:#188908
[pyroom.git] / PyRoom / basic_edit.py
blobfb1575ba4601da3d883bf93812fd16b309533633
1 # -*- coding: utf-8 -*-
2 # -----------------------------------------------------------------------------
3 # PyRoom - A clone of WriteRoom
4 # Copyright (c) 2007 Nicolas P. Rougier & NoWhereMan
5 # Copyright (c) 2008 The Pyroom Team - See AUTHORS file for more information
7 # This program is free software: you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free Software
9 # Foundation, either version 3 of the License, or (at your option) any later
10 # version.
12 # This program is distributed in the hope that it will be useful, but WITHOUT
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15 # details.
17 # You should have received a copy of the GNU General Public License along with
18 # this program. If not, see <http://www.gnu.org/licenses/>.
19 # -----------------------------------------------------------------------------
21 """
22 provide basic editor functionality
24 contains basic functions needed for pyroom - any core functionality is included
25 within this file
26 """
28 import gtk
29 import gtk.glade
30 import gtksourceview
31 import os
33 from pyroom_error import PyroomError
34 from gui import GUI
35 from preferences import Preferences
36 import autosave
38 FILE_UNNAMED = _('* Unnamed *')
40 KEY_BINDINGS = '\n'.join([
41 _('Control-H: Show help in a new buffer'),
42 _('Control-I: Show buffer information'),
43 _('Control-P: Shows Preferences dialog'),
44 _('Control-N: Create a new buffer'),
45 _('Control-O: Open a file in a new buffer'),
46 _('Control-Q: Quit'),
47 _('Control-S: Save current buffer'),
48 _('Control-Shift-S: Save current buffer as'),
49 _('Control-W: Close buffer and exit if it was the last buffer'),
50 _('Control-Y: Redo last typing'),
51 _('Control-Z: Undo last typing'),
52 _('Control-Page Up: Switch to previous buffer'),
53 _('Control-Page Down: Switch to next buffer'), ])
55 HELP = \
56 _("""PyRoom - distraction free writing
57 Copyright (c) 2007 Nicolas Rougier, NoWhereMan
58 Copyright (c) 2008 Bruno Bord and the PyRoom team
60 Welcome to PyRoom and distraction-free writing.
62 To hide this help window, press Control-W.
64 PyRoom stays out of your way with formatting options and buttons,
65 it is largely keyboard controlled, via shortcuts. You can find a list
66 of available keyboard shortcuts later.
68 If enabled in preferences, pyroom will save your files automatically every
69 few minutes or when you press the keyboard shortcut.
71 Commands:
72 ---------
75 """ % KEY_BINDINGS)
77 def define_keybindings(edit_instance):
78 """define keybindings, respectively to keyboard layout"""
79 keymap = gtk.gdk.keymap_get_default()
80 basic_bindings = {
81 gtk.keysyms.Page_Up: edit_instance.prev_buffer,
82 gtk.keysyms.Page_Down: edit_instance.next_buffer,
83 gtk.keysyms.H: edit_instance.show_help,
84 gtk.keysyms.I: edit_instance.show_info,
85 gtk.keysyms.N: edit_instance.new_buffer,
86 gtk.keysyms.O: edit_instance.open_file,
87 gtk.keysyms.P: edit_instance.preferences.show,
88 gtk.keysyms.Q: edit_instance.dialog_quit,
89 gtk.keysyms.S: edit_instance.save_file,
90 gtk.keysyms.W: edit_instance.close_dialog,
91 gtk.keysyms.Y: edit_instance.redo,
92 gtk.keysyms.Z: edit_instance.undo,
94 translated_bindings = {}
95 for key, value in basic_bindings.items():
96 hardware_keycode = keymap.get_entries_for_keyval(key)[0][0]
97 translated_bindings[hardware_keycode] = value
98 return translated_bindings
100 class BasicEdit(object):
101 """editing logic that gets passed around"""
103 def __init__(self, style, pyroom_config):
104 self.current = 0
105 self.buffers = []
106 self.style = style
107 self.config = pyroom_config.config
108 self.gui = GUI(style, pyroom_config, self)
109 self.preferences = Preferences(gui=self.gui, style=style,
110 pyroom_config=pyroom_config)
111 self.status = self.gui.status
112 self.window = self.gui.window
113 self.textbox = self.gui.textbox
115 self.new_buffer()
117 self.textbox.connect('key-press-event', self.key_press_event)
119 # Set line numbers visible, set linespacing
120 self.textbox.set_show_line_numbers(int(self.config.get("visual",
121 "linenumber")))
122 self.textbox.set_pixels_below_lines(int(self.config.get("visual", "linespacing")))
123 self.textbox.set_pixels_above_lines(int(self.config.get("visual", "linespacing")))
124 self.textbox.set_pixels_inside_wrap(int(self.config.get("visual", "linespacing")))
126 # Autosave timer object
127 autosave.autosave_init(self)
129 self.window.show_all()
130 self.window.fullscreen()
132 # Handle multiple monitors
133 screen = gtk.gdk.screen_get_default()
134 root_window = screen.get_root_window()
135 mouse_x, mouse_y, mouse_mods = root_window.get_pointer()
136 current_monitor_number = screen.get_monitor_at_point(mouse_x, mouse_y)
137 monitor_geometry = screen.get_monitor_geometry(current_monitor_number)
138 self.window.move(monitor_geometry.x, monitor_geometry.y)
139 self.window.set_geometry_hints(None, min_width=monitor_geometry.width,
140 min_height=monitor_geometry.height, max_width=monitor_geometry.width,
141 max_height=monitor_geometry.height
144 # Defines the glade file functions for use on closing a buffer
145 self.wTree = gtk.glade.XML(os.path.join(
146 pyroom_config.pyroom_absolute_path, "interface.glade"),
147 "SaveBuffer")
148 self.dialog = self.wTree.get_widget("SaveBuffer")
149 self.dialog.set_transient_for(self.window)
150 dic = {
151 "on_button-close_clicked": self.unsave_dialog,
152 "on_button-cancel_clicked": self.cancel_dialog,
153 "on_button-save_clicked": self.save_dialog,
155 self.wTree.signal_autoconnect(dic)
157 #Defines the glade file functions for use on exit
158 self.aTree = gtk.glade.XML(os.path.join(
159 pyroom_config.pyroom_absolute_path, "interface.glade"),
160 "QuitSave")
161 self.quitdialog = self.aTree.get_widget("QuitSave")
162 self.quitdialog.set_transient_for(self.window)
163 dic = {
164 "on_button-close2_clicked": self.quit_quit,
165 "on_button-cancel2_clicked": self.cancel_quit,
166 "on_button-save2_clicked": self.save_quit,
168 self.aTree.signal_autoconnect(dic)
169 self.keybindings = define_keybindings(self)
171 def key_press_event(self, widget, event):
172 """ key press event dispatcher """
173 if event.state & gtk.gdk.CONTROL_MASK:
174 if event.hardware_keycode in self.keybindings:
175 if self.keybindings[event.hardware_keycode] == self.save_file\
176 and event.state & gtk.gdk.SHIFT_MASK:
177 self.save_file_as()
178 else:
179 self.keybindings[event.hardware_keycode]()
180 return True
181 return False
183 def show_info(self):
184 """ Display buffer information on status label for 5 seconds """
186 buf = self.buffers[self.current]
187 if buf.can_undo() or buf.can_redo():
188 status = _(' (modified)')
189 else:
190 status = ''
191 self.status.set_text(_('Buffer %(buffer_id)d: %(buffer_name)s\
192 %(status)s, %(char_count)d byte(s), %(word_count)d word(s)\
193 , %(lines)d line(s)') % {
194 'buffer_id': self.current + 1,
195 'buffer_name': buf.filename,
196 'status': status,
197 'char_count': buf.get_char_count(),
198 'word_count': self.word_count(buf),
199 'lines': buf.get_line_count(),
200 }, 5000)
202 def undo(self):
203 """ Undo last typing """
205 buf = self.textbox.get_buffer()
206 if buf.can_undo():
207 buf.undo()
208 else:
209 self.status.set_text(_('Nothing more to undo!'))
211 def redo(self):
212 """ Redo last typing """
214 buf = self.textbox.get_buffer()
215 if buf.can_redo():
216 buf.redo()
217 else:
218 self.status.set_text(_('Nothing more to redo!'))
220 def toggle_lines(self):
221 """ Toggle lines number """
222 opposite_state = not self.textbox.get_show_line_numbers()
223 self.textbox.set_show_line_numbers(opposite_state)
225 def open_file(self):
226 """ Open file """
228 chooser = gtk.FileChooserDialog('PyRoom', self.window,
229 gtk.FILE_CHOOSER_ACTION_OPEN,
230 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
231 gtk.STOCK_OPEN, gtk.RESPONSE_OK))
232 chooser.set_default_response(gtk.RESPONSE_OK)
234 res = chooser.run()
235 if res == gtk.RESPONSE_OK:
236 buf = self.new_buffer()
237 buf.filename = chooser.get_filename()
238 try:
239 buffer_file = open(buf.filename, 'r')
240 buf = self.buffers[self.current]
241 buf.begin_not_undoable_action()
242 utf8 = unicode(buffer_file.read(), 'utf-8')
243 buf.set_text(utf8)
244 buf.end_not_undoable_action()
245 buffer_file.close()
246 self.status.set_text(_('File %s open')
247 % buf.filename)
248 except IOError, (errno, strerror):
249 errortext = _('Unable to open %(filename)s.' % {
250 'filename': buf.filename})
251 if errno == 2:
252 errortext += _(' The file does not exist.')
253 elif errno == 13:
254 errortext += _(' You do not have permission to \
255 open the file.')
256 raise PyroomError(errortext)
257 except:
258 raise PyroomError(_('Unable to open %s\n'
259 % buf.filename))
260 else:
261 self.status.set_text(_('Closed, no files selected'))
262 chooser.destroy()
264 def open_file_no_chooser(self, filename):
265 """ Open specified file """
266 buf = self.new_buffer()
267 buf.filename = filename
268 try:
269 buffer_file = open(buf.filename, 'r')
270 buf = self.buffers[self.current]
271 buf.begin_not_undoable_action()
272 utf8 = unicode(buffer_file.read(), 'utf-8')
273 buf.set_text(utf8)
274 buf.end_not_undoable_action()
275 buffer_file.close()
276 self.status.set_text(_('File %s open')
277 % buf.filename)
278 except IOError, (errno, strerror):
279 errortext = _('Unable to open %(filename)s.' % {
280 'filename': buf.filename})
281 if errno == 2:
282 errortext += _(' The file does not exist.')
283 elif errno == 13:
284 errortext += _(' You do not have permission to open \
285 the file.')
286 raise PyroomError(errortext)
287 except:
288 raise PyroomError(_('Unable to open %s\n'
289 % buf.filename))
290 def save_file(self):
291 """ Save file """
292 try:
293 buf = self.buffers[self.current]
294 if buf.filename != FILE_UNNAMED:
295 buffer_file = open(buf.filename, 'w')
296 txt = buf.get_text(buf.get_start_iter(),
297 buf.get_end_iter())
298 buffer_file.write(txt)
299 buffer_file.close()
300 buf.begin_not_undoable_action()
301 buf.end_not_undoable_action()
302 self.status.set_text(_('File %s saved') % buf.filename)
303 else:
304 self.save_file_as()
305 except IOError, (errno, strerror):
306 errortext = _('Unable to save %(filename)s.' % {
307 'filename': buf.filename})
308 if errno == 13:
309 errortext += _(' You do not have permission to write to \
310 the file.')
311 raise PyroomError(errortext)
312 except:
313 raise PyroomError(_('Unable to save %s\n'
314 % buf.filename))
316 def save_file_as(self):
317 """ Save file as """
319 buf = self.buffers[self.current]
320 chooser = gtk.FileChooserDialog('PyRoom', self.window,
321 gtk.FILE_CHOOSER_ACTION_SAVE,
322 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
323 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
324 chooser.set_default_response(gtk.RESPONSE_OK)
325 if buf.filename != FILE_UNNAMED:
326 chooser.set_filename(buf.filename)
327 res = chooser.run()
328 if res == gtk.RESPONSE_OK:
329 buf.filename = chooser.get_filename()
330 self.save_file()
331 else:
332 self.status.set_text(_('Closed, no files selected'))
333 chooser.destroy()
335 def word_count(self, buf):
336 """ Word count in a text buffer """
338 iter1 = buf.get_start_iter()
339 iter2 = iter1.copy()
340 iter2.forward_word_end()
341 count = 0
342 while iter2.get_offset() != iter1.get_offset():
343 count += 1
344 iter1 = iter2.copy()
345 iter2.forward_word_end()
346 return count
348 def show_help(self):
349 """ Create a new buffer and inserts help """
350 buf = self.new_buffer()
351 buf.begin_not_undoable_action()
352 buf.set_text(HELP)
353 buf.end_not_undoable_action()
354 self.status.set_text("Displaying help. Press control W to exit and \
355 continue editing your document.")
357 def new_buffer(self):
358 """ Create a new buffer """
360 buf = gtksourceview.SourceBuffer()
361 buf.set_check_brackets(False)
362 buf.set_highlight(False)
363 buf.filename = FILE_UNNAMED
364 self.buffers.insert(self.current + 1, buf)
365 buf.place_cursor(buf.get_end_iter())
366 self.next_buffer()
367 return buf
369 def close_dialog(self):
370 """ask for confirmation if there are unsaved contents"""
371 buf = self.buffers[self.current]
372 if buf.can_undo() or buf.can_redo():
373 self.dialog.show()
374 else:
375 self.close_buffer()
377 def cancel_dialog(self, widget, data=None):
378 """dialog has been canceled"""
379 self.dialog.hide()
381 def unsave_dialog(self, widget, data =None):
382 """don't save before closing"""
383 self.dialog.hide()
384 self.close_buffer()
386 def save_dialog(self, widget, data=None):
387 """save when closing"""
388 self.dialog.hide()
389 self.save_file()
390 self.close_buffer()
392 def close_buffer(self):
393 """ Close current buffer """
396 if len(self.buffers) > 1:
398 self.buffers.pop(self.current)
399 self.current = min(len(self.buffers) - 1, self.current)
400 self.set_buffer(self.current)
401 else:
402 quit()
404 def set_buffer(self, index):
405 """ Set current buffer """
407 if index >= 0 and index < len(self.buffers):
408 self.current = index
409 buf = self.buffers[index]
410 self.textbox.set_buffer(buf)
411 if hasattr(self, 'status'):
412 self.status.set_text(
413 _('Switching to buffer %(buffer_id)d (%(buffer_name)s)'
414 % {'buffer_id': self.current + 1,
415 'buffer_name': buf.filename}))
417 def next_buffer(self):
418 """ Switch to next buffer """
420 if self.current < len(self.buffers) - 1:
421 self.current += 1
422 else:
423 self.current = 0
424 self.set_buffer(self.current)
425 self.gui.textbox.scroll_to_mark(
426 self.buffers[self.current].get_insert(),
427 0.0,
430 def prev_buffer(self):
431 """ Switch to prev buffer """
433 if self.current > 0:
434 self.current -= 1
435 else:
436 self.current = len(self.buffers) - 1
437 self.set_buffer(self.current)
438 self.gui.textbox.scroll_to_mark(
439 self.buffers[self.current].get_insert(),
440 0.0,
442 def dialog_quit(self):
443 """the quit dialog"""
444 count = 0
445 ret = False
446 for buf in self.buffers:
447 if (buf.can_undo() or buf.can_redo()) and \
448 not buf.get_text(buf.get_start_iter(),
449 buf.get_end_iter()) == '':
450 count = count + 1
451 if count > 0:
452 self.quitdialog.show()
453 else:
454 self.quit()
456 def cancel_quit(self, widget, data=None):
457 """don't quit"""
458 self.quitdialog.hide()
460 def save_quit(self, widget, data=None):
461 """save before quitting"""
462 self.quitdialog.hide()
463 for buf in self.buffers:
464 if buf.can_undo() or buf.can_redo():
465 if buf.filename == FILE_UNNAMED:
466 self.save_file_as()
467 else:
468 self.save_file()
469 self.quit()
471 def quit_quit(self, widget, data=None):
472 """really quit"""
473 self.quitdialog.hide()
474 self.quit()
476 def quit(self):
477 """cleanup before quitting"""
478 autosave.autosave_quit()
479 self.gui.quit()
480 # EOF