10 # ================================================================
12 GUI_DESCRIPTION = 'nubmode.glade'
13 PROFILES = '/etc/pandora/conf/nub_profiles.conf'
15 # Shell command to reset nub: 3-0066 = left-nub, 3-0067 = right-nub
16 # apparently they are linked and resetting one resets both.
17 #RESET_CMD = 'echo %i > /sys/bus/i2c/drivers/vsense/3-00%i/reset'
18 RESET_CMD = "sudo /usr/pandora/scripts/reset_nubs.sh"
20 # Valid values for mode setting
21 MODES = ("mouse", "mbuttons", "scroll", "absolute")
23 # Paths for reading and writing different configuration options %i -> 0 | 1
25 mode='/proc/pandora/nub%s/mode',
26 mouse='/proc/pandora/nub%s/mouse_sensitivity',
27 button='/proc/pandora/nub%s/mbutton_threshold',
28 rate='/proc/pandora/nub%s/scroll_rate',
29 scrollx='/proc/pandora/nub%s/scrollx_sensitivity',
30 scrolly='/proc/pandora/nub%s/scrolly_sensitivity',
33 # format for saving/loading
34 FILE_ORDER = ("mode", "mouse", "button", "rate", "scrollx", "scrolly")
36 # Default configuration
37 DEFAULT_PROFILENAME = "Default"
38 DEFAULT_DICT = dict(mode0='mouse', mode1='mbuttons', mouse0='150', mouse1='150',
39 button0='20', button1='20', rate0='20', rate1='20',
40 scrollx0='7', scrollx1='7', scrolly0='7', scrolly1='7')
42 # Types of model changes for view updates
43 (MODEL_PROFILE_CHANGE, MODEL_VALUE_CHANGE) = range(2)
45 # Documentation for commandline options
46 HELP_RESET = "Reset specified nub(s). Format: left,right"
47 HELP_LEFT_NUB = ("Configure left nub. Include -a flag to activate. Format: %s. E.g. %s" %
48 (' '.join(FILE_ORDER),
49 ' '.join(DEFAULT_DICT[k+'0'] for k in FILE_ORDER)))
50 HELP_RIGHT_NUB = ("Configure right nub. Include -a flag to activate. Format: %s. E.g. %s" %
51 (' '.join(FILE_ORDER),
52 ' '.join(DEFAULT_DICT[k+'1'] for k in FILE_ORDER)))
53 HELP_SAVE = "Store current configuration as specified profile (no spaces allowed)"
54 HELP_APPLY = "Write currently loaded configuration to nubs."
55 HELP_LOAD = "Load and apply specified nub configuration profile"
56 HELP_DEL = "Delete specified nub configuration profile"
58 # Commandline input type checking
59 TYPECHECK = dict(mode='(%s)' % '|'.join(MODES),
60 mouse='(\d+)', button='(\d+)',
61 rate='(\d+)', scrollx='(\d+)', scrolly='(\d+)')
62 RE_FORMAT = ','.join(TYPECHECK[k] for k in FILE_ORDER)
63 BOUNDS = dict(mouse=(50, 300), button=(1, 40), rate=(1, 40),
64 scrollx=(-32,32), scrolly=(-32,32))
66 # ================================================================
67 # There is a bug in setting scrollx/scrolly sensitivity in the
68 # firmware (to be fixed in hotfix 6).
69 # Detect and set FIX_SCROLLXY_BUG to use a workaround.
71 def ReadWriteTest(value=None):
72 with open(SETTINGS['scrollx'] % 0, 'w' if value else 'r') as f:
73 return f.write('%s\n' % value) if value else f.readline().rstrip()
75 tmp = int(ReadWriteTest()) # backup original
76 ReadWriteTest(tmp + (-1 if tmp < 0 else 1)) # write corrected value
77 FIX_SCROLLXY_BUG = int(ReadWriteTest()) == tmp # fix bug if equal to original
78 ReadWriteTest(tmp + ((-1 if tmp < 0 else 1) if FIX_SCROLLXY_BUG else 0)) # restore
80 # ================================================================
84 for key, value in SETTINGS.iteritems():
86 with open(value % c, 'r') as f:
87 config[key+c] = f.readline().strip()
90 def StoreProc(dictionary):
91 # fix for value decrement/increment after written
93 dictionary = dictionary.copy()
94 for key in ('scrollx0', 'scrolly0', 'scrollx1', 'scrolly1'):
95 value = int(dictionary[key])
96 value += -1 if value < 0 else 1
97 dictionary[key] = str(value)
98 for key, value in SETTINGS.iteritems():
100 with open(value % c, 'w') as f:
101 f.write('%s\n' % dictionary[key+c])
103 def ProfileToString(dictionary):
104 return ' '.join(dictionary[k+c] for c in '01' for k in FILE_ORDER)
106 def StringToProfile(line):
107 return dict(zip((k+c for c in '01' for k in FILE_ORDER), line.split(' ')))
110 mo = re.match(RE_FORMAT, value)
111 if mo: # verify bounds
112 values = dict(zip(FILE_ORDER, mo.groups()))
113 if all(BOUNDS[k][0] <= int(values[k]) <= BOUNDS[k][1] for k in BOUNDS):
117 # ================================================================
119 class NubModel(object):
120 def __init__(self, view=None):
121 self.views = [view] if view else []
122 self.settings = ReadProc()
124 with open(PROFILES) as f:
130 self.profiles[name] = StringToProfile(line.rstrip())
133 def notify(self, reason, *args):
135 v.update_view(reason, *args)
137 def set_profile(self, name, dictionary):
138 notify = name not in self.profiles
139 self.profiles.setdefault(name, {}).update(dictionary)
141 self.notify(MODEL_PROFILE_CHANGE, name, self.profiles)
143 def delete_profile(self, name):
144 notify = name in self.profiles
145 del self.profiles[name]
147 self.notify(MODEL_PROFILE_CHANGE, '', self.profiles)
149 def load_profile(self, settings):
150 self.settings.update(settings)
151 self.notify(MODEL_VALUE_CHANGE, self.settings)
153 def load_named_profile(self, name):
155 if name == DEFAULT_PROFILENAME:
156 self.settings.update(DEFAULT_DICT)
157 elif name in self.profiles:
158 self.settings.update(self.profiles[name])
162 self.notify(MODEL_VALUE_CHANGE, self.settings)
164 def store_profiles(self, filename):
165 with open(filename, 'w') as f:
166 for name in sorted(self.profiles.keys()):
167 f.write('%s\n%s\n' % (name, ProfileToString(self.profiles[name])))
170 class NubConfig(object):
171 """GUI application for modifying the pandora nub configuration"""
173 builder = gtk.Builder()
174 builder.add_from_file(
175 os.path.join(os.path.dirname(__file__), GUI_DESCRIPTION))
176 builder.connect_signals(self)
178 # slider widgets, more specifically: their Adjustment objects
180 for s in DEFAULT_DICT:
181 w = builder.get_object(s)
183 w.connect('value-changed', self.on_slider_changed, s)
190 w = builder.get_object('R%s%s' % (m,c))
191 w.connect('clicked', self.on_radio_changed, 'mode'+c, m)
193 self.widgets['mode'+c] = group
195 self.statusbar = builder.get_object('statusbar')
196 self.contextid = self.statusbar.get_context_id('')
198 self.model = NubModel(self)
200 self.profiles = gtk.ListStore(str)
201 self.profiles.append([DEFAULT_PROFILENAME])
202 for name in self.model.profiles:
203 self.profiles.append([name])
205 self.comboentry = builder.get_object('ProfileComboEntry')
206 self.comboentry.set_model(self.profiles)
207 self.comboentry.set_text_column(0)
208 self.comboentry.connect('changed', self.on_combo_changed)
210 self.entry = self.comboentry.get_child()
211 self.entry.connect('activate', self.on_LoadProfile_clicked)
213 # we need a reference to change their sensitive
214 # setting according to the profile
215 self.delete = builder.get_object('DeleteProfile')
216 self.save = builder.get_object('SaveProfile')
218 # this also changes sensitive of self.save & self.load
219 self.comboentry.set_active(0)
221 # read current config
222 self.model.load_profile(ReadProc())
224 self.window = builder.get_object("window")
225 self.window.show_all()
227 accelgrp = gtk.AccelGroup()
228 key, mod = gtk.accelerator_parse('<Control>Q')
229 accelgrp.connect_group(key, mod, 0, self.on_window_destroy)
230 self.window.add_accel_group(accelgrp)
232 def _update_profile_list(self, new, profiles):
233 self.profiles.clear()
234 self.profiles.append([DEFAULT_PROFILENAME])
236 for i, key in enumerate(sorted(profiles.keys())):
237 self.profiles.append([key])
240 self.comboentry.set_active(activate)
242 def _update_widgets(self, settings):
243 for key, value in settings.iteritems():
245 for w, m in zip(self.widgets[key], MODES):
246 w.set_active(m == value)
248 self.widgets[key].value = int(value)
250 def update_view(self, reason, *data):
251 if reason == MODEL_PROFILE_CHANGE:
252 self._update_profile_list(*data)
253 elif reason == MODEL_VALUE_CHANGE:
254 self._update_widgets(*data)
256 def _lose_active(self):
257 # Forces the comboboxentry to lose its active selection
258 # such that it also generates a changed signal when we
259 # select the same value.
260 temp = self.entry.get_text()
261 self.entry.set_text('')
262 self.entry.set_text(temp)
264 def Notify(self, message):
265 self.statusbar.pop(self.contextid)
266 self.statusbar.push(self.contextid, message)
268 def on_slider_changed(self, widget, key):
269 self.model.settings[key] = str(int(widget.value))
271 def on_radio_changed(self, widget, key, mode):
272 self.model.settings[key] = mode
274 def on_combo_changed(self, widget, *data):
275 name = self.entry.get_text()
276 self.delete.set_sensitive(name in self.model.profiles)
277 self.save.set_sensitive(name != '' and name != DEFAULT_PROFILENAME)
278 if self.comboentry.get_active() != -1:
279 self.on_LoadProfile_clicked(None)
281 def on_ResetNubs_clicked(self, widget, *data):
282 self.Notify("Resetting the nubs...")
284 self.Notify("Nubs reset.")
286 def on_ReadNubConfig_clicked(self, widget, *data):
287 self.model.load_profile(ReadProc())
288 self.Notify("Active nub configuration loaded.")
290 def on_WriteNubConfig_clicked(self, widget, *data):
291 StoreProc(self.model.settings)
292 self.Notify("Nub configuration updated.")
294 def on_SaveProfile_clicked(self, widget, *data):
295 name = self.entry.get_text()
296 if name == '' or name == DEFAULT_PROFILENAME:
297 self.Notify("Invalid profile name")
299 self.model.set_profile(name, self.model.settings)
300 self.Notify("Profile saved as: %s" % name)
302 def on_LoadProfile_clicked(self, widget, *data):
303 self._lose_active() # force widget to always emit changed signals,
304 name = self.entry.get_text() # ok since we always lookup by text value anyway
307 elif name not in self.model.profiles and name != DEFAULT_PROFILENAME:
308 self.Notify("Cannot load profile, please select an existing profile.")
310 self.model.load_named_profile(name)
311 self.Notify("Profile loaded, hit 'Write nub settings' to make it active")
313 def on_DeleteProfile_clicked(self, widget, *data):
314 name = self.entry.get_text()
315 if name not in self.model.profiles or name == DEFAULT_PROFILENAME:
316 self.model("Cannot remove profile, please select an existing non-default profile.")
318 dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_WARNING,
319 buttons=gtk.BUTTONS_YES_NO,
320 message_format="Are you sure you want to delete profile %s?" % name)
321 if dialog.run() == gtk.RESPONSE_YES:
322 self.model.delete_profile(name)
325 def on_window_destroy(self, widget, *data):
326 self.Notify("Storing profiles...")
327 self.model.store_profiles(PROFILES)
330 # ================================================================
332 if __name__ == '__main__':
333 parser = optparse.OptionParser()
334 parser.add_option('--reset', default=False, action='store_true', help=HELP_RESET)
335 parser.add_option('-l', '--left_nub', default='', help=HELP_LEFT_NUB)
336 parser.add_option('-r', '--right_nub', default='', help=HELP_RIGHT_NUB)
337 parser.add_option('-s', '--save_profile', default='', help=HELP_SAVE)
338 parser.add_option('-a', '--apply', default=False, action='store_true', help=HELP_APPLY)
339 parser.add_option('-p', '--load_profile', default='', help=HELP_LOAD)
340 parser.add_option('-d', '--remove_profile', default='', help=HELP_DEL)
341 options, args = parser.parse_args()
343 if len(sys.argv) == 1: # no params: run gui app
346 else: # run command line app
351 for key, value in Validate(options.left_nub).iteritems():
352 model.settings[key+'0'] = value
353 for key, value in Validate(options.right_nub).iteritems():
354 model.settings[key+'1'] = value
355 if options.save_profile and options.save_profile != DEFAULT_PROFILENAME:
356 model.set_profile(options.save_profile, model.settings)
358 StoreProc(model.settings)
359 if options.load_profile:
360 model.load_named_profile(options.load_profile)
361 StoreProc(model.settings)
362 if options.remove_profile:
363 model.delete_profile(options.remove_profile)
364 if options.save_profile or options.remove_profile:
365 model.store_profiles(PROFILES)