Home | History | Annotate | Download | only in setup
      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