gtk_app 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/gtk_app.rb +13 -3
- data/lib/gtk_app/application.rb +13 -0
- data/lib/gtk_app/callback_support.rb +5 -0
- data/lib/gtk_app/controller.rb +6 -2
- data/lib/gtk_app/ext/combo_box.rb +11 -0
- data/lib/gtk_app/ext/text_buffer.rb +272 -0
- data/lib/gtk_app/ext/text_view.rb +55 -0
- data/lib/gtk_app/ext/tree_view.rb +11 -0
- data/lib/gtk_app/helpers.rb +8 -3
- data/lib/gtk_app/list_control.rb +7 -0
- data/lib/gtk_app/signal_support.rb +4 -0
- data/lib/gtk_app/text_buffer.rb +133 -17
- data/lib/gtk_app/version.rb +2 -2
- data/lib/gtk_app/view.rb +14 -5
- data/lib/gtk_app/view_helpers.rb +33 -18
- data/lib/gtk_app/widget_observer.rb +6 -0
- metadata +66 -10
data/lib/gtk_app.rb
CHANGED
@@ -12,24 +12,34 @@ module GtkApp
|
|
12
12
|
autoload :SignalSupport, "#{lib}/gtk_app/signal_support"
|
13
13
|
autoload :TextBuffer, "#{lib}/gtk_app/text_buffer"
|
14
14
|
autoload :Observer, "#{lib}/gtk_app/observer"
|
15
|
+
autoload :Drawer, "#{lib}/gtk_app/drawer"
|
15
16
|
autoload :Version, "#{lib}/gtk_app/version"
|
16
17
|
|
18
|
+
# Start the main Gtk loop.
|
17
19
|
def self.run
|
18
20
|
Gtk::main
|
19
21
|
end
|
20
|
-
|
22
|
+
|
23
|
+
# Stop the main Gtk loop.
|
21
24
|
def self.quit
|
22
25
|
Gtk::main_quit
|
23
26
|
end
|
24
|
-
|
27
|
+
|
28
|
+
# Run a single iteration of the main loop while there are pending events
|
29
|
+
# without blocking.
|
25
30
|
def self.refresh
|
26
31
|
Gtk::main_iteration_do(false) while Gtk::events_pending?
|
27
32
|
end
|
28
33
|
|
34
|
+
# Establish a controller method to be invoked at regular intervals.
|
35
|
+
# @param [Fixnum] time_in_milliseconds Time between calls to the receiver method.
|
36
|
+
# @param [Object] controller The class in which the method exists.
|
37
|
+
# @param [String] callback Receiver method name.
|
29
38
|
def self.add_timeout(time_in_milliseconds, controller, callback)
|
30
39
|
GLib::Timeout.add(time_in_milliseconds){ controller.method(:"#{callback}") }
|
31
40
|
end
|
32
41
|
|
33
42
|
end
|
34
43
|
|
35
|
-
require 'gtk_app/
|
44
|
+
require 'gtk_app/ext/text_view'
|
45
|
+
# require 'gtk_app/dialog'
|
data/lib/gtk_app/controller.rb
CHANGED
@@ -3,14 +3,18 @@ class Controller
|
|
3
3
|
include GtkApp::Helpers
|
4
4
|
include GtkApp::SignalSupport
|
5
5
|
|
6
|
-
|
6
|
+
# @attribute [rw] model
|
7
|
+
attr_accessor :model
|
8
|
+
# @attr_accessor [rw] view
|
9
|
+
attr_accessor :view
|
7
10
|
|
8
11
|
def initialize(&block)
|
9
12
|
instance_eval(&block) if block_given?
|
10
|
-
|
13
|
+
|
11
14
|
establish_signal_connections
|
12
15
|
end
|
13
16
|
|
17
|
+
# @param [Boolean] with_validations
|
14
18
|
def quit(with_validations=true)
|
15
19
|
# TODO: if with_validations
|
16
20
|
# end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'raspell'
|
2
|
+
|
3
|
+
module Gtk
|
4
|
+
class TextBuffer
|
5
|
+
|
6
|
+
# @attr_accessor [Aspell] spell_check
|
7
|
+
attr_accessor:spell_check
|
8
|
+
# @attr_accessor [Array] undo_stack
|
9
|
+
attr_accessor:undo_stack
|
10
|
+
# @attr_accessor [Array] redo_stack
|
11
|
+
attr_accessor:redo_stack
|
12
|
+
# @attr_accessor [Gtk::TextMark] insert_start
|
13
|
+
attr_accessor:insert_start
|
14
|
+
# @attr_accessor [Gtk::TextMark] insert_end
|
15
|
+
attr_accessor:insert_end
|
16
|
+
|
17
|
+
DEFAULT_LANG = "en_US"
|
18
|
+
DEFAULT_TAGS = %w[bold italic strikethrough underline error spell_error]
|
19
|
+
|
20
|
+
def initialize(tag_table=nil, options={})
|
21
|
+
super(tag_table)
|
22
|
+
@undo_stack, @redo_stack = [], []
|
23
|
+
@spell_check = Aspell.new(options[:lang] || DEFAULT_LANG)
|
24
|
+
setup_default_tags
|
25
|
+
setup_spell_check_marks
|
26
|
+
setup_signals
|
27
|
+
end
|
28
|
+
|
29
|
+
# Pop the last action off the undo stack and rewind changes. If an action was
|
30
|
+
# performed, the cursor is placed at the actions starting
|
31
|
+
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter].
|
32
|
+
def undo
|
33
|
+
if @undo_stack.empty?
|
34
|
+
Gdk.beep
|
35
|
+
else
|
36
|
+
action = @undo_stack.pop
|
37
|
+
s_iter = get_iter_at_offset(action[1])
|
38
|
+
case action[0]
|
39
|
+
when :insert then delete(s_iter, get_iter_at_offset(action[2]))
|
40
|
+
when :delete then insert(s_iter, action[3])
|
41
|
+
end
|
42
|
+
@redo_stack.push(action)
|
43
|
+
place_cursor(s_iter)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pop the last action off the redo stack and apply the changes. If and action
|
48
|
+
# was performed, the cursor is placed at the actions starting
|
49
|
+
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter].
|
50
|
+
def redo
|
51
|
+
if @redo_stack.empty?
|
52
|
+
Gdk.beep
|
53
|
+
else
|
54
|
+
action = @redo_stack.pop
|
55
|
+
s_iter = get_iter_at_offset(action[1])
|
56
|
+
e_iter = get_iter_at_offset(action[2])
|
57
|
+
case action[0]
|
58
|
+
when :insert then insert(s_iter, action[3])
|
59
|
+
when :delete then delete(s_iter, e_iter)
|
60
|
+
end
|
61
|
+
@undo_stack.push(action)
|
62
|
+
place_cursor(s_iter)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Retrieve the word at the current cursor position
|
67
|
+
# @return [String] The word.
|
68
|
+
def word_at_cursor
|
69
|
+
get_text(*word_bounds).strip
|
70
|
+
end
|
71
|
+
|
72
|
+
# Determine the boudaries of the word at the current cursor position.
|
73
|
+
# @return [Array] The start and end iter.
|
74
|
+
def word_bounds
|
75
|
+
iter = get_iter_at_offset(cursor_position)
|
76
|
+
s_iter, e_iter = iter.clone, iter.clone
|
77
|
+
s_iter.backward_word_start unless s_iter.starts_word?
|
78
|
+
e_iter.forward_word_end unless e_iter.ends_word?
|
79
|
+
|
80
|
+
[s_iter, e_iter]
|
81
|
+
end
|
82
|
+
|
83
|
+
def check_spelling(s_iter=nil, e_iter=nil)
|
84
|
+
s_iter = bounds.first if s_iter.nil?
|
85
|
+
e_iter = bounds.last if e_iter.nil?
|
86
|
+
|
87
|
+
e_iter.forward_word_end if e_iter.inside_word?
|
88
|
+
|
89
|
+
unless s_iter.starts_word?
|
90
|
+
if s_iter.inside_word? || s_iter.ends_word?
|
91
|
+
s_iter.backward_word_start
|
92
|
+
elsif s_iter.forward_word_end
|
93
|
+
s_iter.backward_word_start
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
cursor = get_iter_at_offset(cursor_position)
|
98
|
+
precursor = cursor.clone
|
99
|
+
precursor.backward_char
|
100
|
+
tag = tag_table.lookup('spell_error')
|
101
|
+
has_error = cursor.has_tag?(tag) || precursor.has_tag?(tag)
|
102
|
+
|
103
|
+
unformat(:spell_error, s_iter, e_iter)
|
104
|
+
|
105
|
+
word_start = s_iter.clone
|
106
|
+
while (word_start <=> e_iter) < 0 do
|
107
|
+
word_end = word_start.clone
|
108
|
+
word_end.forward_word_end
|
109
|
+
|
110
|
+
word = get_text(word_start, word_end)
|
111
|
+
if !has_error && word =~ /[A-Za-z]/ && !@spell_check.check(word)
|
112
|
+
# puts "[#{word}]"
|
113
|
+
format(:spell_error, word_start, word_end)
|
114
|
+
end
|
115
|
+
|
116
|
+
# => meow point to the beginning of the next word.
|
117
|
+
word_end.forward_word_end
|
118
|
+
word_end.backward_word_start
|
119
|
+
|
120
|
+
break if word_start == word_end
|
121
|
+
|
122
|
+
word_start = word_end.clone
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Does the
|
127
|
+
# Gtk::TextTagTable[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextTagTable]
|
128
|
+
# contain any 'spell_error' tags?
|
129
|
+
def spelling_errors?
|
130
|
+
!tag_table.lookup('spell_error').nil?
|
131
|
+
end
|
132
|
+
|
133
|
+
# Locate text in selection or the entire buffer. If found, a
|
134
|
+
# Gtk::TextMark[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextMark]
|
135
|
+
# is returned. Else, nil.
|
136
|
+
# @param [String] string Text to search.
|
137
|
+
def find(string)
|
138
|
+
s_iter, e_iter, text_selected = selection_bounds
|
139
|
+
s_iter = start_iter unless text_selected
|
140
|
+
s_iter, e_iter = s_iter.forward_search(string, Gtk::TextIter::SEARCH_TEXT_ONLY, e_iter)
|
141
|
+
s_iter ? create_mark(nil, s_iter, false) : nil
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param [String] string Text to replace the selection with.
|
145
|
+
# @param [Gtk::TextIter] s_iter
|
146
|
+
# @param [Gtk::TextIter] e_iter
|
147
|
+
def replace(string, s_iter, e_iter)
|
148
|
+
begin_user_action
|
149
|
+
delete(s_iter, e_iter)
|
150
|
+
insert(s_iter, string)
|
151
|
+
end_user_action
|
152
|
+
place_cursor(s_iter)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Format text in the current selection range with a
|
156
|
+
# Gtk::TextTag[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextTag]
|
157
|
+
# identified by the given name.
|
158
|
+
# @param [String] tag_name
|
159
|
+
def format_selection(tag_name)
|
160
|
+
s_iter, e_iter, text_selected = selection_bounds
|
161
|
+
format(tag_name, s_iter, e_iter) if text_selected
|
162
|
+
end
|
163
|
+
|
164
|
+
# This is a small wrapper around the
|
165
|
+
# Gtk::TextBuffer#apply_tag[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextBuffer#apply_tag]
|
166
|
+
# method. It allows the Gtk::TextTag name to be passed as a symbol.
|
167
|
+
# @param [] tag_name
|
168
|
+
# @param [Gtk::TextIter] s_iter
|
169
|
+
# @param [Gtk::TextIter] e_iter
|
170
|
+
def format(tag_name, s_iter, e_iter)
|
171
|
+
apply_tag(tag_name.to_s, s_iter, e_iter)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Remove all tags of a given name from from one
|
175
|
+
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter]
|
176
|
+
# to another.
|
177
|
+
# @param [] tag_name
|
178
|
+
# @param [Gtk::TextIter] s_iter
|
179
|
+
# @param [Gtk::TextIter] e_iter
|
180
|
+
def unformat(tag_name, s_iter, e_iter)
|
181
|
+
remove_tag(tag_name.to_s, s_iter, e_iter)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Remove all occurrences of a
|
185
|
+
# Gtk::TextTag[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextTag]
|
186
|
+
# in the given selection range.
|
187
|
+
def unformat_selection(*tag_names)
|
188
|
+
s_iter, e_iter, text_selected = selection_bounds
|
189
|
+
if text_selected
|
190
|
+
if tag_names.empty?
|
191
|
+
clear_all(s_iter, e_iter)
|
192
|
+
else
|
193
|
+
tag_names.each { |tag_name| unformat(tag_name, s_iter, e_iter) }
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Remove all Gtk::TextTag's from one
|
199
|
+
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter]
|
200
|
+
# to another.
|
201
|
+
# @param [Gtk::TextIter] s_iter
|
202
|
+
# @param [Gtk::TextIter] e_iter
|
203
|
+
def unformat_all(s_iter, e_iter)
|
204
|
+
remove_all_tags(s_iter, e_iter)
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
# Establish default tag names for everyday text formatting.
|
210
|
+
def setup_default_tags
|
211
|
+
DEFAULT_TAGS.each do |name|
|
212
|
+
attibs = case name
|
213
|
+
when 'bold' then { weight: Pango::WEIGHT_BOLD }
|
214
|
+
when 'italic' then { style: Pango::STYLE_ITALIC }
|
215
|
+
when 'strikethrough' then { strikethrough: true }
|
216
|
+
when 'underline' then { underline: Pango::UNDERLINE_SINGLE }
|
217
|
+
when 'error' then { underline: Pango::UNDERLINE_ERROR }
|
218
|
+
when 'spell_error' then { underline: Pango::UNDERLINE_ERROR }
|
219
|
+
end
|
220
|
+
create_tag(name, attibs)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def setup_spell_check_marks
|
225
|
+
s_iter, e_iter = bounds
|
226
|
+
@insert_start = create_mark('insert-start', s_iter, true)
|
227
|
+
@insert_end = create_mark('insert-end', s_iter, true)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Establish base signal handlers. Here we track user actions and...
|
231
|
+
def setup_signals
|
232
|
+
signal_connect('begin-user-action') { |me| @user_action = true }
|
233
|
+
signal_connect('end-user-action') { |me| @user_action = false }
|
234
|
+
|
235
|
+
signal_connect('insert-text') do |me, iter, text, len|
|
236
|
+
if user_action?
|
237
|
+
me.undo_stack << [ :insert, iter.offset,
|
238
|
+
(iter.offset + text.scan(/./).size), text ]
|
239
|
+
me.redo_stack.clear
|
240
|
+
end
|
241
|
+
me.move_mark(me.insert_start, iter)
|
242
|
+
end
|
243
|
+
|
244
|
+
signal_connect_after('insert-text') do |me, iter, text, len|
|
245
|
+
s_iter = me.get_iter_at_mark(me.insert_start)
|
246
|
+
me.check_spelling(s_iter, iter)
|
247
|
+
me.insert_end = iter
|
248
|
+
end
|
249
|
+
|
250
|
+
signal_connect('delete-range') do |me, s_iter, e_iter|
|
251
|
+
if user_action?
|
252
|
+
me.undo_stack << [ :delete, s_iter.offset, e_iter.offset, text,
|
253
|
+
me.get_text(s_iter, e_iter) ]
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
signal_connect_after('delete-range') do |me, s_iter, e_iter|
|
258
|
+
me.check_spelling(s_iter, e_iter)
|
259
|
+
end
|
260
|
+
|
261
|
+
# TODO: Add suggestion popups for spelling erros.
|
262
|
+
# tag_table.lookup('spell_error').signal_connect('event') do |tag|
|
263
|
+
# p tag
|
264
|
+
# end
|
265
|
+
end
|
266
|
+
|
267
|
+
def user_action?
|
268
|
+
@user_action
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Gtk
|
2
|
+
class TextView
|
3
|
+
|
4
|
+
alias_method :gtk2_initialize, :initialize
|
5
|
+
alias_method :gtk2_buffer=, :buffer=
|
6
|
+
|
7
|
+
def initialize(buffer = nil)
|
8
|
+
self.buffer = buffer
|
9
|
+
self.gtk2_initialize(nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
def buffer=(buffer)
|
13
|
+
buffer.view = self if buffer.is_a?(GtkApp::TextBuffer)
|
14
|
+
self.gtk2_buffer = buffer
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def __button_press_event(event)
|
19
|
+
if event.button == 3
|
20
|
+
x, y = window_to_buffer_coords(Gtk::TextView::WINDOW_TEXT, *event.coords)
|
21
|
+
iter, _ = get_iter_at_position(x, y)
|
22
|
+
buffer.move_mark('click', iter)
|
23
|
+
end
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def __populate_popup(menu)
|
29
|
+
iter = buffer.get_iter_at_mark(buffer.text_marks[:click])
|
30
|
+
s_iter, e_iter = iter.clone, iter.clone
|
31
|
+
|
32
|
+
s_iter.backward_word_start unless s_iter.starts_word?
|
33
|
+
e_iter.forward_word_end if e_iter.inside_word?
|
34
|
+
|
35
|
+
word = buffer.get_text(s_iter, e_iter)
|
36
|
+
|
37
|
+
menu_item = Gtk::MenuItem.new("Spelling Suggestions")
|
38
|
+
submenu = Gtk::Menu.new
|
39
|
+
buffer.spell_check.send(:suggestions, word).each_with_index do |wurd, ndx|
|
40
|
+
_menu_item = Gtk::MenuItem.new(wurd)
|
41
|
+
_menu_item.signal_connect('activate') do |mi|
|
42
|
+
buffer.begin_user_action
|
43
|
+
buffer.delete(s_iter, e_iter)
|
44
|
+
buffer.insert(s_iter, mi.label)
|
45
|
+
buffer.end_user_action
|
46
|
+
end
|
47
|
+
submenu.append(_menu_item)
|
48
|
+
end
|
49
|
+
menu_item.submenu = submenu
|
50
|
+
menu.prepend(menu_item)
|
51
|
+
menu.show_all
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/gtk_app/helpers.rb
CHANGED
@@ -9,6 +9,10 @@ module SignalSupport
|
|
9
9
|
module ClassMethods
|
10
10
|
attr_reader :signal_connections
|
11
11
|
|
12
|
+
# @param [Symbol] widget_name
|
13
|
+
# @param [String] signal_name
|
14
|
+
# @param [Symbol] receiver_method
|
15
|
+
# @yield [...]
|
12
16
|
def on(widget_name, signal_name, receiver_method=nil, &block)
|
13
17
|
|
14
18
|
sc = SignalConnection.new do
|
data/lib/gtk_app/text_buffer.rb
CHANGED
@@ -1,9 +1,27 @@
|
|
1
|
-
require '
|
1
|
+
require 'ffi/aspell'
|
2
2
|
|
3
3
|
module GtkApp
|
4
4
|
class TextBuffer < Gtk::TextBuffer
|
5
|
+
|
6
|
+
# !@attribute [r] spell_check
|
7
|
+
# Aspell object
|
8
|
+
# @return [FFI::Aspell::Speller]
|
5
9
|
attr_reader :spell_check
|
6
|
-
|
10
|
+
|
11
|
+
# !@attribute [r] undo_stack
|
12
|
+
# Collection of actions performed
|
13
|
+
# @return [Array]
|
14
|
+
attr_reader :undo_stack
|
15
|
+
|
16
|
+
# !@attribute [r] redo_stack
|
17
|
+
# Collection of actions undone
|
18
|
+
# @return [Array]
|
19
|
+
attr_reader :redo_stack
|
20
|
+
|
21
|
+
# !@attribute [r] text_marks
|
22
|
+
# Collection of named text marks used to track user input
|
23
|
+
# @return [Hash]
|
24
|
+
attr_reader :text_marks
|
7
25
|
|
8
26
|
DEFAULT_LANG = "en_US"
|
9
27
|
DEFAULT_TAGS = %w[bold italic strikethrough underline error spell_error]
|
@@ -11,11 +29,23 @@ class TextBuffer < Gtk::TextBuffer
|
|
11
29
|
def initialize(tag_table=nil, options={})
|
12
30
|
super(tag_table)
|
13
31
|
@undo_stack, @redo_stack = [], []
|
14
|
-
|
32
|
+
|
33
|
+
options[:lang] ||= DEFAULT_LANG
|
34
|
+
@spell_check = FFI::Aspell::Speller.new(options[:lang])
|
35
|
+
|
15
36
|
setup_default_tags
|
37
|
+
setup_text_marks
|
16
38
|
setup_signals
|
17
39
|
end
|
18
40
|
|
41
|
+
# @param [Gtk::TextView]
|
42
|
+
def view=(text_view)
|
43
|
+
text_view.instance_eval do
|
44
|
+
signal_connect('button-press-event', &:__button_press_event)
|
45
|
+
signal_connect('populate-popup', &:__populate_popup)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
19
49
|
# Pop the last action off the undo stack and rewind changes. If an action was
|
20
50
|
# performed, the cursor is placed at the actions starting
|
21
51
|
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter].
|
@@ -52,11 +82,15 @@ class TextBuffer < Gtk::TextBuffer
|
|
52
82
|
place_cursor(s_iter)
|
53
83
|
end
|
54
84
|
end
|
55
|
-
|
85
|
+
|
86
|
+
# Retrieve the word at the current cursor position
|
87
|
+
# @return [String] The word.
|
56
88
|
def word_at_cursor
|
57
89
|
get_text(*word_bounds).strip
|
58
90
|
end
|
59
91
|
|
92
|
+
# Determine the boudaries of the word at the current cursor position.
|
93
|
+
# @return [Array] The start and end iter.
|
60
94
|
def word_bounds
|
61
95
|
iter = get_iter_at_offset(cursor_position)
|
62
96
|
s_iter, e_iter = iter.clone, iter.clone
|
@@ -66,15 +100,69 @@ class TextBuffer < Gtk::TextBuffer
|
|
66
100
|
[s_iter, e_iter]
|
67
101
|
end
|
68
102
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
103
|
+
# Helper method to check the spelling of every word in the buffer.
|
104
|
+
def check_spelling
|
105
|
+
check_range(*bounds)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @param [Gtk::TextIter] s_iter
|
109
|
+
# @param [Gtk::TextIter] e_iter
|
110
|
+
def check_range(s_iter, e_iter)
|
111
|
+
e_iter.forward_word_end if e_iter.inside_word?
|
112
|
+
unless s_iter.starts_word?
|
113
|
+
if s_iter.inside_word? || s_iter.ends_word?
|
114
|
+
s_iter.backward_word_start
|
115
|
+
elsif s_iter.forward_word_end
|
116
|
+
s_iter.backward_word_start
|
117
|
+
end
|
118
|
+
end
|
119
|
+
# Get the iter at the current cursor position and the pre cursor.
|
120
|
+
c_iter = get_iter_at_offset(cursor_position)
|
121
|
+
pc_iter = c_iter.clone
|
122
|
+
pc_iter.backward_char
|
123
|
+
|
124
|
+
spell_error = tag_table.lookup('spell_error')
|
125
|
+
has_error = (c_iter.has_tag?(spell_error) || pc_iter.has_tag?(spell_error))
|
126
|
+
clear(:spell_error, s_iter, e_iter)
|
127
|
+
|
128
|
+
# NOTE: From GtkSpell, catch rare cases when the replacement occurs at the
|
129
|
+
# beginning of the buffer. An iter at offset 0 seems to always be inside a
|
130
|
+
# word even if it's not.
|
131
|
+
if get_iter_at_offset(0) == s_iter
|
132
|
+
s_iter.forward_word_end
|
133
|
+
s_iter.backward_word_start
|
134
|
+
end
|
135
|
+
|
136
|
+
ws_iter = s_iter.clone # => Word start iter
|
137
|
+
while (ws_iter <=> e_iter) < 0 do
|
138
|
+
we_iter = ws_iter.clone # => Word end iter
|
139
|
+
we_iter.forward_word_end
|
140
|
+
|
141
|
+
if ((ws_iter <=> c_iter) < 0) && ((c_iter <=> we_iter) <= 0)
|
142
|
+
# The current word is being actively edited. Only check it if it's
|
143
|
+
# already been identified as incorrect. Else, check later.
|
144
|
+
check_word(ws_iter, we_iter) if has_error
|
145
|
+
else
|
146
|
+
check_word(ws_iter, we_iter)
|
147
|
+
end
|
148
|
+
# Move the word end iter the the beginning of the next word.
|
149
|
+
we_iter.forward_word_end
|
150
|
+
we_iter.backward_word_start
|
151
|
+
break if we_iter == ws_iter
|
152
|
+
|
153
|
+
ws_iter = we_iter.clone
|
75
154
|
end
|
76
155
|
end
|
77
|
-
|
156
|
+
|
157
|
+
# @param [Gtk::TextIter] s_iter
|
158
|
+
# @param [Gtk::TextIter] e_iter
|
159
|
+
def check_word(s_iter, e_iter)
|
160
|
+
word = get_text(s_iter, e_iter)
|
161
|
+
unless @spell_check.correct?(word)
|
162
|
+
format(:spell_error, s_iter, e_iter)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
78
166
|
# Does the
|
79
167
|
# Gtk::TextTagTable[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextTagTable]
|
80
168
|
# contain any 'spell_error' tags?
|
@@ -85,13 +173,17 @@ class TextBuffer < Gtk::TextBuffer
|
|
85
173
|
# Locate text in selection or the entire buffer. If found, a
|
86
174
|
# Gtk::TextMark[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextMark]
|
87
175
|
# is returned. Else, nil.
|
176
|
+
# @param [String] string Text to search.
|
88
177
|
def find(string)
|
89
178
|
s_iter, e_iter, text_selected = selection_bounds
|
90
179
|
s_iter = start_iter unless text_selected
|
91
180
|
s_iter, e_iter = s_iter.forward_search(string, Gtk::TextIter::SEARCH_TEXT_ONLY, e_iter)
|
92
181
|
s_iter ? create_mark(nil, s_iter, false) : nil
|
93
182
|
end
|
94
|
-
|
183
|
+
|
184
|
+
# @param [String] string Text to replace the selection with.
|
185
|
+
# @param [Gtk::TextIter] s_iter
|
186
|
+
# @param [Gtk::TextIter] e_iter
|
95
187
|
def replace(string, s_iter, e_iter)
|
96
188
|
begin_user_action
|
97
189
|
delete(s_iter, e_iter)
|
@@ -103,6 +195,7 @@ class TextBuffer < Gtk::TextBuffer
|
|
103
195
|
# Format text in the current selection range with a
|
104
196
|
# Gtk::TextTag[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextTag]
|
105
197
|
# identified by the given name.
|
198
|
+
# @param [String] tag_name
|
106
199
|
def format_selection(tag_name)
|
107
200
|
s_iter, e_iter, text_selected = selection_bounds
|
108
201
|
format(tag_name, s_iter, e_iter) if text_selected
|
@@ -111,6 +204,9 @@ class TextBuffer < Gtk::TextBuffer
|
|
111
204
|
# This is a small wrapper around the
|
112
205
|
# Gtk::TextBuffer#apply_tag[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextBuffer#apply_tag]
|
113
206
|
# method. It allows the Gtk::TextTag name to be passed as a symbol.
|
207
|
+
# @param [] tag_name
|
208
|
+
# @param [Gtk::TextIter] s_iter
|
209
|
+
# @param [Gtk::TextIter] e_iter
|
114
210
|
def format(tag_name, s_iter, e_iter)
|
115
211
|
apply_tag(tag_name.to_s, s_iter, e_iter)
|
116
212
|
end
|
@@ -132,6 +228,9 @@ class TextBuffer < Gtk::TextBuffer
|
|
132
228
|
# Remove all tags of a given name from from one
|
133
229
|
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter]
|
134
230
|
# to another.
|
231
|
+
# @param [] tag_name
|
232
|
+
# @param [Gtk::TextIter] s_iter
|
233
|
+
# @param [Gtk::TextIter] e_iter
|
135
234
|
def clear(tag_name, s_iter, e_iter)
|
136
235
|
remove_tag(tag_name.to_s, s_iter, e_iter)
|
137
236
|
end
|
@@ -139,6 +238,8 @@ class TextBuffer < Gtk::TextBuffer
|
|
139
238
|
# Remove all Gtk::TextTag's from one
|
140
239
|
# Gtk::TextIter[http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3ATextIter]
|
141
240
|
# to another.
|
241
|
+
# @param [Gtk::TextIter] s_iter
|
242
|
+
# @param [Gtk::TextIter] e_iter
|
142
243
|
def clear_all(s_iter, e_iter)
|
143
244
|
remove_all_tags(s_iter, e_iter)
|
144
245
|
end
|
@@ -160,6 +261,14 @@ class TextBuffer < Gtk::TextBuffer
|
|
160
261
|
end
|
161
262
|
end
|
162
263
|
|
264
|
+
def setup_text_marks
|
265
|
+
@text_marks = {
|
266
|
+
insert_start: create_mark('insert_start', start_iter, true),
|
267
|
+
insert_end: create_mark('insert_end', start_iter, true),
|
268
|
+
click: create_mark('click', start_iter, true)
|
269
|
+
}
|
270
|
+
end
|
271
|
+
|
163
272
|
# Establish base signal handlers. Here we track user actions and...
|
164
273
|
def setup_signals
|
165
274
|
signal_connect('begin-user-action') { |me| @user_action = true }
|
@@ -171,6 +280,13 @@ class TextBuffer < Gtk::TextBuffer
|
|
171
280
|
(iter.offset + text.scan(/./).size), text]
|
172
281
|
@redo_stack.clear
|
173
282
|
end
|
283
|
+
move_mark(me.text_marks[:insert_start], iter)
|
284
|
+
end
|
285
|
+
|
286
|
+
signal_connect_after('insert-text') do |me, iter, text, len|
|
287
|
+
s_iter = get_iter_at_mark(me.text_marks[:insert_start])
|
288
|
+
check_range(s_iter, iter)
|
289
|
+
move_mark(me.text_marks[:insert_end], iter)
|
174
290
|
end
|
175
291
|
|
176
292
|
signal_connect('delete-range') do |me, s_iter, e_iter|
|
@@ -180,14 +296,14 @@ class TextBuffer < Gtk::TextBuffer
|
|
180
296
|
end
|
181
297
|
end
|
182
298
|
|
183
|
-
|
184
|
-
|
185
|
-
|
299
|
+
signal_connect_after('delete-range') do |me, s_iter, e_iter|
|
300
|
+
check_range(s_iter, e_iter)
|
301
|
+
end
|
186
302
|
end
|
187
303
|
|
188
304
|
def user_action?
|
189
305
|
@user_action
|
190
306
|
end
|
191
|
-
|
307
|
+
|
308
|
+
end
|
192
309
|
end
|
193
|
-
end
|
data/lib/gtk_app/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module GtkApp
|
2
|
-
Version = VERSION = '0.
|
3
|
-
end
|
2
|
+
Version = VERSION = '0.2.0'
|
3
|
+
end
|
data/lib/gtk_app/view.rb
CHANGED
@@ -3,12 +3,17 @@ class View < Gtk::Builder
|
|
3
3
|
include GtkApp::Helpers
|
4
4
|
include GtkApp::ViewHelpers
|
5
5
|
|
6
|
+
# @param [GtkApp::Controller] controller
|
7
|
+
# @param [String] builder_file Path the Gtk builder file
|
6
8
|
def initialize(controller, builder_file, *args)
|
7
9
|
super()
|
10
|
+
# options = args.extract_options!
|
8
11
|
self.add_from_file(builder_file)
|
9
12
|
self.connect_signals { |handler| controller.method(handler) }
|
13
|
+
|
14
|
+
# self.title = options[:title] if options.key?(:title)
|
10
15
|
end
|
11
|
-
|
16
|
+
|
12
17
|
def method_missing(id, *args, &block)
|
13
18
|
method_name = id.to_s
|
14
19
|
|
@@ -44,7 +49,11 @@ class View < Gtk::Builder
|
|
44
49
|
nil,
|
45
50
|
lambda do |row|
|
46
51
|
iter = widget.model.append
|
47
|
-
row.
|
52
|
+
if row.is_a?(Gtk::TreeIter)
|
53
|
+
(0..model.n_columns).each { |i| iter[i] = row[i] }
|
54
|
+
else
|
55
|
+
row.each_with_index { |v,i| iter[i] = v }
|
56
|
+
end
|
48
57
|
iter
|
49
58
|
end ]
|
50
59
|
else
|
@@ -54,13 +63,13 @@ class View < Gtk::Builder
|
|
54
63
|
lambda { |text| widget.text = ("#{widget.text}" << text) } ]
|
55
64
|
else [nil, nil, nil]; end
|
56
65
|
end
|
57
|
-
|
58
|
-
|
66
|
+
|
67
|
+
class_eval do
|
59
68
|
define_method(:"#{widget_name}", lambda { widget })
|
60
69
|
define_method(:"#{widget_name}!", bang_proc) if bang_proc
|
61
70
|
define_method(:"#{widget_name}=", equal_proc) if equal_proc
|
62
71
|
end
|
63
|
-
|
72
|
+
|
64
73
|
widget.class_eval do
|
65
74
|
define_method("<<", append_proc)
|
66
75
|
end if append_proc
|
data/lib/gtk_app/view_helpers.rb
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
module GtkApp
|
2
2
|
module ViewHelpers
|
3
|
-
|
4
|
-
|
3
|
+
|
4
|
+
# Simple way to setup a listview.
|
5
|
+
# @param [Symbol] Widget name
|
6
|
+
# @param [Hash]
|
7
|
+
# @param [Hash] options
|
8
|
+
# @option options [Proc] :formatter
|
9
|
+
# @option options [Array]
|
10
|
+
# @yield [iter, header, column, renderer] Core listview elements are yielded for each column.
|
11
|
+
# @yieldparam [Integer] iter Column index
|
12
|
+
# @yieldparam [String] header Column header text
|
13
|
+
# @yieldparam [Gtk::TreeViewColumn] column The current column
|
14
|
+
# @yieldparam [Gtk::CellRenderer] renderer Current column cell renderer
|
15
|
+
# @raise [ExceptionClass] If an unhandled column data type is provided, an
|
16
|
+
# exception is raised.
|
17
|
+
# @example Build a simple listview.
|
18
|
+
# @view.build_listview(:listThings, {id: Integer, name: String})
|
19
|
+
def build_listview(widget_name, columns, options={}, &block)
|
5
20
|
list = self.send(:"#{widget_name}")
|
6
21
|
list.model = Gtk::ListStore.new(*columns.values)
|
7
22
|
columns.each_with_index do |keyval, index|
|
8
23
|
header, data_type = keyval
|
9
|
-
renderer, attrs = case data_type
|
10
|
-
when String, Integer
|
11
|
-
[Gtk::CellRendererText.new, :
|
12
|
-
when TrueClass
|
24
|
+
renderer, attrs = case "#{data_type}".to_sym
|
25
|
+
when :String, :Integer
|
26
|
+
[Gtk::CellRendererText.new, text: index]
|
27
|
+
when :TrueClass
|
13
28
|
toggle = Gtk::CellRendererToggle.new
|
14
29
|
toggle.signal_connect('toggled') do |widget, path|
|
15
30
|
iter = list.model.get_iter(path)
|
16
31
|
iter[index] = !iter[index]
|
17
32
|
end
|
18
|
-
[toggle, :
|
19
|
-
when Gtk::ListStore
|
33
|
+
[toggle, active: index]
|
34
|
+
when :'Gtk::ListStore'
|
20
35
|
_renderer = Gtk::CellRendererCombo.new
|
21
36
|
model = Gtk::ListStore.new(String)
|
22
37
|
_renderer.signal_connect("edited") do |cell, path, text|
|
23
38
|
model.get_iter(path)[index] = text
|
24
39
|
end
|
25
|
-
[_renderer, :
|
26
|
-
:editable => index]
|
40
|
+
[_renderer, text_column: 0, model: model, text: index, editable: index]
|
27
41
|
else
|
28
42
|
raise("GtkApp::View##{__method__} does not know how to handle " +
|
29
43
|
"'#{data_type}' data types.")
|
@@ -40,20 +54,21 @@ module ViewHelpers
|
|
40
54
|
list.append_column(column)
|
41
55
|
end
|
42
56
|
end
|
43
|
-
|
44
|
-
# Set
|
45
|
-
|
46
|
-
# @
|
57
|
+
|
58
|
+
# Set widget(s) sensitivity property to true
|
59
|
+
# @overload sensitize(:lblFoo, :txtFoo)
|
60
|
+
# @overload sensitize(:cmbBar)
|
47
61
|
def sensitize(*widgets)
|
48
62
|
widgets.each { |w| self["#{w}"].sensitive = true }
|
49
63
|
end
|
50
|
-
|
51
|
-
# Set
|
52
|
-
|
53
|
-
# @
|
64
|
+
|
65
|
+
# Set widget(s) sensitivity property to false
|
66
|
+
# @overload desensitize(:lblFoo, :lblBar)
|
67
|
+
# @overload desensitize(:cmbBar)
|
54
68
|
def desensitize(*widgets)
|
55
69
|
widgets.each { |w| self["#{w}"].sensitive = false }
|
56
70
|
end
|
71
|
+
|
57
72
|
|
58
73
|
end
|
59
74
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gtk_app
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: pkg-config
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,31 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: gtk2
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
25
46
|
- !ruby/object:Gem::Dependency
|
26
47
|
name: activemodel
|
27
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
28
49
|
none: false
|
29
50
|
requirements:
|
30
51
|
- - ! '>='
|
@@ -32,10 +53,15 @@ dependencies:
|
|
32
53
|
version: 3.0.7
|
33
54
|
type: :runtime
|
34
55
|
prerelease: false
|
35
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.0.7
|
36
62
|
- !ruby/object:Gem::Dependency
|
37
|
-
name:
|
38
|
-
requirement:
|
63
|
+
name: ffi-aspell
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
39
65
|
none: false
|
40
66
|
requirements:
|
41
67
|
- - ! '>='
|
@@ -43,13 +69,36 @@ dependencies:
|
|
43
69
|
version: '0'
|
44
70
|
type: :runtime
|
45
71
|
prerelease: false
|
46
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: yard
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
47
94
|
description:
|
48
95
|
email: dan@dj-agiledev.com
|
49
96
|
executables: []
|
50
97
|
extensions: []
|
51
98
|
extra_rdoc_files: []
|
52
99
|
files:
|
100
|
+
- ./lib/gtk_app/application.rb
|
101
|
+
- ./lib/gtk_app/callback_support.rb
|
53
102
|
- ./lib/gtk_app/config.rb
|
54
103
|
- ./lib/gtk_app/controller.rb
|
55
104
|
- ./lib/gtk_app/dialog/ask.rb
|
@@ -62,7 +111,12 @@ files:
|
|
62
111
|
- ./lib/gtk_app/dialog/warn.rb
|
63
112
|
- ./lib/gtk_app/dialog.rb
|
64
113
|
- ./lib/gtk_app/drawer.rb
|
114
|
+
- ./lib/gtk_app/ext/combo_box.rb
|
115
|
+
- ./lib/gtk_app/ext/text_buffer.rb
|
116
|
+
- ./lib/gtk_app/ext/text_view.rb
|
117
|
+
- ./lib/gtk_app/ext/tree_view.rb
|
65
118
|
- ./lib/gtk_app/helpers.rb
|
119
|
+
- ./lib/gtk_app/list_control.rb
|
66
120
|
- ./lib/gtk_app/model.rb
|
67
121
|
- ./lib/gtk_app/observer.rb
|
68
122
|
- ./lib/gtk_app/partial.rb
|
@@ -72,6 +126,7 @@ files:
|
|
72
126
|
- ./lib/gtk_app/version.rb
|
73
127
|
- ./lib/gtk_app/view.rb
|
74
128
|
- ./lib/gtk_app/view_helpers.rb
|
129
|
+
- ./lib/gtk_app/widget_observer.rb
|
75
130
|
- ./lib/gtk_app.rb
|
76
131
|
homepage: https://github.com/drfeelngood/rb-gtk_app
|
77
132
|
licenses:
|
@@ -94,8 +149,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
149
|
version: '0'
|
95
150
|
requirements: []
|
96
151
|
rubyforge_project:
|
97
|
-
rubygems_version: 1.8.
|
152
|
+
rubygems_version: 1.8.24
|
98
153
|
signing_key:
|
99
154
|
specification_version: 3
|
100
155
|
summary: A ruby-gtk framework
|
101
156
|
test_files: []
|
157
|
+
has_rdoc: true
|