Skip to content

Commit 35b9332

Browse files
committed
Port Calculate activity to GTK4
This commit fully ports the Calculate activity to GTK4 and Sugar4. - Port 'shareable_activity.py' with updated DBus and Sugar4 base classes. - Rework 'layout.py' to use GTK4 box packing ('append', 'set_child') instead of deprecated containers like 'Gtk.Table'. - Replace legacy per-widget styling ('modify_bg', 'modify_font') with semantic CSS classes ('calc-button', 'calc-numpad-btn', 'calc-operator-btn', 'calc-action-btn') to be painted by sugar-artwork's GTK4 theme. - Use 'sugar_style.apply_css_to_widget()' for dynamic per-user XoColor styling on equation history and variable text views. - Replace StackSwitcher with proper Sugar4 ToolbarBox architecture using ToolbarButton with icon resolution (local SVG fallback). - Add EventControllerKey on text_entry and calculator buttons. - Refactor 'toolbars.py' to gracefully render GTK4 toolbar items and correctly update text fallbacks when SVG icons are invalid. - Port 'calculate.py' to use 'Gtk.EventControllerKey' for keystrokes and update clipboard operations to use GTK4 async mechanisms. - Include 'main.py' standalone bootstrapper for isolated GTK4 testing.
1 parent f3bedcf commit 35b9332

File tree

6 files changed

+479
-440
lines changed

6 files changed

+479
-440
lines changed

calculate.py

Lines changed: 143 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@
2626
_logger = logging.getLogger('Calculate')
2727

2828
import gi
29-
gi.require_version('Gtk', '3.0')
29+
gi.require_version('Gtk', '4.0')
3030
from gi.repository import Gtk
3131
from gi.repository import Gdk
3232
import base64
3333

34-
import sugar3.profile
35-
from sugar3.graphics.xocolor import XoColor
34+
import sugar4.profile
35+
from sugar4.graphics.xocolor import XoColor
36+
from sugar4.graphics import style as sugar_style
3637

3738
from shareable_activity import ShareableActivity
3839
from layout import CalcLayout
@@ -64,8 +65,7 @@ def findchar(text, chars, ofs=0):
6465

6566
def _textview_realize_cb(widget):
6667
'''Change textview properties once window is created.'''
67-
win = widget.get_window(Gtk.TextWindowType.TEXT)
68-
win.set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND1))
68+
widget.set_cursor_from_name("pointer")
6969
return False
7070

7171

@@ -194,7 +194,7 @@ def create_lasteq_textbuf(self):
194194
tagsmallnarrow = buf.create_tag(font=CalcLayout.FONT_SMALL_NARROW)
195195
tagbignarrow = buf.create_tag(font=CalcLayout.FONT_BIG_NARROW)
196196
tagbigger = buf.create_tag(font=CalcLayout.FONT_BIGGER)
197-
tagjustright = buf.create_tag(justification=Gtk.Justification.RIGHT)
197+
tagjustright = buf.create_tag(justification=Gtk.Justification.LEFT)
198198
tagred = buf.create_tag(foreground='#FF0000')
199199

200200
# Add label and equation
@@ -244,32 +244,50 @@ def create_history_object(self):
244244
return self.result.get_image()
245245

246246
w = Gtk.TextView()
247-
w.modify_base(
248-
Gtk.StateType.NORMAL, Gdk.color_parse(self.color.get_fill_color()))
249-
w.modify_bg(
250-
Gtk.StateType.NORMAL,
251-
Gdk.color_parse(self.color.get_stroke_color()))
247+
w.set_editable(False)
248+
w.set_cursor_visible(False)
249+
250+
fill_color = self.color.get_fill_color()
251+
stroke_color = self.color.get_stroke_color()
252+
253+
css_str = """
254+
textview {
255+
background-color: %s;
256+
color: %s;
257+
}
258+
textview text {
259+
background-color: %s;
260+
color: %s;
261+
min-height: 20px;
262+
}
263+
""" % (stroke_color, fill_color, stroke_color, fill_color)
264+
265+
sugar_style.apply_css_to_widget(w, css_str)
266+
252267
w.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
253-
w.set_border_window_size(Gtk.TextWindowType.LEFT, 4)
254-
w.set_border_window_size(Gtk.TextWindowType.RIGHT, 4)
255-
w.set_border_window_size(Gtk.TextWindowType.TOP, 4)
256-
w.set_border_window_size(Gtk.TextWindowType.BOTTOM, 4)
268+
w.set_left_margin(4)
269+
w.set_right_margin(4)
270+
w.set_top_margin(4)
271+
w.set_bottom_margin(4)
257272
w.connect('realize', _textview_realize_cb)
258273
buf = w.get_buffer()
259274

260275
tagsmall = buf.create_tag(font=CalcLayout.FONT_SMALL)
261276
tagsmallnarrow = buf.create_tag(font=CalcLayout.FONT_SMALL_NARROW)
262277
tagbig = buf.create_tag(font=CalcLayout.FONT_BIG,
263-
justification=Gtk.Justification.RIGHT)
264-
# TODO Fix for old Sugar 0.82 builds, red_float not available
265-
bright = (
266-
Gdk.color_parse(self.color.get_fill_color()).red_float +
267-
Gdk.color_parse(self.color.get_fill_color()).green_float +
268-
Gdk.color_parse(self.color.get_fill_color()).blue_float) / 3.0
269-
if bright < 0.5:
270-
col = 'white'
278+
justification=Gtk.Justification.LEFT)
279+
280+
# Calculate brightness
281+
c = Gdk.RGBA()
282+
if c.parse(fill_color):
283+
bright = (c.red + c.green + c.blue) / 3.0
284+
if bright < 0.5:
285+
col = 'white'
286+
else:
287+
col = 'black'
271288
else:
272289
col = 'black'
290+
273291
tagcolor = buf.create_tag(foreground=col)
274292

275293
# Add label, equation and result
@@ -279,13 +297,22 @@ def create_history_object(self):
279297
eqnstr = '%s\n' % str(self.equation)
280298
self.append_with_superscript_tags(buf, eqnstr, tagsmall)
281299

282-
resstr = self.ml.format_number(self.result)
283-
resstr = str(resstr).rstrip('0').rstrip('.') \
284-
if '.' in resstr else resstr
300+
try:
301+
resstr = self.ml.format_number(self.result)
302+
except Exception:
303+
resstr = str(self.result)
304+
305+
resstr = str(resstr)
306+
resstr = resstr.rstrip('0').rstrip('.') if '.' in resstr else resstr
307+
308+
if not resstr or resstr == "None":
309+
resstr = "0"
310+
285311
if len(resstr) > 30:
286312
restag = tagsmall
287313
else:
288314
restag = tagbig
315+
289316
self.append_with_superscript_tags(buf, resstr, restag)
290317

291318
buf.apply_tag(tagcolor, buf.get_start_iter(), buf.get_end_iter())
@@ -374,23 +401,28 @@ def __init__(self, handle):
374401
self.KEYMAP['divide'] = self.ml.div_sym
375402
self.KEYMAP['equal'] = self.ml.equ_sym
376403

377-
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
404+
self.clipboard = Gdk.Display.get_default().get_clipboard()
378405
self.select_reason = self.SELECT_SELECT
379406
self.buffer = ""
380407
self.showing_version = 0
381408
self.showing_error = False
382409
self.ans_inserted = False
383410
self.show_vars = False
384411

385-
self.connect("key_press_event", self.keypress_cb)
386-
self.connect("destroy", self.cleanup_cb)
387-
self.color = sugar3.profile.get_color()
412+
key_controller = Gtk.EventControllerKey()
413+
key_controller.connect("key-pressed", self.keypress_cb)
414+
self.add_controller(key_controller)
415+
416+
# self.connect("destroy", self.cleanup_cb) # handled by activity?
417+
418+
self.color = sugar4.profile.get_color()
388419

389420
self.layout = CalcLayout(self)
390421
self.label_entry = self.layout.label_entry
391422
self.text_entry = self.layout.text_entry
392423
self.last_eq_sig = None
393424
self.last_eqn_textview = None
425+
self.last_eqn_controller = None
394426

395427
self.reset()
396428
self.layout.show_it()
@@ -399,7 +431,7 @@ def __init__(self, handle):
399431

400432
self.parser.log_debug_info()
401433

402-
def ignore_key_cb(self, widget, event):
434+
def ignore_key_cb(self, controller, keyval, keycode, state):
403435
return True
404436

405437
def cleanup_cb(self, arg):
@@ -427,14 +459,15 @@ def equation_pressed_cb(self, eqn):
427459
def set_last_equation(self, eqn):
428460
"""Set the 'last equation' TextView."""
429461

430-
if self.last_eq_sig is not None:
431-
self.layout.last_eq.disconnect(self.last_eq_sig)
432-
self.last_eq_sig = None
462+
if self.last_eqn_controller is not None:
463+
self.layout.last_eq.remove_controller(self.last_eqn_controller)
464+
self.last_eqn_controller = None
433465

434466
if not isinstance(eqn.result, ParserError):
435-
self.last_eq_sig = self.layout.last_eq.connect(
436-
'button-press-event',
437-
lambda a1, a2, e: self.equation_pressed_cb(e), eqn)
467+
self.last_eqn_controller = Gtk.GestureClick.new()
468+
self.last_eqn_controller.set_button(0)
469+
self.last_eqn_controller.connect('pressed', lambda gesture, n_press, x, y: self.equation_pressed_cb(eqn))
470+
self.layout.last_eq.add_controller(self.last_eqn_controller)
438471

439472
self.layout.last_eq.set_buffer(eqn.create_lasteq_textbuf())
440473

@@ -490,8 +523,10 @@ def add_equation(self, eq, prepend=False, drawlasteq=False, tree=None):
490523

491524
own = (eq.owner == self.get_owner_id())
492525
w = eq.create_history_object()
493-
w.connect('button-press-event', lambda w,
494-
e: self.equation_pressed_cb(eq))
526+
click_gesture = Gtk.GestureClick.new()
527+
click_gesture.set_button(0)
528+
click_gesture.connect('pressed', lambda gesture, n_press, x, y: self.equation_pressed_cb(eq))
529+
w.add_controller(click_gesture)
495530
if drawlasteq:
496531
self.set_last_equation(eq)
497532

@@ -551,7 +586,7 @@ def process(self):
551586
self.get_owner_id(), ml=self.ml)
552587
self.set_error_equation(eqn)
553588
else:
554-
eqn = Equation(label, _n(s), _n(str(res)), self.color,
589+
eqn = Equation(label, _n(s), res, self.color,
555590
self.get_owner_id(), ml=self.ml)
556591
self.add_equation(eqn, drawlasteq=True, tree=tree)
557592
self.send_message("add_eq", value=str(eqn))
@@ -576,28 +611,38 @@ def create_var_textview(self, name, value):
576611
if name in reserved:
577612
return None
578613
w = Gtk.TextView()
579-
w.modify_base(
580-
Gtk.StateType.NORMAL, Gdk.color_parse(self.color.get_fill_color()))
581-
w.modify_bg(
582-
Gtk.StateType.NORMAL,
583-
Gdk.color_parse(self.color.get_stroke_color()))
614+
fill_color = self.color.get_fill_color()
615+
stroke_color = self.color.get_stroke_color()
616+
617+
css_str = """
618+
textview {
619+
background-color: %s;
620+
color: %s;
621+
}
622+
textview text {
623+
background-color: %s;
624+
color: %s;
625+
}
626+
""" % (fill_color, stroke_color, fill_color, stroke_color)
627+
sugar_style.apply_css_to_widget(w, css_str)
628+
584629
w.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
585-
w.set_border_window_size(Gtk.TextWindowType.LEFT, 4)
586-
w.set_border_window_size(Gtk.TextWindowType.RIGHT, 4)
587-
w.set_border_window_size(Gtk.TextWindowType.TOP, 4)
588-
w.set_border_window_size(Gtk.TextWindowType.BOTTOM, 4)
630+
w.set_left_margin(4)
631+
w.set_right_margin(4)
632+
w.set_top_margin(4)
633+
w.set_bottom_margin(4)
589634
w.connect('realize', _textview_realize_cb)
590635
buf = w.get_buffer()
591636

592-
# TODO Fix for old Sugar 0.82 builds, red_float not available
593-
bright = (
594-
Gdk.color_parse(self.color.get_fill_color()).red_float +
595-
Gdk.color_parse(self.color.get_fill_color()).green_float +
596-
Gdk.color_parse(self.color.get_fill_color()).blue_float) / 3.0
597-
if bright < 0.5:
598-
col = Gdk.color_parse('white')
637+
c = Gdk.RGBA()
638+
if c.parse(fill_color):
639+
bright = (c.red + c.green + c.blue) / 3.0
640+
if bright < 0.5:
641+
col = 'white'
642+
else:
643+
col = 'black'
599644
else:
600-
col = Gdk.color_parse('black')
645+
col = 'black'
601646

602647
tag = buf.create_tag(font=CalcLayout.FONT_SMALL_NARROW,
603648
foreground=col)
@@ -792,67 +837,66 @@ def expand_selection(self, dir):
792837

793838
def text_copy(self):
794839
if self.layout.graph_selected is not None:
795-
self.clipboard.set_image(
796-
self.layout.graph_selected.get_child().get_pixbuf())
797-
self.layout.toggle_select_graph(self.layout.graph_selected)
840+
# TODO: Port image copying for GTK4 (requires Texture)
841+
pass
842+
# texture = Gdk.Texture.new_for_pixbuf(self.layout.graph_selected.get_child().get_pixbuf())
843+
# self.clipboard.set_texture(texture)
844+
# self.layout.toggle_select_graph(self.layout.graph_selected)
798845
else:
799846
str = self.text_entry.get_text()
800847
sel = self.text_entry.get_selection_bounds()
801848
# _logger.info('text_copy, sel: %r, str: %s', sel, str)
802849
if len(sel) == 2:
803850
(start, end) = sel
804-
self.clipboard.set_text(str[start:end], -1)
851+
self.clipboard.set_text(str[start:end])
805852

806853
def text_select_all(self):
807854
end = self.text_entry.get_text_length()
808855
self.text_entry.select_region(0, end)
809856

810-
def get_clipboard_text(self):
811-
text = self.clipboard.wait_for_text()
812-
if text is None:
813-
return ""
814-
else:
815-
return text
816-
817857
def text_paste(self):
818-
self.button_pressed(self.TYPE_TEXT, self.get_clipboard_text())
858+
self.clipboard.read_text_async(None, self._on_paste_text_received, None)
859+
860+
def _on_paste_text_received(self, clipboard, result, user_data):
861+
try:
862+
text = clipboard.read_text_finish(result)
863+
if text:
864+
self.button_pressed(self.TYPE_TEXT, text)
865+
except Exception as e:
866+
_logger.error("Error pasting text: %s", e)
819867

820868
def text_cut(self):
821869
self.text_copy()
822870
self.remove_character(1)
823871

824-
def keypress_cb(self, widget, event):
825-
if not self.text_entry.is_focus():
826-
return
827-
828-
key = Gdk.keyval_name(event.keyval)
829-
if event.hardware_keycode == 219:
830-
if (event.get_state() & Gdk.ModifierType.SHIFT_MASK):
831-
key = 'divide'
832-
else:
833-
key = 'multiply'
834-
_logger.debug('Key: %s (%r, %r)', key,
835-
event.keyval, event.hardware_keycode)
836-
837-
if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
838-
if key in self.CTRL_KEYMAP:
839-
f = self.CTRL_KEYMAP[key]
840-
return f(self)
841-
elif (event.get_state() & Gdk.ModifierType.SHIFT_MASK) and \
842-
key in self.SHIFT_KEYMAP:
843-
f = self.SHIFT_KEYMAP[key]
844-
return f(self)
845-
elif str(key) in self.IDENTIFIER_CHARS:
846-
self.button_pressed(self.TYPE_TEXT, key)
847-
elif key in self.KEYMAP:
848-
f = self.KEYMAP[key]
849-
if isinstance(f, str) or \
850-
isinstance(f, str):
851-
self.button_pressed(self.TYPE_TEXT, f)
872+
def keypress_cb(self, controller, keyval, keycode, state):
873+
keyname = Gdk.keyval_name(keyval)
874+
875+
is_ctrl = (state & Gdk.ModifierType.CONTROL_MASK)
876+
is_shift = (state & Gdk.ModifierType.SHIFT_MASK) and not is_ctrl
877+
878+
if is_ctrl:
879+
if keyname in self.CTRL_KEYMAP:
880+
self.CTRL_KEYMAP[keyname](self)
881+
return True
882+
elif is_shift:
883+
if keyname in self.SHIFT_KEYMAP:
884+
self.SHIFT_KEYMAP[keyname](self)
885+
return True
886+
887+
if "KP_" in keyname:
888+
if keyname == "KP_Enter":
889+
keyname = "Return"
890+
891+
if keyname in self.KEYMAP:
892+
action = self.KEYMAP[keyname]
893+
if callable(action):
894+
action(self)
852895
else:
853-
return f(self)
896+
self.button_pressed(self.TYPE_TEXT, action)
897+
return True
854898

855-
return True
899+
return False
856900

857901
def get_older(self):
858902
self.showing_version = max(0, self.showing_version - 1)
@@ -970,14 +1014,3 @@ def format_insert_ans(self):
9701014
return self.ml.format_number(ans)
9711015
else:
9721016
return ''
973-
974-
975-
def main():
976-
win = Gtk.Window(Gtk.WindowType.TOPLEVEL)
977-
Calculate(win)
978-
Gtk.main()
979-
return 0
980-
981-
982-
if __name__ == "__main__":
983-
main()

0 commit comments

Comments
 (0)