apkpy 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apkpy
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A simple framework to build Android apps using Python and CSS-like styling
5
5
  Home-page: https://github.com/teu-usuario/apkpy
6
6
  Author: Martim
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apkpy
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A simple framework to build Android apps using Python and CSS-like styling
5
5
  Home-page: https://github.com/teu-usuario/apkpy
6
6
  Author: Martim
@@ -13,5 +13,6 @@ apkpy_lib/ui.py
13
13
  apkpy_lib/core/compiler.py
14
14
  apkpy_lib/core/style_parser.py
15
15
  apkpy_lib/core/__pycache__/compiler.cpython-313.pyc
16
+ apkpy_lib/core/__pycache__/style_parser.cpython-313.pyc
16
17
  apkpy_lib/templates/android_project/app/src/main/AndroidManifest.xml
17
18
  apkpy_lib/templates/android_project/app/src/main/java/com/exemplo/MainActivity.java
@@ -80,9 +80,17 @@ def build():
80
80
  else:
81
81
  out = os.path.join(build_dir, rel_path)
82
82
 
83
- with open(out, "w", encoding="utf-8") as f:
84
- f.write(content)
85
- click.echo(f" ✅ {os.path.basename(out)}")
83
+ if isinstance(content, tuple) and content[0] == "COPY_FILE":
84
+ source_path = content[1]
85
+ if os.path.exists(source_path):
86
+ shutil.copyfile(source_path, out)
87
+ click.echo(f" 🖼️ {os.path.basename(out)} (copiado de {source_path})")
88
+ else:
89
+ click.echo(f" ⚠️ Aviso: Imagem não encontrada: {source_path}")
90
+ else:
91
+ with open(out, "w", encoding="utf-8") as f:
92
+ f.write(content)
93
+ click.echo(f" ✅ {os.path.basename(out)}")
86
94
 
87
95
  else:
88
96
  # ── Fallback: Single-Screen (MainActivity) ────────────────────────────
@@ -12,12 +12,24 @@ def _parse_radius(style_dict):
12
12
  except (ValueError, TypeError):
13
13
  return 0
14
14
 
15
+ def _parse_border_width(style_dict):
16
+ raw = style_dict.get('border-width', '0')
17
+ cleaned = re.sub(r'[^0-9.]', '', str(raw))
18
+ try:
19
+ return int(float(cleaned))
20
+ except (ValueError, TypeError):
21
+ return 0
22
+
15
23
  def _get_style_bg(style_dict):
16
24
  return style_dict.get('background-color') or style_dict.get('backgroundcolor')
17
25
 
18
26
  def _get_style_fg(style_dict):
19
27
  return style_dict.get('color')
20
28
 
29
+ def _get_style_pressed_color(style_dict):
30
+ val = style_dict.get('pressed-color')
31
+ return _resolve_color(val) if val else None
32
+
21
33
  def _get_style_border_color(style_dict):
22
34
  val = style_dict.get('border-color')
23
35
  if val and val.lower() == 'none':
@@ -36,22 +48,56 @@ def _resolve_color(c):
36
48
  }
37
49
  return mapping.get(c_lower, c)
38
50
 
39
- def _generate_drawable_xml(bg_color, radius_px, border_color=None):
40
- lines = [
41
- '<?xml version="1.0" encoding="utf-8"?>',
42
- '<shape xmlns:android="http://schemas.android.com/apk/res/android">',
43
- f' <solid android:color="{bg_color}" />',
44
- f' <corners android:radius="{radius_px}dp" />'
45
- ]
51
+ def _generate_drawable_xml(bg_color, radius_px, border_color=None, border_width=0, pressed_color=None):
52
+ stroke_w = border_width if border_width > 0 else 1
46
53
 
54
+ stroke_xml = ""
47
55
  if border_color is None:
48
- lines.append(' <stroke android:width="1dp" android:color="#808080" />')
56
+ stroke_xml = f' <stroke android:width="{stroke_w}dp" android:color="#808080" />'
49
57
  elif border_color != 'none':
50
- lines.append(f' <stroke android:width="1dp" android:color="{border_color}" />')
51
-
52
- lines.append(' <padding android:left="10dp" android:top="5dp" android:right="10dp" android:bottom="5dp" />')
53
- lines.append('</shape>')
54
- return "\\n".join(lines)
58
+ stroke_xml = f' <stroke android:width="{stroke_w}dp" android:color="{border_color}" />'
59
+
60
+ padding_xml = ' <padding android:left="10dp" android:top="5dp" android:right="10dp" android:bottom="5dp" />'
61
+
62
+ if pressed_color:
63
+ lines = [
64
+ '<?xml version="1.0" encoding="utf-8"?>',
65
+ '<selector xmlns:android="http://schemas.android.com/apk/res/android">',
66
+ ' <item android:state_pressed="true">',
67
+ ' <shape>',
68
+ f' <solid android:color="{pressed_color}" />',
69
+ f' <corners android:radius="{radius_px}dp" />'
70
+ ]
71
+ if stroke_xml: lines.append(stroke_xml)
72
+ lines.append(padding_xml)
73
+ lines += [
74
+ ' </shape>',
75
+ ' </item>',
76
+ ' <item>',
77
+ ' <shape>',
78
+ f' <solid android:color="{bg_color}" />',
79
+ f' <corners android:radius="{radius_px}dp" />'
80
+ ]
81
+ if stroke_xml: lines.append(stroke_xml)
82
+ lines.append(padding_xml)
83
+ lines += [
84
+ ' </shape>',
85
+ ' </item>',
86
+ '</selector>'
87
+ ]
88
+ return "\n".join(lines)
89
+ else:
90
+ lines = [
91
+ '<?xml version="1.0" encoding="utf-8"?>',
92
+ '<shape xmlns:android="http://schemas.android.com/apk/res/android">',
93
+ f' <solid android:color="{bg_color}" />',
94
+ f' <corners android:radius="{radius_px}dp" />'
95
+ ]
96
+
97
+ if stroke_xml: lines.append(stroke_xml[4:])
98
+ lines.append(padding_xml[4:])
99
+ lines.append('</shape>')
100
+ return "\n".join(lines)
55
101
 
56
102
 
57
103
  # ─── Legacy Single-Screen Transpiler ──────────────────────────────────────────
@@ -86,8 +132,10 @@ class JavaTranspiler(ast.NodeVisitor):
86
132
  bg = _get_style_bg(s)
87
133
  color = _resolve_color(bg)
88
134
  bc = _get_style_border_color(s)
135
+ bw = _parse_border_width(s)
136
+ pc = _get_style_pressed_color(s)
89
137
  drawable_name = f"{button_id}_shape"
90
- self.drawables[f"res/drawable/{drawable_name}.xml"] = _generate_drawable_xml(color, radius, border_color=bc)
138
+ self.drawables[f"res/drawable/{drawable_name}.xml"] = _generate_drawable_xml(color, radius, border_color=bc, border_width=bw, pressed_color=pc)
91
139
  java_code += f'\n {var_name}.setBackgroundResource(R.drawable.{drawable_name});'
92
140
  elif 'background-color' in s or 'backgroundcolor' in s:
93
141
  bg = _get_style_bg(s)
@@ -122,8 +170,10 @@ class JavaTranspiler(ast.NodeVisitor):
122
170
  bg = _get_style_bg(s)
123
171
  color = _resolve_color(bg)
124
172
  bc = _get_style_border_color(s)
173
+ bw = _parse_border_width(s)
174
+ pc = _get_style_pressed_color(s)
125
175
  drawable_name = f"{input_id}_shape"
126
- self.drawables[f"res/drawable/{drawable_name}.xml"] = _generate_drawable_xml(color, radius, border_color=bc)
176
+ self.drawables[f"res/drawable/{drawable_name}.xml"] = _generate_drawable_xml(color, radius, border_color=bc, border_width=bw, pressed_color=pc)
127
177
  style_code += f'\n {var_name}.setBackgroundResource(R.drawable.{drawable_name});'
128
178
  elif 'background-color' in s or 'backgroundcolor' in s:
129
179
  bg = _get_style_bg(s)
@@ -207,6 +257,7 @@ class MultiScreenTranspiler(ast.NodeVisitor):
207
257
  self.styles = styles or {}
208
258
  # var_name (Python) -> screen_id
209
259
  self.screens = {}
260
+ self.screen_bg_images = {}
210
261
  # screen_id -> lista de dicts de componentes
211
262
  self.screen_components = {}
212
263
  # var_name -> dict do componente (para resolver on_click_navigate)
@@ -214,6 +265,7 @@ class MultiScreenTranspiler(ast.NodeVisitor):
214
265
  # lista de {from_screen_id, button_py_var, to_screen_id}
215
266
  self.navigations = []
216
267
  self.start_screen_id = None
268
+ self.functions = {}
217
269
 
218
270
  # ── Helpers ───────────────────────────────────────────────────────────────
219
271
 
@@ -245,6 +297,8 @@ class MultiScreenTranspiler(ast.NodeVisitor):
245
297
  if comp_type == 'button':
246
298
  comp['text'] = call.args[0].value if call.args else ''
247
299
  comp['id'] = self._kw(call.keywords, 'id')
300
+ comp['command'] = self._kw(call.keywords, 'command')
301
+ comp['on_click'] = self._kw(call.keywords, 'on_click')
248
302
  elif comp_type in ('inputs', 'input_field'):
249
303
  comp['placeholder'] = call.args[0].value if call.args else ''
250
304
  comp['id'] = self._kw(call.keywords, 'id')
@@ -266,6 +320,18 @@ class MultiScreenTranspiler(ast.NodeVisitor):
266
320
 
267
321
  # ── Visitors ──────────────────────────────────────────────────────────────
268
322
 
323
+ def visit_FunctionDef(self, node):
324
+ commands = []
325
+ for stmt in node.body:
326
+ if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
327
+ call = stmt.value
328
+ func_id = getattr(call.func, 'id', getattr(call.func, 'attr', ''))
329
+ if func_id == 'toast':
330
+ msg = call.args[0].value if hasattr(call.args[0], 'value') else ""
331
+ commands.append(msg)
332
+ self.functions[node.name] = commands
333
+ self.generic_visit(node)
334
+
269
335
  def visit_Assign(self, node):
270
336
  if not node.targets or not isinstance(node.targets[0], ast.Name):
271
337
  self.generic_visit(node)
@@ -283,7 +349,10 @@ class MultiScreenTranspiler(ast.NodeVisitor):
283
349
  # login_screen = Screen(id="login")
284
350
  if isinstance(func, ast.Name) and func.id == 'Screen':
285
351
  screen_id = self._kw(value.keywords, 'id', var_name)
352
+ bg_image = self._kw(value.keywords, 'background_image')
286
353
  self.screens[var_name] = screen_id
354
+ if bg_image:
355
+ self.screen_bg_images[screen_id] = bg_image
287
356
  self.screen_components.setdefault(screen_id, [])
288
357
 
289
358
  # btn = button("Entrar", screen=login_screen)
@@ -332,10 +401,11 @@ class MultiScreenTranspiler(ast.NodeVisitor):
332
401
  # ─── Java / XML / Manifest generators ────────────────────────────────────────
333
402
 
334
403
  def _generate_activity_java(screen_id, components, navigations, start_screen_id,
335
- package="com.apkpy.app"):
404
+ package="com.apkpy.app", functions_map=None):
336
405
  """Gera o conteúdo completo de um ficheiro *Activity.java para um Screen."""
337
406
 
338
407
  class_name = f"Screen_{screen_id}Activity"
408
+ functions_map = functions_map or {}
339
409
 
340
410
  # Descobrir quais imports são necessários
341
411
  types = {c.get('input_type', '') for c in components if c['comp_type'] in ('inputs', 'input_field')}
@@ -346,6 +416,14 @@ def _generate_activity_java(screen_id, components, navigations, start_screen_id,
346
416
  has_seekbar = 'range' in types
347
417
  has_radio = 'radio' in types
348
418
  has_nav = any(n['from_screen_id'] == screen_id for n in navigations)
419
+
420
+ has_toast = False
421
+ for comp in components:
422
+ if comp['comp_type'] == 'button':
423
+ cmd = comp.get('command') or comp.get('on_click')
424
+ if cmd and cmd in functions_map and functions_map[cmd]:
425
+ has_toast = True
426
+ break
349
427
 
350
428
  imports = [
351
429
  f"import {package}.R;",
@@ -362,6 +440,7 @@ def _generate_activity_java(screen_id, components, navigations, start_screen_id,
362
440
  if has_radio:
363
441
  imports += ["import android.widget.RadioGroup;", "import android.widget.RadioButton;"]
364
442
  if has_nav: imports.append("import android.content.Intent;")
443
+ if has_toast: imports.append("import android.widget.Toast;")
365
444
 
366
445
  lines = []
367
446
  lines.append(f"package {package};")
@@ -450,27 +529,36 @@ def _generate_activity_java(screen_id, components, navigations, start_screen_id,
450
529
  lines.append(f" layout.addView({jv}Group);")
451
530
  lines.append("")
452
531
 
453
- # OnClickListeners de navegação
454
- for nav in navigations:
455
- if nav['from_screen_id'] != screen_id:
532
+ # OnClickListeners (navegação e custom functions combinados)
533
+ for comp in components:
534
+ if comp['comp_type'] != 'button':
456
535
  continue
457
536
 
458
- btn_py_var = nav['button_py_var']
459
- to_class = f"Screen_{nav['to_screen_id']}Activity"
537
+ btn_py_var = comp.get('_py_var')
538
+ btn_java_var = comp.get('_java_var')
539
+ cmd = comp.get('command') or comp.get('on_click')
460
540
 
461
- # Encontrar o Java var do botão pelo Python var
462
- btn_java_var = None
463
- for comp in components:
464
- if comp.get('_py_var') == btn_py_var:
465
- btn_java_var = comp.get('_java_var')
541
+ nav_info = None
542
+ for n in navigations:
543
+ if n['from_screen_id'] == screen_id and n['button_py_var'] == btn_py_var:
544
+ nav_info = n
466
545
  break
467
546
 
468
- if btn_java_var:
547
+ toast_msgs = functions_map.get(cmd, []) if cmd else []
548
+
549
+ if (nav_info or toast_msgs) and btn_java_var:
469
550
  lines.append(f" {btn_java_var}.setOnClickListener(new View.OnClickListener() {{")
470
551
  lines.append(f" @Override")
471
552
  lines.append(f" public void onClick(View v) {{")
472
- lines.append(f" Intent intent = new Intent({class_name}.this, {to_class}.class);")
473
- lines.append(f" startActivity(intent);")
553
+
554
+ for msg in toast_msgs:
555
+ lines.append(f" Toast.makeText(getApplicationContext(), \"{msg}\", Toast.LENGTH_SHORT).show();")
556
+
557
+ if nav_info:
558
+ to_class = f"Screen_{nav_info['to_screen_id']}Activity"
559
+ lines.append(f" Intent intent = new Intent({class_name}.this, {to_class}.class);")
560
+ lines.append(f" startActivity(intent);")
561
+
474
562
  lines.append(f" }}")
475
563
  lines.append(f" }});")
476
564
  lines.append("")
@@ -480,17 +568,36 @@ def _generate_activity_java(screen_id, components, navigations, start_screen_id,
480
568
  return "\n".join(lines)
481
569
 
482
570
 
483
- def _generate_layout_xml(screen_id, components):
571
+ def _generate_layout_xml(screen_id, components, has_bg=False):
484
572
  """Gera o conteúdo de um ficheiro activity_screen_<id>.xml."""
485
- lines = [
486
- '<?xml version="1.0" encoding="utf-8"?>',
487
- '<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"',
488
- ' android:layout_width="match_parent"',
489
- ' android:layout_height="match_parent"',
490
- ' android:orientation="vertical"',
491
- ' android:padding="16dp">',
492
- '',
493
- ]
573
+ lines = ['<?xml version="1.0" encoding="utf-8"?>']
574
+
575
+ if has_bg:
576
+ lines += [
577
+ '<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"',
578
+ ' android:layout_width="match_parent"',
579
+ ' android:layout_height="match_parent">',
580
+ f' <ImageView',
581
+ f' android:layout_width="match_parent"',
582
+ f' android:layout_height="match_parent"',
583
+ f' android:src="@drawable/screen_{screen_id}_bg"',
584
+ f' android:scaleType="centerCrop" />',
585
+ ' <LinearLayout',
586
+ ' android:layout_width="match_parent"',
587
+ ' android:layout_height="match_parent"',
588
+ ' android:orientation="vertical"',
589
+ ' android:padding="16dp">',
590
+ '',
591
+ ]
592
+ else:
593
+ lines += [
594
+ '<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"',
595
+ ' android:layout_width="match_parent"',
596
+ ' android:layout_height="match_parent"',
597
+ ' android:orientation="vertical"',
598
+ ' android:padding="16dp">',
599
+ '',
600
+ ]
494
601
 
495
602
  lbl_n = btn_n = inp_n = 0
496
603
 
@@ -511,7 +618,7 @@ def _generate_layout_xml(screen_id, components):
511
618
 
512
619
  elif ctype == 'button':
513
620
  btn_n += 1
514
- attr_bg = f'\\n android:background="@drawable/{comp["_drawable"]}"' if comp.get('_drawable') else ''
621
+ attr_bg = f'\n android:background="@drawable/{comp["_drawable"]}"' if comp.get('_drawable') else ''
515
622
  lines += [
516
623
  f' <Button',
517
624
  f' android:id="@+id/btn{btn_n}"',
@@ -527,7 +634,7 @@ def _generate_layout_xml(screen_id, components):
527
634
  placeholder = comp.get('placeholder', '')
528
635
 
529
636
  if itype in ('text', 'search'):
530
- attr_bg = f'\\n android:background="@drawable/{comp["_drawable"]}"' if comp.get('_drawable') else ''
637
+ attr_bg = f'\n android:background="@drawable/{comp["_drawable"]}"' if comp.get('_drawable') else ''
531
638
  lines += [
532
639
  f' <EditText',
533
640
  f' android:id="@+id/inp{inp_n}"',
@@ -537,7 +644,7 @@ def _generate_layout_xml(screen_id, components):
537
644
  '',
538
645
  ]
539
646
  elif itype == 'password':
540
- attr_bg = f'\\n android:background="@drawable/{comp["_drawable"]}"' if comp.get('_drawable') else ''
647
+ attr_bg = f'\n android:background="@drawable/{comp["_drawable"]}"' if comp.get('_drawable') else ''
541
648
  lines += [
542
649
  f' <EditText',
543
650
  f' android:id="@+id/inp{inp_n}"',
@@ -581,7 +688,11 @@ def _generate_layout_xml(screen_id, components):
581
688
  ]
582
689
  lines += [' </RadioGroup>', '']
583
690
 
584
- lines.append('</LinearLayout>')
691
+ if has_bg:
692
+ lines.append(' </LinearLayout>')
693
+ lines.append('</RelativeLayout>')
694
+ else:
695
+ lines.append('</LinearLayout>')
585
696
  return "\n".join(lines)
586
697
 
587
698
 
@@ -668,20 +779,27 @@ def translate_python_to_android(python_code, package="com.apkpy.app"):
668
779
  bg = _get_style_bg(styles[comp_id])
669
780
  color = _resolve_color(bg)
670
781
  bc = _get_style_border_color(styles[comp_id])
782
+ bw = _parse_border_width(styles[comp_id])
783
+ pc = _get_style_pressed_color(styles[comp_id])
671
784
  drawable_name = f"{comp_id}_shape"
672
785
  comp['_drawable'] = drawable_name
673
- output_files[f"res/drawable/{drawable_name}.xml"] = _generate_drawable_xml(color, radius, border_color=bc)
786
+ output_files[f"res/drawable/{drawable_name}.xml"] = _generate_drawable_xml(color, radius, border_color=bc, border_width=bw, pressed_color=pc)
674
787
 
675
788
  # Java Activity
676
789
  java_content = _generate_activity_java(
677
- sid, components, transpiler.navigations, start_screen_id, package
790
+ sid, components, transpiler.navigations, start_screen_id, package, transpiler.functions
678
791
  )
679
792
  output_files[f"java/Screen_{sid}Activity.java"] = java_content
680
793
 
681
794
  # XML Layout
682
- xml_content = _generate_layout_xml(sid, components)
795
+ bg_image = transpiler.screen_bg_images.get(sid)
796
+ xml_content = _generate_layout_xml(sid, components, bool(bg_image))
683
797
  output_files[f"res/layout/activity_screen_{sid}.xml"] = xml_content
684
798
 
799
+ if bg_image:
800
+ ext = bg_image.split('.')[-1]
801
+ output_files[f"res/drawable/screen_{sid}_bg.{ext}"] = ("COPY_FILE", bg_image)
802
+
685
803
  # AndroidManifest
686
804
  output_files["AndroidManifest.xml"] = _generate_manifest(
687
805
  screen_ids, start_screen_id, package
@@ -35,6 +35,10 @@ def _get_style_fg(style_dict):
35
35
  """Lê a cor do texto."""
36
36
  return style_dict.get('color')
37
37
 
38
+ def _get_style_pressed_color(style_dict):
39
+ """Lê a cor de fundo ao pressionar o botão."""
40
+ return style_dict.get('pressed-color')
41
+
38
42
  def _get_style_border_color(style_dict):
39
43
  """Lê a cor da borda, ou 'none'."""
40
44
  val = style_dict.get('border-color')
@@ -51,6 +55,15 @@ def _parse_radius(style_dict):
51
55
  except (ValueError, TypeError):
52
56
  return 0
53
57
 
58
+ def _parse_border_width(style_dict):
59
+ """Extrai a largura da borda (ex: '2px' -> 2)."""
60
+ raw = style_dict.get('border-width', '0')
61
+ cleaned = re.sub(r'[^0-9.]', '', str(raw))
62
+ try:
63
+ return int(float(cleaned))
64
+ except (ValueError, TypeError):
65
+ return 0
66
+
54
67
  def _create_rounded_rect(canvas, x1, y1, x2, y2, r, **kwargs):
55
68
  """Desenha retângulo com cantos arredondados no Canvas."""
56
69
  r = max(0, min(r, (x2 - x1) // 2, (y2 - y1) // 2))
@@ -92,8 +105,9 @@ class Screen:
92
105
  run(start_screen=login)
93
106
  """
94
107
 
95
- def __init__(self, id):
108
+ def __init__(self, id, background_image=None):
96
109
  self.id = id
110
+ self.background_image = background_image
97
111
  self.components = [] # lista de ComponentDef
98
112
 
99
113
  def _add(self, comp_def):
@@ -155,11 +169,12 @@ class AppPreview:
155
169
  lbl.pack(pady=4, fill="x")
156
170
  comp.widget = lbl
157
171
 
158
- def _draw_rounded_button(self, text, bg, fg, radius, command, nav_target, comp=None, border_color=None):
172
+ def _draw_rounded_button(self, text, bg, fg, radius, command, nav_target, comp=None, border_color=None, border_width=0, pressed_color=None):
159
173
  fill_color = bg if bg else "#d9d9d9"
174
+ p_color = pressed_color if pressed_color else fill_color
160
175
  text_color = fg if fg else "black"
161
176
  outline_c = border_color if border_color else ""
162
- outline_w = 1 if outline_c else 0
177
+ outline_w = border_width if border_width > 0 else (1 if outline_c else 0)
163
178
 
164
179
  frame = tk.Frame(self.layout, bg=self.layout.cget("bg"))
165
180
  frame.pack(pady=10, fill="x")
@@ -172,16 +187,22 @@ class AppPreview:
172
187
 
173
188
  def _redraw(event, c=canvas, t=text, fc=fill_color, tc=text_color, r=radius, ol=outline_c, ow=outline_w):
174
189
  c.delete("all")
175
- w, h = event.width, event.height
190
+ w, h = c.winfo_width(), c.winfo_height()
191
+ if w <= 1: w, h = event.width, event.height
176
192
  _create_rounded_rect(c, 2, 2, w - 2, h - 2, r, fill=fc, outline=ol, width=ow)
177
193
  c.create_text(w // 2, h // 2, text=t, fill=tc, font=("Arial", 12, "bold"))
178
194
 
179
- def _on_click(event, nt=nav_target, uc=command):
180
- if uc: uc()
181
- if nt: self.render_screen(nt)
195
+ def _on_press(event, c=canvas, t=text, fc=p_color, tc=text_color, r=radius, ol=outline_c, ow=outline_w):
196
+ _redraw(event, c, t, fc, tc, r, ol, ow)
197
+
198
+ def _on_release(event, c=canvas, t=text, fc=fill_color, tc=text_color, r=radius, ol=outline_c, ow=outline_w):
199
+ _redraw(event, c, t, fc, tc, r, ol, ow)
200
+ if command: command()
201
+ if nav_target: self.render_screen(nav_target)
182
202
 
183
203
  canvas.bind("<Configure>", _redraw)
184
- canvas.bind("<Button-1>", _on_click)
204
+ canvas.bind("<ButtonPress-1>", _on_press)
205
+ canvas.bind("<ButtonRelease-1>", _on_release)
185
206
 
186
207
  if comp is not None: comp.widget = canvas
187
208
  return canvas
@@ -192,17 +213,20 @@ class AppPreview:
192
213
  style = global_styles.get(comp_id, {}) if comp_id else {}
193
214
  bg = _get_style_bg(style)
194
215
  fg = _get_style_fg(style)
216
+ pressed_bg = _get_style_pressed_color(style)
195
217
  radius = _parse_radius(style)
196
218
  bc = _get_style_border_color(style)
219
+ bw = _parse_border_width(style)
197
220
  user_cmd = comp.kwargs.get('command')
198
221
  nav_target = comp.nav_target
199
222
 
200
223
  if radius > 0:
201
- self._draw_rounded_button(text, bg, fg, radius, user_cmd, nav_target, comp, bc)
224
+ self._draw_rounded_button(text, bg, fg, radius, user_cmd, nav_target, comp, bc, bw, pressed_color=pressed_bg)
202
225
  return
203
226
 
204
227
  tk_bg = bg if bg else "SystemButtonFace"
205
228
  tk_fg = fg if fg else "black"
229
+ tk_abg = pressed_bg if pressed_bg else "SystemButtonHighlight"
206
230
 
207
231
  def on_click(nt=nav_target, uc=user_cmd):
208
232
  if uc: uc()
@@ -210,7 +234,7 @@ class AppPreview:
210
234
 
211
235
  btn = tk.Button(
212
236
  self.layout, text=text, font=("Arial", 12), command=on_click,
213
- bg=tk_bg, fg=tk_fg, relief="raised", pady=5
237
+ bg=tk_bg, fg=tk_fg, activebackground=tk_abg, relief="raised", pady=5
214
238
  )
215
239
  btn.pack(pady=10, fill="x")
216
240
  comp.widget = btn
@@ -225,7 +249,8 @@ class AppPreview:
225
249
  bg=_get_style_bg(style),
226
250
  fg=_get_style_fg(style),
227
251
  radius=_parse_radius(style),
228
- border_color=_get_style_border_color(style)
252
+ border_color=_get_style_border_color(style),
253
+ border_width=_parse_border_width(style)
229
254
  )
230
255
 
231
256
  # ── Legacy (sem Screen) ───────────────────────────────────────────────────
@@ -241,21 +266,22 @@ class AppPreview:
241
266
  )
242
267
  lbl.pack(pady=4, fill="x")
243
268
 
244
- def add_button(self, text, command=None, bg=None, fg=None, radius=0, border_color=None):
269
+ def add_button(self, text, command=None, bg=None, fg=None, radius=0, border_color=None, border_width=0, pressed_color=None):
245
270
  if radius > 0:
246
- self._draw_rounded_button(text, bg, fg, radius, command, None, border_color=border_color)
271
+ self._draw_rounded_button(text, bg, fg, radius, command, None, border_color=border_color, border_width=border_width, pressed_color=pressed_color)
247
272
  return
248
273
  tk_bg = bg if bg else "SystemButtonFace"
249
274
  tk_fg = fg if fg else "black"
275
+ tk_abg = pressed_color if pressed_color else "SystemButtonHighlight"
250
276
  btn = tk.Button(
251
277
  self.layout, text=text, font=("Arial", 12), command=command,
252
- bg=tk_bg, fg=tk_fg, relief="raised", pady=5
278
+ bg=tk_bg, fg=tk_fg, activebackground=tk_abg, relief="raised", pady=5
253
279
  )
254
280
  if border_color and border_color != "":
255
- btn.config(highlightbackground=border_color, highlightthickness=1)
281
+ btn.config(highlightbackground=border_color, highlightthickness=border_width if border_width > 0 else 1)
256
282
  btn.pack(pady=10, fill="x")
257
283
 
258
- def add_input(self, placeholder="", input_type="text", bg=None, fg=None, radius=0, border_color=None):
284
+ def add_input(self, placeholder="", input_type="text", bg=None, fg=None, radius=0, border_color=None, border_width=0):
259
285
  """Adiciona um campo de input consoante o tipo pedido."""
260
286
 
261
287
  # ── TEXT / PASSWORD / SEARCH ──────────────────────────────────────────
@@ -268,7 +294,7 @@ class AppPreview:
268
294
  show_char = "*" if input_type == "password" else ""
269
295
 
270
296
  ol_color = border_color if border_color is not None else "grey"
271
- ol_width = 1 if ol_color else 0
297
+ ol_width = border_width if border_width > 0 else 1 if ol_color else 0
272
298
 
273
299
  if radius > 0:
274
300
  canvas = tk.Canvas(
@@ -447,9 +473,11 @@ def button(text, id=None, command=None, screen=None):
447
473
  style = global_styles.get(id, {})
448
474
  bg = _get_style_bg(style)
449
475
  fg = _get_style_fg(style)
476
+ pressed_bg = _get_style_pressed_color(style)
450
477
  radius = _parse_radius(style)
451
478
  bc = _get_style_border_color(style)
452
- _app.add_button(text, command, bg, fg, radius=radius, border_color=bc)
479
+ bw = _parse_border_width(style)
480
+ _app.add_button(text, command, bg, fg, radius=radius, border_color=bc, border_width=bw, pressed_color=pressed_bg)
453
481
  return None
454
482
 
455
483
 
@@ -483,7 +511,8 @@ def inputs(placeholder="", id=None, type="text", screen=None):
483
511
  fg = _get_style_fg(style)
484
512
  radius = _parse_radius(style)
485
513
  bc = _get_style_border_color(style)
486
- _app.add_input(placeholder=placeholder, input_type=type, bg=bg, fg=fg, radius=radius, border_color=bc)
514
+ bw = _parse_border_width(style)
515
+ _app.add_input(placeholder=placeholder, input_type=type, bg=bg, fg=fg, radius=radius, border_color=bc, border_width=bw)
487
516
  return None
488
517
 
489
518
 
@@ -503,4 +532,13 @@ def run(start_screen=None):
503
532
  load_styles_now()
504
533
  if start_screen is not None:
505
534
  _app.render_screen(start_screen)
506
- _app.run()
535
+ _app.run()
536
+
537
+ def toast(message):
538
+ """Exibe uma mensagem flutuante temporária na interface."""
539
+ lbl = tk.Label(
540
+ _app.root, text=message, bg="#333333", fg="white",
541
+ font=("Arial", 11), padx=15, pady=8
542
+ )
543
+ lbl.place(relx=0.5, rely=0.85, anchor="s")
544
+ _app.root.after(2000, lbl.destroy)
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='apkpy',
5
- version='0.2.2', # GARANTE que está 0.1.1
5
+ version='0.2.3', # GARANTE que está 0.1.1
6
6
  author='Martim',
7
7
  description='A simple framework to build Android apps using Python and CSS-like styling',
8
8
  long_description=open('README.md', encoding='utf-8').read(), # ADICIONA O ENCODING AQUI
File without changes
File without changes
File without changes
File without changes
File without changes