Files
linuxcncAllWasm/lib/python/gladevcp/hal_lightbutton.py
2026-08-20 06:17:39 +08:00

399 lines
19 KiB
Python

# GladeVcp HAL lighted button
#
# Copyright (c) 2015 Moses McKnight <moses@texband.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import gi
gi.require_version("Gtk","3.0")
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
gi.require_version("PangoCairo", "1.0")
from gi.repository import Pango, PangoCairo
import cairo
import math
# This creates the custom lighted button widget
# A lighted button has a HAL_OUT or HAL_IO pin for the button. This pin indicates if the button is pressed or toggled or not.
# If it is an HAL_IO pin, the pin is set True when the button is clicked and must be set False externally.
# This gives an external component a chance to "see" the pin state.
# If the pin is a HAL_OUT pin, the button acts as a toggle button and the pin state is toggled with each click.
# A HAL pin to enable/disable the button can be created. One use is to connect it to halui.machine.is-on to enable a button
# when the machine is on and disable it when the machine is off.
# There is also a HAL IN pin for the light, which operates independently of the button state.
#
# If has_hal_pins is true, the button creates several pins with the base name being the name of the widget
# widgetname-button OUT or IO - indicates button state
# widgetname-button-not OUT - only created if 'button_halio' is NOT true, inverse of widgetname-button
# widgetname-enable IN - only created if 'create_enable_pin' is true, allows button to be enabled and disabled by a HAL signal
# widgetname-light IN - controls light on/off
#
# If 'has_hal_pins' is false, no hal pins are created and the button can be completely controlled in code. It emits the 'clicked'
# signal when pressed. You can also leave 'has_hal_pins' true, and connect the -light pin to a HAL signal while handling the
# button 'clicked' signal in code. For this use you want to make sure 'button_halio_pin' is false or the button will remain 'active'
# after the first click, and calls to get_active() will always return True.
if __name__ == "__main__":
from hal_widgets import _HalWidgetBase, hal, hal_pin_changed_signal
else:
from .hal_widgets import _HalWidgetBase, hal, hal_pin_changed_signal
clicked_signal = ('clicked', (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT,)))
class HAL_LightButton(Gtk.DrawingArea, _HalWidgetBase):
__gtype_name__ = 'HAL_LightButton'
__gsignals__ = dict([clicked_signal])
__gproperties__ = {
'has_hal_pins' : ( GObject.TYPE_BOOLEAN, 'Has HAL pins', 'Set false if this button will not be controlled using HAL',
True, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'is_momentary' : ( GObject.TYPE_BOOLEAN, 'Is momentary',
'Set True if this button will be momentary rather then toggle',
False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'button_halio_pin' : ( GObject.TYPE_BOOLEAN, 'Button pin is HAL_IO', 'If HAL_IO, pin is set true on button press; if HAL_OUT, pin state is toggled by button press',
True, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'create_enable_pin' : ( GObject.TYPE_BOOLEAN, 'Create enable pin', 'Creates an enable pin which enables the button if True, and disables when False',
False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'dual_color' : ( GObject.TYPE_BOOLEAN, 'Dual Color Light', 'If true, light is on always, but changes color for OFF state',
False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
#'use_bitmaps' : ( GObject.TYPE_BOOLEAN, 'Use Bitmaps', 'If true, you must select bitmaps for each button state',
# False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'light_is_on' : ( GObject.TYPE_BOOLEAN, 'Light is on', 'Turns light on - for testing in glade',
False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'light_on_color' : ( Gdk.Color.__gtype__, 'Light ON color', "Set color for light ON state",
GObject.ParamFlags.READWRITE),
'light_off_color' : ( Gdk.Color.__gtype__, 'Light OFF color', "Set color for light OFF state",
GObject.ParamFlags.READWRITE),
'border_width' : ( GObject.TYPE_INT, 'Border width', 'Number of pixels extra border around label',
0, 50, 6, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'corner_radius' : ( GObject.TYPE_INT, 'Corner radius', 'Radius of the button corners',
1, 20, 4, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'button_text' : ( GObject.TYPE_STRING, 'Button default/off text', 'Text shown when light is off, or all the time if \"Button ON text\" is blank',
"Button", GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT),
'button_on_text' : ( GObject.TYPE_STRING, 'Button ON text', 'If not blank, this text will be shown on the button when light is on',
"", GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT),
'font_on_color' : ( Gdk.Color.__gtype__, 'Font ON color', "Set color for button text when light is ON",
GObject.ParamFlags.READWRITE),
'font_off_color' : ( Gdk.Color.__gtype__, 'Font OFF color', "Set color for button text when light is OFF",
GObject.ParamFlags.READWRITE),
'font_face' : ( GObject.TYPE_STRING, 'Font name', 'Button text',
"Sans", GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT),
'font_bold' : ( GObject.TYPE_BOOLEAN, 'Bold font', 'Set button font to bold',
False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
'font_size' : ( GObject.TYPE_INT, 'Font size', 'Number of pixels extra border around label',
0, 100, 10, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
}
__gproperties = __gproperties__
_size_request = (35, 35)
def __init__(self):
super(HAL_LightButton, self).__init__()
self.has_hal_pins = True
# When True, button_pin is HAL_IO.
# Pressing the button sets the pin True, and it must be reset to False externally
# When False, button_pin is HAL_OUT, and it's state is toggled by a button press,
# which means this button becomes a togglebutton.
self.button_halio_pin = True
self.dual_color = False
self.use_bitmaps = False #this feature is not implemented yet
self.light_on_color = Gdk.Color.parse('green')[1]
self.light_off_color = Gdk.Color.parse('gray')[1]
self.border_width = 6
self.corner_radius = 4
self.button_text = 'Button'
self.button_on_text = ''
self.font_face = 'Sans'
self.font_bold = False
self.font_size = 10
self.font_on_color = Gdk.Color.parse('black')[1]
self.font_off_color = Gdk.Color.parse('black')[1]
self.create_enable_pin = False
self.active = False
self.light_is_on = False
self.mouseover = False
self.default_pangolayout = self.create_pango_layout(self.button_text)
self.on_pangolayout = self.create_pango_layout(self.button_on_text)
self.set_size_request(*self._size_request)
self.set_events( Gdk.EventMask.EXPOSURE_MASK
| Gdk.EventMask.ENTER_NOTIFY_MASK
| Gdk.EventMask.LEAVE_NOTIFY_MASK
| Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.POINTER_MOTION_MASK
| Gdk.EventMask.POINTER_MOTION_HINT_MASK)
self.connect("draw", self.expose)
#self.connect('button-press-event', self.pressed)
#self.connect('button-release-event', self.released)
#self.connect('state-changed', self._on_state_changed)
def do_enter_notify_event(self, event):
self.mouseover = True
self.queue_draw()
def do_leave_notify_event(self, event):
self.mouseover = False
self.queue_draw()
def do_button_press_event(self, event):
if event.button == 1:
if self.is_momentary:
active = True
else:
active = not self.active
if (self.has_hal_pins):
if (self.button_halio_pin):
self.set_active(True)
try:
self.button_pin.set(True)
except:
pass
else:
self.set_active(active)
try:
self.button_pin.set(active)
self.button_pin_not.set(not active)
except:
pass
else:
self.set_active(active)
self.emit("clicked", self)
return True
def do_button_release_event(self, event):
if event.button == 1:
if not self.is_momentary:
return True
if (self.has_hal_pins):
self.set_active(False)
try:
self.button_pin.set(False)
self.button_pin_not.set(True)
except:
pass
else:
self.set_active(False)
self.emit("clicked", self)
return True
def expose(self, widget, event):
if self.is_sensitive():
alpha = 1
else:
alpha = 0.3
w = self.get_allocated_width()
h = self.get_allocated_height()
cr = widget.get_property('window').cairo_create()
def set_color(c):
return cr.set_source_rgba(c.red_float, c.green_float, c.blue_float, alpha)
if (self.use_bitmaps == False):
linewidth = 4
x = y = linewidth/2
w2 = w - linewidth
h2 = h - linewidth
x_center = x + w2/2
y_center = y + h2/2
degrees = math.pi / 180.0
cr.new_sub_path()
cr.arc(x + w2 - self.corner_radius, y + self.corner_radius, self.corner_radius, -90 * degrees, 0 * degrees)
cr.arc(x + w2 - self.corner_radius, y + h2 - self.corner_radius, self.corner_radius, 0 * degrees, 90 * degrees)
cr.arc(x + self.corner_radius, y + h2 - self.corner_radius, self.corner_radius, 90 * degrees, 180 * degrees)
cr.arc(x + self.corner_radius, y + self.corner_radius, self.corner_radius, 180 * degrees, 270 * degrees)
cr.close_path()
def modify_hsv_color(color, h_mult, s_mult, v_mult):
hsv = Gtk.HSV()
(h,s,v) = Gtk.rgb_to_hsv(color.red_float, color.green_float, color.blue_float)
h = h*h_mult
s = s*s_mult
v = v*v_mult
# suppress Gtk assertion error when value is > 1.0 due to rounding errors
if h > 1.0: h = 1.0
if s > 1.0: s = 1.0
if v > 1.0: v = 1.0
(r,g,b) = hsv.to_rgb(h, s, v)
return Gdk.Color.from_floats(r,g,b)
if (self.light_is_on):
color = self.light_on_color
if self.mouseover:
color = modify_hsv_color(color, 1.0, .5, 1.5)
color2 = modify_hsv_color(color, 1.0, .25, 1.0)
linecolor = modify_hsv_color(color, 1.0, 1.0, .95)
else:
color = self.light_off_color
if self.mouseover:
color = modify_hsv_color(color, 1.0, .5, 1.5)
if (self.dual_color):
color2 = modify_hsv_color(color, 1.0, .25, 1.0)
linecolor = modify_hsv_color(color, 1.0, 1.0, .95)
else:
color1 = modify_hsv_color(color, 1.0, .50, 2)
color2 = modify_hsv_color(color, 1.0, 1.0, .50)
linecolor = modify_hsv_color(color, 1.0, .50, .75)
cr.set_line_width(linewidth)
set_color(linecolor)
cr.stroke_preserve()
if (self.light_is_on == True) or (self.dual_color == True):
gradient_radius = (w2 + h2)
g1 = cairo.RadialGradient(x_center, y_center, 0, x_center, y_center, gradient_radius)
g1.add_color_stop_rgb(0.0, color2.red_float, color2.green_float, color2.blue_float)
g1.add_color_stop_rgb(1.0, color.red_float, color.green_float, color.blue_float)
else:
g1 = cairo.LinearGradient(x, y, x, y + h2)
g1.add_color_stop_rgb(0.0, color1.red_float, color1.green_float, color1.blue_float)
g1.add_color_stop_rgb(0.0, color.red_float, color.green_float, color.blue_float)
g1.add_color_stop_rgb(1.0, color2.red_float, color2.green_float, color2.blue_float)
cr.set_source(g1)
cr.fill()
#Using bitmaps is not implemented. Bitmaps should not be scaled unless you figure out a way to make them scale nicely
#The main reason to use bitmaps would be if someone wants a different look from the default one
#The code below is the basic way to do it. update_widget_size() should use the button size instead of text size
#and there should be properties to set image filenames for each state (light on, light off, mouseover, etc)
else:
cr.save()
image = cairo.ImageSurface.create_from_png('resources/k_green.png')
img_w = image.get_width()
img_h = image.get_height()
print(float(w)/img_w, float(h)/img_h)
cr.set_source_surface(image, 0, 0)
cr.paint()
cr.restore()
# write text
_layout = self.default_pangolayout
if (self.light_is_on):
set_color(self.font_on_color)
if (not self.button_on_text == ""):
_layout = self.on_pangolayout
else:
set_color(self.font_off_color)
fontw, fonth = _layout.get_pixel_size()
cr.move_to((w - fontw)/2, (h - fonth)/2)
PangoCairo.update_layout(cr, _layout)
PangoCairo.show_layout(cr, _layout)
return False
def update_font(self):
if self.font_bold == True:
fontweight = "bold"
else:
fontweight = ""
self.default_pangolayout.set_font_description(Pango.FontDescription(self.font_face + ' ' + fontweight + ' ' + str(self.font_size)))
self.on_pangolayout.set_font_description(Pango.FontDescription(self.font_face + ' ' + fontweight + ' ' + str(self.font_size)))
self.update_widget_size()
def update_widget_size(self):
w1, h1 = self.default_pangolayout.get_pixel_size()
w2, h2 = self.on_pangolayout.get_pixel_size()
width = max(w1 + self.border_width*2, w2 + self.border_width*2)
height = max(h1 + self.border_width*2, h2 + self.border_width*2)
self.set_size_request(int(width), int(height))
# These set_* functions are called by the hal pin callbacks.
# Set self.has_hal_pins = False if you want to control state with these functions directly
#**************************************************************
def set_active(self, active):
self.active = active
self.queue_draw()
def get_active(self):
return self.active
def set_light_on(self, state):
self.light_is_on = state
self.queue_draw()
def get_light_on(self):
return self.light_is_on
def set_text(self, text):
self.button_text = text
self.default_pangolayout.set_text(text, -1)
self.update_widget_size()
def set_on_text(self, text):
self.button_on_text = text
self.on_pangolayout.set_text(text, -1)
self.update_widget_size()
#**************************************************************
def do_get_property(self, property):
name = property.name.replace('-', '_')
if name in list(self.__gproperties.keys()):
return getattr(self, name)
else:
raise AttributeError('unknown property %s' % property.name)
def do_set_property(self, property, value):
name = property.name.replace('-', '_')
if name == 'button_text':
self.set_text(value)
elif name == 'button_on_text':
self.set_on_text(value)
elif name in list(self.__gproperties.keys()):
setattr(self, name, value)
if name in ['font_face', 'font_bold', 'font_size']:
self.update_font()
if name == 'border_width':
self.update_widget_size()
self.queue_draw()
else:
raise AttributeError('unknown property %s' % property.name)
return True
def _hal_init(self):
if (self.has_hal_pins):
_HalWidgetBase._hal_init(self)
self.set_active(False)
if (self.button_halio_pin):
self.button_pin = self.hal.newpin(self.hal_name+'-button', hal.HAL_BIT, hal.HAL_IO)
self.button_pin.connect('value-changed', self.button_pin_update)
else:
self.button_pin = self.hal.newpin(self.hal_name+'-button', hal.HAL_BIT, hal.HAL_OUT)
self.button_pin_not = self.hal.newpin(self.hal_name+'-button-not', hal.HAL_BIT, hal.HAL_OUT)
if (self.create_enable_pin):
self.enable_pin = self.hal.newpin(self.hal_name+'-enable', hal.HAL_BIT, hal.HAL_IN)
self.enable_pin.connect('value-changed', self.enable_pin_update)
self.light_pin = self.hal.newpin(self.hal_name+'-light', hal.HAL_BIT, hal.HAL_IN)
self.light_pin.connect('value-changed', self.light_pin_update)
def button_pin_update(self, hal_pin, data=None):
self.set_active(bool(self.button_pin.get()))
def enable_pin_update(self, hal_pin, data=None):
self.set_sensitive(hal_pin.get())
def light_pin_update(self, hal_pin, data=None):
self.set_light_on(bool(self.light_pin.get()))
GObject.type_register(HAL_LightButton)