-
Notifications
You must be signed in to change notification settings - Fork 102
Expand file tree
/
Copy pathcolumn.py
More file actions
442 lines (391 loc) · 20.1 KB
/
column.py
File metadata and controls
442 lines (391 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
from __future__ import annotations
import tkinter as tk
import warnings
import FreeSimpleGUI
from FreeSimpleGUI import _make_ttk_scrollbar
from FreeSimpleGUI import _random_error_emoji
from FreeSimpleGUI import ELEM_TYPE_COLUMN
from FreeSimpleGUI import popup_error
from FreeSimpleGUI import VarHolder
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.elements.base import Element
class TkFixedFrame(tk.Frame):
"""
A tkinter frame that is used with Column Elements that do not have a scrollbar
"""
def __init__(self, master, **kwargs):
"""
:param master: The parent widget
:type master: (tk.Widget)
:param **kwargs: The keyword args
:type **kwargs:
"""
tk.Frame.__init__(self, master, **kwargs)
self.canvas = tk.Canvas(self)
self.canvas.pack(side='left', fill='both', expand=True)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.TKFrame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window(0, 0, window=self.TKFrame, anchor='nw')
self.canvas.config(borderwidth=0, highlightthickness=0)
self.TKFrame.config(borderwidth=0, highlightthickness=0)
self.config(borderwidth=0, highlightthickness=0)
class TkScrollableFrame(tk.Frame):
"""
A frame with one or two scrollbars. Used to make Columns with scrollbars
"""
def __init__(self, master, vertical_only, element, window, **kwargs):
"""
:param master: The parent widget
:type master: (tk.Widget)
:param vertical_only: if True the only a vertical scrollbar will be shown
:type vertical_only: (bool)
:param element: The element containing this object
:type element: (Column)
"""
tk.Frame.__init__(self, master, **kwargs)
# create a canvas object and a vertical scrollbar for scrolling it
self.canvas = tk.Canvas(self)
element.Widget = self.canvas
# Okay, we're gonna make a list. Containing the y-min, x-min, y-max, and x-max of the frame
element.element_frame = self
_make_ttk_scrollbar(element, 'v', window)
# element.vsb = tk.Scrollbar(self, orient=tk.VERTICAL)
element.vsb.pack(side='right', fill='y', expand='false')
if not vertical_only:
_make_ttk_scrollbar(element, 'h', window)
# self.hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL)
element.hsb.pack(side='bottom', fill='x', expand='false')
self.canvas.config(xscrollcommand=element.hsb.set)
self.canvas.config(yscrollcommand=element.vsb.set)
self.canvas.pack(side='left', fill='both', expand=True)
element.vsb.config(command=self.canvas.yview)
if not vertical_only:
element.hsb.config(command=self.canvas.xview)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.TKFrame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window(0, 0, window=self.TKFrame, anchor='nw')
self.canvas.config(borderwidth=0, highlightthickness=0)
self.TKFrame.config(borderwidth=0, highlightthickness=0)
self.config(borderwidth=0, highlightthickness=0)
# Canvas can be: master, canvas, TKFrame
self.unhookMouseWheel(None)
self.canvas.bind('<Enter>', self.hookMouseWheel)
self.canvas.bind('<Leave>', self.unhookMouseWheel)
self.bind('<Configure>', self.set_scrollregion)
def hookMouseWheel(self, e):
# print("enter")
VarHolder.canvas_holder = self.canvas
self.canvas.bind_all('<4>', self.yscroll, add='+')
self.canvas.bind_all('<5>', self.yscroll, add='+')
self.canvas.bind_all('<MouseWheel>', self.yscroll, add='+')
self.canvas.bind_all('<Shift-MouseWheel>', self.xscroll, add='+')
# Chr0nic
def unhookMouseWheel(self, e):
# print("leave")
VarHolder.canvas_holder = None
self.canvas.unbind_all('<4>')
self.canvas.unbind_all('<5>')
self.canvas.unbind_all('<MouseWheel>')
self.canvas.unbind_all('<Shift-MouseWheel>')
def resize_frame(self, e):
self.canvas.itemconfig(self.frame_id, height=e.height, width=e.width)
def yscroll(self, event):
if self.canvas.yview() == (0.0, 1.0):
return
if event.num == 5 or event.delta < 0:
self.canvas.yview_scroll(1, 'unit')
elif event.num == 4 or event.delta > 0:
self.canvas.yview_scroll(-1, 'unit')
def xscroll(self, event):
if event.num == 5 or event.delta < 0:
self.canvas.xview_scroll(1, 'unit')
elif event.num == 4 or event.delta > 0:
self.canvas.xview_scroll(-1, 'unit')
def bind_mouse_scroll(self, parent, mode):
# ~~ Windows only
parent.bind('<MouseWheel>', mode)
# ~~ Unix only
parent.bind('<Button-4>', mode)
parent.bind('<Button-5>', mode)
def set_scrollregion(self, event=None):
"""Set the scroll region on the canvas"""
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
class Column(Element):
"""
A container element that is used to create a layout within your window's layout
"""
def __init__(
self,
layout,
background_color=None,
size=(None, None),
s=(None, None),
size_subsample_width=1,
size_subsample_height=2,
pad=None,
p=None,
scrollable=False,
vertical_scroll_only=False,
right_click_menu=None,
key=None,
k=None,
visible=True,
justification=None,
element_justification=None,
vertical_alignment=None,
grab=None,
expand_x=None,
expand_y=None,
metadata=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
):
"""
:param layout: Layout that will be shown in the Column container
:type layout: List[List[Element]]
:param background_color: color of background of entire Column
:type background_color: (str)
:param size: (width, height) size in pixels (doesn't work quite right, sometimes only 1 dimension is set by tkinter. Use a Sizer Element to help set sizes
:type size: (int | None, int | None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int | None, int | None)
:param size_subsample_width: Determines the size of a scrollable column width based on 1/size_subsample * required size. 1 = match the contents exactly, 2 = 1/2 contents size, 3 = 1/3. Can be a fraction to make larger than required.
:type size_subsample_width: (float)
:param size_subsample_height: Determines the size of a scrollable height based on 1/size_subsample * required size. 1 = match the contents exactly, 2 = 1/2 contents size, 3 = 1/3. Can be a fraction to make larger than required..
:type size_subsample_height: (float)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param scrollable: if True then scrollbars will be added to the column. If you update the contents of a scrollable column, be sure and call Column.contents_changed also
:type scrollable: (bool)
:param vertical_scroll_only: if True then no horizontal scrollbar will be shown if a scrollable column
:type vertical_scroll_only: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set visibility state of the element
:type visible: (bool)
:param justification: set justification for the Column itself. Note entire row containing the Column will be affected
:type justification: (str)
:param element_justification: All elements inside the Column will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param vertical_alignment: Place the column at the 'top', 'center', 'bottom' of the row (can also use t,c,r). Defaults to no setting (tkinter decides)
:type vertical_alignment: (str)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param expand_x: If True the column will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the column will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.ParentPanedWindow = None
self.Rows = []
self.TKFrame = None
self.TKColFrame = None # type: tk.Frame
self.Scrollable = scrollable
self.VerticalScrollOnly = vertical_scroll_only
self.RightClickMenu = right_click_menu
bg = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
self.Justification = justification
self.VerticalAlignment = vertical_alignment
key = key if key is not None else k
self.Grab = grab
self.expand_x = expand_x
self.expand_y = expand_y
self.Layout(layout)
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.size_subsample_width = size_subsample_width
self.size_subsample_height = size_subsample_height
super().__init__(
ELEM_TYPE_COLUMN,
background_color=bg,
size=sz,
pad=pad,
key=key,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Column Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
CurrentRow = [] # start with a blank row and build up
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if type(element) is list:
popup_error(
'Error creating Column layout',
'Layout has a LIST instead of an ELEMENT',
'This sometimes means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
elif callable(element) and not isinstance(element, Element):
popup_error(
'Error creating Column layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO REUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning,
)
popup_error(
'Error creating Column layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ',
element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
CurrentRow.append(element)
if element.Key is not None:
self.UseDictionary = True
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
def layout(self, rows):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Column)
"""
for row in rows:
try:
iter(row)
except TypeError:
popup_error(
'Error creating Column layout',
'Your row is not an iterable (e.g. a list)',
f'Instead of a list, the type found was {type(row)}',
'The offensive row = ',
row,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
row_num, col_num = location
row = self.Rows[row_num]
element = row[col_num]
return element
def update(self, visible=None):
"""
Changes some of the settings for the Column Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Column.update - The window was closed')
return
if visible is False:
if self.TKColFrame:
self._pack_forget_save_settings()
if self.ParentPanedWindow:
self.ParentPanedWindow.remove(self.TKColFrame)
elif visible is True:
if self.TKColFrame:
self._pack_restore_settings()
if self.ParentPanedWindow:
self.ParentPanedWindow.add(self.TKColFrame)
if visible is not None:
self._visible = visible
def contents_changed(self):
"""
When a scrollable column has part of its layout changed by making elements visible or invisible or the
layout is extended for the Column, then this method needs to be called so that the new scroll area
is computed to match the new contents.
"""
self.TKColFrame.canvas.config(scrollregion=self.TKColFrame.canvas.bbox('all'))
AddRow = add_row
Layout = layout
Update = update
from FreeSimpleGUI.window import Window