Completed
Push — develop ( 538237...38b266 )
by Jace
14s queued 10s
created

Application.create_properties_widget()   A

Complexity

Conditions 1

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 25
rs 9.45
c 0
b 0
f 0
cc 1
nop 2
1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
3
4
"""Graphical interface for Doorstop."""
5
6
import functools
7
import logging
8
import sys
9
from itertools import chain
10
from unittest.mock import Mock
11
12
from doorstop import common
13
from doorstop.common import DoorstopError
14
from doorstop.core import builder, vcs
15
from doorstop.gui import utilTkinter, widget
16
17
try:
18
    import tkinter as tk
19
    from tkinter import ttk
20
    from tkinter import filedialog
21
except ImportError as _exc:
22
    sys.stderr.write("WARNING: {}\n".format(_exc))
23
    tk = Mock()
24
    ttk = Mock()
25
26
27
log = common.logger(__name__)
28
29
30 View Code Duplication
def _log(func):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
31
    """Log name and arguments."""
32
33
    @functools.wraps(func)
34
    def wrapped(self, *args, **kwargs):
35
        sargs = "{}, {}".format(
36
            ', '.join(repr(a) for a in args),
37
            ', '.join("{}={}".format(k, repr(v)) for k, v in kwargs.items()),
38
        )
39
        msg = "log: {}: {}".format(func.__name__, sargs.strip(", "))
40
        if not isinstance(self, ttk.Frame) or not self.ignore:
41
            log.debug(msg.strip())
42
        return func(self, *args, **kwargs)
43
44
    return wrapped
45
46
47
class Application(ttk.Frame):
48
    """Graphical application for Doorstop."""
49
50
    def __init__(self, root, cwd, project):
51
        ttk.Frame.__init__(self, root)
52
53
        # Create Doorstop variables
54
        self.cwd = cwd
55
        self.tree = None
56
        self.document = None
57
        self.item = None
58
59
        # Create string variables
60
        self.stringvar_project = tk.StringVar(value=project or '')
61
        self.stringvar_project.trace('w', self.display_tree)
62
        self.stringvar_document = tk.StringVar()
63
        self.stringvar_document.trace('w', self.display_document)
64
65
        # The stringvar_item holds the uid of the main selected item (or empty string if nothing is selected).
66
        self.stringvar_item = tk.StringVar()
67
        self.stringvar_item.trace('w', self.display_item)
68
69
        self.stringvar_text = tk.StringVar()
70
        self.stringvar_text.trace('w', self.update_item)
71
        self.intvar_active = tk.IntVar()
72
        self.intvar_active.trace('w', self.update_item)
73
        self.intvar_derived = tk.IntVar()
74
        self.intvar_derived.trace('w', self.update_item)
75
        self.intvar_normative = tk.IntVar()
76
        self.intvar_normative.trace('w', self.update_item)
77
        self.intvar_heading = tk.IntVar()
78
        self.intvar_heading.trace('w', self.update_item)
79
        self.stringvar_link = tk.StringVar()  # no trace event
80
        self.stringvar_ref = tk.StringVar()
81
        self.stringvar_ref.trace('w', self.update_item)
82
        self.stringvar_extendedkey = tk.StringVar()
83
        self.stringvar_extendedkey.trace('w', self.display_extended)
84
        self.stringvar_extendedvalue = tk.StringVar()
85
        self.stringvar_extendedvalue.trace('w', self.update_item)
86
87
        # Create widget variables
88
        self.combobox_documents = None
89
        self.text_items = None
90
        self.text_item = None
91
        self.listbox_links = None
92
        self.combobox_extended = None
93
        self.text_extendedvalue = None
94
        self.text_parents = None
95
        self.text_children = None
96
97
        # Initialize the GUI
98
        self.ignore = False  # flag to ignore internal events
99
        frame = self.init(root)
100
        frame.pack(fill=tk.BOTH, expand=1)
101
102
        # Start the application
103
        root.after(500, self.find)
104
105
    def init(self, root):
106
        """Initialize and return the main frame."""
107
        # pylint: disable=attribute-defined-outside-init
108
109
        # Shared arguments
110
        width_text = 30
111
        height_text = 10
112
        height_ext = 5
113
114
        # Shared keyword arguments
115
        kw_f = {'padding': 5}  # constructor arguments for frames
116
        kw_gp = {'padx': 2, 'pady': 2}  # grid arguments for padded widgets
117
        kw_gs = {'sticky': tk.NSEW}  # grid arguments for sticky widgets
118
        kw_gsp = dict(
119
            chain(kw_gs.items(), kw_gp.items())
120
        )  # grid arguments for sticky padded widgets
121
122
        root.bind_all("<Control-minus>", lambda arg: widget.adjustFontSize(-1))
123
        root.bind_all("<Control-equal>", lambda arg: widget.adjustFontSize(1))
124
        root.bind_all("<Control-0>", lambda arg: widget.resetFontSize())
125
126
        # Configure grid
127
        frame = ttk.Frame(root, **kw_f)
128
        frame.rowconfigure(0, weight=0)
129
        frame.rowconfigure(1, weight=1)
130
        frame.columnconfigure(0, weight=2)
131
        frame.columnconfigure(1, weight=1)
132
        frame.columnconfigure(2, weight=1)
133
        frame.columnconfigure(3, weight=2)
134
135
        # Create widgets
136
        def frame_project(root):
137
            """Frame for the current project."""
138
            # Configure grid
139
            frame = ttk.Frame(root, **kw_f)
140
            frame.rowconfigure(0, weight=1)
141
            frame.columnconfigure(0, weight=0)
142
            frame.columnconfigure(1, weight=1)
143
144
            # Place widgets
145
            widget.Label(frame, text="Project:").grid(row=0, column=0, **kw_gp)
146
            widget.Entry(frame, textvariable=self.stringvar_project).grid(
147
                row=0, column=1, **kw_gsp
148
            )
149
150
            return frame
151
152
        def frame_tree(root):
153
            """Frame for the current document."""
154
            # Configure grid
155
            frame = ttk.Frame(root, **kw_f)
156
            frame.rowconfigure(0, weight=1)
157
            frame.columnconfigure(0, weight=0)
158
            frame.columnconfigure(1, weight=1)
159
160
            # Place widgets
161
            widget.Label(frame, text="Document:").grid(row=0, column=0, **kw_gp)
162
            self.combobox_documents = widget.Combobox(
163
                frame, textvariable=self.stringvar_document, state="readonly"
164
            )
165
            self.combobox_documents.grid(row=0, column=1, **kw_gsp)
166
167
            return frame
168
169
        def frame_document(root):
170
            """Frame for current document's outline and items."""
171
            # Configure grid
172
            frame = ttk.Frame(root, **kw_f)
173
            frame.rowconfigure(0, weight=0)
174
            frame.rowconfigure(1, weight=5)
175
            frame.rowconfigure(2, weight=0)
176
            frame.rowconfigure(3, weight=0)
177
            frame.columnconfigure(0, weight=0)
178
            frame.columnconfigure(1, weight=0)
179
            frame.columnconfigure(2, weight=0)
180
            frame.columnconfigure(3, weight=0)
181
            frame.columnconfigure(4, weight=1)
182
            frame.columnconfigure(5, weight=1)
183
184
            @_log
185
            def treeview_outline_treeviewselect(event):
186
                """Handle selecting an item in the tree view."""
187
                if self.ignore:
188
                    return
189
                thewidget = event.widget
190
                curselection = thewidget.selection()
191
                if curselection:
192
                    uid = curselection[0]
193
                    self.stringvar_item.set(uid)
194
195
            @_log
196
            def treeview_outline_delete(event):  # pylint: disable=W0613
197
                """Handle deleting an item in the tree view."""
198
                if self.ignore:
199
                    return
200
                self.remove()
201
202
            # Place widgets
203
            widget.Label(frame, text="Outline:").grid(
204
                row=0, column=0, columnspan=4, sticky=tk.W, **kw_gp
205
            )
206
            widget.Label(frame, text="Items:").grid(
207
                row=0, column=4, columnspan=2, sticky=tk.W, **kw_gp
208
            )
209
            c_columnId = ("Id",)
210
            self.treeview_outline = widget.TreeView(frame, columns=c_columnId)
211
            for col in c_columnId:
212
                self.treeview_outline.heading(col, text=col)
213
214
            # Add a Vertical scrollbar to the Treeview Outline
215
            treeview_outline_verticalScrollBar = widget.ScrollbarV(
216
                frame, command=self.treeview_outline.yview
217
            )
218
            treeview_outline_verticalScrollBar.grid(
219
                row=1, column=0, columnspan=1, **kw_gs
220
            )
221
            self.treeview_outline.configure(
222
                yscrollcommand=treeview_outline_verticalScrollBar.set
223
            )
224
225
            self.treeview_outline.bind(
226
                "<<TreeviewSelect>>", treeview_outline_treeviewselect
227
            )
228
            self.treeview_outline.bind("<Delete>", treeview_outline_delete)
229
            self.treeview_outline.grid(row=1, column=1, columnspan=3, **kw_gsp)
230
            self.text_items = widget.noUserInput_init(
231
                widget.Text(frame, width=width_text, wrap=tk.WORD)
232
            )
233
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
234
            self.text_items_hyperlink = utilTkinter.HyperlinkManager(self.text_items)
235
            widget.Button(frame, text="<", width=0, command=self.left).grid(
236
                row=2, column=0, sticky=tk.EW, padx=(2, 0)
237
            )
238
            widget.Button(frame, text="v", width=0, command=self.down).grid(
239
                row=2, column=1, sticky=tk.EW
240
            )
241
            widget.Button(frame, text="^", width=0, command=self.up).grid(
242
                row=2, column=2, sticky=tk.EW
243
            )
244
            widget.Button(frame, text=">", width=0, command=self.right).grid(
245
                row=2, column=3, sticky=tk.EW, padx=(0, 2)
246
            )
247
            widget.Button(frame, text="Add Item", command=self.add).grid(
248
                row=2, column=4, sticky=tk.W, **kw_gp
249
            )
250
            widget.Button(frame, text="Remove Selected Item", command=self.remove).grid(
251
                row=2, column=5, sticky=tk.E, **kw_gp
252
            )
253
254
            return frame
255
256
        def frame_item(root):
257
            """Frame for the currently selected item."""
258
            # Configure grid
259
            frame = ttk.Frame(root, **kw_f)
260
            frame.rowconfigure(0, weight=0)
261
            frame.rowconfigure(1, weight=4)
262
            frame.rowconfigure(2, weight=0)
263
            frame.rowconfigure(3, weight=1)
264
            frame.rowconfigure(4, weight=1)
265
            frame.rowconfigure(5, weight=1)
266
            frame.rowconfigure(6, weight=1)
267
            frame.rowconfigure(7, weight=0)
268
            frame.rowconfigure(8, weight=0)
269
            frame.rowconfigure(9, weight=0)
270
            frame.rowconfigure(10, weight=0)
271
            frame.rowconfigure(11, weight=4)
272
            frame.columnconfigure(0, weight=1, pad=kw_f['padding'] * 2)
273
            frame.columnconfigure(1, weight=1)
274
275
            @_log
276
            def text_focusin(_):
277
                """Handle entering a text field."""
278
                self.ignore = True
279
280
            @_log
281
            def text_item_focusout(event):
282
                """Handle updated text text."""
283
                self.ignore = False
284
                thewidget = event.widget
285
                value = thewidget.get('1.0', tk.END)
286
                self.stringvar_text.set(value)
287
288
            @_log
289
            def text_extendedvalue_focusout(event):
290
                """Handle updated extended attributes."""
291
                self.ignore = False
292
                thewidget = event.widget
293
                value = thewidget.get('1.0', tk.END)
294
                self.stringvar_extendedvalue.set(value)
295
296
            # Selected Item
297
            widget.Label(frame, text="Selected Item:").grid(
298
                row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp
299
            )
300
            self.text_item = widget.Text(
301
                frame, width=width_text, height=height_text, wrap=tk.WORD
302
            )
303
            self.text_item.bind('<FocusIn>', text_focusin)
304
            self.text_item.bind('<FocusOut>', text_item_focusout)
305
            self.text_item.grid(row=1, column=0, columnspan=3, **kw_gsp)
306
307
            # Column: Properties
308
            self.create_properties_widget(frame).grid(
309
                row=2, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
310
            )
311
312
            # Column: Links
313
            self.create_links_widget(frame).grid(
314
                row=4, rowspan=3, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
315
            )
316
317
            # External Reference
318
            self.create_reference_widget(frame).grid(
319
                row=7, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
320
            )
321
322
            widget.Label(frame, text="Extended Attributes:").grid(
323
                row=9, column=0, columnspan=3, sticky=tk.W, **kw_gp
324
            )
325
            self.combobox_extended = widget.Combobox(
326
                frame, textvariable=self.stringvar_extendedkey
327
            )
328
            self.combobox_extended.grid(row=10, column=0, columnspan=3, **kw_gsp)
329
            self.text_extendedvalue = widget.Text(
330
                frame, width=width_text, height=height_ext, wrap=tk.WORD
331
            )
332
            self.text_extendedvalue.bind('<FocusIn>', text_focusin)
333
            self.text_extendedvalue.bind('<FocusOut>', text_extendedvalue_focusout)
334
            self.text_extendedvalue.grid(row=11, column=0, columnspan=3, **kw_gsp)
335
336
            return frame
337
338
        def frame_family(root):
339
            """Frame for the parent and child document items."""
340
            # Configure grid
341
            frame = ttk.Frame(root, **kw_f)
342
            frame.rowconfigure(0, weight=0)
343
            frame.rowconfigure(1, weight=1)
344
            frame.rowconfigure(2, weight=0)
345
            frame.rowconfigure(3, weight=1)
346
            frame.columnconfigure(0, weight=1)
347
348
            # Place widgets
349
            widget.Label(frame, text="Linked To:").grid(
350
                row=0, column=0, sticky=tk.W, **kw_gp
351
            )
352
            self.text_parents = widget.noUserInput_init(
353
                widget.Text(frame, width=width_text, wrap=tk.WORD)
354
            )
355
            self.text_parents_hyperlink = utilTkinter.HyperlinkManager(
356
                self.text_parents
357
            )
358
            self.text_parents.grid(row=1, column=0, **kw_gsp)
359
            widget.Label(frame, text="Linked From:").grid(
360
                row=2, column=0, sticky=tk.W, **kw_gp
361
            )
362
            self.text_children = widget.noUserInput_init(
363
                widget.Text(frame, width=width_text, wrap=tk.WORD)
364
            )
365
            self.text_children_hyperlink = utilTkinter.HyperlinkManager(
366
                self.text_children
367
            )
368
            self.text_children.grid(row=3, column=0, **kw_gsp)
369
370
            return frame
371
372
        # Place widgets
373
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
374
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
375
        frame_document(frame).grid(row=1, column=0, **kw_gs)
376
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
377
        frame_family(frame).grid(row=1, column=3, **kw_gs)
378
379
        return frame
380
381
    @_log
382
    def find(self):
383
        """Find the root of the project."""
384
        if not self.stringvar_project.get():
385
            try:
386
                path = vcs.find_root(self.cwd)
387
            except DoorstopError as exc:
388
                log.error(exc)
389
            else:
390
                self.stringvar_project.set(path)
391
392
    @_log
393
    def browse(self):
394
        """Browse for the root of a project."""
395
        path = filedialog.askdirectory()
396
        log.debug("path: {}".format(path))
397
        if path:
398
            self.stringvar_project.set(path)
399
400
    @_log
401
    def display_tree(self, *_):
402
        """Display the currently selected tree."""
403
        # Set the current tree
404
        self.tree = builder.build(root=self.stringvar_project.get())
405
        log.info("displaying tree...")
406
407
        # Display the documents in the tree
408
        values = [
409
            "{} ({})".format(document.prefix, document.relpath)
410
            for document in self.tree
411
        ]
412
        self.combobox_documents['values'] = values
413
414
        # Select the first document
415
        if len(self.tree):  # pylint: disable=len-as-condition
416
            self.combobox_documents.current(0)
417
        else:
418
            logging.warning("no documents to display")
419
420
    @_log
421
    def display_document(self, *_):
422
        """Display the currently selected document."""
423
        # Set the current document
424
        index = self.combobox_documents.current()
425
        self.document = list(self.tree)[index]
426
        log.info("displaying document {}...".format(self.document))
427
428
        # Record the currently opened items.
429
        c_openItem = []
430
        for c_currUID in utilTkinter.getAllChildren(self.treeview_outline):
431
            if self.treeview_outline.item(c_currUID)["open"]:
432
                c_openItem.append(c_currUID)
433
434
        # Record the currently selected items.
435
        c_selectedItem = self.treeview_outline.selection()
436
437
        # Clear the widgets
438
        self.treeview_outline.delete(*self.treeview_outline.get_children())
439
        widget.noUserInput_delete(self.text_items, '1.0', tk.END)
440
        self.text_items_hyperlink.reset()
441
442
        # Display the items in the document
443
        c_levelsItem = [""]
444
        for item in self.document.items:
445
            theParent = next(
446
                iter(reversed([x for x in c_levelsItem[: item.depth]])), ""
447
            )
448
449
            while len(c_levelsItem) < item.depth:
450
                c_levelsItem.append(item.uid)
451
            c_levelsItem = c_levelsItem[: item.depth]
452
            for x in range(item.depth):
453
                c_levelsItem.append(item.uid)
454
455
            # Add the item to the document outline
456
            self.treeview_outline.insert(
457
                theParent,
458
                tk.END,
459
                item.uid,
460
                text=item.level,
461
                values=(item.uid,),
462
                open=item.uid in c_openItem,
463
            )
464
465
            # Add the item to the document text
466
            widget.noUserInput_insert(
467
                self.text_items, tk.END, "{t}".format(t=item.text or item.ref or '???')
468
            )
469
            widget.noUserInput_insert(self.text_items, tk.END, " [")
470
            widget.noUserInput_insert(
471
                self.text_items,
472
                tk.END,
473
                item.uid,
474
                self.text_items_hyperlink.add(
475
                    # pylint: disable=unnecessary-lambda
476
                    lambda c_theURL: self.followlink(c_theURL),
477
                    item.uid,
478
                    ["refLink"],
479
                ),
480
            )
481
            widget.noUserInput_insert(self.text_items, tk.END, "]\n\n")
482
483
        # Set tree view selection
484
        c_selectedItem = [
485
            x
486
            for x in c_selectedItem
487
            if x in utilTkinter.getAllChildren(self.treeview_outline)
488
        ]
489
        if c_selectedItem:
490
            # Restore selection
491
            self.treeview_outline.selection_set(c_selectedItem)
492
        else:
493
            # Select the first item
494
            for uid in utilTkinter.getAllChildren(self.treeview_outline):
495
                self.stringvar_item.set(uid)
496
                break
497
            else:
498
                logging.warning("no items to display")
499
                self.stringvar_item.set("")
500
501
    @_log
502
    def display_item(self, *_):
503
        """Display the currently selected item."""
504
        try:
505
            self.ignore = True
506
507
            # Fetch the current item
508
            uid = self.stringvar_item.get()
509
            if uid == "":
510
                self.item = None
511
            else:
512
                try:
513
                    self.item = self.tree.find_item(uid)
514
                except DoorstopError:
515
                    pass
516
            log.info("displaying item {}...".format(self.item))
517
518
            if uid != "":
519
                if uid not in self.treeview_outline.selection():
520
                    self.treeview_outline.selection_set((uid,))
521
                self.treeview_outline.see(uid)
522
523
            # Display the item's text
524
            self.text_item.replace(
525
                '1.0', tk.END, "" if self.item is None else self.item.text
526
            )
527
528
            # Display the item's properties
529
            self.stringvar_text.set("" if self.item is None else self.item.text)
530
            self.intvar_active.set(False if self.item is None else self.item.active)
531
            self.intvar_derived.set(False if self.item is None else self.item.derived)
532
            self.intvar_normative.set(
533
                False if self.item is None else self.item.normative
534
            )
535
            self.intvar_heading.set(False if self.item is None else self.item.heading)
536
537
            # Display the item's links
538
            self.listbox_links.delete(0, tk.END)
539
            if self.item is not None:
540
                for uid in self.item.links:
541
                    self.listbox_links.insert(tk.END, uid)
542
            self.stringvar_link.set('')
543
544
            # Display the item's external reference
545
            self.stringvar_ref.set("" if self.item is None else self.item.ref)
546
547
            # Display the item's extended attributes
548
            values = None if self.item is None else self.item.extended
549
            self.combobox_extended['values'] = values or ['']
550
            if self.item is not None:
551
                self.combobox_extended.current(0)
552
553
            # Display the items this item links to
554
            widget.noUserInput_delete(self.text_parents, '1.0', tk.END)
555
            self.text_parents_hyperlink.reset()
556
            if self.item is not None:
557
                for uid in self.item.links:
558
                    try:
559
                        item = self.tree.find_item(uid)
560
                    except DoorstopError:
561
                        text = "???"
562
                    else:
563
                        text = item.text or item.ref or '???'
564
                        uid = item.uid
565
566
                    widget.noUserInput_insert(
567
                        self.text_parents, tk.END, "{t}".format(t=text)
568
                    )
569
                    widget.noUserInput_insert(self.text_parents, tk.END, " [")
570
                    widget.noUserInput_insert(
571
                        self.text_parents,
572
                        tk.END,
573
                        uid,
574
                        self.text_parents_hyperlink.add(
575
                            # pylint: disable=unnecessary-lambda
576
                            lambda c_theURL: self.followlink(c_theURL),
577
                            uid,
578
                            ["refLink"],
579
                        ),
580
                    )
581
                    widget.noUserInput_insert(self.text_parents, tk.END, "]\n\n")
582
583
            # Display the items this item has links from
584
            widget.noUserInput_delete(self.text_children, '1.0', 'end')
585
            self.text_children_hyperlink.reset()
586
            if self.item is not None:
587
                for uid in self.item.find_child_links():
588
                    item = self.tree.find_item(uid)
589
                    text = item.text or item.ref or '???'
590
                    uid = item.uid
591
592
                    widget.noUserInput_insert(
593
                        self.text_children, tk.END, "{t}".format(t=text)
594
                    )
595
                    widget.noUserInput_insert(self.text_children, tk.END, " [")
596
                    widget.noUserInput_insert(
597
                        self.text_children,
598
                        tk.END,
599
                        uid,
600
                        self.text_children_hyperlink.add(
601
                            # pylint: disable=unnecessary-lambda
602
                            lambda c_theURL: self.followlink(c_theURL),
603
                            uid,
604
                            ["refLink"],
605
                        ),
606
                    )
607
                    widget.noUserInput_insert(self.text_children, tk.END, "]\n\n")
608
        finally:
609
            self.ignore = False
610
611
    @_log
612
    def display_extended(self, *_):
613
        """Display the currently selected extended attribute."""
614
        try:
615
            self.ignore = True
616
617
            name = self.stringvar_extendedkey.get()
618
            log.debug("displaying extended attribute '{}'...".format(name))
619
            self.text_extendedvalue.replace('1.0', tk.END, self.item.get(name, ""))
620
        finally:
621
            self.ignore = False
622
623
    @_log
624
    def update_item(self, *_):
625
        """Update the current item from the fields."""
626
        if self.ignore:
627
            return
628
        if not self.item:
629
            logging.warning("no item selected")
630
            return
631
632
        # Update the current item
633
        log.info("updating {}...".format(self.item))
634
        self.item.auto = False
635
        self.item.text = self.stringvar_text.get()
636
        self.item.active = self.intvar_active.get()
637
        self.item.derived = self.intvar_derived.get()
638
        self.item.normative = self.intvar_normative.get()
639
        self.item.heading = self.intvar_heading.get()
640
        self.item.links = self.listbox_links.get(0, tk.END)
641
        self.item.ref = self.stringvar_ref.get()
642
        name = self.stringvar_extendedkey.get()
643
        if name:
644
            self.item.set(name, self.stringvar_extendedvalue.get())
645
        self.item.save()
646
647
        # Re-select this item
648
        self.display_document()
649
650
    @_log
651
    def left(self):
652
        """Dedent the current item's level."""
653
        self.item.level <<= 1
654
        self.document.reorder(keep=self.item)
655
        self.display_document()
656
657
    @_log
658
    def down(self):
659
        """Increment the current item's level."""
660
        self.item.level += 1
661
        self.document.reorder(keep=self.item)
662
        self.display_document()
663
664
    @_log
665
    def up(self):
666
        """Decrement the current item's level."""
667
        self.item.level -= 1
668
        self.document.reorder(keep=self.item)
669
        self.display_document()
670
671
    @_log
672
    def right(self):
673
        """Indent the current item's level."""
674
        self.item.level >>= 1
675
        self.document.reorder(keep=self.item)
676
        self.display_document()
677
678
    @_log
679
    def add(self):
680
        """Add a new item to the document."""
681
        logging.info("adding item to {}...".format(self.document))
682
        if self.item:
683
            level = self.item.level + 1
684
        else:
685
            level = None
686
        item = self.document.add_item(level=level)
687
        logging.info("added item: {}".format(item))
688
        # Refresh the document view
689
        self.display_document()
690
        # Set the new selection
691
        self.stringvar_item.set(item.uid)
692
693
    @_log
694
    def remove(self):
695
        """Remove the selected item from the document."""
696
        newSelection = ""
697
        for c_currUID in self.treeview_outline.selection():
698
            # Find the item which should be selected once the current selection is removed.
699
            for currNeighbourStrategy in (
700
                self.treeview_outline.next,
701
                self.treeview_outline.prev,
702
                self.treeview_outline.parent,
703
            ):
704
                newSelection = currNeighbourStrategy(c_currUID)
705
                if newSelection != "":
706
                    break
707
            # Remove the item
708
            item = self.tree.find_item(c_currUID)
709
            logging.info("removing item {}...".format(item))
710
            item = self.tree.remove_item(item)
711
            logging.info("removed item: {}".format(item))
712
            # Set the new selection
713
            self.stringvar_item.set(newSelection)
714
            # Refresh the document view
715
            self.display_document()
716
717
    @_log
718
    def link(self):
719
        """Add the specified link to the current item."""
720
        # Add the specified link to the list
721
        uid = self.stringvar_link.get()
722
        if uid:
723
            self.listbox_links.insert(tk.END, uid)
724
            self.stringvar_link.set('')
725
726
            # Update the current item
727
            self.update_item()
728
729
    @_log
730
    def unlink(self):
731
        """Remove the currently selected link from the current item."""
732
        # Remove the selected link from the list (if selected)
733
        index = self.listbox_links.curselection()
734
        if not index:
735
            return
736
        self.listbox_links.delete(index)
737
738
        # Update the current item
739
        self.update_item()
740
741
    @_log
742
    def followlink(self, uid):
743
        """Display a given uid."""
744
        # Update the current item
745
        self.ignore = False
746
        self.update_item()
747
748
        # Load the good document.
749
        document = self.tree.find_document(uid.prefix)
750
        index = list(self.tree).index(document)
751
        self.combobox_documents.current(index)
752
        self.display_document()
753
754
        # load the good Item
755
        self.stringvar_item.set(uid)
756
757
    def create_properties_widget(self, parent):
758
        frame = ttk.Frame(parent)
759
760
        frame.columnconfigure(0, weight=1)
761
        frame.rowconfigure(0, weight=1)
762
        frame.rowconfigure(1, weight=1)
763
        frame.rowconfigure(2, weight=1)
764
        frame.rowconfigure(3, weight=1)
765
        frame.rowconfigure(4, weight=1)
766
767
        widget.Label(frame, text="Properties:").grid(row=0, column=0, sticky=tk.NW)
768
        widget.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(
769
            row=1, column=0, sticky=tk.NW
770
        )
771
        widget.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(
772
            row=2, column=0, sticky=tk.NW
773
        )
774
        widget.Checkbutton(
775
            frame, text="Normative", variable=self.intvar_normative
776
        ).grid(row=3, column=0, sticky=tk.NW)
777
        widget.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(
778
            row=4, column=0, sticky=tk.NW
779
        )
780
781
        return frame
782
783
    def create_links_widget(self, parent):
784
        frame = ttk.Frame(parent)
785
786
        frame.columnconfigure(0, weight=1)
787
        frame.columnconfigure(1, weight=1)
788
        frame.columnconfigure(2, weight=0)
789
        frame.columnconfigure(3, weight=0)
790
        frame.rowconfigure(0, weight=1)
791
        frame.rowconfigure(1, weight=1)
792
        frame.rowconfigure(2, weight=1)
793
794
        width_uid = 10
795
        widget.Label(frame, text="Links:").grid(
796
            row=0, column=0, columnspan=1, sticky=tk.NW
797
        )
798
        widget.Entry(frame, textvariable=self.stringvar_link).grid(
799
            row=1, column=0, columnspan=2, sticky=tk.EW + tk.N
800
        )
801
        widget.Button(frame, text="+", command=self.link).grid(
802
            row=1, column=2, columnspan=1, sticky=tk.EW + tk.N
803
        )
804
        widget.Button(frame, text="-", command=self.unlink).grid(
805
            row=1, column=3, columnspan=1, sticky=tk.EW + tk.N
806
        )
807
        self.listbox_links = widget.Listbox(frame, width=width_uid)
808
        self.listbox_links.grid(
809
            row=2,
810
            column=0,
811
            rowspan=2,
812
            columnspan=4,
813
            padx=(3, 0),
814
            pady=(3, 0),
815
            sticky=tk.NSEW,
816
        )
817
818
        return frame
819
820
    def create_reference_widget(self, parent):
821
        frame = ttk.Frame(parent)
822
823
        frame.columnconfigure(0, weight=1)
824
        frame.rowconfigure(0, weight=1)
825
        frame.rowconfigure(1, weight=1)
826
827
        widget.Label(frame, text="External Reference:").grid(
828
            row=0, column=0, sticky=tk.W
829
        )
830
        widget.Entry(frame, textvariable=self.stringvar_ref).grid(
831
            row=1, column=0, sticky=tk.NSEW
832
        )
833
834
        return frame
835