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
|
# -*- coding: utf-8 -*-
require 'gtk3'
require 'cairo'
=begin rdoc
ナウい引用っぽく _Cairo::MiraclePainter_ のSubPartsとして別の _Message_ を表示するSubPartsを作るときの基底クラス。
= 使い方
このクラスを継承しましょう。
そして、以下のドキュメントを参考に、必要なメソッドをオーバライドします。
=end
class Gdk::SubPartsMessageBase < Gdk::SubParts
extend Memoist
DEFAULT_ICON_SIZE = 32
WHITE = [1.0, 1.0, 1.0].freeze
# SubPartsに表示する _Message_ 。
# 複数表示可能なので、それらを上に表示されるものから順番に返す。
# サブクラスで処理を実装すること。
# このメソッドはサブパーツの描画中に何回も呼ばれるので、キャッシュなどで高速化に努めること。
# ==== Return
# _Array_ :: このSubParts上に表示する _Message_
def messages
[] end
# :nodoc:
memoize def score(message)
Plugin[:gtk3].score_of(message)
end
# ヘッダの左の、Screen name、名前が表示されている場所に表示するテキスト。
# オーバライドしなければ、 _message_ の投稿者のscreen nameと名前が表示される。
# nilを返した場合、ヘッダは表示されない。この場合、ヘッダ右も表示されない。
# ==== Args
# [message] Message 表示するMessage
# ==== Return
# 次の3つの値またはnil(ヘッダ左を使用しない場合)
# [String] 表示する文字列
# [Pango::FontDescription] フォント情報
# [Pango::Attribute] マークアップ情報
def header_left_content(message)
user = message.user
if user.respond_to?(:idname)
attr_list, text = Pango.parse_markup("<b>#{Pango.escape(user.idname)}</b> #{Pango.escape(user.name || '')}")
else
attr_list, text = Pango.parse_markup(Pango.escape(user.name || ''))
end
return text, header_left_font(message), attr_list
end
# ヘッダ左に使用するフォントを返す
# ==== Args
# [message] Message 表示するMessage
# ==== Return
# [Pango::FontDescription] フォント情報
def header_left_font(message)
default_font end
# ヘッダの右の、タイムスタンプが表示されているところに表示するテキスト。
# オーバーライドしなければ、 _message_ のタイムスタンプが表示される。
# 表示される時に、 Pango.escape を通るので、この戻り値がエスケープを考慮する必要はないが、装飾を指定することはできない。
# ==== Args
# [message] Message 対象のMessage
# ==== Return
# [String] 表示する文字列。
def header_right_text(message)
now = Time.now
if message[:created].year == now.year && message[:created].month == now.month && message[:created].day == now.day
message[:created].strftime('%H:%M:%S'.freeze)
else
message[:created].strftime('%Y/%m/%d %H:%M:%S'.freeze) end end
# Gdk::SubPartsMessageBase#header_right_text にマークアップを足した文字列を返す。
# 通常は header_right_text をオーバライドするようにし、
# テキストがエスケープされるのが問題になる場合は、こちらをオーバライドする。
# nilを返した場合、ヘッダ右は表示されない。この場合、ヘッダ左のみが表示される。
# ヘッダ自体を消す方法については、 Gdk::SubPartsMessageBase#header_left_text を参照
# ==== Args
# [message] Message 対象のMessage
# ==== Return
# 次の3つの値またはnil(ヘッダ右を使用しない場合)
# [String] 表示する文字列
# [Pango::FontDescription] フォント情報
# [Pango::Attribute] マークアップ情報
def header_right_content(message)
attr_list, text = Pango.parse_markup("<span foreground=\"#999999\">#{Pango.escape(header_right_text(message))}</span>")
return text, header_right_font(message), attr_list end
# ヘッダ右に使用するフォントを返す
# ==== Args
# [message] Message 表示するMessage
# ==== Return
# [Pango::FontDescription] フォント情報
def header_right_font(message)
default_font end
# SubParts内の _Message_ の左上に表示するバッジ。
# サブクラスで処理を実装すること。
# ==== Args
# [message] Message
# ==== Return
# 以下の値のいずれか一つ
# GdkPixbuf::Pixbuf :: _message_ の左上に表示するバッジ画像
# nil :: バッジを表示しない
def badge(message)
nil end
# 表示している _Message_ がクリックされた時、その _Message_ を引数に呼ばれる。
# サブクラスで処理を実装すること。
# ==== Args
# [e] Gdk::EventButton クリックイベント
# [message] Message クリックされた _Message_
def on_click(e, message)
end
# SubParts内の _Diva::Model_ の背景色を返す
# ==== Args
# [model] Diva::Model
# ==== Return
# Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
def background_color(model)
color = Plugin.filtering(:message_bg_color, model, nil).last
rgb(color || WHITE)
end
# SubParts内の _Message_ の枠の色を返す
# ==== Args
# [message] Message
# ==== Return
# Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
def edge_color(message)
[0.5]*3 end
# GTK2のGtk::ColorとGTK3のGdk::RGBAをRGBの_Float_値に変換する
def rgb(color)
r, g, b = color
return [r, g, b] if r.is_a? Float
[r.fdiv(65536), g.fdiv(65536), b.fdiv(65536)].freeze
end
# アイコンのサイズを返す。
# ==== Return
# [Gdk::Rectangle] サイズ(px)。xとyは無視され、widthとheightのみが利用される
# [nil] アイコンを表示しない
def icon_size
Gdk::Rectangle.new(0, 0, Gdk.scale(DEFAULT_ICON_SIZE), Gdk.scale(DEFAULT_ICON_SIZE))
end
# _message_ の本文のテキスト色を返す
# ==== Args
# [message] Message
# ==== Return
# Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
def main_text_color(message)
([0,0,0]).map{ |c| c.to_f / 65536 } end
# 本文使用するフォントを返す
# ==== Args
# [message] Message 表示するMessage
# ==== Return
# [Pango::FontDescription] フォント情報
def main_text_font(message)
default_font end
# 本文の最大表示行数を返す。
# この行数を超えた行は表示されない
# ==== Args
# [message] Message 表示するMessage
# ==== Return
# Fixnum 行数
def text_max_line_count(message)
3 end
# :nodoc:
memoize def default_font
helper.font_description(UserConfig[:reply_text_font])
end
def margin
Gdk.scale(@margin)
end
def edge
Gdk.scale(@edge)
end
# Fixnum 枠線の太さ(px)
def border_weight
Gdk.scale(@border_weight)
end
# Fixnum バッジの半径(px)
def badge_radius
Gdk.scale(@badge_radius)
end
# :nodoc:
def initialize(*args)
super
@margin, @edge, @border_weight, @badge_radius = 2, 8, 1, 6 end
# :nodoc:
def render_messages
if not helper.destroyed?
helper.queue_allocate
helper.ssc(:clicked) { |_, ev|
x, y = ev.x, ev.y
ofsty = helper.mainpart_height
helper.subparts.each { |part|
break if part == self
ofsty += part.height }
if ofsty <= y and (ofsty + height) >= y
my = 0
messages.each { |m|
my += message_height(m)
if y <= ofsty + my
on_click(ev, m)
break end } end } end end
# :nodoc:
def render(context)
if messages and not messages.empty?
messages.inject(0) { |base_y, message|
render_single_message(message, context, base_y) } end end
# :nodoc:
def height
if helper.destroyed?
0
else
messages&.sum(&method(:message_height)) || 0
end
end
private
def icon_width
size = icon_size
if size
size.width
else
0 end end
def icon_height
size = icon_size
if size
size.height
else
0 end end
def render_single_message(message, context, base_y)
render_outline(message, context, base_y)
_header_width, header_height = render_header(message, context, base_y)
context.save do
context.translate(margin + edge, margin + edge + base_y)
render_icon(message, context)
context.save do
context.translate(icon_width + margin*2, header_height || 0)
context.set_source_rgb(*main_text_color(message))
pango_layout = main_message(message)
if pango_layout.line_count <= text_max_line_count(message)
context.show_pango_layout(pango_layout)
else
line_height = pango_layout.pixel_size[1] / pango_layout.line_count + pango_layout.spacing / Pango::SCALE
context.translate(0, line_height*0.75)
(0...text_max_line_count(message)).map(&pango_layout.method(:get_line)).each do |line|
context.show_pango_layout_line(line)
context.translate(0, line_height) end end end
render_badge(message, context) end
base_y + message_height(message) end
def message_height(message)
header_height = [0, *[header_left(message), header_right(message)].compact.map{|h|
h.pixel_size[1]}].max
[icon_height, (header_height + main_message_height(message))].max + (margin + edge) * 2 end
def main_message_height(message)
pango_layout = main_message(message)
result = pango_layout.pixel_size[1]
if pango_layout.line_count <= text_max_line_count(message)
result
else
(result / pango_layout.line_count) * text_max_line_count(message) + pango_layout.spacing/Pango::SCALE * 2 end end
def header_left_context(message)
PangoCairo::FontMap.default.create_context.tap do |context|
context.set_font_description(header_left_font(message))
end
end
# ヘッダ(左)のための Pango::Layout のインスタンスを返す
def header_left(message, context=nil)
text, _font, attr_list = header_left_content(message)
if text
layout = Pango::Layout.new(header_left_context(message))
layout.attributes = attr_list if attr_list
layout.text = text
layout
end
end
def header_right_context(message)
PangoCairo::FontMap.default.create_context.tap do |context|
context.set_font_description(header_right_font(message))
end
end
# ヘッダ(右)のための Pango::Layout のインスタンスを返す
def header_right(message, context=nil)
text, _font, attr_list = header_right_content(message)
if text
layout = Pango::Layout.new(header_right_context(message))
layout.attributes = attr_list if attr_list
layout.text = text
layout.alignment = Pango::Alignment::RIGHT
layout
end
end
def render_header(message, context, base_y)
context.save do
context.translate(icon_width + margin*2 + edge, margin + edge + base_y)
context.set_source_rgb(0,0,0)
hl_layout = header_left(message, context)
if hl_layout
context.show_pango_layout(hl_layout)
hl_w, hl_h = hl_layout.pixel_size
hr_layout = render_header_right(message, context, base_y, hl_w)
if hr_layout
[[hl_w, hr_layout.pixel_size[0]].max,
[hl_h, hr_layout.pixel_size[1]].max]
else
[hl_w, hl_h] end end end end
def render_header_right(message, context, base_y, header_left_width)
header_w = width - icon_width - margin*3 - edge*2
hr_layout = header_right(message, context)
if hr_layout
context.save do
context.translate(header_w - hr_layout.pixel_size[0], 0)
if header_left_width > header_w - hr_layout.pixel_size[0] - 20
r, g, b = background_color(message)
grad = Cairo::LinearPattern.new(-20, base_y, hr_layout.pixel_size[0] + 20, base_y)
grad.add_color_stop_rgba(0.0, r, g, b, 0.0)
grad.add_color_stop_rgba(20.0 / (hr_layout.pixel_size[0] + 20), r, g, b, 1.0)
grad.add_color_stop_rgba(1.0, r, g, b, 1.0)
context.rectangle(-20, 0, hr_layout.pixel_size[0] + 20, hr_layout.pixel_size[1])
context.set_source(grad)
context.fill() end
context.show_pango_layout(hr_layout)
hr_layout end end end
def main_message_context
PangoCairo::FontMap.default.create_context.tap do |context|
context.set_font_description(default_font)
end
end
# _message_ に対する _Pango::Layout_ を得る。
# @params message [Diva::Model] 対象Message Model
# @params _context [nil] 互換性のため
def main_message(message, _context=nil)
Pango::Layout.new(main_message_context).tap do |layout|
layout.width = (width - icon_width - margin*3 - edge*2) * Pango::SCALE
layout.attributes = description_attr_list(
message,
emoji_height: layout.context.font_description.forecast_font_size
)
layout.wrap = Pango::WrapMode::CHAR
layout.text = plain_description(message)
layout.context.set_shape_renderer do |c, shape, _|
photo = shape.data
if photo
draw_area = layout.index_to_pos(shape.start_index)
width = draw_area.width / Pango::SCALE
height = draw_area.height / Pango::SCALE
pixbuf = photo.load_pixbuf(width: width, height: height) do
helper.queue_resize
end
x = draw_area.x / Pango::SCALE
y = draw_area.y / Pango::SCALE
c.translate(x, y)
c.set_source_pixbuf(pixbuf)
c.rectangle(0, 0, width, height)
c.fill
end
end
end
end
def render_outline(message, context, base_y)
render_outline_floating(message, context, base_y) end
# エッジの描画。
# 影にblurを入れて、浮いているような感じに
def render_outline_floating(message, context, base_y, radius: 4, blur: 4)
x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
context.save {
context.pseudo_blur(blur) {
context.fill {
context.set_source_rgb(*edge_color(message))
context.rounded_rectangle(x,y,w,h, radius) } }
context.fill {
context.set_source_rgb(*background_color(message))
context.rounded_rectangle(x,y,w,h, radius) } } end
# エッジの描画。
# 細い線を入れる
def render_outline_solid(message, context, base_y, radius: 4)
context.save {
x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
#context.fill {
context.rounded_rectangle(x,y,w,h, radius)
context.set_source_rgb(*background_color(message))
context.fill_preserve
context.set_line_width(border_weight)
context.set_source_rgb(*edge_color(message))
context.stroke } end
# エッジの描画。
# 枠線なし
def render_outline_flat(message, context, base_y, radius: 4)
context.save {
x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
context.fill {
context.set_source_rgb(*background_color(message))
context.rounded_rectangle(x,y,w,h, radius) } } end
def render_badge(message, context)
render_badge_floating(message, context) end
# バッジの描画。
# 影にblurを入れて、浮いているような感じに
def render_badge_floating(message, context)
badge_pixbuf = badge(message)
if badge_pixbuf
context.save {
context.pseudo_blur(4) {
context.fill {
context.set_source_rgb(*edge_color(message))
context.circle(0, 0, badge_radius) } }
context.fill {
context.set_source_rgb(*background_color(message))
context.circle(0, 0, badge_radius) } }
context.translate(-badge_radius, -badge_radius)
context.set_source_pixbuf(badge_pixbuf)
context.paint end end
# バッジの描画。
# 細い線を入れる
def render_badge_solid(message, context)
badge_pixbuf = badge(message)
if badge_pixbuf
context.save {
context.circle(0, 0, badge_radius)
context.set_source_rgb(*background_color(message))
context.fill_preserve
context.set_source_rgb(*edge_color(message))
context.set_line_width(border_weight)
context.stroke }
context.translate(-badge_radius, -badge_radius)
context.set_source_pixbuf(badge_pixbuf)
context.paint end end
# バッジの描画。
# 枠線を入れない
def render_badge_flat(message, context)
badge_pixbuf = badge(message)
if badge_pixbuf
context.fill {
context.set_source_rgb(*background_color(message))
context.circle(0, 0, badge_radius) }
context.translate(-badge_radius, -badge_radius)
context.set_source_pixbuf(badge_pixbuf)
context.paint end end
def render_icon(message, context)
if icon_size
context.set_source_pixbuf(main_icon(message))
context.paint end end
def main_icon(message)
message.user.icon.load_pixbuf(width: icon_size.width, height: icon_size.width){ helper.queue_draw }
end
# 表示する際に本文に適用すべき装飾オブジェクトを作成する
# ==== Return
# Pango::AttrList 本文に適用する装飾
def description_attr_list(message, attr_list: Pango::AttrList.new, emoji_height: 24)
score(message).inject(0){|start_index, note|
end_index = start_index + note.description.bytesize
if UserConfig[:miraclepainter_expand_custom_emoji] && note.respond_to?(:inline_photo)
end_index += -note.description.bytesize + 1
rect = Pango::Rectangle.new(0, 0, emoji_height * Pango::SCALE, emoji_height * Pango::SCALE)
shape = Pango::AttrShape.new(rect, rect, note.inline_photo)
shape.start_index = start_index
shape.end_index = end_index
attr_list.insert(shape)
end
end_index
}
attr_list
end
# Entityを適用したあとのプレーンテキストを返す。
# Pangoの都合上、絵文字は1文字で表現する
def plain_description(message)
_plain_description[[message, UserConfig[:miraclepainter_expand_custom_emoji]]]
end
private memoize def _plain_description
Hash.new do |h, k|
message, expand_emoji = k
h[k] = score(message).map{|note|
if expand_emoji && note.respond_to?(:inline_photo)
'.'
else
note.description
end
}.to_a.join
end
end
end
|