| 1 | #!/usr/bin/python |
|---|
| 2 | # -*- coding: utf-8 -*- |
|---|
| 3 | """blosxom-client v0.05 : A simple blosxom client.""" |
|---|
| 4 | import pygtk |
|---|
| 5 | pygtk.require("2.0") |
|---|
| 6 | import gtk |
|---|
| 7 | import pango |
|---|
| 8 | import ftplib |
|---|
| 9 | import time |
|---|
| 10 | import os |
|---|
| 11 | import re |
|---|
| 12 | |
|---|
| 13 | class BlosxomClient: |
|---|
| 14 | def __init__(self): |
|---|
| 15 | """ |
|---|
| 16 | 初期化処理 |
|---|
| 17 | """ |
|---|
| 18 | self.settings = {} |
|---|
| 19 | self.links = {} |
|---|
| 20 | self.load_settings() |
|---|
| 21 | |
|---|
| 22 | def is_wysiwyg(self): |
|---|
| 23 | if self.settings.has_key("wysiwyg"): |
|---|
| 24 | return self.settings["wysiwyg"] == "true" |
|---|
| 25 | return False |
|---|
| 26 | |
|---|
| 27 | def get_html(self): |
|---|
| 28 | """ |
|---|
| 29 | TextViewからのHTML取得処理 |
|---|
| 30 | TextTagからHTMLタグに変換 |
|---|
| 31 | """ |
|---|
| 32 | iter = self.buffer.get_start_iter() |
|---|
| 33 | if not self.is_wysiwyg(): |
|---|
| 34 | end_iter = self.buffer.get_end_iter() |
|---|
| 35 | return self.buffer.get_text(iter, end_iter) |
|---|
| 36 | html = '' |
|---|
| 37 | bold_prev = False |
|---|
| 38 | italic_prev = False |
|---|
| 39 | link_prev = False |
|---|
| 40 | while not iter.is_end(): |
|---|
| 41 | bold = iter.has_tag(self.bold_tag) |
|---|
| 42 | italic = iter.has_tag(self.italic_tag) |
|---|
| 43 | link = iter.has_tag(self.link_tag) |
|---|
| 44 | image = iter.has_tag(self.image_tag) |
|---|
| 45 | url = '' |
|---|
| 46 | for tag in iter.get_tags(): |
|---|
| 47 | for mark in iter.get_marks(): |
|---|
| 48 | url = self.links.get(mark.get_name(), '') |
|---|
| 49 | |
|---|
| 50 | if link == True and link != link_prev: |
|---|
| 51 | html += "<a href=\"%s\">" % url; |
|---|
| 52 | if bold == True and bold != bold_prev: |
|---|
| 53 | html += "<b>"; |
|---|
| 54 | if italic == True and italic != italic_prev: |
|---|
| 55 | html += "<i>"; |
|---|
| 56 | if image == True: |
|---|
| 57 | html += "<img src=\"%s\">" % url; |
|---|
| 58 | if italic == False and italic != italic_prev: |
|---|
| 59 | html += "</i>"; |
|---|
| 60 | if bold == False and bold != bold_prev: |
|---|
| 61 | html += "</b>"; |
|---|
| 62 | if link == False and link != link_prev: |
|---|
| 63 | html += "</a>"; |
|---|
| 64 | bold_prev = bold |
|---|
| 65 | italic_prev = italic |
|---|
| 66 | link_prev = link |
|---|
| 67 | |
|---|
| 68 | iter_orig = iter.copy() |
|---|
| 69 | tag_found = iter.forward_to_tag_toggle(None) |
|---|
| 70 | if not tag_found: |
|---|
| 71 | iter = self.buffer.get_end_iter() |
|---|
| 72 | text = self.buffer.get_text(iter_orig, iter) |
|---|
| 73 | text = text.replace("&", "&") |
|---|
| 74 | text = text.replace("\"", """) |
|---|
| 75 | text = text.replace("<", ">") |
|---|
| 76 | text = text.replace(">", "<") |
|---|
| 77 | text = text.replace("\n", "<br>\n") |
|---|
| 78 | html += text |
|---|
| 79 | return html |
|---|
| 80 | |
|---|
| 81 | def insert_link(self, is_image = False): |
|---|
| 82 | """ |
|---|
| 83 | リンク挿入処理 |
|---|
| 84 | ダイアログから入力されたリンク(もしくは画像パス)を挿入 |
|---|
| 85 | """ |
|---|
| 86 | dialog = gtk.Dialog("追加", |
|---|
| 87 | buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, |
|---|
| 88 | "追加(_A)", gtk.RESPONSE_ACCEPT)) |
|---|
| 89 | |
|---|
| 90 | dialog.set_default_response(gtk.RESPONSE_ACCEPT) |
|---|
| 91 | dialog.set_has_separator(False) |
|---|
| 92 | dialog.set_resizable(False) |
|---|
| 93 | dialog.vbox.set_spacing(2) |
|---|
| 94 | |
|---|
| 95 | table = gtk.Table(2, 2) |
|---|
| 96 | dialog.vbox.pack_start(table) |
|---|
| 97 | |
|---|
| 98 | label = gtk.Label("タイトル:") |
|---|
| 99 | label.set_alignment(0.0, 0.5) |
|---|
| 100 | table.attach(label, 0, 1, 0, 1, gtk.FILL) |
|---|
| 101 | |
|---|
| 102 | title = gtk.Entry() |
|---|
| 103 | table.attach(title, 1, 2, 0, 1) |
|---|
| 104 | |
|---|
| 105 | label = gtk.Label("URL:") |
|---|
| 106 | label.set_alignment(0.0, 0.5) |
|---|
| 107 | table.attach(label, 0, 1, 1, 2, gtk.FILL) |
|---|
| 108 | |
|---|
| 109 | url = gtk.Entry() |
|---|
| 110 | url.set_activates_default(True) |
|---|
| 111 | table.attach(url, 1, 2, 1, 2) |
|---|
| 112 | |
|---|
| 113 | selection = self.buffer.get_selection_bounds() |
|---|
| 114 | if selection: |
|---|
| 115 | title.set_text(self.buffer.get_text(selection[0], selection[1])) |
|---|
| 116 | |
|---|
| 117 | table.show_all() |
|---|
| 118 | |
|---|
| 119 | if selection: |
|---|
| 120 | url.grab_focus() |
|---|
| 121 | else: |
|---|
| 122 | title.grab_focus() |
|---|
| 123 | |
|---|
| 124 | response = dialog.run() |
|---|
| 125 | |
|---|
| 126 | link_title = None |
|---|
| 127 | link_url = None |
|---|
| 128 | if response == gtk.RESPONSE_ACCEPT: |
|---|
| 129 | link_title = title.get_text() |
|---|
| 130 | link_url = url.get_text() |
|---|
| 131 | ins_mark = self.buffer.get_insert() |
|---|
| 132 | if not is_image: |
|---|
| 133 | tag = self.link_tag |
|---|
| 134 | else: |
|---|
| 135 | tag = self.image_tag |
|---|
| 136 | mark_iter = self.buffer.get_iter_at_mark(ins_mark) |
|---|
| 137 | if self.is_wysiwyg(): |
|---|
| 138 | if not selection: |
|---|
| 139 | self.buffer.insert_with_tags(mark_iter, link_title, tag) |
|---|
| 140 | else: |
|---|
| 141 | self.buffer.apply_tag(tag, selection[0], selection[1]) |
|---|
| 142 | else: |
|---|
| 143 | if is_image: |
|---|
| 144 | link_title = "<img src=\"%s\" title=\"%s\">" % (link_url, link_title) |
|---|
| 145 | else: |
|---|
| 146 | link_title = "<a href=\"%s\" target=\"_blank\">" % link_url |
|---|
| 147 | if not selection: |
|---|
| 148 | self.buffer.insert(mark_iter, link_title) |
|---|
| 149 | else: |
|---|
| 150 | self.buffer.insert(selection[0], link_title) |
|---|
| 151 | self.buffer.insert(selection[1], tag.closing_tag) |
|---|
| 152 | mark = self.buffer.create_mark("%03d" % len(self.links), mark_iter) |
|---|
| 153 | self.links[mark.get_name()] = link_url |
|---|
| 154 | |
|---|
| 155 | dialog.hide() |
|---|
| 156 | |
|---|
| 157 | def on_menu_bold(self, widget): |
|---|
| 158 | """ |
|---|
| 159 | メニュー「太字」のハンドラ |
|---|
| 160 | 太字タグを適応する |
|---|
| 161 | """ |
|---|
| 162 | selection = self.buffer.get_selection_bounds() |
|---|
| 163 | if not selection: |
|---|
| 164 | return |
|---|
| 165 | if self.is_wysiwyg(): |
|---|
| 166 | if selection[0].has_tag(self.bold_tag): |
|---|
| 167 | self.buffer.remove_tag(self.bold_tag, selection[0], selection[1]) |
|---|
| 168 | else: |
|---|
| 169 | self.buffer.apply_tag(self.bold_tag, selection[0], selection[1]) |
|---|
| 170 | else: |
|---|
| 171 | text = self.bold_tag.opening_tag + self.buffer.get_text(selection[0], selection[1]) + self.bold_tag.closing_tag |
|---|
| 172 | self.buffer.delete(selection[0], selection[1]) |
|---|
| 173 | self.buffer.insert_at_cursor(text) |
|---|
| 174 | |
|---|
| 175 | def on_menu_italic(self, widget): |
|---|
| 176 | """ |
|---|
| 177 | メニュー「斜体」のハンドラ |
|---|
| 178 | 斜体タグを適応する |
|---|
| 179 | """ |
|---|
| 180 | selection = self.buffer.get_selection_bounds() |
|---|
| 181 | if not selection: |
|---|
| 182 | return |
|---|
| 183 | if self.is_wysiwyg(): |
|---|
| 184 | if selection[0].has_tag(self.italic_tag): |
|---|
| 185 | self.buffer.remove_tag(self.italic_tag, selection[0], selection[1]) |
|---|
| 186 | else: |
|---|
| 187 | self.buffer.apply_tag(self.italic_tag, selection[0], selection[1]) |
|---|
| 188 | else: |
|---|
| 189 | text = self.italic_tag.opening_tag + self.buffer.get_text(selection[0], selection[1]) + self.italic_tag.closing_tag |
|---|
| 190 | self.buffer.delete(selection[0], selection[1]) |
|---|
| 191 | self.buffer.insert_at_cursor(text) |
|---|
| 192 | |
|---|
| 193 | def on_menu_link(self, widget): |
|---|
| 194 | """ |
|---|
| 195 | メニュー「リンク」のハンドラ |
|---|
| 196 | リンク挿入処理を呼び出す |
|---|
| 197 | """ |
|---|
| 198 | self.insert_link(False) |
|---|
| 199 | |
|---|
| 200 | def on_menu_image(self, widget): |
|---|
| 201 | """ |
|---|
| 202 | メニュー「画像」のハンドラ |
|---|
| 203 | リンク挿入処理(画像)を呼び出す |
|---|
| 204 | """ |
|---|
| 205 | self.insert_link(True) |
|---|
| 206 | |
|---|
| 207 | def on_menu_help(self, widget): |
|---|
| 208 | """ |
|---|
| 209 | メニュー「ヘルプ」のハンドラ |
|---|
| 210 | ヘルプダイアログを表示する |
|---|
| 211 | """ |
|---|
| 212 | dialog = gtk.MessageDialog(self.window, |
|---|
| 213 | gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, |
|---|
| 214 | gtk.BUTTONS_CLOSE, |
|---|
| 215 | "blosxom-client v0.05 : A simple blosxom client.\n\tby mattn") |
|---|
| 216 | dialog.run() |
|---|
| 217 | dialog.destroy() |
|---|
| 218 | |
|---|
| 219 | def on_button_clicked(self, button): |
|---|
| 220 | """ |
|---|
| 221 | ボタン「公開」のハンドラ |
|---|
| 222 | FTPにアクセスしてフォルダを生成し、記事をポストする |
|---|
| 223 | """ |
|---|
| 224 | buffer = self.body.get_buffer() |
|---|
| 225 | start_iter = buffer.get_start_iter() |
|---|
| 226 | end_iter = buffer.get_end_iter() |
|---|
| 227 | txt = buffer.get_text(start_iter, end_iter, 0) |
|---|
| 228 | filename = "%i.%s" % (int(time.time()), "txt") |
|---|
| 229 | message = "サーバに接続中..." |
|---|
| 230 | try: |
|---|
| 231 | ftp = ftplib.FTP(self.settings["server"]) |
|---|
| 232 | message = "サーバにログイン中..." |
|---|
| 233 | ftp.login(self.settings["userid"], self.settings["password"]) |
|---|
| 234 | message = "公開ディレクトリに移動中..." |
|---|
| 235 | ftp.cwd(self.settings["publish_root"]) |
|---|
| 236 | categories = self.category.get_text().split("/") |
|---|
| 237 | for c in categories: |
|---|
| 238 | if not c: |
|---|
| 239 | continue |
|---|
| 240 | try: |
|---|
| 241 | ftp.cwd(c) |
|---|
| 242 | except: |
|---|
| 243 | message = "公開ディレクトリにディレクトリを作成中..." |
|---|
| 244 | ftp.mkd(c); |
|---|
| 245 | ftp.cwd(c) |
|---|
| 246 | message = "ローカルにテンポラリを作成中..." |
|---|
| 247 | file = open(filename, "w") |
|---|
| 248 | file.write(self.title.get_text()) |
|---|
| 249 | file.write("\n") |
|---|
| 250 | file.write(txt) |
|---|
| 251 | file.close() |
|---|
| 252 | message = "ローカルのテンポラリを読み込み中..." |
|---|
| 253 | file = open(filename, "rb") |
|---|
| 254 | message = "エントリファイルのアップロード中..." |
|---|
| 255 | ftp.storbinary("STOR %s" % filename, file) |
|---|
| 256 | ftp.quit() |
|---|
| 257 | file.close() |
|---|
| 258 | message = "ローカルのテンポラリを削除中..." |
|---|
| 259 | os.remove(filename) |
|---|
| 260 | except Exception, e: |
|---|
| 261 | try: |
|---|
| 262 | ftp.close() |
|---|
| 263 | except: |
|---|
| 264 | pass |
|---|
| 265 | try: |
|---|
| 266 | file.close() |
|---|
| 267 | except: |
|---|
| 268 | pass |
|---|
| 269 | try: |
|---|
| 270 | os.remove(filename) |
|---|
| 271 | except: |
|---|
| 272 | pass |
|---|
| 273 | dialog = gtk.MessageDialog(self.window, |
|---|
| 274 | gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, |
|---|
| 275 | gtk.BUTTONS_CLOSE, |
|---|
| 276 | message + "\n" + str(e)) |
|---|
| 277 | dialog.run() |
|---|
| 278 | dialog.destroy() |
|---|
| 279 | |
|---|
| 280 | def load_settings(self): |
|---|
| 281 | """ |
|---|
| 282 | 設定ファイルを読み込む |
|---|
| 283 | """ |
|---|
| 284 | fname = os.path.expanduser("~/.blosxom-client") |
|---|
| 285 | try: |
|---|
| 286 | file = open(fname, "r") |
|---|
| 287 | reg = re.compile("^([^=]+)=(\S*)$") |
|---|
| 288 | while 1: |
|---|
| 289 | line = file.readline() |
|---|
| 290 | if not line: |
|---|
| 291 | break |
|---|
| 292 | m = reg.search(line) |
|---|
| 293 | self.settings[m.group(1)] = m.group(2) |
|---|
| 294 | file.close |
|---|
| 295 | except: |
|---|
| 296 | pass |
|---|
| 297 | |
|---|
| 298 | def main(self): |
|---|
| 299 | """ |
|---|
| 300 | メイン処理 |
|---|
| 301 | """ |
|---|
| 302 | self.window = gtk.Window() |
|---|
| 303 | self.window.set_title("Blosxom Client") |
|---|
| 304 | self.window.connect("delete-event", gtk.main_quit) |
|---|
| 305 | |
|---|
| 306 | accel_group = gtk.AccelGroup() |
|---|
| 307 | self.window.add_accel_group(accel_group) |
|---|
| 308 | |
|---|
| 309 | vbox = gtk.VBox() |
|---|
| 310 | vbox.set_spacing(2) |
|---|
| 311 | self.window.add(vbox) |
|---|
| 312 | |
|---|
| 313 | menubar = gtk.MenuBar() |
|---|
| 314 | vbox.pack_start(menubar, expand=False) |
|---|
| 315 | |
|---|
| 316 | # ファイルメニュー |
|---|
| 317 | menuitem = gtk.MenuItem('ファイル(_F)') |
|---|
| 318 | |
|---|
| 319 | menu = gtk.Menu() |
|---|
| 320 | submenuitem = gtk.MenuItem('終了(_X)') |
|---|
| 321 | submenuitem.connect("activate", gtk.main_quit, "file.quit") |
|---|
| 322 | submenuitem.add_accelerator('activate', accel_group, gtk.gdk.keyval_from_name("q"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) |
|---|
| 323 | menu.add(submenuitem) |
|---|
| 324 | menuitem.set_submenu(menu) |
|---|
| 325 | |
|---|
| 326 | menubar.add(menuitem) |
|---|
| 327 | |
|---|
| 328 | # 編集メニュー |
|---|
| 329 | menuitem = gtk.MenuItem('編集(_E)') |
|---|
| 330 | |
|---|
| 331 | menu = gtk.Menu() |
|---|
| 332 | submenuitem = gtk.MenuItem('太字(_B)') |
|---|
| 333 | submenuitem.connect("activate", self.on_menu_bold) |
|---|
| 334 | submenuitem.add_accelerator('activate', accel_group, gtk.gdk.keyval_from_name("b"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) |
|---|
| 335 | menu.add(submenuitem) |
|---|
| 336 | submenuitem = gtk.MenuItem('斜体(_I)') |
|---|
| 337 | submenuitem.connect("activate", self.on_menu_italic) |
|---|
| 338 | submenuitem.add_accelerator('activate', accel_group, gtk.gdk.keyval_from_name("i"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) |
|---|
| 339 | menu.add(submenuitem) |
|---|
| 340 | menuitem.set_submenu(menu) |
|---|
| 341 | submenuitem = gtk.MenuItem('リンク(_L)') |
|---|
| 342 | submenuitem.connect("activate", self.on_menu_link) |
|---|
| 343 | submenuitem.add_accelerator('activate', accel_group, gtk.gdk.keyval_from_name("r"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) |
|---|
| 344 | menu.add(submenuitem) |
|---|
| 345 | menuitem.set_submenu(menu) |
|---|
| 346 | submenuitem = gtk.MenuItem('画像(_L)') |
|---|
| 347 | submenuitem.connect("activate", self.on_menu_image) |
|---|
| 348 | submenuitem.add_accelerator('activate', accel_group, gtk.gdk.keyval_from_name("t"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) |
|---|
| 349 | menu.add(submenuitem) |
|---|
| 350 | menuitem.set_submenu(menu) |
|---|
| 351 | |
|---|
| 352 | menubar.add(menuitem) |
|---|
| 353 | |
|---|
| 354 | # ヘルプメニュー |
|---|
| 355 | menuitem = gtk.MenuItem('ヘルプ(_H)') |
|---|
| 356 | menuitem.set_right_justified(True) |
|---|
| 357 | |
|---|
| 358 | menu = gtk.Menu() |
|---|
| 359 | submenuitem = gtk.MenuItem('blosxom-clientについて(_A)') |
|---|
| 360 | submenuitem.connect("activate", self.on_menu_help) |
|---|
| 361 | menu.add(submenuitem) |
|---|
| 362 | menuitem.set_submenu(menu) |
|---|
| 363 | |
|---|
| 364 | menubar.add(menuitem) |
|---|
| 365 | |
|---|
| 366 | table = gtk.Table(2, 3, False) |
|---|
| 367 | table.set_border_width(10) |
|---|
| 368 | table.show() |
|---|
| 369 | vbox.add(table) |
|---|
| 370 | |
|---|
| 371 | label = gtk.Label("タイトル: ") |
|---|
| 372 | table.attach(label, |
|---|
| 373 | 0, 1, 0, 1, |
|---|
| 374 | gtk.FILL, gtk.EXPAND | gtk.FILL, |
|---|
| 375 | 0, 0) |
|---|
| 376 | self.title = gtk.Entry() |
|---|
| 377 | table.attach(self.title, |
|---|
| 378 | 1, 2, 0, 1, |
|---|
| 379 | gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, |
|---|
| 380 | 0, 0) |
|---|
| 381 | |
|---|
| 382 | label = gtk.Label("カテゴリ: ") |
|---|
| 383 | table.attach(label, |
|---|
| 384 | 0, 1, 1, 2, |
|---|
| 385 | gtk.FILL, gtk.EXPAND | gtk.FILL, |
|---|
| 386 | 0, 0) |
|---|
| 387 | self.category = gtk.Entry() |
|---|
| 388 | table.attach(self.category, |
|---|
| 389 | 1, 2, 1, 2, |
|---|
| 390 | gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, |
|---|
| 391 | 0, 0) |
|---|
| 392 | |
|---|
| 393 | sw = gtk.ScrolledWindow() |
|---|
| 394 | sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
|---|
| 395 | table.attach(sw, |
|---|
| 396 | 0, 2, 2, 3, |
|---|
| 397 | gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, |
|---|
| 398 | 0, 0) |
|---|
| 399 | |
|---|
| 400 | self.body = gtk.TextView() |
|---|
| 401 | self.body.set_size_request(500, 200) |
|---|
| 402 | self.body.queue_resize() |
|---|
| 403 | font = pango.FontDescription('Sans 12') |
|---|
| 404 | self.body.modify_font(font) |
|---|
| 405 | sw.add(self.body) |
|---|
| 406 | |
|---|
| 407 | self.buffer = self.body.get_buffer() |
|---|
| 408 | self.bold_tag = self.buffer.create_tag("b") |
|---|
| 409 | self.bold_tag.opening_tag = '<b>' |
|---|
| 410 | self.bold_tag.closing_tag = '</b>' |
|---|
| 411 | self.bold_tag.set_property("weight", pango.WEIGHT_BOLD) |
|---|
| 412 | self.italic_tag = self.buffer.create_tag("i") |
|---|
| 413 | self.italic_tag.opening_tag = '<i>' |
|---|
| 414 | self.italic_tag.closing_tag = '</i>' |
|---|
| 415 | self.italic_tag.set_property("style", pango.STYLE_ITALIC) |
|---|
| 416 | self.link_tag = self.buffer.create_tag("a") |
|---|
| 417 | self.link_tag.opening_tag = '<a>' |
|---|
| 418 | self.link_tag.closing_tag = '</a>' |
|---|
| 419 | self.link_tag.set_property("underline", pango.UNDERLINE_SINGLE) |
|---|
| 420 | self.link_tag.set_property("foreground", "#0000FF") |
|---|
| 421 | self.image_tag = self.buffer.create_tag("img") |
|---|
| 422 | self.image_tag.opening_tag = '<img>' |
|---|
| 423 | self.image_tag.closing_tag = None |
|---|
| 424 | |
|---|
| 425 | button = gtk.Button("公開") |
|---|
| 426 | button.connect("clicked", self.on_button_clicked) |
|---|
| 427 | vbox.add(button) |
|---|
| 428 | |
|---|
| 429 | self.window.show_all() |
|---|
| 430 | gtk.main() |
|---|
| 431 | |
|---|
| 432 | if __name__=='__main__': |
|---|
| 433 | try: |
|---|
| 434 | blosxom_client = BlosxomClient() |
|---|
| 435 | blosxom_client.main() |
|---|
| 436 | except Exception, e: |
|---|
| 437 | print(e) |
|---|
| 438 | pass |
|---|