Repository: pwr-Solaar/Solaar

Issue:
Put "dangerous" features under warning
**Is your feature request related to a problem? Please describe.**

I had a particularly hectic conference where I had to switch between the two PCs constantly and fast [ using host switch feature ] and when doing that via GUI I've disabled left mouse click accidentally while being completely oblivious to it,  which was really bad [ as you can imagine ] and took me a bit to figure it out.
[ I will do that guiless next time, just didn't have time to prepare for that particular event ]


**Describe the solution you'd like**
Perhaps put it behind some kind of a pop-up warning about what's to happen?



Discussion:
I sympathize.  But what is the right way to do this?  I'm trying to abide by the Unix philosophy and not include "nanny" features.

One simple thing to do would be to reverse the order of the keys in the menu so that the left button doesn't show normally.  But that would mean that any function keys would show up in reverse order for many keyboards.  A slightly more complex thing to do would be to swap the order of left and right buttons.  A third thing to do would be to suppress the left and right buttons.  But that is a "nanny' feature and also prevents swapping the left and right buttons.
Maybe a "lock settings, prevent modificiation" type toggle? Seems like a pretty common thing, or just some command line switch
Yes, a method for locking any setting would be useful.  Now how to implement it. ...
@barolo One way to do this is to make the setting label sensitive.  A click on it changes the sensitivity of the setting value.  If the tooltips are set up correctly settings could be insensitive by default.
I'm up for trying anything ;)
Try PR #980 
It still needs a bit of polish, but the basic idea is that you click on a setting name to toggle whether the setting is active.  Your choices persist.  So try moving your MX Master between different machines (BT connect, receiver connect) and try killing and restarting Solaar.  The initial default is that settings are active.

Resolving patch:
diff --git a/lib/solaar/configuration.py b/lib/solaar/configuration.py
--- a/lib/solaar/configuration.py
+++ b/lib/solaar/configuration.py
@@ -114,6 +114,14 @@ def update(self, device):
         if device.unitId:
             self[_KEY_UNIT_ID] = device.unitId
 
+    def get_sensitivity(self, name):
+        return self.get('_sensitive', {}).get(name, False)
+
+    def set_sensitivity(self, name, value):
+        sensitives = self.get('_sensitive', {})
+        sensitives[name] = value
+        self['_sensitive'] = sensitives
+
 
 def persister(device):
     if not _configuration:
diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py
--- a/lib/solaar/ui/config_panel.py
+++ b/lib/solaar/ui/config_panel.py
@@ -31,12 +31,12 @@
 #
 
 
-def _read_async(setting, force_read, sbox, device_is_online):
-    def _do_read(s, force, sb, online):
+def _read_async(setting, force_read, sbox, device_is_online, sensitive):
+    def _do_read(s, force, sb, online, sensitive):
         v = s.read(not force)
-        GLib.idle_add(_update_setting_item, sb, v, online, priority=99)
+        GLib.idle_add(_update_setting_item, sb, v, online, sensitive, priority=99)
 
-    _ui_async(_do_read, setting, force_read, sbox, device_is_online)
+    _ui_async(_do_read, setting, force_read, sbox, device_is_online, sensitive)
 
 
 def _write_async(setting, value, sbox):
@@ -181,7 +181,7 @@ def _changed(self):
     return control.gtk_range
 
 
-def _create_multiple_toggle_control(setting):
+def _create_multiple_toggle_control(setting, change):
     def _toggle_notify(control, _, setting):
         if control.get_sensitive():
             key = control._setting_key
@@ -234,7 +234,8 @@ def _toggle_display(lb):
     lb._toggle_display()
     btn.connect('clicked', lambda _: lb._toggle_display())
 
-    hbox = Gtk.HBox(homogeneous=False, spacing=60)
+    hbox = Gtk.HBox(homogeneous=False, spacing=6)
+    hbox.pack_end(change, False, False, 0)
     hbox.pack_end(btn, False, False, 0)
     btn.set_alignment(1.0, 0.5)
     vbox = Gtk.VBox(homogeneous=False, spacing=6)
@@ -243,7 +244,7 @@ def _toggle_display(lb):
     return vbox
 
 
-def _create_multiple_range_control(setting):
+def _create_multiple_range_control(setting, change):
     def _write(control, setting, item, sub_item):
         control._timer.cancel()
         delattr(control, '_timer')
@@ -330,6 +331,7 @@ def _toggle_display(lb):
     btn.connect('clicked', lambda _: lb._toggle_display())
     btn.set_alignment(1.0, 0.5)
     hbox = Gtk.HBox(homogeneous=False, spacing=6)
+    hbox.pack_end(change, False, False, 0)
     hbox.pack_end(btn, False, False, 0)
     vbox = Gtk.VBox(homogeneous=False, spacing=6)
     vbox.pack_start(hbox, True, True, 0)
@@ -342,10 +344,28 @@ def _toggle_display(lb):
 #
 
 
-def _create_sbox(s):
+# clicking on the lock icon changes the sensitivity of the setting
+def _change_click(eb, button, arg):
+    control, device, name = arg
+    sensitive = not control.get_sensitive()
+    control.set_sensitive(sensitive)
+    icon = eb.get_children()[0]
+    _change_icon(sensitive, icon)
+    if device.persister:  # remember the new setting sensitivity
+        device.persister.set_sensitivity(name, sensitive)
+    return True
+
+
+def _change_icon(allowed, icon):
+    icon.set_from_icon_name('changes-allow' if allowed else 'changes-prevent', Gtk.IconSize.LARGE_TOOLBAR)
+    icon.set_tooltip_text(_('Click to prevent changes.') if allowed else _('Click to allow changes.'))
+
+
+def _create_sbox(s, device):
     sbox = Gtk.HBox(homogeneous=False, spacing=6)
-    label = Gtk.Label(s.label)
-    sbox.pack_start(label, False, False, 0)
+    lbl = Gtk.Label(s.label)
+    label = Gtk.EventBox()
+    label.add(lbl)
 
     spinner = Gtk.Spinner()
     spinner.set_tooltip_text(_('Working') + '...')
@@ -353,41 +373,45 @@ def _create_sbox(s):
     failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
     failed.set_tooltip_text(_('Read/write operation failed.'))
 
+    change_icon = Gtk.Image.new_from_icon_name('changes-prevent', Gtk.IconSize.LARGE_TOOLBAR)
+    _change_icon(False, change_icon)
+    change = Gtk.EventBox()
+    change.add(change_icon)
+
     if s.kind == _SETTING_KIND.toggle:
         control = _create_toggle_control(s)
-        sbox.pack_end(control, False, False, 0)
     elif s.kind == _SETTING_KIND.choice:
         control = _create_choice_control(s)
-        sbox.pack_end(control, False, False, 0)
     elif s.kind == _SETTING_KIND.range:
         control = _create_slider_control(s)
-        sbox.pack_end(control, True, True, 0)
     elif s.kind == _SETTING_KIND.map_choice:
         control = _create_map_choice_control(s)
-        sbox.pack_end(control, True, True, 0)
     elif s.kind == _SETTING_KIND.multiple_toggle:
-        vbox = _create_multiple_toggle_control(s)
+        vbox = _create_multiple_toggle_control(s, change)
         control = vbox.get_children()[1]
-        sbox.remove(label)
-        vbox.get_children()[0].pack_start(label, False, False, 0)
-        label.set_alignment(0.0, 0.5)
+        lbl.set_alignment(0.0, 0.5)
         sbox.pack_start(vbox, True, True, 0)
     elif s.kind == _SETTING_KIND.multiple_range:
-        vbox = _create_multiple_range_control(s)
+        vbox = _create_multiple_range_control(s, change)
         control = vbox.get_children()[1]
-        sbox.remove(label)
-        vbox.get_children()[0].pack_start(label, False, False, 0)
-        label.set_alignment(0.0, 0.5)
+        lbl.set_alignment(0.0, 0.5)
         sbox.pack_start(vbox, True, True, 0)
     else:
         raise Exception('NotImplemented')
     control.set_sensitive(False)  # the first read will enable it
     control.kind = s.kind
 
+    change.set_sensitive(True)
+    change.connect('button-press-event', _change_click, (control, device, s.name))
+
     if s.kind in [_SETTING_KIND.multiple_toggle, _SETTING_KIND.multiple_range]:
+        vbox.get_children()[0].pack_start(label, False, False, 0)
         vbox.get_children()[0].pack_end(spinner, False, False, 0)
         vbox.get_children()[0].pack_end(failed, False, False, 0)
     else:
+        sbox.pack_start(label, False, False, 0)
+        sbox.pack_end(change, False, False, 0)
+        sbox.pack_end(control, s.kind == _SETTING_KIND.range, s.kind == _SETTING_KIND.range, 0)
         sbox.pack_end(spinner, False, False, 0)
         sbox.pack_end(failed, False, False, 0)
 
@@ -402,13 +426,14 @@ def _create_sbox(s):
     return sbox
 
 
-def _update_setting_item(sbox, value, is_online=True):
+def _update_setting_item(sbox, value, is_online=True, sensitive=True):
     failed, spinner, control = _get_failed_spinner_control(sbox)
     spinner.set_visible(False)
     spinner.stop()
 
     if value is None:
         control.set_sensitive(False)
+        _change_icon(False, _get_change(sbox))
         failed.set_visible(is_online)
         return
 
@@ -438,7 +463,7 @@ def _update_setting_item(sbox, value, is_online=True):
                     active += 1
                 to_join.append(elem.get_parent().get_children()[0].get_text() + ': ' + str(elem.get_active()))
             b = ', '.join(to_join)
-            btn = control.get_parent().get_children()[0].get_children()[-1]
+            btn = control.get_parent().get_children()[0].get_children()[-2]
             btn.set_label(f'{active} / {total}')
             btn.set_tooltip_text(b)
         elif control.kind == _SETTING_KIND.multiple_range:
@@ -460,20 +485,22 @@ def _update_setting_item(sbox, value, is_online=True):
                         n += 1
                         to_join.append(str(sub_item) + f'={v[str(sub_item)]}')
                     b += ', '.join(to_join) + ') '
-                btn = control.get_parent().get_children()[0].get_children()[-1]
+                btn = control.get_parent().get_children()[0].get_children()[3]
                 btn.set_label(f'{n} value' + ('s' if n != 1 else ''))  # TODO: i18n, singular/plural
                 btn.set_tooltip_text(b)
         else:
             raise NotImplementedError
     else:
         raise Exception('NotImplemented')
-    control.set_sensitive(True)
+
+    control.set_sensitive(sensitive)
+    _change_icon(sensitive, _get_change(sbox))
 
 
 def _get_failed_spinner_control(sbox):
     children = sbox.get_children()
-    if len(children) == 4:
-        _ignore, failed, spinner, control = sbox.get_children()  # depends on box layout
+    if len(children) == 5:
+        _ignore, failed, spinner, control, _ignore = sbox.get_children()  # depends on box layout
     else:
         assert len(children) == 1
         control = children[0].get_children()[-1]
@@ -482,6 +509,24 @@ def _get_failed_spinner_control(sbox):
     return failed, spinner, control
 
 
+def _get_label(sbox):
+    children = sbox.get_children()
+    if len(children) == 5:
+        return children[0].get_children()[0]
+    else:
+        assert len(children) == 1
+        return children[0].get_children()[0].get_children()[0].get_children()[0]
+
+
+def _get_change(sbox):
+    children = sbox.get_children()
+    if len(children) == 5:
+        return children[-1].get_children()[0]
+    else:
+        assert len(children) == 1
+        return children[0].get_children()[0].get_children()[-1].get_children()[0]
+
+
 def _disable_listbox_highlight_bg(lb):
     colour = Gdk.RGBA()
     colour.parse('rgba(0,0,0,0)')
@@ -528,10 +573,11 @@ def update(device, is_online=None):
         if k in _items:
             sbox = _items[k]
         else:
-            sbox = _items[k] = _create_sbox(s)
+            sbox = _items[k] = _create_sbox(s, device)
             _box.pack_start(sbox, False, False, 0)
 
-        _read_async(s, False, sbox, is_online)
+        sensitive = device.persister.get_sensitivity(s.name) if device.persister else True
+        _read_async(s, False, sbox, is_online, sensitive)
 
     _box.set_visible(True)
 


Test patch: