ยปCore Development>Code coverage>Lib/idlelib/tabbedpages.py

Python code coverage for Lib/idlelib/tabbedpages.py

#countcontent
1n/a"""An implementation of tabbed pages using only standard Tkinter.
2n/a
3n/aOriginally developed for use in IDLE. Based on tabpage.py.
4n/a
5n/aClasses exported:
6n/aTabbedPageSet -- A Tkinter implementation of a tabbed-page widget.
7n/aTabSet -- A widget containing tabs (buttons) in one or more rows.
8n/a
9n/a"""
10n/afrom tkinter import *
11n/a
12n/aclass InvalidNameError(Exception): pass
13n/aclass AlreadyExistsError(Exception): pass
14n/a
15n/a
16n/aclass TabSet(Frame):
17n/a """A widget containing tabs (buttons) in one or more rows.
18n/a
19n/a Only one tab may be selected at a time.
20n/a
21n/a """
22n/a def __init__(self, page_set, select_command,
23n/a tabs=None, n_rows=1, max_tabs_per_row=5,
24n/a expand_tabs=False, **kw):
25n/a """Constructor arguments:
26n/a
27n/a select_command -- A callable which will be called when a tab is
28n/a selected. It is called with the name of the selected tab as an
29n/a argument.
30n/a
31n/a tabs -- A list of strings, the names of the tabs. Should be specified in
32n/a the desired tab order. The first tab will be the default and first
33n/a active tab. If tabs is None or empty, the TabSet will be initialized
34n/a empty.
35n/a
36n/a n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is
37n/a None, then the number of rows will be decided by TabSet. See
38n/a _arrange_tabs() for details.
39n/a
40n/a max_tabs_per_row -- Used for deciding how many rows of tabs are needed,
41n/a when the number of rows is not constant. See _arrange_tabs() for
42n/a details.
43n/a
44n/a """
45n/a Frame.__init__(self, page_set, **kw)
46n/a self.select_command = select_command
47n/a self.n_rows = n_rows
48n/a self.max_tabs_per_row = max_tabs_per_row
49n/a self.expand_tabs = expand_tabs
50n/a self.page_set = page_set
51n/a
52n/a self._tabs = {}
53n/a self._tab2row = {}
54n/a if tabs:
55n/a self._tab_names = list(tabs)
56n/a else:
57n/a self._tab_names = []
58n/a self._selected_tab = None
59n/a self._tab_rows = []
60n/a
61n/a self.padding_frame = Frame(self, height=2,
62n/a borderwidth=0, relief=FLAT,
63n/a background=self.cget('background'))
64n/a self.padding_frame.pack(side=TOP, fill=X, expand=False)
65n/a
66n/a self._arrange_tabs()
67n/a
68n/a def add_tab(self, tab_name):
69n/a """Add a new tab with the name given in tab_name."""
70n/a if not tab_name:
71n/a raise InvalidNameError("Invalid Tab name: '%s'" % tab_name)
72n/a if tab_name in self._tab_names:
73n/a raise AlreadyExistsError("Tab named '%s' already exists" %tab_name)
74n/a
75n/a self._tab_names.append(tab_name)
76n/a self._arrange_tabs()
77n/a
78n/a def remove_tab(self, tab_name):
79n/a """Remove the tab named <tab_name>"""
80n/a if not tab_name in self._tab_names:
81n/a raise KeyError("No such Tab: '%s" % tab_name)
82n/a
83n/a self._tab_names.remove(tab_name)
84n/a self._arrange_tabs()
85n/a
86n/a def set_selected_tab(self, tab_name):
87n/a """Show the tab named <tab_name> as the selected one"""
88n/a if tab_name == self._selected_tab:
89n/a return
90n/a if tab_name is not None and tab_name not in self._tabs:
91n/a raise KeyError("No such Tab: '%s" % tab_name)
92n/a
93n/a # deselect the current selected tab
94n/a if self._selected_tab is not None:
95n/a self._tabs[self._selected_tab].set_normal()
96n/a self._selected_tab = None
97n/a
98n/a if tab_name is not None:
99n/a # activate the tab named tab_name
100n/a self._selected_tab = tab_name
101n/a tab = self._tabs[tab_name]
102n/a tab.set_selected()
103n/a # move the tab row with the selected tab to the bottom
104n/a tab_row = self._tab2row[tab]
105n/a tab_row.pack_forget()
106n/a tab_row.pack(side=TOP, fill=X, expand=0)
107n/a
108n/a def _add_tab_row(self, tab_names, expand_tabs):
109n/a if not tab_names:
110n/a return
111n/a
112n/a tab_row = Frame(self)
113n/a tab_row.pack(side=TOP, fill=X, expand=0)
114n/a self._tab_rows.append(tab_row)
115n/a
116n/a for tab_name in tab_names:
117n/a tab = TabSet.TabButton(tab_name, self.select_command,
118n/a tab_row, self)
119n/a if expand_tabs:
120n/a tab.pack(side=LEFT, fill=X, expand=True)
121n/a else:
122n/a tab.pack(side=LEFT)
123n/a self._tabs[tab_name] = tab
124n/a self._tab2row[tab] = tab_row
125n/a
126n/a # tab is the last one created in the above loop
127n/a tab.is_last_in_row = True
128n/a
129n/a def _reset_tab_rows(self):
130n/a while self._tab_rows:
131n/a tab_row = self._tab_rows.pop()
132n/a tab_row.destroy()
133n/a self._tab2row = {}
134n/a
135n/a def _arrange_tabs(self):
136n/a """
137n/a Arrange the tabs in rows, in the order in which they were added.
138n/a
139n/a If n_rows >= 1, this will be the number of rows used. Otherwise the
140n/a number of rows will be calculated according to the number of tabs and
141n/a max_tabs_per_row. In this case, the number of rows may change when
142n/a adding/removing tabs.
143n/a
144n/a """
145n/a # remove all tabs and rows
146n/a while self._tabs:
147n/a self._tabs.popitem()[1].destroy()
148n/a self._reset_tab_rows()
149n/a
150n/a if not self._tab_names:
151n/a return
152n/a
153n/a if self.n_rows is not None and self.n_rows > 0:
154n/a n_rows = self.n_rows
155n/a else:
156n/a # calculate the required number of rows
157n/a n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1
158n/a
159n/a # not expanding the tabs with more than one row is very ugly
160n/a expand_tabs = self.expand_tabs or n_rows > 1
161n/a i = 0 # index in self._tab_names
162n/a for row_index in range(n_rows):
163n/a # calculate required number of tabs in this row
164n/a n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1
165n/a tab_names = self._tab_names[i:i + n_tabs]
166n/a i += n_tabs
167n/a self._add_tab_row(tab_names, expand_tabs)
168n/a
169n/a # re-select selected tab so it is properly displayed
170n/a selected = self._selected_tab
171n/a self.set_selected_tab(None)
172n/a if selected in self._tab_names:
173n/a self.set_selected_tab(selected)
174n/a
175n/a class TabButton(Frame):
176n/a """A simple tab-like widget."""
177n/a
178n/a bw = 2 # borderwidth
179n/a
180n/a def __init__(self, name, select_command, tab_row, tab_set):
181n/a """Constructor arguments:
182n/a
183n/a name -- The tab's name, which will appear in its button.
184n/a
185n/a select_command -- The command to be called upon selection of the
186n/a tab. It is called with the tab's name as an argument.
187n/a
188n/a """
189n/a Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED)
190n/a
191n/a self.name = name
192n/a self.select_command = select_command
193n/a self.tab_set = tab_set
194n/a self.is_last_in_row = False
195n/a
196n/a self.button = Radiobutton(
197n/a self, text=name, command=self._select_event,
198n/a padx=5, pady=1, takefocus=FALSE, indicatoron=FALSE,
199n/a highlightthickness=0, selectcolor='', borderwidth=0)
200n/a self.button.pack(side=LEFT, fill=X, expand=True)
201n/a
202n/a self._init_masks()
203n/a self.set_normal()
204n/a
205n/a def _select_event(self, *args):
206n/a """Event handler for tab selection.
207n/a
208n/a With TabbedPageSet, this calls TabbedPageSet.change_page, so that
209n/a selecting a tab changes the page.
210n/a
211n/a Note that this does -not- call set_selected -- it will be called by
212n/a TabSet.set_selected_tab, which should be called when whatever the
213n/a tabs are related to changes.
214n/a
215n/a """
216n/a self.select_command(self.name)
217n/a return
218n/a
219n/a def set_selected(self):
220n/a """Assume selected look"""
221n/a self._place_masks(selected=True)
222n/a
223n/a def set_normal(self):
224n/a """Assume normal look"""
225n/a self._place_masks(selected=False)
226n/a
227n/a def _init_masks(self):
228n/a page_set = self.tab_set.page_set
229n/a background = page_set.pages_frame.cget('background')
230n/a # mask replaces the middle of the border with the background color
231n/a self.mask = Frame(page_set, borderwidth=0, relief=FLAT,
232n/a background=background)
233n/a # mskl replaces the bottom-left corner of the border with a normal
234n/a # left border
235n/a self.mskl = Frame(page_set, borderwidth=0, relief=FLAT,
236n/a background=background)
237n/a self.mskl.ml = Frame(self.mskl, borderwidth=self.bw,
238n/a relief=RAISED)
239n/a self.mskl.ml.place(x=0, y=-self.bw,
240n/a width=2*self.bw, height=self.bw*4)
241n/a # mskr replaces the bottom-right corner of the border with a normal
242n/a # right border
243n/a self.mskr = Frame(page_set, borderwidth=0, relief=FLAT,
244n/a background=background)
245n/a self.mskr.mr = Frame(self.mskr, borderwidth=self.bw,
246n/a relief=RAISED)
247n/a
248n/a def _place_masks(self, selected=False):
249n/a height = self.bw
250n/a if selected:
251n/a height += self.bw
252n/a
253n/a self.mask.place(in_=self,
254n/a relx=0.0, x=0,
255n/a rely=1.0, y=0,
256n/a relwidth=1.0, width=0,
257n/a relheight=0.0, height=height)
258n/a
259n/a self.mskl.place(in_=self,
260n/a relx=0.0, x=-self.bw,
261n/a rely=1.0, y=0,
262n/a relwidth=0.0, width=self.bw,
263n/a relheight=0.0, height=height)
264n/a
265n/a page_set = self.tab_set.page_set
266n/a if selected and ((not self.is_last_in_row) or
267n/a (self.winfo_rootx() + self.winfo_width() <
268n/a page_set.winfo_rootx() + page_set.winfo_width())
269n/a ):
270n/a # for a selected tab, if its rightmost edge isn't on the
271n/a # rightmost edge of the page set, the right mask should be one
272n/a # borderwidth shorter (vertically)
273n/a height -= self.bw
274n/a
275n/a self.mskr.place(in_=self,
276n/a relx=1.0, x=0,
277n/a rely=1.0, y=0,
278n/a relwidth=0.0, width=self.bw,
279n/a relheight=0.0, height=height)
280n/a
281n/a self.mskr.mr.place(x=-self.bw, y=-self.bw,
282n/a width=2*self.bw, height=height + self.bw*2)
283n/a
284n/a # finally, lower the tab set so that all of the frames we just
285n/a # placed hide it
286n/a self.tab_set.lower()
287n/a
288n/a
289n/aclass TabbedPageSet(Frame):
290n/a """A Tkinter tabbed-pane widget.
291n/a
292n/a Constains set of 'pages' (or 'panes') with tabs above for selecting which
293n/a page is displayed. Only one page will be displayed at a time.
294n/a
295n/a Pages may be accessed through the 'pages' attribute, which is a dictionary
296n/a of pages, using the name given as the key. A page is an instance of a
297n/a subclass of Tk's Frame widget.
298n/a
299n/a The page widgets will be created (and destroyed when required) by the
300n/a TabbedPageSet. Do not call the page's pack/place/grid/destroy methods.
301n/a
302n/a Pages may be added or removed at any time using the add_page() and
303n/a remove_page() methods.
304n/a
305n/a """
306n/a
307n/a class Page(object):
308n/a """Abstract base class for TabbedPageSet's pages.
309n/a
310n/a Subclasses must override the _show() and _hide() methods.
311n/a
312n/a """
313n/a uses_grid = False
314n/a
315n/a def __init__(self, page_set):
316n/a self.frame = Frame(page_set, borderwidth=2, relief=RAISED)
317n/a
318n/a def _show(self):
319n/a raise NotImplementedError
320n/a
321n/a def _hide(self):
322n/a raise NotImplementedError
323n/a
324n/a class PageRemove(Page):
325n/a """Page class using the grid placement manager's "remove" mechanism."""
326n/a uses_grid = True
327n/a
328n/a def _show(self):
329n/a self.frame.grid(row=0, column=0, sticky=NSEW)
330n/a
331n/a def _hide(self):
332n/a self.frame.grid_remove()
333n/a
334n/a class PageLift(Page):
335n/a """Page class using the grid placement manager's "lift" mechanism."""
336n/a uses_grid = True
337n/a
338n/a def __init__(self, page_set):
339n/a super(TabbedPageSet.PageLift, self).__init__(page_set)
340n/a self.frame.grid(row=0, column=0, sticky=NSEW)
341n/a self.frame.lower()
342n/a
343n/a def _show(self):
344n/a self.frame.lift()
345n/a
346n/a def _hide(self):
347n/a self.frame.lower()
348n/a
349n/a class PagePackForget(Page):
350n/a """Page class using the pack placement manager's "forget" mechanism."""
351n/a def _show(self):
352n/a self.frame.pack(fill=BOTH, expand=True)
353n/a
354n/a def _hide(self):
355n/a self.frame.pack_forget()
356n/a
357n/a def __init__(self, parent, page_names=None, page_class=PageLift,
358n/a n_rows=1, max_tabs_per_row=5, expand_tabs=False,
359n/a **kw):
360n/a """Constructor arguments:
361n/a
362n/a page_names -- A list of strings, each will be the dictionary key to a
363n/a page's widget, and the name displayed on the page's tab. Should be
364n/a specified in the desired page order. The first page will be the default
365n/a and first active page. If page_names is None or empty, the
366n/a TabbedPageSet will be initialized empty.
367n/a
368n/a n_rows, max_tabs_per_row -- Parameters for the TabSet which will
369n/a manage the tabs. See TabSet's docs for details.
370n/a
371n/a page_class -- Pages can be shown/hidden using three mechanisms:
372n/a
373n/a * PageLift - All pages will be rendered one on top of the other. When
374n/a a page is selected, it will be brought to the top, thus hiding all
375n/a other pages. Using this method, the TabbedPageSet will not be resized
376n/a when pages are switched. (It may still be resized when pages are
377n/a added/removed.)
378n/a
379n/a * PageRemove - When a page is selected, the currently showing page is
380n/a hidden, and the new page shown in its place. Using this method, the
381n/a TabbedPageSet may resize when pages are changed.
382n/a
383n/a * PagePackForget - This mechanism uses the pack placement manager.
384n/a When a page is shown it is packed, and when it is hidden it is
385n/a unpacked (i.e. pack_forget). This mechanism may also cause the
386n/a TabbedPageSet to resize when the page is changed.
387n/a
388n/a """
389n/a Frame.__init__(self, parent, **kw)
390n/a
391n/a self.page_class = page_class
392n/a self.pages = {}
393n/a self._pages_order = []
394n/a self._current_page = None
395n/a self._default_page = None
396n/a
397n/a self.columnconfigure(0, weight=1)
398n/a self.rowconfigure(1, weight=1)
399n/a
400n/a self.pages_frame = Frame(self)
401n/a self.pages_frame.grid(row=1, column=0, sticky=NSEW)
402n/a if self.page_class.uses_grid:
403n/a self.pages_frame.columnconfigure(0, weight=1)
404n/a self.pages_frame.rowconfigure(0, weight=1)
405n/a
406n/a # the order of the following commands is important
407n/a self._tab_set = TabSet(self, self.change_page, n_rows=n_rows,
408n/a max_tabs_per_row=max_tabs_per_row,
409n/a expand_tabs=expand_tabs)
410n/a if page_names:
411n/a for name in page_names:
412n/a self.add_page(name)
413n/a self._tab_set.grid(row=0, column=0, sticky=NSEW)
414n/a
415n/a self.change_page(self._default_page)
416n/a
417n/a def add_page(self, page_name):
418n/a """Add a new page with the name given in page_name."""
419n/a if not page_name:
420n/a raise InvalidNameError("Invalid TabPage name: '%s'" % page_name)
421n/a if page_name in self.pages:
422n/a raise AlreadyExistsError(
423n/a "TabPage named '%s' already exists" % page_name)
424n/a
425n/a self.pages[page_name] = self.page_class(self.pages_frame)
426n/a self._pages_order.append(page_name)
427n/a self._tab_set.add_tab(page_name)
428n/a
429n/a if len(self.pages) == 1: # adding first page
430n/a self._default_page = page_name
431n/a self.change_page(page_name)
432n/a
433n/a def remove_page(self, page_name):
434n/a """Destroy the page whose name is given in page_name."""
435n/a if not page_name in self.pages:
436n/a raise KeyError("No such TabPage: '%s" % page_name)
437n/a
438n/a self._pages_order.remove(page_name)
439n/a
440n/a # handle removing last remaining, default, or currently shown page
441n/a if len(self._pages_order) > 0:
442n/a if page_name == self._default_page:
443n/a # set a new default page
444n/a self._default_page = self._pages_order[0]
445n/a else:
446n/a self._default_page = None
447n/a
448n/a if page_name == self._current_page:
449n/a self.change_page(self._default_page)
450n/a
451n/a self._tab_set.remove_tab(page_name)
452n/a page = self.pages.pop(page_name)
453n/a page.frame.destroy()
454n/a
455n/a def change_page(self, page_name):
456n/a """Show the page whose name is given in page_name."""
457n/a if self._current_page == page_name:
458n/a return
459n/a if page_name is not None and page_name not in self.pages:
460n/a raise KeyError("No such TabPage: '%s'" % page_name)
461n/a
462n/a if self._current_page is not None:
463n/a self.pages[self._current_page]._hide()
464n/a self._current_page = None
465n/a
466n/a if page_name is not None:
467n/a self._current_page = page_name
468n/a self.pages[page_name]._show()
469n/a
470n/a self._tab_set.set_selected_tab(page_name)
471n/a
472n/a
473n/adef _tabbed_pages(parent): # htest #
474n/a top=Toplevel(parent)
475n/a x, y = map(int, parent.geometry().split('+')[1:])
476n/a top.geometry("+%d+%d" % (x, y + 175))
477n/a top.title("Test tabbed pages")
478n/a tabPage=TabbedPageSet(top, page_names=['Foobar','Baz'], n_rows=0,
479n/a expand_tabs=False,
480n/a )
481n/a tabPage.pack(side=TOP, expand=TRUE, fill=BOTH)
482n/a Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack()
483n/a Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack()
484n/a Label(tabPage.pages['Baz'].frame, text='Baz').pack()
485n/a entryPgName=Entry(top)
486n/a buttonAdd=Button(top, text='Add Page',
487n/a command=lambda:tabPage.add_page(entryPgName.get()))
488n/a buttonRemove=Button(top, text='Remove Page',
489n/a command=lambda:tabPage.remove_page(entryPgName.get()))
490n/a labelPgName=Label(top, text='name of page to add/remove:')
491n/a buttonAdd.pack(padx=5, pady=5)
492n/a buttonRemove.pack(padx=5, pady=5)
493n/a labelPgName.pack(padx=5)
494n/a entryPgName.pack(padx=5)
495n/a
496n/aif __name__ == '__main__':
497n/a from idlelib.idle_test.htest import run
498n/a run(_tabbed_pages)