1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
|
# -*- coding: utf-8 -*-
# RubyGnome2を用いてUIを表示するプラグイン
require "gtk2"
miquire :mui,
'cell_renderer_message', 'coordinate_module', 'icon_over_button', 'inner_tl', 'markup_generator',
'miracle_painter', 'replyviewer', 'sub_parts_favorite', 'sub_parts_helper',
'sub_parts_retweet', 'sub_parts_voter', 'textselector', 'timeline', 'contextmenu', 'crud',
'extension', 'intelligent_textview', 'keyconfig', 'listlist', 'message_picker', 'mtk', 'postbox',
'pseudo_signal_handler', 'selectbox', 'timeline_utils', 'userlist', 'webicon'
require File.expand_path File.join(File.dirname(__FILE__), 'mikutter_window')
require File.expand_path File.join(File.dirname(__FILE__), 'tab_container')
require File.expand_path File.join(File.dirname(__FILE__), 'tab_toolbar')
require File.expand_path File.join(File.dirname(__FILE__), 'delayer')
require File.expand_path File.join(File.dirname(__FILE__), 'slug_dictionary')
require File.expand_path File.join(File.dirname(__FILE__), 'mainloop')
require File.expand_path File.join(File.dirname(__FILE__), 'konami_watcher')
Plugin.create :gtk do
@slug_dictionary = Plugin::Gtk::SlugDictionary.new # widget_type => {slug => Gtk}
@tabs_promise = {} # slug => Deferred
TABPOS = [Gtk::POS_TOP, Gtk::POS_BOTTOM, Gtk::POS_LEFT, Gtk::POS_RIGHT]
# ウィンドウ作成。
# PostBoxとか複数のペインを持つための処理が入るので、Gtk::MikutterWindowクラスを新設してそれを使う
on_window_created do |i_window|
window = ::Gtk::MikutterWindow.new(i_window, self)
@slug_dictionary.add(i_window, window)
window.title = i_window.name
window.set_size_request(240, 240)
geometry = get_window_geometry(i_window.slug)
window.set_default_size(*geometry[:size])
window.move(*geometry[:position])
window.ssc(:event){ |window, event|
if event.is_a? Gdk::EventConfigure
geometry = (UserConfig[:windows_geometry] || {}).melt
size = window.window.geometry[2,2]
position = window.position
modified = false
if defined?(geometry[i_window.slug]) and geometry[i_window.slug].is_a? Hash
geometry[i_window.slug] = geometry[i_window.slug].melt
if geometry[i_window.slug][:size] != size
modified = geometry[i_window.slug][:size] = size end
if geometry[i_window.slug][:position] != position
modified = geometry[i_window.slug][:position] = position end
else
modified = geometry[i_window.slug] = {
size: size,
position: position } end
if modified
UserConfig[:windows_geometry] = geometry end end
false }
window.ssc("destroy"){
Delayer.freeze
window.destroy
::Gtk::Object.main_quit
# Gtk.main_quit
false }
window.ssc(:focus_in_event) {
i_window.active!(true, true)
false
}
window.ssc('key_press_event'){ |widget, event|
Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_window) }
window.show_all
end
on_gui_window_change_icon do |i_window, icon|
window = widgetof(i_window)
if window
window.icon = icon.load_pixbuf(width: 256, height: 256){|pb|
window.icon = pb if not window.destroyed?
}
end
end
# ペイン作成。
# ペインはGtk::NoteBook
on_pane_created do |i_pane|
pane = create_pane(i_pane)
pane.set_tab_border(0).set_group_id(0).set_scrollable(true)
pane.set_tab_pos(TABPOS[UserConfig[:tab_position]])
tab_position_hook_id = UserConfig.connect(:tab_position){ |key, val, before_val, id|
pane.set_tab_pos(TABPOS[val]) unless pane.destroyed? }
pane.ssc(:page_reordered){ |this, tabcontainer, index|
Plugin.call(:rewind_window_order, i_pane.parent) if i_pane.parent
i_tab = tabcontainer.i_tab
if i_tab
i_pane.reorder_child(i_tab, index) end
Plugin.call(:after_gui_tab_reordered, i_tab)
false }
pane.ssc(:switch_page){ |this, page, pagenum|
if pagenum == pane.page
i_pane.set_active_child(pane.get_nth_page(pagenum).i_tab, true) end }
pane.signal_connect(:page_added){ |this, tabcontainer, index|
type_strict tabcontainer => ::Gtk::TabContainer
Plugin.call(:rewind_window_order, i_pane.parent) if i_pane.parent
i_tab = tabcontainer.i_tab
next false if i_tab.parent == i_pane
Plugin.call(:after_gui_tab_reparent, i_tab, i_tab.parent, i_pane)
i_pane.add_child(i_tab, index)
false }
# 子が無くなった時 : このpaneを削除
pane.signal_connect(:page_removed){
if not(pane.destroyed?) and pane.children.empty? and pane.parent
pane.parent.remove(pane)
UserConfig.disconnect(tab_position_hook_id)
pane_order_delete(i_pane)
i_pane.destroy end
false }
end
# タブ作成。
# タブには実体が無いので、タブのアイコンのところをGtk::EventBoxにしておいて、それを実体ということにしておく
on_tab_created do |i_tab|
tab = create_tab(i_tab)
if @tabs_promise[i_tab.slug]
@tabs_promise[i_tab.slug].call(tab)
@tabs_promise.delete(i_tab.slug) end end
on_cluster_created do |i_cluster|
create_pane(i_cluster) end
on_fragment_created do |i_fragment|
create_tab(i_fragment) end
# タブを作成する
# ==== Args
# [i_tab] タブ
# ==== Return
# Tab(Gtk::EventBox)
def create_tab(i_tab)
tab = ::Gtk::EventBox.new.tooltip(i_tab.name)
@slug_dictionary.add(i_tab, tab)
tab_update_icon(i_tab)
tab.ssc(:focus_in_event) {
i_tab.active!(true, true)
false
}
tab.ssc(:key_press_event){ |widget, event|
Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_tab) }
tab.ssc(:button_press_event) { |this, event|
if event.button == 3
Plugin::GUI::Command.menu_pop(i_tab)
else
Plugin::GUI.keypress(::Gtk::buttonname([event.event_type, event.button, event.state]), i_tab)
end
false }
tab.ssc(:destroy) {
i_tab.destroy
false }
tab.show_all end
on_tab_toolbar_created do |i_tab_toolbar|
tab_toolbar = ::Gtk::TabToolbar.new(i_tab_toolbar).show_all
@slug_dictionary.add(i_tab_toolbar, tab_toolbar)
end
on_gui_tab_toolbar_join_tab do |i_tab_toolbar, i_tab|
widget = widgetof(i_tab_toolbar)
widget_join_tab(i_tab, widget) if widget
end
# タイムライン作成。
# Gtk::TimeLine
on_timeline_created do |i_timeline|
gtk_timeline = ::Gtk::TimeLine.new(i_timeline)
@slug_dictionary.add(i_timeline, gtk_timeline)
gtk_timeline.tl.ssc(key_press_event: timeline_key_press_event(i_timeline),
focus_in_event: timeline_focus_in_event(i_timeline),
destroy: timeline_destroy_event(i_timeline))
gtk_timeline.show_all
end
# Timelineウィジェットのfocus_in_eventのコールバックを返す
# ==== Args
# [i_timeline] タイムラインのインターフェイス
# ==== Return
# Proc
def timeline_focus_in_event(i_timeline)
lambda { |this, event|
if this.focus?
i_timeline.active!(true, true) end
false } end
# Timelineウィジェットのkey_press_eventのコールバックを返す
# ==== Args
# [i_timeline] タイムラインのインターフェイス
# ==== Return
# Proc
def timeline_key_press_event(i_timeline)
lambda { |widget, event|
Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_timeline) } end
# Timelineウィジェットのdestroyのコールバックを返す
# ==== Args
# [i_timeline] タイムラインのインターフェイス
# ==== Return
# Proc
def timeline_destroy_event(i_timeline)
lambda { |this|
i_timeline.destroy
false } end
on_gui_pane_join_window do |i_pane, i_window|
window = widgetof(i_window)
pane = widgetof(i_pane)
if pane.parent
if pane.parent != window.panes
pane.parent.remove(pane)
window.panes.pack_end(pane, false).show_all end
else
window.panes.pack_end(pane, false).show_all
end
end
on_gui_tab_join_pane do |i_tab, i_pane|
i_widget = i_tab.children.first
next if not i_widget
widget = widgetof(i_widget)
next if not widget
tab = widgetof(i_tab)
pane = widgetof(i_pane)
old_pane = widget.get_ancestor(::Gtk::Notebook)
if tab and pane and old_pane and pane != old_pane
if tab.parent
page_num = tab.parent.get_tab_pos_by_tab(tab)
if page_num
tab.parent.remove_page(page_num)
else
raise Plugin::Gtk::GtkError, "#{tab} not found in #{tab.parent}" end end
i_tab.children.each{ |i_child|
w_child = widgetof(i_child)
w_child.parent.remove(w_child)
widget_join_tab(i_tab, w_child) }
tab.show_all end
Plugin.call(:rewind_window_order, i_pane.parent) if i_pane.parent
end
on_gui_timeline_join_tab do |i_timeline, i_tab|
widget = widgetof(i_timeline)
widget_join_tab(i_tab, widget) if widget end
on_gui_cluster_join_tab do |i_cluster, i_tab|
widget = widgetof(i_cluster)
widget_join_tab(i_tab, widget) if widget end
on_gui_timeline_add_messages do |i_timeline, messages|
gtk_timeline = widgetof(i_timeline)
gtk_timeline.add(messages) if gtk_timeline and not gtk_timeline.destroyed? end
on_gui_postbox_join_widget do |i_postbox|
type_strict i_postbox => Plugin::GUI::Postbox
i_postbox_parent = i_postbox.parent
next if not i_postbox_parent
postbox_parent = widgetof(i_postbox_parent)
next if not postbox_parent
postbox = @slug_dictionary.add(i_postbox, postbox_parent.add_postbox(i_postbox))
postbox.post.ssc(:focus_in_event) {
i_postbox.active!(true, true)
false }
postbox.post.ssc("populate-popup"){ |widget, menu|
(event, items) = Plugin::GUI::Command.get_menu_items(i_postbox)
menu.append(Gtk::SeparatorMenuItem.new) if items.length != 0
menu2 = Gtk::ContextMenu.new(*items).build!(i_postbox, event, menu)
menu2.show_all
true }
postbox.post.ssc('key_press_event'){ |widget, event|
Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_postbox) }
postbox.post.ssc(:destroy){
i_postbox.destroy
false }
end
on_gui_tab_change_icon do |i_tab|
tab_update_icon(i_tab) end
on_tab_toolbar_rewind do |i_tab_toolbar|
tab_toolbar = widgetof(i_tab_toolbar)
if tab_toolbar
tab_toolbar.set_button end end
on_gui_contextmenu do |event, contextmenu|
widget = widgetof(event.widget)
if widget
::Gtk::ContextMenu.new(*contextmenu).popup(widget, event) end end
on_gui_timeline_clear do |i_timeline|
timeline = widgetof(i_timeline)
if timeline
timeline.clear end end
on_gui_timeline_scroll_to_top do |i_timeline|
timeline = widgetof(i_timeline)
if timeline
timeline.set_cursor_to_display_top end end
on_gui_timeline_move_cursor_to do |i_timeline, message|
tl = widgetof(i_timeline)
if tl
path, column = tl.cursor
if path and column
case message
when :prev
path.prev!
tl.set_cursor(path, column, false)
when :next
path.next!
tl.set_cursor(path, column, false)
else
if message.is_a? Integer
path, = *tl.get_path(0, message)
tl.set_cursor(path, column, false) if path end end end end end
on_gui_timeline_set_order do |i_timeline, order|
widgetof(i_timeline).set_order(&order) end
filter_gui_timeline_select_messages do |i_timeline, messages|
[i_timeline,
messages.select(&widgetof(i_timeline).method(:include?))] end
filter_gui_timeline_reject_messages do |i_timeline, messages|
[i_timeline,
messages.reject(&widgetof(i_timeline).method(:include?))] end
on_gui_postbox_post do |i_postbox|
postbox = widgetof(i_postbox)
if postbox
postbox.post_it end end
# i_widget.destroyされた時に呼ばれる。
# 必要ならば、ウィジェットの実体もあわせて削除する。
on_gui_destroy do |i_widget|
widget = widgetof(i_widget)
if widget and not widget.destroyed?
if i_widget.is_a?(Plugin::GUI::Tab) and i_widget.parent
pane = widgetof(i_widget.parent)
if pane
pane.n_pages.times{ |pagenum|
if widget == pane.get_tab_label(pane.get_nth_page(pagenum))
Plugin.call(:rewind_window_order, i_widget.parent.parent)
pane.remove_page(pagenum)
break end } end
else
widget.parent.remove(widget) if widget.parent
widget.destroy end end end
# 互換性のため
on_mui_tab_regist do |container, name, icon|
slug = name.to_sym
i_tab = Plugin::GUI::Tab.instance(slug, name)
i_tab.set_icon(icon).expand
i_container = Plugin::GUI::TabChildWidget.instance
@slug_dictionary.add(i_container, container)
i_tab << i_container
@tabs_promise[i_tab.slug] = (@tabs_promise[i_tab.slug] || Deferred.new).next{ |tab|
widget_join_tab(i_tab, container.show_all) } end
# Gtkオブジェクトをタブに入れる
on_gui_nativewidget_join_tab do |i_tab, i_container, container|
@slug_dictionary.add(i_container, container)
widget_join_tab(i_tab, container.show_all) end
on_gui_nativewidget_join_fragment do |i_fragment, i_container, container|
@slug_dictionary.add(i_container, container)
widget_join_tab(i_fragment, container.show_all) end
on_gui_window_rewindstatus do |i_window, text, expire|
window = @slug_dictionary.get(Plugin::GUI::Window, :default)
next if not window
statusbar = window.statusbar
cid = statusbar.get_context_id("system")
mid = statusbar.push(cid, text)
if expire != 0
Reserver.new(expire, thread: Delayer) do
if not statusbar.destroyed?
statusbar.remove(cid, mid)
end
end
end
end
on_gui_child_activated do |i_parent, i_child, activated_by_toolkit|
type_strict i_parent => Plugin::GUI::HierarchyParent, i_child => Plugin::GUI::HierarchyChild
if !activated_by_toolkit
if i_child.is_a?(Plugin::GUI::TabLike)
i_pane = i_parent
i_tab = i_child
pane = widgetof(i_pane)
tab = widgetof(i_tab)
if pane and tab
pagenum = pane.get_tab_pos_by_tab(tab)
pane.page = pagenum if pagenum and pane.page != pagenum end
elsif i_parent.is_a?(Plugin::GUI::Window)
i_term = i_child.respond_to?(:active_chain) ? i_child.active_chain.last : i_child
if i_term
window = widgetof(i_parent)
widget = widgetof(i_term)
if window and widget
if widget.respond_to? :active
widget.active
else
window.set_focus(widget) end end end end end end
on_posted do |service, messages|
messages.each{ |message|
if(replyto_source = message.replyto_source)
Gdk::MiraclePainter.findbymessage(replyto_source).each{ |mp|
mp.on_modify } end } end
on_favorite do |service, user, message|
if(user.me?)
Gdk::MiraclePainter.findbymessage(message).each{ |mp|
mp.on_modify } end end
on_konami_activate do
Gtk.konami_load
end
filter_gui_postbox_input_editable do |i_postbox, editable|
postbox = widgetof(i_postbox)
if postbox
[i_postbox, postbox && postbox.post.editable?]
else
[i_postbox, editable] end end
filter_gui_timeline_cursor_position do |i_timeline, y|
timeline = widgetof(i_timeline)
if timeline
path, column = *timeline.cursor
if path
rect = timeline.get_cell_area(path, column)
next [i_timeline, rect.y + (rect.height / 2).to_i] end
end
[i_timeline, y] end
filter_gui_timeline_selected_messages do |i_timeline, messages|
timeline = widgetof(i_timeline)
if timeline
[i_timeline, messages + timeline.get_active_messages]
else
[i_timeline, messages] end end
filter_gui_timeline_selected_text do |i_timeline, message, text|
timeline = widgetof(i_timeline)
next [i_timeline, message, text] if not timeline
record = timeline.get_record_by_message(message)
next [i_timeline, message, text] if not record
range = record.miracle_painter.textselector_range
next [i_timeline, message, text] if not range
[i_timeline, message, message.entity.to_s[range]]
end
filter_gui_destroyed do |i_widget|
if i_widget.is_a? Plugin::GUI::Widget
[!widgetof(i_widget)]
else
[i_widget] end end
filter_gui_get_gtk_widget do |i_widget|
[widgetof(i_widget)] end
# タブ _tab_ に _widget_ を入れる
# ==== Args
# [i_tab] タブ
# [widget] Gtkウィジェット
def widget_join_tab(i_tab, widget)
tab = widgetof(i_tab)
return false if not tab
i_pane = i_tab.parent
return false if not i_pane
pane = widgetof(i_pane)
return false if not pane
is_tab = i_tab.is_a?(Plugin::GUI::Tab)
has_child = is_tab and
not(i_tab.temporary_tab?) and
not(i_tab.children.any?{ |child|
not child.is_a? Plugin::GUI::TabToolbar })
if has_child
Plugin.call(:rewind_window_order, i_pane.parent) end
container_index = pane.get_tab_pos_by_tab(tab)
if container_index
container = pane.get_nth_page(container_index)
if container
return container.pack_start(widget, i_tab.pack_rule[container.children.size]) end end
if tab.parent
raise Plugin::Gtk::GtkError, "Gtk Widget #{tab.inspect} of Tab(#{i_tab.slug.inspect}) has parent Gtk Widget #{tab.parent.inspect}" end
container = ::Gtk::TabContainer.new(i_tab).show_all
container.ssc(:key_press_event){ |w, event|
Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_tab) }
container.pack_start(widget, i_tab.pack_rule[container.children.size])
pane.append_page(container, tab)
pane.set_tab_reorderable(container, true).set_tab_detachable(container, true)
true end
def tab_update_icon(i_tab)
type_strict i_tab => Plugin::GUI::TabLike
tab = widgetof(i_tab)
if tab
tab.tooltip(i_tab.name)
tab.remove(tab.child) if tab.child
if i_tab.icon
tab.add(::Gtk::WebIcon.new(i_tab.icon, 24, 24).show)
else
tab.add(::Gtk::Label.new(i_tab.name).show) end end
self end
def get_window_geometry(slug)
type_strict slug => Symbol
geo = UserConfig[:windows_geometry]
if defined? geo[slug]
geo[slug]
else
size = [Gdk.screen_width/3, Gdk.screen_height*4/5]
{ size: size,
position: [Gdk.screen_width - size[0], Gdk.screen_height/2 - size[1]/2] } end end
# ペインを作成
# ==== Args
# [i_pane] ペイン
# ==== Return
# ペイン(Gtk::Notebook)
def create_pane(i_pane)
pane = ::Gtk::Notebook.new
@slug_dictionary.add(i_pane, pane)
pane.ssc('key_press_event'){ |widget, event|
Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_pane) }
pane.ssc(:destroy){
i_pane.destroy if i_pane.destroyed?
false }
pane.show_all end
# ウィンドウ内のペイン、タブの現在の順序を設定に保存する
on_rewind_window_order do |i_window|
if :default == i_window.slug
panes_order = {}
i_window.children.each{ |i_pane|
if i_pane.is_a? Plugin::GUI::Pane
tab_order = []
pane = widgetof(i_pane)
if pane
pane.n_pages.times{ |page_num|
i_widget = find_implement_widget_by_gtkwidget(pane.get_tab_label(pane.get_nth_page(page_num)))
if i_widget and not i_widget.temporary_tab? and i_widget.children.any?{ |child| not child.is_a? Plugin::GUI::TabToolbar }
tab_order << i_widget.slug end } end
panes_order[i_pane.slug] = tab_order if not tab_order.empty? end }
ui_tab_order = (UserConfig[:ui_tab_order] || {}).melt
ui_tab_order[i_window.slug] = panes_order
UserConfig[:ui_tab_order] = ui_tab_order end end
# ペインを順序リストから削除する
# ==== Args
# [i_pane] ペイン
def pane_order_delete(i_pane)
order = UserConfig[:ui_tab_order].melt
i_window = i_pane.parent
order[i_window.slug] = order[i_window.slug].melt
order[i_window.slug].delete(i_pane.slug)
UserConfig[:ui_tab_order] = order
end
# _cuscadable_ に対応するGtkオブジェクトを返す
# ==== Args
# [cuscadable] ウィンドウ、ペイン、タブ、タイムライン等
# ==== Return
# 対応するGtkオブジェクト
def widgetof(cuscadable)
type_strict cuscadable => :slug
result = @slug_dictionary.get(cuscadable)
if result and result.destroyed?
nil
else
result end end
# Gtkオブジェクト _widget_ に対応するウィジェットのオブジェクトを返す
# ==== Args
# [widget] Gtkウィジェット
# ==== Return
# _widget_ に対応するウィジェットオブジェクトまたは偽
def find_implement_widget_by_gtkwidget(widget)
@slug_dictionary.imaginally_by_gtk(widget) end
end
module Plugin::Gtk
class GtkError < Exception
end end
|