1 # -*- coding: utf-8 -*- 2 # 3 # Copyright (c) 2009 Leo Zheng <zym361 (at] gmail.com>, Kov Chai <tchaikov (at] gmail.com> 4 # * 5 # The contents of this file are subject to the terms of either the GNU Lesser 6 # General Public License Version 2.1 only ("LGPL") or the Common Development and 7 # Distribution License ("CDDL")(collectively, the "License"). You may not use this 8 # file except in compliance with the License. You can obtain a copy of the CDDL at 9 # http://www.opensource.org/licenses/cddl1.php and a copy of the LGPLv2.1 at 10 # http://www.opensource.org/licenses/lgpl-license.php. See the License for the 11 # specific language governing permissions and limitations under the License. When 12 # distributing the software, include this License Header Notice in each file and 13 # include the full text of the License in the License file as well as the 14 # following notice: 15 # 16 # NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE 17 # (CDDL) 18 # For Covered Software in this distribution, this License shall be governed by the 19 # laws of the State of California (excluding conflict-of-law provisions). 20 # Any litigation relating to this License shall be subject to the jurisdiction of 21 # the Federal Courts of the Northern District of California and the state courts 22 # of the State of California, with venue lying in Santa Clara County, California. 23 # 24 # Contributor(s): 25 # 26 # If you wish your version of this file to be governed by only the CDDL or only 27 # the LGPL Version 2.1, indicate your decision by adding "[Contributor]" elects to 28 # include this software in this distribution under the [CDDL or LGPL Version 2.1] 29 # license." If you don't indicate a single choice of license, a recipient has the 30 # option to distribute your version of this file under either the CDDL or the LGPL 31 # Version 2.1, or to extend the choice of license to its licensees as provided 32 # above. However, if you add LGPL Version 2.1 code and therefore, elected the LGPL 33 # Version 2 license, then the option applies only if the new code is made subject 34 # to such option by the copyright holder. 35 # 36 37 import sys 38 import os 39 from os import path 40 import gtk 41 import gtk.glade as glade 42 import ibus 43 import gettext 44 import locale 45 46 GETTEXT_PACKAGE="sunpinyin" 47 _ = lambda msg: gettext.gettext(msg) 48 49 GLADE_FILE = path.join(path.dirname(__file__), "setup.glade") 50 SEPARATOR = "/" 51 52 class Logger: 53 @staticmethod 54 def pr(message): 55 print >> sys.stderr, message 56 57 class Option(object): 58 """Option serves as an interface of ibus.config 59 60 it is used to synchronize the configuration with setting on user interface 61 """ 62 config = ibus.Bus().get_config() 63 64 def __init__(self, name, default): 65 self.name = name 66 self.default = default 67 68 def read(self): 69 section, key = self.__get_config_name() 70 return self.config.get_value(section, key, self.default) 71 72 def write(self, v): 73 section, key = self.__get_config_name() 74 return self.config.set_value(section, key, v) 75 76 77 def __get_config_name(self): 78 keys = self.name.rsplit(SEPARATOR ,1) 79 if len(keys) == 2: 80 return SEPARATOR.join(("engine/SunPinyin", keys[0])), keys[1] 81 else: 82 assert len(keys) == 1 83 return "engine/SunPinyin", keys[0] 84 85 class TrivalOption(Option): 86 """option represented using a simple gtk widget 87 """ 88 def __init__(self, name, default, owner): 89 super(TrivalOption, self).__init__(name, default) 90 self.xml = owner 91 self.widget = owner.get_widget(name) 92 assert self.widget is not None, "%s not found in glade" % name 93 94 def init_ui(self): 95 self.init() 96 self.read_config() 97 98 def init(self): 99 pass 100 101 def read_config(self): 102 """update user inferface with ibus.config 103 """ 104 self.v = self.read() 105 self.widget.set_active(self.v) 106 107 def write_config(self): 108 v = self.save_ui_setting() 109 self.write(v) 110 111 def save_ui_setting(self): 112 """save user interface settings into self.v 113 """ 114 self.v = self.widget.get_active() 115 return self.v 116 117 def is_changed(self): 118 return self.v != self.widget.get_active() 119 120 class CheckBoxOption(TrivalOption): 121 def __init__(self, name, default, owner): 122 super(CheckBoxOption, self).__init__(name, default, owner) 123 124 class ComboBoxOption(TrivalOption): 125 def __init__(self, name, default, options, owner): 126 try: 127 default = int(default) 128 except ValueError: 129 default = options.index(default) 130 super(ComboBoxOption, self).__init__(name, default, owner) 131 self.options = options 132 133 def init(self): 134 model = gtk.ListStore(str) 135 for v in self.options: 136 model.append([str(v)]) 137 self.widget.set_model(model) 138 139 def save_ui_setting(self): 140 active = self.widget.get_active() 141 try: 142 # if the options are numbers, save the liternal of active option as 143 # a number 144 self.v = int(self.options[active]) 145 except ValueError: 146 # otherwise save its index 147 self.v = active 148 return self.v 149 150 def read_config(self): 151 self.v = self.read() 152 assert self.options, "options should not be empty" 153 try: 154 # if the options are just numbers, we treat 'self.v' as the literal 155 # of option 156 dummy = int(self.options[0]) 157 active = self.options.index(self.v) 158 except ValueError: 159 active = self.v 160 self.widget.set_active(active) 161 162 class RadioOption(Option): 163 """option represented using multiple Raidio buttons 164 """ 165 def __init__(self, name, default, options, owner): 166 super(RadioOption, self).__init__(name, default) 167 self.options = options 168 self.xml = owner 169 170 def init_ui(self): 171 self.read_config() 172 173 def read_config(self): 174 self.v = self.read() 175 name = SEPARATOR.join([self.name, self.v]) 176 button = self.xml.get_widget(name) 177 assert button is not None, "button: %r not found" % name 178 button.set_active(True) 179 180 def write_config(self): 181 active_opt = None 182 for opt in self.options: 183 radio_name = SEPARATOR.join([self.name, opt]) 184 radio = self.xml.get_widget(radio_name) 185 if radio.get_active(): 186 active_opt = opt 187 break 188 assert active_opt is not None 189 self.write(active_opt) 190 191 class MappingInfo: 192 def __init__(self, name, mapping): 193 self.name = name 194 self.mapping = mapping 195 196 class MappingOption(object): 197 """an option which presents some sort of mapping, e.g. fuzzy pinyin mapping 198 199 it is not directly related to a config option like TrivalOption does, but 200 we always have a checkbox in UI for each of it so user can change it easily. 201 """ 202 def __init__(self, name, mappings, owner): 203 self.name = name 204 self.widget = owner.get_widget(name) 205 self.mappings = mappings 206 207 def get_mappings(self): 208 if self.widget.get_active(): 209 return [':'.join(self.mappings)] 210 else: 211 return [] 212 213 def set_active(self, enabled): 214 self.widget.set_active(enabled) 215 216 def get_active(self): 217 return self.widget.get_active() 218 219 is_enabled = property(get_active, set_active) 220 221 def key(self): 222 return self.mappings[0] 223 224 class MultiMappingOption(Option): 225 def __init__(self, name, options, default=[]): 226 Option.__init__(self, name, default) 227 self.options = options 228 self.saved_pairs = default 229 230 def read_config(self): 231 if not self.saved_pairs: 232 self.saved_pairs = self.read() 233 keys = set([pair.split(':')[0] for pair in self.saved_pairs]) 234 for opt in self.options: 235 opt.is_enabled = (opt.key() in keys) 236 # throw away unknown pair 237 238 def write_config(self): 239 # ignore empty settings 240 if self.saved_pairs: 241 self.write(self.saved_pairs) 242 243 def save_ui_setting(self): 244 self.saved_pairs = sum([opt.get_mappings() for opt in self.options 245 if opt.is_enabled], []) 246 return self.saved_pairs 247 248 def set_active_all(self, enabled): 249 for opt in self.options: 250 opt.is_enabled = enabled 251 252 class MultiCheckDialog (object): 253 """ a modal dialog box with 'choose all' and 'choose none' button 254 255 TODO: another option is to use radio button 256 """ 257 def __init__ (self, ui_name, config_name, mappings, option_klass=MappingOption): 258 self.ui_name = ui_name 259 self.config_name = config_name 260 self.mappings = mappings 261 self.option_klass = option_klass 262 self.saved_settings = [] 263 self.mapping_options = None 264 265 def get_setup_name(self): 266 """assuming the name of dialog looks like 'dlg_fuzzy_setup' 267 """ 268 return '_'.join(['dlg', self.ui_name, 'setup']) 269 270 def __init_ui(self): 271 dlg_name = self.get_setup_name() 272 self.__xml = glade.XML(GLADE_FILE, dlg_name) 273 self.__dlg = self.__xml.get_widget(dlg_name) 274 assert self.__dlg is not None, "dialog %s not found in %s" % (dlg_name, GLADE_FILE) 275 handlers = {'_'.join(["on", self.ui_name, "select_all_clicked"]) : self.on_button_check_all_clicked, 276 '_'.join(["on", self.ui_name, "unselect_all_clicked"]) : self.on_button_uncheck_all_clicked, 277 '_'.join(["on", self.ui_name, "ok_clicked"]) : self.on_button_ok_clicked, 278 '_'.join(["on", self.ui_name, "cancel_clicked"]) : self.on_button_cancel_clicked} 279 self.__xml.signal_autoconnect(handlers) 280 281 options = [self.option_klass(m.name, m.mapping, self.__xml) 282 for m in self.mappings] 283 self.mapping_options = MultiMappingOption(self.config_name, options, self.saved_settings) 284 285 def dummy(self): 286 """a dummy func, i don't initialize myself upon other's request. 287 instead, i will do it by myself. 288 """ 289 pass 290 291 init_ui = read_config = dummy 292 293 def run(self): 294 self.__init_ui() 295 self.__read_config() 296 self.__dlg.run() 297 298 def __read_config(self): 299 self.mapping_options.read_config() 300 301 def __save_ui_settings(self): 302 """save to in-memory storage, will flush to config if not canceled in main_window 303 """ 304 self.saved_settings = self.mapping_options.save_ui_setting() 305 306 def write_config(self): 307 if self.mapping_options is not None: 308 self.mapping_options.write_config() 309 310 def on_button_check_all_clicked(self, button): 311 self.mapping_options.set_active_all(True) 312 313 def on_button_uncheck_all_clicked(self, button): 314 self.mapping_options.set_active_all(False) 315 316 def on_button_ok_clicked(self, button): 317 """update given options with settings in UI, these settings will be 318 written to config if user push 'OK' or 'Apply' in the main window 319 """ 320 self.__save_ui_settings() 321 self.__dlg.destroy() 322 323 def on_button_cancel_clicked(self, button): 324 self.__dlg.destroy() 325 326 class FuzzySetupDialog (MultiCheckDialog): 327 def __init__(self): 328 mappings = [MappingInfo('QuanPin/Fuzzy/ShiSi', ('sh','s')), 329 MappingInfo('QuanPin/Fuzzy/ZhiZi', ('zh','z')), 330 MappingInfo('QuanPin/Fuzzy/ChiCi', ('ch','c')), 331 MappingInfo('QuanPin/Fuzzy/ShiSi', ('sh','s')), 332 MappingInfo('QuanPin/Fuzzy/AnAng', ('an','ang')), 333 MappingInfo('QuanPin/Fuzzy/OnOng', ('on','ong')), 334 MappingInfo('QuanPin/Fuzzy/EnEng', ('en','eng')), 335 MappingInfo('QuanPin/Fuzzy/InIng', ('in','ing')), 336 MappingInfo('QuanPin/Fuzzy/EngOng', ('eng','ong')), 337 MappingInfo('QuanPin/Fuzzy/IanIang', ('ian','iang')), 338 MappingInfo('QuanPin/Fuzzy/UanUang', ('uan','uang')), 339 MappingInfo('QuanPin/Fuzzy/NeLe', ('n','l')), 340 MappingInfo('QuanPin/Fuzzy/FoHe', ('f','h')), 341 MappingInfo('QuanPin/Fuzzy/LeRi', ('l','r')), 342 MappingInfo('QuanPin/Fuzzy/KeGe', ('k','g'))] 343 MultiCheckDialog.__init__(self, 344 ui_name = 'fuzzy', 345 config_name = 'QuanPin/Fuzzy/Pinyins', 346 mappings = mappings) 347 348 class CorrectionSetupDialog (MultiCheckDialog): 349 def __init__(self): 350 mappings = [MappingInfo('QuanPin/AutoCorrection/IgnIng', ('ign','ing')), 351 MappingInfo('QuanPin/AutoCorrection/UenUn', ('uen','un')), 352 MappingInfo('QuanPin/AutoCorrection/ImgIng', ('img','ing')), 353 MappingInfo('QuanPin/AutoCorrection/IouIu', ('iou','iu')), 354 MappingInfo('QuanPin/AutoCorrection/UeiUi', ('uei','ui'))] 355 MultiCheckDialog.__init__(self, 356 ui_name = 'correction', 357 config_name = 'QuanPin/AutoCorrection/Pinyins', 358 mappings = mappings) 359 360 class PunctMapping(MappingOption): 361 def __init__(self, name, mappings, owner): 362 MappingOption.__init__(self, name, mappings, owner) 363 if mappings: 364 self.widget.set_sensitive(True) 365 self.init_keys_values(mappings) 366 else: 367 self.widget.set_sensitive(False) 368 369 def init_keys_values(self, mappings): 370 self.keys = [m[0] for m in mappings] 371 values_with_closing = [v or k for k, v in mappings] 372 self.values = [] 373 for v in values_with_closing: 374 try: 375 self.values.append(v[0]) 376 except: 377 self.values.append(v) 378 self.keys.reverse() 379 self.values.reverse() 380 381 def get_mappings(self): 382 if self.widget.get_active(): 383 pairs = [] 384 for k,vs in self.mappings: 385 try: 386 for v in vs: 387 pairs.append(':'.join([k,v])) 388 except: 389 v = vs 390 if v is None: 391 continue 392 pairs.append(':'.join([k,v])) 393 return pairs 394 else: 395 return [] 396 397 def set_active(self, enabled): 398 if not self.mappings: return 399 if enabled: 400 self.widget.set_label('\n'.join(self.values)) 401 else: 402 self.widget.set_label('\n'.join(self.keys)) 403 self.widget.set_active(enabled) 404 405 is_enabled = property(MappingOption.get_active, set_active) 406 407 def key(self): 408 for k, v in self.mappings: 409 if v is not None: 410 return k 411 else: 412 return None 413 414 class PunctMappingSetupDialog (MultiCheckDialog): 415 # TODO: the UI should looks like a virtual keyboard, 416 # user are allowed to choose the mappings to all punctuation keys. 417 def __init__(self): 418 mappings = [MappingInfo('togglebutton1', [('`',None), ('~',u'')]), 419 MappingInfo('togglebutton2', []), 420 MappingInfo('togglebutton3', [('2',None), ('@',u'')]), 421 MappingInfo('togglebutton4', [('3',None), ('#',u'')]), 422 MappingInfo('togglebutton5', []), 423 MappingInfo('togglebutton6', [('5',None), ('%',u'')]), 424 MappingInfo('togglebutton7', []), 425 MappingInfo('togglebutton8', [('7',None), ('&',u'')]), 426 MappingInfo('togglebutton9', [('8',None), ('*',u'')]), 427 MappingInfo('togglebutton14', [('\\',None), ('|',u'')]), 428 MappingInfo('togglebutton27', [('[',u''), ('{',u'')]), 429 MappingInfo('togglebutton28', [(']',u''), (']',u'')]), 430 MappingInfo('togglebutton39', []), 431 MappingInfo('togglebutton40', []), 432 MappingInfo('togglebutton50', [(',',None), ('<',u'')]), 433 MappingInfo('togglebutton51', [('.',u''), ('>',u'')]), 434 MappingInfo('togglebutton52', [('/',u''), ('?',None)])] 435 #'\'',(u'',u''), 436 MultiCheckDialog.__init__(self, ui_name="punctmapping", 437 config_name="General/PunctMapping/Mappings", 438 mappings=mappings, 439 option_klass=PunctMapping) 440 441 class MainWindow (): 442 def __init__ (self): 443 self.__bus = ibus.Bus() 444 self.__config = self.__bus.get_config() 445 446 447 def run(self): 448 self.__init_ui("main_window") 449 self.__read_config() 450 gtk.main() 451 452 def __init_ui(self, name): 453 self.__init_gettext() 454 glade_file = path.join(path.dirname(__file__), GLADE_FILE) 455 self.__xml = glade.XML (glade_file, name) 456 self.__init_options() 457 self.window = self.__xml.get_widget(name) 458 self.__xml.signal_autoconnect(self) 459 self.window.show_all() 460 461 def __init_gettext(self): 462 locale.setlocale(locale.LC_ALL, "") 463 localedir = os.getenv("IBUS_LOCALEDIR") 464 gettext.bindtextdomain(GETTEXT_PACKAGE, localedir) 465 gettext.bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8") 466 glade.bindtextdomain(GETTEXT_PACKAGE, localedir) 467 glade.textdomain(GETTEXT_PACKAGE) 468 469 def __init_options(self): 470 self.__fuzzy_setup = FuzzySetupDialog() 471 self.__correction_setup = CorrectionSetupDialog() 472 self.__punctmapping_setup = PunctMappingSetupDialog() 473 474 self.__options = [ 475 ComboBoxOption("General/MemoryPower", 3, range(10), self.__xml), 476 RadioOption("General/InitialStatus/Mode", 'Chinese', ['Chinese', 'English'], self.__xml), 477 RadioOption("General/InitialStatus/Punct", 'Full', ['Full', 'Half'], self.__xml), 478 RadioOption("General/InitialStatus/Letter", 'Half', ['Full', 'Half'], self.__xml), 479 RadioOption("General/Charset", 'GBK', ['GB2312', 'GBK', 'GB18030'], self.__xml), 480 CheckBoxOption("General/PunctMapping/Enabled", False, self.__xml), 481 482 ComboBoxOption("General/PageSize", 10, range(5, 11), self.__xml), 483 484 RadioOption("Keyboard/ModeSwitch", 'Shift', ['Shift', 'Control'], self.__xml), 485 RadioOption("Keyboard/PunctSwitch", 'None', ['ControlComma', 486 'ControlPeriod', 487 'None'], self.__xml), 488 CheckBoxOption("Keyboard/Page/MinusEquals", False, self.__xml), 489 CheckBoxOption("Keyboard/Page/Brackets", False, self.__xml), 490 CheckBoxOption("Keyboard/Page/CommaPeriod", False, self.__xml), 491 492 RadioOption("Pinyin/Scheme", 'QuanPin', ['QuanPin', 'ShuangPin'], self.__xml), 493 ComboBoxOption("Pinyin/ShuangPinType", 'MS2003', ['MS2003', 494 'ABC', 495 'ZiRanMa', 496 'Pinyin++', 497 'ZiGuang'], self.__xml), 498 CheckBoxOption("QuanPin/Fuzzy/Enabled", False, self.__xml), 499 CheckBoxOption("QuanPin/AutoCorrection/Enabled", False, self.__xml), 500 self.__fuzzy_setup, 501 self.__correction_setup, 502 self.__punctmapping_setup, 503 ] 504 505 def __get_option(self, name): 506 for opt in self.__options: 507 if opt.name == name: 508 return opt 509 else: 510 return None 511 512 def __read_config(self): 513 for opt in self.__options: 514 opt.init_ui() 515 opt.read_config() 516 self.on_chk_fuzzy_enabled_toggled(None) 517 self.on_chk_correction_enabled_toggled(None) 518 self.on_chk_punctmapping_enabled_toggled(None) 519 self.on_radio_shuangpin_toggled(None) 520 521 def __write_config(self): 522 for opt in self.__options: 523 opt.write_config() 524 525 def __update_enabling_button(self, checkbox_name, button_name): 526 """enable a setup button when checked, disable it otherwise 527 """ 528 checkbox = self.__xml.get_widget(checkbox_name) 529 assert checkbox is not None, "checkbox: %s not found" % checkbox_name 530 button = self.__xml.get_widget(button_name) 531 assert button is not None, "button: %s not found" % button_name 532 button_enabled = checkbox.get_active() 533 button.set_sensitive(button_enabled) 534 535 def on_radio_shuangpin_toggled(self, button): 536 radio = self.__xml.get_widget("Pinyin/Scheme/ShuangPin") 537 enabled = radio.get_active() 538 combo = self.__xml.get_widget("Pinyin/ShuangPinType") 539 combo.set_sensitive(enabled) 540 541 def on_chk_fuzzy_enabled_toggled(self, button): 542 self.__update_enabling_button("QuanPin/Fuzzy/Enabled", 543 "button_fuzzy_setup") 544 545 def on_button_fuzzy_setup_clicked(self, button): 546 self.__fuzzy_setup.run() 547 548 def on_chk_correction_enabled_toggled(self, button): 549 self.__update_enabling_button("QuanPin/AutoCorrection/Enabled", 550 "button_correction_setup") 551 552 def on_button_correction_setup_clicked(self, button): 553 self.__correction_setup.run() 554 555 def on_chk_punctmapping_enabled_toggled(self, button): 556 self.__update_enabling_button("General/PunctMapping/Enabled", 557 "button_punctmapping_setup") 558 559 def on_button_punctmapping_setup_clicked(self, button): 560 self.__punctmapping_setup.run() 561 562 def on_main_ok_clicked(self, button): 563 self.__write_config() 564 self.__quit() 565 566 def on_main_apply_clicked(self, button): 567 self.__write_config() 568 569 def on_main_cancel_clicked(self, button): 570 self.__quit() 571 572 def __quit(self): 573 gtk.main_quit() 574 575 if __name__ == "__main__": 576 MainWindow().run() 577