File: gtk3.rb

package info (click to toggle)
mikutter 5.1.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,780 kB
  • sloc: ruby: 22,912; sh: 186; makefile: 21
file content (748 lines) | stat: -rw-r--r-- 22,476 bytes parent folder | download
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
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
# -*- coding: utf-8 -*-

# RubyGnomeを用いてUIを表示するプラグイン
require 'gtk3'

require_relative 'patch'

require 'mui/gtk_contextmenu'
require 'mui/gtk_compatlistview'
require 'mui/gtk_crud'
require 'mui/gtk_extension'
require 'mui/gtk_intelligent_textview'
require 'mui/gtk_keyconfig'
require 'mui/gtk_message_picker'
require 'mui/gtk_mtk'
require 'mui/gtk_postbox'
require 'mui/gtk_pseudo_signal_handler'
require 'mui/gtk_selectbox'
require 'mui/gtk_timeline_utils'
require 'mui/gtk_userlist'
require 'mui/gtk_webicon'

require_relative 'widget/dialog'
require_relative 'widget/mikutterwindow'
require_relative 'widget/miraclepainter'
require_relative 'widget/tabcontainer'
require_relative 'widget/tabtoolbar'
require_relative 'widget/timeline'
require_relative 'widget/worldshifter'

require_relative 'konami_watcher'
require_relative 'mainloop'
require_relative 'slug_dictionary'
require_relative 'toolbar_generator'

Plugin.create :gtk3 do
  @slug_dictionary = Plugin::Gtk3::SlugDictionary.new # widget_type => {slug => Gtk}
  @tabs_promise = {} # slug => Deferred

  # ウィンドウ作成。
  # PostBoxとか複数のペインを持つための処理が入るので、Gtk::MikutterWindowクラスを新設してそれを使う
  on_window_created do |i_window|
    window = Plugin::Gtk3::MikutterWindow.new(i_window, self)
    @parent = window
    @slug_dictionary.add(i_window, window)
    window.title = i_window.name

    geometry = get_window_geometry(i_window.slug)
    if geometry
      window.set_default_size(*geometry[:size])
      window.move(*geometry[:position])
    end

    window.ssc(:event) do |t_window, event|
      if event.is_a? Gdk::EventConfigure
        geometry = (UserConfig[:windows_geometry] || {}).melt
        size = t_window.window.geometry[2, 2]
        position = t_window.position
        modified = false
        if geometry&.dig(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:,
            position:
          }
        end
        if modified
          UserConfig[:windows_geometry] = geometry
        end
      end
      false
    end
    window.ssc('destroy') do
      Delayer.freeze
      window.destroy
      Mainloop.reserve_exit
      false
    end
    window.ssc(:focus_in_event) do
      i_window.active!(true, true)
      false
    end
    window.ssc('key_press_event') do |_widget, event|
      Plugin::GUI.keypress(Gtk.keyname([event.keyval, event.state]), i_window)
    end
    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) do |pb|
        window.icon = pb unless window.destroyed?
      end
    end
  end

  # ペイン作成。
  # ペインはGtk::NoteBook
  on_pane_created do |i_pane|
    tabpos = %i[top bottom left right].freeze
    # pane => Gtk::Notebook
    pane = create_pane(i_pane)
    pane.group_name = '0'
    pane.scrollable = true
    pane.show_border = false
    pane.set_tab_pos(tabpos[UserConfig[:tab_position]])
    pane.hexpand = true
    tab_position_listener = on_userconfig_modify do |key, val|
      next if key != :tab_position
      if pane.destroyed?
        tab_position_listener.detach
      else
        pane.set_tab_pos(tabpos[val])
      end
    end
    pane.ssc(:page_reordered) do |_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
    end
    pane.ssc :switch_page do |_, tab|
      i_pane.set_active_child(tab.i_tab, true)
    end
    pane.signal_connect(:page_added) do |_this, tabcontainer, index|
      type_strict tabcontainer => Plugin::Gtk3::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
    end
    # 子が無くなった時 : このpaneを削除
    pane.signal_connect(:page_removed) do
      if !pane.destroyed? && pane.children.empty? && pane.parent
        pane.parent.remove(pane)
        tab_position_listener.detach
        pane_order_delete(i_pane)
        i_pane.destroy end
      false
    end
  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|
    pane = create_pane(i_cluster)
    pane.scrollable = true
    pane.ssc(:page_reordered) do |_this, tabcontainer, index|
      tabcontainer.i_tab&.then do |i_tab|
        i_cluster.reorder_child(i_tab, index)
        Plugin.call(:after_gui_tab_reordered, i_tab)
      end
      false
    end
  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
    tab.tooltip_text = i_tab.name
    tab.visible_window = false
    @slug_dictionary.add(i_tab, tab)
    tab_update_icon(i_tab)
    tab.ssc(:focus_in_event) do
      i_tab.active!(true, true)
      false
    end
    tab.ssc(:key_press_event) do |_widget, event|
      Plugin::GUI.keypress(Gtk.keyname([event.keyval, event.state]), i_tab)
    end
    tab.ssc(:button_press_event) do |_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
    end
    tab.ssc(:destroy) do
      i_tab.destroy
      false
    end
    tab.show_all
  end

  on_tab_toolbar_created do |i_tab_toolbar|
    tab_toolbar = Plugin::Gtk3::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

  # タイムライン作成。
  on_timeline_created do |i_timeline|
    timeline = Plugin::Gtk3::Timeline.new(i_timeline)
    @slug_dictionary.add(i_timeline, timeline)
    timeline.show_all
  end

  on_gui_pane_join_window do |i_pane, i_window|
    window = widgetof(i_window)
    pane = widgetof(i_pane)
    pane.parent && pane.parent != window.panes and pane.parent.remove(pane)
    # 左端にペインを追加
    pane.parent && pane.parent == window.panes or
      window.panes.attach_next_to pane, nil, :left, 1, 1
  end

  on_gui_tab_join_pane do |i_tab, i_pane|
    i_widget = i_tab.children.first
    next unless i_widget
    widget = widgetof(i_widget)
    next unless widget
    tab = widgetof(i_tab)
    pane = widgetof(i_pane)
    old_pane = widget.get_ancestor(Gtk::Notebook)
    if tab && pane && old_pane && (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 do |i_child|
        w_child = widgetof(i_child)
        w_child.parent.remove(w_child)
        widget_join_tab(i_tab, w_child)
      end
      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|
    timeline = widgetof(i_timeline)
    timeline.bulk_add(messages) if timeline && !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 unless i_postbox_parent
    postbox_parent = widgetof(i_postbox_parent)
    next unless postbox_parent
    postbox = @slug_dictionary.add(i_postbox, postbox_parent.add_postbox(i_postbox))
    postbox.post.ssc(:focus_in_event) do
      i_postbox.active!(true, true)
      false
    end

    postbox.post.ssc('populate-popup') do |_widget, menu|
      (event, items) = Plugin::GUI::Command.get_menu_items(i_postbox)

      menu.append(Gtk::SeparatorMenuItem.new) unless items.empty?
      menu2 = Gtk::ContextMenu.new(*items).build!(i_postbox, event, menu)
      menu2.show_all
      true
    end

    postbox.post.ssc('key_press_event') do |_widget, event|
      Plugin::GUI.keypress(Gtk.keyname([event.keyval, event.state]), i_postbox)
    end
    postbox.post.ssc(:destroy) do
      i_postbox.destroy
      false
    end
  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)
    tab_toolbar&.set_button
  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)
    timeline&.clear
  end

  on_gui_timeline_scroll do |i_timeline, to|
    timeline = widgetof(i_timeline) or next
    timeline.jump_to to
  end

  on_gui_timeline_move_cursor_to do |i_timeline, direction_or_index|
    timeline = widgetof(i_timeline) or next

    row = timeline.selected_rows.first or next
    i = case direction_or_index
        when :prev
          [row.index - 1, 0].max
        when :next
          [row.index + 1, timeline.size].min
        else
          next unless direction_or_index.is_a? Integer
          direction_or_index
        end

    timeline.select_row_at_index i
  end

  on_gui_timeline_set_order do |i_timeline, order|
    widgetof(i_timeline).order = order
  end

  filter_gui_timeline_select_messages do |i_timeline, messages|
    timeline = widgetof(i_timeline)
    if timeline
      [i_timeline,
       messages.select(&timeline.method(:include?))]
    else
      [i_timeline, messages]
    end
  end

  filter_gui_timeline_reject_messages do |i_timeline, messages|
    w_timeline = widgetof(i_timeline)
    if w_timeline
      [i_timeline,
       messages.reject(&w_timeline.method(:include?))]
    else
      [i_timeline, messages]
    end
  end

  on_gui_postbox_post do |i_postbox, options|
    widgetof(i_postbox)&.post_it(world: options[:world])
  end

  # i_widget.destroyされた時に呼ばれる。
  # 必要ならば、ウィジェットの実体もあわせて削除する。
  on_gui_destroy do |i_widget|
    widget = widgetof(i_widget)
    if widget && !widget.destroyed?
      if i_widget.is_a?(Plugin::GUI::Tab) && i_widget.parent
        pane = widgetof(i_widget.parent)
        pane&.n_pages&.times do |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)
        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 do |_tab|
      widget_join_tab(i_tab, container.show_all)
    end
  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 unless window
    statusbar = window.statusbar
    cid = statusbar.get_context_id('system')
    mid = statusbar.push(cid, text)
    if expire != 0
      Delayer.new(delay: expire) do
        unless 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
    unless 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)
        pane && tab and pane.page = pane.get_tab_pos_by_tab(tab)
      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 && 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 do |message|
      replyto_source = message.replyto_source
      if replyto_source
        Plugin::Gtk3::Timeline.update_rows(replyto_source)
      end
    end
  end

  on_message_modified(&Plugin::Gtk3::Timeline.method(:update_rows))
  on_destroyed(&Plugin::Gtk3::Timeline.method(:remove_rows))

  share = ->(_, model) do
    Plugin::Gtk3::Timeline.update_rows(model)
  end

  on_share(&share)
  on_before_share(&share)
  on_fail_share(&share)
  on_destroy_share(&share)

  favorite = ->(_, _, model) do
    Plugin::Gtk3::Timeline.update_rows(model)
  end

  on_favorite(&favorite)
  on_before_favorite(&favorite)
  on_fail_favorite(&favorite)

  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&.post&.editable?]
    else
      [i_postbox, editable] end
  end

  filter_gui_timeline_cursor_position do |_i_timeline, _y|
    raise NotImplementedError
  end

  filter_gui_timeline_selected_messages do |i_timeline, messages|
    timeline = widgetof(i_timeline)
    if timeline
      [i_timeline, messages + timeline.selected_rows.map(&:model)]
    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] unless timeline
    record = timeline.selected_rows.find { |row| row.model == message }
    next [i_timeline, message, text] unless record
    range = record.textselector_range
    next [i_timeline, message, text] unless range
    if UserConfig[:miraclepainter_expand_custom_emoji]
      adjust = score_of(message).each_with_object(Hash.new(0)) do |note, state|
        if note.respond_to?(:inline_photo)
          # 1 -> cairo_markup_generatorで便宜上置換された、絵文字の文字長
          if range.include?(state[:index])
            state[:end] += note.description.size - 1
          elsif state[:index] < range.begin
            state[:begin] += note.description.size - 1
          end
          state[:index] += 1
        else
          state[:index] += note.description.size
        end
      end
      range = Range.new(range.begin + adjust[:begin], range.end + adjust[:begin] + adjust[:end], true)
    end
    [i_timeline, message, score_of(message).map(&:description).join[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

  on_gui_dialog do |plugin, title, default, proc, promise|
    Plugin::Gtk3::Dialog.open(
      plugin:,
      title:,
      default:,
      promise:,
      parent: @parent,
      &proc
    )
  end

  filter_before_mainloop_exit do
    unless @slug_dictionary.widgets(Plugin::GUI::Window).all?(&:destroyed?)
      error 'Filter before_mainloop_exit was canceled because window already exists.'
      Plugin.filter_cancel!
    end
    []
  end

  # タブ _tab_ に _widget_ を入れる
  # ==== Args
  # [i_tab] タブ
  # [widget] Gtkウィジェット
  def widget_join_tab(i_tab, widget)
    tab = widgetof(i_tab)
    return false unless tab
    i_pane = i_tab.parent
    return false unless i_pane
    pane = widgetof(i_pane)
    return false unless pane
    is_tab = i_tab.is_a?(Plugin::GUI::Tab)
    has_child = is_tab &&
                !i_tab.temporary_tab? &&
                i_tab.children.any?(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
        widget.vexpand = i_tab.pack_rule[container.children.size]
        return container.add(widget)
      end
    end
    if tab.parent
      raise Plugin::Gtk3::GtkError, "Gtk Widget #{tab.inspect} of Tab(#{i_tab.slug.inspect}) has parent Gtk Widget #{tab.parent.inspect}"
    end
    container = Plugin::Gtk3::TabContainer.new(i_tab).show_all
    container.ssc(:key_press_event) do |_w, event|
      Plugin::GUI.keypress(Gtk.keyname([event.keyval, event.state]), i_tab)
    end
    widget.vexpand = i_tab.pack_rule[container.children.size]
    container.add(widget)
    pos = where_should_insert_it(
      i_tab,
      pane.each_pages.map do |target_page|
        find_implement_widget_by_gtkwidget(pane.get_tab_label(target_page))
      end,
      i_tab.parent.children
    )
    pane.insert_page(container, tab, pos)
    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_text = 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
    geometry = UserConfig[:windows_geometry]
    geometry and geometry[slug]
  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') do |_widget, event|
      Plugin::GUI.keypress(Gtk.keyname([event.keyval, event.state]), i_pane)
    end
    pane.ssc(:destroy) do
      i_pane.destroy if i_pane.destroyed?
      false
    end
    pane.show_all
  end

  # ウィンドウ内のペイン、タブの現在の順序を設定に保存する
  on_rewind_window_order do |i_window|
    if i_window.slug == :default
      panes_order = i_window.children.select { |i_pane|
                      i_pane.is_a?(Plugin::GUI::Pane)
                    }.map { |i_pane|
        pane = widgetof(i_pane)
        tab_order = pane.each_pages.map { |page|
          find_implement_widget_by_gtkwidget(pane.get_tab_label(page))
        }.select { |i_widget|
          i_widget &&
            !i_widget.temporary_tab? &&
            i_widget.children.any? { |child| !child.is_a?(Plugin::GUI::TabToolbar) }
        }.map(&:slug)
        [i_pane.slug, tab_order] unless tab_order.empty?
      }.compact.to_h
      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&.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

  # timeline_maxを取得するフィルタ
  filter_gui_timeline_get_timeline_max do |i_tl, _|
    [i_tl, widgetof(i_tl).timeline_max]
  end

  # timeline_maxを設定するフィルタ
  filter_gui_timeline_set_timeline_max do |i_tl, n|
    widgetof(i_tl).timeline_max = n
    [i_tl, n]
  end

  # タイムラインのメッセージを順に処理するフィルタ
  filter_gui_timeline_each_messages do |i_tl, y|
    widgetof(i_tl).each do |m|
      y << m
    end
    [i_tl, y]
  end

  on_clipboard_write do |value|
    Gtk::Clipboard.copy(value)
  end

  filter_clipboard_read do |_|
    [Gtk::Clipboard.paste]
  end
end

module Plugin::Gtk3
  class GtkError < Exception # rubocop:disable Lint/InheritException
  end
end