pgwidgets-python 0.3.3__py3-none-any.whl → 0.3.5__py3-none-any.whl

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.
@@ -434,6 +434,14 @@ class Session:
434
434
  widget._state[spec[0]] = tuple(args)
435
435
  else:
436
436
  widget._state[spec] = args[0]
437
+ # ComboBox: a value committed with Enter in the browser may be
438
+ # a new entry the browser appended to its list; mirror that
439
+ # append so our _items (and thus get_index) stays consistent.
440
+ if (widget._js_class == "ComboBox" and action == "activated"
441
+ and len(args) >= 2):
442
+ items = widget._state.setdefault("_items", [])
443
+ if args[1] not in items:
444
+ items.append(args[1])
437
445
 
438
446
  # Dialog autoclose: the JS side auto-hides when a button is
439
447
  # clicked. Sync that to Python so reconstruction doesn't
@@ -209,6 +209,10 @@ class FileBrowser(Callbacks):
209
209
 
210
210
  # ── Public API ──────────────────────────────────────────────
211
211
 
212
+ def set_title(self, title):
213
+ """Set the dialog's title bar text."""
214
+ self._dialog.set_title(title)
215
+
212
216
  def set_mode(self, mode):
213
217
  """Set the selection mode: 'file', 'files', 'directory', 'save'."""
214
218
  self._mode = mode
pgwidgets/method_types.py CHANGED
@@ -62,7 +62,8 @@ JS_ONLY_METHODS = {
62
62
  # MDIWidget
63
63
  "get_subwin", "get_subwindows", "get_configuration",
64
64
  "get_child_size", "get_child_position",
65
- # Button icon (set_icon is an action, so get_icon must round-trip)
65
+ # Button icon: set_icon stores state (so it reconstructs); get_icon
66
+ # reads the live browser value, so it round-trips.
66
67
  "get_icon",
67
68
  }
68
69
 
@@ -107,7 +108,7 @@ ACTION_METHODS = {
107
108
  # Timer
108
109
  "start", "cancel", "set", "cond_set",
109
110
  # Table/Tree row-level modifications (tracked via bulk set_data)
110
- "add_item", "remove_item", "update_tree", "remove_items",
111
+ "add_item", "remove_item", "update_tree", "remove_items", "delete_tree",
111
112
  "insert_row", "append_row", "delete_row",
112
113
  "insert_column", "append_column", "delete_column",
113
114
  "set_cell",
@@ -121,8 +122,6 @@ ACTION_METHODS = {
121
122
  "set_minimum_size",
122
123
  # TreeView per-column
123
124
  "set_column_width", "set_column_editable",
124
- # TextSource per-line
125
- "set_icon",
126
125
  }
127
126
 
128
127
  # Setter methods that DON'T follow the set_* naming convention.
@@ -420,7 +419,34 @@ BINARY_STATE_KEYS = {
420
419
  }
421
420
 
422
421
 
422
+ def _combobox_get_index(self):
423
+ """Index of the item matching the current text, or -1 when the text is
424
+ not one of the offerings (e.g. a value typed into an editable combo box
425
+ but not yet committed with Enter). Mirrors the JS get_index(), which
426
+ is items.indexOf(input value), and stays consistent with get_text()
427
+ (the cached entry text, kept live by the 'modified' callback)."""
428
+ items = self._state.get("_items", [])
429
+ try:
430
+ return items.index(self._state.get("text", ""))
431
+ except ValueError:
432
+ return -1
433
+
434
+
435
+ def _combobox_set_index(self, idx):
436
+ """Select the item at *idx*, keeping the cached text consistent with
437
+ the index so get_text() agrees with get_index() after a programmatic
438
+ change (the generic setter would leave 'text' stale)."""
439
+ items = self._state.get("_items", [])
440
+ if 0 <= idx < len(items):
441
+ self._state["text"] = items[idx]
442
+ self._state["index"] = idx
443
+ self._user_set_state.add("index")
444
+ return self._call("set_index", idx)
445
+
446
+
423
447
  CUSTOM_METHODS = {
448
+ ("ComboBox", "get_index"): _combobox_get_index,
449
+ ("ComboBox", "set_index"): _combobox_set_index,
424
450
  ("TabWidget", "index_to_widget"): _index_to_widget,
425
451
  ("TabWidget", "index_of"): _index_of,
426
452
  ("StackWidget", "index_to_widget"): _index_to_widget,
@@ -524,6 +524,14 @@ class Session:
524
524
  widget._state[spec[0]] = tuple(args)
525
525
  else:
526
526
  widget._state[spec] = args[0]
527
+ # ComboBox: a value committed with Enter in the browser may be
528
+ # a new entry the browser appended to its list; mirror that
529
+ # append so our _items (and thus get_index) stays consistent.
530
+ if (widget._js_class == "ComboBox" and action == "activated"
531
+ and len(args) >= 2):
532
+ items = widget._state.setdefault("_items", [])
533
+ if args[1] not in items:
534
+ items.append(args[1])
527
535
  # Dialog autoclose: the JS side auto-hides when a button is
528
536
  # clicked. Sync that to Python so reconstruction doesn't
529
537
  # re-show a dialog that was autoclosed.
@@ -1399,6 +1407,18 @@ class Session:
1399
1407
  # callbacks transferred by _transfer_proxy during step 3.
1400
1408
  self._reregister_callbacks(widget)
1401
1409
 
1410
+ # 6. Widgets with a Python-authoritative model (TextSource) push
1411
+ # their full model state (text, tags, cursor) to the freshly
1412
+ # (re)connected browser. Guard it: a failure here must not abort
1413
+ # reconstruction of the whole widget tree.
1414
+ if hasattr(widget, "_reconstruct_model"):
1415
+ try:
1416
+ widget._reconstruct_model()
1417
+ except Exception:
1418
+ self._logger.error(
1419
+ "TextSource model reconstruction failed for wid=%s",
1420
+ getattr(widget, "_wid", "?"), exc_info=True)
1421
+
1402
1422
  def _reregister_callbacks(self, widget):
1403
1423
  """Re-register user callbacks and auto-sync listeners on *widget*.
1404
1424
 
@@ -0,0 +1,200 @@
1
+ """Python-authoritative TextSource for the synchronous API.
2
+
3
+ Unlike most widgets (whose state is a handful of scalars), a text buffer
4
+ has rich structure -- content, live position refs, a tag table, cursor and
5
+ selection. We keep the *authoritative* copy of all of that on the Python
6
+ side in a :class:`~pgwidgets.text_model.TextModel`, so the whole API works
7
+ before a browser is ever connected (the UI can be built up headless and
8
+ replayed on connect). The browser is driven as a *view*: model changes are
9
+ pushed as offset-based operations through the render hooks, and are no-ops
10
+ (``_call`` returns None) until a browser attaches, at which point
11
+ ``_reconstruct_model`` replays the full state.
12
+
13
+ Refs never cross the wire -- they are ordinary Python ``TextBufferRef``
14
+ objects owned by the local model. (The in-situ / JS-only case, where the
15
+ browser is authoritative, is handled by the separate pgwidgets-js/pyodide
16
+ wrapper, not here.)
17
+
18
+ The class subclasses the *generated* TextSource proxy so it inherits all the
19
+ base-widget methods (show/hide/set_tooltip/get_size/...) and the widget
20
+ option setters (set_editable/set_wrap/scrolling); it overrides only the
21
+ methods that manipulate the text model.
22
+ """
23
+
24
+ from pgwidgets.sync.widget import build_widget_class
25
+ from pgwidgets.defs import WIDGETS
26
+ from pgwidgets.text_model import TextModel
27
+
28
+
29
+ _GeneratedTextSource = build_widget_class("TextSource", WIDGETS["TextSource"])
30
+
31
+
32
+ class _BrowserTextModel(TextModel):
33
+ """A TextModel whose render hooks push offset-based operations to the
34
+ browser via the owning widget. When no browser is connected the calls
35
+ are no-ops, so the model is fully usable headless."""
36
+
37
+ def __init__(self, widget):
38
+ self._widget = widget
39
+ super().__init__()
40
+
41
+ def _push(self, method, *args):
42
+ # _call round-trips when a browser is connected and is a no-op
43
+ # returning None otherwise.
44
+ self._widget._call(method, *args)
45
+
46
+ # -- render hooks (see TextModel) --
47
+ def _set_editor_text(self, text):
48
+ self._push("set_text", text)
49
+
50
+ def _replace_editor_range(self, start, end, new_text):
51
+ # Drive the JS buffer by offset; pushUndo=False because the Python
52
+ # model owns undo/redo.
53
+ self._push("_replaceRange", start, end, new_text, {"pushUndo": False})
54
+
55
+ def _apply_all_formats(self, region=None):
56
+ # Push the current tag intervals; the JS side re-renders styling.
57
+ # (Tag *definitions* are pushed by TextSource.create_tag.)
58
+ self._push("_restoreTagIntervals", self._tag_intervals())
59
+
60
+ def _apply_selection_to_editor(self):
61
+ if self._sel_start == self._sel_end:
62
+ self._push("_setCursorOffset", self._cursor)
63
+ else:
64
+ self._push("_setSelectionOffsets", self._sel_start, self._sel_end)
65
+
66
+ def _refresh_icon_gutter(self):
67
+ # TODO: render ref-anchored gutter icons in the browser.
68
+ pass
69
+
70
+ def _mark_clean(self):
71
+ pass
72
+
73
+ def _tag_intervals(self):
74
+ return [{"name": t["name"], "start": t["start"], "end": t["end"]}
75
+ for t in self._tags]
76
+
77
+
78
+ # Model methods delegated verbatim to self._model. Refs in/out are the
79
+ # model's own TextBufferRefs; nothing here touches the wire. These override
80
+ # the generated round-trip proxies of the same name.
81
+ _DELEGATED = (
82
+ # content
83
+ "get_length", "get_text", "get_text_range", "clear", "set_text",
84
+ "insert_text", "delete_range",
85
+ # refs
86
+ "create_ref", "remove_ref", "create_named_ref", "get_named_ref",
87
+ "remove_named_ref", "get_ref_start", "get_ref_end", "get_ref_bounds",
88
+ "get_ref_line_start", "get_ref_line_end", "set_icon",
89
+ # tags (create_tag handled explicitly to also push the tag definition)
90
+ "remove_tag_def", "has_tag", "apply_tag", "remove_tag",
91
+ "get_tags_at", "get_tags_range", "get_tag_region", "get_tag_regions",
92
+ # find / replace
93
+ "find", "find_all", "replace",
94
+ # cursor / selection
95
+ "get_cursor", "set_cursor", "has_selection", "get_selection_range",
96
+ "get_selection_bounds", "set_selection_range",
97
+ # undo / redo / dirty flag
98
+ "can_undo", "can_redo", "undo", "redo", "get_modified", "mark_clean",
99
+ # misc convenience
100
+ "get_end_lineno", "clear_icons",
101
+ )
102
+
103
+ # Callback actions that are model-authoritative (fire from the Python model)
104
+ # rather than being browser events.
105
+ _MODEL_CALLBACKS = {"changed", "cursor-moved", "cursor_moved"}
106
+
107
+
108
+ class TextSource(_GeneratedTextSource):
109
+
110
+ def __init__(self, session, *args, **kwargs):
111
+ super().__init__(session, *args, **kwargs)
112
+ # The authoritative model. Seed its text from the initial value the
113
+ # generated __init__ recorded (the JS side already has it from the
114
+ # constructor, so no push). Text/tags/cursor are reconstructed by
115
+ # _reconstruct_model(), not by the generic state replay, so drop
116
+ # 'text' from _state.
117
+ self._model = _BrowserTextModel(self)
118
+ text = self._state.get("text")
119
+ self._model._text = "" if text is None else str(text)
120
+ self._state.pop("text", None)
121
+
122
+ def create_tag(self, name, attrs=None, **kwdargs):
123
+ """Define a display tag locally and push its styling to the browser."""
124
+ merged = {} if attrs is None else dict(attrs)
125
+ merged.update(kwdargs)
126
+ self._model.create_tag(name, merged)
127
+ self._call("create_tag", name, merged)
128
+
129
+ def scroll_to_ref(self, ref):
130
+ """Scroll the view to a ref, addressed by its current offset."""
131
+ self._call("_scrollToOffset", self._model._offset_of(ref))
132
+
133
+ def scroll_to_lineno(self, lineno):
134
+ """Scroll so that line ``lineno`` is visible."""
135
+ self._call("_scrollToOffset",
136
+ self._model._offset_of_line_start(lineno))
137
+
138
+ def scroll_to_end(self):
139
+ """Scroll to the end of the buffer."""
140
+ self._call("_scrollToOffset", self._model.get_length())
141
+
142
+ def show_tooltip(self, text, x, y):
143
+ """Show a hover tooltip (``text``) near viewport point (x, y), or
144
+ hide it when ``text`` is empty. Normally called from a handler of
145
+ the 'tooltip' callback."""
146
+ self._call("_showTooltip", text, x, y)
147
+
148
+ # -- callbacks: 'changed'/'cursor_moved' are model-authoritative --
149
+ def add_callback(self, action, handler, *extra_args, **extra_kwargs):
150
+ if action in _MODEL_CALLBACKS:
151
+ self._model.add_callback(
152
+ self._model_cb_name(action),
153
+ lambda _m, *a: handler(self, *a, *extra_args, **extra_kwargs))
154
+ return
155
+ super().add_callback(action, handler, *extra_args, **extra_kwargs)
156
+
157
+ def on(self, action, handler, *extra_args, **extra_kwargs):
158
+ if action in _MODEL_CALLBACKS:
159
+ self._model.add_callback(
160
+ self._model_cb_name(action),
161
+ lambda _m, *a: handler(*a, *extra_args, **extra_kwargs))
162
+ return
163
+ super().on(action, handler, *extra_args, **extra_kwargs)
164
+
165
+ @staticmethod
166
+ def _model_cb_name(action):
167
+ return "cursor-moved" if action in ("cursor_moved",
168
+ "cursor-moved") else "changed"
169
+
170
+ # -- reconstruction: push the full model state to a freshly-connected
171
+ # browser (called by Session._reconstruct_widget after the generic
172
+ # create/state/callback replay) --
173
+ def _reconstruct_model(self):
174
+ m = self._model
175
+ self._call("set_text", m.get_text())
176
+ for name, attrs in m._tag_defs.items():
177
+ self._call("create_tag", name, attrs)
178
+ intervals = m._tag_intervals()
179
+ if intervals:
180
+ self._call("_restoreTagIntervals", intervals)
181
+ if m._sel_start != m._sel_end:
182
+ self._call("_setSelectionOffsets", m._sel_start, m._sel_end)
183
+ else:
184
+ self._call("_setCursorOffset", m._cursor)
185
+
186
+
187
+ def _install_delegators():
188
+ """Override the generated round-trip proxies with model-delegating
189
+ methods (defined at class scope so ``hasattr(TextSource, name)`` holds)."""
190
+ for _name in _DELEGATED:
191
+ def _make(n):
192
+ def method(self, *args, **kwargs):
193
+ return getattr(self._model, n)(*args, **kwargs)
194
+ method.__name__ = n
195
+ method.__qualname__ = "TextSource." + n
196
+ return method
197
+ setattr(TextSource, _name, _make(_name))
198
+
199
+
200
+ _install_delegators()
pgwidgets/sync/widget.py CHANGED
@@ -965,4 +965,9 @@ def build_all_widget_classes():
965
965
  classes = {}
966
966
  for js_class, defn in WIDGETS.items():
967
967
  classes[js_class] = build_widget_class(js_class, defn)
968
+ # TextSource is hand-written (Python-authoritative local text model)
969
+ # rather than generated -- see pgwidgets.sync.text_source. Deferred
970
+ # import avoids a circular import at module load.
971
+ from pgwidgets.sync.text_source import TextSource as _TextSource
972
+ classes['TextSource'] = _TextSource
968
973
  return classes
@@ -0,0 +1,792 @@
1
+ #
2
+ # text_model.py -- toolkit-neutral text buffer model with tags and refs
3
+ #
4
+ # This is open-source software licensed under a BSD license.
5
+ # Please see the file LICENSE.txt for details.
6
+ #
7
+ """A backend-neutral text buffer model.
8
+
9
+ ``TextModel`` holds the content string, a set of live position references
10
+ (``TextBufferRef``) that follow edits, a named tag table with interval
11
+ bookkeeping, find/replace, and a model-level undo/redo stack. None of it
12
+ depends on a GUI toolkit.
13
+
14
+ A toolkit widget (qt, gtk, ...) subclasses ``TextModel`` and implements the
15
+ small set of *render hooks* to reflect model changes in a native widget:
16
+
17
+ _set_editor_text(text) push the whole text into the widget
18
+ _replace_editor_range(s, e, text) edit just the changed span
19
+ _apply_all_formats(region=None) (re)apply tag styling to a range
20
+ _apply_selection_to_editor() mirror model selection into the widget
21
+ _refresh_icon_gutter() redraw ref-anchored gutter icons
22
+ _mark_clean() clear the widget's "modified" flag
23
+
24
+ The default implementations are no-ops, so a bare ``TextModel`` is usable
25
+ headless (e.g. for tests or a non-visual buffer).
26
+ """
27
+ import weakref
28
+
29
+ from pgwidgets.callbacks import Callbacks
30
+
31
+
32
+ class TextBufferRef:
33
+ """Live reference to a character offset in a text model buffer.
34
+
35
+ The ref follows inserts and deletes performed through the owning buffer.
36
+ Gravity controls how the ref behaves when text is inserted at exactly the
37
+ ref's offset.
38
+ """
39
+
40
+ def __init__(self, buffer, offset, gravity='right'):
41
+ if gravity not in ('left', 'right'):
42
+ raise ValueError("gravity must be 'left' or 'right'")
43
+ self._buffer = buffer
44
+ self._offset = buffer._clamp_offset(offset)
45
+ self._gravity = gravity
46
+ self._valid = True
47
+
48
+ def get_offset(self):
49
+ return self._offset
50
+
51
+ def get_gravity(self):
52
+ return self._gravity
53
+
54
+ def is_valid(self):
55
+ return self._valid
56
+
57
+ def get_line_column(self):
58
+ self._check_valid()
59
+ text = self._buffer.get_text()
60
+ prefix = text[:self._offset]
61
+ line = prefix.count('\n')
62
+ last_nl = prefix.rfind('\n')
63
+ col = self._offset if last_nl < 0 else self._offset - last_nl - 1
64
+ return (line, col)
65
+
66
+ def get_line(self):
67
+ self._check_valid()
68
+ return self._buffer._line_of_offset(self._offset)
69
+
70
+ def set_offset(self, offset):
71
+ self._check_valid()
72
+ self._set_offset(offset)
73
+
74
+ def set_line(self, lineno):
75
+ self._check_valid()
76
+ self._set_offset(self._buffer._offset_of_line_start(lineno))
77
+
78
+ def to_ref(self, other):
79
+ self._check_valid()
80
+ if not isinstance(other, TextBufferRef):
81
+ raise TypeError("to_ref requires a TextBufferRef")
82
+ if other._buffer is not self._buffer:
83
+ raise ValueError("TextBufferRef belongs to a different buffer")
84
+ if not other._valid:
85
+ raise ValueError("Source TextBufferRef has been invalidated")
86
+ self._set_offset(other._offset)
87
+
88
+ def copy(self):
89
+ self._check_valid()
90
+ return self._buffer.create_ref(self._offset, self._gravity)
91
+
92
+ def to_line_start(self):
93
+ self._check_valid()
94
+ self._set_offset(self._buffer._offset_of_line_start(self.get_line()))
95
+
96
+ def to_line_end(self):
97
+ self._check_valid()
98
+ text = self._buffer.get_text()
99
+ idx = text.find('\n', self._offset)
100
+ if idx < 0:
101
+ idx = len(text)
102
+ self._set_offset(idx)
103
+
104
+ def to_next_line(self):
105
+ self._check_valid()
106
+ text = self._buffer.get_text()
107
+ idx = text.find('\n', self._offset)
108
+ if idx >= 0:
109
+ self._set_offset(idx + 1)
110
+
111
+ def to_prev_line(self):
112
+ self._check_valid()
113
+ line = self.get_line()
114
+ if line > 0:
115
+ self._set_offset(self._buffer._offset_of_line_start(line - 1))
116
+
117
+ def to_next_char(self):
118
+ self._check_valid()
119
+ self._set_offset(self._offset + 1)
120
+
121
+ def to_prev_char(self):
122
+ self._check_valid()
123
+ self._set_offset(self._offset - 1)
124
+
125
+ def _check_valid(self):
126
+ if not self._valid:
127
+ raise ValueError("TextBufferRef has been invalidated")
128
+
129
+ def _set_offset(self, new_offset):
130
+ self._offset = self._buffer._clamp_offset(new_offset)
131
+ self._buffer._refresh_icon_gutter()
132
+
133
+ def _invalidate(self):
134
+ self._valid = False
135
+
136
+
137
+ class TextModel(Callbacks):
138
+ """Toolkit-neutral text buffer: content, refs, a tag table, find/replace
139
+ and model-level undo/redo. Subclass and implement the render hooks to
140
+ drive a native widget."""
141
+
142
+ def __init__(self):
143
+ Callbacks.__init__(self)
144
+
145
+ self._text = ''
146
+ self._refs = weakref.WeakSet()
147
+ self._named_refs = {}
148
+ self._tag_defs = {}
149
+ self._tags = []
150
+ self._tag_seq = 0
151
+ self._icon_refs = {}
152
+ self._cursor = 0
153
+ self._sel_start = 0
154
+ self._sel_end = 0
155
+
156
+ # Model-level undo/redo. Each entry records a single _replace_range
157
+ # edit as (start, old_text, new_text, cursor_before, cursor_after) so
158
+ # it can be inverted with refs and tags kept consistent.
159
+ self._undo_stack = []
160
+ self._redo_stack = []
161
+ self._undo_limit = 500
162
+ self._in_undo_redo = False
163
+ # dirty flag: True after any edit, cleared by mark_clean()/set_text()
164
+ self._modified = False
165
+
166
+ for name in ('changed', 'cursor-moved'):
167
+ self.enable_callback(name)
168
+
169
+ # ------------------------------------------------------------------
170
+ # Render hooks -- subclasses override these to reflect model changes in
171
+ # a native widget. Defaults are no-ops so a bare model works headless.
172
+ # ------------------------------------------------------------------
173
+ def _set_editor_text(self, text):
174
+ pass
175
+
176
+ def _replace_editor_range(self, start, end, new_text):
177
+ pass
178
+
179
+ def _apply_all_formats(self, region=None):
180
+ pass
181
+
182
+ def _apply_selection_to_editor(self):
183
+ pass
184
+
185
+ def _refresh_icon_gutter(self):
186
+ pass
187
+
188
+ def _mark_clean(self):
189
+ pass
190
+
191
+ # ------------------------------------------------------------------
192
+ # Content
193
+ # ------------------------------------------------------------------
194
+ def get_length(self):
195
+ return len(self._text)
196
+
197
+ def get_text(self):
198
+ return self._text
199
+
200
+ def get_text_range(self, start_ref, end_ref):
201
+ """Return the text spanning ``[start_ref, end_ref)``."""
202
+ start = self._offset_of(start_ref)
203
+ end = self._offset_of(end_ref)
204
+ if start > end:
205
+ start, end = end, start
206
+ return self._text[start:end]
207
+
208
+ def clear(self):
209
+ self.set_text('')
210
+
211
+ def set_text(self, text):
212
+ """Replace the full buffer contents.
213
+
214
+ This is destructive with respect to refs and applied tags: existing
215
+ refs are invalidated and applied tag intervals are cleared.
216
+ """
217
+ self._text = '' if text is None else str(text)
218
+ self._cursor = 0
219
+ self._sel_start = 0
220
+ self._sel_end = 0
221
+ self._tags = []
222
+ self._undo_stack = []
223
+ self._redo_stack = []
224
+ self._invalidate_all_refs()
225
+ self._named_refs.clear()
226
+ self._icon_refs.clear()
227
+ self._modified = False
228
+ self._set_editor_text(self._text)
229
+ self._apply_all_formats()
230
+ self._refresh_icon_gutter()
231
+ self._mark_clean()
232
+
233
+ def insert_text(self, ref, text, tags=None):
234
+ """Insert text at ``ref`` and optionally apply tags to that range."""
235
+ if text is None or text == '':
236
+ return
237
+ offset = self._offset_of(ref)
238
+ self._replace_range(offset, offset, str(text), tags=tags)
239
+
240
+ def delete_range(self, start_ref, end_ref):
241
+ """Delete the text spanning ``[start_ref, end_ref)``."""
242
+ start = self._offset_of(start_ref)
243
+ end = self._offset_of(end_ref)
244
+ if start > end:
245
+ start, end = end, start
246
+ if start == end:
247
+ return
248
+ self._replace_range(start, end, '')
249
+
250
+ # ------------------------------------------------------------------
251
+ # Refs
252
+ # ------------------------------------------------------------------
253
+ def create_ref(self, offset, gravity='right'):
254
+ """Create a live buffer ref at ``offset``."""
255
+ ref = TextBufferRef(self, offset, gravity=gravity)
256
+ self._refs.add(ref)
257
+ return ref
258
+
259
+ def remove_ref(self, ref):
260
+ """Invalidate a ref and detach any named/icon bindings that use it."""
261
+ if not isinstance(ref, TextBufferRef):
262
+ return
263
+ if not ref.is_valid():
264
+ return
265
+ for name, named_ref in list(self._named_refs.items()):
266
+ if named_ref is ref:
267
+ del self._named_refs[name]
268
+ if ref in self._icon_refs:
269
+ del self._icon_refs[ref]
270
+ ref._invalidate()
271
+ self._refresh_icon_gutter()
272
+
273
+ def create_named_ref(self, name, offset, gravity='right'):
274
+ """Create a live ref and bind it to ``name``."""
275
+ existing = self._named_refs.get(name)
276
+ if existing is not None:
277
+ self.remove_ref(existing)
278
+ ref = self.create_ref(offset, gravity=gravity)
279
+ self._named_refs[name] = ref
280
+ return ref
281
+
282
+ def get_named_ref(self, name):
283
+ return self._named_refs.get(name)
284
+
285
+ def remove_named_ref(self, name):
286
+ ref = self._named_refs.pop(name, None)
287
+ if ref is not None:
288
+ self.remove_ref(ref)
289
+
290
+ def get_ref_start(self):
291
+ return self.create_ref(0, 'right')
292
+
293
+ def get_ref_end(self):
294
+ return self.create_ref(len(self._text), 'right')
295
+
296
+ def get_ref_bounds(self):
297
+ return (self.get_ref_start(), self.get_ref_end())
298
+
299
+ def get_ref_line_start(self, lineno):
300
+ return self.create_ref(self._offset_of_line_start(lineno), 'right')
301
+
302
+ def get_ref_line_end(self, lineno):
303
+ start = self._offset_of_line_start(lineno)
304
+ idx = self._text.find('\n', start)
305
+ end = len(self._text) if idx < 0 else idx
306
+ return self.create_ref(end, 'right')
307
+
308
+ def get_end_lineno(self):
309
+ """Return the 0-based line number of the end of the buffer."""
310
+ return self._line_of_offset(len(self._text))
311
+
312
+ def set_icon(self, ref, image):
313
+ """Associate an icon with a ref so it follows text movement by line."""
314
+ if not isinstance(ref, TextBufferRef):
315
+ raise TypeError("set_icon requires a TextBufferRef")
316
+ if ref._buffer is not self:
317
+ raise ValueError("TextBufferRef belongs to a different buffer")
318
+ if image is None:
319
+ self._icon_refs.pop(ref, None)
320
+ else:
321
+ self._icon_refs.pop(ref, None)
322
+ self._icon_refs[ref] = image
323
+ self._refresh_icon_gutter()
324
+
325
+ def clear_icons(self):
326
+ """Remove all ref-anchored gutter icons."""
327
+ self._icon_refs.clear()
328
+ self._refresh_icon_gutter()
329
+
330
+ # ------------------------------------------------------------------
331
+ # Tags
332
+ # ------------------------------------------------------------------
333
+ def create_tag(self, name, attrs=None, **kwdargs):
334
+ """Define or redefine a named display tag."""
335
+ attrs = {} if attrs is None else dict(attrs)
336
+ attrs.update(kwdargs)
337
+ self._tag_defs[name] = attrs
338
+ # A tag definition only affects rendering where the tag is applied.
339
+ # Defining a brand-new (unapplied) tag needs no reformat, which keeps
340
+ # bulk tag creation (e.g. one tag per AST node) cheap.
341
+ if self.has_tag(name):
342
+ self._apply_all_formats()
343
+
344
+ def remove_tag_def(self, name):
345
+ if name in self._tag_defs:
346
+ del self._tag_defs[name]
347
+ self._tags = [tag for tag in self._tags if tag['name'] != name]
348
+ self._apply_all_formats()
349
+
350
+ def has_tag(self, name):
351
+ return any(tag['name'] == name for tag in self._tags)
352
+
353
+ def apply_tag(self, name, start_ref, end_ref):
354
+ """Apply a previously-defined tag across a buffer range."""
355
+ if name not in self._tag_defs:
356
+ raise ValueError("Unknown tag: %s" % (name,))
357
+ start = self._offset_of(start_ref)
358
+ end = self._offset_of(end_ref)
359
+ if start > end:
360
+ start, end = end, start
361
+ if start == end:
362
+ return
363
+ self._add_tag_interval(name, start, end)
364
+ self._apply_all_formats(region=(start, end))
365
+
366
+ def remove_tag(self, name, start_ref, end_ref):
367
+ """Clip a tag out of the given buffer range."""
368
+ start = self._offset_of(start_ref)
369
+ end = self._offset_of(end_ref)
370
+ if start > end:
371
+ start, end = end, start
372
+ next_tags = []
373
+ for tag in self._tags:
374
+ if tag['name'] != name or tag['end'] <= start or tag['start'] >= end:
375
+ next_tags.append(tag)
376
+ continue
377
+ if tag['start'] < start:
378
+ next_tags.append(dict(tag, end=start))
379
+ if tag['end'] > end:
380
+ next_tags.append(dict(tag, start=end))
381
+ self._tags = next_tags
382
+ self._apply_all_formats(region=(start, end))
383
+
384
+ def get_tags_at(self, ref):
385
+ offset = self._offset_of(ref)
386
+ names = []
387
+ seen = set()
388
+ for tag in self._tags:
389
+ if tag['start'] <= offset < tag['end'] and tag['name'] not in seen:
390
+ names.append(tag['name'])
391
+ seen.add(tag['name'])
392
+ return names
393
+
394
+ def get_tags_range(self, start_ref, end_ref):
395
+ start = self._offset_of(start_ref)
396
+ end = self._offset_of(end_ref)
397
+ if start > end:
398
+ start, end = end, start
399
+ names = []
400
+ seen = set()
401
+ for tag in self._tags:
402
+ if tag['end'] <= start or tag['start'] >= end:
403
+ continue
404
+ if tag['name'] not in seen:
405
+ names.append(tag['name'])
406
+ seen.add(tag['name'])
407
+ return names
408
+
409
+ def get_tag_region(self, name):
410
+ """Return the overall ``(start_ref, end_ref)`` span of a named tag.
411
+
412
+ This mirrors the old GTK ``get_region`` helper: it returns a single
413
+ ref pair covering from the first occurrence of the tag to the last.
414
+ Returns ``None`` if the tag is not applied anywhere.
415
+ """
416
+ spans = [tag for tag in self._tags if tag['name'] == name]
417
+ if not spans:
418
+ return None
419
+ start = min(tag['start'] for tag in spans)
420
+ end = max(tag['end'] for tag in spans)
421
+ return (self.create_ref(start, 'right'),
422
+ self.create_ref(end, 'right'))
423
+
424
+ def get_tag_regions(self, name):
425
+ """Return a list of ``(start_ref, end_ref)`` pairs, one per maximal
426
+ contiguous run of the named tag."""
427
+ spans = sorted((tag for tag in self._tags if tag['name'] == name),
428
+ key=lambda tag: tag['start'])
429
+ if not spans:
430
+ return []
431
+ merged = []
432
+ cur_start, cur_end = spans[0]['start'], spans[0]['end']
433
+ for tag in spans[1:]:
434
+ if tag['start'] <= cur_end:
435
+ cur_end = max(cur_end, tag['end'])
436
+ else:
437
+ merged.append((cur_start, cur_end))
438
+ cur_start, cur_end = tag['start'], tag['end']
439
+ merged.append((cur_start, cur_end))
440
+ return [(self.create_ref(s, 'right'), self.create_ref(e, 'right'))
441
+ for s, e in merged]
442
+
443
+ # ------------------------------------------------------------------
444
+ # Find / replace
445
+ # ------------------------------------------------------------------
446
+ def find(self, query, start=None, case_insensitive=False):
447
+ """Return the first match as ``(start_ref, end_ref)`` or ``None``."""
448
+ match = self._find_offset(query, start=start,
449
+ case_insensitive=case_insensitive)
450
+ if match is None:
451
+ return None
452
+ return (self.create_ref(match[0], 'right'),
453
+ self.create_ref(match[1], 'right'))
454
+
455
+ def find_all(self, query, start=None, case_insensitive=False):
456
+ """Return all non-overlapping matches as ref pairs."""
457
+ matches = self._find_all_offsets(query, start=start,
458
+ case_insensitive=case_insensitive)
459
+ return [(self.create_ref(start_off, 'right'),
460
+ self.create_ref(end_off, 'right'))
461
+ for start_off, end_off in matches]
462
+
463
+ def replace(self, query, replacement, all=False, start=None,
464
+ case_insensitive=False):
465
+ """Replace one or all matches and return the replacement count."""
466
+ if not query:
467
+ return 0
468
+ if all:
469
+ matches = self._find_all_offsets(query, start=start,
470
+ case_insensitive=case_insensitive)
471
+ else:
472
+ match = self._find_offset(query, start=start,
473
+ case_insensitive=case_insensitive)
474
+ matches = [] if match is None else [match]
475
+ for start_off, end_off in reversed(matches):
476
+ self._replace_range(start_off, end_off, replacement)
477
+ return len(matches)
478
+
479
+ # ------------------------------------------------------------------
480
+ # Cursor / selection
481
+ # ------------------------------------------------------------------
482
+ def get_cursor(self):
483
+ return self.create_ref(self._cursor, 'right')
484
+
485
+ def set_cursor(self, ref):
486
+ """Move the editor cursor to ``ref`` and clear any selection."""
487
+ offset = self._offset_of(ref)
488
+ self._cursor = offset
489
+ self._sel_start = offset
490
+ self._sel_end = offset
491
+ self._apply_selection_to_editor()
492
+
493
+ def has_selection(self):
494
+ return self._sel_start != self._sel_end
495
+
496
+ def get_selection_range(self):
497
+ if self._sel_start == self._sel_end:
498
+ return None
499
+ start = min(self._sel_start, self._sel_end)
500
+ end = max(self._sel_start, self._sel_end)
501
+ return (self.create_ref(start, 'right'),
502
+ self.create_ref(end, 'right'))
503
+
504
+ # get_selection_bounds() is the canonical accessor used by page code;
505
+ # it returns a (start_ref, end_ref) pair or None when there is no
506
+ # selection. Callers guard with has_selection().
507
+ get_selection_bounds = get_selection_range
508
+
509
+ def set_selection_range(self, start_ref, end_ref):
510
+ """Select the text spanning ``[start_ref, end_ref)``."""
511
+ start = self._offset_of(start_ref)
512
+ end = self._offset_of(end_ref)
513
+ self._sel_start = start
514
+ self._sel_end = end
515
+ self._cursor = end
516
+ self._apply_selection_to_editor()
517
+
518
+ # ------------------------------------------------------------------
519
+ # Undo / redo
520
+ # ------------------------------------------------------------------
521
+ def can_undo(self):
522
+ return len(self._undo_stack) > 0
523
+
524
+ def can_redo(self):
525
+ return len(self._redo_stack) > 0
526
+
527
+ def get_modified(self):
528
+ """True if the buffer has been edited since the last mark_clean()
529
+ (or set_text)."""
530
+ return self._modified
531
+
532
+ def mark_clean(self):
533
+ """Reset the modified flag (e.g. after saving)."""
534
+ self._modified = False
535
+ self._mark_clean()
536
+
537
+ def undo(self):
538
+ if not self._undo_stack:
539
+ return False
540
+ start, old_text, new_text, cursor_before, cursor_after = \
541
+ self._undo_stack.pop()
542
+ self._in_undo_redo = True
543
+ try:
544
+ self._replace_range(start, start + len(new_text), old_text,
545
+ push_undo=False)
546
+ self._cursor, self._sel_start, self._sel_end = cursor_before
547
+ self._apply_selection_to_editor()
548
+ finally:
549
+ self._in_undo_redo = False
550
+ self._redo_stack.append((start, old_text, new_text, cursor_before,
551
+ cursor_after))
552
+ return True
553
+
554
+ def redo(self):
555
+ if not self._redo_stack:
556
+ return False
557
+ start, old_text, new_text, cursor_before, cursor_after = \
558
+ self._redo_stack.pop()
559
+ self._in_undo_redo = True
560
+ try:
561
+ self._replace_range(start, start + len(old_text), new_text,
562
+ push_undo=False)
563
+ self._cursor, self._sel_start, self._sel_end = cursor_after
564
+ self._apply_selection_to_editor()
565
+ finally:
566
+ self._in_undo_redo = False
567
+ self._undo_stack.append((start, old_text, new_text, cursor_before,
568
+ cursor_after))
569
+ return True
570
+
571
+ # ------------------------------------------------------------------
572
+ # Internal offset / ref / tag bookkeeping (pure)
573
+ # ------------------------------------------------------------------
574
+ def _clamp_offset(self, offset):
575
+ try:
576
+ offset = int(offset)
577
+ except Exception:
578
+ offset = 0
579
+ return max(0, min(len(self._text), offset))
580
+
581
+ def _offset_of(self, ref):
582
+ if not isinstance(ref, TextBufferRef):
583
+ raise TypeError("API requires a TextBufferRef")
584
+ if ref._buffer is not self:
585
+ raise ValueError("TextBufferRef belongs to a different buffer")
586
+ if not ref.is_valid():
587
+ raise ValueError("TextBufferRef has been invalidated")
588
+ return self._clamp_offset(ref._offset)
589
+
590
+ def _line_of_offset(self, offset):
591
+ return self._text[:self._clamp_offset(offset)].count('\n')
592
+
593
+ def _offset_of_line_start(self, lineno):
594
+ if lineno <= 0:
595
+ return 0
596
+ offset = 0
597
+ for _idx in range(lineno):
598
+ nl = self._text.find('\n', offset)
599
+ if nl < 0:
600
+ return len(self._text)
601
+ offset = nl + 1
602
+ return offset
603
+
604
+ def _replace_range(self, start, end, new_text, tags=None,
605
+ selection_after=None, sync_editor=True, push_undo=True):
606
+ """Replace ``[start, end)`` in the model and synchronize the view.
607
+
608
+ All ref and tag shifting happens here so every edit path shares the
609
+ same semantics. ``sync_editor`` is disabled only when the edit
610
+ originated from the native widget (which already reflects the new
611
+ text). ``push_undo`` is disabled when the edit is itself the result
612
+ of an undo/redo replay.
613
+ """
614
+ old_text = self._text[start:end]
615
+ if old_text == new_text:
616
+ if selection_after is not None:
617
+ self._cursor, self._sel_start, self._sel_end = selection_after
618
+ return
619
+
620
+ cursor_before = (self._cursor, self._sel_start, self._sel_end)
621
+
622
+ self._text = self._text[:start] + new_text + self._text[end:]
623
+ if end > start:
624
+ self._update_refs_on_delete(start, end)
625
+ self._update_tags_on_delete(start, end)
626
+ if new_text:
627
+ self._update_refs_on_insert(start, len(new_text))
628
+ self._update_tags_on_insert(start, len(new_text))
629
+ if tags:
630
+ for name in tags:
631
+ self._add_tag_interval(name, start, start + len(new_text))
632
+ if selection_after is None:
633
+ new_pos = start + len(new_text)
634
+ self._cursor = new_pos
635
+ self._sel_start = new_pos
636
+ self._sel_end = new_pos
637
+ else:
638
+ self._cursor, self._sel_start, self._sel_end = selection_after
639
+
640
+ if not self._in_undo_redo and push_undo:
641
+ self._undo_stack.append((start, old_text, new_text, cursor_before,
642
+ (self._cursor, self._sel_start, self._sel_end)))
643
+ if len(self._undo_stack) > self._undo_limit:
644
+ self._undo_stack.pop(0)
645
+ self._redo_stack = []
646
+
647
+ if sync_editor:
648
+ # Edit only the changed span in the editor (not the whole
649
+ # document), preserving existing formatting outside it.
650
+ self._replace_editor_range(start, end, new_text)
651
+ self._apply_selection_to_editor()
652
+ # Only the inserted span needs (re)formatting: on an incremental edit
653
+ # the toolkit keeps the formatting of the surrounding text, which has
654
+ # merely shifted. This keeps appends O(edit) rather than O(buffer).
655
+ self._apply_all_formats(region=(start, start + len(new_text)))
656
+ self._refresh_icon_gutter()
657
+
658
+ self._modified = True
659
+ self.make_callback('changed')
660
+
661
+ def _invalidate_all_refs(self):
662
+ for ref in list(self._refs):
663
+ ref._offset = 0
664
+ ref._invalidate()
665
+
666
+ def _update_refs_on_insert(self, offset, amount):
667
+ for ref in list(self._refs):
668
+ if not ref.is_valid():
669
+ continue
670
+ if ref._offset > offset or (
671
+ ref._offset == offset and ref.get_gravity() == 'right'):
672
+ ref._offset += amount
673
+
674
+ def _update_refs_on_delete(self, start, end):
675
+ amount = end - start
676
+ for ref in list(self._refs):
677
+ if not ref.is_valid():
678
+ continue
679
+ if ref._offset <= start:
680
+ continue
681
+ if ref._offset >= end:
682
+ ref._offset -= amount
683
+ else:
684
+ ref._offset = start
685
+
686
+ def _add_tag_interval(self, name, start, end):
687
+ self._tag_seq += 1
688
+ self._tags.append(dict(name=name, start=start, end=end,
689
+ seq=self._tag_seq))
690
+
691
+ def _update_tags_on_insert(self, offset, amount):
692
+ for tag in self._tags:
693
+ if tag['start'] >= offset:
694
+ tag['start'] += amount
695
+ if tag['end'] > offset or (
696
+ tag['end'] == offset and tag['start'] == tag['end']):
697
+ tag['end'] += amount
698
+ if tag['end'] < tag['start']:
699
+ tag['end'] = tag['start']
700
+
701
+ def _update_tags_on_delete(self, start, end):
702
+ amount = end - start
703
+ next_tags = []
704
+ for tag in self._tags:
705
+ tag_start = tag['start']
706
+ tag_end = tag['end']
707
+ if tag_end <= start:
708
+ next_tags.append(tag)
709
+ continue
710
+ if tag_start >= end:
711
+ next_tags.append(dict(tag, start=tag_start - amount,
712
+ end=tag_end - amount))
713
+ continue
714
+ new_start = tag_start if tag_start < start else start
715
+ new_end = tag_end - amount if tag_end > end else start
716
+ if new_end > new_start:
717
+ next_tags.append(dict(tag, start=new_start, end=new_end))
718
+ self._tags = next_tags
719
+
720
+ def _segments_for_range(self, start, end):
721
+ """Split a range into maximal subranges sharing the same tag stack.
722
+
723
+ Returns a list of ``(seg_start, seg_end, [tag_name, ...])`` with the
724
+ tag names ordered by application sequence (so later tags win)."""
725
+ points = {start, end}
726
+ active = []
727
+ for tag in self._tags:
728
+ if tag['end'] <= start or tag['start'] >= end:
729
+ continue
730
+ active.append(tag)
731
+ points.add(max(tag['start'], start))
732
+ points.add(min(tag['end'], end))
733
+ sorted_points = sorted(points)
734
+ segments = []
735
+ for idx in range(len(sorted_points) - 1):
736
+ seg_start = sorted_points[idx]
737
+ seg_end = sorted_points[idx + 1]
738
+ if seg_start >= seg_end:
739
+ continue
740
+ in_seg = [tag for tag in active
741
+ if tag['start'] <= seg_start and tag['end'] >= seg_end]
742
+ in_seg.sort(key=lambda item: item['seq'])
743
+ segments.append((seg_start, seg_end,
744
+ [tag['name'] for tag in in_seg]))
745
+ return segments
746
+
747
+ def _merged_attrs(self, tag_names):
748
+ """Merge the attr dicts of the given tags (later tags win)."""
749
+ attrs = {}
750
+ for name in tag_names:
751
+ attrs.update(self._tag_defs.get(name, {}))
752
+ return attrs
753
+
754
+ def _style_list_from_attrs(self, attrs):
755
+ style = []
756
+ if attrs.get('bold'):
757
+ style.append('bold')
758
+ if attrs.get('italic'):
759
+ style.append('italic')
760
+ return style
761
+
762
+ def _find_offset(self, query, start=None, case_insensitive=False):
763
+ if not query:
764
+ return None
765
+ offset = 0 if start is None else self._offset_of(start)
766
+ haystack = self._text
767
+ needle = query
768
+ if case_insensitive:
769
+ haystack = haystack.lower()
770
+ needle = needle.lower()
771
+ idx = haystack.find(needle, offset)
772
+ if idx < 0:
773
+ return None
774
+ return (idx, idx + len(query))
775
+
776
+ def _find_all_offsets(self, query, start=None, case_insensitive=False):
777
+ if not query:
778
+ return []
779
+ offsets = []
780
+ offset = 0 if start is None else self._offset_of(start)
781
+ haystack = self._text
782
+ needle = query
783
+ if case_insensitive:
784
+ haystack = haystack.lower()
785
+ needle = needle.lower()
786
+ while True:
787
+ idx = haystack.find(needle, offset)
788
+ if idx < 0:
789
+ break
790
+ offsets.append((idx, idx + len(query)))
791
+ offset = idx + max(1, len(query))
792
+ return offsets
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -17,7 +17,7 @@ Classifier: Topic :: Software Development :: User Interfaces
17
17
  Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE.md
20
- Requires-Dist: pgwidgets-js>=0.2.0
20
+ Requires-Dist: pgwidgets-js>=0.3.4
21
21
  Requires-Dist: websockets>=12
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: sphinx; extra == "dev"
@@ -3,22 +3,22 @@ pgwidgets/_json.py,sha256=o21qywJ6yAldbqxTq3nLwgK9O67r3a8JxhvI_WAJnMY,2184
3
3
  pgwidgets/buffer.py,sha256=BYj_nb6fuyGsBqUp3_Z1YrzUm4h5VmoWPGKvpTjWrYk,4048
4
4
  pgwidgets/callbacks.py,sha256=gA2FnX0N5BmbmOMyEV35yHySgAfVjfAZcZhZbo7j4-c,3314
5
5
  pgwidgets/defs.py,sha256=Q8qhvTeansFU1Av6j5ncdwF2xSNHrpXuKErdHWQ3HiA,436
6
- pgwidgets/method_types.py,sha256=_-rdE5BOYLra8x3BMKv5-QS4Z8YB29DBhUCDsGiB-Gw,18314
6
+ pgwidgets/method_types.py,sha256=kfLBghv0nQFKr4ZrF0VC7MGOEU5qvdbW0PCLPhsrFJg,19462
7
+ pgwidgets/text_model.py,sha256=xlSAoh1ikIG9ZYVwx5wHWrohkGkyu7R6Cbs36_xnbMg,29313
7
8
  pgwidgets/async_/Widgets.py,sha256=vld2xkvusBBEVbhbezaJsjBRRj3L7c17Up0pOVs8PGo,723
8
9
  pgwidgets/async_/__init__.py,sha256=rXB-v9XRrYt1imYuYikhkzIyiRqaYhlTpQei2LHaQ18,564
9
- pgwidgets/async_/application.py,sha256=2RJNRerXu5LXG_UcMSi9HWOCAlEOu96TrRaoIcUZ86Q,80957
10
+ pgwidgets/async_/application.py,sha256=VndsY-j1nD_MQjCQz6nugww8hQjJVIwsn6nOpp3qBUA,81438
10
11
  pgwidgets/async_/widget.py,sha256=gO7eJh9LV3UY7yRLdeml_0gqmHnFB5VU3NWlDURLtfc,38042
11
12
  pgwidgets/extras/__init__.py,sha256=AXUmFtnn4RSlp6pJX6z55j-m_29VmfzpYIjQvAy7pO0,325
12
- pgwidgets/extras/file_browser.py,sha256=CXQSfJx1VQ-fJfhKNrVyqrkKTJEQngmiP954y70Fjo0,17229
13
+ pgwidgets/extras/file_browser.py,sha256=Lkz_n56_YW7rV_k8pmhgZrwulGsujk9vrzv66zgIgec,17347
13
14
  pgwidgets/icons/pgw-python-logo.png,sha256=_Rxicp9EKy2Pzz1HtobxEnntSI-y5TtAVPlIzZIsfYs,2428840
14
15
  pgwidgets/sync/Widgets.py,sha256=7SaocMVMzFHhi51pjuEAr47Wez90b_a61kDbiv0jOfM,706
15
16
  pgwidgets/sync/__init__.py,sha256=SF5RTAvtu8BbYBWzpiCPipy6DJzNXf6nqk9xdvwxUhQ,542
16
- pgwidgets/sync/application.py,sha256=tcAJvSVdijfT_KBP4llY1tyg2AAmQM-1qocw8Zf51nM,96240
17
- pgwidgets/sync/widget.py,sha256=yrScNkZB34u0JXn3-kCB-1mPLxLysVbuaPsCjC8kq0E,38343
18
- pgwidgets_python-0.3.3.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
19
- pgwidgets_python-0.3.3.dist-info/METADATA,sha256=zGKe1dIOc9aQaTS5ROZQu5NFO0rXlqfv3FuavZmjKsA,4683
20
- pgwidgets_python-0.3.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
- pgwidgets_python-0.3.3.dist-info/scm_file_list.json,sha256=EpRYjywwhXPISys2-onyGmwpWbD6-PyVRKtlpIbaRsM,1943
22
- pgwidgets_python-0.3.3.dist-info/scm_version.json,sha256=QuXBS8rkNOzsOyOLL_7d599vXJRfhJZxVPwFnAqG4fk,160
23
- pgwidgets_python-0.3.3.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
24
- pgwidgets_python-0.3.3.dist-info/RECORD,,
17
+ pgwidgets/sync/application.py,sha256=_2VG9h8tJjZZBD3x1oLA216jN8CxAYt0_fECwwcmx5k,97313
18
+ pgwidgets/sync/text_source.py,sha256=Vz8mJsQXLFOAVwZCOrAXSixNfYQHjYZxxAIZjWBCAnY,8341
19
+ pgwidgets/sync/widget.py,sha256=Kw2CQFOaa7rfroKfn0MwNwXT-PjoecqCErav5BEc0UI,38652
20
+ pgwidgets_python-0.3.5.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
21
+ pgwidgets_python-0.3.5.dist-info/METADATA,sha256=QpifhF0maJe_O5ECaHL4pd-IaPI2Sd2GT5hL8_cDOao,4683
22
+ pgwidgets_python-0.3.5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
23
+ pgwidgets_python-0.3.5.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
24
+ pgwidgets_python-0.3.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,68 +0,0 @@
1
- {
2
- "files": [
3
- ".flake8",
4
- "test_pg2.py",
5
- "README.md",
6
- "pyproject.toml",
7
- "LICENSE.md",
8
- ".readthedocs.yaml",
9
- ".gitignore",
10
- "docs/web-servers.rst",
11
- "docs/WhatsNew.rst",
12
- "docs/subclassing.rst",
13
- "docs/sync.rst",
14
- "docs/utilities.rst",
15
- "docs/extras.rst",
16
- "docs/async.rst",
17
- "docs/architecture.rst",
18
- "docs/widgets.rst",
19
- "docs/conf.py",
20
- "docs/Makefile",
21
- "docs/index.rst",
22
- "docs/callbacks.rst",
23
- "docs/getting-started.rst",
24
- "docs/api/async.rst",
25
- "docs/api/sync.rst",
26
- "docs/api/index.rst",
27
- "tests/test_protocol.py",
28
- "tests/__init__.py",
29
- "tests/test_stateful.py",
30
- "tests/test_resolve_kwargs.py",
31
- "tests/test_async_session.py",
32
- "tests/test_defs.py",
33
- "tests/test_fonts.py",
34
- "tests/test_session.py",
35
- "tests/test_extras.py",
36
- "tests/test_widget_classes.py",
37
- "tests/test_reconstruct.py",
38
- "pgwidgets/_json.py",
39
- "pgwidgets/__init__.py",
40
- "pgwidgets/callbacks.py",
41
- "pgwidgets/method_types.py",
42
- "pgwidgets/buffer.py",
43
- "pgwidgets/defs.py",
44
- "pgwidgets/sync/Widgets.py",
45
- "pgwidgets/sync/__init__.py",
46
- "pgwidgets/sync/application.py",
47
- "pgwidgets/sync/widget.py",
48
- "pgwidgets/extras/__init__.py",
49
- "pgwidgets/extras/file_browser.py",
50
- "pgwidgets/icons/pgw-python-logo.png",
51
- "pgwidgets/async_/Widgets.py",
52
- "pgwidgets/async_/application.py",
53
- "pgwidgets/async_/__init__.py",
54
- "pgwidgets/async_/widget.py",
55
- "examples/demo_sync.py",
56
- "examples/all_widgets.py",
57
- "examples/all_widgets_async.py",
58
- "examples/README.md",
59
- "examples/demo_treeview.py",
60
- "examples/demo_async.py",
61
- "examples/flask-multi-process/server.py",
62
- "examples/flask-multi-process/README.md",
63
- "examples/flask-multi-process/nginx.conf",
64
- "examples/flask-multi-process/gunicorn.conf.py",
65
- "examples/flask-multi-process/user_app.py",
66
- ".github/workflows/tests.yml"
67
- ]
68
- }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.3.3",
3
- "distance": 0,
4
- "node": "g522f05596d51c6b3f6f671895e54ec014515d3c7",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-06-25"
8
- }