Passed
Pull Request — develop (#399)
by
unknown
01:51
created

doorstop.gui.application.Application.link_ref()   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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