2
# -*- coding: utf-8 -*-
3
# ------------------------------------------------------------------------------
4
# PyRoom - A clone of WriteRoom
5
# Copyright (c) 2007 Nicolas P. Rougier & NoWhereMan
6
# Copyright (c) 2008 Bruno Bord
8
# This program is free software: you can redistribute it and/or modify it under
9
# the terms of the GNU General Public License as published by the Free Software
10
# Foundation, either version 3 of the License, or (at your option) any later
13
# This program is distributed in the hope that it will be useful, but WITHOUT
14
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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
# Based on code posted on ubuntu forums by NoWhereMan (www.nowhereland.it)
22
# (Ubuntu thread was "WriteRoom/Darkroom/?")
24
# ------------------------------------------------------------------------------
35
gettext.install('pyroom', 'locale')
39
styles = {'darkgreen': {
41
'background': '#000000',
42
'foreground': '#007700',
46
'font': 'Droid Sans Mono',
52
'background': '#000000',
53
'foreground': '#00ff00',
57
'font': 'Droid Sans Mono',
63
'background': '#0000ff',
64
'foreground': '#ffffff',
68
'font': 'Droid Sans Mono',
74
FILE_UNNAMED = _('* Unnamed *')
77
_("""PyRoom - an adaptation of write room
78
Copyright (c) 2007 Nicolas Rougier, NoWhereMan
79
Copyright (c) 2008 Bruno Bord
81
This program is free software: you can redistribute it and/or modify it under
82
the terms of the GNU General Public License as published by the Free Software
83
Foundation, either version 3 of the License, or (at your option) any later
89
pyroom.py [-style] file1 file2 ...
90
style can be either 'blue', 'green', 'darkgreen'
95
Control-H: Show help in a new buffer
96
Control-I: Show buffer information
97
Control-L: Toggle line number
98
Control-N: Create a new buffer
99
Control-O: Open a file in a new buffer
101
Control-S: Save current buffer
102
Control-Shift-S: Save current buffer as
103
Control-W: Close buffer and exit if it was the last buffer
104
Control-Y: Redo last typing
105
Control-Z: Undo last typing
106
Control-Left: Switch to previous buffer
107
Control-Right: Switch to next buffer
108
Control-Plus: Increases font size
109
Control-Minus: Decreases font size
114
No question whether to close a modified buffer or not
118
class FadeLabel(gtk.Label):
119
""" GTK Label with timed fade out effect """
121
active_duration = 3000 # Fade start after this time
122
fade_duration = 1500 # Fade duration
124
def __init__(self, message='', active_color=None, inactive_color=None):
125
gtk.Label.__init__(self, message)
127
active_color = '#ffffff'
128
self.active_color = active_color
129
if not inactive_color:
130
inactive_color = '#000000'
131
self.inactive_color = inactive_color
134
def set_text(self, message, duration=None):
136
duration = self.active_duration
137
self.modify_fg(gtk.STATE_NORMAL,
138
gtk.gdk.color_parse(self.active_color))
139
gtk.Label.set_text(self, message)
141
gobject.source_remove(self.idle)
142
self.idle = gobject.timeout_add(duration, self.fade_start)
144
def fade_start(self):
145
self.fade_level = 1.0
147
gobject.source_remove(self.idle)
148
self.idle = gobject.timeout_add(25, self.fade_out)
151
color = gtk.gdk.color_parse(self.inactive_color)
152
(red1, green1, blue1) = (color.red, color.green, color.blue)
153
color = gtk.gdk.color_parse(self.active_color)
154
(red2, green2, blue2) = (color.red, color.green, color.blue)
155
red = red1 + int(self.fade_level * abs(red1 - red2))
156
green = green1 + int(self.fade_level * abs(green1 - green2))
157
blue = blue1 + int(self.fade_level * abs(blue1 - blue2))
158
self.modify_fg(gtk.STATE_NORMAL, gtk.gdk.Color(red, green, blue))
159
self.fade_level -= 1.0 / (self.fade_duration / 25)
160
if self.fade_level > 0:
167
""" The PyRoom class"""
172
def __init__(self, style):
178
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
179
self.window.set_name('PyRoom')
180
self.window.fullscreen()
181
self.window.connect('delete_event', self.delete_event)
182
self.window.connect('destroy', self.destroy)
184
self.textbox = gtksourceview.SourceView()
186
self.textbox.connect('scroll-event', self.scroll_event)
187
self.textbox.connect('key-press-event', self.key_press_event)
188
self.textbox.set_wrap_mode(gtk.WRAP_WORD)
190
self.fixed = gtk.Fixed()
191
self.vbox = gtk.VBox()
192
self.window.add(self.fixed)
193
self.fixed.put(self.vbox, 0, 0)
195
self.boxout = gtk.EventBox()
196
self.boxout.set_border_width(1)
197
self.boxin = gtk.EventBox()
198
self.boxin.set_border_width(1)
199
self.vbox.pack_start(self.boxout, True, True, 6)
200
self.boxout.add(self.boxin)
202
self.scrolled = gtk.ScrolledWindow()
203
self.boxin.add(self.scrolled)
204
self.scrolled.add(self.textbox)
205
self.scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
207
self.scrolled.set_property('resize-mode', gtk.RESIZE_PARENT)
208
self.textbox.set_property('resize-mode', gtk.RESIZE_PARENT)
209
self.vbox.set_property('resize-mode', gtk.RESIZE_PARENT)
214
self.status = FadeLabel()
215
self.hbox = gtk.HBox()
216
self.hbox.set_spacing(12)
217
self.hbox.pack_end(self.status, True, True, 0)
218
self.vbox.pack_end(self.hbox, False, False, 0)
219
self.status.set_alignment(0.0, 0.5)
220
self.status.set_justify(gtk.JUSTIFY_LEFT)
222
self.window.show_all()
223
self.status.set_text(
224
_('Welcome to PyRoom 1.0, type Control-H for help'))
226
def delete_event(self, widget, event, data=None):
230
def destroy(self, widget, data=None):
234
def key_press_event(self, widget, event):
235
""" key press event dispatcher """
238
gtk.keysyms.Left: self.prev_buffer,
239
gtk.keysyms.Right: self.next_buffer,
240
gtk.keysyms.h: self.show_help,
241
gtk.keysyms.i: self.show_info,
242
gtk.keysyms.l: self.toggle_lines,
243
gtk.keysyms.n: self.new_buffer,
244
gtk.keysyms.o: self.open_file,
245
gtk.keysyms.q: self.quit,
246
gtk.keysyms.s: self.save_file,
247
gtk.keysyms.w: self.close_buffer,
248
gtk.keysyms.y: self.redo,
249
gtk.keysyms.z: self.undo,
250
gtk.keysyms.plus: self.plus,
251
gtk.keysyms.minus: self.minus,
253
if event.state & gtk.gdk.CONTROL_MASK:
255
# Special case for Control-Shift-s
257
if event.state & gtk.gdk.SHIFT_MASK:
259
if event.state & gtk.gdk.SHIFT_MASK and event.keyval\
263
if bindings.has_key(event.keyval):
264
bindings[event.keyval]()
268
def scroll_event(self, widget, event):
269
"""\" Scroll event dispatcher """
271
if event.direction == gtk.gdk.SCROLL_UP:
273
elif event.direction == gtk.gdk.SCROLL_DOWN:
277
""" Create a new buffer and inserts help """
279
buffer = self.new_buffer()
280
buffer.begin_not_undoable_action()
281
buffer.set_text(HELP)
282
buffer.end_not_undoable_action()
284
def new_buffer(self):
285
""" Create a new buffer """
287
buffer = gtksourceview.SourceBuffer()
288
buffer.set_check_brackets(False)
289
buffer.set_highlight(False)
290
buffer.filename = FILE_UNNAMED
291
self.buffers.insert(self.current + 1, buffer)
292
buffer.place_cursor(buffer.get_start_iter())
296
def close_buffer(self):
297
""" Close current buffer """
299
if len(self.buffers) > 1:
300
self.buffers.pop(self.current)
301
self.current = min(len(self.buffers) - 1, self.current)
302
self.set_buffer(self.current)
306
def set_buffer(self, index):
307
""" Set current buffer """
309
if index >= 0 and index < len(self.buffers):
311
buffer = self.buffers[index]
312
self.textbox.set_buffer(buffer)
313
if hasattr(self, 'status'):
314
self.status.set_text(
315
_('Switching to buffer %(buffer_id)d (%(buffer_name)s)'
316
% {'buffer_id': self.current + 1, 'buffer_name'
319
def next_buffer(self):
320
""" Switch to next buffer """
322
if self.current < len(self.buffers) - 1:
326
self.set_buffer(self.current)
328
def prev_buffer(self):
329
""" Switch to prev buffer """
334
self.current = len(self.buffers) - 1
335
self.set_buffer(self.current)
337
def apply_style(self, style=None):
342
self.window.modify_bg(gtk.STATE_NORMAL,
343
gtk.gdk.color_parse(self.style['background'
345
self.textbox.modify_bg(gtk.STATE_NORMAL,
346
gtk.gdk.color_parse(self.style['background'
348
self.textbox.modify_base(gtk.STATE_NORMAL,
349
gtk.gdk.color_parse(self.style['background'
351
self.textbox.modify_text(gtk.STATE_NORMAL,
352
gtk.gdk.color_parse(self.style['foreground'
354
self.textbox.modify_fg(gtk.STATE_NORMAL,
355
gtk.gdk.color_parse(self.style['lines']))
356
self.status.active_color = self.style['foreground']
357
self.status.inactive_color = self.style['background']
358
self.boxout.modify_bg(gtk.STATE_NORMAL,
359
gtk.gdk.color_parse(self.style['border']))
360
font_and_size = '%s %d' % (self.style['font'],
361
self.style['fontsize'])
362
self.textbox.modify_font(pango.FontDescription(font_and_size))
364
gtk.rc_parse_string("""
365
style "pyroom-colored-cursor" {
366
GtkTextView::cursor-color = '"""
367
+ self.style['foreground']
370
class "GtkWidget" style "pyroom-colored-cursor"
372
(w, h) = (gtk.gdk.screen_width(), gtk.gdk.screen_height())
373
width = int(self.style['size'][0] * w)
374
height = int(self.style['size'][1] * h)
375
self.vbox.set_size_request(width, height)
376
self.fixed.move(self.vbox, int(((1 - self.style['size'][0]) * w)/ 2),
377
int(((1 - self.style['size'][1]) * h) / 2))
378
self.textbox.set_border_width(self.style['padding'])
380
def word_count(self, buffer):
381
""" Word count in a text buffer """
383
iter1 = buffer.get_start_iter()
385
iter2.forward_word_end()
387
while iter2.get_offset() != iter1.get_offset():
390
iter2.forward_word_end()
399
""" Display buffer information on status label for 5 seconds """
401
buffer = self.buffers[self.current]
402
if buffer.can_undo() or buffer.can_redo():
403
status = _(' (modified)')
406
self.status.set_text(_('Buffer %(buffer_id)d: %(buffer_name)s%(status)s, %(char_count)d byte(s), %(word_count)d word(s), %(lines)d line(s)') % {
407
'buffer_id': self.current + 1,
408
'buffer_name': buffer.filename,
410
'char_count': buffer.get_char_count(),
411
'word_count': self.word_count(buffer),
412
'lines': buffer.get_line_count(),
415
def scroll_down(self):
416
""" Scroll window down """
418
adj = self.scrolled.get_vadjustment()
419
if adj.upper > adj.page_size:
420
adj.value = min(adj.upper - adj.page_size, adj.value
421
+ adj.step_increment)
424
""" Scroll window up """
426
adj = self.scrolled.get_vadjustment()
427
if adj.value > adj.step_increment:
428
adj.value -= adj.step_increment
433
""" Undo last typing """
435
buffer = self.textbox.get_buffer()
436
if buffer.can_undo():
439
self.status.set_text(_('No more undo!'))
442
""" Redo last typing """
444
buffer = self.textbox.get_buffer()
445
if buffer.can_redo():
448
self.status.set_text(_('No more redo!'))
450
def toggle_lines(self):
451
""" Toggle lines number """
453
b = not self.textbox.get_show_line_numbers()
454
self.textbox.set_show_line_numbers(b)
459
chooser = gtk.FileChooserDialog('PyRoom', self.window,
460
gtk.FILE_CHOOSER_ACTION_OPEN,
461
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
462
gtk.STOCK_OPEN, gtk.RESPONSE_OK))
463
chooser.set_default_response(gtk.RESPONSE_OK)
466
if res == gtk.RESPONSE_OK:
467
buffer = self.new_buffer()
468
buffer.filename = chooser.get_filename()
470
f = open(buffer.filename, 'r')
471
buffer = self.buffers[self.current]
472
buffer.begin_not_undoable_action()
473
utf8 = unicode(f.read(), 'utf-8')
474
buffer.set_text(utf8)
475
buffer.end_not_undoable_action()
477
self.status.set_text(_('File %s open')
480
buffer.set_text(_('''Unable to open %(filename)s
483
% {'filename': buffer.filename,
484
'traceback': traceback.format_exc()}))
485
self.status.set_text(_('Failed to open %s')
487
buffer.filename = FILE_UNNAMED
489
self.status.set_text(_('Closed, no files selected'))
495
buffer = self.buffers[self.current]
496
if buffer.filename != FILE_UNNAMED:
497
f = open(buffer.filename, 'w')
498
txt = buffer.get_text(buffer.get_start_iter(),
499
buffer.get_end_iter())
502
buffer.begin_not_undoable_action()
503
buffer.end_not_undoable_action()
504
self.status.set_text(_('File %s saved') % buffer.filename)
508
def save_file_as(self):
511
buffer = self.buffers[self.current]
512
chooser = gtk.FileChooserDialog('PyRoom', self.window,
513
gtk.FILE_CHOOSER_ACTION_SAVE,
514
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
515
gtk.STOCK_SAVE, gtk.RESPONSE_OK))
516
chooser.set_default_response(gtk.RESPONSE_OK)
517
if buffer.filename != FILE_UNNAMED:
518
chooser.set_filename(buffer.filename)
520
if res == gtk.RESPONSE_OK:
521
buffer.filename = chooser.get_filename()
524
self.status.set_text(_('Closed, no files selected'))
530
""" Increases the font size"""
532
self.style['fontsize'] += 2
534
self.status.set_text(_('Font size increased'))
537
""" Decreases the font size"""
539
self.style['fontsize'] -= 2
541
self.status.set_text(_('Font size decreased'))
544
if __name__ == '__main__':
548
# Look for style and file on command line
549
for arg in sys.argv[1:]:
552
if styles.has_key(arg[1:]):
557
# Create relevant buffers for file and load them
558
pyroom = PyRoom(styles[style])
560
for filename in files:
561
buffer = pyroom.new_buffer()
562
buffer.filename = filename
563
if os.path.exists(filename):
566
f = open(filename, 'r')
567
buffer.begin_not_undoable_action()
568
buffer.set_text(unicode(f.read(), 'utf-8'))
569
buffer.end_not_undoable_action()
572
buffer.set_text(_('Unable to open %s\n'
574
buffer.set_text(_('Unable to open %(filename)s %(traceback)s')
575
% {'filename': buffer.filename,
576
'traceback': traceback.format_exc()})
577
buffer.filename = FILE_UNNAMED
579
pyroom.close_buffer()