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

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